feat(tui): add busy status indicator during processing

This commit is contained in:
William Valentin
2026-02-19 11:34:44 -08:00
parent 7a2176c15c
commit 7cb647cbb8
2 changed files with 72 additions and 0 deletions
+11
View File
@@ -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": {
+61
View File
@@ -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;
}
}