From 722a3a536710a47137e8e0d84470df5add7c977a Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 16 Jan 2026 20:58:44 -0700 Subject: [PATCH 1/3] feat(session): add ts_before and breakpoint params to Session.messages API Add optional parameters to Session.messages() for loading older messages: - ts_before: filter to messages created before this timestamp - breakpoint: stop at first compaction summary when true This is a foundational API enhancement that enables clients to implement pagination and history loading without breaking existing functionality. --- .../opencode/src/server/routes/session.ts | 4 + packages/opencode/src/session/index.ts | 4 + .../sdk/js/src/v2/gen/client/client.gen.ts | 9 +- .../src/v2/gen/core/serverSentEvents.gen.ts | 2 - packages/sdk/js/src/v2/gen/sdk.gen.ts | 412 ++++-------------- packages/sdk/js/src/v2/gen/types.gen.ts | 2 + packages/sdk/openapi.json | 14 + 7 files changed, 104 insertions(+), 343 deletions(-) diff --git a/packages/opencode/src/server/routes/session.ts b/packages/opencode/src/server/routes/session.ts index 82e6f3121bf7..4c7d5545bc5a 100644 --- a/packages/opencode/src/server/routes/session.ts +++ b/packages/opencode/src/server/routes/session.ts @@ -571,6 +571,8 @@ export const SessionRoutes = lazy(() => "query", z.object({ limit: z.coerce.number().optional(), + ts_before: z.coerce.number().optional(), + breakpoint: z.coerce.boolean().optional(), }), ), async (c) => { @@ -578,6 +580,8 @@ export const SessionRoutes = lazy(() => const messages = await Session.messages({ sessionID: c.req.valid("param").sessionID, limit: query.limit, + ts_before: query.ts_before, + breakpoint: query.breakpoint, }) return c.json(messages) }, diff --git a/packages/opencode/src/session/index.ts b/packages/opencode/src/session/index.ts index 556fad01f59a..5912f2095c0c 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -317,12 +317,16 @@ export namespace Session { z.object({ sessionID: Identifier.schema("session"), limit: z.number().optional(), + ts_before: z.number().optional(), + breakpoint: z.boolean().optional(), }), async (input) => { const result = [] as MessageV2.WithParts[] for await (const msg of MessageV2.stream(input.sessionID)) { + if (input.ts_before && msg.info.time.created >= input.ts_before) continue if (input.limit && result.length >= input.limit) break result.push(msg) + if (input.ts_before && input.breakpoint && msg.parts.some((p) => p.type === "compaction")) break } result.reverse() return result diff --git a/packages/sdk/js/src/v2/gen/client/client.gen.ts b/packages/sdk/js/src/v2/gen/client/client.gen.ts index 627e98ec4206..47f1403429d2 100644 --- a/packages/sdk/js/src/v2/gen/client/client.gen.ts +++ b/packages/sdk/js/src/v2/gen/client/client.gen.ts @@ -162,16 +162,10 @@ export const createClient = (config: Config = {}): Client => { case "arrayBuffer": case "blob": case "formData": + case "json": case "text": data = await response[parseAs]() break - case "json": { - // Some servers return 200 with no Content-Length and empty body. - // response.json() would throw; read as text and parse if non-empty. - const text = await response.text() - data = text ? JSON.parse(text) : {} - break - } case "stream": return opts.responseStyle === "data" ? response.body @@ -250,7 +244,6 @@ export const createClient = (config: Config = {}): Client => { } return request }, - serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, url, }) } diff --git a/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts b/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts index 056a81259322..09ef3fb39360 100644 --- a/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts +++ b/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts @@ -151,8 +151,6 @@ export const createSseClient = ({ const { done, value } = await reader.read() if (done) break buffer += value - // Normalize line endings: CRLF -> LF, then CR -> LF - buffer = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n") const chunks = buffer.split("\n\n") buffer = chunks.pop() ?? "" diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index b757b7535075..46abfc3030b4 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -8,13 +8,11 @@ import type { AppLogErrors, AppLogResponses, AppSkillsResponses, - Auth as Auth3, - AuthRemoveErrors, - AuthRemoveResponses, + Auth as Auth2, AuthSetErrors, AuthSetResponses, CommandListResponses, - Config as Config3, + Config as Config2, ConfigGetResponses, ConfigProvidersResponses, ConfigUpdateErrors, @@ -34,9 +32,6 @@ import type { FindSymbolsResponses, FindTextResponses, FormatterStatusResponses, - GlobalConfigGetResponses, - GlobalConfigUpdateErrors, - GlobalConfigUpdateResponses, GlobalDisposeResponses, GlobalEventResponses, GlobalHealthResponses, @@ -167,12 +162,6 @@ import type { WorktreeCreateInput, WorktreeCreateResponses, WorktreeListResponses, - WorktreeRemoveErrors, - WorktreeRemoveInput, - WorktreeRemoveResponses, - WorktreeResetErrors, - WorktreeResetInput, - WorktreeResetResponses, } from "./types.gen.js" export type Options = Options2< @@ -218,44 +207,6 @@ class HeyApiRegistry { } } -export class Config extends HeyApiClient { - /** - * Get global configuration - * - * Retrieve the current global OpenCode configuration settings and preferences. - */ - public get(options?: Options) { - return (options?.client ?? this.client).get({ - url: "/global/config", - ...options, - }) - } - - /** - * Update global configuration - * - * Update global OpenCode configuration settings and preferences. - */ - public update( - parameters?: { - config?: Config3 - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ key: "config", map: "body" }] }]) - return (options?.client ?? this.client).patch({ - url: "/global/config", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } -} - export class Global extends HeyApiClient { /** * Get health @@ -292,67 +243,6 @@ export class Global extends HeyApiClient { ...options, }) } - - private _config?: Config - get config(): Config { - return (this._config ??= new Config({ client: this.client })) - } -} - -export class Auth extends HeyApiClient { - /** - * Remove auth credentials - * - * Remove authentication credentials - */ - public remove( - parameters: { - providerID: string - }, - options?: Options, - ) { - const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "providerID" }] }]) - return (options?.client ?? this.client).delete({ - url: "/auth/{providerID}", - ...options, - ...params, - }) - } - - /** - * Set auth credentials - * - * Set authentication credentials - */ - public set( - parameters: { - providerID: string - auth?: Auth3 - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "providerID" }, - { key: "auth", map: "body" }, - ], - }, - ], - ) - return (options?.client ?? this.client).put({ - url: "/auth/{providerID}", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } } export class Project extends HeyApiClient { @@ -397,7 +287,7 @@ export class Project extends HeyApiClient { /** * Update project * - * Update project properties such as name, icon, and commands. + * Update project properties such as name, icon and color. */ public update( parameters: { @@ -406,15 +296,8 @@ export class Project extends HeyApiClient { name?: string icon?: { url?: string - override?: string color?: string } - commands?: { - /** - * Startup script to run when creating a new workspace (worktree) - */ - start?: string - } }, options?: Options, ) { @@ -427,7 +310,6 @@ export class Project extends HeyApiClient { { in: "query", key: "directory" }, { in: "body", key: "name" }, { in: "body", key: "icon" }, - { in: "body", key: "commands" }, ], }, ], @@ -643,7 +525,7 @@ export class Pty extends HeyApiClient { } } -export class Config2 extends HeyApiClient { +export class Config extends HeyApiClient { /** * Get configuration * @@ -671,7 +553,7 @@ export class Config2 extends HeyApiClient { public update( parameters?: { directory?: string - config?: Config3 + config?: Config2 }, options?: Options, ) { @@ -772,41 +654,6 @@ export class Tool extends HeyApiClient { } export class Worktree extends HeyApiClient { - /** - * Remove worktree - * - * Remove a git worktree and delete its branch. - */ - public remove( - parameters?: { - directory?: string - worktreeRemoveInput?: WorktreeRemoveInput - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { key: "worktreeRemoveInput", map: "body" }, - ], - }, - ], - ) - return (options?.client ?? this.client).delete({ - url: "/experimental/worktree", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } - /** * List worktrees * @@ -829,7 +676,7 @@ export class Worktree extends HeyApiClient { /** * Create worktree * - * Create a new git worktree for the current project and run any configured startup scripts. + * Create a new git worktree for the current project. */ public create( parameters?: { @@ -860,41 +707,6 @@ export class Worktree extends HeyApiClient { }, }) } - - /** - * Reset worktree - * - * Reset a worktree branch to the primary default branch. - */ - public reset( - parameters?: { - directory?: string - worktreeResetInput?: WorktreeResetInput - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { key: "worktreeResetInput", map: "body" }, - ], - }, - ], - ) - return (options?.client ?? this.client).post({ - url: "/experimental/worktree/reset", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } } export class Resource extends HeyApiClient { @@ -919,10 +731,7 @@ export class Resource extends HeyApiClient { } export class Experimental extends HeyApiClient { - private _resource?: Resource - get resource(): Resource { - return (this._resource ??= new Resource({ client: this.client })) - } + resource = new Resource({ client: this.client }) } export class Session extends HeyApiClient { @@ -1432,6 +1241,8 @@ export class Session extends HeyApiClient { sessionID: string directory?: string limit?: number + ts_before?: number + breakpoint?: boolean }, options?: Options, ) { @@ -1443,6 +1254,8 @@ export class Session extends HeyApiClient { { in: "path", key: "sessionID" }, { in: "query", key: "directory" }, { in: "query", key: "limit" }, + { in: "query", key: "ts_before" }, + { in: "query", key: "breakpoint" }, ], }, ], @@ -2155,10 +1968,7 @@ export class Provider extends HeyApiClient { }) } - private _oauth?: Oauth - get oauth(): Oauth { - return (this._oauth ??= new Oauth({ client: this.client })) - } + oauth = new Oauth({ client: this.client }) } export class Find extends HeyApiClient { @@ -2340,7 +2150,7 @@ export class File extends HeyApiClient { } } -export class Auth2 extends HeyApiClient { +export class Auth extends HeyApiClient { /** * Remove MCP OAuth * @@ -2469,6 +2279,43 @@ export class Auth2 extends HeyApiClient { }, ) } + + /** + * Set auth credentials + * + * Set authentication credentials + */ + public set( + parameters: { + providerID: string + directory?: string + auth?: Auth2 + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "providerID" }, + { in: "query", key: "directory" }, + { key: "auth", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).put({ + url: "/auth/{providerID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } } export class Mcp extends HeyApiClient { @@ -2584,10 +2431,7 @@ export class Mcp extends HeyApiClient { }) } - private _auth?: Auth2 - get auth(): Auth2 { - return (this._auth ??= new Auth2({ client: this.client })) - } + auth = new Auth({ client: this.client }) } export class Control extends HeyApiClient { @@ -2622,17 +2466,7 @@ export class Control extends HeyApiClient { }, options?: Options, ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { key: "body", map: "body" }, - ], - }, - ], - ) + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }, { in: "body" }] }]) return (options?.client ?? this.client).post({ url: "/tui/control/response", ...options, @@ -2884,17 +2718,7 @@ export class Tui extends HeyApiClient { }, options?: Options, ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "query", key: "directory" }, - { key: "body", map: "body" }, - ], - }, - ], - ) + const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }, { in: "body" }] }]) return (options?.client ?? this.client).post({ url: "/tui/publish", ...options, @@ -2942,10 +2766,7 @@ export class Tui extends HeyApiClient { }) } - private _control?: Control - get control(): Control { - return (this._control ??= new Control({ client: this.client })) - } + control = new Control({ client: this.client }) } export class Instance extends HeyApiClient { @@ -3186,128 +3007,53 @@ export class OpencodeClient extends HeyApiClient { OpencodeClient.__registry.set(this, args?.key) } - private _global?: Global - get global(): Global { - return (this._global ??= new Global({ client: this.client })) - } + global = new Global({ client: this.client }) - private _auth?: Auth - get auth(): Auth { - return (this._auth ??= new Auth({ client: this.client })) - } + project = new Project({ client: this.client }) - private _project?: Project - get project(): Project { - return (this._project ??= new Project({ client: this.client })) - } + pty = new Pty({ client: this.client }) - private _pty?: Pty - get pty(): Pty { - return (this._pty ??= new Pty({ client: this.client })) - } + config = new Config({ client: this.client }) - private _config?: Config2 - get config(): Config2 { - return (this._config ??= new Config2({ client: this.client })) - } + tool = new Tool({ client: this.client }) - private _tool?: Tool - get tool(): Tool { - return (this._tool ??= new Tool({ client: this.client })) - } + worktree = new Worktree({ client: this.client }) - private _worktree?: Worktree - get worktree(): Worktree { - return (this._worktree ??= new Worktree({ client: this.client })) - } + experimental = new Experimental({ client: this.client }) - private _experimental?: Experimental - get experimental(): Experimental { - return (this._experimental ??= new Experimental({ client: this.client })) - } + session = new Session({ client: this.client }) - private _session?: Session - get session(): Session { - return (this._session ??= new Session({ client: this.client })) - } + part = new Part({ client: this.client }) - private _part?: Part - get part(): Part { - return (this._part ??= new Part({ client: this.client })) - } + permission = new Permission({ client: this.client }) - private _permission?: Permission - get permission(): Permission { - return (this._permission ??= new Permission({ client: this.client })) - } + question = new Question({ client: this.client }) - private _question?: Question - get question(): Question { - return (this._question ??= new Question({ client: this.client })) - } + provider = new Provider({ client: this.client }) - private _provider?: Provider - get provider(): Provider { - return (this._provider ??= new Provider({ client: this.client })) - } + find = new Find({ client: this.client }) - private _find?: Find - get find(): Find { - return (this._find ??= new Find({ client: this.client })) - } + file = new File({ client: this.client }) - private _file?: File - get file(): File { - return (this._file ??= new File({ client: this.client })) - } + mcp = new Mcp({ client: this.client }) - private _mcp?: Mcp - get mcp(): Mcp { - return (this._mcp ??= new Mcp({ client: this.client })) - } + tui = new Tui({ client: this.client }) - private _tui?: Tui - get tui(): Tui { - return (this._tui ??= new Tui({ client: this.client })) - } + instance = new Instance({ client: this.client }) - private _instance?: Instance - get instance(): Instance { - return (this._instance ??= new Instance({ client: this.client })) - } + path = new Path({ client: this.client }) - private _path?: Path - get path(): Path { - return (this._path ??= new Path({ client: this.client })) - } + vcs = new Vcs({ client: this.client }) - private _vcs?: Vcs - get vcs(): Vcs { - return (this._vcs ??= new Vcs({ client: this.client })) - } + command = new Command({ client: this.client }) - private _command?: Command - get command(): Command { - return (this._command ??= new Command({ client: this.client })) - } + app = new App({ client: this.client }) - private _app?: App - get app(): App { - return (this._app ??= new App({ client: this.client })) - } + lsp = new Lsp({ client: this.client }) - private _lsp?: Lsp - get lsp(): Lsp { - return (this._lsp ??= new Lsp({ client: this.client })) - } + formatter = new Formatter({ client: this.client }) - private _formatter?: Formatter - get formatter(): Formatter { - return (this._formatter ??= new Formatter({ client: this.client })) - } + auth = new Auth({ client: this.client }) - private _event?: Event - get event(): Event { - return (this._event ??= new Event({ client: this.client })) - } + event = new Event({ 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 d72c37a28b5a..114fec8e43e1 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -3332,6 +3332,8 @@ export type SessionMessagesData = { query?: { directory?: string limit?: number + ts_before?: number + breakpoint?: boolean } url: "/session/{sessionID}/message" } diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index f50cc06c1010..d903cd88dc24 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -2258,6 +2258,20 @@ "schema": { "type": "number" } + }, + { + "in": "query", + "name": "ts_before", + "schema": { + "type": "number" + } + }, + { + "in": "query", + "name": "breakpoint", + "schema": { + "type": "boolean" + } } ], "summary": "Get session messages", From 2b891d161d79bc32624b071bb37440d82d3ad2d2 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 7 Feb 2026 02:04:40 -0700 Subject: [PATCH 2/3] chore: regenerate SDK with ts_before and breakpoint params --- .../sdk/js/src/v2/gen/client/client.gen.ts | 9 +- .../src/v2/gen/core/serverSentEvents.gen.ts | 2 + packages/sdk/js/src/v2/gen/sdk.gen.ts | 408 ++++++++++++++---- 3 files changed, 343 insertions(+), 76 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/client/client.gen.ts b/packages/sdk/js/src/v2/gen/client/client.gen.ts index 47f1403429d2..627e98ec4206 100644 --- a/packages/sdk/js/src/v2/gen/client/client.gen.ts +++ b/packages/sdk/js/src/v2/gen/client/client.gen.ts @@ -162,10 +162,16 @@ export const createClient = (config: Config = {}): Client => { case "arrayBuffer": case "blob": case "formData": - case "json": case "text": data = await response[parseAs]() break + case "json": { + // Some servers return 200 with no Content-Length and empty body. + // response.json() would throw; read as text and parse if non-empty. + const text = await response.text() + data = text ? JSON.parse(text) : {} + break + } case "stream": return opts.responseStyle === "data" ? response.body @@ -244,6 +250,7 @@ export const createClient = (config: Config = {}): Client => { } return request }, + serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, url, }) } diff --git a/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts b/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts index 09ef3fb39360..056a81259322 100644 --- a/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts +++ b/packages/sdk/js/src/v2/gen/core/serverSentEvents.gen.ts @@ -151,6 +151,8 @@ export const createSseClient = ({ const { done, value } = await reader.read() if (done) break buffer += value + // Normalize line endings: CRLF -> LF, then CR -> LF + buffer = buffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n") const chunks = buffer.split("\n\n") buffer = chunks.pop() ?? "" diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 46abfc3030b4..26b150b871a5 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -8,11 +8,13 @@ import type { AppLogErrors, AppLogResponses, AppSkillsResponses, - Auth as Auth2, + Auth as Auth3, + AuthRemoveErrors, + AuthRemoveResponses, AuthSetErrors, AuthSetResponses, CommandListResponses, - Config as Config2, + Config as Config3, ConfigGetResponses, ConfigProvidersResponses, ConfigUpdateErrors, @@ -32,6 +34,9 @@ import type { FindSymbolsResponses, FindTextResponses, FormatterStatusResponses, + GlobalConfigGetResponses, + GlobalConfigUpdateErrors, + GlobalConfigUpdateResponses, GlobalDisposeResponses, GlobalEventResponses, GlobalHealthResponses, @@ -162,6 +167,12 @@ import type { WorktreeCreateInput, WorktreeCreateResponses, WorktreeListResponses, + WorktreeRemoveErrors, + WorktreeRemoveInput, + WorktreeRemoveResponses, + WorktreeResetErrors, + WorktreeResetInput, + WorktreeResetResponses, } from "./types.gen.js" export type Options = Options2< @@ -207,6 +218,44 @@ class HeyApiRegistry { } } +export class Config extends HeyApiClient { + /** + * Get global configuration + * + * Retrieve the current global OpenCode configuration settings and preferences. + */ + public get(options?: Options) { + return (options?.client ?? this.client).get({ + url: "/global/config", + ...options, + }) + } + + /** + * Update global configuration + * + * Update global OpenCode configuration settings and preferences. + */ + public update( + parameters?: { + config?: Config3 + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ key: "config", map: "body" }] }]) + return (options?.client ?? this.client).patch({ + url: "/global/config", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } +} + export class Global extends HeyApiClient { /** * Get health @@ -243,6 +292,67 @@ export class Global extends HeyApiClient { ...options, }) } + + private _config?: Config + get config(): Config { + return (this._config ??= new Config({ client: this.client })) + } +} + +export class Auth extends HeyApiClient { + /** + * Remove auth credentials + * + * Remove authentication credentials + */ + public remove( + parameters: { + providerID: string + }, + options?: Options, + ) { + const params = buildClientParams([parameters], [{ args: [{ in: "path", key: "providerID" }] }]) + return (options?.client ?? this.client).delete({ + url: "/auth/{providerID}", + ...options, + ...params, + }) + } + + /** + * Set auth credentials + * + * Set authentication credentials + */ + public set( + parameters: { + providerID: string + auth?: Auth3 + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "providerID" }, + { key: "auth", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).put({ + url: "/auth/{providerID}", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } } export class Project extends HeyApiClient { @@ -287,7 +397,7 @@ export class Project extends HeyApiClient { /** * Update project * - * Update project properties such as name, icon and color. + * Update project properties such as name, icon, and commands. */ public update( parameters: { @@ -296,8 +406,15 @@ export class Project extends HeyApiClient { name?: string icon?: { url?: string + override?: string color?: string } + commands?: { + /** + * Startup script to run when creating a new workspace (worktree) + */ + start?: string + } }, options?: Options, ) { @@ -310,6 +427,7 @@ export class Project extends HeyApiClient { { in: "query", key: "directory" }, { in: "body", key: "name" }, { in: "body", key: "icon" }, + { in: "body", key: "commands" }, ], }, ], @@ -525,7 +643,7 @@ export class Pty extends HeyApiClient { } } -export class Config extends HeyApiClient { +export class Config2 extends HeyApiClient { /** * Get configuration * @@ -553,7 +671,7 @@ export class Config extends HeyApiClient { public update( parameters?: { directory?: string - config?: Config2 + config?: Config3 }, options?: Options, ) { @@ -654,6 +772,41 @@ export class Tool extends HeyApiClient { } export class Worktree extends HeyApiClient { + /** + * Remove worktree + * + * Remove a git worktree and delete its branch. + */ + public remove( + parameters?: { + directory?: string + worktreeRemoveInput?: WorktreeRemoveInput + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { key: "worktreeRemoveInput", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete({ + url: "/experimental/worktree", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } + /** * List worktrees * @@ -676,7 +829,7 @@ export class Worktree extends HeyApiClient { /** * Create worktree * - * Create a new git worktree for the current project. + * Create a new git worktree for the current project and run any configured startup scripts. */ public create( parameters?: { @@ -707,6 +860,41 @@ export class Worktree extends HeyApiClient { }, }) } + + /** + * Reset worktree + * + * Reset a worktree branch to the primary default branch. + */ + public reset( + parameters?: { + directory?: string + worktreeResetInput?: WorktreeResetInput + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { key: "worktreeResetInput", map: "body" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/experimental/worktree/reset", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } } export class Resource extends HeyApiClient { @@ -731,7 +919,10 @@ export class Resource extends HeyApiClient { } export class Experimental extends HeyApiClient { - resource = new Resource({ client: this.client }) + private _resource?: Resource + get resource(): Resource { + return (this._resource ??= new Resource({ client: this.client })) + } } export class Session extends HeyApiClient { @@ -1968,7 +2159,10 @@ export class Provider extends HeyApiClient { }) } - oauth = new Oauth({ client: this.client }) + private _oauth?: Oauth + get oauth(): Oauth { + return (this._oauth ??= new Oauth({ client: this.client })) + } } export class Find extends HeyApiClient { @@ -2150,7 +2344,7 @@ export class File extends HeyApiClient { } } -export class Auth extends HeyApiClient { +export class Auth2 extends HeyApiClient { /** * Remove MCP OAuth * @@ -2279,43 +2473,6 @@ export class Auth extends HeyApiClient { }, ) } - - /** - * Set auth credentials - * - * Set authentication credentials - */ - public set( - parameters: { - providerID: string - directory?: string - auth?: Auth2 - }, - options?: Options, - ) { - const params = buildClientParams( - [parameters], - [ - { - args: [ - { in: "path", key: "providerID" }, - { in: "query", key: "directory" }, - { key: "auth", map: "body" }, - ], - }, - ], - ) - return (options?.client ?? this.client).put({ - url: "/auth/{providerID}", - ...options, - ...params, - headers: { - "Content-Type": "application/json", - ...options?.headers, - ...params.headers, - }, - }) - } } export class Mcp extends HeyApiClient { @@ -2431,7 +2588,10 @@ export class Mcp extends HeyApiClient { }) } - auth = new Auth({ client: this.client }) + private _auth?: Auth2 + get auth(): Auth2 { + return (this._auth ??= new Auth2({ client: this.client })) + } } export class Control extends HeyApiClient { @@ -2466,7 +2626,17 @@ export class Control extends HeyApiClient { }, options?: Options, ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }, { in: "body" }] }]) + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { key: "body", map: "body" }, + ], + }, + ], + ) return (options?.client ?? this.client).post({ url: "/tui/control/response", ...options, @@ -2718,7 +2888,17 @@ export class Tui extends HeyApiClient { }, options?: Options, ) { - const params = buildClientParams([parameters], [{ args: [{ in: "query", key: "directory" }, { in: "body" }] }]) + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { key: "body", map: "body" }, + ], + }, + ], + ) return (options?.client ?? this.client).post({ url: "/tui/publish", ...options, @@ -2766,7 +2946,10 @@ export class Tui extends HeyApiClient { }) } - control = new Control({ client: this.client }) + private _control?: Control + get control(): Control { + return (this._control ??= new Control({ client: this.client })) + } } export class Instance extends HeyApiClient { @@ -3007,53 +3190,128 @@ export class OpencodeClient extends HeyApiClient { OpencodeClient.__registry.set(this, args?.key) } - global = new Global({ client: this.client }) + private _global?: Global + get global(): Global { + return (this._global ??= new Global({ client: this.client })) + } - project = new Project({ client: this.client }) + private _auth?: Auth + get auth(): Auth { + return (this._auth ??= new Auth({ client: this.client })) + } - pty = new Pty({ client: this.client }) + private _project?: Project + get project(): Project { + return (this._project ??= new Project({ client: this.client })) + } - config = new Config({ client: this.client }) + private _pty?: Pty + get pty(): Pty { + return (this._pty ??= new Pty({ client: this.client })) + } - tool = new Tool({ client: this.client }) + private _config?: Config2 + get config(): Config2 { + return (this._config ??= new Config2({ client: this.client })) + } - worktree = new Worktree({ client: this.client }) + private _tool?: Tool + get tool(): Tool { + return (this._tool ??= new Tool({ client: this.client })) + } - experimental = new Experimental({ client: this.client }) + private _worktree?: Worktree + get worktree(): Worktree { + return (this._worktree ??= new Worktree({ client: this.client })) + } - session = new Session({ client: this.client }) + private _experimental?: Experimental + get experimental(): Experimental { + return (this._experimental ??= new Experimental({ client: this.client })) + } - part = new Part({ client: this.client }) + private _session?: Session + get session(): Session { + return (this._session ??= new Session({ client: this.client })) + } - permission = new Permission({ client: this.client }) + private _part?: Part + get part(): Part { + return (this._part ??= new Part({ client: this.client })) + } - question = new Question({ client: this.client }) + private _permission?: Permission + get permission(): Permission { + return (this._permission ??= new Permission({ client: this.client })) + } - provider = new Provider({ client: this.client }) + private _question?: Question + get question(): Question { + return (this._question ??= new Question({ client: this.client })) + } - find = new Find({ client: this.client }) + private _provider?: Provider + get provider(): Provider { + return (this._provider ??= new Provider({ client: this.client })) + } - file = new File({ client: this.client }) + private _find?: Find + get find(): Find { + return (this._find ??= new Find({ client: this.client })) + } - mcp = new Mcp({ client: this.client }) + private _file?: File + get file(): File { + return (this._file ??= new File({ client: this.client })) + } - tui = new Tui({ client: this.client }) + private _mcp?: Mcp + get mcp(): Mcp { + return (this._mcp ??= new Mcp({ client: this.client })) + } - instance = new Instance({ client: this.client }) + private _tui?: Tui + get tui(): Tui { + return (this._tui ??= new Tui({ client: this.client })) + } - path = new Path({ client: this.client }) + private _instance?: Instance + get instance(): Instance { + return (this._instance ??= new Instance({ client: this.client })) + } - vcs = new Vcs({ client: this.client }) + private _path?: Path + get path(): Path { + return (this._path ??= new Path({ client: this.client })) + } - command = new Command({ client: this.client }) + private _vcs?: Vcs + get vcs(): Vcs { + return (this._vcs ??= new Vcs({ client: this.client })) + } - app = new App({ client: this.client }) + private _command?: Command + get command(): Command { + return (this._command ??= new Command({ client: this.client })) + } - lsp = new Lsp({ client: this.client }) + private _app?: App + get app(): App { + return (this._app ??= new App({ client: this.client })) + } - formatter = new Formatter({ client: this.client }) + private _lsp?: Lsp + get lsp(): Lsp { + return (this._lsp ??= new Lsp({ client: this.client })) + } - auth = new Auth({ client: this.client }) + private _formatter?: Formatter + get formatter(): Formatter { + return (this._formatter ??= new Formatter({ client: this.client })) + } - event = new Event({ client: this.client }) + private _event?: Event + get event(): Event { + return (this._event ??= new Event({ client: this.client })) + } } From 8c332f95a8d4a384be0fe0be8038fc3884adf210 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 17 Jan 2026 21:24:58 -0700 Subject: [PATCH 3/3] feat(tui): add progressive message loading UI Adds TUI integration for loading older messages in sessions with 100+ messages. Implementation: - loadConversationHistory(): Loads messages up to next compaction summary - loadFullSessionHistory(): Loads entire remaining session history UI Integration: - Displays 'Load more messages' when 100+ messages present - Two clickable options for conversation vs full history - Toast notifications show count of messages loaded - Uses synthetic message pattern for clean positioning Depends on the ts_before and breakpoint API parameters added in the previous commit. --- .../opencode/src/cli/cmd/tui/context/sync.tsx | 51 +++++++++++ .../src/cli/cmd/tui/routes/session/index.tsx | 87 ++++++++++++++++++- 2 files changed, 137 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index eb8ed2d9bbad..1362f0a01848 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -462,6 +462,57 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({ ) fullSyncedSessions.add(sessionID) }, + async loadConversationHistory(sessionID: string) { + const messages = store.message[sessionID] + if (!messages || messages.length === 0) return + + const earliest = messages[0] + const result = await sdk.client.session.messages({ + sessionID, + ts_before: earliest.time.created, + breakpoint: true, + }) + + if (!result.data || result.data.length === 0) { + return 0 + } + + setStore( + produce((draft) => { + const existing = draft.message[sessionID] ?? [] + draft.message[sessionID] = [...result.data!.map((x) => x.info), ...existing] + for (const message of result.data!) { + draft.part[message.info.id] = message.parts + } + }), + ) + return result.data.length + }, + async loadFullSessionHistory(sessionID: string) { + const messages = store.message[sessionID] + if (!messages || messages.length === 0) return + + const earliest = messages[0] + const result = await sdk.client.session.messages({ + sessionID, + ts_before: earliest.time.created, + }) + + if (!result.data || result.data.length === 0) { + return 0 + } + + setStore( + produce((draft) => { + const existing = draft.message[sessionID] ?? [] + draft.message[sessionID] = [...result.data!.map((x) => x.info), ...existing] + for (const message of result.data!) { + draft.part[message.info.id] = message.parts + } + }), + ) + return result.data.length + }, }, bootstrap, } diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 77872eedaddd..ccb743932e4e 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -123,6 +123,22 @@ export function Session() { .toSorted((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)) }) const messages = createMemo(() => sync.data.message[route.sessionID] ?? []) + + const messagesDisplay = createMemo(() => { + const msgs = messages() + if (msgs.length >= 100) { + const synthetic = { + id: "__load_more__", + sessionID: route.sessionID, + role: "system" as const, + time: { created: 0, updated: 0, completed: null }, + _synthetic: true, + } as any + return [synthetic, ...msgs] + } + return msgs + }) + const permissions = createMemo(() => { if (session()?.parentID) return [] return children().flatMap((x) => sync.data.permission[x.id] ?? []) @@ -979,9 +995,78 @@ export function Session() { flexGrow={1} scrollAcceleration={scrollAcceleration()} > - + {(message, index) => ( + + {(function () { + const [hoveredButton, setHoveredButton] = createSignal<"conversation" | "full" | null>(null) + const [loading, setLoading] = createSignal(false) + + const handleLoadConversation = async () => { + if (loading()) return + setLoading(true) + try { + const count = await sync.session.loadConversationHistory(route.sessionID) + if (count === 0) { + toast.show({ message: "No more messages loaded", variant: "info" }) + } else { + toast.show({ message: `History loaded (${count} messages)`, variant: "success" }) + } + } finally { + setLoading(false) + } + } + + const handleLoadFull = async () => { + if (loading()) return + setLoading(true) + try { + const count = await sync.session.loadFullSessionHistory(route.sessionID) + if (count === 0) { + toast.show({ message: "No more messages loaded", variant: "info" }) + } else { + toast.show({ message: `History loaded (${count} messages)`, variant: "success" }) + } + } finally { + setLoading(false) + } + } + + return ( + + Load more messages: + setHoveredButton("conversation")} + onMouseOut={() => setHoveredButton(null)} + onMouseUp={handleLoadConversation} + > + + load conversation history + + + or + setHoveredButton("full")} + onMouseOut={() => setHoveredButton(null)} + onMouseUp={handleLoadFull} + > + + load full session history + + + + ) + })()} + {(function () { const command = useCommandDialog()