diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 97c910a47d4b..6f53124dafb4 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -261,6 +261,18 @@ function App() { console.log(JSON.stringify(route.data)) }) + // Report active session to server whenever route changes + createEffect(() => { + const sessionID = route.data.type === "session" ? route.data.sessionID : null + sdk + .fetch(sdk.url + "/tui/active-session", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sessionID }), + }) + .catch(() => {}) + }) + // Update terminal window title based on current route and session createEffect(() => { if (!terminalTitleEnabled() || Flag.OPENCODE_DISABLE_TERMINAL_TITLE) return diff --git a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx index 7fa7e05c3d25..c54e5b444bd6 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx @@ -96,6 +96,19 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ if (timer) clearTimeout(timer) }) - return { client: sdk, event: emitter, url: props.url } + const raw = props.fetch ?? fetch + const wrapped = ((input: RequestInfo | URL, init?: RequestInit) => { + const merged = new Headers(props.headers as HeadersInit) + if (init?.headers) { + const h = new Headers(init.headers as HeadersInit) + h.forEach((v, k) => { + merged.set(k, v) + }) + } + if (props.directory && !merged.has("x-opencode-directory")) merged.set("x-opencode-directory", props.directory) + return raw(input as RequestInfo, { ...init, headers: merged }) + }) as typeof fetch + + return { client: sdk, event: emitter, url: props.url, fetch: wrapped } }, }) diff --git a/packages/opencode/src/server/routes/tui.ts b/packages/opencode/src/server/routes/tui.ts index 8650a0cccf76..afc3afb67701 100644 --- a/packages/opencode/src/server/routes/tui.ts +++ b/packages/opencode/src/server/routes/tui.ts @@ -8,6 +8,8 @@ import { AsyncQueue } from "../../util/queue" import { errors } from "../error" import { lazy } from "../../util/lazy" +let activeSessionID: string | undefined + const TuiRequest = z.object({ path: z.string(), body: z.any(), @@ -372,6 +374,57 @@ export const TuiRoutes = lazy(() => const { sessionID } = c.req.valid("json") await Session.get(sessionID) await Bus.publish(TuiEvent.SessionSelect, { sessionID }) + activeSessionID = sessionID + return c.json(true) + }, + ) + .get( + "/active-session", + describeRoute({ + summary: "Get active session", + description: "Get the session currently being viewed in the TUI.", + operationId: "tui.activeSession.get", + responses: { + 200: { + description: "Currently active session, or null if on home screen", + content: { + "application/json": { + schema: resolver(Session.Info.nullable()), + }, + }, + }, + ...errors(404), + }, + }), + async (c) => { + if (!activeSessionID) return c.json(null) + const session = await Session.get(activeSessionID) + return c.json(session) + }, + ) + .post( + "/active-session", + describeRoute({ + summary: "Report active session", + description: "Report which session the TUI is currently viewing.", + operationId: "tui.activeSession.set", + responses: { + 200: { + description: "Active session updated", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + ...errors(400, 404), + }, + }), + validator("json", z.object({ sessionID: z.string().nullable() })), + async (c) => { + const { sessionID } = c.req.valid("json") + if (sessionID !== null) await Session.get(sessionID) + activeSessionID = sessionID ?? undefined return c.json(true) }, )