Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,20 @@ export function Prompt(props: PromptProps) {
}

command.register(() => [
{
title: "Add directory to context",
value: "context.addDir",
category: "Session",
slash: { name: "add-dir" },
onSelect: (dialog) => {
dialog.clear()
const newText = "/add-dir "
const cursor = input.logicalCursor
input.deleteRange(0, 0, cursor.row, cursor.col)
input.insertText(newText)
input.cursorOffset = Bun.stringWidth(newText)
},
},
{
title: "Stash prompt",
value: "prompt.stash",
Expand Down Expand Up @@ -543,6 +557,22 @@ export function Prompt(props: PromptProps) {
command: inputText,
})
setStore("mode", "normal")
} else if (inputText.startsWith("/add-dir ")) {
const dirPath = inputText.slice("/add-dir ".length).trim()
if (!dirPath) {
toast.show({ title: "Usage: /add-dir <path>", message: "Please provide a directory path", variant: "error" })
return
}
const result = await sdk.client.session.addDir({ path: dirPath, sessionID })
if (result.error) {
toast.error(result.error)
return
}
const resolved = result.data!.path
input.clear()
setStore("prompt", { input: "", parts: [] })
toast.show({ title: "Directory added", message: resolved, variant: "success" })
return
} else if (
inputText.startsWith("/") &&
iife(() => {
Expand Down
17 changes: 17 additions & 0 deletions packages/opencode/src/permission/next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,4 +266,21 @@ export namespace PermissionNext {
export async function list() {
return state().then((x) => Object.values(x.pending).map((x) => x.info))
}

export const grant = fn(
z.object({
permission: z.string(),
patterns: z.string().array(),
}),
async (input) => {
const s = await state()
for (const pattern of input.patterns) {
s.approved.push({
permission: input.permission,
pattern,
action: "allow",
})
}
},
)
}
78 changes: 78 additions & 0 deletions packages/opencode/src/server/routes/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Hono } from "hono"
import { stream } from "hono/streaming"
import { describeRoute, validator, resolver } from "hono-openapi"
import z from "zod"
import * as path from "node:path"
import * as fs from "node:fs/promises"
import { Session } from "../../session"
import { MessageV2 } from "../../session/message-v2"
import { SessionPrompt } from "../../session/prompt"
Expand All @@ -16,6 +18,9 @@ import { Log } from "../../util/log"
import { PermissionNext } from "@/permission/next"
import { errors } from "../error"
import { lazy } from "../../util/lazy"
import { Instance } from "../../project/instance"
import { Ripgrep } from "../../file/ripgrep"
import { IGNORE_PATTERNS } from "../../tool/ls"

const log = Log.create({ service: "server" })

Expand Down Expand Up @@ -931,5 +936,78 @@ export const SessionRoutes = lazy(() =>
})
return c.json(true)
},
)
.post(
"/add-dir",
describeRoute({
summary: "Add directory to context",
description:
"Add a directory to the conversation context. If the directory is outside the project, grants external_directory permission.",
operationId: "session.addDir",
responses: {
200: {
description: "Directory added successfully",
content: {
"application/json": {
schema: resolver(
z.object({
path: z.string(),
granted: z.boolean(),
}),
),
},
},
},
...errors(400),
},
}),
validator(
"json",
z.object({
path: z.string(),
sessionID: z.string(),
}),
),
async (c) => {
const body = c.req.valid("json")
const input = body.path

const expanded = input.startsWith("~/")
? path.join(process.env.HOME || process.env.USERPROFILE || "", input.slice(2))
: input.startsWith("~")
? path.join(process.env.HOME || process.env.USERPROFILE || "", input.slice(1))
: input

const absolute = path.isAbsolute(expanded) ? expanded : path.resolve(Instance.directory, expanded)

const info = await fs.stat(absolute).catch(() => null)
if (!info) return c.json({ error: "Directory does not exist" }, 400)
if (!info.isDirectory()) return c.json({ error: "Path is not a directory" }, 400)

const resolved = await fs.realpath(absolute)
const granted = !Instance.containsPath(resolved)
if (granted) {
await PermissionNext.grant({
permission: "external_directory",
patterns: [path.join(resolved, "*")],
})
}

const globs = IGNORE_PATTERNS.map((p) => `!${p}*`)
const files: string[] = []
for await (const file of Ripgrep.files({ cwd: resolved, glob: globs })) {
files.push(file)
if (files.length >= 100) break
}
const listing = files.sort().join("\n")

await SessionPrompt.prompt({
sessionID: body.sessionID,
noReply: true,
parts: [{ type: "text", text: `Added directory to context: ${resolved}\n\n${listing}` }],
})

return c.json({ path: resolved, granted })
},
),
)
39 changes: 39 additions & 0 deletions packages/sdk/js/src/v2/gen/sdk.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ import type {
QuestionReplyResponses,
SessionAbortErrors,
SessionAbortResponses,
SessionAddDirErrors,
SessionAddDirResponses,
SessionChildrenErrors,
SessionChildrenResponses,
SessionCommandErrors,
Expand Down Expand Up @@ -1654,6 +1656,43 @@ export class Session extends HeyApiClient {
...params,
})
}

/**
* Add directory to context
*
* Add a directory to the conversation context. If the directory is outside the project, grants external_directory permission.
*/
public addDir<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
path?: string
sessionID?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "body", key: "path" },
{ in: "body", key: "sessionID" },
],
},
],
)
return (options?.client ?? this.client).post<SessionAddDirResponses, SessionAddDirErrors, ThrowOnError>({
url: "/session/add-dir",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
}

export class Part extends HeyApiClient {
Expand Down
33 changes: 33 additions & 0 deletions packages/sdk/js/src/v2/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3710,6 +3710,39 @@ export type PermissionListResponses = {

export type PermissionListResponse = PermissionListResponses[keyof PermissionListResponses]

export type SessionAddDirData = {
body?: {
path: string
sessionID: string
}
path?: never
query?: {
directory?: string
}
url: "/session/add-dir"
}

export type SessionAddDirErrors = {
/**
* Bad request
*/
400: BadRequestError
}

export type SessionAddDirError = SessionAddDirErrors[keyof SessionAddDirErrors]

export type SessionAddDirResponses = {
/**
* Directory added successfully
*/
200: {
path: string
granted: boolean
}
}

export type SessionAddDirResponse = SessionAddDirResponses[keyof SessionAddDirResponses]

export type QuestionListData = {
body?: never
path?: never
Expand Down