diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 8465898d701..006d04f1cb3 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -96,7 +96,7 @@ async function getTerminalBackgroundColor(): Promise<"dark" | "light"> { }) } -export function tui(input: { url: string; args: Args; onExit?: () => Promise }) { +export function tui(input: { url: string; args: Args; directory?: string; onExit?: () => Promise }) { // promise to prevent immediate exit return new Promise(async (resolve) => { const mode = await getTerminalBackgroundColor() @@ -116,7 +116,7 @@ export function tui(input: { url: string; args: Args; onExit?: () => Promise - + diff --git a/packages/opencode/src/cli/cmd/tui/attach.ts b/packages/opencode/src/cli/cmd/tui/attach.ts index 5d1a4ded206..ee98c181e18 100644 --- a/packages/opencode/src/cli/cmd/tui/attach.ts +++ b/packages/opencode/src/cli/cmd/tui/attach.ts @@ -21,10 +21,11 @@ export const AttachCommand = cmd({ describe: "session id to continue", }), handler: async (args) => { - if (args.dir) process.chdir(args.dir) + const directory = args.dir ? args.dir : process.cwd() await tui({ url: args.url, args: { sessionID: args.session }, + directory, }) }, }) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 2e68fdcd924..3b2fc7d3b2a 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -75,6 +75,7 @@ export function Autocomplete(props: { const command = useCommandDialog() const { theme } = useTheme() const dimensions = useTerminalDimensions() + const baseDirectory = () => sync.data.path.directory || process.cwd() const [store, setStore] = createStore({ index: 0, @@ -188,7 +189,7 @@ export function Autocomplete(props: { const width = props.anchor().width - 4 options.push( ...result.data.map((item): AutocompleteOption => { - let url = `file://${process.cwd()}/${item}` + let url = `file://${baseDirectory()}/${item}` let filename = item if (lineRange && !item.endsWith("/")) { filename = `${item}#${lineRange.startLine}${lineRange.endLine ? `-${lineRange.endLine}` : ""}` diff --git a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx index 3ea7c90b700..4bcabaeaf54 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx @@ -5,11 +5,12 @@ import { batch, onCleanup, onMount } from "solid-js" export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ name: "SDK", - init: (props: { url: string }) => { + init: (props: { url: string; directory?: string }) => { const abort = new AbortController() const sdk = createOpencodeClient({ baseUrl: props.url, signal: abort.signal, + directory: props.directory, }) const emitter = createGlobalEmitter<{ diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 26701f95374..5dd93a482f6 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -292,7 +292,8 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ }) createEffect(() => { - getCustomThemes() + const directory = sync.data.path.directory || process.cwd() + getCustomThemes(directory) .then((custom) => { setStore( produce((draft) => { @@ -307,7 +308,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ if (store.active !== "system") { setStore("ready", true) } - }) + }) }) const renderer = useRenderer() @@ -378,13 +379,13 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ }) const CUSTOM_THEME_GLOB = new Bun.Glob("themes/*.json") -async function getCustomThemes() { +async function getCustomThemes(startDir?: string) { const directories = [ Global.Path.config, ...(await Array.fromAsync( Filesystem.up({ targets: [".opencode"], - start: process.cwd(), + start: startDir || process.cwd(), }), )), ] diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index d049ec4373c..48236e94922 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -66,8 +66,9 @@ import stripAnsi from "strip-ansi" import { Footer } from "./footer.tsx" import { usePromptRef } from "../../context/prompt" import { Filesystem } from "@/util/filesystem" -import { PermissionPrompt } from "./permission" import { DialogExportOptions } from "../../ui/dialog-export-options" +import { normalizePathFromDirectory } from "@tui/util/paths" +import { PermissionPrompt } from "./permission" import { formatTranscript } from "../../util/transcript" addDefaultParsers(parsers.parsers) @@ -770,7 +771,7 @@ export function Session() { // Just open in editor without saving await Editor.open({ value: transcript, renderer }) } else { - const exportDir = process.cwd() + const exportDir = sync.data.path.directory || process.cwd() const filename = options.filename.trim() const filepath = path.join(exportDir, filename) @@ -1777,11 +1778,8 @@ function TodoWrite(props: ToolProps) { } function normalizePath(input?: string) { - if (!input) return "" - if (path.isAbsolute(input)) { - return path.relative(process.cwd(), input) || "." - } - return input + const directory = use().sync.data.path.directory || process.cwd() + return normalizePathFromDirectory(input, directory) } function input(input: Record, omit?: string[]): string { diff --git a/packages/opencode/src/cli/cmd/tui/util/paths.ts b/packages/opencode/src/cli/cmd/tui/util/paths.ts new file mode 100644 index 00000000000..5a976755134 --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/util/paths.ts @@ -0,0 +1,9 @@ +import path from "path" + +export function normalizePathFromDirectory(input: string | undefined, directory: string) { + if (!input) return "" + if (path.isAbsolute(input)) { + return path.relative(directory, input) || "." + } + return input +} diff --git a/packages/opencode/test/tui/paths.test.ts b/packages/opencode/test/tui/paths.test.ts new file mode 100644 index 00000000000..dddb2d31526 --- /dev/null +++ b/packages/opencode/test/tui/paths.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, test } from "bun:test" +import path from "path" +import { normalizePathFromDirectory } from "../../src/cli/cmd/tui/util/paths" + +describe("normalizePathFromDirectory", () => { + test("returns relative path for absolute input", () => { + const directory = "/tmp/opencode-test" + const input = path.join(directory, "src", "index.ts") + expect(normalizePathFromDirectory(input, directory)).toBe(path.join("src", "index.ts")) + }) + + test("keeps relative input unchanged", () => { + const directory = "/tmp/opencode-test" + const input = "src/index.ts" + expect(normalizePathFromDirectory(input, directory)).toBe(input) + }) + + test("returns dot when input equals base directory", () => { + const directory = "/tmp/opencode-test" + expect(normalizePathFromDirectory(directory, directory)).toBe(".") + }) + + test("returns empty string for empty input", () => { + const directory = "/tmp/opencode-test" + expect(normalizePathFromDirectory("", directory)).toBe("") + }) +})