opencode(run): add queued prompt management (#30103)

Direct run mode previously made submitted follow-up prompts irrevocable while a response was still running. Let users edit or remove queued prompts before dispatch without interrupting the active turn.
This commit is contained in:
Simon Klee
2026-06-01 09:26:09 +02:00
committed by GitHub
parent f9ba23ab62
commit 7ccb7889af
12 changed files with 570 additions and 64 deletions

View File

@@ -4,6 +4,7 @@ import type { FooterApi, FooterEvent, RunPrompt, StreamCommit } from "@/cli/cmd/
function footer() {
const prompts = new Set<(input: RunPrompt) => void>()
const queuedRemoves = new Set<(messageID: string) => void>()
const closes = new Set<() => void>()
const events: FooterEvent[] = []
const commits: StreamCommit[] = []
@@ -19,6 +20,12 @@ function footer() {
prompts.delete(fn)
}
},
onQueuedRemove(fn) {
queuedRemoves.add(fn)
return () => {
queuedRemoves.delete(fn)
}
},
onClose(fn) {
if (closed) {
fn()
@@ -66,6 +73,9 @@ function footer() {
fn(next)
}
},
removeQueued(messageID: string) {
for (const fn of [...queuedRemoves]) fn(messageID)
},
}
}
@@ -289,6 +299,82 @@ describe("run runtime queue", () => {
expect(seen).toEqual(["one", "two"])
})
test("exposes ordinary in-flight prompts for removal before sending", async () => {
const ui = footer()
const turns: RunPrompt[] = []
let wake: (() => void) | undefined
const gate = new Promise<void>((resolve) => {
wake = resolve
})
const task = runPromptQueue({
footer: ui.api,
run: async (input) => {
turns.push(input)
await gate
},
})
ui.submit("one")
ui.submit("two")
await Promise.resolve()
await Promise.resolve()
expect(turns.map((item) => item.text)).toEqual(["one"])
expect(turns[0]?.messageID).toBeUndefined()
expect(ui.commits.map((item) => item.text)).toEqual(["one"])
const first = ui.events.find((item) => item.type === "queued.prompts")
const event = ui.events.findLast((item) => item.type === "queued.prompts")
expect(first?.type === "queued.prompts" ? first.prompts : []).toEqual([])
expect(first?.type === "queued.prompts" && event?.type === "queued.prompts" ? first.prompts === event.prompts : true).toBe(
false,
)
expect(ui.events.findLast((item) => item.type === "queue")).toEqual({ type: "queue", queue: 1 })
expect(event?.type === "queued.prompts" ? event.prompts.map((item) => item.prompt.text) : []).toEqual(["two"])
if (event?.type === "queued.prompts") ui.removeQueued(event.prompts[0]!.messageID)
await Promise.resolve()
wake?.()
ui.api.close()
await task
expect(turns.map((item) => item.text)).toEqual(["one"])
})
test("removing one managed queued prompt preserves the others", async () => {
const ui = footer()
const turns: string[] = []
let wake: (() => void) | undefined
const gate = new Promise<void>((resolve) => {
wake = resolve
})
const task = runPromptQueue({
footer: ui.api,
run: async (input) => {
turns.push(input.text)
if (input.text === "active") await gate
if (input.text === "queued three") ui.api.close()
},
})
ui.submit("active")
ui.submit("queued one")
ui.submit("queued two")
ui.submit("queued three")
await Promise.resolve()
await Promise.resolve()
const event = ui.events.findLast((item) => item.type === "queued.prompts")
if (event?.type === "queued.prompts") {
const second = event.prompts.find((item) => item.prompt.text === "queued two")
if (second) ui.removeQueued(second.messageID)
}
wake?.()
await task
expect(turns).toEqual(["active", "queued one", "queued three"])
})
test("drains a prompt queued during an in-flight turn", async () => {
const ui = footer()
const seen: string[] = []