diff --git a/docs/plans/state.json b/docs/plans/state.json index 1198ab1..ba7c341 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -5745,6 +5745,17 @@ "docs/plans/state.json" ], "test_status": "pnpm test:run src/gateway/handlers/handlers.test.ts + pnpm typecheck passing" + }, + "minimal-tui-busy-status-indicator": { + "status": "completed", + "date": "2026-02-19", + "updated": "2026-02-19", + "summary": "Added an explicit busy status indicator in minimal TUI while Flynn is processing requests, including a spinner and cancel hint, with safe teardown before streamed output/tool logs so the terminal no longer appears frozen between prompt submit and response.", + "files_modified": [ + "src/frontends/tui/minimal.ts", + "docs/plans/state.json" + ], + "test_status": "pnpm test:run src/frontends/tui/minimal.test.ts passing" } }, "overall_progress": { diff --git a/src/frontends/tui/minimal.ts b/src/frontends/tui/minimal.ts index 240af1e..757d0e0 100644 --- a/src/frontends/tui/minimal.ts +++ b/src/frontends/tui/minimal.ts @@ -73,6 +73,7 @@ export interface MinimalTuiConfig { export class MinimalTui { private static readonly CTRL_C_EXIT_WINDOW_MS = 1_500; + private static readonly BUSY_FRAMES = ['-', '\\', '|', '/'] as const; private rl: readline.Interface | null = null; private running = false; private totalUsage: TokenUsage = { inputTokens: 0, outputTokens: 0 }; @@ -81,6 +82,9 @@ export class MinimalTui { private activePromptCancel: (() => void) | null = null; private activeOperationCancel: (() => void) | null = null; private keypressHandler: ((char: string, key: readline.Key) => void) | null = null; + private busyTimer: NodeJS.Timeout | null = null; + private busyFrameIndex = 0; + private busyActive = false; private verbose = false; private lastCtrlCAtMs = 0; @@ -182,10 +186,53 @@ export class MinimalTui { }).join(', '); } + private startBusyIndicator(): void { + if (this.busyActive) { + return; + } + + this.busyActive = true; + const render = () => { + const frame = MinimalTui.BUSY_FRAMES[this.busyFrameIndex]; + const message = `${formatPrompt('thinking')}${colors.gray}${frame} Flynn is thinking... (Esc to cancel)${colors.reset}`; + if (process.stdout.isTTY) { + process.stdout.write(`\r${message}`); + } else { + console.log(message); + } + }; + + render(); + if (!process.stdout.isTTY) { + return; + } + + this.busyTimer = setInterval(() => { + this.busyFrameIndex = (this.busyFrameIndex + 1) % MinimalTui.BUSY_FRAMES.length; + render(); + }, 125); + } + + private stopBusyIndicator(): void { + if (!this.busyActive) { + return; + } + this.busyActive = false; + this.busyFrameIndex = 0; + if (this.busyTimer) { + clearInterval(this.busyTimer); + this.busyTimer = null; + } + if (process.stdout.isTTY) { + process.stdout.write('\r\x1b[K'); + } + } + private handleToolEvent(event: ToolUseEvent): void { if (!this.verbose) { return; } + this.stopBusyIndicator(); if (event.type === 'start') { const label = this.formatToolName(event.tool); const argsStr = event.args ? ` (${this.formatToolArgs(event.args)})` : ''; @@ -1268,6 +1315,7 @@ export class MinimalTui { process.stdout.write( `\n${colors.orange}${colors.bold}Flynn${colors.reset} ${colors.gray}[${formatMessageTime(Date.now())}]${colors.reset}\n`, ); + this.startBusyIndicator(); try { // Use agent if available (supports tool loop) @@ -1283,6 +1331,7 @@ export class MinimalTui { }; const response = await this.config.agent.process(content); this.activeOperationCancel = null; + this.stopBusyIndicator(); const rendered = renderMarkdown(response); console.log(rendered); console.log(); @@ -1295,6 +1344,7 @@ export class MinimalTui { // Try streaming if available if (this.config.modelClient.chatStream) { let fullContent = ''; + let streamStarted = false; let cancelRequested = false; this.activeOperationCancel = () => { if (cancelRequested) { @@ -1313,10 +1363,18 @@ export class MinimalTui { break; } if (event.type === 'content' && event.content) { + if (!streamStarted) { + streamStarted = true; + this.stopBusyIndicator(); + } process.stdout.write(event.content); fullContent += event.content; } if (event.type === 'fallback_warning' && event.fallbackReason) { + if (!streamStarted) { + streamStarted = true; + this.stopBusyIndicator(); + } console.warn('\n⚠ Using fallback model'); } if (event.type === 'done' && event.usage) { @@ -1342,6 +1400,7 @@ export class MinimalTui { system: this.config.systemPrompt, }); this.activeOperationCancel = null; + this.stopBusyIndicator(); const rendered = renderMarkdown(response.content); console.log(rendered); @@ -1353,9 +1412,11 @@ export class MinimalTui { this.config.session.addMessage({ role: 'assistant', content: response.content }); } } catch (error) { + this.stopBusyIndicator(); console.error('Error:', error instanceof Error ? error.message : error); console.log(); } finally { + this.stopBusyIndicator(); this.activeOperationCancel = null; } }