diff --git a/packages/opencode/src/session/compaction.ts b/packages/opencode/src/session/compaction.ts index ae69221288f8..b2c0fb607ec6 100644 --- a/packages/opencode/src/session/compaction.ts +++ b/packages/opencode/src/session/compaction.ts @@ -110,7 +110,7 @@ export namespace SessionCompaction { agent: "compaction", summary: true, path: { - cwd: Instance.directory, + cwd: Session.directory.get(), root: Instance.worktree, }, cost: 0, diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 3fcdab5238c3..f249b535588e 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -23,9 +23,22 @@ import type { Provider } from "@/provider/provider" import { PermissionNext } from "@/permission/next" import { Global } from "@/global" +// Current working directory for the active session. +let _sessionDirectory: string | undefined + export namespace Session { const log = Log.create({ service: "session" }) + /** Get/set the current session's working directory. Falls back to Instance.directory if not set. */ + export const directory = { + get() { + return _sessionDirectory ?? Instance.directory + }, + set(value: string | undefined) { + _sessionDirectory = value + }, + } + const parentTitlePrefix = "New session - " const childTitlePrefix = "Child session - " diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index f4793d1a7987..1c7558e0f626 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -267,6 +267,10 @@ export namespace SessionPrompt { let step = 0 const session = await Session.get(sessionID) + + // Set the working directory for this session (enables session-specific directories) + Session.directory.set(session.directory) + while (true) { SessionStatus.set(sessionID, { type: "busy" }) log.info("loop", { step, sessionID }) @@ -325,7 +329,7 @@ export namespace SessionPrompt { mode: task.agent, agent: task.agent, path: { - cwd: Instance.directory, + cwd: Session.directory.get(), root: Instance.worktree, }, cost: 0, @@ -525,7 +529,7 @@ export namespace SessionPrompt { mode: agent.name, agent: agent.name, path: { - cwd: Instance.directory, + cwd: Session.directory.get(), root: Instance.worktree, }, cost: 0, @@ -1350,6 +1354,10 @@ NOTE: At any point in time through this workflow you should feel free to ask the using _ = defer(() => cancel(input.sessionID)) const session = await Session.get(input.sessionID) + + // Set the working directory for this session (enables session-specific directories) + Session.directory.set(session.directory) + if (session.revert) { SessionRevert.cleanup(session) } @@ -1478,7 +1486,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the const args = matchingInvocation?.args const proc = spawn(shell, args, { - cwd: Instance.directory, + cwd: Session.directory.get(), detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"], env: { diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index fff90808864b..99ef4aba5715 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -4,6 +4,7 @@ import { Filesystem } from "../util/filesystem" import { Config } from "../config/config" import { Instance } from "../project/instance" +import { Session } from "./index" import path from "path" import os from "os" @@ -43,7 +44,7 @@ export namespace SystemPrompt { [ `Here is some useful information about the environment you are running in:`, ``, - ` Working directory: ${Instance.directory}`, + ` Working directory: ${Session.directory.get()}`, ` Is directory a git repo: ${project.vcs === "git" ? "yes" : "no"}`, ` Platform: ${process.platform}`, ` Today's date: ${new Date().toDateString()}`, @@ -52,7 +53,7 @@ export namespace SystemPrompt { ` ${ project.vcs === "git" && false ? await Ripgrep.tree({ - cwd: Instance.directory, + cwd: Session.directory.get(), limit: 200, }) : "" diff --git a/packages/opencode/src/tool/apply_patch.ts b/packages/opencode/src/tool/apply_patch.ts index d070eaefa978..f64f0dbbcda1 100644 --- a/packages/opencode/src/tool/apply_patch.ts +++ b/packages/opencode/src/tool/apply_patch.ts @@ -6,6 +6,7 @@ import { FileTime } from "../file/time" import { Bus } from "../bus" import { FileWatcher } from "../file/watcher" import { Instance } from "../project/instance" +import { Session } from "../session" import { Patch } from "../patch" import { createTwoFilesPatch, diffLines } from "diff" import { assertExternalDirectory } from "./external-directory" @@ -57,7 +58,7 @@ export const ApplyPatchTool = Tool.define("apply_patch", { let totalDiff = "" for (const hunk of hunks) { - const filePath = path.resolve(Instance.directory, hunk.path) + const filePath = path.resolve(Session.directory.get(), hunk.path) await assertExternalDirectory(ctx, filePath) switch (hunk.type) { @@ -117,7 +118,7 @@ export const ApplyPatchTool = Tool.define("apply_patch", { 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(Session.directory.get(), hunk.move_path) : undefined await assertExternalDirectory(ctx, movePath) fileChanges.push({ diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts index f3a1b04d4310..e49c2d00c41d 100644 --- a/packages/opencode/src/tool/bash.ts +++ b/packages/opencode/src/tool/bash.ts @@ -5,6 +5,7 @@ import path from "path" import DESCRIPTION from "./bash.txt" import { Log } from "../util/log" import { Instance } from "../project/instance" +import { Session } from "../session" import { lazy } from "@/util/lazy" import { Language } from "web-tree-sitter" @@ -56,7 +57,7 @@ export const BashTool = Tool.define("bash", async () => { log.info("bash tool using shell", { shell }) return { - description: DESCRIPTION.replaceAll("${directory}", Instance.directory) + description: DESCRIPTION.replaceAll("${directory}", Session.directory.get()) .replaceAll("${maxLines}", String(Truncate.MAX_LINES)) .replaceAll("${maxBytes}", String(Truncate.MAX_BYTES)), parameters: z.object({ @@ -65,7 +66,7 @@ export const BashTool = Tool.define("bash", async () => { workdir: z .string() .describe( - `The working directory to run the command in. Defaults to ${Instance.directory}. Use this instead of 'cd' commands.`, + `The working directory to run the command in. Defaults to ${Session.directory.get()}. Use this instead of 'cd' commands.`, ) .optional(), description: z @@ -75,7 +76,7 @@ export const BashTool = Tool.define("bash", async () => { ), }), async execute(params, ctx) { - const cwd = params.workdir || Instance.directory + const cwd = params.workdir || Session.directory.get() if (params.timeout !== undefined && params.timeout < 0) { throw new Error(`Invalid timeout value: ${params.timeout}. Timeout must be a positive number.`) } diff --git a/packages/opencode/src/tool/edit.ts b/packages/opencode/src/tool/edit.ts index 26db5b228362..7715b1a404b1 100644 --- a/packages/opencode/src/tool/edit.ts +++ b/packages/opencode/src/tool/edit.ts @@ -14,6 +14,7 @@ import { Bus } from "../bus" import { FileTime } from "../file/time" import { Filesystem } from "../util/filesystem" import { Instance } from "../project/instance" +import { Session } from "../session" import { Snapshot } from "@/snapshot" import { assertExternalDirectory } from "./external-directory" @@ -40,7 +41,7 @@ export const EditTool = Tool.define("edit", { throw new Error("oldString and newString must be different") } - const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath) + const filePath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Session.directory.get(), params.filePath) await assertExternalDirectory(ctx, filePath) let diff = "" diff --git a/packages/opencode/src/tool/glob.ts b/packages/opencode/src/tool/glob.ts index dda57f6ee1b9..b831f8eea151 100644 --- a/packages/opencode/src/tool/glob.ts +++ b/packages/opencode/src/tool/glob.ts @@ -4,6 +4,7 @@ import { Tool } from "./tool" import DESCRIPTION from "./glob.txt" import { Ripgrep } from "../file/ripgrep" import { Instance } from "../project/instance" +import { Session } from "../session" import { assertExternalDirectory } from "./external-directory" export const GlobTool = Tool.define("glob", { @@ -28,8 +29,8 @@ export const GlobTool = Tool.define("glob", { }, }) - let search = params.path ?? Instance.directory - search = path.isAbsolute(search) ? search : path.resolve(Instance.directory, search) + let search = params.path ?? Session.directory.get() + search = path.isAbsolute(search) ? search : path.resolve(Session.directory.get(), search) await assertExternalDirectory(ctx, search, { kind: "directory" }) const limit = 100 diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts index 097dedf4aafc..7ca7e3262e9a 100644 --- a/packages/opencode/src/tool/grep.ts +++ b/packages/opencode/src/tool/grep.ts @@ -4,6 +4,7 @@ import { Ripgrep } from "../file/ripgrep" import DESCRIPTION from "./grep.txt" import { Instance } from "../project/instance" +import { Session } from "../session" import path from "path" import { assertExternalDirectory } from "./external-directory" @@ -32,8 +33,8 @@ export const GrepTool = Tool.define("grep", { }, }) - let searchPath = params.path ?? Instance.directory - searchPath = path.isAbsolute(searchPath) ? searchPath : path.resolve(Instance.directory, searchPath) + let searchPath = params.path ?? Session.directory.get() + searchPath = path.isAbsolute(searchPath) ? searchPath : path.resolve(Session.directory.get(), searchPath) await assertExternalDirectory(ctx, searchPath, { kind: "directory" }) const rgPath = await Ripgrep.filepath() diff --git a/packages/opencode/src/tool/ls.ts b/packages/opencode/src/tool/ls.ts index cc3d750078f1..800e4126d737 100644 --- a/packages/opencode/src/tool/ls.ts +++ b/packages/opencode/src/tool/ls.ts @@ -3,6 +3,7 @@ import { Tool } from "./tool" import * as path from "path" import DESCRIPTION from "./ls.txt" import { Instance } from "../project/instance" +import { Session } from "../session" import { Ripgrep } from "../file/ripgrep" import { assertExternalDirectory } from "./external-directory" @@ -42,7 +43,7 @@ export const ListTool = Tool.define("list", { ignore: z.array(z.string()).describe("List of glob patterns to ignore").optional(), }), async execute(params, ctx) { - const searchPath = path.resolve(Instance.directory, params.path || ".") + const searchPath = path.resolve(Session.directory.get(), params.path || ".") await assertExternalDirectory(ctx, searchPath, { kind: "directory" }) await ctx.ask({ diff --git a/packages/opencode/src/tool/lsp.ts b/packages/opencode/src/tool/lsp.ts index ca352280b2a9..77eb2b385a87 100644 --- a/packages/opencode/src/tool/lsp.ts +++ b/packages/opencode/src/tool/lsp.ts @@ -4,6 +4,7 @@ import path from "path" import { LSP } from "../lsp" import DESCRIPTION from "./lsp.txt" import { Instance } from "../project/instance" +import { Session } from "../session" import { pathToFileURL } from "url" import { assertExternalDirectory } from "./external-directory" @@ -28,7 +29,7 @@ export const LspTool = Tool.define("lsp", { character: z.number().int().min(1).describe("The character offset (1-based, as shown in editors)"), }), execute: async (args, ctx) => { - const file = path.isAbsolute(args.filePath) ? args.filePath : path.join(Instance.directory, args.filePath) + const file = path.isAbsolute(args.filePath) ? args.filePath : path.join(Session.directory.get(), args.filePath) await assertExternalDirectory(ctx, file) await ctx.ask({ diff --git a/packages/opencode/src/tool/write.ts b/packages/opencode/src/tool/write.ts index cfcf6a0dab7e..3542fd6ba4b8 100644 --- a/packages/opencode/src/tool/write.ts +++ b/packages/opencode/src/tool/write.ts @@ -9,6 +9,7 @@ import { File } from "../file" import { FileTime } from "../file/time" import { Filesystem } from "../util/filesystem" import { Instance } from "../project/instance" +import { Session } from "../session" import { trimDiff } from "./edit" import { assertExternalDirectory } from "./external-directory" @@ -22,7 +23,7 @@ export const WriteTool = Tool.define("write", { filePath: z.string().describe("The absolute path to the file to write (must be absolute, not relative)"), }), async execute(params, ctx) { - const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Instance.directory, params.filePath) + const filepath = path.isAbsolute(params.filePath) ? params.filePath : path.join(Session.directory.get(), params.filePath) await assertExternalDirectory(ctx, filepath) const file = Bun.file(filepath) diff --git a/packages/opencode/test/session/directory.test.ts b/packages/opencode/test/session/directory.test.ts new file mode 100644 index 000000000000..daa3db1bfa27 --- /dev/null +++ b/packages/opencode/test/session/directory.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, test, beforeEach } from "bun:test" +import { Session } from "../../src/session" + +describe("Session.directory", () => { + beforeEach(() => { + // Reset to undefined before each test + Session.directory.set(undefined) + }) + + test("should return custom directory when set", () => { + const customDir = "/custom/worktree/path" + Session.directory.set(customDir) + expect(Session.directory.get()).toBe(customDir) + }) + + test("should update when set multiple times", () => { + Session.directory.set("/first/path") + expect(Session.directory.get()).toBe("/first/path") + + Session.directory.set("/second/path") + expect(Session.directory.get()).toBe("/second/path") + }) +})