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 f8c9de0ba2
commit 401dbd7e05
12 changed files with 570 additions and 64 deletions
@@ -5,7 +5,7 @@ import fuzzysort from "fuzzysort"
import { createEffect, createMemo, createSignal, type Accessor } from "solid-js"
import { RunFooterMenu, createFooterMenuState, type RunFooterMenuItem } from "./footer.menu"
import type { RunFooterTheme } from "./theme"
import type { FooterSubagentTab, RunCommand, RunInput, RunProvider } from "./types"
import type { FooterQueuedPrompt, FooterSubagentTab, RunCommand, RunInput, RunProvider } from "./types"
type PanelEntry = RunFooterMenuItem & {
category: string
@@ -14,6 +14,7 @@ type PanelEntry = RunFooterMenuItem & {
type CommandEntry =
| (PanelEntry & { action: "model" })
| (PanelEntry & { action: "queued" })
| (PanelEntry & { action: "subagent" })
| (PanelEntry & { action: "variant.cycle" })
| (PanelEntry & { action: "variant.list" })
@@ -37,6 +38,10 @@ type SubagentEntry = PanelEntry & {
current: boolean
}
type QueuedEntry = PanelEntry & {
prompt: FooterQueuedPrompt
}
type MenuState = ReturnType<typeof createFooterMenuState>
const PANEL_PAD = 2
@@ -294,11 +299,13 @@ export function RunCommandMenuBody(props: {
theme: Accessor<RunFooterTheme>
commands: Accessor<RunCommand[] | undefined>
subagents: Accessor<FooterSubagentTab[]>
queued: Accessor<FooterQueuedPrompt[]>
variants: Accessor<string[]>
variantCycle: string
onClose: () => void
onModel: () => void
onSubagent: () => void
onQueued: () => void
onVariant: () => void
onVariantCycle: () => void
onCommand: (name: string) => void
@@ -315,6 +322,17 @@ export function RunCommandMenuBody(props: {
category: "Suggested",
display: "Switch model",
},
...(props.queued().length > 0
? [
{
action: "queued" as const,
category: "Suggested",
display: "Manage queued prompts",
footer: `${props.queued().length} queued`,
keywords: props.queued().map((item) => item.prompt.text).join(" "),
},
]
: []),
...(props.subagents().length > 0
? [
{
@@ -387,6 +405,11 @@ export function RunCommandMenuBody(props: {
return
}
if (item.action === "queued") {
props.onQueued()
return
}
if (item.action === "variant.cycle") {
props.onVariantCycle()
return
@@ -559,6 +582,102 @@ export function RunSubagentSelectBody(props: {
)
}
export function RunQueuedPromptSelectBody(props: {
theme: Accessor<RunFooterTheme>
prompts: Accessor<FooterQueuedPrompt[]>
onClose: () => void
onEdit: (prompt: FooterQueuedPrompt) => void | Promise<void>
onDelete: (prompt: FooterQueuedPrompt) => void | Promise<void>
onRows?: (rows: number) => void
}) {
let field: InputRenderable | undefined
const [query, setQuery] = createSignal("")
const entries = createMemo<QueuedEntry[]>(() =>
props.prompts().map((prompt) => ({
category: "",
display: prompt.prompt.text.replaceAll("\n", " "),
footer: "queued · ctrl+e edit · ctrl+d remove",
keywords: prompt.prompt.text,
prompt,
})),
)
const items = createMemo<QueuedEntry[]>(() => match(query(), entries()))
const menu = createFooterMenuState({ count: () => items().length, limit: SUBAGENT_LIST_ROWS })
const selected = () => items()[menu.selected()]
createEffect(() => {
query()
menu.reset()
})
createEffect(() => {
props.onRows?.(menu.rows() + PANEL_FRAME_ROWS)
})
useKeyboard((event) => {
if (event.defaultPrevented) {
return
}
const item = selected()
const ctrl = event.ctrl && !event.meta && !event.shift && !event.super
if (item && (event.name === "delete" || (ctrl && event.name === "d"))) {
event.preventDefault()
props.onDelete(item.prompt)
return
}
if (item && ctrl && event.name === "e") {
event.preventDefault()
props.onEdit(item.prompt)
return
}
handleKey({
event,
menu,
field: () => field,
setQuery,
select: () => {
const item = selected()
if (item) props.onEdit(item.prompt)
},
close: props.onClose,
})
})
return (
<PanelShell
id="run-direct-footer-queued-panel"
title="Queued prompts"
query={query()}
count={items().length}
total={entries().length}
placeholder="Search"
theme={props.theme}
inputRef={(input) => {
field = input
}}
onQuery={setQuery}
>
<RunFooterMenu
id="run-direct-footer-queued-list"
theme={props.theme}
items={items}
selected={menu.selected}
offset={menu.offset}
rows={menu.rows}
limit={SUBAGENT_LIST_ROWS}
empty="No queued prompts"
border={false}
paddingLeft={PANEL_PAD}
paddingRight={PANEL_PAD}
grouped={false}
/>
</PanelShell>
)
}
export function RunVariantSelectBody(props: {
theme: Accessor<RunFooterTheme>
variants: Accessor<string[]>