Skip to content

feat(opencode): add GET/POST /tui/active-session endpoint#15765

Open
JSCOP wants to merge 2 commits intoanomalyco:devfrom
JSCOP:feat/tui-active-session-endpoint
Open

feat(opencode): add GET/POST /tui/active-session endpoint#15765
JSCOP wants to merge 2 commits intoanomalyco:devfrom
JSCOP:feat/tui-active-session-endpoint

Conversation

@JSCOP
Copy link

@JSCOP JSCOP commented Mar 2, 2026

Issue for this PR

Closes #15759

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

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-session could tell the TUI to navigate, but nothing exposed the current state back.

This adds GET /tui/active-session (returns Session.Info | null) and POST /tui/active-session (receives session reports from TUI).

Server side: a module-level variable in tui.ts stores the active sessionID. Updated on both select-session calls and TUI reports.

Client side: a createEffect in app.tsx watches route.data and POSTs the current sessionID whenever navigation happens — covers keybinds, dialogs, CLI flags, all paths.

Also exposed fetch from 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?

  • Built with bun run --cwd packages/opencode script/build.ts --single — success
  • LSP diagnostics clean on all 3 changed files
  • No unrelated changes to bun.lock or package.json

Screenshots / recordings

Not a UI change — API-only addition.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@github-actions github-actions bot added needs:compliance This means the issue will auto-close after 2 hours. and removed needs:compliance This means the issue will auto-close after 2 hours. labels Mar 2, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Mar 2, 2026

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@JSCOP JSCOP marked this pull request as ready for review March 2, 2026 22:35
Copilot AI review requested due to automatic review settings March 2, 2026 22:35
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-session and POST /tui/active-session, backed by an in-memory activeSessionID value 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 fetch function 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.

Comment on lines +11 to +12
let activeSessionID: string | undefined

Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 376 to 377
activeSessionID = sessionID
await Bus.publish(TuiEvent.SessionSelect, { sessionID })
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
activeSessionID = sessionID
await Bus.publish(TuiEvent.SessionSelect, { sessionID })
await Bus.publish(TuiEvent.SessionSelect, { sessionID })
activeSessionID = sessionID

Copilot uses AI. Check for mistakes.
})

return { client: sdk, event: emitter, url: props.url }
return { client: sdk, event: emitter, url: props.url, fetch: props.fetch ?? fetch }
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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 }

Copilot uses AI. Check for mistakes.
Comment on lines +267 to +270
sdk
.fetch(sdk.url + "/tui/active-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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" },

Copilot uses AI. Check for mistakes.
schema: resolver(z.boolean()),
},
},
},
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
},
},
...errors(400),

Copilot uses AI. Check for mistakes.
Comment on lines +420 to +424
},
}),
validator("json", z.object({ sessionID: z.string().nullable() })),
async (c) => {
activeSessionID = c.req.valid("json").sessionID ?? undefined
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
},
}),
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

Copilot uses AI. Check for mistakes.
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
@JSCOP JSCOP force-pushed the feat/tui-active-session-endpoint branch from 6557696 to e4218d9 Compare March 2, 2026 23:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: Add HTTP API endpoint to identify the active session on a TUI

2 participants