diff --git a/packages/opencode/src/cli/cmd/tui/context/sync.tsx b/packages/opencode/src/cli/cmd/tui/context/sync.tsx index 0edc911344c..7f5e4914e9b 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sync.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sync.tsx @@ -419,6 +419,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 1d64a2ff156..169c80b0e5d 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -119,6 +119,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] ?? []) @@ -921,9 +937,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() diff --git a/packages/opencode/src/server/routes/session.ts b/packages/opencode/src/server/routes/session.ts index a98624dfae2..6d856e3dbb3 100644 --- a/packages/opencode/src/server/routes/session.ts +++ b/packages/opencode/src/server/routes/session.ts @@ -567,6 +567,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) => { @@ -574,6 +576,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 3fcdab5238c..94dfabdc36f 100644 --- a/packages/opencode/src/session/index.ts +++ b/packages/opencode/src/session/index.ts @@ -293,12 +293,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/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 6f699319965..46abfc3030b 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -8,7 +8,7 @@ import type { AppLogErrors, AppLogResponses, AppSkillsResponses, - Auth as Auth3, + Auth as Auth2, AuthSetErrors, AuthSetResponses, CommandListResponses, @@ -731,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 { @@ -1244,6 +1241,8 @@ export class Session extends HeyApiClient { sessionID: string directory?: string limit?: number + ts_before?: number + breakpoint?: boolean }, options?: Options, ) { @@ -1255,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" }, ], }, ], @@ -1967,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 { @@ -2281,6 +2279,43 @@ 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 { @@ -2396,10 +2431,7 @@ export class Mcp extends HeyApiClient { }) } - private _auth?: Auth - get auth(): Auth { - return (this._auth ??= new Auth({ client: this.client })) - } + auth = new Auth({ client: this.client }) } export class Control extends HeyApiClient { @@ -2734,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 { @@ -2949,45 +2978,6 @@ export class Formatter extends HeyApiClient { } } -export class Auth2 extends HeyApiClient { - /** - * Set auth credentials - * - * Set authentication credentials - */ - public set( - parameters: { - providerID: string - directory?: string - auth?: Auth3 - }, - 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 Event extends HeyApiClient { /** * Subscribe to events @@ -3017,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 _project?: Project - get project(): Project { - return (this._project ??= new Project({ client: this.client })) - } + project = new Project({ client: this.client }) - private _pty?: Pty - get pty(): Pty { - return (this._pty ??= new Pty({ client: this.client })) - } + pty = new Pty({ client: this.client }) - private _config?: Config - get config(): Config { - return (this._config ??= new Config({ client: this.client })) - } + config = new Config({ client: this.client }) - private _tool?: Tool - get tool(): Tool { - return (this._tool ??= new Tool({ client: this.client })) - } + tool = new Tool({ client: this.client }) - private _worktree?: Worktree - get worktree(): Worktree { - return (this._worktree ??= new Worktree({ client: this.client })) - } + worktree = new Worktree({ client: this.client }) - private _experimental?: Experimental - get experimental(): Experimental { - return (this._experimental ??= new Experimental({ client: this.client })) - } + experimental = new Experimental({ client: this.client }) - private _session?: Session - get session(): Session { - return (this._session ??= new Session({ client: this.client })) - } + session = new Session({ client: this.client }) - private _part?: Part - get part(): Part { - return (this._part ??= new Part({ client: this.client })) - } + part = new Part({ client: this.client }) - private _permission?: Permission - get permission(): Permission { - return (this._permission ??= new Permission({ client: this.client })) - } + permission = new Permission({ client: this.client }) - private _question?: Question - get question(): Question { - return (this._question ??= new Question({ client: this.client })) - } + question = new Question({ client: this.client }) - private _provider?: Provider - get provider(): Provider { - return (this._provider ??= new Provider({ client: this.client })) - } + provider = new Provider({ client: this.client }) - private _find?: Find - get find(): Find { - return (this._find ??= new Find({ client: this.client })) - } + find = new Find({ client: this.client }) - private _file?: File - get file(): File { - return (this._file ??= new File({ client: this.client })) - } + file = new File({ client: this.client }) - private _mcp?: Mcp - get mcp(): Mcp { - return (this._mcp ??= new Mcp({ client: this.client })) - } + mcp = new Mcp({ client: this.client }) - private _tui?: Tui - get tui(): Tui { - return (this._tui ??= new Tui({ client: this.client })) - } + tui = new Tui({ client: this.client }) - private _instance?: Instance - get instance(): Instance { - return (this._instance ??= new Instance({ client: this.client })) - } + instance = new Instance({ client: this.client }) - private _path?: Path - get path(): Path { - return (this._path ??= new Path({ client: this.client })) - } + path = new Path({ client: this.client }) - private _vcs?: Vcs - get vcs(): Vcs { - return (this._vcs ??= new Vcs({ client: this.client })) - } + vcs = new Vcs({ client: this.client }) - private _command?: Command - get command(): Command { - return (this._command ??= new Command({ client: this.client })) - } + command = new Command({ client: this.client }) - private _app?: App - get app(): App { - return (this._app ??= new App({ client: this.client })) - } + app = new App({ client: this.client }) - private _lsp?: Lsp - get lsp(): Lsp { - return (this._lsp ??= new Lsp({ client: this.client })) - } + lsp = new Lsp({ client: this.client }) - private _formatter?: Formatter - get formatter(): Formatter { - return (this._formatter ??= new Formatter({ client: this.client })) - } + formatter = new Formatter({ client: this.client }) - private _auth?: Auth2 - get auth(): Auth2 { - return (this._auth ??= new Auth2({ 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 32321a7dfd8..0d78f220951 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -3117,6 +3117,8 @@ export type SessionMessagesData = { query?: { directory?: string limit?: number + ts_before?: number + breakpoint?: boolean } url: "/session/{sessionID}/message" }