summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorYeonGyu-Kim <[email protected]>2026-01-03 07:57:21 +0900
committerGitHub <[email protected]>2026-01-02 16:57:21 -0600
commita3f38e0533e1a89fd20e66bf8bf3cefed6652ec8 (patch)
treeaa92ce6b8aec36b9e9fb3000f82bb282e689dbd5
parent681a257df67d8e2f51bdee7d5cde24102a567276 (diff)
downloadopencode-a3f38e0533e1a89fd20e66bf8bf3cefed6652ec8.tar.gz
opencode-a3f38e0533e1a89fd20e66bf8bf3cefed6652ec8.zip
feat(plugin): add tui.session.select API endpoint for TUI navigation (#6565)
Co-authored-by: Aiden Cline <[email protected]>
-rw-r--r--packages/opencode/src/cli/cmd/tui/app.tsx7
-rw-r--r--packages/opencode/src/cli/cmd/tui/event.ts6
-rw-r--r--packages/opencode/src/server/server.ts27
-rw-r--r--packages/opencode/test/server/session-select.test.ts78
-rw-r--r--packages/sdk/js/src/v2/gen/sdk.gen.ts40
-rw-r--r--packages/sdk/js/src/v2/gen/types.gen.ts49
6 files changed, 205 insertions, 2 deletions
diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx
index b91df0540..8465898d7 100644
--- a/packages/opencode/src/cli/cmd/tui/app.tsx
+++ b/packages/opencode/src/cli/cmd/tui/app.tsx
@@ -549,6 +549,13 @@ function App() {
})
})
+ sdk.event.on(TuiEvent.SessionSelect.type, (evt) => {
+ route.navigate({
+ type: "session",
+ sessionID: evt.properties.sessionID,
+ })
+ })
+
sdk.event.on(SessionApi.Event.Deleted.type, (evt) => {
if (route.data.type === "session" && route.data.sessionID === evt.properties.info.id) {
route.navigate({ type: "home" })
diff --git a/packages/opencode/src/cli/cmd/tui/event.ts b/packages/opencode/src/cli/cmd/tui/event.ts
index acaa38e80..7c75523c1 100644
--- a/packages/opencode/src/cli/cmd/tui/event.ts
+++ b/packages/opencode/src/cli/cmd/tui/event.ts
@@ -37,4 +37,10 @@ export const TuiEvent = {
duration: z.number().default(5000).optional().describe("Duration in milliseconds"),
}),
),
+ SessionSelect: BusEvent.define(
+ "tui.session.select",
+ z.object({
+ sessionID: z.string().regex(/^ses/).describe("Session ID to navigate to"),
+ }),
+ ),
}
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index 4c6dac415..5940d3334 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -974,6 +974,7 @@ export namespace Server {
return c.json(true)
},
)
+
.post(
"/session/:sessionID/share",
describeRoute({
@@ -2600,6 +2601,32 @@ export namespace Server {
return c.json(true)
},
)
+ .post(
+ "/tui/select-session",
+ describeRoute({
+ summary: "Select session",
+ description: "Navigate the TUI to display the specified session.",
+ operationId: "tui.selectSession",
+ responses: {
+ 200: {
+ description: "Session selected successfully",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
+ },
+ },
+ },
+ ...errors(400, 404),
+ },
+ }),
+ validator("json", TuiEvent.SessionSelect.properties),
+ async (c) => {
+ const { sessionID } = c.req.valid("json")
+ await Session.get(sessionID)
+ await Bus.publish(TuiEvent.SessionSelect, { sessionID })
+ return c.json(true)
+ },
+ )
.route("/tui/control", TuiRoute)
.put(
"/auth/:providerID",
diff --git a/packages/opencode/test/server/session-select.test.ts b/packages/opencode/test/server/session-select.test.ts
new file mode 100644
index 000000000..479be4a17
--- /dev/null
+++ b/packages/opencode/test/server/session-select.test.ts
@@ -0,0 +1,78 @@
+import { describe, expect, test } from "bun:test"
+import path from "path"
+import { Session } from "../../src/session"
+import { Log } from "../../src/util/log"
+import { Instance } from "../../src/project/instance"
+import { Server } from "../../src/server/server"
+
+const projectRoot = path.join(__dirname, "../..")
+Log.init({ print: false })
+
+describe("tui.selectSession endpoint", () => {
+ test("should return 200 when called with valid session", async () => {
+ await Instance.provide({
+ directory: projectRoot,
+ fn: async () => {
+ // #given
+ const session = await Session.create({})
+
+ // #when
+ const app = Server.App()
+ const response = await app.request("/tui/select-session", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ sessionID: session.id }),
+ })
+
+ // #then
+ expect(response.status).toBe(200)
+ const body = await response.json()
+ expect(body).toBe(true)
+
+ await Session.remove(session.id)
+ },
+ })
+ })
+
+ test("should return 404 when session does not exist", async () => {
+ await Instance.provide({
+ directory: projectRoot,
+ fn: async () => {
+ // #given
+ const nonExistentSessionID = "ses_nonexistent123"
+
+ // #when
+ const app = Server.App()
+ const response = await app.request("/tui/select-session", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ sessionID: nonExistentSessionID }),
+ })
+
+ // #then
+ expect(response.status).toBe(404)
+ },
+ })
+ })
+
+ test("should return 400 when session ID format is invalid", async () => {
+ await Instance.provide({
+ directory: projectRoot,
+ fn: async () => {
+ // #given
+ const invalidSessionID = "invalid_session_id"
+
+ // #when
+ const app = Server.App()
+ const response = await app.request("/tui/select-session", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ sessionID: invalidSessionID }),
+ })
+
+ // #then
+ expect(response.status).toBe(400)
+ },
+ })
+ })
+})
diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts
index f56e83677..5319a714b 100644
--- a/packages/sdk/js/src/v2/gen/sdk.gen.ts
+++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts
@@ -19,6 +19,7 @@ import type {
EventSubscribeResponses,
EventTuiCommandExecute,
EventTuiPromptAppend,
+ EventTuiSessionSelect,
EventTuiToastShow,
FileListResponses,
FilePartInput,
@@ -144,6 +145,8 @@ import type {
TuiOpenThemesResponses,
TuiPublishErrors,
TuiPublishResponses,
+ TuiSelectSessionErrors,
+ TuiSelectSessionResponses,
TuiShowToastResponses,
TuiSubmitPromptResponses,
VcsGetResponses,
@@ -2688,7 +2691,7 @@ export class Tui extends HeyApiClient {
public publish<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
- body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow
+ body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect
},
options?: Options<never, ThrowOnError>,
) {
@@ -2705,6 +2708,41 @@ export class Tui extends HeyApiClient {
})
}
+ /**
+ * Select session
+ *
+ * Navigate the TUI to display the specified session.
+ */
+ public selectSession<ThrowOnError extends boolean = false>(
+ parameters?: {
+ directory?: string
+ sessionID?: string
+ },
+ options?: Options<never, ThrowOnError>,
+ ) {
+ const params = buildClientParams(
+ [parameters],
+ [
+ {
+ args: [
+ { in: "query", key: "directory" },
+ { in: "body", key: "sessionID" },
+ ],
+ },
+ ],
+ )
+ return (options?.client ?? this.client).post<TuiSelectSessionResponses, TuiSelectSessionErrors, ThrowOnError>({
+ url: "/tui/select-session",
+ ...options,
+ ...params,
+ headers: {
+ "Content-Type": "application/json",
+ ...options?.headers,
+ ...params.headers,
+ },
+ })
+ }
+
control = new Control({ client: this.client })
}
diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts
index 10764bebe..f0aa4a406 100644
--- a/packages/sdk/js/src/v2/gen/types.gen.ts
+++ b/packages/sdk/js/src/v2/gen/types.gen.ts
@@ -592,6 +592,16 @@ export type EventTuiToastShow = {
}
}
+export type EventTuiSessionSelect = {
+ type: "tui.session.select"
+ properties: {
+ /**
+ * Session ID to navigate to
+ */
+ sessionID: string
+ }
+}
+
export type EventMcpToolsChanged = {
type: "mcp.tools.changed"
properties: {
@@ -776,6 +786,7 @@ export type Event =
| EventTuiPromptAppend
| EventTuiCommandExecute
| EventTuiToastShow
+ | EventTuiSessionSelect
| EventMcpToolsChanged
| EventCommandExecuted
| EventSessionCreated
@@ -4310,7 +4321,7 @@ export type TuiShowToastResponses = {
export type TuiShowToastResponse = TuiShowToastResponses[keyof TuiShowToastResponses]
export type TuiPublishData = {
- body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow
+ body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect
path?: never
query?: {
directory?: string
@@ -4336,6 +4347,42 @@ export type TuiPublishResponses = {
export type TuiPublishResponse = TuiPublishResponses[keyof TuiPublishResponses]
+export type TuiSelectSessionData = {
+ body?: {
+ /**
+ * Session ID to navigate to
+ */
+ sessionID: string
+ }
+ path?: never
+ query?: {
+ directory?: string
+ }
+ url: "/tui/select-session"
+}
+
+export type TuiSelectSessionErrors = {
+ /**
+ * Bad request
+ */
+ 400: BadRequestError
+ /**
+ * Not found
+ */
+ 404: NotFoundError
+}
+
+export type TuiSelectSessionError = TuiSelectSessionErrors[keyof TuiSelectSessionErrors]
+
+export type TuiSelectSessionResponses = {
+ /**
+ * Session selected successfully
+ */
+ 200: boolean
+}
+
+export type TuiSelectSessionResponse = TuiSelectSessionResponses[keyof TuiSelectSessionResponses]
+
export type TuiControlNextData = {
body?: never
path?: never