feat(tui): add busy status indicator during processing
This commit is contained in:
@@ -5745,6 +5745,17 @@
|
|||||||
"docs/plans/state.json"
|
"docs/plans/state.json"
|
||||||
],
|
],
|
||||||
"test_status": "pnpm test:run src/gateway/handlers/handlers.test.ts + pnpm typecheck passing"
|
"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": {
|
"overall_progress": {
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export interface MinimalTuiConfig {
|
|||||||
|
|
||||||
export class MinimalTui {
|
export class MinimalTui {
|
||||||
private static readonly CTRL_C_EXIT_WINDOW_MS = 1_500;
|
private static readonly CTRL_C_EXIT_WINDOW_MS = 1_500;
|
||||||
|
private static readonly BUSY_FRAMES = ['-', '\\', '|', '/'] as const;
|
||||||
private rl: readline.Interface | null = null;
|
private rl: readline.Interface | null = null;
|
||||||
private running = false;
|
private running = false;
|
||||||
private totalUsage: TokenUsage = { inputTokens: 0, outputTokens: 0 };
|
private totalUsage: TokenUsage = { inputTokens: 0, outputTokens: 0 };
|
||||||
@@ -81,6 +82,9 @@ export class MinimalTui {
|
|||||||
private activePromptCancel: (() => void) | null = null;
|
private activePromptCancel: (() => void) | null = null;
|
||||||
private activeOperationCancel: (() => void) | null = null;
|
private activeOperationCancel: (() => void) | null = null;
|
||||||
private keypressHandler: ((char: string, key: readline.Key) => 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 verbose = false;
|
||||||
private lastCtrlCAtMs = 0;
|
private lastCtrlCAtMs = 0;
|
||||||
|
|
||||||
@@ -182,10 +186,53 @@ export class MinimalTui {
|
|||||||
}).join(', ');
|
}).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 {
|
private handleToolEvent(event: ToolUseEvent): void {
|
||||||
if (!this.verbose) {
|
if (!this.verbose) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.stopBusyIndicator();
|
||||||
if (event.type === 'start') {
|
if (event.type === 'start') {
|
||||||
const label = this.formatToolName(event.tool);
|
const label = this.formatToolName(event.tool);
|
||||||
const argsStr = event.args ? ` (${this.formatToolArgs(event.args)})` : '';
|
const argsStr = event.args ? ` (${this.formatToolArgs(event.args)})` : '';
|
||||||
@@ -1268,6 +1315,7 @@ export class MinimalTui {
|
|||||||
process.stdout.write(
|
process.stdout.write(
|
||||||
`\n${colors.orange}${colors.bold}Flynn${colors.reset} ${colors.gray}[${formatMessageTime(Date.now())}]${colors.reset}\n`,
|
`\n${colors.orange}${colors.bold}Flynn${colors.reset} ${colors.gray}[${formatMessageTime(Date.now())}]${colors.reset}\n`,
|
||||||
);
|
);
|
||||||
|
this.startBusyIndicator();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use agent if available (supports tool loop)
|
// Use agent if available (supports tool loop)
|
||||||
@@ -1283,6 +1331,7 @@ export class MinimalTui {
|
|||||||
};
|
};
|
||||||
const response = await this.config.agent.process(content);
|
const response = await this.config.agent.process(content);
|
||||||
this.activeOperationCancel = null;
|
this.activeOperationCancel = null;
|
||||||
|
this.stopBusyIndicator();
|
||||||
const rendered = renderMarkdown(response);
|
const rendered = renderMarkdown(response);
|
||||||
console.log(rendered);
|
console.log(rendered);
|
||||||
console.log();
|
console.log();
|
||||||
@@ -1295,6 +1344,7 @@ export class MinimalTui {
|
|||||||
// Try streaming if available
|
// Try streaming if available
|
||||||
if (this.config.modelClient.chatStream) {
|
if (this.config.modelClient.chatStream) {
|
||||||
let fullContent = '';
|
let fullContent = '';
|
||||||
|
let streamStarted = false;
|
||||||
let cancelRequested = false;
|
let cancelRequested = false;
|
||||||
this.activeOperationCancel = () => {
|
this.activeOperationCancel = () => {
|
||||||
if (cancelRequested) {
|
if (cancelRequested) {
|
||||||
@@ -1313,10 +1363,18 @@ export class MinimalTui {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (event.type === 'content' && event.content) {
|
if (event.type === 'content' && event.content) {
|
||||||
|
if (!streamStarted) {
|
||||||
|
streamStarted = true;
|
||||||
|
this.stopBusyIndicator();
|
||||||
|
}
|
||||||
process.stdout.write(event.content);
|
process.stdout.write(event.content);
|
||||||
fullContent += event.content;
|
fullContent += event.content;
|
||||||
}
|
}
|
||||||
if (event.type === 'fallback_warning' && event.fallbackReason) {
|
if (event.type === 'fallback_warning' && event.fallbackReason) {
|
||||||
|
if (!streamStarted) {
|
||||||
|
streamStarted = true;
|
||||||
|
this.stopBusyIndicator();
|
||||||
|
}
|
||||||
console.warn('\n⚠ Using fallback model');
|
console.warn('\n⚠ Using fallback model');
|
||||||
}
|
}
|
||||||
if (event.type === 'done' && event.usage) {
|
if (event.type === 'done' && event.usage) {
|
||||||
@@ -1342,6 +1400,7 @@ export class MinimalTui {
|
|||||||
system: this.config.systemPrompt,
|
system: this.config.systemPrompt,
|
||||||
});
|
});
|
||||||
this.activeOperationCancel = null;
|
this.activeOperationCancel = null;
|
||||||
|
this.stopBusyIndicator();
|
||||||
|
|
||||||
const rendered = renderMarkdown(response.content);
|
const rendered = renderMarkdown(response.content);
|
||||||
console.log(rendered);
|
console.log(rendered);
|
||||||
@@ -1353,9 +1412,11 @@ export class MinimalTui {
|
|||||||
this.config.session.addMessage({ role: 'assistant', content: response.content });
|
this.config.session.addMessage({ role: 'assistant', content: response.content });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
this.stopBusyIndicator();
|
||||||
console.error('Error:', error instanceof Error ? error.message : error);
|
console.error('Error:', error instanceof Error ? error.message : error);
|
||||||
console.log();
|
console.log();
|
||||||
} finally {
|
} finally {
|
||||||
|
this.stopBusyIndicator();
|
||||||
this.activeOperationCancel = null;
|
this.activeOperationCancel = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user