feat(ui): standardize semantic focus colors and enhance history visibility (#20745)

Co-authored-by: jacob314 <jacob314@gmail.com>
This commit is contained in:
Keith Guerin
2026-03-03 16:10:09 -08:00
committed by GitHub
parent 75737c1b44
commit d25088956d
70 changed files with 1427 additions and 406 deletions
@@ -65,7 +65,7 @@ describe('<ShellToolMessage />', () => {
['SHELL_COMMAND_NAME', SHELL_COMMAND_NAME],
['SHELL_TOOL_NAME', SHELL_TOOL_NAME],
])('clicks inside the shell area sets focus for %s', async (_, name) => {
const { lastFrame, simulateClick } = renderShell(
const { lastFrame, simulateClick, unmount } = renderShell(
{ name },
{ mouseEventsEnabled: true },
);
@@ -79,6 +79,7 @@ describe('<ShellToolMessage />', () => {
await waitFor(() => {
expect(mockSetEmbeddedShellFocused).toHaveBeenCalledWith(true);
});
unmount();
});
it('resets focus when shell finishes', async () => {
let updateStatus: (s: CoreToolCallStatus) => void = () => {};
@@ -91,7 +92,7 @@ describe('<ShellToolMessage />', () => {
return <ShellToolMessage {...baseProps} status={status} ptyId={1} />;
};
const { lastFrame } = renderWithProviders(<Wrapper />, {
const { lastFrame, unmount } = renderWithProviders(<Wrapper />, {
uiActions,
uiState: {
streamingState: StreamingState.Idle,
@@ -115,6 +116,7 @@ describe('<ShellToolMessage />', () => {
expect(mockSetEmbeddedShellFocused).toHaveBeenCalledWith(false);
expect(lastFrame()).not.toContain('(Shift+Tab to unfocus)');
});
unmount();
});
});
@@ -164,9 +166,13 @@ describe('<ShellToolMessage />', () => {
},
],
])('%s', async (_, props, options) => {
const { lastFrame, waitUntilReady } = renderShell(props, options);
const { lastFrame, waitUntilReady, unmount } = renderShell(
props,
options,
);
await waitUntilReady();
expect(lastFrame()).toMatchSnapshot();
unmount();
});
});
@@ -197,7 +203,7 @@ describe('<ShellToolMessage />', () => {
false,
],
])('%s', async (_, availableTerminalHeight, expectedMaxLines, focused) => {
const { lastFrame, waitUntilReady } = renderShell(
const { lastFrame, waitUntilReady, unmount } = renderShell(
{
resultDisplay: LONG_OUTPUT,
renderOutputAsMarkdown: false,
@@ -218,10 +224,11 @@ describe('<ShellToolMessage />', () => {
const frame = lastFrame();
expect(frame.match(/Line \d+/g)?.length).toBe(expectedMaxLines);
expect(frame).toMatchSnapshot();
unmount();
});
it('fully expands in standard mode when availableTerminalHeight is undefined', async () => {
const { lastFrame } = renderShell(
const { lastFrame, unmount } = renderShell(
{
resultDisplay: LONG_OUTPUT,
renderOutputAsMarkdown: false,
@@ -236,10 +243,11 @@ describe('<ShellToolMessage />', () => {
// Should show all 100 lines
expect(frame.match(/Line \d+/g)?.length).toBe(100);
});
unmount();
});
it('fully expands in alternate buffer mode when constrainHeight is false and isExpandable is true', async () => {
const { lastFrame, waitUntilReady } = renderShell(
const { lastFrame, waitUntilReady, unmount } = renderShell(
{
resultDisplay: LONG_OUTPUT,
renderOutputAsMarkdown: false,
@@ -262,10 +270,11 @@ describe('<ShellToolMessage />', () => {
expect(frame.match(/Line \d+/g)?.length).toBe(100);
});
expect(lastFrame()).toMatchSnapshot();
unmount();
});
it('stays constrained in alternate buffer mode when isExpandable is false even if constrainHeight is false', async () => {
const { lastFrame, waitUntilReady } = renderShell(
const { lastFrame, waitUntilReady, unmount } = renderShell(
{
resultDisplay: LONG_OUTPUT,
renderOutputAsMarkdown: false,
@@ -288,6 +297,7 @@ describe('<ShellToolMessage />', () => {
expect(frame.match(/Line \d+/g)?.length).toBe(15);
});
expect(lastFrame()).toMatchSnapshot();
unmount();
});
});
});
@@ -125,7 +125,11 @@ export const ShellToolMessage: React.FC<ShellToolMessageProps> = ({
borderDimColor={borderDimColor}
containerRef={headerRef}
>
<ToolStatusIndicator status={status} name={name} />
<ToolStatusIndicator
status={status}
name={name}
isFocused={isThisShellFocused}
/>
<ToolInfo
name={name}
@@ -88,7 +88,11 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
borderColor={borderColor}
borderDimColor={borderDimColor}
>
<ToolStatusIndicator status={status} name={name} />
<ToolStatusIndicator
status={status}
name={name}
isFocused={isThisShellFocused}
/>
<ToolInfo
name={name}
status={status}
@@ -7,7 +7,7 @@
import React, { useState, useEffect } from 'react';
import { Box, Text } from 'ink';
import { ToolCallStatus, mapCoreStatusToDisplayStatus } from '../../types.js';
import { GeminiRespondingSpinner } from '../GeminiRespondingSpinner.js';
import { CliSpinner } from '../CliSpinner.js';
import {
SHELL_COMMAND_NAME,
SHELL_NAME,
@@ -123,7 +123,7 @@ export const FocusHint: React.FC<{
return (
<Box marginLeft={1} flexShrink={0}>
<Text color={theme.text.accent}>
<Text color={isThisShellFocused ? theme.ui.focus : theme.ui.active}>
{isThisShellFocused
? `(${formatCommand(Command.UNFOCUS_SHELL_INPUT)} to unfocus)`
: `(${formatCommand(Command.FOCUS_SHELL_INPUT)} to focus)`}
@@ -137,15 +137,21 @@ export type TextEmphasis = 'high' | 'medium' | 'low';
type ToolStatusIndicatorProps = {
status: CoreToolCallStatus;
name: string;
isFocused?: boolean;
};
export const ToolStatusIndicator: React.FC<ToolStatusIndicatorProps> = ({
status: coreStatus,
name,
isFocused,
}) => {
const status = mapCoreStatusToDisplayStatus(coreStatus);
const isShell = isShellTool(name);
const statusColor = isShell ? theme.ui.symbol : theme.status.warning;
const statusColor = isFocused
? theme.ui.focus
: isShell
? theme.ui.active
: theme.status.warning;
return (
<Box minWidth={STATUS_INDICATOR_WIDTH}>
@@ -153,10 +159,9 @@ export const ToolStatusIndicator: React.FC<ToolStatusIndicatorProps> = ({
<Text color={theme.status.success}>{TOOL_STATUS.PENDING}</Text>
)}
{status === ToolCallStatus.Executing && (
<GeminiRespondingSpinner
spinnerType="toggle"
nonRespondingDisplay={TOOL_STATUS.EXECUTING}
/>
<Text color={statusColor}>
<CliSpinner type="toggle" />
</Text>
)}
{status === ToolCallStatus.Success && (
<Text color={theme.status.success} aria-label={'Success:'}>
@@ -29,7 +29,7 @@ export const UserMessage: React.FC<UserMessageProps> = ({ text, width }) => {
const config = useConfig();
const useBackgroundColor = config.getUseBackgroundColor();
const textColor = isSlashCommand ? theme.text.accent : theme.text.secondary;
const textColor = isSlashCommand ? theme.text.accent : theme.text.primary;
const displayText = useMemo(() => {
if (!text) return text;
@@ -7,7 +7,7 @@ Note: Command contains redirection which can be undesirable.
Tip: Toggle auto-edit (Shift+Tab) to allow redirection in the future.
Allow execution of: 'echo, redirection (>)'?
● 1. Allow once
● 1. Allow once
2. Allow for this session
3. No, suggest changes (esc)
"
@@ -2,7 +2,7 @@
exports[`<ShellToolMessage /> > Height Constraints > defaults to ACTIVE_SHELL_MAX_LINES in alternate buffer when availableTerminalHeight is undefined 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
Shell Command A shell command │
Shell Command A shell command │
│ │
│ Line 86 │
│ Line 87 │
@@ -131,7 +131,7 @@ exports[`<ShellToolMessage /> > Height Constraints > fully expands in alternate
exports[`<ShellToolMessage /> > Height Constraints > respects availableTerminalHeight when it is smaller than ACTIVE_SHELL_MAX_LINES 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
Shell Command A shell command │
Shell Command A shell command │
│ │
│ Line 93 │
│ Line 94 │
@@ -168,7 +168,7 @@ exports[`<ShellToolMessage /> > Height Constraints > stays constrained in altern
exports[`<ShellToolMessage /> > Height Constraints > uses ACTIVE_SHELL_MAX_LINES when availableTerminalHeight is large 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
Shell Command A shell command │
Shell Command A shell command │
│ │
│ Line 86 │
│ Line 87 │
@@ -190,7 +190,7 @@ exports[`<ShellToolMessage /> > Height Constraints > uses ACTIVE_SHELL_MAX_LINES
exports[`<ShellToolMessage /> > Height Constraints > uses full availableTerminalHeight when focused in alternate buffer mode 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
Shell Command A shell command (Shift+Tab to unfocus) │
Shell Command A shell command (Shift+Tab to unfocus) │
│ │
│ Line 3 │
│ Line 4 │
@@ -295,7 +295,7 @@ exports[`<ShellToolMessage /> > Height Constraints > uses full availableTerminal
exports[`<ShellToolMessage /> > Snapshots > renders in Alternate Buffer mode while focused 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
Shell Command A shell command (Shift+Tab to unfocus) │
Shell Command A shell command (Shift+Tab to unfocus) │
│ │
│ Test result │
"
@@ -303,7 +303,7 @@ exports[`<ShellToolMessage /> > Snapshots > renders in Alternate Buffer mode whi
exports[`<ShellToolMessage /> > Snapshots > renders in Alternate Buffer mode while unfocused 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
Shell Command A shell command │
Shell Command A shell command │
│ │
│ Test result │
"
@@ -319,7 +319,7 @@ exports[`<ShellToolMessage /> > Snapshots > renders in Error state 1`] = `
exports[`<ShellToolMessage /> > Snapshots > renders in Executing state 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
Shell Command A shell command │
Shell Command A shell command │
│ │
│ Test result │
"
@@ -6,7 +6,7 @@ ls -la
whoami
Allow execution of 3 commands?
● 1. Allow once
● 1. Allow once
2. Allow for this session
3. No, suggest changes (esc)
"
@@ -19,7 +19,7 @@ URLs to fetch:
- https://raw.githubusercontent.com/google/gemini-react/main/README.md
Do you want to proceed?
● 1. Allow once
● 1. Allow once
2. Allow for this session
3. No, suggest changes (esc)
"
@@ -29,7 +29,7 @@ exports[`ToolConfirmationMessage > should not display urls if prompt and url are
"https://example.com
Do you want to proceed?
● 1. Allow once
● 1. Allow once
2. Allow for this session
3. No, suggest changes (esc)
"
@@ -40,7 +40,7 @@ exports[`ToolConfirmationMessage > should strip BiDi characters from MCP tool an
Tool: testtool
Allow execution of MCP tool "testtool" from server "testserver"?
● 1. Allow once
● 1. Allow once
2. Allow tool for this session
3. Allow all server tools for this session
4. No, suggest changes (esc)
@@ -55,7 +55,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for edit confirmations'
╰──────────────────────────────────────────────────────────────────────────────╯
Apply this change?
● 1. Allow once
● 1. Allow once
2. Modify with external editor
3. No, suggest changes (esc)
"
@@ -69,7 +69,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for edit confirmations'
╰──────────────────────────────────────────────────────────────────────────────╯
Apply this change?
● 1. Allow once
● 1. Allow once
2. Allow for this session
3. Modify with external editor
4. No, suggest changes (esc)
@@ -80,7 +80,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for exec confirmations'
"echo "hello"
Allow execution of: 'echo'?
● 1. Allow once
● 1. Allow once
2. No, suggest changes (esc)
"
`;
@@ -89,7 +89,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for exec confirmations'
"echo "hello"
Allow execution of: 'echo'?
● 1. Allow once
● 1. Allow once
2. Allow for this session
3. No, suggest changes (esc)
"
@@ -99,7 +99,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for info confirmations'
"https://example.com
Do you want to proceed?
● 1. Allow once
● 1. Allow once
2. No, suggest changes (esc)
"
`;
@@ -108,7 +108,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for info confirmations'
"https://example.com
Do you want to proceed?
● 1. Allow once
● 1. Allow once
2. Allow for this session
3. No, suggest changes (esc)
"
@@ -119,7 +119,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for mcp confirmations' >
Tool: test-tool
Allow execution of MCP tool "test-tool" from server "test-server"?
● 1. Allow once
● 1. Allow once
2. No, suggest changes (esc)
"
`;
@@ -129,7 +129,7 @@ exports[`ToolConfirmationMessage > with folder trust > 'for mcp confirmations' >
Tool: test-tool
Allow execution of MCP tool "test-tool" from server "test-server"?
● 1. Allow once
● 1. Allow once
2. Allow tool for this session
3. Allow all server tools for this session
4. No, suggest changes (esc)
@@ -71,7 +71,7 @@ exports[`<ToolGroupMessage /> > Golden Snapshots > renders mixed tool calls incl
│ │
│ Test result │
│ │
run_shell_command Run command │
run_shell_command Run command │
│ │
│ Test result │
╰──────────────────────────────────────────────────────────────────────────╯
@@ -29,7 +29,7 @@ exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows - for Canceled
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows MockRespondingSpinner for Executing status when streamingState is Responding 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
MockRespondingSpinnertest-tool A tool for testing │
test-tool A tool for testing
│ │
│ Test result │
"
@@ -45,7 +45,7 @@ exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows o for Pending s
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows paused spinner for Executing status when streamingState is Idle 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
MockRespondingSpinnertest-tool A tool for testing │
test-tool A tool for testing
│ │
│ Test result │
"
@@ -53,7 +53,7 @@ exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows paused spinner
exports[`<ToolMessage /> > ToolStatusIndicator rendering > shows paused spinner for Executing status when streamingState is WaitingForConfirmation 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
MockRespondingSpinnertest-tool A tool for testing │
test-tool A tool for testing
│ │
│ Test result │
"
@@ -94,7 +94,7 @@ exports[`<ToolMessage /> > renders DiffRenderer for diff results 1`] = `
exports[`<ToolMessage /> > renders McpProgressIndicator with percentage and message for executing tools 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
MockRespondingSpinnertest-tool A tool for testing │
test-tool A tool for testing
│ │
│ ████████░░░░░░░░░░░░ 42% │
│ Working on it... │
@@ -128,7 +128,7 @@ exports[`<ToolMessage /> > renders emphasis correctly 2`] = `
exports[`<ToolMessage /> > renders indeterminate progress when total is missing 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
MockRespondingSpinnertest-tool A tool for testing │
test-tool A tool for testing
│ │
│ ███████░░░░░░░░░░░░░ 7 │
│ Test result │
@@ -137,7 +137,7 @@ exports[`<ToolMessage /> > renders indeterminate progress when total is missing
exports[`<ToolMessage /> > renders only percentage when progressMessage is missing 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
MockRespondingSpinnertest-tool A tool for testing │
test-tool A tool for testing
│ │
│ ███████████████░░░░░ 75% │
│ Test result │
@@ -2,63 +2,63 @@
exports[`Focus Hint > 'ShellToolMessage' > shows focus hint after delay even with NO output > after-delay-no-output 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
Shell Command A tool for testing (Tab to focus) │
Shell Command A tool for testing (Tab to focus) │
│ │
"
`;
exports[`Focus Hint > 'ShellToolMessage' > shows focus hint after delay even with NO output > initial-no-output 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
Shell Command A tool for testing │
Shell Command A tool for testing │
│ │
"
`;
exports[`Focus Hint > 'ShellToolMessage' > shows focus hint after delay with output > after-delay-with-output 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
Shell Command A tool for testing (Tab to focus) │
Shell Command A tool for testing (Tab to focus) │
│ │
"
`;
exports[`Focus Hint > 'ShellToolMessage' > shows focus hint after delay with output > initial-with-output 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
Shell Command A tool for testing │
Shell Command A tool for testing │
│ │
"
`;
exports[`Focus Hint > 'ToolMessage' > shows focus hint after delay even with NO output > after-delay-no-output 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
Shell Command A tool for testing (Tab to focus) │
Shell Command A tool for testing (Tab to focus) │
│ │
"
`;
exports[`Focus Hint > 'ToolMessage' > shows focus hint after delay even with NO output > initial-no-output 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
Shell Command A tool for testing │
Shell Command A tool for testing │
│ │
"
`;
exports[`Focus Hint > 'ToolMessage' > shows focus hint after delay with output > after-delay-with-output 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
Shell Command A tool for testing (Tab to focus) │
Shell Command A tool for testing (Tab to focus) │
│ │
"
`;
exports[`Focus Hint > 'ToolMessage' > shows focus hint after delay with output > initial-with-output 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
Shell Command A tool for testing │
Shell Command A tool for testing │
│ │
"
`;
exports[`Focus Hint > handles long descriptions by shrinking them to show the focus hint > long-description 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
Shell Command AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA… (Tab to focus) │
Shell Command AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA… (Tab to focus) │
│ │
"
`;