diff --git a/ui-tui/src/app/turnController.ts b/ui-tui/src/app/turnController.ts index 1041b4d4f5..90c4ac12bf 100644 --- a/ui-tui/src/app/turnController.ts +++ b/ui-tui/src/app/turnController.ts @@ -1,4 +1,4 @@ -import { REASONING_PULSE_MS, STREAM_BATCH_MS } from '../config/timing.js' +import { REASONING_PULSE_MS, STREAM_BATCH_MS, STREAM_IDLE_BATCH_MS, STREAM_TYPING_BATCH_MS } from '../config/timing.js' import type { SessionInterruptResponse, SubagentEventPayload } from '../gatewayTypes.js' import { hasReasoningTag, splitReasoning } from '../lib/reasoning.js' import { @@ -75,8 +75,17 @@ class TurnController { private reasoningStreamingTimer: Timer = null private reasoningTimer: Timer = null private streamTimer: Timer = null + private streamDelay = STREAM_IDLE_BATCH_MS private toolProgressTimer: Timer = null + boostStreamingForTyping() { + this.streamDelay = STREAM_TYPING_BATCH_MS + } + + relaxStreaming() { + this.streamDelay = STREAM_IDLE_BATCH_MS + } + clearReasoning() { this.reasoningTimer = clear(this.reasoningTimer) this.reasoningText = '' @@ -493,7 +502,7 @@ class TurnController { const raw = this.bufRef.trimStart() const visible = hasReasoningTag(raw) ? splitReasoning(raw).text : raw patchTurnState({ streaming: visible }) - }, STREAM_BATCH_MS) + }, this.streamDelay) } startMessage() { diff --git a/ui-tui/src/app/useSubmission.ts b/ui-tui/src/app/useSubmission.ts index f09dc36340..9bca65815d 100644 --- a/ui-tui/src/app/useSubmission.ts +++ b/ui-tui/src/app/useSubmission.ts @@ -1,5 +1,6 @@ -import { type MutableRefObject, useCallback, useRef } from 'react' +import { type MutableRefObject, useCallback, useEffect, useRef } from 'react' +import { TYPING_IDLE_MS } from '../config/timing.js' import { attachedImageNotice } from '../domain/messages.js' import { looksLikeSlashCommand } from '../domain/slash.js' import type { GatewayClient } from '../gatewayClient.js' @@ -14,6 +15,9 @@ import { turnController } from './turnController.js' import { getUiState, patchUiState } from './uiStore.js' const DOUBLE_ENTER_MS = 450 +const SESSION_BUSY_RE = /session busy|waiting for model response/i + +const isSessionBusyError = (e: unknown) => e instanceof Error && SESSION_BUSY_RE.test(e.message) const expandSnips = (snips: PasteSnippet[]) => { const byLabel = new Map() @@ -44,6 +48,30 @@ export function useSubmission(opts: UseSubmissionOptions) { } = opts const lastEmptyAt = useRef(0) + const typingIdleTimer = useRef | null>(null) + + useEffect(() => { + if (composerState.input || composerState.inputBuf.length) { + if (getUiState().busy) { + turnController.boostStreamingForTyping() + } + + if (typingIdleTimer.current) { + clearTimeout(typingIdleTimer.current) + } + + typingIdleTimer.current = setTimeout(() => { + typingIdleTimer.current = null + turnController.relaxStreaming() + }, TYPING_IDLE_MS) + } + + return () => { + if (typingIdleTimer.current) { + clearTimeout(typingIdleTimer.current) + } + } + }, [composerState.input, composerState.inputBuf]) const send = useCallback( (text: string) => { @@ -65,6 +93,13 @@ export function useSubmission(opts: UseSubmissionOptions) { turnController.interrupted = false gw.request('prompt.submit', { session_id: sid, text: submitText }).catch((e: Error) => { + if (isSessionBusyError(e)) { + composerActions.enqueue(text) + patchUiState({ busy: true, status: 'queued for next turn' }) + + return sys(`queued: "${text.slice(0, 50)}${text.length > 50 ? '…' : ''}"`) + } + sys(`error: ${e.message}`) patchUiState({ busy: false, status: 'ready' }) }) @@ -92,7 +127,7 @@ export function useSubmission(opts: UseSubmissionOptions) { }) .catch(() => startSubmit(text, expand(text))) }, - [appendMessage, composerState.pasteSnips, gw, maybeGoodVibes, setLastUserMsg, sys] + [appendMessage, composerActions, composerState.pasteSnips, gw, maybeGoodVibes, setLastUserMsg, sys] ) const shellExec = useCallback( diff --git a/ui-tui/src/config/timing.ts b/ui-tui/src/config/timing.ts index 63498dbae8..8fdf6b5fc5 100644 --- a/ui-tui/src/config/timing.ts +++ b/ui-tui/src/config/timing.ts @@ -1,2 +1,5 @@ export const STREAM_BATCH_MS = 16 +export const STREAM_IDLE_BATCH_MS = 16 +export const STREAM_TYPING_BATCH_MS = 80 +export const TYPING_IDLE_MS = 120 export const REASONING_PULSE_MS = 700