refactor(tool): yield InstanceState context (#25199)

This commit is contained in:
Kit Langton
2026-04-30 21:01:06 -04:00
committed by GitHub
parent 5c2e06f353
commit 668d77bb4e
8 changed files with 51 additions and 36 deletions

View File

@@ -256,7 +256,8 @@ export const layer = Layer.effect(
const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant") const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant")
if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") { 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 if (!(yield* fsys.existsSafe(plan))) return input.messages
const part = yield* sessions.updatePart({ const part = yield* sessions.updatePart({
id: PartID.ascending(), id: PartID.ascending(),
@@ -272,7 +273,8 @@ export const layer = Layer.effect(
if (input.agent.name !== "plan" || assistantMessage?.info.agent === "plan") return input.messages 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) const exists = yield* fsys.existsSafe(plan)
if (!exists) yield* fsys.ensureDir(path.dirname(plan)).pipe(Effect.catch(Effect.die)) if (!exists) yield* fsys.ensureDir(path.dirname(plan)).pipe(Effect.catch(Effect.die))
const part = yield* sessions.updatePart({ const part = yield* sessions.updatePart({

View File

@@ -26,7 +26,7 @@ import { ProjectTable } from "../project/project.sql"
import { Storage } from "@/storage/storage" import { Storage } from "@/storage/storage"
import * as Log from "@opencode-ai/core/util/log" import * as Log from "@opencode-ai/core/util/log"
import { MessageV2 } from "./message-v2" import { MessageV2 } from "./message-v2"
import { Instance } from "../project/instance" import { Instance, type InstanceContext } from "../project/instance"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { Snapshot } from "@/snapshot" import { Snapshot } from "@/snapshot"
import { ProjectID } from "../project/schema" import { ProjectID } from "../project/schema"
@@ -311,9 +311,9 @@ export const Event = {
), ),
} }
export function plan(input: { slug: string; time: { created: number } }) { export function plan(input: { slug: string; time: { created: number } }, instance: InstanceContext) {
const base = Instance.project.vcs const base = instance.project.vcs
? path.join(Instance.worktree, ".opencode", "plans") ? path.join(instance.worktree, ".opencode", "plans")
: path.join(Global.Path.data, "plans") : path.join(Global.Path.data, "plans")
return path.join(base, [input.time.created, input.slug].join("-") + ".md") return path.join(base, [input.time.created, input.slug].join("-") + ".md")
} }

View File

@@ -3,7 +3,7 @@ import { Effect, Schema } from "effect"
import * as Tool from "./tool" import * as Tool from "./tool"
import { Bus } from "../bus" import { Bus } from "../bus"
import { FileWatcher } from "../file/watcher" import { FileWatcher } from "../file/watcher"
import { Instance } from "../project/instance" import { InstanceState } from "@/effect/instance-state"
import { Patch } from "../patch" import { Patch } from "../patch"
import { createTwoFilesPatch, diffLines } from "diff" import { createTwoFilesPatch, diffLines } from "diff"
import { assertExternalDirectoryEffect } from "./external-directory" 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")) return yield* Effect.fail(new Error("apply_patch verification failed: no hunks found"))
} }
const instance = yield* InstanceState.context
// Validate file paths and check permissions // Validate file paths and check permissions
const fileChanges: Array<{ const fileChanges: Array<{
filePath: string filePath: string
@@ -68,7 +70,7 @@ export const ApplyPatchTool = Tool.define(
let totalDiff = "" let totalDiff = ""
for (const hunk of hunks) { 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) yield* assertExternalDirectoryEffect(ctx, filePath)
switch (hunk.type) { switch (hunk.type) {
@@ -133,7 +135,7 @@ export const ApplyPatchTool = Tool.define(
if (change.removed) deletions += change.count || 0 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) yield* assertExternalDirectoryEffect(ctx, movePath)
fileChanges.push({ fileChanges.push({
@@ -187,7 +189,7 @@ export const ApplyPatchTool = Tool.define(
// Build per-file metadata for UI rendering (used for both permission and result) // Build per-file metadata for UI rendering (used for both permission and result)
const files = fileChanges.map((change) => ({ const files = fileChanges.map((change) => ({
filePath: change.filePath, 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, type: change.type,
patch: change.diff, patch: change.diff,
additions: change.additions, additions: change.additions,
@@ -196,7 +198,7 @@ export const ApplyPatchTool = Tool.define(
})) }))
// Check permissions if needed // 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({ yield* ctx.ask({
permission: "edit", permission: "edit",
patterns: relativePaths, patterns: relativePaths,
@@ -267,13 +269,13 @@ export const ApplyPatchTool = Tool.define(
// Generate output summary // Generate output summary
const summaryLines = fileChanges.map((change) => { const summaryLines = fileChanges.map((change) => {
if (change.type === "add") { 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") { 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 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")}` 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 target = change.movePath ?? change.filePath
const block = LSP.Diagnostic.report(target, diagnostics[AppFileSystem.normalizePath(target)] ?? []) const block = LSP.Diagnostic.report(target, diagnostics[AppFileSystem.normalizePath(target)] ?? [])
if (!block) continue 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}` output += `\n\nLSP errors detected in ${rel}, please fix:\n${block}`
} }

View File

@@ -6,7 +6,7 @@ import * as Tool from "./tool"
import path from "path" import path from "path"
import DESCRIPTION from "./bash.txt" import DESCRIPTION from "./bash.txt"
import * as Log from "@opencode-ai/core/util/log" 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 { lazy } from "@/util/lazy"
import { Language, type Node } from "web-tree-sitter" import { Language, type Node } from "web-tree-sitter"
@@ -363,7 +363,13 @@ export const BashTool = Tool.define(
return yield* resolvePath(next, cwd, shell) 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 = { const scan: Scan = {
dirs: new Set<string>(), dirs: new Set<string>(),
patterns: new Set<string>(), patterns: new Set<string>(),
@@ -379,7 +385,7 @@ export const BashTool = Tool.define(
for (const arg of pathArgs(command, ps)) { for (const arg of pathArgs(command, ps)) {
const resolved = yield* argPath(arg, cwd, ps, shell) const resolved = yield* argPath(arg, cwd, ps, shell)
log.info("resolved path", { arg, resolved }) 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) const dir = (yield* fs.isDir(resolved)) ? resolved : path.dirname(resolved)
scan.dirs.add(dir) scan.dirs.add(dir)
} }
@@ -589,9 +595,10 @@ export const BashTool = Tool.define(
parameters: Parameters, parameters: Parameters,
execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) => execute: (params: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
Effect.gen(function* () { Effect.gen(function* () {
const executeInstance = yield* InstanceState.context
const cwd = params.workdir const cwd = params.workdir
? yield* resolvePath(params.workdir, Instance.directory, shell) ? yield* resolvePath(params.workdir, executeInstance.directory, shell)
: Instance.directory : executeInstance.directory
if (params.timeout !== undefined && params.timeout < 0) { if (params.timeout !== undefined && params.timeout < 0) {
throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`) 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) => const tree = yield* Effect.acquireRelease(parse(params.command, ps), (tree) =>
Effect.sync(() => tree.delete()), Effect.sync(() => tree.delete()),
) )
const scan = yield* collect(tree.rootNode, cwd, ps, shell) const scan = yield* collect(tree.rootNode, cwd, ps, shell, executeInstance)
if (!Instance.containsPath(cwd)) scan.dirs.add(cwd) if (!Instance.containsPath(cwd, executeInstance)) scan.dirs.add(cwd)
yield* ask(ctx, scan) yield* ask(ctx, scan)
}), }),
) )

View File

@@ -13,7 +13,7 @@ import { File } from "../file"
import { FileWatcher } from "../file/watcher" import { FileWatcher } from "../file/watcher"
import { Bus } from "../bus" import { Bus } from "../bus"
import { Format } from "../format" import { Format } from "../format"
import { Instance } from "../project/instance" import { InstanceState } from "@/effect/instance-state"
import { Snapshot } from "@/snapshot" import { Snapshot } from "@/snapshot"
import { assertExternalDirectoryEffect } from "./external-directory" import { assertExternalDirectoryEffect } from "./external-directory"
import { AppFileSystem } from "@opencode-ai/core/filesystem" 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.") throw new Error("No changes to apply: oldString and newString are identical.")
} }
const instance = yield* InstanceState.context
const filePath = path.isAbsolute(params.filePath) const filePath = path.isAbsolute(params.filePath)
? params.filePath ? params.filePath
: path.join(Instance.directory, params.filePath) : path.join(instance.directory, params.filePath)
yield* assertExternalDirectoryEffect(ctx, filePath) yield* assertExternalDirectoryEffect(ctx, filePath)
let diff = "" let diff = ""
@@ -96,7 +97,7 @@ export const EditTool = Tool.define(
diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew)) diff = trimDiff(createTwoFilesPatch(filePath, filePath, contentOld, contentNew))
yield* ctx.ask({ yield* ctx.ask({
permission: "edit", permission: "edit",
patterns: [path.relative(Instance.worktree, filePath)], patterns: [path.relative(instance.worktree, filePath)],
always: ["*"], always: ["*"],
metadata: { metadata: {
filepath: filePath, filepath: filePath,
@@ -139,7 +140,7 @@ export const EditTool = Tool.define(
) )
yield* ctx.ask({ yield* ctx.ask({
permission: "edit", permission: "edit",
patterns: [path.relative(Instance.worktree, filePath)], patterns: [path.relative(instance.worktree, filePath)],
always: ["*"], always: ["*"],
metadata: { metadata: {
filepath: filePath, filepath: filePath,
@@ -201,7 +202,7 @@ export const EditTool = Tool.define(
diff, diff,
filediff, filediff,
}, },
title: `${path.relative(Instance.worktree, filePath)}`, title: `${path.relative(instance.worktree, filePath)}`,
output, output,
} }
}), }),

View File

@@ -3,7 +3,7 @@ import * as Tool from "./tool"
import path from "path" import path from "path"
import { LSP } from "@/lsp/lsp" import { LSP } from "@/lsp/lsp"
import DESCRIPTION from "./lsp.txt" import DESCRIPTION from "./lsp.txt"
import { Instance } from "../project/instance" import { InstanceState } from "@/effect/instance-state"
import { pathToFileURL } from "url" import { pathToFileURL } from "url"
import { assertExternalDirectoryEffect } from "./external-directory" import { assertExternalDirectoryEffect } from "./external-directory"
import { AppFileSystem } from "@opencode-ai/core/filesystem" import { AppFileSystem } from "@opencode-ai/core/filesystem"
@@ -44,7 +44,8 @@ export const LspTool = Tool.define(
parameters: Parameters, parameters: Parameters,
execute: (args: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) => execute: (args: Schema.Schema.Type<typeof Parameters>, ctx: Tool.Context) =>
Effect.gen(function* () { 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) yield* assertExternalDirectoryEffect(ctx, file)
const meta = const meta =
args.operation === "workspaceSymbol" args.operation === "workspaceSymbol"
@@ -61,7 +62,7 @@ export const LspTool = Tool.define(
const uri = pathToFileURL(file).href const uri = pathToFileURL(file).href
const position = { file, line: args.line - 1, character: args.character - 1 } 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 = const detail =
args.operation === "workspaceSymbol" args.operation === "workspaceSymbol"
? "" ? ""

View File

@@ -5,7 +5,7 @@ import { Question } from "../question"
import { Session } from "@/session/session" import { Session } from "@/session/session"
import { MessageV2 } from "../session/message-v2" import { MessageV2 } from "../session/message-v2"
import { Provider } from "@/provider/provider" 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 { type SessionID, MessageID, PartID } from "../session/schema"
import EXIT_DESCRIPTION from "./plan-exit.txt" import EXIT_DESCRIPTION from "./plan-exit.txt"
@@ -30,8 +30,9 @@ export const PlanExitTool = Tool.define(
parameters: Parameters, parameters: Parameters,
execute: (_params: {}, ctx: Tool.Context) => execute: (_params: {}, ctx: Tool.Context) =>
Effect.gen(function* () { Effect.gen(function* () {
const instance = yield* InstanceState.context
const info = yield* session.get(ctx.sessionID) 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({ const answers = yield* question.ask({
sessionID: ctx.sessionID, sessionID: ctx.sessionID,
questions: [ questions: [

View File

@@ -10,7 +10,7 @@ import { File } from "../file"
import { FileWatcher } from "../file/watcher" import { FileWatcher } from "../file/watcher"
import { Format } from "../format" import { Format } from "../format"
import { AppFileSystem } from "@opencode-ai/core/filesystem" import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Instance } from "../project/instance" import { InstanceState } from "@/effect/instance-state"
import { trimDiff } from "./edit" import { trimDiff } from "./edit"
import { assertExternalDirectoryEffect } from "./external-directory" import { assertExternalDirectoryEffect } from "./external-directory"
import * as Bom from "@/util/bom" import * as Bom from "@/util/bom"
@@ -37,9 +37,10 @@ export const WriteTool = Tool.define(
parameters: Parameters, parameters: Parameters,
execute: (params: { content: string; filePath: string }, ctx: Tool.Context) => execute: (params: { content: string; filePath: string }, ctx: Tool.Context) =>
Effect.gen(function* () { Effect.gen(function* () {
const instance = yield* InstanceState.context
const filepath = path.isAbsolute(params.filePath) const filepath = path.isAbsolute(params.filePath)
? params.filePath ? params.filePath
: path.join(Instance.directory, params.filePath) : path.join(instance.directory, params.filePath)
yield* assertExternalDirectoryEffect(ctx, filepath) yield* assertExternalDirectoryEffect(ctx, filepath)
const exists = yield* fs.existsSafe(filepath) const exists = yield* fs.existsSafe(filepath)
@@ -52,7 +53,7 @@ export const WriteTool = Tool.define(
const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, contentNew)) const diff = trimDiff(createTwoFilesPatch(filepath, filepath, contentOld, contentNew))
yield* ctx.ask({ yield* ctx.ask({
permission: "edit", permission: "edit",
patterns: [path.relative(Instance.worktree, filepath)], patterns: [path.relative(instance.worktree, filepath)],
always: ["*"], always: ["*"],
metadata: { metadata: {
filepath, filepath,
@@ -89,7 +90,7 @@ export const WriteTool = Tool.define(
} }
return { return {
title: path.relative(Instance.worktree, filepath), title: path.relative(instance.worktree, filepath),
metadata: { metadata: {
diagnostics, diagnostics,
filepath, filepath,