From 668d77bb4e5955eb56a81b3db13ea1dd74400cc2 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Thu, 30 Apr 2026 21:01:06 -0400 Subject: [PATCH] refactor(tool): yield InstanceState context (#25199) --- packages/opencode/src/session/prompt.ts | 6 ++++-- packages/opencode/src/session/session.ts | 8 ++++---- packages/opencode/src/tool/apply_patch.ts | 20 +++++++++++--------- packages/opencode/src/tool/bash.ts | 21 ++++++++++++++------- packages/opencode/src/tool/edit.ts | 11 ++++++----- packages/opencode/src/tool/lsp.ts | 7 ++++--- packages/opencode/src/tool/plan.ts | 5 +++-- packages/opencode/src/tool/write.ts | 9 +++++---- 8 files changed, 51 insertions(+), 36 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 155b86b583..58edffe3f9 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -256,7 +256,8 @@ export const layer = Layer.effect( const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant") if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") { - const plan = Session.plan(input.session) + const ctx = yield* InstanceState.context + const plan = Session.plan(input.session, ctx) if (!(yield* fsys.existsSafe(plan))) return input.messages const part = yield* sessions.updatePart({ id: PartID.ascending(), @@ -272,7 +273,8 @@ export const layer = Layer.effect( if (input.agent.name !== "plan" || assistantMessage?.info.agent === "plan") return input.messages - const plan = Session.plan(input.session) + const ctx = yield* InstanceState.context + const plan = Session.plan(input.session, ctx) const exists = yield* fsys.existsSafe(plan) if (!exists) yield* fsys.ensureDir(path.dirname(plan)).pipe(Effect.catch(Effect.die)) const part = yield* sessions.updatePart({ diff --git a/packages/opencode/src/session/session.ts b/packages/opencode/src/session/session.ts index 5534976e39..7e6016b87f 100644 --- a/packages/opencode/src/session/session.ts +++ b/packages/opencode/src/session/session.ts @@ -26,7 +26,7 @@ import { ProjectTable } from "../project/project.sql" import { Storage } from "@/storage/storage" import * as Log from "@opencode-ai/core/util/log" import { MessageV2 } from "./message-v2" -import { Instance } from "../project/instance" +import { Instance, type InstanceContext } from "../project/instance" import { InstanceState } from "@/effect/instance-state" import { Snapshot } from "@/snapshot" import { ProjectID } from "../project/schema" @@ -311,9 +311,9 @@ export const Event = { ), } -export function plan(input: { slug: string; time: { created: number } }) { - const base = Instance.project.vcs - ? path.join(Instance.worktree, ".opencode", "plans") +export function plan(input: { slug: string; time: { created: number } }, instance: InstanceContext) { + const base = instance.project.vcs + ? path.join(instance.worktree, ".opencode", "plans") : path.join(Global.Path.data, "plans") return path.join(base, [input.time.created, input.slug].join("-") + ".md") } diff --git a/packages/opencode/src/tool/apply_patch.ts b/packages/opencode/src/tool/apply_patch.ts index 2de18ad081..916e11f1e3 100644 --- a/packages/opencode/src/tool/apply_patch.ts +++ b/packages/opencode/src/tool/apply_patch.ts @@ -3,7 +3,7 @@ import { Effect, Schema } from "effect" import * as Tool from "./tool" import { Bus } from "../bus" import { FileWatcher } from "../file/watcher" -import { Instance } from "../project/instance" +import { InstanceState } from "@/effect/instance-state" import { Patch } from "../patch" import { createTwoFilesPatch, diffLines } from "diff" import { assertExternalDirectoryEffect } from "./external-directory" @@ -52,6 +52,8 @@ export const ApplyPatchTool = Tool.define( return yield* Effect.fail(new Error("apply_patch verification failed: no hunks found")) } + const instance = yield* InstanceState.context + // Validate file paths and check permissions const fileChanges: Array<{ filePath: string @@ -68,7 +70,7 @@ export const ApplyPatchTool = Tool.define( let totalDiff = "" for (const hunk of hunks) { - const filePath = path.resolve(Instance.directory, hunk.path) + const filePath = path.resolve(instance.directory, hunk.path) yield* assertExternalDirectoryEffect(ctx, filePath) switch (hunk.type) { @@ -133,7 +135,7 @@ export const ApplyPatchTool = Tool.define( if (change.removed) deletions += change.count || 0 } - const movePath = hunk.move_path ? path.resolve(Instance.directory, hunk.move_path) : undefined + const movePath = hunk.move_path ? path.resolve(instance.directory, hunk.move_path) : undefined yield* assertExternalDirectoryEffect(ctx, movePath) fileChanges.push({ @@ -187,7 +189,7 @@ export const ApplyPatchTool = Tool.define( // Build per-file metadata for UI rendering (used for both permission and result) const files = fileChanges.map((change) => ({ filePath: change.filePath, - relativePath: path.relative(Instance.worktree, change.movePath ?? change.filePath).replaceAll("\\", "/"), + relativePath: path.relative(instance.worktree, change.movePath ?? change.filePath).replaceAll("\\", "/"), type: change.type, patch: change.diff, additions: change.additions, @@ -196,7 +198,7 @@ export const ApplyPatchTool = Tool.define( })) // Check permissions if needed - const relativePaths = fileChanges.map((c) => path.relative(Instance.worktree, c.filePath).replaceAll("\\", "/")) + const relativePaths = fileChanges.map((c) => path.relative(instance.worktree, c.filePath).replaceAll("\\", "/")) yield* ctx.ask({ permission: "edit", patterns: relativePaths, @@ -267,13 +269,13 @@ export const ApplyPatchTool = Tool.define( // Generate output summary const summaryLines = fileChanges.map((change) => { if (change.type === "add") { - return `A ${path.relative(Instance.worktree, change.filePath).replaceAll("\\", "/")}` + return `A ${path.relative(instance.worktree, change.filePath).replaceAll("\\", "/")}` } if (change.type === "delete") { - return `D ${path.relative(Instance.worktree, change.filePath).replaceAll("\\", "/")}` + return `D ${path.relative(instance.worktree, change.filePath).replaceAll("\\", "/")}` } const target = change.movePath ?? change.filePath - return `M ${path.relative(Instance.worktree, target).replaceAll("\\", "/")}` + return `M ${path.relative(instance.worktree, target).replaceAll("\\", "/")}` }) let output = `Success. Updated the following files:\n${summaryLines.join("\n")}` @@ -282,7 +284,7 @@ export const ApplyPatchTool = Tool.define( const target = change.movePath ?? change.filePath const block = LSP.Diagnostic.report(target, diagnostics[AppFileSystem.normalizePath(target)] ?? []) if (!block) continue - const rel = path.relative(Instance.worktree, target).replaceAll("\\", "/") + const rel = path.relative(instance.worktree, target).replaceAll("\\", "/") output += `\n\nLSP errors detected in ${rel}, please fix:\n${block}` } diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index c32c3963ba..c50b259f7a 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -6,7 +6,7 @@ import * as Tool from "./tool" import path from "path" import DESCRIPTION from "./bash.txt" import * as Log from "@opencode-ai/core/util/log" -import { Instance } from "../project/instance" +import { Instance, type InstanceContext } from "../project/instance" import { lazy } from "@/util/lazy" import { Language, type Node } from "web-tree-sitter" @@ -363,7 +363,13 @@ export const BashTool = Tool.define( return yield* resolvePath(next, cwd, shell) }) - const collect = Effect.fn("BashTool.collect")(function* (root: Node, cwd: string, ps: boolean, shell: string) { + const collect = Effect.fn("BashTool.collect")(function* ( + root: Node, + cwd: string, + ps: boolean, + shell: string, + instance: InstanceContext, + ) { const scan: Scan = { dirs: new Set(), patterns: new Set(), @@ -379,7 +385,7 @@ export const BashTool = Tool.define( for (const arg of pathArgs(command, ps)) { const resolved = yield* argPath(arg, cwd, ps, shell) log.info("resolved path", { arg, resolved }) - if (!resolved || Instance.containsPath(resolved)) continue + if (!resolved || Instance.containsPath(resolved, instance)) continue const dir = (yield* fs.isDir(resolved)) ? resolved : path.dirname(resolved) scan.dirs.add(dir) } @@ -589,9 +595,10 @@ export const BashTool = Tool.define( parameters: Parameters, execute: (params: Schema.Schema.Type, ctx: Tool.Context) => Effect.gen(function* () { + const executeInstance = yield* InstanceState.context const cwd = params.workdir - ? yield* resolvePath(params.workdir, Instance.directory, shell) - : Instance.directory + ? yield* resolvePath(params.workdir, executeInstance.directory, shell) + : executeInstance.directory if (params.timeout !== undefined && params.timeout < 0) { throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`) } @@ -602,8 +609,8 @@ export const BashTool = Tool.define( const tree = yield* Effect.acquireRelease(parse(params.command, ps), (tree) => Effect.sync(() => tree.delete()), ) - const scan = yield* collect(tree.rootNode, cwd, ps, shell) - if (!Instance.containsPath(cwd)) scan.dirs.add(cwd) + const scan = yield* collect(tree.rootNode, cwd, ps, shell, executeInstance) + if (!Instance.containsPath(cwd, executeInstance)) scan.dirs.add(cwd) yield* ask(ctx, scan) }), ) diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index 2463d48fae..ea3aac3480 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -13,7 +13,7 @@ import { File } from "../file" import { FileWatcher } from "../file/watcher" import { Bus } from "../bus" import { Format } from "../format" -import { Instance } from "../project/instance" +import { InstanceState } from "@/effect/instance-state" import { Snapshot } from "@/snapshot" import { assertExternalDirectoryEffect } from "./external-directory" import { AppFileSystem } from "@opencode-ai/core/filesystem" @@ -76,9 +76,10 @@ export const EditTool = Tool.define( throw new Error("No changes to apply: oldString and newString are identical.") } + const instance = yield* InstanceState.context const filePath = path.isAbsolute(params.filePath) ? params.filePath - : path.join(Instance.directory, params.filePath) + : path.join(instance.directory, params.filePath) yield* assertExternalDirectoryEffect(ctx, filePath) let diff = "" @@ -96,7 +97,7 @@ export const EditTool = Tool.define( diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew)) yield* ctx.ask({ permission: "edit", - patterns: [path.relative(Instance.worktree, filePath)], + patterns: [path.relative(instance.worktree, filePath)], always: ["*"], metadata: { filepath: filePath, @@ -139,7 +140,7 @@ export const EditTool = Tool.define( ) yield* ctx.ask({ permission: "edit", - patterns: [path.relative(Instance.worktree, filePath)], + patterns: [path.relative(instance.worktree, filePath)], always: ["*"], metadata: { filepath: filePath, @@ -201,7 +202,7 @@ export const EditTool = Tool.define( diff, filediff, }, - title: `${path.relative(Instance.worktree, filePath)}`, + title: `${path.relative(instance.worktree, filePath)}`, output, } }), diff --git a/packages/opencode/src/tool/lsp.ts b/packages/opencode/src/tool/lsp.ts index 3a555c2ce8..6f1532ca0c 100644 --- a/packages/opencode/src/tool/lsp.ts +++ b/packages/opencode/src/tool/lsp.ts @@ -3,7 +3,7 @@ import * as Tool from "./tool" import path from "path" import { LSP } from "@/lsp/lsp" import DESCRIPTION from "./lsp.txt" -import { Instance } from "../project/instance" +import { InstanceState } from "@/effect/instance-state" import { pathToFileURL } from "url" import { assertExternalDirectoryEffect } from "./external-directory" import { AppFileSystem } from "@opencode-ai/core/filesystem" @@ -44,7 +44,8 @@ export const LspTool = Tool.define( parameters: Parameters, execute: (args: Schema.Schema.Type, ctx: Tool.Context) => Effect.gen(function* () { - const file = path.isAbsolute(args.filePath) ? args.filePath : path.join(Instance.directory, args.filePath) + const instance = yield* InstanceState.context + const file = path.isAbsolute(args.filePath) ? args.filePath : path.join(instance.directory, args.filePath) yield* assertExternalDirectoryEffect(ctx, file) const meta = args.operation === "workspaceSymbol" @@ -61,7 +62,7 @@ export const LspTool = Tool.define( const uri = pathToFileURL(file).href const position = { file, line: args.line - 1, character: args.character - 1 } - const relPath = path.relative(Instance.worktree, file) + const relPath = path.relative(instance.worktree, file) const detail = args.operation === "workspaceSymbol" ? "" diff --git a/packages/opencode/src/tool/plan.ts b/packages/opencode/src/tool/plan.ts index dc49ef48ba..d5195376b3 100644 --- a/packages/opencode/src/tool/plan.ts +++ b/packages/opencode/src/tool/plan.ts @@ -5,7 +5,7 @@ import { Question } from "../question" import { Session } from "@/session/session" import { MessageV2 } from "../session/message-v2" import { Provider } from "@/provider/provider" -import { Instance } from "../project/instance" +import { InstanceState } from "@/effect/instance-state" import { type SessionID, MessageID, PartID } from "../session/schema" import EXIT_DESCRIPTION from "./plan-exit.txt" @@ -30,8 +30,9 @@ export const PlanExitTool = Tool.define( parameters: Parameters, execute: (_params: {}, ctx: Tool.Context) => Effect.gen(function* () { + const instance = yield* InstanceState.context const info = yield* session.get(ctx.sessionID) - const plan = path.relative(Instance.worktree, Session.plan(info)) + const plan = path.relative(instance.worktree, Session.plan(info, instance)) const answers = yield* question.ask({ sessionID: ctx.sessionID, questions: [ diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index 09c3a38055..c2be73ab1c 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -10,7 +10,7 @@ import { File } from "../file" import { FileWatcher } from "../file/watcher" import { Format } from "../format" import { AppFileSystem } from "@opencode-ai/core/filesystem" -import { Instance } from "../project/instance" +import { InstanceState } from "@/effect/instance-state" import { trimDiff } from "./edit" import { assertExternalDirectoryEffect } from "./external-directory" import * as Bom from "@/util/bom" @@ -37,9 +37,10 @@ export const WriteTool = Tool.define( parameters: Parameters, execute: (params: { content: string; filePath: string }, ctx: Tool.Context) => Effect.gen(function* () { + const instance = yield* InstanceState.context const filepath = path.isAbsolute(params.filePath) ? params.filePath - : path.join(Instance.directory, params.filePath) + : path.join(instance.directory, params.filePath) yield* assertExternalDirectoryEffect(ctx, filepath) const exists = yield* fs.existsSafe(filepath) @@ -52,7 +53,7 @@ export const WriteTool = Tool.define( const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, contentNew)) yield* ctx.ask({ permission: "edit", - patterns: [path.relative(Instance.worktree, filepath)], + patterns: [path.relative(instance.worktree, filepath)], always: ["*"], metadata: { filepath, @@ -89,7 +90,7 @@ export const WriteTool = Tool.define( } return { - title: path.relative(Instance.worktree, filepath), + title: path.relative(instance.worktree, filepath), metadata: { diagnostics, filepath,