Compare commits

...

3 Commits

Author SHA1 Message Date
Kit Langton 98622d247a fix(tui): dock queued prompts by composer 2026-08-06 20:56:54 +00:00
Kit Langton 9d348a7f39 feat(tui): collapse large prompt queues 2026-08-06 20:49:01 +00:00
Kit Langton d4686f247b feat(tui): queue prompts with option enter 2026-08-06 20:26:05 +00:00
17 changed files with 242 additions and 58 deletions
+44 -3
View File
@@ -157,6 +157,7 @@ export function Prompt(props: PromptProps) {
const route = useRoute()
const data = useData()
const keymapCommands = Keymap.useCommands()
const queueShortcut = Keymap.useShortcut("prompt.queue")
const currentLocation = useLocation()
const config = useConfig().data
const dialog = useDialog()
@@ -361,6 +362,20 @@ export function Prompt(props: PromptProps) {
dialog.clear()
},
},
{
title: "Queue prompt",
name: "prompt.queue",
category: "Prompt",
palette: undefined,
run: async (_input: string | undefined, event?: KeyEvent) => {
event?.preventDefault()
event?.stopPropagation()
if (!input.focused) return
const handled = await submit("queue")
if (!handled) return
dialog.clear()
},
},
{
title: "Remove editor context",
name: "prompt.editor_context.clear",
@@ -519,6 +534,11 @@ export function Prompt(props: PromptProps) {
commands: promptCommands(),
}))
Keymap.createLayer(() => ({
priority: 1,
bindings: ["prompt.queue"],
}))
Keymap.createLayer(() => ({
bindings: [
"prompt.submit",
@@ -904,7 +924,7 @@ export function Prompt(props: PromptProps) {
})
let submitting = false
async function submit() {
async function submit(delivery: "steer" | "queue" = "steer") {
// Prevent overlapping invocations (e.g. a double-pressed Enter, or the
// input's native onSubmit racing another dispatch). Without this guard,
// a second call slips past the empty-input check before the first call
@@ -914,13 +934,13 @@ export function Prompt(props: PromptProps) {
if (submitting) return false
submitting = true
try {
return await submitInner()
return await submitInner(delivery)
} finally {
submitting = false
}
}
async function submitInner() {
async function submitInner(delivery: "steer" | "queue") {
// IME: double-defer may fire before onContentChange flushes the last
// composed character (e.g. Korean hangul) to the store, so read
// plainText directly and sync before any downstream reads.
@@ -933,12 +953,20 @@ export function Prompt(props: PromptProps) {
if (auto()?.visible) return false
if (!store.prompt.text) return false
const trimmed = store.prompt.text.trim()
if (delivery === "queue" && (store.mode === "shell" || trimmed === "exit" || trimmed === "quit" || trimmed === ":q")) {
toast.show({ message: "This prompt cannot be queued", variant: "warning" })
return false
}
if (trimmed === "exit" || trimmed === "quit" || trimmed === ":q") {
void exit()
return true
}
const slash = argumentSlash(store.prompt.text, keymapCommands())
if (slash) {
if (delivery === "queue") {
toast.show({ message: "This prompt cannot be queued", variant: "warning" })
return false
}
clearPrompt()
await slash.command.run(slash.input)
return true
@@ -1036,6 +1064,7 @@ export function Prompt(props: PromptProps) {
model: { providerID: selectedModel.providerID, id: selectedModel.modelID, variant },
files: store.prompt.files,
agents: store.prompt.agents,
delivery,
})
.catch((error) => {
toast.show({ title: "Failed to run command", message: errorMessage(error), variant: "error" })
@@ -1046,6 +1075,10 @@ export function Prompt(props: PromptProps) {
(skill) => skill.slash === true && skill.id === inputText.split("\n")[0].split(" ")[0].slice(1),
)
) {
if (delivery === "queue") {
toast.show({ message: "Skills cannot be queued", variant: "warning" })
return false
}
move.startSubmit()
void client.api.session.skill({
sessionID,
@@ -1103,6 +1136,7 @@ export function Prompt(props: PromptProps) {
text: inputText,
files: store.prompt.files,
agents: store.prompt.agents,
delivery,
})
.then(
() => undefined,
@@ -1575,6 +1609,13 @@ export function Prompt(props: PromptProps) {
{store.interrupt > 0 ? "again to interrupt" : "interrupt"}
</span>
</text>
<Show when={queueShortcut()}>
{(shortcut) => (
<text fg={theme.text.default} wrapMode="none" flexShrink={0}>
{shortcut()} <span style={{ fg: theme.text.subdued }}>queue</span>
</text>
)}
</Show>
</box>
</Match>
<Match when={move.progress()}>
+3 -1
View File
@@ -161,6 +161,7 @@ export const Definitions = {
display_thinking: keybind("none", "Toggle thinking blocks visibility"),
prompt_submit: keybind("none", "Submit prompt"),
prompt_queue: keybind("alt+return", "Queue prompt"),
prompt_editor_context_clear: keybind("none", "Clear editor context"),
prompt_skills: keybind("none", "Open skill selector"),
prompt_stash: keybind("none", "Stash prompt"),
@@ -170,7 +171,7 @@ export const Definitions = {
input_clear: keybind("ctrl+c", "Clear input field"),
input_paste: keybind({ key: "ctrl+v", preventDefault: false }, "Paste from clipboard"),
input_submit: keybind("return", "Submit input"),
input_newline: keybind("shift+return,ctrl+return,alt+return,ctrl+j", "Insert newline in input"),
input_newline: keybind("shift+return,ctrl+return,ctrl+j", "Insert newline in input"),
input_move_left: keybind("left,ctrl+b", "Move cursor left in input"),
input_move_right: keybind("right,ctrl+f", "Move cursor right in input"),
input_move_up: keybind("up", "Move cursor up in input"),
@@ -359,6 +360,7 @@ export const CommandMap = {
messages_redo: "session.redo",
display_thinking: "session.toggle.thinking",
prompt_submit: "prompt.submit",
prompt_queue: "prompt.queue",
prompt_editor_context_clear: "prompt.editor_context.clear",
prompt_skills: "prompt.skills",
prompt_stash: "prompt.stash",
+22 -4
View File
@@ -19,6 +19,7 @@ import {
displayCharAt,
displaySlice,
isExitCommand,
isCompactCommand,
mentionTriggerIndex,
isNewCommand,
movePromptHistory,
@@ -980,8 +981,18 @@ export function createPromptState(input: PromptInput): PromptState {
}))
Keymap.createLayer(() => ({
priority: 1,
enabled: input.prompt() && !visible(),
commands: [
{
id: "prompt.queue",
title: "Queue prompt",
group: "Prompt",
run() {
syncDraft()
submitPrompt(promptCopy(draft), "queue")
},
},
{
id: "prompt.editor",
title: "Open editor",
@@ -1116,7 +1127,7 @@ export function createPromptState(input: PromptInput): PromptState {
}
}
const submitPrompt = (next: RunPrompt) => {
const submitPrompt = (next: RunPrompt, delivery: "steer" | "queue" = "steer") => {
if (!area || area.isDestroyed) {
draft = promptCopy(next)
}
@@ -1136,6 +1147,13 @@ export function createPromptState(input: PromptInput): PromptState {
}
const command = next.mode === "shell" ? undefined : selectedCommand(next.text, next.command)
if (
delivery === "queue" &&
(next.mode === "shell" || command?.source === "skill" || isNewCommand(next.text) || isCompactCommand(next.text))
) {
input.onStatus("this prompt cannot be queued")
return
}
if (!command && next.mode !== "shell" && isExitCommand(next.text)) {
input.onExit()
return
@@ -1157,10 +1175,10 @@ export function createPromptState(input: PromptInput): PromptState {
}
const submit = command
? { ...next, command }
? { ...next, command, delivery }
: parsed?.type === "command"
? { ...next, command: parsed.command }
: next
? { ...next, command: parsed.command, delivery }
: { ...next, delivery }
const shellMode = next.mode === "shell"
resetDraft()
+4
View File
@@ -185,6 +185,7 @@ export function RunFooterView(props: RunFooterViewProps) {
const command = () => shortcut("command.palette.show")
const subagentShortcut = () => shortcut("session.child.first")
const queuedShortcut = () => shortcut("session.queued_prompts")
const queueShortcut = () => shortcut("prompt.queue")
const backgroundShortcut = () => shortcut("session.background")
const subagentInterruptShortcut = () => shortcut("subagent.interrupt")
const interrupt = () => shortcut("session.interrupt")
@@ -457,6 +458,9 @@ export function RunFooterView(props: RunFooterViewProps) {
if (activeTabs().length > 0 && subagentShortcut()) {
items.push({ key: subagentShortcut(), label: "subagents" })
}
if (busy() && queueShortcut()) {
items.push({ key: queueShortcut(), label: "queue" })
}
return items
})
+5 -4
View File
@@ -25,7 +25,7 @@ export type QueueInput = {
onAdmissionError?: (prompt: RunPrompt, error: unknown) => void | Promise<void>
onNewSession?: () => void | Promise<void>
onCompact?: () => void | Promise<void>
admit: (prompt: RunPrompt, signal: AbortSignal) => Promise<void>
admit: (prompt: RunPrompt, delivery: "steer" | "queue", signal: AbortSignal) => Promise<void>
settle: () => Promise<void>
run: (prompt: RunPrompt, signal: AbortSignal, admitted: () => void) => Promise<void>
}
@@ -183,7 +183,7 @@ export async function runPromptQueue(input: QueueInput): Promise<void> {
input.trace?.write("ui.commit", commit)
input.footer.append(commit)
}
input.onSend?.(sent, "steer")
input.onSend?.(sent, sent.delivery ?? "steer")
if (state.closed) {
break
@@ -276,10 +276,11 @@ export async function runPromptQueue(input: QueueInput): Promise<void> {
const sent = { ...prompt, messageID: SessionMessage.ID.create() }
const admission = state.admission
admissionVersion += 1
input.onSend?.(sent, "queue")
const delivery = prompt.delivery ?? "queue"
input.onSend?.(sent, delivery)
admissions = admissions
.then(() => admission)
.then(() => input.admit(sent, admissionController.signal))
.then(() => input.admit(sent, delivery, admissionController.signal))
.catch((error) => (state.closed ? undefined : input.onAdmissionError?.(sent, error)))
return
}
+14 -11
View File
@@ -892,7 +892,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
trace: log,
onSend: (prompt, delivery) => {
state.shown = true
state.history.push(prompt)
state.history.push({ ...prompt, delivery: undefined })
if (prompt.mode !== "shell" && delivery === "steer") {
rememberLocal({
kind: "user",
@@ -903,18 +903,21 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
})
}
},
admit: async (prompt, signal) => {
admit: async (prompt, delivery, signal) => {
await state.switching?.catch(() => {})
const next = await ensureStream()
await next.handle.queuePromptTurn({
agent: state.agent,
model: state.model,
variant: state.activeVariant,
prompt,
files: input.files,
includeFiles: false,
signal,
})
await next.handle.admitPromptTurn(
{
agent: state.agent,
model: state.model,
variant: state.activeVariant,
prompt,
files: input.files,
includeFiles: false,
signal,
},
delivery,
)
},
onAdmissionError: renderPromptError,
onCompact: async () => {
+5 -5
View File
@@ -71,7 +71,7 @@ export type SessionResizeReplayInput = {
export type SessionTransport = {
runPromptTurn(input: SessionTurnInput, admitted?: () => void): Promise<void>
queuePromptTurn(input: SessionTurnInput): Promise<void>
admitPromptTurn(input: SessionTurnInput, delivery: "steer" | "queue"): Promise<void>
waitForIdle(): Promise<void>
interruptActiveTurn(): Promise<void>
selectSubagent(sessionID: string | undefined): void
@@ -1643,14 +1643,14 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
}
return {
async queuePromptTurn(next) {
async admitPromptTurn(next, delivery) {
if (next.prompt.mode === "shell" || next.prompt.command?.source === "skill")
throw new Error("This prompt cannot be queued")
if (!state.connected) throw new Error("Event stream is reconnecting")
const client = sdk
if (next.agent)
await client.session.switchAgent({ sessionID: input.sessionID, agent: next.agent }, { signal: next.signal })
mergePending(await admitPrompt(next, client, "queue"))
mergePending(await admitPrompt(next, client, delivery))
settlementClient = client
},
async waitForIdle() {
@@ -1688,7 +1688,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
return
}
if (command) {
await runTurnWait(next, messageID, client, () => admitPrompt(next, client, "steer"), admitted)
await runTurnWait(next, messageID, client, () => admitPrompt(next, client, next.prompt.delivery ?? "steer"), admitted)
return
}
@@ -1700,7 +1700,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
if (selected)
await client.session.switchModel({ sessionID: input.sessionID, model: selected }, { signal: next.signal })
await runTurnWait(next, messageID, client, () => admitPrompt(next, client, "steer"), admitted)
await runTurnWait(next, messageID, client, () => admitPrompt(next, client, next.prompt.delivery ?? "steer"), admitted)
},
async interruptActiveTurn() {
// A running shell holds no drain, so session.interrupt cannot reach it;
+1
View File
@@ -75,6 +75,7 @@ export type RunPrompt = {
messageID?: string
text: string
parts: RunPromptPart[]
delivery?: "steer" | "queue"
mode?: "shell"
command?: {
name: string
+48 -4
View File
@@ -175,6 +175,11 @@ export function Session() {
.flatMap((sessionID) => data.session.form.list(sessionID) ?? [])
.concat(global)
})
const queuedPrompts = createMemo(() =>
data.session.pending.list(route.sessionID).flatMap((item) =>
item.type === "user" && item.delivery === "queue" ? [{ id: item.id, text: item.data.text }] : [],
),
)
const [composer, setComposer] = createStore({
open: false,
tab: undefined as string | undefined,
@@ -1006,6 +1011,9 @@ export function Session() {
</Show>
</scrollbox>
<box flexShrink={0}>
<Show when={!composer.open && !disabled() && queuedPrompts().length > 0}>
<QueuedPromptDock prompts={queuedPrompts()} />
</Show>
<PluginSlot name="session.composer.top" input={{ sessionID: route.sessionID }} mode="all" />
<Composer
sessionID={route.sessionID}
@@ -1849,9 +1857,10 @@ function UserMessage(props: { message: SessionMessageUser }) {
const mode = themes.mode
const [hover, setHover] = createSignal(false)
const color = createMemo(() => local.agent.color(data.session.get(ctx.sessionID)?.agent ?? "build"))
const queued = createMemo(
() => data.session.status(ctx.sessionID) === "running" && data.session.input.has(ctx.sessionID, props.message.id),
)
const delivery = createMemo(() => {
const pending = data.session.pending.list(ctx.sessionID).find((item) => item.id === props.message.id)
return pending?.type === "user" ? pending.delivery : undefined
})
const dialog = useDialog()
const renderer = useRenderer()
const promptRef = usePromptRef()
@@ -1860,7 +1869,7 @@ function UserMessage(props: { message: SessionMessageUser }) {
<Show when={props.message.text.trim() || files().length}>
<box
border={["left"]}
borderColor={queued() ? theme.border.default : color()}
borderColor={delivery() ? theme.border.default : color()}
customBorderChars={SplitBorder.customBorderChars}
>
<box
@@ -1887,6 +1896,9 @@ function UserMessage(props: { message: SessionMessageUser }) {
flexShrink={0}
>
<text fg={theme.text.default}>{props.message.text}</text>
<Show when={delivery()}>
{(value) => <text fg={theme.text.subdued}>{value() === "queue" ? "queued" : "steering"}</text>}
</Show>
<Show when={files().length}>
<box flexDirection="row" paddingTop={1} gap={1} flexWrap="wrap">
<For each={files()}>
@@ -1919,6 +1931,38 @@ function UserMessage(props: { message: SessionMessageUser }) {
)
}
function QueuedPromptDock(props: { prompts: { id: string; text: string }[] }) {
const theme = useTheme("elevated")
const shortcut = Keymap.useShortcut("command.palette.show")
const next = createMemo(() => props.prompts[0]?.text)
return (
<box
border={["left"]}
borderColor={theme.border.default}
customBorderChars={SplitBorder.customBorderChars}
paddingLeft={2}
paddingRight={1}
backgroundColor={theme.background.default}
flexDirection="row"
justifyContent="space-between"
gap={2}
>
<text fg={theme.text.subdued} wrapMode="none" truncate flexGrow={1} flexShrink={1} minWidth={0}>
<span style={{ fg: theme.text.default }}>{props.prompts.length} queued</span>
<Show when={next()}>{(text) => <> · Next · {text()}</>}</Show>
</text>
<Show when={shortcut()}>
{(key) => (
<text fg={theme.text.subdued} wrapMode="none" flexShrink={0}>
<span style={{ fg: theme.text.default }}>{key()}</span> view all
</text>
)}
</Show>
</box>
)
}
function AssistantRetry(props: { retry: SessionMessageAssistant["retry"] }) {
const theme = useTheme()
return (
+18 -7
View File
@@ -46,6 +46,7 @@ export function createSessionRows(sessionID: Accessor<string>) {
function reduce() {
const messages = data.session.message.list(sessionID())
const inputs = new Set(data.session.input.list(sessionID()))
const pending = data.session.pending.list(sessionID())
const boundary = revertBoundary()
const rows = reduceSessionRows(
boundary ? messages.filter((message) => message.id < boundary) : messages,
@@ -53,12 +54,17 @@ export function createSessionRows(sessionID: Accessor<string>) {
turnTokens(),
)
partitionPending(rows, pendingPermissions())
removeQueuedPrompts(
rows,
pending
.filter((item) => item.type === "user" && item.delivery === "queue")
.map((item) => item.id),
)
const position = rows.findIndex((row) => row.type === "message" && inputs.has(row.messageID))
rows.splice(
position === -1 ? rows.length : position,
0,
...data.session.pending
.list(sessionID())
...pending
.filter((item) => item.type === "compaction")
.map((item): SessionRow => ({ type: "compaction-queued", inputID: item.id })),
)
@@ -112,10 +118,7 @@ export function createSessionRows(sessionID: Accessor<string>) {
createEffect(
on(
() =>
data.session.pending
.list(sessionID())
.filter((item) => item.type === "compaction")
.map((item) => item.id),
data.session.pending.list(sessionID()).map((item) => `${item.id}:${"delivery" in item ? item.delivery : item.type}`),
() => setRows(reconcile(reduce())),
{ defer: true },
),
@@ -196,7 +199,9 @@ export function createSessionRows(sessionID: Accessor<string>) {
const queuedStart = (rows: SessionRow[]) => {
const index = rows.findIndex(
(row) => row.type === "compaction-queued" || (row.type === "message" && isPending(row.messageID)),
(row) =>
row.type === "compaction-queued" ||
(row.type === "message" && isPending(row.messageID)),
)
return index === -1 ? rows.length : index
}
@@ -281,6 +286,12 @@ export function createSessionRows(sessionID: Accessor<string>) {
return rows
}
export function removeQueuedPrompts(rows: SessionRow[], messageIDs: string[]) {
const queued = new Set(messageIDs)
const visible = rows.filter((row) => row.type !== "message" || !queued.has(row.messageID))
rows.splice(0, rows.length, ...visible)
}
export function reduceSessionRows(
messages: SessionMessageInfo[],
inputs = new Set<string>(),
+25 -1
View File
@@ -1,6 +1,30 @@
import { expect, test } from "bun:test"
import type { SessionMessageAssistant, SessionMessageInfo } from "@opencode-ai/client"
import { cacheReuseDrop, messageBoundaryIDs, reduceSessionRows } from "../../../src/routes/session/rows"
import {
cacheReuseDrop,
messageBoundaryIDs,
removeQueuedPrompts,
reduceSessionRows,
} from "../../../src/routes/session/rows"
import type { SessionRow } from "../../../src/routes/session/rows"
test("removes queued prompts from transcript rows", () => {
const rows: SessionRow[] = [
{ type: "message" as const, messageID: "active" },
{ type: "message" as const, messageID: "queue-1" },
{ type: "message" as const, messageID: "steer" },
{ type: "message" as const, messageID: "queue-2" },
{ type: "message" as const, messageID: "queue-3" },
{ type: "message" as const, messageID: "queue-4" },
]
removeQueuedPrompts(rows, ["queue-1", "queue-2", "queue-3", "queue-4"])
expect(rows).toEqual([
{ type: "message", messageID: "active" },
{ type: "message", messageID: "steer" },
])
})
test("filters OpenAI cache quantization from cache reuse drops", () => {
const openai = { id: "gpt", providerID: "openai" }
+2 -2
View File
@@ -56,9 +56,9 @@ export function createFooterApiFixture(input: { events?: FooterEvent[]; commits?
commits,
calls,
promptReady,
submit(text: string, mode?: RunPrompt["mode"]) {
submit(text: string, mode?: RunPrompt["mode"], delivery?: RunPrompt["delivery"]) {
if (prompts.size === 0) return false
const prompt: RunPrompt = mode ? { text, parts: [], mode } : { text, parts: [] }
const prompt: RunPrompt = { text, parts: [], ...(mode ? { mode } : {}), ...(delivery ? { delivery } : {}) }
for (const fn of [...prompts]) fn(prompt)
return true
},
+14 -7
View File
@@ -1068,11 +1068,11 @@ test("direct footer submits slash autocomplete selections without dispatching sh
await app.renderOnce()
expect(submits).toEqual([
{ text: "/review ", parts: [], command: { name: "review", arguments: "" } },
{ text: "/review ", parts: [], command: { name: "review", arguments: "" } },
{ text: "/review branch", parts: [], command: { name: "review", arguments: "branch" } },
{ text: "/new ", parts: [] },
{ text: "/new ", parts: [] },
{ text: "/review ", parts: [], command: { name: "review", arguments: "" }, delivery: "steer" },
{ text: "/review ", parts: [], command: { name: "review", arguments: "" }, delivery: "steer" },
{ text: "/review branch", parts: [], command: { name: "review", arguments: "branch" }, delivery: "steer" },
{ text: "/new ", parts: [], delivery: "steer" },
{ text: "/new ", parts: [], delivery: "steer" },
])
expect(app.renderer.currentFocusedEditor?.plainText).toBe("/settings ")
} finally {
@@ -1100,7 +1100,9 @@ test("direct footer slash autocomplete keeps a real skills command", async () =>
app.mockInput.pressEnter()
await app.renderOnce()
expect(submits).toEqual([{ text: "/skills ", parts: [], command: { name: "skills", arguments: "" } }])
expect(submits).toEqual([
{ text: "/skills ", parts: [], command: { name: "skills", arguments: "" }, delivery: "steer" },
])
expect(app.captureCharFrame()).not.toContain("Apply formatter fixes")
} finally {
app.cleanup()
@@ -1158,7 +1160,12 @@ test("direct footer tags skill slash submissions with their catalog source", asy
await app.renderOnce()
expect(submits).toEqual([
{ text: "/formatter src", parts: [], command: { name: "formatter", arguments: "src", source: "skill" } },
{
text: "/formatter src",
parts: [],
command: { name: "formatter", arguments: "src", source: "skill" },
delivery: "steer",
},
])
} finally {
app.cleanup()
+2 -1
View File
@@ -82,7 +82,8 @@ describe("run runtime boot", () => {
expect(result.keybinds.get("prompt.history.next")?.[0]?.key).toBe("down")
expect(result.keybinds.get("prompt.clear")?.[0]?.key).toBe("ctrl+c")
expect(result.keybinds.get("input.submit")?.[0]?.key).toBe("return")
expect(result.keybinds.get("input.newline")?.[0]?.key).toBe("shift+return,ctrl+return,alt+return,ctrl+j")
expect(result.keybinds.get("input.newline")?.[0]?.key).toBe("shift+return,ctrl+return,ctrl+j")
expect(result.keybinds.get("prompt.queue")?.[0]?.key).toBe("alt+return")
})
test("preserves disabled leader from resolved tui config", async () => {
+28 -1
View File
@@ -265,6 +265,33 @@ describe("run runtime queue", () => {
await task
})
test("preserves explicit steer and queue delivery for in-flight prompts", async () => {
const ui = createFooterApiFixture()
const admitted: string[] = []
const gate = Promise.withResolvers<void>()
const task = runPromptQueue({
footer: ui.api,
run: async (_input, _signal, onAdmitted) => {
onAdmitted()
await gate.promise
},
admit: async (input, delivery) => {
admitted.push(`${input.text}:${delivery}`)
},
settle: async () => ui.api.close(),
})
ui.submit("one")
ui.submit("two", undefined, "steer")
ui.submit("three", undefined, "queue")
while (admitted.length < 2) await Bun.sleep(0)
expect(admitted).toEqual(["two:steer", "three:queue"])
gate.resolve()
await task
})
test("continues durable admission after one fails", async () => {
const ui = createFooterApiFixture()
const admitted: string[] = []
@@ -308,7 +335,7 @@ describe("run runtime queue", () => {
admitted()
await new Promise<void>((resolve) => signal.addEventListener("abort", () => resolve(), { once: true }))
},
admit: async (_prompt, signal) => {
admit: async (_prompt, _delivery, signal) => {
admissionStarted.resolve()
await new Promise<void>((resolve) => {
if (signal.aborted) {
+3 -3
View File
@@ -126,7 +126,7 @@ describe("run interactive runtime", () => {
turnStarted.resolve()
api.close()
},
queuePromptTurn: async () => {},
admitPromptTurn: async () => {},
waitForIdle: async () => {},
interruptActiveTurn: async () => {},
selectSubagent: () => {},
@@ -209,7 +209,7 @@ describe("run interactive runtime", () => {
streamStarted.resolve()
return {
runPromptTurn: async () => {},
queuePromptTurn: async () => {},
admitPromptTurn: async () => {},
waitForIdle: async () => {},
interruptActiveTurn: async () => {},
selectSubagent: () => {},
@@ -556,7 +556,7 @@ describe("run interactive runtime", () => {
setTimeout(() => input.footer.close(), 0)
return {
runPromptTurn: async () => {},
queuePromptTurn: async () => {},
admitPromptTurn: async () => {},
waitForIdle: async () => {},
interruptActiveTurn: async () => {},
selectSubagent: () => {},
@@ -701,14 +701,14 @@ describe("V2 mini transport", () => {
const prompt = spyOn(client.session, "prompt").mockImplementation(
(request) => ok(promptAdmission(request)) as never,
)
await transport.queuePromptTurn({
await transport.admitPromptTurn({
agent: "review",
model: undefined,
variant: undefined,
prompt: { messageID: "msg_next", text: "another", parts: [] },
files: [],
includeFiles: false,
})
}, "queue")
expect(client.session.switchAgent).toHaveBeenCalledWith({ sessionID: "ses_1", agent: "review" }, expect.anything())
expect(prompt).toHaveBeenCalledWith(expect.objectContaining({ delivery: "queue" }), expect.anything())
events.push({
@@ -813,14 +813,14 @@ describe("V2 mini transport", () => {
durable: durable("ses_1", 2),
data: { sessionID: "ses_1", inputID: "msg_prompt" },
})
await transport.queuePromptTurn({
await transport.admitPromptTurn({
agent: undefined,
model: undefined,
variant: undefined,
prompt: { messageID: "msg_queued", text: "follow up", parts: [] },
files: [],
includeFiles: false,
})
}, "queue")
events.push({
id: "evt_queued_promoted",
created: 3,