From b0aeeb53b101ed73dfebbff74197efdc4e18b142 Mon Sep 17 00:00:00 2001 From: Olcan Date: Fri, 30 May 2025 00:02:30 -0700 Subject: [PATCH] update shell output at an interval to reduce flicker (#614) --- .../cli/src/ui/hooks/shellCommandProcessor.ts | 14 +++++++--- packages/cli/src/ui/hooks/useToolScheduler.ts | 12 ++++----- packages/server/src/tools/shell.ts | 27 ++++++++++++------- packages/server/src/tools/tools.ts | 4 +-- 4 files changed, 35 insertions(+), 22 deletions(-) diff --git a/packages/cli/src/ui/hooks/shellCommandProcessor.ts b/packages/cli/src/ui/hooks/shellCommandProcessor.ts index 74dade5e..59e337b4 100644 --- a/packages/cli/src/ui/hooks/shellCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/shellCommandProcessor.ts @@ -16,6 +16,8 @@ import path from 'path'; import os from 'os'; import fs from 'fs'; +const OUTPUT_UPDATE_INTERVAL_MS = 1000; + /** * Hook to process shell commands (e.g., !ls, $pwd). * Executes the command in the target directory and adds output/errors to history. @@ -122,16 +124,20 @@ export const useShellCommandProcessor = ( let exited = false; let output = ''; + let lastUpdateTime = Date.now(); const handleOutput = (data: string) => { // continue to consume post-exit for background processes // removing listeners can overflow OS buffer and block subprocesses // destroying (e.g. child.stdout.destroy()) can terminate subprocesses via SIGPIPE if (!exited) { output += data; - setPendingHistoryItem({ - type: 'info', - text: output, - }); + if (Date.now() - lastUpdateTime > OUTPUT_UPDATE_INTERVAL_MS) { + setPendingHistoryItem({ + type: 'info', + text: output, + }); + lastUpdateTime = Date.now(); + } } }; child.stdout.on('data', handleOutput); diff --git a/packages/cli/src/ui/hooks/useToolScheduler.ts b/packages/cli/src/ui/hooks/useToolScheduler.ts index e6e80785..af8715e9 100644 --- a/packages/cli/src/ui/hooks/useToolScheduler.ts +++ b/packages/cli/src/ui/hooks/useToolScheduler.ts @@ -288,11 +288,9 @@ export function useToolScheduler( const callId = t.request.callId; setToolCalls(setStatus(t.request.callId, 'executing')); - let accumulatedOutput = ''; - const onOutputChunk = + const updateOutput = t.tool.name === 'execute_bash_command' - ? (chunk: string) => { - accumulatedOutput += chunk; + ? (output: string) => { setPendingHistoryItem( (prevItem: HistoryItemWithoutId | null) => { if (prevItem?.type === 'tool_group') { @@ -304,7 +302,7 @@ export function useToolScheduler( toolDisplay.status === ToolCallStatus.Executing ? { ...toolDisplay, - resultDisplay: accumulatedOutput, + resultDisplay: output, } : toolDisplay, ), @@ -319,7 +317,7 @@ export function useToolScheduler( setToolCalls((prevToolCalls) => prevToolCalls.map((tc) => tc.request.callId === callId && tc.status === 'executing' - ? { ...tc, liveOutput: accumulatedOutput } + ? { ...tc, liveOutput: output } : tc, ), ); @@ -327,7 +325,7 @@ export function useToolScheduler( : undefined; t.tool - .execute(t.request.args, signal, onOutputChunk) + .execute(t.request.args, signal, updateOutput) .then((result: ToolResult) => { if (signal.aborted) { // TODO(jacobr): avoid stringifying the LLM content. diff --git a/packages/server/src/tools/shell.ts b/packages/server/src/tools/shell.ts index 5e1edd85..38848c4f 100644 --- a/packages/server/src/tools/shell.ts +++ b/packages/server/src/tools/shell.ts @@ -25,6 +25,8 @@ export interface ShellToolParams { } import { spawn } from 'child_process'; +const OUTPUT_UPDATE_INTERVAL_MS = 1000; + export class ShellTool extends BaseTool { static Name: string = 'execute_bash_command'; private whitelist: Set = new Set(); @@ -124,7 +126,7 @@ export class ShellTool extends BaseTool { async execute( params: ShellToolParams, abortSignal: AbortSignal, - onOutputChunk?: (chunk: string) => void, + updateOutput?: (output: string) => void, ): Promise { const validationError = this.validateToolParams(params); if (validationError) { @@ -155,6 +157,19 @@ export class ShellTool extends BaseTool { let exited = false; let stdout = ''; let output = ''; + let lastUpdateTime = Date.now(); + + const appendOutput = (str: string) => { + output += str; + if ( + updateOutput && + Date.now() - lastUpdateTime > OUTPUT_UPDATE_INTERVAL_MS + ) { + updateOutput(output); + lastUpdateTime = Date.now(); + } + }; + shell.stdout.on('data', (data: Buffer) => { // continue to consume post-exit for background processes // removing listeners can overflow OS buffer and block subprocesses @@ -162,10 +177,7 @@ export class ShellTool extends BaseTool { if (!exited) { const str = data.toString(); stdout += str; - output += str; - if (onOutputChunk) { - onOutputChunk(str); - } + appendOutput(str); } }); @@ -174,10 +186,7 @@ export class ShellTool extends BaseTool { if (!exited) { const str = data.toString(); stderr += str; - output += str; - if (onOutputChunk) { - onOutputChunk(str); - } + appendOutput(str); } }); diff --git a/packages/server/src/tools/tools.ts b/packages/server/src/tools/tools.ts index 8ec11bf0..eb1da248 100644 --- a/packages/server/src/tools/tools.ts +++ b/packages/server/src/tools/tools.ts @@ -68,7 +68,7 @@ export interface Tool< execute( params: TParams, signal: AbortSignal, - onOutputChunk?: (chunk: string) => void, + updateOutput?: (output: string) => void, ): Promise; } @@ -154,7 +154,7 @@ export abstract class BaseTool< abstract execute( params: TParams, signal: AbortSignal, - onOutputChunk?: (chunk: string) => void, + updateOutput?: (output: string) => void, ): Promise; }