feat(opencode): add GET/POST /tui/active-session endpoint#15765
feat(opencode): add GET/POST /tui/active-session endpoint#15765JSCOP wants to merge 2 commits intoanomalyco:devfrom
Conversation
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
There was a problem hiding this comment.
Pull request overview
Adds a way for external tools (and the server itself) to query which session the TUI is currently viewing by introducing /tui/active-session endpoints, and wires the TUI to report navigation changes back to the server.
Changes:
- Server: add
GET /tui/active-sessionandPOST /tui/active-session, backed by an in-memoryactiveSessionIDvalue updated by both server-driven navigation and TUI reports. - TUI: on route changes, POST the current session ID (or
null) to/tui/active-session. - TUI SDK context: expose a
fetchfunction on the SDK context for making non-SDK HTTP/RPC calls.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| packages/opencode/src/server/routes/tui.ts | Adds the new active-session endpoints and updates active session state on server-driven session selection. |
| packages/opencode/src/cli/cmd/tui/context/sdk.tsx | Exposes fetch via SDK context to enable direct calls outside the generated SDK client. |
| packages/opencode/src/cli/cmd/tui/app.tsx | Reports route-derived active session changes back to the server via POST. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let activeSessionID: string | undefined | ||
|
|
There was a problem hiding this comment.
activeSessionID is module-level state, but requests are scoped to an Instance via the x-opencode-directory/directory middleware. This means different directories/workspaces handled by the same server process will overwrite each other’s “active session”. If instance scoping matters here, store this value in Instance.state(...) (keyed by Instance.directory) instead of a module global.
| activeSessionID = sessionID | ||
| await Bus.publish(TuiEvent.SessionSelect, { sessionID }) |
There was a problem hiding this comment.
activeSessionID is updated before Bus.publish(...). If a subscriber throws during publish, the route will error but activeSessionID will still be mutated, leaving server state out of sync with the TUI. To keep state consistent, set activeSessionID only after a successful publish (or wrap publish to ensure failures don’t partially apply state).
| activeSessionID = sessionID | |
| await Bus.publish(TuiEvent.SessionSelect, { sessionID }) | |
| await Bus.publish(TuiEvent.SessionSelect, { sessionID }) | |
| activeSessionID = sessionID |
| }) | ||
|
|
||
| return { client: sdk, event: emitter, url: props.url } | ||
| return { client: sdk, event: emitter, url: props.url, fetch: props.fetch ?? fetch } |
There was a problem hiding this comment.
fetch is exposed as props.fetch ?? fetch, but that bypasses the SDK’s configured default headers (e.g. Authorization for OPENCODE_SERVER_PASSWORD) and instance scoping (x-opencode-directory / workspace). Any caller using sdk.fetch will send unauthenticated/unscoped requests, which will fail against password-protected servers and may target the wrong instance. Consider returning a small wrapper around the underlying fetch that merges props.headers and injects the directory/workspace headers (or provides a helper that reuses the SDK client’s configured request pipeline).
| return { client: sdk, event: emitter, url: props.url, fetch: props.fetch ?? fetch } | |
| const sdkFetch: typeof fetch = async (input, init) => { | |
| const underlyingFetch = props.fetch ?? fetch | |
| // Start with SDK-level/default headers if provided | |
| const mergedHeaders = new Headers(props.headers ?? {}) | |
| // Merge any per-request headers from the caller, overriding defaults | |
| if (init?.headers) { | |
| const requestHeaders = new Headers(init.headers as HeadersInit) | |
| requestHeaders.forEach((value, key) => { | |
| mergedHeaders.set(key, value) | |
| }) | |
| } | |
| // Ensure directory scoping header is present if a directory is configured | |
| if (props.directory && !mergedHeaders.has("x-opencode-directory")) { | |
| mergedHeaders.set("x-opencode-directory", props.directory) | |
| } | |
| const nextInit: RequestInit = { | |
| ...init, | |
| headers: mergedHeaders, | |
| } | |
| return underlyingFetch(input as RequestInfo, nextInit) | |
| } | |
| return { client: sdk, event: emitter, url: props.url, fetch: sdkFetch } |
| sdk | ||
| .fetch(sdk.url + "/tui/active-session", { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, |
There was a problem hiding this comment.
This POST uses sdk.fetch with only a Content-Type header. In HTTP attach mode (and when OPENCODE_SERVER_PASSWORD is set), this request won’t include the Authorization header or x-opencode-directory/workspace headers that the SDK client sends, so reporting will silently fail or be associated with the wrong instance. Please ensure this call uses the same default headers/scoping as other SDK requests (e.g. via a wrapped sdk.fetch, or by explicitly merging the SDK provider headers here).
| sdk | |
| .fetch(sdk.url + "/tui/active-session", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| const defaultHeaders = (sdk as any).headers ?? {} | |
| sdk | |
| .fetch(sdk.url + "/tui/active-session", { | |
| method: "POST", | |
| headers: { ...defaultHeaders, "Content-Type": "application/json" }, |
| schema: resolver(z.boolean()), | ||
| }, | ||
| }, | ||
| }, |
There was a problem hiding this comment.
The OpenAPI responses for this route don’t include validation errors, but validator("json", ...) will return a 400 on invalid input. Add ...errors(400) here so the spec matches runtime behavior (and aligns with other routes in this file that use validator).
| }, | |
| }, | |
| ...errors(400), |
| }, | ||
| }), | ||
| validator("json", z.object({ sessionID: z.string().nullable() })), | ||
| async (c) => { | ||
| activeSessionID = c.req.valid("json").sessionID ?? undefined |
There was a problem hiding this comment.
POST /tui/active-session stores sessionID without verifying the session exists (or even matches the expected session ID format). If a client reports a stale/typo’d ID, GET /tui/active-session will start returning 404s until another update arrives. Consider validating non-null sessionID (e.g. await Session.get(sessionID) and/or Identifier/session regex) and documenting/returning 404 on unknown sessions.
| }, | |
| }), | |
| validator("json", z.object({ sessionID: z.string().nullable() })), | |
| async (c) => { | |
| activeSessionID = c.req.valid("json").sessionID ?? undefined | |
| ...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 |
Track which session the TUI is currently viewing and expose it via HTTP API. The TUI reports route changes to the server, and external clients can query the active session. Fixes anomalyco#15759
6557696 to
e4218d9
Compare
Issue for this PR
Closes #15759
Type of change
What does this PR do?
There was no way to query which session the TUI is currently viewing. The server had no awareness of the TUI's route state —
POST /tui/select-sessioncould tell the TUI to navigate, but nothing exposed the current state back.This adds
GET /tui/active-session(returnsSession.Info | null) andPOST /tui/active-session(receives session reports from TUI).Server side: a module-level variable in
tui.tsstores the active sessionID. Updated on bothselect-sessioncalls and TUI reports.Client side: a
createEffectinapp.tsxwatchesroute.dataand POSTs the current sessionID whenever navigation happens — covers keybinds, dialogs, CLI flags, all paths.Also exposed
fetchfrom the SDK context so the TUI can communicate back to the server in both HTTP and RPC modes.How did you verify your code works?
bun run --cwd packages/opencode script/build.ts --single— successbun.lockorpackage.jsonScreenshots / recordings
Not a UI change — API-only addition.
Checklist