Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
14 changes: 12 additions & 2 deletions packages/app/src/context/permission.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,19 @@ export const { use: usePermission, provider: PermissionProvider } = createSimple
const globalSDK = useGlobalSDK()
const globalSync = useGlobalSync()

const directory = createMemo(() => {
if (!params.dir) return
try {
return base64Decode(params.dir)
} catch {
return
}
})

const permissionsEnabled = createMemo(() => {
if (!params.dir || !base64Decode(params.dir)) return false
const [store] = globalSync.child(base64Decode(params.dir))
const dir = directory()
if (!dir || !base64Decode(dir)) return false
const [store] = globalSync.child(base64Decode(dir))
return store.config.permission !== undefined
})

Expand Down
18 changes: 17 additions & 1 deletion packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,22 @@ export namespace Config {
result.plugin.push(...(await loadPlugin(dir)))
}

const claudeDirs = await Array.fromAsync(
Filesystem.up({
targets: [".claude"],
start: Instance.directory,
stop: Instance.worktree,
}),
)
const globalClaude = `${Global.Path.home}/.claude`
const globalClaudeExists = await fs.exists(globalClaude)
if (globalClaudeExists) claudeDirs.push(globalClaude)

const uniqueClaudeDirs = unique(claudeDirs)
for (const dir of uniqueClaudeDirs) {
result.command = mergeDeep(result.command ?? {}, await loadCommand(dir))
}

// Migrate deprecated mode field to agent field
for (const [name, mode] of Object.entries(result.mode)) {
result.agent = mergeDeep(result.agent ?? {}, {
Expand Down Expand Up @@ -201,7 +217,7 @@ export namespace Config {
if (!md.data) continue

const name = (() => {
const patterns = ["/.opencode/command/", "/command/"]
const patterns = ["/.opencode/command/", "/.claude/commands/", "/command/"]
const pattern = patterns.find((p) => item.includes(p))

if (pattern) {
Expand Down
13 changes: 9 additions & 4 deletions packages/opencode/src/project/instance.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Log } from "@/util/log"
import { ProcessCwd } from "@/util/process-cwd"
import { Context } from "../util/context"
import { Project } from "./project"
import { State } from "./state"
Expand All @@ -25,16 +26,20 @@ export const Instance = {
worktree: sandbox,
project,
}
await context.provide(ctx, async () => {
await input.init?.()
await ProcessCwd.run(ctx.worktree, async () => {
await context.provide(ctx, async () => {
await input.init?.()
})
})
return ctx
})
cache.set(input.directory, existing)
}
const ctx = await existing
return context.provide(ctx, async () => {
return input.fn()
return ProcessCwd.run(ctx.worktree, async () => {
return context.provide(ctx, async () => {
return input.fn()
})
})
},
get directory() {
Expand Down
21 changes: 21 additions & 0 deletions packages/opencode/src/project/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { $ } from "bun"
import { Storage } from "../storage/storage"
import { Log } from "../util/log"
import { Flag } from "@/flag/flag"
import { Global } from "@/global"
import { Session } from "../session"
import { work } from "../util/queue"
import { fn } from "@opencode-ai/util/fn"
Expand Down Expand Up @@ -109,6 +110,26 @@ export namespace Project {
}
}

const resolvedDirectory = path.resolve(directory)
const homeDirectory = path.resolve(Global.Path.home)
const rootDirectory = path.parse(resolvedDirectory).root

if (
Flag.OPENCODE_CLIENT === "desktop" &&
resolvedDirectory !== rootDirectory &&
resolvedDirectory !== homeDirectory
) {
const hasher = new Bun.CryptoHasher("sha1")
hasher.update(resolvedDirectory)
const id = `dir_${hasher.digest("hex")}`
return {
id,
worktree: resolvedDirectory,
sandbox: resolvedDirectory,
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
}
}

return {
id: "global",
worktree: "/",
Expand Down
42 changes: 32 additions & 10 deletions packages/opencode/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ export namespace Server {
return _url ?? new URL("http://localhost:4096")
}

function getDirectory(c: {
req: { query: (k: string) => string | undefined; header: (k: string) => string | undefined }
}) {
return c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
}

export const Event = {
Connected: BusEvent.define("server.connected", z.object({})),
Disposed: BusEvent.define("global.disposed", z.object({})),
Expand Down Expand Up @@ -246,7 +252,7 @@ export namespace Server {
},
)
.use(async (c, next) => {
const directory = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
const directory = getDirectory(c)
return Instance.provide({
directory,
init: InstanceBootstrap,
Expand Down Expand Up @@ -1410,12 +1416,20 @@ export namespace Server {
),
validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
async (c) => {
const directory = getDirectory(c)
const sessionID = c.req.valid("param").sessionID
const body = c.req.valid("json")

c.status(200)
c.header("Content-Type", "application/json")
return stream(c, async (stream) => {
const sessionID = c.req.valid("param").sessionID
const body = c.req.valid("json")
const msg = await SessionPrompt.prompt({ ...body, sessionID })
const msg = await Instance.provide({
directory,
init: InstanceBootstrap,
async fn() {
return SessionPrompt.prompt({ ...body, sessionID })
},
})
stream.write(JSON.stringify(msg))
})
},
Expand All @@ -1442,13 +1456,21 @@ export namespace Server {
),
validator("json", SessionPrompt.PromptInput.omit({ sessionID: true })),
async (c) => {
c.status(204)
c.header("Content-Type", "application/json")
return stream(c, async () => {
const sessionID = c.req.valid("param").sessionID
const body = c.req.valid("json")
SessionPrompt.prompt({ ...body, sessionID })
const directory = getDirectory(c)
const sessionID = c.req.valid("param").sessionID
const body = c.req.valid("json")

void Instance.provide({
directory,
init: InstanceBootstrap,
async fn() {
await SessionPrompt.prompt({ ...body, sessionID })
},
}).catch((error) => {
log.error("prompt_async failed", { error })
})

return c.body(null, 204)
},
)
.post(
Expand Down
164 changes: 89 additions & 75 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Provider } from "../provider/provider"
import { type Tool as AITool, tool, jsonSchema, type ToolCallOptions } from "ai"
import { SessionCompaction } from "./compaction"
import { Instance } from "../project/instance"
import { InstanceBootstrap } from "../project/bootstrap"
import { Bus } from "../bus"
import { ProviderTransform } from "../provider/transform"
import { SystemPrompt } from "./system"
Expand Down Expand Up @@ -640,6 +641,7 @@ export namespace SessionPrompt {
}) {
using _ = log.time("resolveTools")
const tools: Record<string, AITool> = {}
const directory = Instance.directory

const context = (args: any, options: ToolCallOptions): Tool.Context => ({
sessionID: input.session.id,
Expand Down Expand Up @@ -682,29 +684,35 @@ export namespace SessionPrompt {
description: item.description,
inputSchema: jsonSchema(schema as any),
async execute(args, options) {
const ctx = context(args, options)
await Plugin.trigger(
"tool.execute.before",
{
tool: item.id,
sessionID: ctx.sessionID,
callID: ctx.callID,
},
{
args,
},
)
const result = await item.execute(args, ctx)
await Plugin.trigger(
"tool.execute.after",
{
tool: item.id,
sessionID: ctx.sessionID,
callID: ctx.callID,
return Instance.provide({
directory,
init: InstanceBootstrap,
async fn() {
const ctx = context(args, options)
await Plugin.trigger(
"tool.execute.before",
{
tool: item.id,
sessionID: ctx.sessionID,
callID: ctx.callID,
},
{
args,
},
)
const result = await item.execute(args, ctx)
await Plugin.trigger(
"tool.execute.after",
{
tool: item.id,
sessionID: ctx.sessionID,
callID: ctx.callID,
},
result,
)
return result
},
result,
)
return result
})
},
toModelOutput(result) {
return {
Expand All @@ -721,65 +729,71 @@ export namespace SessionPrompt {

// Wrap execute to add plugin hooks and format output
item.execute = async (args, opts) => {
const ctx = context(args, opts)

await Plugin.trigger(
"tool.execute.before",
{
tool: key,
sessionID: ctx.sessionID,
callID: opts.toolCallId,
},
{
args,
},
)
return Instance.provide({
directory,
init: InstanceBootstrap,
async fn() {
const ctx = context(args, opts)

await Plugin.trigger(
"tool.execute.before",
{
tool: key,
sessionID: ctx.sessionID,
callID: opts.toolCallId,
},
{
args,
},
)

await ctx.ask({
permission: key,
metadata: {},
patterns: ["*"],
always: ["*"],
})
await ctx.ask({
permission: key,
metadata: {},
patterns: ["*"],
always: ["*"],
})

const result = await execute(args, opts)
const result = await execute(args, opts)

await Plugin.trigger(
"tool.execute.after",
{
tool: key,
sessionID: ctx.sessionID,
callID: opts.toolCallId,
},
result,
)
await Plugin.trigger(
"tool.execute.after",
{
tool: key,
sessionID: ctx.sessionID,
callID: opts.toolCallId,
},
result,
)

const textParts: string[] = []
const attachments: MessageV2.FilePart[] = []
const textParts: string[] = []
const attachments: MessageV2.FilePart[] = []

for (const contentItem of result.content) {
if (contentItem.type === "text") {
textParts.push(contentItem.text)
} else if (contentItem.type === "image") {
attachments.push({
id: Identifier.ascending("part"),
sessionID: input.session.id,
messageID: input.processor.message.id,
type: "file",
mime: contentItem.mimeType,
url: `data:${contentItem.mimeType};base64,${contentItem.data}`,
})
}
// Add support for other types if needed
}
for (const contentItem of result.content) {
if (contentItem.type === "text") {
textParts.push(contentItem.text)
} else if (contentItem.type === "image") {
attachments.push({
id: Identifier.ascending("part"),
sessionID: input.session.id,
messageID: input.processor.message.id,
type: "file",
mime: contentItem.mimeType,
url: `data:${contentItem.mimeType};base64,${contentItem.data}`,
})
}
// Add support for other types if needed
}

return {
title: "",
metadata: result.metadata ?? {},
output: textParts.join("\n\n"),
attachments,
content: result.content, // directly return content to preserve ordering when outputting to model
}
return {
title: "",
metadata: result.metadata ?? {},
output: textParts.join("\n\n"),
attachments,
content: result.content, // directly return content to preserve ordering when outputting to model
}
},
})
}
item.toModelOutput = (result) => {
return {
Expand Down
Loading