feat(tui): add busy status indicator during processing
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user