diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index c40aa114ac8..c46c68f255d 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -271,6 +271,11 @@ export function Autocomplete(props: { description: "toggle thinking visibility", onSelect: () => command.trigger("session.toggle.thinking"), }, + { + display: "/continue", + description: "continue interrupted conversation", + onSelect: () => command.trigger("session.continue"), + }, ) if (sync.data.config.share !== "disabled") { results.push({ @@ -301,7 +306,7 @@ export function Autocomplete(props: { }, { display: "/session", - aliases: ["/resume", "/continue"], + aliases: ["/resume"], description: "list sessions", onSelect: () => command.trigger("session.list"), }, 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 1c1e4b65ec1..1ebfb118b5a 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -389,6 +389,23 @@ export function Session() { }) }, }, + { + title: "Continue interrupted conversation", + value: "session.continue", + keybind: "session_continue", + category: "Session", + onSelect: async (dialog) => { + const result = await sdk.client.session.continue({ + sessionID: route.sessionID, + }) + + if (result.data) { + toBottom() + } else { + dialog.clear() + } + }, + }, { title: sidebarVisible() ? "Hide sidebar" : "Show sidebar", value: "session.sidebar.toggle", diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index f1485ec0150..55cece42275 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -1375,6 +1375,49 @@ export namespace Server { return c.json(session) }, ) + .post( + "/session/:sessionID/continue", + describeRoute({ + description: "Continue interrupted conversation", + operationId: "session.continue", + responses: { + 200: { + description: "Conversation continued", + content: { + "application/json": { + schema: resolver(z.boolean()), + }, + }, + }, + ...errors(400, 404), + }, + }), + validator( + "param", + z.object({ + sessionID: z.string(), + }), + ), + async (c) => { + const sessionID = c.req.valid("param").sessionID + + // Check if there's a user message to continue from + const msgs = await Session.messages({ sessionID }) + const hasUserMessage = msgs.some((m) => m.info.role === "user") + if (!hasUserMessage) { + return c.json(false) + } + + // Cancel any existing session state to ensure clean start + SessionPrompt.cancel(sessionID) + + // Start conversation loop - it will continue from where it left off + // The loop handles incomplete assistant messages automatically + SessionPrompt.loop(sessionID) + + return c.json(true) + }, + ) .post( "/session/:sessionID/permissions/:permissionID", describeRoute({ diff --git a/packages/sdk/js/src/gen/sdk.gen.ts b/packages/sdk/js/src/gen/sdk.gen.ts index 5e3e67e1c03..3388898b4ed 100644 --- a/packages/sdk/js/src/gen/sdk.gen.ts +++ b/packages/sdk/js/src/gen/sdk.gen.ts @@ -109,6 +109,9 @@ import type { SessionUnrevertData, SessionUnrevertResponses, SessionUnrevertErrors, + SessionContinueData, + SessionContinueResponses, + SessionContinueErrors, PostSessionIdPermissionsPermissionIdData, PostSessionIdPermissionsPermissionIdResponses, PostSessionIdPermissionsPermissionIdErrors, @@ -698,6 +701,16 @@ class Session extends _HeyApiClient { ...options, }) } + + /** + * Continue interrupted conversation + */ + public continue(options: Options) { + return (options.client ?? this._client).post({ + url: "/session/{id}/continue", + ...options, + }) + } } class Command extends _HeyApiClient { diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 8d455052537..cf74ee8171c 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -2859,6 +2859,39 @@ export type SessionUnrevertResponses = { export type SessionUnrevertResponse = SessionUnrevertResponses[keyof SessionUnrevertResponses] +export type SessionContinueData = { + body?: never + path: { + id: string + } + query?: { + directory?: string + } + url: "/session/{id}/continue" +} + +export type SessionContinueErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionContinueError = SessionContinueErrors[keyof SessionContinueErrors] + +export type SessionContinueResponses = { + /** + * Conversation continued + */ + 200: boolean +} + +export type SessionContinueResponse = SessionContinueResponses[keyof SessionContinueResponses] + export type PostSessionIdPermissionsPermissionIdData = { body?: { response: "once" | "always" | "reject" diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 90df76c2234..2032067ce90 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -77,6 +77,8 @@ import type { SessionChildrenResponses, SessionCommandErrors, SessionCommandResponses, + SessionContinueErrors, + SessionContinueResponses, SessionCreateErrors, SessionCreateResponses, SessionDeleteErrors, @@ -1484,6 +1486,34 @@ export class Session extends HeyApiClient { ...params, }) } + + /** + * Continue interrupted conversation + */ + public continue( + parameters: { + sessionID: string + directory?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "query", key: "directory" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/session/{sessionID}/continue", + ...options, + ...params, + }) + } } export class Permission extends HeyApiClient { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 9d0bbcc92cd..eec2a0970e9 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -3089,6 +3089,39 @@ export type SessionUnrevertResponses = { export type SessionUnrevertResponse = SessionUnrevertResponses[keyof SessionUnrevertResponses] +export type SessionContinueData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + } + url: "/session/{sessionID}/continue" +} + +export type SessionContinueErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionContinueError = SessionContinueErrors[keyof SessionContinueErrors] + +export type SessionContinueResponses = { + /** + * Conversation continued + */ + 200: boolean +} + +export type SessionContinueResponse = SessionContinueResponses[keyof SessionContinueResponses] + export type PermissionRespondData = { body?: { response: "once" | "always" | "reject"