Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 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 @@ -395,6 +395,16 @@ export function Session() {
dialog.clear()
},
},
{
title: "Remove queued messages",
value: "session.unqueue",
keybind: "session_unqueue",
category: "Session",
onSelect: (dialog) => {
sdk.client.session.unqueue({ sessionID: route.sessionID })
dialog.clear()
},
},
{
title: "Unshare session",
value: "session.unshare",
Expand Down
6 changes: 6 additions & 0 deletions packages/opencode/src/command/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export namespace Command {
export const Default = {
INIT: "init",
REVIEW: "review",
UNQUEUE: "unqueue",
} as const

const state = Instance.state(async () => {
Expand All @@ -54,6 +55,11 @@ export namespace Command {
template: PROMPT_REVIEW.replace("${path}", Instance.worktree),
subtask: true,
},
[Default.UNQUEUE]: {
name: Default.UNQUEUE,
description: "remove all queued messages",
template: "",
},
}

for (const [name, command] of Object.entries(cfg.command ?? {})) {
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ export namespace Config {
session_share: z.string().optional().default("none").describe("Share current session"),
session_unshare: z.string().optional().default("none").describe("Unshare current session"),
session_interrupt: z.string().optional().default("escape").describe("Interrupt current session"),
session_unqueue: z.string().optional().default("none").describe("Remove queued messages"),
session_compact: z.string().optional().default("<leader>c").describe("Compact the session"),
messages_page_up: z.string().optional().default("pageup").describe("Scroll messages up by one page"),
messages_page_down: z.string().optional().default("pagedown").describe("Scroll messages down by one page"),
Expand Down
29 changes: 29 additions & 0 deletions packages/opencode/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -948,6 +948,34 @@ export namespace Server {
return c.json(true)
},
)
.post(
"/session/:sessionID/unqueue",
describeRoute({
description: "Remove queued messages from session",
operationId: "session.unqueue",
responses: {
200: {
description: "Removed queued messages",
content: {
"application/json": {
schema: resolver(z.boolean()),
},
},
},
...errors(400, 404),
},
}),
validator(
"param",
z.object({
sessionID: z.string(),
}),
),
async (c) => {
await SessionPrompt.removeQueued(c.req.valid("param").sessionID)
return c.json(true)
},
)
.post(
"/session/:sessionID/share",
describeRoute({
Expand Down Expand Up @@ -2461,6 +2489,7 @@ export namespace Server {
session_new: "session.new",
session_share: "session.share",
session_interrupt: "session.interrupt",
session_unqueue: "session.unqueue",
session_compact: "session.compact",
messages_page_up: "session.page.up",
messages_page_down: "session.page.down",
Expand Down
61 changes: 61 additions & 0 deletions packages/opencode/src/session/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,18 @@ export namespace SessionPrompt {
return
}

export function clearQueue(sessionID: string) {
log.info("clearQueue", { sessionID })
const s = state()
const match = s[sessionID]
if (!match) return

for (const callback of match.callbacks) {
callback.reject()
}
match.callbacks = []
}

export const loop = fn(Identifier.schema("session"), async (sessionID) => {
const abort = start(sessionID)
if (!abort) {
Expand Down Expand Up @@ -1279,8 +1291,57 @@ export namespace SessionPrompt {
* Does not match when preceded by word characters or backticks (to avoid email addresses and quoted references)
*/

export async function removeQueued(sessionID: string) {
const queued: string[] = []

// MessageV2.stream yields newest->oldest, so queued messages appear first
for await (const msg of MessageV2.stream(sessionID)) {
if (msg.info.role === "assistant") {
// Only delete if incomplete assistant found
if (!msg.info.time.completed && queued.length > 0) {
await Promise.all(queued.map((messageID) => Session.removeMessage({ sessionID, messageID })))
}
break
}

if (msg.info.role === "user") {
queued.push(msg.info.id)
}
}

clearQueue(sessionID)
}

async function handleUnqueue(sessionID: string) {
await removeQueued(sessionID)

return {
info: {
id: "",
sessionID,
role: "assistant" as const,
parentID: "",
mode: "",
time: { created: 0 },
agent: "",
cost: 0,
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
path: { cwd: "", root: "" },
modelID: "",
providerID: "",
},
parts: [],
}
}

export async function command(input: CommandInput) {
log.info("command", input)

// Special handling for unqueue command
if (input.command === "unqueue") {
return await handleUnqueue(input.sessionID)
}

const command = await Command.get(input.command)
const agentName = command.agent ?? input.agent ?? (await Agent.defaultAgent())

Expand Down
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 @@ -73,6 +73,9 @@ import type {
SessionAbortData,
SessionAbortResponses,
SessionAbortErrors,
SessionUnqueueData,
SessionUnqueueResponses,
SessionUnqueueErrors,
SessionUnshareData,
SessionUnshareResponses,
SessionUnshareErrors,
Expand Down Expand Up @@ -555,6 +558,16 @@ class Session extends _HeyApiClient {
})
}

/**
* Remove queued messages from session
*/
public unqueue<ThrowOnError extends boolean = false>(options: Options<SessionUnqueueData, ThrowOnError>) {
return (options.client ?? this._client).post<SessionUnqueueResponses, SessionUnqueueErrors, ThrowOnError>({
url: "/session/{id}/unqueue",
...options,
})
}

/**
* Unshare the session
*/
Expand Down
37 changes: 37 additions & 0 deletions packages/sdk/js/src/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,10 @@ export type KeybindsConfig = {
* Interrupt current session
*/
session_interrupt?: string
/**
* Remove queued messages
*/
session_unqueue?: string
/**
* Compact the session
*/
Expand Down Expand Up @@ -2392,6 +2396,39 @@ export type SessionAbortResponses = {

export type SessionAbortResponse = SessionAbortResponses[keyof SessionAbortResponses]

export type SessionUnqueueData = {
body?: never
path: {
id: string
}
query?: {
directory?: string
}
url: "/session/{id}/unqueue"
}

export type SessionUnqueueErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}

export type SessionUnqueueError = SessionUnqueueErrors[keyof SessionUnqueueErrors]

export type SessionUnqueueResponses = {
/**
* Removed queued messages
*/
200: boolean
}

export type SessionUnqueueResponse = SessionUnqueueResponses[keyof SessionUnqueueResponses]

export type SessionUnshareData = {
body?: never
path: {
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 @@ -116,6 +116,8 @@ import type {
SessionSummarizeResponses,
SessionTodoErrors,
SessionTodoResponses,
SessionUnqueueErrors,
SessionUnqueueResponses,
SessionUnrevertErrors,
SessionUnrevertResponses,
SessionUnshareErrors,
Expand Down Expand Up @@ -1043,6 +1045,34 @@ export class Session extends HeyApiClient {
})
}

/**
* Remove queued messages from session
*/
public unqueue<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<SessionUnqueueResponses, SessionUnqueueErrors, ThrowOnError>({
url: "/session/{sessionID}/unqueue",
...options,
...params,
})
}

/**
* Unshare session
*
Expand Down
37 changes: 37 additions & 0 deletions packages/sdk/js/src/v2/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,10 @@ export type KeybindsConfig = {
* Interrupt current session
*/
session_interrupt?: string
/**
* Remove queued messages
*/
session_unqueue?: string
/**
* Compact the session
*/
Expand Down Expand Up @@ -2748,6 +2752,39 @@ export type SessionAbortResponses = {

export type SessionAbortResponse = SessionAbortResponses[keyof SessionAbortResponses]

export type SessionUnqueueData = {
body?: never
path: {
sessionID: string
}
query?: {
directory?: string
}
url: "/session/{sessionID}/unqueue"
}

export type SessionUnqueueErrors = {
/**
* Bad request
*/
400: BadRequestError
/**
* Not found
*/
404: NotFoundError
}

export type SessionUnqueueError = SessionUnqueueErrors[keyof SessionUnqueueErrors]

export type SessionUnqueueResponses = {
/**
* Removed queued messages
*/
200: boolean
}

export type SessionUnqueueResponse = SessionUnqueueResponses[keyof SessionUnqueueResponses]

export type SessionUnshareData = {
body?: never
path: {
Expand Down