diff --git a/ui-tui/src/app/interfaces.ts b/ui-tui/src/app/interfaces.ts index af13e047c..da9d0baed 100644 --- a/ui-tui/src/app/interfaces.ts +++ b/ui-tui/src/app/interfaces.ts @@ -303,6 +303,7 @@ export interface AppLayoutStatusProps { showStickyPrompt: boolean statusColor: string stickyPrompt: string + turnStartedAt: null | number voiceLabel: string } diff --git a/ui-tui/src/app/useMainApp.ts b/ui-tui/src/app/useMainApp.ts index 27401b418..28b2a26f9 100644 --- a/ui-tui/src/app/useMainApp.ts +++ b/ui-tui/src/app/useMainApp.ts @@ -102,6 +102,7 @@ export function useMainApp(gw: GatewayClient) { const [voiceRecording, setVoiceRecording] = useState(false) const [voiceProcessing, setVoiceProcessing] = useState(false) const [sessionStartedAt, setSessionStartedAt] = useState(() => Date.now()) + const [turnStartedAt, setTurnStartedAt] = useState(null) const [goodVibesTick, setGoodVibesTick] = useState(0) const [bellOnComplete, setBellOnComplete] = useState(false) @@ -283,6 +284,14 @@ export function useMainApp(gw: GatewayClient) { sys }) + useEffect(() => { + if (ui.busy) { + setTurnStartedAt(prev => prev ?? Date.now()) + } else { + setTurnStartedAt(null) + } + }, [ui.busy]) + useConfigSync({ gw, setBellOnComplete, setVoiceEnabled, sid: ui.sid }) // ── Terminal tab title ───────────────────────────────────────────── @@ -635,9 +644,21 @@ export function useMainApp(gw: GatewayClient) { showStickyPrompt: !!stickyPrompt, statusColor: statusColorOf(ui.status, ui.theme.color), stickyPrompt, + turnStartedAt: ui.sid ? turnStartedAt : null, voiceLabel: voiceRecording ? 'REC' : voiceProcessing ? 'STT' : `voice ${voiceEnabled ? 'on' : 'off'}` }), - [cwd, gitBranch, goodVibesTick, sessionStartedAt, stickyPrompt, ui, voiceEnabled, voiceProcessing, voiceRecording] + [ + cwd, + gitBranch, + goodVibesTick, + sessionStartedAt, + stickyPrompt, + turnStartedAt, + ui, + voiceEnabled, + voiceProcessing, + voiceRecording + ] ) const appTranscript = useMemo( diff --git a/ui-tui/src/components/appChrome.tsx b/ui-tui/src/components/appChrome.tsx index 2f5f807de..da5507e28 100644 --- a/ui-tui/src/components/appChrome.tsx +++ b/ui-tui/src/components/appChrome.tsx @@ -12,18 +12,24 @@ import type { Msg, Usage } from '../types.js' const FACE_TICK_MS = 2500 const HEART_COLORS = ['#ff5fa2', '#ff4d6d'] -function FaceTicker({ color }: { color: string }) { +function FaceTicker({ color, startedAt }: { color: string; startedAt?: null | number }) { const [tick, setTick] = useState(() => Math.floor(Math.random() * 1000)) + const [now, setNow] = useState(() => Date.now()) useEffect(() => { - const id = setInterval(() => setTick(n => n + 1), FACE_TICK_MS) + const face = setInterval(() => setTick(n => n + 1), FACE_TICK_MS) + const clock = setInterval(() => setNow(Date.now()), 1000) - return () => clearInterval(id) + return () => { + clearInterval(face) + clearInterval(clock) + } }, []) return ( {FACES[tick % FACES.length]} {VERBS[tick % VERBS.length]}… + {startedAt ? ` · ${fmtDuration(now - startedAt)}` : ''} ) } @@ -100,6 +106,7 @@ export function StatusRule({ bgCount, sessionStartedAt, showCost, + turnStartedAt, voiceLabel, t }: StatusRuleProps) { @@ -120,7 +127,7 @@ export function StatusRule({ {'─ '} - {busy ? : {status}} + {busy ? : {status}} │ {model} {ctxLabel ? │ {ctxLabel} : null} {bar ? ( @@ -288,11 +295,12 @@ interface StatusRuleProps { cols: number cwdLabel: string model: string - sessionStartedAt?: number | null + sessionStartedAt?: null | number showCost: boolean status: string statusColor: string t: Theme + turnStartedAt?: null | number usage: Usage voiceLabel?: string } diff --git a/ui-tui/src/components/appLayout.tsx b/ui-tui/src/components/appLayout.tsx index f13adf1bb..ad854033a 100644 --- a/ui-tui/src/components/appLayout.tsx +++ b/ui-tui/src/components/appLayout.tsx @@ -194,6 +194,7 @@ const ComposerPane = memo(function ComposerPane({ status={ui.status} statusColor={status.statusColor} t={ui.theme} + turnStartedAt={status.turnStartedAt} usage={ui.usage} voiceLabel={status.voiceLabel} />