summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/components
diff options
context:
space:
mode:
authorJacob Richman <[email protected]>2025-05-24 00:44:17 -0700
committerGitHub <[email protected]>2025-05-24 00:44:17 -0700
commitb4c16d1f56f4e19fffd3d7608b410570f35045f9 (patch)
treedfda3634f4d266f5472bd37b78362abebe6a9c9e /packages/cli/src/ui/components
parent1c3d9d7623ecff0437db0627cace0cbb421b458a (diff)
Code review comment fixes and some refactors. (#525)
No intentional different behavior aside for tweaks suggested from the code review of #506 Refactor: Extract console message logic to custom hook This commit refactors the console message handling from App.tsx into a new custom hook useConsoleMessages. This change improves the testability of the console message logic and declutters the main App component. Created useConsoleMessages.ts to encapsulate console message state and update logic. Updated App.tsx to utilize the new useConsoleMessages hook. Added unit tests for useConsoleMessages.ts to ensure its functionality. I deleted and started over on LoadingIndicator.test.tsx as I spent way too much time trying to fix it before just regenerating the tests as the code was easier to write tests for from scratch and the existing tests were not that good (I added them in the previous pull request).
Diffstat (limited to 'packages/cli/src/ui/components')
-rw-r--r--packages/cli/src/ui/components/HistoryItemDisplay.tsx5
-rw-r--r--packages/cli/src/ui/components/LoadingIndicator.test.tsx189
-rw-r--r--packages/cli/src/ui/components/LoadingIndicator.tsx16
-rw-r--r--packages/cli/src/ui/components/messages/ToolGroupMessage.tsx9
-rw-r--r--packages/cli/src/ui/components/messages/ToolMessage.test.tsx82
-rw-r--r--packages/cli/src/ui/components/messages/ToolMessage.tsx77
6 files changed, 223 insertions, 155 deletions
diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
index 21ffb5a1..39cc5308 100644
--- a/packages/cli/src/ui/components/HistoryItemDisplay.tsx
+++ b/packages/cli/src/ui/components/HistoryItemDisplay.tsx
@@ -5,7 +5,7 @@
*/
import React from 'react';
-import type { HistoryItem, StreamingState } from '../types.js';
+import type { HistoryItem } from '../types.js';
import { UserMessage } from './messages/UserMessage.js';
import { UserShellMessage } from './messages/UserShellMessage.js';
import { GeminiMessage } from './messages/GeminiMessage.js';
@@ -20,14 +20,12 @@ interface HistoryItemDisplayProps {
item: HistoryItem;
availableTerminalHeight: number;
isPending: boolean;
- streamingState?: StreamingState;
}
export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
item,
availableTerminalHeight,
isPending,
- streamingState,
}) => (
<Box flexDirection="column" key={item.id}>
{/* Render standard message types */}
@@ -62,7 +60,6 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
toolCalls={item.tools}
groupId={item.id}
availableTerminalHeight={availableTerminalHeight}
- streamingState={streamingState}
/>
)}
</Box>
diff --git a/packages/cli/src/ui/components/LoadingIndicator.test.tsx b/packages/cli/src/ui/components/LoadingIndicator.test.tsx
index 6a8880d4..a03fb230 100644
--- a/packages/cli/src/ui/components/LoadingIndicator.test.tsx
+++ b/packages/cli/src/ui/components/LoadingIndicator.test.tsx
@@ -4,91 +4,158 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import React from 'react';
import { render } from 'ink-testing-library';
-import { Text } from 'ink'; // Import Text directly from ink
+import { Text } from 'ink';
import { LoadingIndicator } from './LoadingIndicator.js';
+import {
+ StreamingContext,
+ StreamingContextType,
+} from '../contexts/StreamingContext.js';
+import { StreamingState } from '../types.js';
+import { vi } from 'vitest';
+// Mock ink-spinner
vi.mock('ink-spinner', () => ({
- default: function MockSpinner() {
- return <Text>MockSpinner</Text>;
- },
+ default: () => <Text>MockSpinner</Text>,
}));
+const renderWithContext = (
+ ui: React.ReactElement,
+ streamingStateValue: StreamingState,
+) => {
+ const contextValue: StreamingContextType = {
+ streamingState: streamingStateValue,
+ };
+ return render(
+ <StreamingContext.Provider value={contextValue}>
+ {ui}
+ </StreamingContext.Provider>,
+ );
+};
+
describe('<LoadingIndicator />', () => {
- it('should not render when isLoading is false', () => {
- const { lastFrame } = render(
- <LoadingIndicator
- isLoading={false}
- showSpinner={true}
- currentLoadingPhrase="Loading..."
- elapsedTime={0}
- />,
+ const defaultProps = {
+ currentLoadingPhrase: 'Loading...',
+ elapsedTime: 5,
+ };
+
+ it('should not render when streamingState is Idle', () => {
+ const { lastFrame } = renderWithContext(
+ <LoadingIndicator {...defaultProps} />,
+ StreamingState.Idle,
);
expect(lastFrame()).toBe('');
});
- it('should render spinner, phrase, and time when isLoading is true and showSpinner is true', () => {
- const phrase = 'Processing data...';
- const time = 5;
- const { lastFrame } = render(
- <LoadingIndicator
- isLoading={true}
- showSpinner={true}
- currentLoadingPhrase={phrase}
- elapsedTime={time}
- />,
+ it('should render spinner, phrase, and time when streamingState is Responding', () => {
+ const { lastFrame } = renderWithContext(
+ <LoadingIndicator {...defaultProps} />,
+ StreamingState.Responding,
);
-
const output = lastFrame();
- expect(output).toContain(phrase);
- expect(output).toContain(`(esc to cancel, ${time}s)`);
- // Check for spinner presence by looking for its characteristic characters or structure
- // This is a bit fragile as it depends on Spinner's output.
- // A more robust way would be to mock Spinner and check if it was rendered.
- expect(output).toContain('MockSpinner'); // Check for the mocked spinner text
+ expect(output).toContain('MockSpinner');
+ expect(output).toContain('Loading...');
+ expect(output).toContain('(esc to cancel, 5s)');
});
- it('should render phrase and time but no spinner when isLoading is true and showSpinner is false', () => {
- const phrase = 'Waiting for input...';
- const time = 10;
- const { lastFrame } = render(
- <LoadingIndicator
- isLoading={true}
- showSpinner={false}
- currentLoadingPhrase={phrase}
- elapsedTime={time}
- />,
+ it('should render phrase and time but no spinner when streamingState is WaitingForConfirmation', () => {
+ const props = {
+ currentLoadingPhrase: 'Confirm action',
+ elapsedTime: 10,
+ };
+ const { lastFrame } = renderWithContext(
+ <LoadingIndicator {...props} />,
+ StreamingState.WaitingForConfirmation,
);
const output = lastFrame();
- expect(output).toContain(phrase);
- expect(output).toContain(`(esc to cancel, ${time}s)`);
- // Ensure spinner characters are NOT present
expect(output).not.toContain('MockSpinner');
+ expect(output).toContain('Confirm action');
+ expect(output).not.toContain('(esc to cancel)');
+ expect(output).not.toContain(', 10s');
});
it('should display the currentLoadingPhrase correctly', () => {
- const specificPhrase = 'Almost there!';
- const { lastFrame } = render(
- <LoadingIndicator
- isLoading={true}
- showSpinner={true}
- currentLoadingPhrase={specificPhrase}
- elapsedTime={3}
- />,
+ const props = {
+ currentLoadingPhrase: 'Processing data...',
+ elapsedTime: 3,
+ };
+ const { lastFrame } = renderWithContext(
+ <LoadingIndicator {...props} />,
+ StreamingState.Responding,
+ );
+ expect(lastFrame()).toContain('Processing data...');
+ });
+
+ it('should display the elapsedTime correctly when Responding', () => {
+ const props = {
+ currentLoadingPhrase: 'Working...',
+ elapsedTime: 8,
+ };
+ const { lastFrame } = renderWithContext(
+ <LoadingIndicator {...props} />,
+ StreamingState.Responding,
+ );
+ expect(lastFrame()).toContain('(esc to cancel, 8s)');
+ });
+
+ it('should render rightContent when provided', () => {
+ const rightContent = <Text>Extra Info</Text>;
+ const { lastFrame } = renderWithContext(
+ <LoadingIndicator {...defaultProps} rightContent={rightContent} />,
+ StreamingState.Responding,
);
- expect(lastFrame()).toContain(specificPhrase);
+ expect(lastFrame()).toContain('Extra Info');
});
- it('should display the elapsedTime correctly', () => {
- const specificTime = 7;
- const { lastFrame } = render(
- <LoadingIndicator
- isLoading={true}
- showSpinner={true}
- currentLoadingPhrase="Working..."
- elapsedTime={specificTime}
- />,
+ it('should transition correctly between states using rerender', () => {
+ const { lastFrame, rerender } = renderWithContext(
+ <LoadingIndicator {...defaultProps} />,
+ StreamingState.Idle,
);
- expect(lastFrame()).toContain(`(esc to cancel, ${specificTime}s)`);
+ expect(lastFrame()).toBe(''); // Initial: Idle
+
+ // Transition to Responding
+ rerender(
+ <StreamingContext.Provider
+ value={{ streamingState: StreamingState.Responding }}
+ >
+ <LoadingIndicator
+ currentLoadingPhrase="Now Responding"
+ elapsedTime={2}
+ />
+ </StreamingContext.Provider>,
+ );
+ let output = lastFrame();
+ expect(output).toContain('MockSpinner');
+ expect(output).toContain('Now Responding');
+ expect(output).toContain('(esc to cancel, 2s)');
+
+ // Transition to WaitingForConfirmation
+ rerender(
+ <StreamingContext.Provider
+ value={{ streamingState: StreamingState.WaitingForConfirmation }}
+ >
+ <LoadingIndicator
+ currentLoadingPhrase="Please Confirm"
+ elapsedTime={15}
+ />
+ </StreamingContext.Provider>,
+ );
+ output = lastFrame();
+ expect(output).not.toContain('MockSpinner');
+ expect(output).toContain('Please Confirm');
+ expect(output).not.toContain('(esc to cancel)');
+ expect(output).not.toContain(', 15s');
+
+ // Transition back to Idle
+ rerender(
+ <StreamingContext.Provider
+ value={{ streamingState: StreamingState.Idle }}
+ >
+ <LoadingIndicator {...defaultProps} />
+ </StreamingContext.Provider>,
+ );
+ expect(lastFrame()).toBe('');
});
});
diff --git a/packages/cli/src/ui/components/LoadingIndicator.tsx b/packages/cli/src/ui/components/LoadingIndicator.tsx
index b0c24f80..d24b6a56 100644
--- a/packages/cli/src/ui/components/LoadingIndicator.tsx
+++ b/packages/cli/src/ui/components/LoadingIndicator.tsx
@@ -8,36 +8,38 @@ import React from 'react';
import { Box, Text } from 'ink';
import Spinner from 'ink-spinner';
import { Colors } from '../colors.js';
+import { useStreamingContext } from '../contexts/StreamingContext.js';
+import { StreamingState } from '../types.js';
interface LoadingIndicatorProps {
- isLoading: boolean;
- showSpinner: boolean;
currentLoadingPhrase: string;
elapsedTime: number;
rightContent?: React.ReactNode;
}
export const LoadingIndicator: React.FC<LoadingIndicatorProps> = ({
- isLoading,
- showSpinner,
currentLoadingPhrase,
elapsedTime,
rightContent,
}) => {
- if (!isLoading) {
+ const { streamingState } = useStreamingContext();
+
+ if (streamingState === StreamingState.Idle) {
return null;
}
return (
<Box marginTop={1} paddingLeft={0}>
- {showSpinner && (
+ {streamingState === StreamingState.Responding && (
<Box marginRight={1}>
<Spinner type="dots" />
</Box>
)}
<Text color={Colors.AccentPurple}>
{currentLoadingPhrase}
- {isLoading && ` (esc to cancel, ${elapsedTime}s)`}
+ {streamingState === StreamingState.WaitingForConfirmation
+ ? ''
+ : ` (esc to cancel, ${elapsedTime}s)`}
</Text>
<Box flexGrow={1}>{/* Spacer */}</Box>
{rightContent && <Box>{rightContent}</Box>}
diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
index c6c8b874..8bcde3bb 100644
--- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
@@ -6,11 +6,7 @@
import React, { useMemo } from 'react';
import { Box } from 'ink';
-import {
- IndividualToolCallDisplay,
- StreamingState,
- ToolCallStatus,
-} from '../../types.js';
+import { IndividualToolCallDisplay, ToolCallStatus } from '../../types.js';
import { ToolMessage } from './ToolMessage.js';
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
import { Colors } from '../../colors.js';
@@ -19,14 +15,12 @@ interface ToolGroupMessageProps {
groupId: number;
toolCalls: IndividualToolCallDisplay[];
availableTerminalHeight: number;
- streamingState?: StreamingState;
}
// Main component renders the border and maps the tools using ToolMessage
export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
toolCalls,
availableTerminalHeight,
- streamingState,
}) => {
const hasPending = !toolCalls.every(
(t) => t.status === ToolCallStatus.Success,
@@ -78,7 +72,6 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
? 'low'
: 'medium'
}
- streamingState={streamingState}
/>
</Box>
{tool.status === ToolCallStatus.Confirming &&
diff --git a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx
index 1c81cc36..10380ad4 100644
--- a/packages/cli/src/ui/components/messages/ToolMessage.test.tsx
+++ b/packages/cli/src/ui/components/messages/ToolMessage.test.tsx
@@ -8,6 +8,7 @@ import { render } from 'ink-testing-library';
import { ToolMessage, ToolMessageProps } from './ToolMessage.js';
import { StreamingState, ToolCallStatus } from '../../types.js';
import { Text } from 'ink';
+import { StreamingContext } from '../../contexts/StreamingContext.js';
// Mock child components or utilities if they are complex or have side effects
vi.mock('ink-spinner', () => ({
@@ -28,6 +29,17 @@ vi.mock('../../utils/MarkdownDisplay.js', () => ({
},
}));
+// Helper to render with context
+const renderWithContext = (
+ ui: React.ReactElement,
+ streamingState: StreamingState,
+) =>
+ render(
+ <StreamingContext.Provider value={{ streamingState }}>
+ {ui}
+ </StreamingContext.Provider>,
+ );
+
describe('<ToolMessage />', () => {
const baseProps: ToolMessageProps = {
callId: 'tool-123',
@@ -38,11 +50,13 @@ describe('<ToolMessage />', () => {
availableTerminalHeight: 20,
confirmationDetails: undefined,
emphasis: 'medium',
- streamingState: StreamingState.Idle,
};
it('renders basic tool information', () => {
- const { lastFrame } = render(<ToolMessage {...baseProps} />);
+ const { lastFrame } = renderWithContext(
+ <ToolMessage {...baseProps} />,
+ StreamingState.Idle,
+ );
const output = lastFrame();
expect(output).toContain('✔'); // Success indicator
expect(output).toContain('test-tool');
@@ -52,73 +66,72 @@ describe('<ToolMessage />', () => {
describe('ToolStatusIndicator rendering', () => {
it('shows ✔ for Success status', () => {
- const { lastFrame } = render(
+ const { lastFrame } = renderWithContext(
<ToolMessage {...baseProps} status={ToolCallStatus.Success} />,
+ StreamingState.Idle,
);
expect(lastFrame()).toContain('✔');
});
it('shows o for Pending status', () => {
- const { lastFrame } = render(
+ const { lastFrame } = renderWithContext(
<ToolMessage {...baseProps} status={ToolCallStatus.Pending} />,
+ StreamingState.Idle,
);
expect(lastFrame()).toContain('o');
});
it('shows ? for Confirming status', () => {
- const { lastFrame } = render(
+ const { lastFrame } = renderWithContext(
<ToolMessage {...baseProps} status={ToolCallStatus.Confirming} />,
+ StreamingState.Idle,
);
expect(lastFrame()).toContain('?');
});
it('shows - for Canceled status', () => {
- const { lastFrame } = render(
+ const { lastFrame } = renderWithContext(
<ToolMessage {...baseProps} status={ToolCallStatus.Canceled} />,
+ StreamingState.Idle,
);
expect(lastFrame()).toContain('-');
});
it('shows x for Error status', () => {
- const { lastFrame } = render(
+ const { lastFrame } = renderWithContext(
<ToolMessage {...baseProps} status={ToolCallStatus.Error} />,
+ StreamingState.Idle,
);
expect(lastFrame()).toContain('x');
});
- it('shows MockSpinner for Executing status when streamingState is Idle', () => {
- const { lastFrame } = render(
- <ToolMessage
- {...baseProps}
- status={ToolCallStatus.Executing}
- streamingState={StreamingState.Idle}
- />,
+ it('shows paused spiner for Executing status when streamingState is Idle', () => {
+ const { lastFrame } = renderWithContext(
+ <ToolMessage {...baseProps} status={ToolCallStatus.Executing} />,
+ StreamingState.Idle,
);
- expect(lastFrame()).toContain('MockSpinner');
+ expect(lastFrame()).toContain('⠇');
+ expect(lastFrame()).not.toContain('MockSpinner');
expect(lastFrame()).not.toContain('✔');
});
- it('shows MockSpinner for Executing status when streamingState is undefined (default behavior)', () => {
- const { lastFrame } = render(
+ it('shows paused spiner for Executing status when streamingState is WaitingForConfirmation', () => {
+ const { lastFrame } = renderWithContext(
<ToolMessage {...baseProps} status={ToolCallStatus.Executing} />,
+ StreamingState.WaitingForConfirmation,
);
- expect(lastFrame()).toContain('MockSpinner');
+ expect(lastFrame()).toContain('⠇');
+ expect(lastFrame()).not.toContain('MockSpinner');
expect(lastFrame()).not.toContain('✔');
});
- it('shows ✔ (paused/confirmed look) for Executing status when streamingState is Responding', () => {
- // This is the key change from the commit: if the overall app is still responding
- // (e.g., waiting for other tool confirmations), an already confirmed and executing tool
- // should show a static checkmark to avoid spinner flicker.
- const { lastFrame } = render(
- <ToolMessage
- {...baseProps}
- status={ToolCallStatus.Executing}
- streamingState={StreamingState.Responding} // Simulate app still responding
- />,
+ it('shows MockSpinner for Executing status when streamingState is Responding', () => {
+ const { lastFrame } = renderWithContext(
+ <ToolMessage {...baseProps} status={ToolCallStatus.Executing} />,
+ StreamingState.Responding, // Simulate app still responding
);
- expect(lastFrame()).toContain('✔'); // Should be a checkmark, not spinner
- expect(lastFrame()).not.toContain('MockSpinner');
+ expect(lastFrame()).toContain('MockSpinner');
+ expect(lastFrame()).not.toContain('✔');
});
});
@@ -127,22 +140,25 @@ describe('<ToolMessage />', () => {
fileDiff: '--- a/file.txt\n+++ b/file.txt\n@@ -1 +1 @@\n-old\n+new',
fileName: 'file.txt',
};
- const { lastFrame } = render(
+ const { lastFrame } = renderWithContext(
<ToolMessage {...baseProps} resultDisplay={diffResult} />,
+ StreamingState.Idle,
);
// Check that the output contains the MockDiff content as part of the whole message
expect(lastFrame()).toMatch(/MockDiff:--- a\/file\.txt/);
});
it('renders emphasis correctly', () => {
- const { lastFrame: highEmphasisFrame } = render(
+ const { lastFrame: highEmphasisFrame } = renderWithContext(
<ToolMessage {...baseProps} emphasis="high" />,
+ StreamingState.Idle,
);
// Check for trailing indicator or specific color if applicable (Colors are not easily testable here)
expect(highEmphasisFrame()).toContain('←'); // Trailing indicator for high emphasis
- const { lastFrame: lowEmphasisFrame } = render(
+ const { lastFrame: lowEmphasisFrame } = renderWithContext(
<ToolMessage {...baseProps} emphasis="low" />,
+ StreamingState.Idle,
);
// For low emphasis, the name and description might be dimmed (check for dimColor if possible)
// This is harder to assert directly in text output without color checks.
diff --git a/packages/cli/src/ui/components/messages/ToolMessage.tsx b/packages/cli/src/ui/components/messages/ToolMessage.tsx
index 49743190..775b4177 100644
--- a/packages/cli/src/ui/components/messages/ToolMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolMessage.tsx
@@ -15,6 +15,7 @@ import {
import { DiffRenderer } from './DiffRenderer.js';
import { Colors } from '../../colors.js';
import { MarkdownDisplay } from '../../utils/MarkdownDisplay.js';
+import { useStreamingContext } from '../../contexts/StreamingContext.js';
const STATIC_HEIGHT = 1;
const RESERVED_LINE_COUNT = 5; // for tool name, status, padding etc.
@@ -25,7 +26,6 @@ export type TextEmphasis = 'high' | 'medium' | 'low';
export interface ToolMessageProps extends IndividualToolCallDisplay {
availableTerminalHeight: number;
emphasis?: TextEmphasis;
- streamingState?: StreamingState;
}
export const ToolMessage: React.FC<ToolMessageProps> = ({
@@ -35,7 +35,6 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
status,
availableTerminalHeight,
emphasis = 'medium',
- streamingState,
}) => {
const contentHeightEstimate =
availableTerminalHeight - STATIC_HEIGHT - RESERVED_LINE_COUNT;
@@ -63,7 +62,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
<Box paddingX={1} paddingY={0} flexDirection="column">
<Box minHeight={1}>
{/* Status Indicator */}
- <ToolStatusIndicator status={status} streamingState={streamingState} />
+ <ToolStatusIndicator status={status} />
<ToolInfo
name={name}
status={status}
@@ -107,48 +106,42 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
type ToolStatusIndicatorProps = {
status: ToolCallStatus;
- streamingState?: StreamingState;
};
const ToolStatusIndicator: React.FC<ToolStatusIndicatorProps> = ({
status,
- streamingState,
-}) => (
- <Box minWidth={STATUS_INDICATOR_WIDTH}>
- {status === ToolCallStatus.Pending && (
- <Text color={Colors.AccentGreen}>o</Text>
- )}
- {status === ToolCallStatus.Executing &&
- (streamingState === StreamingState.Responding ? (
- // If the tool is responding that means the user has already confirmed
- // this tool call, so we can show a checkmark. The call won't complete
- // executing until all confirmations are done. Showing a spinner would
- // be misleading as the task is not actually executing at the moment
- // and also has flickering issues due to Ink rendering limitations.
- // If this hack becomes a problem, we can always add an additional prop
- // indicating that the tool was indeed confirmed. If the tool was not
- // confirmed we could show a paused version of the spinner.
- <Text color={Colors.Gray}>✔</Text>
- ) : (
- <Spinner type="dots" />
- ))}
- {status === ToolCallStatus.Success && (
- <Text color={Colors.AccentGreen}>✔</Text>
- )}
- {status === ToolCallStatus.Confirming && (
- <Text color={Colors.AccentYellow}>?</Text>
- )}
- {status === ToolCallStatus.Canceled && (
- <Text color={Colors.AccentYellow} bold>
- -
- </Text>
- )}
- {status === ToolCallStatus.Error && (
- <Text color={Colors.AccentRed} bold>
- x
- </Text>
- )}
- </Box>
-);
+}) => {
+ const { streamingState } = useStreamingContext();
+ return (
+ <Box minWidth={STATUS_INDICATOR_WIDTH}>
+ {status === ToolCallStatus.Pending && (
+ <Text color={Colors.AccentGreen}>o</Text>
+ )}
+ {status === ToolCallStatus.Executing &&
+ (streamingState === StreamingState.Responding ? (
+ <Spinner type="dots" />
+ ) : (
+ // Paused spinner to avoid flicker.
+ <Text>⠇</Text>
+ ))}
+ {status === ToolCallStatus.Success && (
+ <Text color={Colors.AccentGreen}>✔</Text>
+ )}
+ {status === ToolCallStatus.Confirming && (
+ <Text color={Colors.AccentYellow}>?</Text>
+ )}
+ {status === ToolCallStatus.Canceled && (
+ <Text color={Colors.AccentYellow} bold>
+ -
+ </Text>
+ )}
+ {status === ToolCallStatus.Error && (
+ <Text color={Colors.AccentRed} bold>
+ x
+ </Text>
+ )}
+ </Box>
+ );
+};
type ToolInfo = {
name: string;