From f3920fec0b184bdf511a7adf7a2b844e494d3f8a Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Thu, 16 Apr 2026 15:04:18 -0500 Subject: [PATCH] feat(tui): queue pre-session input, auto-flush when session lands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The TUI is fully interactive from the first frame but `session.create` (agent + tools + MCP) takes ~2s. Plain-text messages typed before the session is live used to fail with "session not ready yet"; slash and shell commands worked but agent prompts were dropped. Now: - `dispatchSubmission` enqueues plain text when `sid` is null (slash/shell still short-circuit first) - `useMainApp` tracks sid transitions and kicks off one `sendQueued()` when the session first becomes ready; subsequent queued messages drain on `message.complete` as before - Fixed pre-existing double-Enter bug that dequeued without sid check User flow: type `hello` → shows in `queuedDisplay` preview → 2s later agent wakes → message auto-sends → reply streams. Zero wasted input. --- ui-tui/src/app/useMainApp.ts | 22 ++++++++++++++++++++++ ui-tui/src/app/useSubmission.ts | 22 ++++++++++++++-------- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/ui-tui/src/app/useMainApp.ts b/ui-tui/src/app/useMainApp.ts index e6e8cfe1f4..c97fde79b4 100644 --- a/ui-tui/src/app/useMainApp.ts +++ b/ui-tui/src/app/useMainApp.ts @@ -372,6 +372,28 @@ export function useMainApp(gw: GatewayClient) { sys }) + // Flush any pre-session queued input once the session lands. + // Message.complete already drains subsequent items; this only kicks off the first. + const prevSidRef = useRef(null) + useEffect(() => { + const prev = prevSidRef.current + prevSidRef.current = ui.sid + + if (prev !== null || !ui.sid || ui.busy) { + return + } + + if (composerRefs.queueEditRef.current !== null) { + return + } + + const next = composerActions.dequeue() + + if (next) { + sendQueued(next) + } + }, [ui.sid, ui.busy, composerActions, composerRefs, sendQueued]) + const { pagerPageSize } = useInputHandlers({ actions: { answerClarify, diff --git a/ui-tui/src/app/useSubmission.ts b/ui-tui/src/app/useSubmission.ts index 4fc699676e..fee2dd7272 100644 --- a/ui-tui/src/app/useSubmission.ts +++ b/ui-tui/src/app/useSubmission.ts @@ -183,12 +183,7 @@ export function useSubmission(opts: UseSubmissionOptions) { return } - const live = getUiState() - - if (!live.sid) { - return sys('session not ready yet') - } - + // Slash + shell run regardless of session state (each handles its own sid needs). if (looksLikeSlashCommand(full)) { appendMessage({ kind: 'slash', role: 'system', text: full }) composerActions.pushHistory(full) @@ -204,6 +199,17 @@ export function useSubmission(opts: UseSubmissionOptions) { return shellExec(full.slice(1).trim()) } + const live = getUiState() + + // No session yet — queue the text and let the ready-flush effect send it. + if (!live.sid) { + composerActions.pushHistory(full) + composerActions.enqueue(full) + composerActions.clearIn() + + return + } + const editIdx = composerRefs.queueEditRef.current composerActions.clearIn() @@ -269,10 +275,10 @@ export function useSubmission(opts: UseSubmissionOptions) { return turnController.interruptTurn({ appendMessage, gw, sid: live.sid, sys }) } - if (doubleTap && composerRefs.queueRef.current.length) { + if (doubleTap && live.sid && composerRefs.queueRef.current.length) { const next = composerActions.dequeue() - if (next && live.sid) { + if (next) { composerActions.setQueueEdit(null) dispatchSubmission(next) }