fix(tui): show tool activity in fullscreen mode via Ink-compatible callback

Replace process.stdout.write-based onToolUse callback (which corrupts
Ink rendering) with a React state-driven approach that shows tool names,
args, and completion status in the streaming content area.
This commit is contained in:
William Valentin
2026-02-10 13:26:47 -08:00
parent e46e8740a1
commit 671ec035e9
+59 -1
View File
@@ -1,4 +1,4 @@
import React, { useState, useCallback, useRef } from 'react';
import React, { useState, useCallback, useRef, useEffect } from 'react';
import { Box, useApp, useInput } from 'ink';
import { StatusBar } from './StatusBar.js';
import { MessageList } from './MessageList.js';
@@ -8,6 +8,34 @@ import type { Message, ModelClient, TokenUsage } from '../../../models/types.js'
import type { ModelRouter } from '../../../models/router.js';
import type { ManagedSession } from '../../../session/index.js';
import type { NativeAgent } from '../../../backends/native/agent.js';
import type { ToolUseEvent } from '../../../backends/native/agent.js';
/** Format a tool name like "gmail.list" -> "Gmail: List" */
function formatToolName(name: string): string {
const parts = name.split('.');
return parts.map((p, i) => {
const capitalized = p.charAt(0).toUpperCase() + p.slice(1);
return i === 0 && parts.length > 1 ? capitalized + ':' : capitalized;
}).join(' ');
}
/** Format tool args as a compact, readable summary. */
function formatToolArgs(args: unknown): string {
if (!args || typeof args !== 'object') return '';
const entries = Object.entries(args as Record<string, unknown>);
if (entries.length === 0) return '';
const parts = entries.map(([key, value]) => {
if (typeof value === 'string') {
const display = value.length > 50 ? value.slice(0, 47) + '...' : value;
return `${key}: "${display}"`;
}
if (typeof value === 'number' || typeof value === 'boolean') {
return `${key}: ${value}`;
}
return `${key}: ${JSON.stringify(value)}`;
});
return parts.join(', ');
}
export interface AppProps {
session: ManagedSession;
@@ -37,6 +65,35 @@ export function App({
const [tokenUsage, setTokenUsage] = useState<TokenUsage>({ inputTokens: 0, outputTokens: 0 });
const [currentModel, setCurrentModel] = useState(model);
const abortRef = useRef(false);
const toolLinesRef = useRef<string[]>([]);
// Set up an Ink-compatible onToolUse callback for the agent.
// This replaces the process.stdout.write callback (which corrupts Ink rendering)
// with one that updates React state to show tool activity in the streaming area.
useEffect(() => {
if (!agent) return;
const handleToolEvent = (event: ToolUseEvent) => {
if (event.type === 'start') {
const label = formatToolName(event.tool);
const argsStr = event.args ? ` (${formatToolArgs(event.args)})` : '';
toolLinesRef.current = [...toolLinesRef.current, `> ${label}${argsStr}`];
setStreamingContent(toolLinesRef.current.join('\n'));
} else if (event.type === 'end' && event.result) {
const icon = event.result.success ? 'done' : 'error';
const detail = event.result.success
? `(${event.result.output.split('\n').length} lines)`
: (event.result.error ?? 'unknown error');
toolLinesRef.current = [...toolLinesRef.current, ` ${icon} ${detail}`];
setStreamingContent(toolLinesRef.current.join('\n'));
}
};
agent.setOnToolUse(handleToolEvent);
return () => {
agent.setOnToolUse(undefined);
};
}, [agent]);
useInput((inputChar, key) => {
if (key.escape) {
@@ -172,6 +229,7 @@ export function App({
// Process response
setIsStreaming(true);
setStreamingContent('');
toolLinesRef.current = [];
abortRef.current = false;
try {