From 7d1df2ac339fa53e2a29f36f6075571647178b35 Mon Sep 17 00:00:00 2001 From: Joseph Harrison Date: Fri, 6 Feb 2026 06:09:05 -0500 Subject: [PATCH 1/2] feat(api): add POST /session/:sessionID/todo endpoint Add endpoint to update session todos programmatically. This enables plugins to sync their todo lists to the TUI sidebar by calling Todo.update() which publishes the todo.updated Bus event that the TUI subscribes to. This is needed for plugins like speckit that manage their own todo lists and want them to appear in the opencode sidebar. --- .../opencode/src/server/routes/session.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/opencode/src/server/routes/session.ts b/packages/opencode/src/server/routes/session.ts index 613c8b05c17..b1123ed8a1f 100644 --- a/packages/opencode/src/server/routes/session.ts +++ b/packages/opencode/src/server/routes/session.ts @@ -185,6 +185,38 @@ export const SessionRoutes = lazy(() => return c.json(todos) }, ) + .post( + "/:sessionID/todo", + describeRoute({ + summary: "Update session todos", + description: "Update the todo list for a specific session. This triggers a todo.updated event that refreshes the TUI sidebar.", + operationId: "session.todo.update", + responses: { + 200: { + description: "Todos updated successfully", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + ...errors(400, 404), + }, + }), + validator( + "param", + z.object({ + sessionID: z.string().meta({ description: "Session ID" }), + }), + ), + validator("json", z.object({ todos: z.array(Todo.Info) })), + async (c) => { + const sessionID = c.req.valid("param").sessionID + const { todos } = c.req.valid("json") + await Todo.update({ sessionID, todos }) + return c.json(true) + }, + ) .post( "/", describeRoute({ From 6e2af64e7db2e9fc81d24abcfcf9ed1ec922fb5b Mon Sep 17 00:00:00 2001 From: Joseph Harrison Date: Wed, 18 Mar 2026 20:21:19 -0400 Subject: [PATCH 2/2] fix(api): improve todo update endpoint and regenerate SDK - Validate session existence using Session.get.schema instead of bare z.string() (matches pattern used by GET /:sessionID/todo and all other session routes) - Return the written todo array instead of boolean (matches other mutating endpoints) - Fix operationId from session.todo.update to session.todoUpdate to generate a flat Session.todoUpdate() method instead of a nested Todo subclass - Regenerate SDK: adds typed todoUpdate() method on Session class - Also fix GET /:sessionID/todo param validator for consistency --- bun.lock | 4 +- .../opencode/src/server/routes/session.ts | 10 ++--- packages/sdk/js/src/v2/gen/sdk.gen.ts | 42 +++++++++++++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 36 ++++++++++++++++ 4 files changed, 85 insertions(+), 7 deletions(-) diff --git a/bun.lock b/bun.lock index 115100e1048..6dbc147de7c 100644 --- a/bun.lock +++ b/bun.lock @@ -1900,7 +1900,7 @@ "@solidjs/router": ["@solidjs/router@0.15.4", "", { "peerDependencies": { "solid-js": "^1.8.6" } }, "sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ=="], - "@solidjs/start": ["@solidjs/start@https://pkg.pr.new/@solidjs/start@dfb2020", { "dependencies": { "@babel/core": "^7.28.3", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.5", "@solidjs/meta": "^0.29.4", "@tanstack/server-functions-plugin": "1.134.5", "@types/babel__traverse": "^7.28.0", "@types/micromatch": "^4.0.9", "cookie-es": "^2.0.0", "defu": "^6.1.4", "error-stack-parser": "^2.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.25.3", "fast-glob": "^3.3.3", "h3": "npm:h3@2.0.1-rc.4", "html-to-image": "^1.11.13", "micromatch": "^4.0.8", "path-to-regexp": "^8.2.0", "pathe": "^2.0.3", "radix3": "^1.1.2", "seroval": "^1.3.2", "seroval-plugins": "^1.2.1", "shiki": "^1.26.1", "solid-js": "^1.9.9", "source-map-js": "^1.2.1", "srvx": "^0.9.1", "terracotta": "^1.0.6", "vite": "7.1.10", "vite-plugin-solid": "^2.11.9", "vitest": "^4.0.10" } }, "sha512-7JjjA49VGNOsMRI8QRUhVudZmv0CnJ18SliSgK1ojszs/c3ijftgVkzvXdkSLN4miDTzbkXewf65D6ZBo6W+GQ=="], + "@solidjs/start": ["@solidjs/start@https://pkg.pr.new/@solidjs/start@dfb2020", { "dependencies": { "@babel/core": "^7.28.3", "@babel/traverse": "^7.28.3", "@babel/types": "^7.28.5", "@solidjs/meta": "^0.29.4", "@tanstack/server-functions-plugin": "1.134.5", "@types/babel__traverse": "^7.28.0", "@types/micromatch": "^4.0.9", "cookie-es": "^2.0.0", "defu": "^6.1.4", "error-stack-parser": "^2.1.4", "es-module-lexer": "^1.7.0", "esbuild": "^0.25.3", "fast-glob": "^3.3.3", "h3": "npm:h3@2.0.1-rc.4", "html-to-image": "^1.11.13", "micromatch": "^4.0.8", "path-to-regexp": "^8.2.0", "pathe": "^2.0.3", "radix3": "^1.1.2", "seroval": "^1.3.2", "seroval-plugins": "^1.2.1", "shiki": "^1.26.1", "solid-js": "^1.9.9", "source-map-js": "^1.2.1", "srvx": "^0.9.1", "terracotta": "^1.0.6", "vite": "7.1.10", "vite-plugin-solid": "^2.11.9", "vitest": "^4.0.10" } }], "@speed-highlight/core": ["@speed-highlight/core@1.2.14", "", {}, "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA=="], @@ -3020,7 +3020,7 @@ "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], - "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#4af877d", {}, "anomalyco-ghostty-web-4af877d", "sha512-fbEK8mtr7ar4ySsF+JUGjhaZrane7dKphanN+SxHt5XXI6yLMAh/Hpf6sNCOyyVa2UlGCd7YpXG/T2v2RUAX+A=="], + "ghostty-web": ["ghostty-web@github:anomalyco/ghostty-web#4af877d", {}, "anomalyco-ghostty-web-4af877d"], "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="], diff --git a/packages/opencode/src/server/routes/session.ts b/packages/opencode/src/server/routes/session.ts index b1123ed8a1f..38906a46be3 100644 --- a/packages/opencode/src/server/routes/session.ts +++ b/packages/opencode/src/server/routes/session.ts @@ -114,7 +114,7 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: Session.get.schema, + sessionID: SessionID.zod, }), ), async (c) => { @@ -190,13 +190,13 @@ export const SessionRoutes = lazy(() => describeRoute({ summary: "Update session todos", description: "Update the todo list for a specific session. This triggers a todo.updated event that refreshes the TUI sidebar.", - operationId: "session.todo.update", + operationId: "session.todoUpdate", responses: { 200: { description: "Todos updated successfully", content: { "application/json": { - schema: resolver(z.boolean()), + schema: resolver(Todo.Info.array()), }, }, }, @@ -206,7 +206,7 @@ export const SessionRoutes = lazy(() => validator( "param", z.object({ - sessionID: z.string().meta({ description: "Session ID" }), + sessionID: SessionID.zod, }), ), validator("json", z.object({ todos: z.array(Todo.Info) })), @@ -214,7 +214,7 @@ export const SessionRoutes = lazy(() => const sessionID = c.req.valid("param").sessionID const { todos } = c.req.valid("json") await Todo.update({ sessionID, todos }) - return c.json(true) + return c.json(todos) }, ) .post( diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index aa759bb1e09..baa6e2675df 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -143,6 +143,8 @@ import type { SessionSummarizeResponses, SessionTodoErrors, SessionTodoResponses, + SessionTodoUpdateErrors, + SessionTodoUpdateResponses, SessionUnrevertErrors, SessionUnrevertResponses, SessionUnshareErrors, @@ -151,6 +153,7 @@ import type { SessionUpdateResponses, SubtaskPartInput, TextPartInput, + Todo, ToolIdsErrors, ToolIdsResponses, ToolListErrors, @@ -1527,6 +1530,45 @@ export class Session2 extends HeyApiClient { }) } + /** + * Update session todos + * + * Update the todo list for a specific session. This triggers a todo.updated event that refreshes the TUI sidebar. + */ + public todoUpdate( + parameters: { + sessionID: string + directory?: string + workspace?: string + todos?: Array + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "todos" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/todo", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + /** * Initialize session * diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 41aa248171c..f771d06e29e 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -3032,6 +3032,42 @@ export type SessionTodoResponses = { export type SessionTodoResponse = SessionTodoResponses[keyof SessionTodoResponses] +export type SessionTodoUpdateData = { + body?: { + todos: Array + } + path: { + sessionID: string + } + query?: { + directory?: string + workspace?: string + } + url: "/session/{sessionID}/todo" +} + +export type SessionTodoUpdateErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionTodoUpdateError = SessionTodoUpdateErrors[keyof SessionTodoUpdateErrors] + +export type SessionTodoUpdateResponses = { + /** + * Todos updated successfully + */ + 200: Array +} + +export type SessionTodoUpdateResponse = SessionTodoUpdateResponses[keyof SessionTodoUpdateResponses] + export type SessionInitData = { body?: { modelID: string