diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx index d19e93188b2..300e9a66601 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-command.tsx @@ -47,7 +47,7 @@ function init() { if (suspended()) return if (dialog.stack.length > 0) return for (const option of options()) { - if (option.keybind && keybind.match(option.keybind, evt)) { + if (option.keybind && !option.disabled && keybind.match(option.keybind, evt)) { evt.preventDefault() option.onSelect?.(dialog) return diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 4558914cb7e..c90be99bcd6 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -44,6 +44,7 @@ export type PromptProps = { export type PromptRef = { focused: boolean current: PromptInfo + editingQueuedMessageID: string | undefined set(prompt: PromptInfo): void reset(): void blur(): void @@ -73,6 +74,16 @@ export function Prompt(props: PromptProps) { const { theme, syntax } = useTheme() const kv = useKV() + const queuedMessages = createMemo(() => { + if (!props.sessionID) return [] + const messages = sync.data.message[props.sessionID] ?? [] + if (status().type !== "busy") return [] + const sorted = [...messages].sort((a, b) => a.id.localeCompare(b.id)) + const pending = sorted.findLast((m) => m.role === "assistant" && m.time.completed === undefined) + if (!pending) return [] + return sorted.filter((m) => m.role === "user" && m.id > pending.id) + }) + function promptModelWarning() { toast.show({ variant: "warning", @@ -118,6 +129,7 @@ export function Prompt(props: PromptProps) { extmarkToPartIndex: Map interrupt: number placeholder: number + editingQueuedMessageID: string | undefined }>({ placeholder: Math.floor(Math.random() * PLACEHOLDERS.length), prompt: { @@ -127,6 +139,7 @@ export function Prompt(props: PromptProps) { mode: "normal", extmarkToPartIndex: new Map(), interrupt: 0, + editingQueuedMessageID: undefined, }) // Initialize agent/model/variant from last user message when session changes @@ -160,6 +173,7 @@ export function Prompt(props: PromptProps) { onSelect: (dialog) => { input.extmarks.clear() input.clear() + setStore("editingQueuedMessageID", undefined) dialog.clear() }, }, @@ -307,6 +321,79 @@ export function Prompt(props: PromptProps) { input.cursorOffset = Bun.stringWidth(content) }, }, + { + title: "Edit queued message", + category: "Queue", + keybind: "queue_edit", + value: "queue.edit", + disabled: queuedMessages().length === 0 || store.editingQueuedMessageID !== undefined, + onSelect: async (dialog) => { + dialog.clear() + if (!props.sessionID) return + const queued = queuedMessages() + const last = queued.at(-1) + if (!last) return + const response = await sdk.client.session.getQueue({ + sessionID: props.sessionID, + messageID: last.id, + }) + if (!response.data) return + const textParts = response.data.parts.filter((p) => p.type === "text") + const fileParts = response.data.parts.filter((p) => p.type === "file") + const text = textParts.map((p) => p.text).join("") + + let fullText = text + const parts: typeof store.prompt.parts = [] + + for (const file of fileParts) { + const start = fullText.length + (fullText.length > 0 ? 1 : 0) + const virtualText = file.filename ? `[File: ${file.filename}]` : `[Image ${parts.length + 1}]` + const end = start + virtualText.length + fullText += (fullText.length > 0 ? " " : "") + virtualText + parts.push({ + type: "file", + mime: file.mime, + filename: file.filename, + url: file.url, + source: { + type: "file", + path: file.filename ?? "", + text: { start, end, value: virtualText }, + }, + }) + } + + input.setText(fullText) + input.cursorOffset = fullText.length + setStore("prompt", { input: fullText, parts }) + restoreExtmarksFromParts(parts) + setStore("editingQueuedMessageID", last.id) + }, + }, + { + title: "Discard queued message", + category: "Queue", + keybind: "queue_discard", + value: "queue.discard", + disabled: queuedMessages().length === 0, + onSelect: async (dialog) => { + dialog.clear() + if (!props.sessionID) return + const messageID = store.editingQueuedMessageID ?? queuedMessages().at(-1)?.id + if (!messageID) return + await sdk.client.session.cancelQueue({ + sessionID: props.sessionID, + messageID, + }) + if (store.editingQueuedMessageID) { + input.clear() + input.extmarks.clear() + setStore("prompt", { input: "", parts: [] }) + setStore("extmarkToPartIndex", new Map()) + setStore("editingQueuedMessageID", undefined) + } + }, + }, ] }) @@ -315,6 +402,24 @@ export function Prompt(props: PromptProps) { if (props.visible === false) input?.blur() }) + // Clear editing state if the message being edited is no longer in the queue (was processed) + createEffect(() => { + if (!store.editingQueuedMessageID) return + const stillQueued = queuedMessages().some((m) => m.id === store.editingQueuedMessageID) + if (!stillQueued) { + input.clear() + input.extmarks.clear() + setStore("prompt", { input: "", parts: [] }) + setStore("extmarkToPartIndex", new Map()) + setStore("editingQueuedMessageID", undefined) + toast.show({ + variant: "info", + message: "Queued message was processed", + duration: 3000, + }) + } + }) + onMount(() => { promptPartTypeId = input.extmarks.registerType("prompt-part") }) @@ -459,6 +564,9 @@ export function Prompt(props: PromptProps) { get current() { return store.prompt }, + get editingQueuedMessageID() { + return store.editingQueuedMessageID + }, focus() { input.focus() }, @@ -479,6 +587,7 @@ export function Prompt(props: PromptProps) { parts: [], }) setStore("extmarkToPartIndex", new Map()) + setStore("editingQueuedMessageID", undefined) }, submit() { submit() @@ -567,6 +676,12 @@ export function Prompt(props: PromptProps) { })), }) } else { + if (store.editingQueuedMessageID) { + await sdk.client.session.cancelQueue({ + sessionID, + messageID: store.editingQueuedMessageID, + }) + } sdk.client.session.prompt({ sessionID, ...selectedModel, @@ -597,6 +712,7 @@ export function Prompt(props: PromptProps) { parts: [], }) setStore("extmarkToPartIndex", new Map()) + setStore("editingQueuedMessageID", undefined) props.onSubmit?.() // temporary hack to make sure the message is sent @@ -804,9 +920,19 @@ export function Prompt(props: PromptProps) { parts: [], }) setStore("extmarkToPartIndex", new Map()) + setStore("editingQueuedMessageID", undefined) return } if (keybind.match("app_exit", e)) { + if (store.editingQueuedMessageID) { + input.clear() + input.extmarks.clear() + setStore("prompt", { input: "", parts: [] }) + setStore("extmarkToPartIndex", new Map()) + setStore("editingQueuedMessageID", undefined) + e.preventDefault() + return + } if (store.prompt.input === "") { await exit() // Don't preventDefault - let textarea potentially handle the event 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 dbd6743c89a..cf758ac34a9 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -1009,6 +1009,7 @@ export function Session() { message={message as UserMessage} parts={sync.data.part[message.id] ?? []} pending={pending()} + editingMessageID={prompt?.editingQueuedMessageID} /> @@ -1076,15 +1077,18 @@ function UserMessage(props: { onMouseUp: () => void index: number pending?: string + editingMessageID?: string }) { const ctx = use() const local = useLocal() + const keybind = useKeybind() const text = createMemo(() => props.parts.flatMap((x) => (x.type === "text" && !x.synthetic ? [x] : []))[0]) const files = createMemo(() => props.parts.flatMap((x) => (x.type === "file" ? [x] : []))) const sync = useSync() const { theme } = useTheme() const [hover, setHover] = createSignal(false) const queued = createMemo(() => props.pending && props.message.id > props.pending) + const editing = createMemo(() => props.editingMessageID === props.message.id) const color = createMemo(() => (queued() ? theme.accent : local.agent.color(props.message.agent))) const metadataVisible = createMemo(() => queued() || ctx.showTimestamps()) @@ -1148,8 +1152,24 @@ function UserMessage(props: { > QUEUED - - + + {" "} + {keybind.print("queue_edit")} edit · {keybind.print("queue_discard")} discard + + } + > + + EDITING + + {" "} + enter submit · ctrl+c cancel · {keybind.print("queue_discard")} discard + + + + diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index be234948424..3f9e136ef6e 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -692,6 +692,8 @@ export namespace Config { .describe("Delete word backward in input"), history_previous: z.string().optional().default("up").describe("Previous history item"), history_next: z.string().optional().default("down").describe("Next history item"), + queue_edit: z.string().optional().default("i").describe("Edit queued message"), + queue_discard: z.string().optional().default("d").describe("Discard queued message"), session_child_cycle: z.string().optional().default("right").describe("Next child session"), session_child_cycle_reverse: z.string().optional().default("left").describe("Previous child session"), session_parent: z.string().optional().default("up").describe("Go to parent session"), diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index c7baec778c6..c032acad2e7 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -72,26 +72,26 @@ export namespace Server { } const app = new Hono() - export const App: () => Hono = lazy( - () => - app - .onError((err, c) => { - log.error("failed", { - error: err, - }) - if (err instanceof NamedError) { - let status: ContentfulStatusCode - if (err instanceof Storage.NotFoundError) status = 404 - else if (err instanceof Provider.ModelNotFoundError) status = 400 - else if (err.name.startsWith("Worktree")) status = 400 - else status = 500 - return c.json(err.toObject(), { status }) - } - const message = err instanceof Error && err.stack ? err.stack : err.toString() - return c.json(new NamedError.Unknown({ message }).toObject(), { - status: 500, - }) + export const App = lazy(() => + // @ts-expect-error Type instantiation is excessively deep - known TypeScript limitation with Hono + app + .onError((err, c) => { + log.error("failed", { + error: err, + }) + if (err instanceof NamedError) { + let status: ContentfulStatusCode + if (err instanceof Storage.NotFoundError) status = 404 + else if (err instanceof Provider.ModelNotFoundError) status = 400 + else if (err.name.startsWith("Worktree")) status = 400 + else status = 500 + return c.json(err.toObject(), { status }) + } + const message = err instanceof Error && err.stack ? err.stack : err.toString() + return c.json(new NamedError.Unknown({ message }).toObject(), { + status: 500, }) + }) .use(async (c, next) => { const skipLogging = c.req.path === "/log" if (!skipLogging) { @@ -1042,15 +1042,106 @@ export namespace Server { sessionID: z.string(), }), ), - async (c) => { - SessionPrompt.cancel(c.req.valid("param").sessionID) - return c.json(true) - }, - ) + async (c) => { + SessionPrompt.cancel(c.req.valid("param").sessionID) + return c.json(true) + }, + ) + .get( + "/session/:sessionID/queue", + describeRoute({ + summary: "Get queued messages", + description: "Get list of message IDs that are queued and waiting to be processed.", + operationId: "session.queue", + responses: { + 200: { + description: "List of queued message IDs", + content: { + "application/json": { + schema: resolver(z.array(z.string())), + }, + }, + }, + ...errors(400, 404), + }, + }), + validator( + "param", + z.object({ + sessionID: z.string(), + }), + ), + async (c) => { + const queued = SessionPrompt.queued(c.req.valid("param").sessionID) + return c.json(queued) + }, + ) + .get( + "/session/:sessionID/queue/:messageID", + describeRoute({ + summary: "Get queued message", + description: "Get a queued message without removing it from the queue.", + operationId: "session.getQueue", + responses: { + 200: { + description: "Queued message with parts", + content: { + "application/json": { + schema: resolver(MessageV2.WithParts.nullable()), + }, + }, + }, + ...errors(400, 404), + }, + }), + validator( + "param", + z.object({ + sessionID: z.string(), + messageID: z.string(), + }), + ), + async (c) => { + const { sessionID, messageID } = c.req.valid("param") + const message = await SessionPrompt.getQueued(sessionID, messageID) + return c.json(message ?? null) + }, + ) + .delete( + "/session/:sessionID/queue/:messageID", + describeRoute({ + summary: "Cancel queued message", + description: "Cancel a queued message that has not yet been processed by the agent.", + operationId: "session.cancelQueue", + responses: { + 200: { + description: "Cancelled message with parts", + content: { + "application/json": { + schema: resolver(MessageV2.WithParts.nullable()), + }, + }, + }, + ...errors(400, 404), + }, + }), + validator( + "param", + z.object({ + sessionID: z.string(), + messageID: z.string(), + }), + ), + async (c) => { + const { sessionID, messageID } = c.req.valid("param") + const message = await SessionPrompt.cancelQueued(sessionID, messageID) + return c.json(message ?? null) + }, + ) - .post( - "/session/:sessionID/share", - describeRoute({ + .post( + "/session/:sessionID/share", + describeRoute({ summary: "Share session", description: "Create a shareable link for a session, allowing others to view the conversation.", operationId: "session.share", @@ -2831,8 +2922,8 @@ export namespace Server { ) export async function openapi() { - // Cast to break excessive type recursion from long route chains - const result = await generateSpecs(App() as Hono, { + // @ts-expect-error Type instantiation is excessively deep - known TypeScript limitation with Hono + const result = await generateSpecs(App(), { documentation: { info: { title: "opencode", diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index f891612272c..94c249dad0e 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -59,6 +59,7 @@ export namespace SessionPrompt { { abort: AbortController callbacks: { + messageID: string resolve(input: MessageV2.WithParts): void reject(): void }[] @@ -175,7 +176,7 @@ export namespace SessionPrompt { return message } - return loop(input.sessionID) + return loop(input.sessionID, message.info.id) }) export async function resolvePromptParts(template: string): Promise { @@ -254,12 +255,51 @@ export namespace SessionPrompt { return } - export const loop = fn(Identifier.schema("session"), async (sessionID) => { + export function queued(sessionID: string) { + const s = state() + const match = s[sessionID] + if (!match) return [] + return match.callbacks.map((c) => c.messageID).filter(Boolean) + } + + export async function getQueued(sessionID: string, messageID: string) { + const s = state() + const match = s[sessionID] + if (!match) return + + const index = match.callbacks.findIndex((c) => c.messageID === messageID) + if (index === -1) return + + const messages = await Session.messages({ sessionID }) + return messages.find((m) => m.info.id === messageID) + } + + export async function cancelQueued(sessionID: string, messageID: string) { + log.info("cancelQueued", { sessionID, messageID }) + const s = state() + const match = s[sessionID] + if (!match) return + + const index = match.callbacks.findIndex((c) => c.messageID === messageID) + if (index === -1) return + + const [removed] = match.callbacks.splice(index, 1) + removed.reject() + + const messages = await Session.messages({ sessionID }) + const message = messages.find((m) => m.info.id === messageID) + if (!message) return + + await Session.removeMessage({ sessionID, messageID }) + return message + } + + export async function loop(sessionID: string, messageID?: string) { const abort = start(sessionID) if (!abort) { return new Promise((resolve, reject) => { const callbacks = state()[sessionID].callbacks - callbacks.push({ resolve, reject }) + callbacks.push({ messageID: messageID!, resolve, reject }) }) } @@ -629,7 +669,7 @@ export namespace SessionPrompt { return item } throw new Error("Impossible") - }) + } async function lastModel(sessionID: string) { for await (const item of MessageV2.stream(sessionID)) { diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index b52f3ef7f77..6288d8da833 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -915,6 +915,74 @@ test("permission config preserves key order", async () => { }) }) +test("keybinds queue_edit defaults to i", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.keybinds?.queue_edit).toBe("i") + }, + }) +}) + +test("keybinds queue_discard defaults to d", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.keybinds?.queue_discard).toBe("d") + }, + }) +}) + +test("keybinds queue_edit can be customized", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + keybinds: { + queue_edit: "ctrl+e", + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.keybinds?.queue_edit).toBe("ctrl+e") + }, + }) +}) + +test("keybinds queue_discard can be customized", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + keybinds: { + queue_discard: "ctrl+shift+d", + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const config = await Config.get() + expect(config.keybinds?.queue_discard).toBe("ctrl+shift+d") + }, + }) +}) + // MCP config merging tests test("project config can override MCP server enabled status", async () => { diff --git a/packages/opencode/test/session/queue.test.ts b/packages/opencode/test/session/queue.test.ts new file mode 100644 index 00000000000..83e116017e1 --- /dev/null +++ b/packages/opencode/test/session/queue.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, test } from "bun:test" +import path from "path" +import { SessionPrompt } from "../../src/session/prompt" +import { Log } from "../../src/util/log" +import { Instance } from "../../src/project/instance" + +const projectRoot = path.join(__dirname, "../..") +Log.init({ print: false }) + +describe("SessionPrompt.queued", () => { + test("returns empty array for non-existent session", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const result = SessionPrompt.queued("non-existent-session-id") + expect(result).toEqual([]) + }, + }) + }) +}) + +describe("SessionPrompt.getQueued", () => { + test("returns undefined for non-existent session", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const result = await SessionPrompt.getQueued("non-existent-session-id", "non-existent-message-id") + expect(result).toBeUndefined() + }, + }) + }) +}) + +describe("SessionPrompt.cancelQueued", () => { + test("returns undefined for non-existent session", async () => { + await Instance.provide({ + directory: projectRoot, + fn: async () => { + const result = await SessionPrompt.cancelQueued("non-existent-session-id", "non-existent-message-id") + expect(result).toBeUndefined() + }, + }) + }) +}) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index dae865a7cfc..c627ec583c6 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -91,6 +91,8 @@ import type { QuestionReplyResponses, SessionAbortErrors, SessionAbortResponses, + SessionCancelQueueErrors, + SessionCancelQueueResponses, SessionChildrenErrors, SessionChildrenResponses, SessionCommandErrors, @@ -103,6 +105,8 @@ import type { SessionDiffResponses, SessionForkResponses, SessionGetErrors, + SessionGetQueueErrors, + SessionGetQueueResponses, SessionGetResponses, SessionInitErrors, SessionInitResponses, @@ -115,6 +119,8 @@ import type { SessionPromptAsyncResponses, SessionPromptErrors, SessionPromptResponses, + SessionQueueErrors, + SessionQueueResponses, SessionRevertErrors, SessionRevertResponses, SessionShareErrors, @@ -1133,6 +1139,102 @@ export class Session extends HeyApiClient { }) } + /** + * Get queued messages + * + * Get list of message IDs that are queued and waiting to be processed. + */ + public queue( + 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).get({ + url: "/session/{sessionID}/queue", + ...options, + ...params, + }) + } + + /** + * Cancel queued message + * + * Cancel a queued message that has not yet been processed by the agent. + */ + public cancelQueue( + parameters: { + sessionID: string + messageID: string + directory?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "messageID" }, + { in: "query", key: "directory" }, + ], + }, + ], + ) + return (options?.client ?? this.client).delete( + { + url: "/session/{sessionID}/queue/{messageID}", + ...options, + ...params, + }, + ) + } + + /** + * Get queued message + * + * Get a queued message without removing it from the queue. + */ + public getQueue( + parameters: { + sessionID: string + messageID: string + directory?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "path", key: "sessionID" }, + { in: "path", key: "messageID" }, + { in: "query", key: "directory" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/session/{sessionID}/queue/{messageID}", + ...options, + ...params, + }) + } + /** * Unshare session * diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index ea86b022daf..9d359fa699e 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1223,6 +1223,14 @@ export type KeybindsConfig = { * Next history item */ history_next?: string + /** + * Edit queued message + */ + queue_edit?: string + /** + * Discard queued message + */ + queue_discard?: string /** * Next child session */ @@ -2931,6 +2939,113 @@ export type SessionAbortResponses = { export type SessionAbortResponse = SessionAbortResponses[keyof SessionAbortResponses] +export type SessionQueueData = { + body?: never + path: { + sessionID: string + } + query?: { + directory?: string + } + url: "/session/{sessionID}/queue" +} + +export type SessionQueueErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionQueueError = SessionQueueErrors[keyof SessionQueueErrors] + +export type SessionQueueResponses = { + /** + * List of queued message IDs + */ + 200: Array +} + +export type SessionQueueResponse = SessionQueueResponses[keyof SessionQueueResponses] + +export type SessionCancelQueueData = { + body?: never + path: { + sessionID: string + messageID: string + } + query?: { + directory?: string + } + url: "/session/{sessionID}/queue/{messageID}" +} + +export type SessionCancelQueueErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionCancelQueueError = SessionCancelQueueErrors[keyof SessionCancelQueueErrors] + +export type SessionCancelQueueResponses = { + /** + * Cancelled message with parts + */ + 200: { + info: Message + parts: Array + } | null +} + +export type SessionCancelQueueResponse = SessionCancelQueueResponses[keyof SessionCancelQueueResponses] + +export type SessionGetQueueData = { + body?: never + path: { + sessionID: string + messageID: string + } + query?: { + directory?: string + } + url: "/session/{sessionID}/queue/{messageID}" +} + +export type SessionGetQueueErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type SessionGetQueueError = SessionGetQueueErrors[keyof SessionGetQueueErrors] + +export type SessionGetQueueResponses = { + /** + * Queued message with parts + */ + 200: { + info: Message + parts: Array + } | null +} + +export type SessionGetQueueResponse = SessionGetQueueResponses[keyof SessionGetQueueResponses] + export type SessionUnshareData = { body?: never path: { diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 13346a625d9..45d5fe12450 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -1691,6 +1691,247 @@ ] } }, + "/session/{sessionID}/queue": { + "get": { + "operationId": "session.queue", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "Get queued messages", + "description": "Get list of message IDs that are queued and waiting to be processed.", + "responses": { + "200": { + "description": "List of queued message IDs", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.queue({\n ...\n})" + } + ] + } + }, + "/session/{sessionID}/queue/{messageID}": { + "get": { + "operationId": "session.getQueue", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string" + }, + "required": true + }, + { + "in": "path", + "name": "messageID", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "Get queued message", + "description": "Get a queued message without removing it from the queue.", + "responses": { + "200": { + "description": "Queued message with parts", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/Message" + }, + "parts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Part" + } + } + }, + "required": ["info", "parts"] + }, + { + "type": "null" + } + ] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.getQueue({\n ...\n})" + } + ] + }, + "delete": { + "operationId": "session.cancelQueue", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string" + }, + "required": true + }, + { + "in": "path", + "name": "messageID", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "Cancel queued message", + "description": "Cancel a queued message that has not yet been processed by the agent.", + "responses": { + "200": { + "description": "Cancelled message with parts", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/Message" + }, + "parts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Part" + } + } + }, + "required": ["info", "parts"] + }, + { + "type": "null" + } + ] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.session.cancelQueue({\n ...\n})" + } + ] + } + }, "/session/{sessionID}/share": { "post": { "operationId": "session.share", @@ -8479,6 +8720,16 @@ "default": "down", "type": "string" }, + "queue_edit": { + "description": "Edit queued message", + "default": "i", + "type": "string" + }, + "queue_discard": { + "description": "Discard queued message", + "default": "d", + "type": "string" + }, "session_child_cycle": { "description": "Next child session", "default": "right",