Skip to content

Commit a3f38e0

Browse files
feat(plugin): add tui.session.select API endpoint for TUI navigation (#6565)
Co-authored-by: Aiden Cline <[email protected]>
1 parent 681a257 commit a3f38e0

File tree

6 files changed

+205
-2
lines changed

6 files changed

+205
-2
lines changed

packages/opencode/src/cli/cmd/tui/app.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,13 @@ function App() {
549549
})
550550
})
551551

552+
sdk.event.on(TuiEvent.SessionSelect.type, (evt) => {
553+
route.navigate({
554+
type: "session",
555+
sessionID: evt.properties.sessionID,
556+
})
557+
})
558+
552559
sdk.event.on(SessionApi.Event.Deleted.type, (evt) => {
553560
if (route.data.type === "session" && route.data.sessionID === evt.properties.info.id) {
554561
route.navigate({ type: "home" })

packages/opencode/src/cli/cmd/tui/event.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,10 @@ export const TuiEvent = {
3737
duration: z.number().default(5000).optional().describe("Duration in milliseconds"),
3838
}),
3939
),
40+
SessionSelect: BusEvent.define(
41+
"tui.session.select",
42+
z.object({
43+
sessionID: z.string().regex(/^ses/).describe("Session ID to navigate to"),
44+
}),
45+
),
4046
}

packages/opencode/src/server/server.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,7 @@ export namespace Server {
974974
return c.json(true)
975975
},
976976
)
977+
977978
.post(
978979
"/session/:sessionID/share",
979980
describeRoute({
@@ -2600,6 +2601,32 @@ export namespace Server {
26002601
return c.json(true)
26012602
},
26022603
)
2604+
.post(
2605+
"/tui/select-session",
2606+
describeRoute({
2607+
summary: "Select session",
2608+
description: "Navigate the TUI to display the specified session.",
2609+
operationId: "tui.selectSession",
2610+
responses: {
2611+
200: {
2612+
description: "Session selected successfully",
2613+
content: {
2614+
"application/json": {
2615+
schema: resolver(z.boolean()),
2616+
},
2617+
},
2618+
},
2619+
...errors(400, 404),
2620+
},
2621+
}),
2622+
validator("json", TuiEvent.SessionSelect.properties),
2623+
async (c) => {
2624+
const { sessionID } = c.req.valid("json")
2625+
await Session.get(sessionID)
2626+
await Bus.publish(TuiEvent.SessionSelect, { sessionID })
2627+
return c.json(true)
2628+
},
2629+
)
26032630
.route("/tui/control", TuiRoute)
26042631
.put(
26052632
"/auth/:providerID",
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { describe, expect, test } from "bun:test"
2+
import path from "path"
3+
import { Session } from "../../src/session"
4+
import { Log } from "../../src/util/log"
5+
import { Instance } from "../../src/project/instance"
6+
import { Server } from "../../src/server/server"
7+
8+
const projectRoot = path.join(__dirname, "../..")
9+
Log.init({ print: false })
10+
11+
describe("tui.selectSession endpoint", () => {
12+
test("should return 200 when called with valid session", async () => {
13+
await Instance.provide({
14+
directory: projectRoot,
15+
fn: async () => {
16+
// #given
17+
const session = await Session.create({})
18+
19+
// #when
20+
const app = Server.App()
21+
const response = await app.request("/tui/select-session", {
22+
method: "POST",
23+
headers: { "Content-Type": "application/json" },
24+
body: JSON.stringify({ sessionID: session.id }),
25+
})
26+
27+
// #then
28+
expect(response.status).toBe(200)
29+
const body = await response.json()
30+
expect(body).toBe(true)
31+
32+
await Session.remove(session.id)
33+
},
34+
})
35+
})
36+
37+
test("should return 404 when session does not exist", async () => {
38+
await Instance.provide({
39+
directory: projectRoot,
40+
fn: async () => {
41+
// #given
42+
const nonExistentSessionID = "ses_nonexistent123"
43+
44+
// #when
45+
const app = Server.App()
46+
const response = await app.request("/tui/select-session", {
47+
method: "POST",
48+
headers: { "Content-Type": "application/json" },
49+
body: JSON.stringify({ sessionID: nonExistentSessionID }),
50+
})
51+
52+
// #then
53+
expect(response.status).toBe(404)
54+
},
55+
})
56+
})
57+
58+
test("should return 400 when session ID format is invalid", async () => {
59+
await Instance.provide({
60+
directory: projectRoot,
61+
fn: async () => {
62+
// #given
63+
const invalidSessionID = "invalid_session_id"
64+
65+
// #when
66+
const app = Server.App()
67+
const response = await app.request("/tui/select-session", {
68+
method: "POST",
69+
headers: { "Content-Type": "application/json" },
70+
body: JSON.stringify({ sessionID: invalidSessionID }),
71+
})
72+
73+
// #then
74+
expect(response.status).toBe(400)
75+
},
76+
})
77+
})
78+
})

packages/sdk/js/src/v2/gen/sdk.gen.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type {
1919
EventSubscribeResponses,
2020
EventTuiCommandExecute,
2121
EventTuiPromptAppend,
22+
EventTuiSessionSelect,
2223
EventTuiToastShow,
2324
FileListResponses,
2425
FilePartInput,
@@ -144,6 +145,8 @@ import type {
144145
TuiOpenThemesResponses,
145146
TuiPublishErrors,
146147
TuiPublishResponses,
148+
TuiSelectSessionErrors,
149+
TuiSelectSessionResponses,
147150
TuiShowToastResponses,
148151
TuiSubmitPromptResponses,
149152
VcsGetResponses,
@@ -2688,7 +2691,7 @@ export class Tui extends HeyApiClient {
26882691
public publish<ThrowOnError extends boolean = false>(
26892692
parameters?: {
26902693
directory?: string
2691-
body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow
2694+
body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect
26922695
},
26932696
options?: Options<never, ThrowOnError>,
26942697
) {
@@ -2705,6 +2708,41 @@ export class Tui extends HeyApiClient {
27052708
})
27062709
}
27072710

2711+
/**
2712+
* Select session
2713+
*
2714+
* Navigate the TUI to display the specified session.
2715+
*/
2716+
public selectSession<ThrowOnError extends boolean = false>(
2717+
parameters?: {
2718+
directory?: string
2719+
sessionID?: string
2720+
},
2721+
options?: Options<never, ThrowOnError>,
2722+
) {
2723+
const params = buildClientParams(
2724+
[parameters],
2725+
[
2726+
{
2727+
args: [
2728+
{ in: "query", key: "directory" },
2729+
{ in: "body", key: "sessionID" },
2730+
],
2731+
},
2732+
],
2733+
)
2734+
return (options?.client ?? this.client).post<TuiSelectSessionResponses, TuiSelectSessionErrors, ThrowOnError>({
2735+
url: "/tui/select-session",
2736+
...options,
2737+
...params,
2738+
headers: {
2739+
"Content-Type": "application/json",
2740+
...options?.headers,
2741+
...params.headers,
2742+
},
2743+
})
2744+
}
2745+
27082746
control = new Control({ client: this.client })
27092747
}
27102748

packages/sdk/js/src/v2/gen/types.gen.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,16 @@ export type EventTuiToastShow = {
592592
}
593593
}
594594

595+
export type EventTuiSessionSelect = {
596+
type: "tui.session.select"
597+
properties: {
598+
/**
599+
* Session ID to navigate to
600+
*/
601+
sessionID: string
602+
}
603+
}
604+
595605
export type EventMcpToolsChanged = {
596606
type: "mcp.tools.changed"
597607
properties: {
@@ -776,6 +786,7 @@ export type Event =
776786
| EventTuiPromptAppend
777787
| EventTuiCommandExecute
778788
| EventTuiToastShow
789+
| EventTuiSessionSelect
779790
| EventMcpToolsChanged
780791
| EventCommandExecuted
781792
| EventSessionCreated
@@ -4310,7 +4321,7 @@ export type TuiShowToastResponses = {
43104321
export type TuiShowToastResponse = TuiShowToastResponses[keyof TuiShowToastResponses]
43114322

43124323
export type TuiPublishData = {
4313-
body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow
4324+
body?: EventTuiPromptAppend | EventTuiCommandExecute | EventTuiToastShow | EventTuiSessionSelect
43144325
path?: never
43154326
query?: {
43164327
directory?: string
@@ -4336,6 +4347,42 @@ export type TuiPublishResponses = {
43364347

43374348
export type TuiPublishResponse = TuiPublishResponses[keyof TuiPublishResponses]
43384349

4350+
export type TuiSelectSessionData = {
4351+
body?: {
4352+
/**
4353+
* Session ID to navigate to
4354+
*/
4355+
sessionID: string
4356+
}
4357+
path?: never
4358+
query?: {
4359+
directory?: string
4360+
}
4361+
url: "/tui/select-session"
4362+
}
4363+
4364+
export type TuiSelectSessionErrors = {
4365+
/**
4366+
* Bad request
4367+
*/
4368+
400: BadRequestError
4369+
/**
4370+
* Not found
4371+
*/
4372+
404: NotFoundError
4373+
}
4374+
4375+
export type TuiSelectSessionError = TuiSelectSessionErrors[keyof TuiSelectSessionErrors]
4376+
4377+
export type TuiSelectSessionResponses = {
4378+
/**
4379+
* Session selected successfully
4380+
*/
4381+
200: boolean
4382+
}
4383+
4384+
export type TuiSelectSessionResponse = TuiSelectSessionResponses[keyof TuiSelectSessionResponses]
4385+
43394386
export type TuiControlNextData = {
43404387
body?: never
43414388
path?: never

0 commit comments

Comments
 (0)