feat(tui): queue pre-session input, auto-flush when session lands

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.
This commit is contained in:
Brooklyn Nicholson 2026-04-16 15:04:18 -05:00
parent c6ed61430a
commit f3920fec0b
2 changed files with 36 additions and 8 deletions

View file

@ -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 | string>(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,

View file

@ -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)
}