Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f36a9ea
feat: Add /continue command for interrupted conversations
ariane-emory Dec 7, 2025
ae76551
Merge remote-tracking branch 'upstream/dev' into feat/continue-command
ariane-emory Dec 7, 2025
8a9f370
Merge remote-tracking branch 'upstream/dev' into feat/continue-command
ariane-emory Dec 7, 2025
55216bb
Merge remote-tracking branch 'upstream/dev' into feat/continue-command
ariane-emory Dec 7, 2025
1213887
tidy: unwanted indentation.
ariane-emory Dec 8, 2025
af450a8
Merge upstream/dev into feat/continue-command
ariane-emory Dec 8, 2025
3e066aa
Merge branch 'dev' into feat/continue-command
ariane-emory Dec 8, 2025
b31cb9d
fix: resolve TypeScript errors after dev merge
ariane-emory Dec 8, 2025
c600ac6
fix: make /continue command work correctly
ariane-emory Dec 8, 2025
088be14
Merge remote-tracking branch 'upstream/dev' into feat/continue-command
ariane-emory Dec 9, 2025
ac88fc3
fix: restore corrupted file
ariane-emory Dec 9, 2025
6f252e8
Merge remote-tracking branch 'upstream/dev' into feat/continue-command
ariane-emory Dec 9, 2025
56f2d3a
Merge branch 'dev' into feat/continue-command
ariane-emory Dec 9, 2025
1358040
Merge remote-tracking branch 'upstream/dev' into feat/continue-command
ariane-emory Dec 10, 2025
b382f7f
Merge dev into feat/continue-command
ariane-emory Dec 10, 2025
e56e7fd
Merge branch 'dev' into feat/continue-command
ariane-emory Dec 10, 2025
c45042e
Merge remote-tracking branch 'upstream/dev' into feat/continue-command
ariane-emory Dec 10, 2025
3d039fa
Merge branch 'dev' into feat/continue-command
ariane-emory Dec 10, 2025
91ad622
Merge branch 'dev' into feat/continue-command
ariane-emory Dec 11, 2025
0a125d2
Merge branch 'dev' into feat/continue-command
ariane-emory Dec 11, 2025
244fd87
Merge branch 'dev' into feat/continue-command
ariane-emory Dec 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -301,7 +306,7 @@ export function Autocomplete(props: {
},
{
display: "/session",
aliases: ["/resume", "/continue"],
aliases: ["/resume"],
description: "list sessions",
onSelect: () => command.trigger("session.list"),
},
Expand Down
17 changes: 17 additions & 0 deletions packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,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",
Expand Down
43 changes: 43 additions & 0 deletions packages/opencode/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1373,6 +1373,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({
Expand Down
18 changes: 18 additions & 0 deletions packages/opencode/test/session/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,22 @@ describe("session.started event", () => {
},
})
})

describe("session continue functionality", () => {
test("should identify unfinished assistant messages correctly", async () => {
await Instance.provide({
directory: projectRoot,
fn: async () => {
// Create a session
const session = await Session.create({})

// Test that empty session has no messages
const emptyMsgs = await Session.messages({ sessionID: session.id })
expect(emptyMsgs).toEqual([])

await Session.remove(session.id)
},
})
})
})
})
13 changes: 13 additions & 0 deletions packages/sdk/js/src/gen/sdk.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ import type {
SessionUnrevertData,
SessionUnrevertResponses,
SessionUnrevertErrors,
SessionContinueData,
SessionContinueResponses,
SessionContinueErrors,
PostSessionIdPermissionsPermissionIdData,
PostSessionIdPermissionsPermissionIdResponses,
PostSessionIdPermissionsPermissionIdErrors,
Expand Down Expand Up @@ -698,6 +701,16 @@ class Session extends _HeyApiClient {
...options,
})
}

/**
* Continue interrupted conversation
*/
public continue<ThrowOnError extends boolean = false>(options: Options<SessionContinueData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionContinueResponses, SessionContinueErrors, ThrowOnError>({
url: "/session/{id}/continue",
...options,
})
}
}

class Command extends _HeyApiClient {
Expand Down
33 changes: 33 additions & 0 deletions packages/sdk/js/src/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
30 changes: 30 additions & 0 deletions packages/sdk/js/src/v2/gen/sdk.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ import type {
SessionChildrenResponses,
SessionCommandErrors,
SessionCommandResponses,
SessionContinueErrors,
SessionContinueResponses,
SessionCreateErrors,
SessionCreateResponses,
SessionDeleteErrors,
Expand Down Expand Up @@ -1484,6 +1486,34 @@ export class Session extends HeyApiClient {
...params,
})
}

/**
* Continue interrupted conversation
*/
public continue<ThrowOnError extends boolean = false>(
parameters: {
sessionID: string
directory?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "path", key: "sessionID" },
{ in: "query", key: "directory" },
],
},
],
)
return (options?.client ?? this.client).post<SessionContinueResponses, SessionContinueErrors, ThrowOnError>({
url: "/session/{sessionID}/continue",
...options,
...params,
})
}
}

export class Permission extends HeyApiClient {
Expand Down
33 changes: 33 additions & 0 deletions packages/sdk/js/src/v2/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down