Преглед на файлове

feat(plugin): add tui.session.select API endpoint for TUI navigation (#6565)

Co-authored-by: Aiden Cline <[email protected]>
YeonGyu-Kim преди 1 месец
родител
ревизия
a3f38e0533

+ 7 - 0
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" })

+ 6 - 0
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"),
+    }),
+  ),
 }

+ 27 - 0
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",

+ 78 - 0
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)
+      },
+    })
+  })
+})

+ 39 - 1
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 })
 }
 

+ 48 - 1
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