diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 377ebb598be..c5a299b94b6 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -38,6 +38,7 @@ import { TuiStartupProvider, TuiTerminalEnvironmentProvider, useTuiApp, + useTuiPaths, useTuiStartup, type TuiApp, } from "./context/runtime" @@ -85,6 +86,7 @@ import { ArgsProvider, useArgs, type Args } from "./context/args" import open from "open" import { PromptRefProvider, usePromptRef } from "./context/prompt" import { Config, ConfigProvider, useConfig } from "./config" +import { newSessionLocation } from "./config/new-session-location" import { PluginProvider, usePlugin, type PackageResolver } from "./plugin/context" import { tuiPluginDirectories } from "./plugin/discovery" import { PluginRoute, Slot } from "./plugin/render" @@ -453,6 +455,7 @@ function App(props: { pair?: DialogPairCredentials }) { const log = useLog({ component: "app" }) const app = useTuiApp() const startup = useTuiStartup() + const paths = useTuiPaths() const config = useConfig() const devtools = createMemo(() => config.data.debug?.devtools ?? app.channel === "local") const route = useRoute() @@ -659,10 +662,13 @@ function App(props: { pair?: DialogPairCredentials }) { run: () => { route.navigate({ type: "home", - location: + location: newSessionLocation( + config.data.session.new_location, + paths.cwd, route.data.type === "session" ? (data.session.get(route.data.sessionID)?.location ?? location.ref) : undefined, + ), }) dialog.clear() }, diff --git a/packages/tui/src/component/dialog-config.tsx b/packages/tui/src/component/dialog-config.tsx index 47a45a770aa..9f84e810cd6 100644 --- a/packages/tui/src/component/dialog-config.tsx +++ b/packages/tui/src/component/dialog-config.tsx @@ -93,6 +93,15 @@ export const settings: Setting[] = [ labels: ["off", "on"], keywords: ["attachments", "images", "tool output"], }, + { + title: "New session location", + category: "Session", + path: ["session", "new_location"], + default: "launch", + values: ["launch", "inherit"], + labels: ["launch directory", "active session"], + keywords: ["directory", "cwd", "inherit"], + }, { title: "Enabled", category: "Tabs", diff --git a/packages/tui/src/config/index.tsx b/packages/tui/src/config/index.tsx index 2c9e90b3362..b0bfbab5b0d 100644 --- a/packages/tui/src/config/index.tsx +++ b/packages/tui/src/config/index.tsx @@ -137,6 +137,9 @@ export const Info = Schema.Struct({ markdown: Schema.optional(Schema.Literals(["source", "rendered"])).annotate({ description: "Show Markdown syntax markers or conceal them in rendered transcript content", }), + new_location: Schema.optional(Schema.Literals(["launch", "inherit"])).annotate({ + description: "Start new sessions in the TUI launch directory or inherit the active session location", + }), }), ).annotate({ description: "Session transcript presentation settings" }), tabs: Schema.optional( @@ -202,7 +205,7 @@ export const Info = Schema.Struct({ }) export type Info = Schema.Schema.Type -export type Resolved = Omit & { +export type Resolved = Omit & { attention: { enabled: boolean notifications: boolean @@ -218,6 +221,9 @@ export type Resolved = Omit, "new_location"> & { + new_location: "launch" | "inherit" + } tabs: { enabled: boolean scope: "global" | "cwd" @@ -259,6 +265,10 @@ export function resolve(input: Info, options: { terminalSuspend: boolean }): Res blinking: input.cursor.blinking ?? true, } : undefined, + session: { + ...input.session, + new_location: input.session?.new_location ?? "launch", + }, tabs: { ...input.tabs, enabled: input.tabs?.enabled ?? true, diff --git a/packages/tui/src/config/new-session-location.ts b/packages/tui/src/config/new-session-location.ts new file mode 100644 index 00000000000..7496aa4557f --- /dev/null +++ b/packages/tui/src/config/new-session-location.ts @@ -0,0 +1,10 @@ +import type { LocationRef } from "@opencode-ai/client/promise" + +export function newSessionLocation( + mode: "launch" | "inherit", + launchDirectory: string, + current?: LocationRef, +): LocationRef { + if (mode === "inherit" && current) return current + return { directory: launchDirectory } +} diff --git a/packages/tui/src/mini/runtime.ts b/packages/tui/src/mini/runtime.ts index b6053ee242e..fac00954e93 100644 --- a/packages/tui/src/mini/runtime.ts +++ b/packages/tui/src/mini/runtime.ts @@ -11,6 +11,7 @@ import { SessionMessage } from "@opencode-ai/schema/session-message" import type { LocationRef } from "@opencode-ai/client/promise" import type { Config } from "../config" +import { newSessionLocation } from "../config/new-session-location" import { loadRunAgents, loadRunCommands, loadRunReferences } from "./catalog.shared" import { resolveMiniSettings, @@ -48,6 +49,7 @@ type Reconnect = (signal: AbortSignal) => Promise type RunRuntimeInput = { host: MiniHost + directory: string boot: () => Promise resolveSession: (sdk: RunInput["sdk"], signal: AbortSignal) => Promise createSession?: CreateSession @@ -941,7 +943,11 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep const created = await createSession( state.sdk, { - location: state.location, + location: newSessionLocation( + (await tuiConfigTask).session.new_location, + input.directory, + state.location, + ), agent: state.agent, model: state.model, variant: state.activeVariant, @@ -1099,6 +1105,7 @@ export async function runInteractiveDeferredMode(input: RunDeferredInput, deps?: return runInteractiveRuntime( { host: input.host, + directory: input.directory, files: input.files, initialInput: input.initialInput, thinking: input.thinking, diff --git a/packages/tui/src/mini/types.ts b/packages/tui/src/mini/types.ts index 21feadf49d2..eb9def1b826 100644 --- a/packages/tui/src/mini/types.ts +++ b/packages/tui/src/mini/types.ts @@ -392,7 +392,7 @@ export type FormCancel = { location?: LocationRef } -export type RunTuiConfig = Pick +export type RunTuiConfig = Pick export type MiniSettings = { thinking: "show" | "hide" diff --git a/packages/tui/test/config-v2.test.tsx b/packages/tui/test/config-v2.test.tsx index 1e917f3a147..c774fb04843 100644 --- a/packages/tui/test/config-v2.test.tsx +++ b/packages/tui/test/config-v2.test.tsx @@ -25,6 +25,8 @@ test("validates the session tabs setting", () => { expect(() => decode({ tabs: { enabled: "on" } })).toThrow() expect(decode({ prompt: { image_preview: true } })).toEqual({ prompt: { image_preview: true } }) expect(decode({ session: { image_preview: true } })).toEqual({ session: { image_preview: true } }) + expect(decode({ session: { new_location: "inherit" } })).toEqual({ session: { new_location: "inherit" } }) + expect(() => decode({ session: { new_location: "current" } })).toThrow() }) test("resolves nested config and keybind defaults", () => { @@ -45,6 +47,7 @@ test("resolves nested config and keybind defaults", () => { expect(config.diffs).toEqual({ view: "split" }) expect(config.debug).toEqual({ devtools: true }) expect(config.tabs).toEqual({ enabled: true, scope: "cwd", layout: "horizontal" }) + expect(config.session.new_location).toBe("launch") }) test("shows resolved tab defaults in settings", () => { @@ -53,6 +56,10 @@ test("shows resolved tab defaults in settings", () => { expect(settings.find((setting) => setting.path.join(".") === "tabs.layout")?.default).toBe("horizontal") }) +test("shows the new session location default in settings", () => { + expect(settings.find((setting) => setting.path.join(".") === "session.new_location")?.default).toBe("launch") +}) + test("provides config and its host interface", async () => { const config = resolve({}, { terminalSuspend: true }) let current = {} diff --git a/packages/tui/test/new-session-location.test.ts b/packages/tui/test/new-session-location.test.ts new file mode 100644 index 00000000000..4a76fda69b7 --- /dev/null +++ b/packages/tui/test/new-session-location.test.ts @@ -0,0 +1,19 @@ +import { expect, test } from "bun:test" +import { newSessionLocation } from "../src/config/new-session-location" + +test("uses the launch directory by default", () => { + expect(newSessionLocation("launch", "/launch", { directory: "/session", workspaceID: "work-1" })).toEqual({ + directory: "/launch", + }) +}) + +test("inherits the active session location when configured", () => { + expect(newSessionLocation("inherit", "/launch", { directory: "/session", workspaceID: "work-1" })).toEqual({ + directory: "/session", + workspaceID: "work-1", + }) +}) + +test("falls back to the launch directory without an active session", () => { + expect(newSessionLocation("inherit", "/launch")).toEqual({ directory: "/launch" }) +})