From a6337dc709e0626f51b4fa72025cc0b394c9ac62 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 19 Mar 2026 22:12:02 +0100 Subject: [PATCH] fix(coding-agent): show bash tool elapsed time at bottom closes #2406 --- .../interactive/components/tool-execution.ts | 49 +++++++++++++++++++ .../src/modes/interactive/interactive-mode.ts | 8 +-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/packages/coding-agent/src/modes/interactive/components/tool-execution.ts b/packages/coding-agent/src/modes/interactive/components/tool-execution.ts index bf282ad2..1d249aff 100644 --- a/packages/coding-agent/src/modes/interactive/components/tool-execution.ts +++ b/packages/coding-agent/src/modes/interactive/components/tool-execution.ts @@ -105,6 +105,8 @@ export class ToolExecutionComponent extends Container { private writeHighlightCache?: WriteHighlightCache; // When true, this component intentionally renders no lines private hideComponent = false; + private bashStartedAt?: number; + private bashElapsedInterval?: NodeJS.Timeout; constructor( toolName: string, @@ -158,6 +160,38 @@ export class ToolExecutionComponent extends Container { this.updateDisplay(); } + markExecutionStarted(): void { + if (this.toolName !== "bash" || this.bashStartedAt !== undefined) return; + this.bashStartedAt = Date.now(); + this.ensureBashElapsedTimer(); + this.updateDisplay(); + this.ui.requestRender(); + } + + private ensureBashElapsedTimer(): void { + if (this.toolName !== "bash" || !this.isPartial || this.bashStartedAt === undefined || this.bashElapsedInterval) + return; + this.bashElapsedInterval = setInterval(() => { + this.updateDisplay(); + this.ui.requestRender(); + }, 1000); + } + + private stopBashElapsedTimer(): void { + if (!this.bashElapsedInterval) return; + clearInterval(this.bashElapsedInterval); + this.bashElapsedInterval = undefined; + } + + private getBashDurationMs(): number | undefined { + if (this.toolName !== "bash" || this.bashStartedAt === undefined) return undefined; + return Date.now() - this.bashStartedAt; + } + + private formatDuration(ms: number): string { + return `${(ms / 1000).toFixed(1)}s`; + } + private highlightSingleLine(line: string, lang: string): string { const highlighted = highlightCode(line, lang); return highlighted[0] ?? ""; @@ -308,6 +342,13 @@ export class ToolExecutionComponent extends Container { ): void { this.result = result; this.isPartial = isPartial; + if (this.toolName === "bash") { + if (isPartial) { + this.ensureBashElapsedTimer(); + } else { + this.stopBashElapsedTimer(); + } + } if (this.toolName === "write" && !isPartial) { const rawPath = str(this.args?.file_path ?? this.args?.path); const fileContent = str(this.args?.content); @@ -582,6 +623,14 @@ export class ToolExecutionComponent extends Container { this.contentBox.addChild(new Text(`\n${theme.fg("warning", `[${warnings.join(". ")}]`)}`, 0, 0)); } } + + const bashDurationMs = this.getBashDurationMs(); + if (bashDurationMs !== undefined) { + const label = this.isPartial ? "Elapsed" : "Took"; + this.contentBox.addChild( + new Text(`\n${theme.fg("muted", `${label} ${this.formatDuration(bashDurationMs)}`)}`, 0, 0), + ); + } } private getTextOutput(): string { diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 8d9a261d..f5b52316 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -2260,8 +2260,9 @@ export class InteractiveMode { break; case "tool_execution_start": { - if (!this.pendingTools.has(event.toolCallId)) { - const component = new ToolExecutionComponent( + let component = this.pendingTools.get(event.toolCallId); + if (!component) { + component = new ToolExecutionComponent( event.toolName, event.args, { @@ -2273,8 +2274,9 @@ export class InteractiveMode { component.setExpanded(this.toolOutputExpanded); this.chatContainer.addChild(component); this.pendingTools.set(event.toolCallId, component); - this.ui.requestRender(); } + component.markExecutionStarted(); + this.ui.requestRender(); break; }