From d32e8d2ace98a24ce22d014ddf8da44812aee37a Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sun, 19 Apr 2026 08:56:29 -0500 Subject: [PATCH] =?UTF-8?q?fix(tui):=20drain=20message=20queue=20on=20ever?= =?UTF-8?q?y=20busy=20=E2=86=92=20false=20transition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the queue only drained inside the message.complete event handler, so anything enqueued while a shell.exec (!sleep, !cmd) or a failed agent turn was running would stay stuck forever — neither of those paths emits message.complete. After Ctrl+C an interrupted session would also orphan the queue because idle() flips busy=false locally without going through message.complete. Single source of truth: a useEffect that watches ui.busy. When the session is settled (sid present, busy false, not editing a queue item), pull one message and send it. Covers agent turn end, interrupt, shell.exec completion, error recovery, and the original startup hydration (first-sid case) all at once. Dropped the now-redundant dequeue/sendQueued from createGatewayEventHandler.message.complete and the accompanying GatewayEventHandlerContext.composer field — the effect handles it. --- ui-tui/src/app/createGatewayEventHandler.ts | 11 ----------- ui-tui/src/app/interfaces.ts | 5 ----- ui-tui/src/app/useMainApp.ts | 15 ++++++--------- 3 files changed, 6 insertions(+), 25 deletions(-) diff --git a/ui-tui/src/app/createGatewayEventHandler.ts b/ui-tui/src/app/createGatewayEventHandler.ts index 699a3794d..8f45bb3d7 100644 --- a/ui-tui/src/app/createGatewayEventHandler.ts +++ b/ui-tui/src/app/createGatewayEventHandler.ts @@ -46,7 +46,6 @@ const pushNote = pushUnique(6) const pushTool = pushUnique(8) export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev: GatewayEvent) => void { - const { dequeue, queueEditRef, sendQueued } = ctx.composer const { rpc } = ctx.gateway const { STARTUP_RESUME_ID, newSession, resumeById, setCatalog } = ctx.session const { bellOnComplete, stdout, sys } = ctx.system @@ -394,16 +393,6 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev: patchUiState(state => ({ ...state, usage: { ...state.usage, ...ev.payload!.usage } })) } - if (queueEditRef.current !== null) { - return - } - - const next = dequeue() - - if (next) { - sendQueued(next) - } - return } diff --git a/ui-tui/src/app/interfaces.ts b/ui-tui/src/app/interfaces.ts index 353c56535..af13e047c 100644 --- a/ui-tui/src/app/interfaces.ts +++ b/ui-tui/src/app/interfaces.ts @@ -193,11 +193,6 @@ export interface InputHandlerResult { } export interface GatewayEventHandlerContext { - composer: { - dequeue: () => string | undefined - queueEditRef: MutableRefObject - sendQueued: (text: string) => void - } gateway: GatewayServices session: { STARTUP_RESUME_ID: string diff --git a/ui-tui/src/app/useMainApp.ts b/ui-tui/src/app/useMainApp.ts index fb48badea..e0c18dec6 100644 --- a/ui-tui/src/app/useMainApp.ts +++ b/ui-tui/src/app/useMainApp.ts @@ -380,12 +380,13 @@ export function useMainApp(gw: GatewayClient) { sys }) - const prevSidRef = useRef(null) + // Drain one queued message whenever the session settles (busy → false): + // agent turn ends, interrupt, shell.exec finishes, error recovered, or the + // session first comes up with pre-queued messages. Without this, shell.exec + // and error paths never emit message.complete, so anything enqueued while + // `!sleep` / a failed turn was running would stay stuck forever. useEffect(() => { - const prev = prevSidRef.current - prevSidRef.current = ui.sid - - if (prev !== null || !ui.sid || ui.busy || composerRefs.queueEditRef.current !== null) { + if (!ui.sid || ui.busy || composerRefs.queueEditRef.current !== null) { return } @@ -416,7 +417,6 @@ export function useMainApp(gw: GatewayClient) { const onEvent = useMemo( () => createGatewayEventHandler({ - composer: { dequeue: composerActions.dequeue, queueEditRef: composerRefs.queueEditRef, sendQueued }, gateway, session: { STARTUP_RESUME_ID, @@ -432,11 +432,8 @@ export function useMainApp(gw: GatewayClient) { [ appendMessage, bellOnComplete, - composerActions, - composerRefs, gateway, panel, - sendQueued, session.newSession, session.resetSession, session.resumeById,