From d25088956d110a6a7e046e6383d522d2b93fe8bf Mon Sep 17 00:00:00 2001 From: Keith Guerin Date: Tue, 3 Mar 2026 16:10:09 -0800 Subject: [PATCH] feat(ui): standardize semantic focus colors and enhance history visibility (#20745) Co-authored-by: jacob314 --- packages/cli/src/ui/auth/ApiAuthDialog.tsx | 2 +- packages/cli/src/ui/auth/AuthDialog.tsx | 4 +- .../ui/components/BackgroundShellDisplay.tsx | 4 +- .../src/ui/components/ColorsDisplay.test.tsx | 118 ++++++++ .../cli/src/ui/components/ColorsDisplay.tsx | 277 ++++++++++++++++++ .../ui/components/GradientRegression.test.tsx | 5 + .../cli/src/ui/components/Header.test.tsx | 4 +- .../cli/src/ui/components/InputPrompt.tsx | 2 +- .../src/ui/components/LoadingIndicator.tsx | 32 +- .../components/LogoutConfirmationDialog.tsx | 2 +- .../src/ui/components/MainContent.test.tsx | 42 ++- .../cli/src/ui/components/SessionBrowser.tsx | 8 +- .../src/ui/components/SuggestionsDisplay.tsx | 8 +- .../src/ui/components/ThemeDialog.test.tsx | 40 ++- .../cli/src/ui/components/ThemeDialog.tsx | 76 ++--- .../src/ui/components/ThemedGradient.test.tsx | 4 + .../__snapshots__/AskUserDialog.test.tsx.snap | 95 +++++- .../ExitPlanModeDialog.test.tsx.snap | 140 ++++++++- .../__snapshots__/InputPrompt.test.tsx.snap | 33 ++- .../LoadingIndicator.test.tsx.snap | 2 +- .../__snapshots__/MainContent.test.tsx.snap | 8 +- .../SessionBrowser.test.tsx.snap | 4 +- ...tings-list-with-visual-indicators.snap.svg | 20 +- ...bility-settings-enabled-correctly.snap.svg | 20 +- ...olean-settings-disabled-correctly.snap.svg | 20 +- ...ld-render-default-state-correctly.snap.svg | 20 +- ...ing-settings-configured-correctly.snap.svg | 20 +- ...cused-on-scope-selector-correctly.snap.svg | 6 + ...ean-and-number-settings-correctly.snap.svg | 20 +- ...s-and-security-settings-correctly.snap.svg | 20 +- ...oolean-settings-enabled-correctly.snap.svg | 20 +- .../SuggestionsDisplay.test.tsx.snap | 8 +- .../__snapshots__/ThemeDialog.test.tsx.snap | 89 +++++- .../messages/ShellToolMessage.test.tsx | 24 +- .../components/messages/ShellToolMessage.tsx | 6 +- .../ui/components/messages/ToolMessage.tsx | 6 +- .../src/ui/components/messages/ToolShared.tsx | 19 +- .../ui/components/messages/UserMessage.tsx | 2 +- .../RedirectionConfirmation.test.tsx.snap | 2 +- .../ShellToolMessage.test.tsx.snap | 14 +- .../ToolConfirmationMessage.test.tsx.snap | 24 +- .../ToolGroupMessage.test.tsx.snap | 2 +- .../__snapshots__/ToolMessage.test.tsx.snap | 12 +- .../ToolMessageFocusHint.test.tsx.snap | 18 +- .../shared/BaseSelectionList.test.tsx | 16 +- .../components/shared/BaseSelectionList.tsx | 12 +- .../components/shared/BaseSettingsDialog.tsx | 21 +- .../DescriptiveRadioButtonSelect.test.tsx | 6 + .../shared/RadioButtonSelect.test.tsx | 2 + packages/cli/src/ui/constants.ts | 1 + packages/cli/src/ui/hooks/useBanner.test.ts | 3 + packages/cli/src/ui/themes/ansi.ts | 1 + packages/cli/src/ui/themes/color-utils.ts | 155 ++-------- packages/cli/src/ui/themes/github-light.ts | 1 + packages/cli/src/ui/themes/holiday.ts | 1 + packages/cli/src/ui/themes/no-color.ts | 5 +- packages/cli/src/ui/themes/semantic-tokens.ts | 12 +- packages/cli/src/ui/themes/solarized-dark.ts | 9 +- packages/cli/src/ui/themes/solarized-light.ts | 9 +- packages/cli/src/ui/themes/theme-manager.ts | 15 +- packages/cli/src/ui/themes/theme.ts | 190 +++++++++++- packages/cli/src/ui/themes/xcode.ts | 1 + ...-search-dialog-google_web_search-.snap.svg | 2 +- ...der-SVG-snapshot-for-a-shell-tool.snap.svg | 18 +- ...pty-slice-following-a-search-tool.snap.svg | 2 +- .../__snapshots__/borderStyles.test.tsx.snap | 6 +- .../cli/src/ui/utils/borderStyles.test.tsx | 30 +- packages/cli/src/ui/utils/borderStyles.ts | 7 +- .../src/ui/utils/markdownParsingUtils.test.ts | 3 + packages/core/src/config/config.ts | 3 +- 70 files changed, 1427 insertions(+), 406 deletions(-) create mode 100644 packages/cli/src/ui/components/ColorsDisplay.test.tsx create mode 100644 packages/cli/src/ui/components/ColorsDisplay.tsx diff --git a/packages/cli/src/ui/auth/ApiAuthDialog.tsx b/packages/cli/src/ui/auth/ApiAuthDialog.tsx index c5ac742955..2caad6fd27 100644 --- a/packages/cli/src/ui/auth/ApiAuthDialog.tsx +++ b/packages/cli/src/ui/auth/ApiAuthDialog.tsx @@ -98,7 +98,7 @@ export function ApiAuthDialog({ return ( {renderTabs()} diff --git a/packages/cli/src/ui/components/ColorsDisplay.test.tsx b/packages/cli/src/ui/components/ColorsDisplay.test.tsx new file mode 100644 index 0000000000..ec44bd6406 --- /dev/null +++ b/packages/cli/src/ui/components/ColorsDisplay.test.tsx @@ -0,0 +1,118 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { renderWithProviders } from '../../test-utils/render.js'; +import { ColorsDisplay } from './ColorsDisplay.js'; +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { themeManager } from '../themes/theme-manager.js'; +import type { Theme, ColorsTheme } from '../themes/theme.js'; +import type { SemanticColors } from '../themes/semantic-tokens.js'; + +describe('ColorsDisplay', () => { + beforeEach(() => { + vi.spyOn(themeManager, 'getSemanticColors').mockReturnValue({ + text: { + primary: '#ffffff', + secondary: '#cccccc', + link: '#0000ff', + accent: '#ff00ff', + response: '#ffffff', + }, + background: { + primary: '#000000', + message: '#111111', + input: '#222222', + focus: '#333333', + diff: { + added: '#003300', + removed: '#330000', + }, + }, + border: { + default: '#555555', + }, + ui: { + comment: '#666666', + symbol: '#cccccc', + active: '#0000ff', + dark: '#333333', + focus: '#0000ff', + gradient: undefined, + }, + status: { + error: '#ff0000', + success: '#00ff00', + warning: '#ffff00', + }, + }); + + vi.spyOn(themeManager, 'getActiveTheme').mockReturnValue({ + name: 'Test Theme', + type: 'dark', + colors: {} as unknown as ColorsTheme, + semanticColors: { + text: { + primary: '#ffffff', + secondary: '#cccccc', + link: '#0000ff', + accent: '#ff00ff', + response: '#ffffff', + }, + background: { + primary: '#000000', + message: '#111111', + input: '#222222', + diff: { + added: '#003300', + removed: '#330000', + }, + }, + border: { + default: '#555555', + }, + ui: { + comment: '#666666', + symbol: '#cccccc', + active: '#0000ff', + dark: '#333333', + focus: '#0000ff', + gradient: undefined, + }, + status: { + error: '#ff0000', + success: '#00ff00', + warning: '#ffff00', + }, + } as unknown as SemanticColors, + } as unknown as Theme); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('renders correctly', async () => { + const mockTheme = themeManager.getActiveTheme(); + const { lastFrame, waitUntilReady, unmount } = renderWithProviders( + , + ); + await waitUntilReady(); + const output = lastFrame(); + + // Check for title and description + expect(output).toContain('How do colors get applied?'); + expect(output).toContain('Hex:'); + + // Check for some color names and values expect(output).toContain('text.primary'); + expect(output).toContain('#ffffff'); + expect(output).toContain('background.diff.added'); + expect(output).toContain('#003300'); + expect(output).toContain('border.default'); + expect(output).toContain('#555555'); + + unmount(); + }); +}); diff --git a/packages/cli/src/ui/components/ColorsDisplay.tsx b/packages/cli/src/ui/components/ColorsDisplay.tsx new file mode 100644 index 0000000000..96b98bf540 --- /dev/null +++ b/packages/cli/src/ui/components/ColorsDisplay.tsx @@ -0,0 +1,277 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type React from 'react'; +import { Box, Text } from 'ink'; +import Gradient from 'ink-gradient'; +import { theme } from '../semantic-colors.js'; +import type { Theme } from '../themes/theme.js'; + +interface StandardColorRow { + type: 'standard'; + name: string; + value: string; +} + +interface GradientColorRow { + type: 'gradient'; + name: string; + value: string[]; +} + +interface BackgroundColorRow { + type: 'background'; + name: string; + value: string; +} + +type ColorRow = StandardColorRow | GradientColorRow | BackgroundColorRow; + +const VALUE_COLUMN_WIDTH = 10; + +const COLOR_DESCRIPTIONS: Record = { + 'text.primary': 'Primary text color (uses terminal default if blank)', + 'text.secondary': 'Secondary/dimmed text color', + 'text.link': 'Hyperlink and highlighting color', + 'text.accent': 'Accent color for emphasis', + 'text.response': + 'Color for model response text (uses terminal default if blank)', + 'background.primary': 'Main terminal background color', + 'background.message': 'Subtle background for message blocks', + 'background.input': 'Background for the input prompt', + 'background.focus': 'Background highlight for selected/focused items', + 'background.diff.added': 'Background for added lines in diffs', + 'background.diff.removed': 'Background for removed lines in diffs', + 'border.default': 'Standard border color', + 'ui.comment': 'Color for code comments and metadata', + 'ui.symbol': 'Color for technical symbols and UI icons', + 'ui.active': 'Border color for active or running elements', + 'ui.dark': 'Deeply dimmed color for subtle UI elements', + 'ui.focus': + 'Color for focused elements (e.g. selected menu items, focused borders)', + 'status.error': 'Color for error messages and critical status', + 'status.success': 'Color for success messages and positive status', + 'status.warning': 'Color for warnings and cautionary status', +}; + +interface ColorsDisplayProps { + activeTheme: Theme; +} + +/** + * Determines a contrasting text color (black or white) based on the background color's luminance. + */ +function getContrastingTextColor(hex: string): string { + if (!hex || !hex.startsWith('#') || hex.length < 7) { + // Fallback for invalid hex codes or named colors + return theme.text.primary; + } + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + // Using YIQ formula to determine luminance + const yiq = (r * 299 + g * 587 + b * 114) / 1000; + return yiq >= 128 ? '#000000' : '#FFFFFF'; +} + +export const ColorsDisplay: React.FC = ({ + activeTheme, +}) => { + const semanticColors = activeTheme.semanticColors; + + const backgroundRows: BackgroundColorRow[] = []; + const standardRows: StandardColorRow[] = []; + let gradientRow: GradientColorRow | null = null; + + if (semanticColors.ui.gradient && semanticColors.ui.gradient.length > 0) { + gradientRow = { + type: 'gradient', + name: 'ui.gradient', + value: semanticColors.ui.gradient, + }; + } + + /** + * Recursively flattens the semanticColors object. + */ + const flattenColors = (obj: object, path: string = '') => { + for (const [key, value] of Object.entries(obj)) { + if (value === undefined || value === null) continue; + const newPath = path ? `${path}.${key}` : key; + + if (key === 'gradient' && Array.isArray(value)) { + // Gradient handled separately + continue; + } + + if (typeof value === 'object' && !Array.isArray(value)) { + flattenColors(value, newPath); + } else if (typeof value === 'string') { + if (newPath.startsWith('background.')) { + backgroundRows.push({ + type: 'background', + name: newPath, + value, + }); + } else { + standardRows.push({ + type: 'standard', + name: newPath, + value, + }); + } + } + } + }; + + flattenColors(semanticColors); + + // Final order: Backgrounds first, then Standards, then Gradient + const allRows: ColorRow[] = [ + ...backgroundRows, + ...standardRows, + ...(gradientRow ? [gradientRow] : []), + ]; + + return ( + + + + DEVELOPER TOOLS (Not visible to users) + + + + How do colors get applied? + + + + • Hex: Rendered exactly by modern terminals. Not + overridden by app themes. + + + • Blank: Uses your terminal's default + foreground/background. + + + • Compatibility: On older terminals, hex is + approximated to the nearest ANSI color. + + + • ANSI Names: 'red', + 'green', etc. are mapped to your terminal app's + palette. + + + + + + {/* Header */} + + + + Value + + + + + Name + + + + + {/* All Rows */} + + {allRows.map((row) => { + if (row.type === 'standard') return renderStandardRow(row); + if (row.type === 'gradient') return renderGradientRow(row); + if (row.type === 'background') return renderBackgroundRow(row); + return null; + })} + + + ); +}; + +function renderStandardRow({ name, value }: StandardColorRow) { + const isHex = value.startsWith('#'); + const displayColor = isHex ? value : theme.text.primary; + const description = COLOR_DESCRIPTIONS[name] || ''; + + return ( + + + {value || '(blank)'} + + + + {name} + + + {description} + + + + ); +} + +function renderGradientRow({ name, value }: GradientColorRow) { + const description = COLOR_DESCRIPTIONS[name] || ''; + + return ( + + + {value.map((c, i) => ( + + {c} + + ))} + + + + + {name} + + + + {description} + + + + ); +} + +function renderBackgroundRow({ name, value }: BackgroundColorRow) { + const description = COLOR_DESCRIPTIONS[name] || ''; + + return ( + + + + {value || 'default'} + + + + + {name} + + + {description} + + + + ); +} diff --git a/packages/cli/src/ui/components/GradientRegression.test.tsx b/packages/cli/src/ui/components/GradientRegression.test.tsx index 91193e8087..bc836a1102 100644 --- a/packages/cli/src/ui/components/GradientRegression.test.tsx +++ b/packages/cli/src/ui/components/GradientRegression.test.tsx @@ -22,8 +22,13 @@ vi.mock('../semantic-colors.js', async (importOriginal) => { ...original, theme: { ...original.theme, + background: { + ...original.theme.background, + focus: '#004000', + }, ui: { ...original.theme.ui, + focus: '#00ff00', gradient: [], // Empty array to potentially trigger the crash }, }, diff --git a/packages/cli/src/ui/components/Header.test.tsx b/packages/cli/src/ui/components/Header.test.tsx index 4d59bf14aa..46cdaf5ba0 100644 --- a/packages/cli/src/ui/components/Header.test.tsx +++ b/packages/cli/src/ui/components/Header.test.tsx @@ -98,16 +98,18 @@ describe('
', () => { primary: '', message: '', input: '', + focus: '', diff: { added: '', removed: '' }, }, border: { default: '', - focused: '', }, ui: { comment: '', symbol: '', + active: '', dark: '', + focus: '', gradient: undefined, }, status: { diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index 38b62ad927..e8a01fa716 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -1427,7 +1427,7 @@ export const InputPrompt: React.FC = ({ const borderColor = isShellFocused && !isEmbeddedShellFocused - ? (statusColor ?? theme.border.focused) + ? (statusColor ?? theme.ui.focus) : theme.border.default; return ( diff --git a/packages/cli/src/ui/components/LoadingIndicator.tsx b/packages/cli/src/ui/components/LoadingIndicator.tsx index 2d603ebbdd..f9fff9fa9b 100644 --- a/packages/cli/src/ui/components/LoadingIndicator.tsx +++ b/packages/cli/src/ui/components/LoadingIndicator.tsx @@ -79,10 +79,18 @@ export const LoadingIndicator: React.FC = ({ /> {primaryText && ( - - {thinkingIndicator} - {primaryText} - + + + {thinkingIndicator} + {primaryText} + + {primaryText === INTERACTIVE_SHELL_WAITING_PHRASE && ( + + {' '} + (press tab to focus) + + )} + )} {cancelAndTimerContent && ( <> @@ -113,10 +121,18 @@ export const LoadingIndicator: React.FC = ({ /> {primaryText && ( - - {thinkingIndicator} - {primaryText} - + + + {thinkingIndicator} + {primaryText} + + {primaryText === INTERACTIVE_SHELL_WAITING_PHRASE && ( + + {' '} + (press tab to focus) + + )} + )} {!isNarrow && cancelAndTimerContent && ( <> diff --git a/packages/cli/src/ui/components/LogoutConfirmationDialog.tsx b/packages/cli/src/ui/components/LogoutConfirmationDialog.tsx index e50d7ef568..3bcb4a9f35 100644 --- a/packages/cli/src/ui/components/LogoutConfirmationDialog.tsx +++ b/packages/cli/src/ui/components/LogoutConfirmationDialog.tsx @@ -53,7 +53,7 @@ export const LogoutConfirmationDialog: React.FC< { }); }); - it('returns symbol border for executing shell commands', () => { + it('returns active border for executing shell commands', () => { const item = { type: 'tool_group' as const, tools: [ @@ -219,7 +219,37 @@ describe('getToolGroupBorderAppearance', () => { ], id: 1, }; - // While executing shell commands, it's dim false, border symbol + // While executing shell commands, it's dim false, border active + const result = getToolGroupBorderAppearance( + item, + activeShellPtyId, + false, + [], + mockBackgroundShells, + ); + expect(result).toEqual({ + borderColor: theme.ui.active, + borderDimColor: true, + }); + }); + + it('returns focus border for focused executing shell commands', () => { + const item = { + type: 'tool_group' as const, + tools: [ + { + callId: '1', + name: SHELL_COMMAND_NAME, + description: '', + status: CoreToolCallStatus.Executing, + ptyId: activeShellPtyId, + resultDisplay: undefined, + confirmationDetails: undefined, + } as IndividualToolCallDisplay, + ], + id: 1, + }; + // When focused, it's dim false, border focus const result = getToolGroupBorderAppearance( item, activeShellPtyId, @@ -228,12 +258,12 @@ describe('getToolGroupBorderAppearance', () => { mockBackgroundShells, ); expect(result).toEqual({ - borderColor: theme.ui.symbol, + borderColor: theme.ui.focus, borderDimColor: false, }); }); - it('returns symbol border and dims color for background executing shell command when another shell is active', () => { + it('returns active border and dims color for background executing shell command when another shell is active', () => { const item = { type: 'tool_group' as const, tools: [ @@ -257,7 +287,7 @@ describe('getToolGroupBorderAppearance', () => { mockBackgroundShells, ); expect(result).toEqual({ - borderColor: theme.ui.symbol, + borderColor: theme.ui.active, borderDimColor: true, }); }); @@ -275,7 +305,7 @@ describe('getToolGroupBorderAppearance', () => { ); // Since there are no tools to inspect, it falls back to empty pending, but isCurrentlyInShellTurn=true // so it counts as pending shell. - expect(result.borderColor).toEqual(theme.ui.symbol); + expect(result.borderColor).toEqual(theme.ui.focus); // It shouldn't be dim because there are no tools to say it isEmbeddedShellFocused = false expect(result.borderDimColor).toBe(false); }); diff --git a/packages/cli/src/ui/components/SessionBrowser.tsx b/packages/cli/src/ui/components/SessionBrowser.tsx index 9d1ce57f52..154ad62522 100644 --- a/packages/cli/src/ui/components/SessionBrowser.tsx +++ b/packages/cli/src/ui/components/SessionBrowser.tsx @@ -7,6 +7,7 @@ import type React from 'react'; import { useState, useCallback, useMemo, useEffect, useRef } from 'react'; import { Box, Text } from 'ink'; +import { theme } from '../semantic-colors.js'; import { Colors } from '../colors.js'; import { useTerminalSize } from '../hooks/useTerminalSize.js'; import { useKeypress } from '../hooks/useKeypress.js'; @@ -436,7 +437,7 @@ const SessionItem = ({ if (isDisabled) { return Colors.Gray; } - return isActive ? Colors.AccentPurple : c; + return isActive ? theme.ui.focus : c; }; const prefix = isActive ? '❯ ' : ' '; @@ -483,7 +484,10 @@ const SessionItem = ({ )); return ( - + {prefix} diff --git a/packages/cli/src/ui/components/SuggestionsDisplay.tsx b/packages/cli/src/ui/components/SuggestionsDisplay.tsx index d9498e7a6b..7ce950eec9 100644 --- a/packages/cli/src/ui/components/SuggestionsDisplay.tsx +++ b/packages/cli/src/ui/components/SuggestionsDisplay.tsx @@ -84,7 +84,7 @@ export function SuggestionsDisplay({ const originalIndex = startIndex + index; const isActive = originalIndex === activeIndex; const isExpanded = originalIndex === expandedIndex; - const textColor = isActive ? theme.text.accent : theme.text.secondary; + const textColor = isActive ? theme.ui.focus : theme.text.secondary; const isLong = suggestion.value.length >= MAX_WIDTH; const labelElement = ( + ({ + mockIsDevelopment: { value: false }, +})); + +vi.mock('../../utils/installationInfo.js', async (importOriginal) => { + const actual = + await importOriginal(); + return { + ...actual, + get isDevelopment() { + return mockIsDevelopment.value; + }, + }; +}); + import { createMockSettings } from '../../test-utils/settings.js'; import { DEFAULT_THEME, themeManager } from '../themes/theme-manager.js'; import { act } from 'react'; @@ -30,17 +46,21 @@ describe('ThemeDialog Snapshots', () => { vi.restoreAllMocks(); }); - it('should render correctly in theme selection mode', async () => { - const settings = createMockSettings(); - const { lastFrame, waitUntilReady, unmount } = renderWithProviders( - , - { settings }, - ); - await waitUntilReady(); + it.each([true, false])( + 'should render correctly in theme selection mode (isDevelopment: %s)', + async (isDev) => { + mockIsDevelopment.value = isDev; + const settings = createMockSettings(); + const { lastFrame, waitUntilReady, unmount } = renderWithProviders( + , + { settings }, + ); + await waitUntilReady(); - expect(lastFrame()).toMatchSnapshot(); - unmount(); - }); + expect(lastFrame()).toMatchSnapshot(); + unmount(); + }, + ); it('should render correctly in scope selector mode', async () => { const settings = createMockSettings(); diff --git a/packages/cli/src/ui/components/ThemeDialog.tsx b/packages/cli/src/ui/components/ThemeDialog.tsx index c4bfe66897..4bfb623db7 100644 --- a/packages/cli/src/ui/components/ThemeDialog.tsx +++ b/packages/cli/src/ui/components/ThemeDialog.tsx @@ -23,6 +23,8 @@ import { useKeypress } from '../hooks/useKeypress.js'; import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js'; import { ScopeSelector } from './shared/ScopeSelector.js'; import { useUIState } from '../contexts/UIStateContext.js'; +import { ColorsDisplay } from './ColorsDisplay.js'; +import { isDevelopment } from '../../utils/installationInfo.js'; interface ThemeDialogProps { /** Callback function when a theme is selected */ @@ -245,6 +247,11 @@ export function ThemeDialog({ // The code block is slightly longer than the diff, so give it more space. const codeBlockHeight = Math.ceil(availableHeightForPanes * 0.6); const diffHeight = Math.floor(availableHeightForPanes * 0.4); + + const previewTheme = + themeManager.getTheme(highlightedThemeName || DEFAULT_THEME.name) || + DEFAULT_THEME; + return ( Preview - {/* Get the Theme object for the highlighted theme, fall back to default if not found */} - {(() => { - const previewTheme = - themeManager.getTheme( - highlightedThemeName || DEFAULT_THEME.name, - ) || DEFAULT_THEME; - - return ( - - {colorizeCode({ - code: `# function + + {colorizeCode({ + code: `# function def fibonacci(n): a, b = 0, 1 for _ in range(n): a, b = b, a + b return a`, - language: 'python', - availableHeight: - isAlternateBuffer === false ? codeBlockHeight : undefined, - maxWidth: colorizeCodeWidth, - settings, - })} - - + - - ); - })()} + availableTerminalHeight={ + isAlternateBuffer === false ? diffHeight : undefined + } + terminalWidth={colorizeCodeWidth} + theme={previewTheme} + /> + + {isDevelopment && ( + + + + )} ) : ( diff --git a/packages/cli/src/ui/components/ThemedGradient.test.tsx b/packages/cli/src/ui/components/ThemedGradient.test.tsx index 60507015b5..6632a63300 100644 --- a/packages/cli/src/ui/components/ThemedGradient.test.tsx +++ b/packages/cli/src/ui/components/ThemedGradient.test.tsx @@ -13,6 +13,10 @@ vi.mock('../semantic-colors.js', () => ({ theme: { ui: { gradient: ['red', 'blue'], + focus: 'green', + }, + background: { + focus: 'darkgreen', }, text: { accent: 'cyan', diff --git a/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap index 29a7683d06..9644026634 100644 --- a/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/AskUserDialog.test.tsx.snap @@ -1,6 +1,17 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`AskUserDialog > Choice question placeholder > uses default placeholder when not provided 1`] = ` +"Select your preferred language: + + 1. TypeScript + 2. JavaScript +● 3. Enter a custom value + +Enter to submit · Esc to cancel +" +`; + +exports[`AskUserDialog > Choice question placeholder > uses default placeholder when not provided 2`] = ` "Select your preferred language: 1. TypeScript @@ -12,6 +23,17 @@ Enter to submit · Esc to cancel `; exports[`AskUserDialog > Choice question placeholder > uses placeholder for "Other" option when provided 1`] = ` +"Select your preferred language: + + 1. TypeScript + 2. JavaScript +● 3. Type another language... + +Enter to submit · Esc to cancel +" +`; + +exports[`AskUserDialog > Choice question placeholder > uses placeholder for "Other" option when provided 2`] = ` "Select your preferred language: 1. TypeScript @@ -25,6 +47,20 @@ Enter to submit · Esc to cancel exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: false) > shows scroll arrows correctly when useAlternateBuffer is false 1`] = ` "Choose an option +▲ +● 1. Option 1 + Description 1 + 2. Option 2 + Description 2 +▼ + +Enter to select · ↑/↓ to navigate · Esc to cancel +" +`; + +exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: false) > shows scroll arrows correctly when useAlternateBuffer is false 2`] = ` +"Choose an option + ▲ ● 1. Option 1 Description 1 @@ -39,6 +75,45 @@ Enter to select · ↑/↓ to navigate · Esc to cancel exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: true) > shows scroll arrows correctly when useAlternateBuffer is true 1`] = ` "Choose an option +● 1. Option 1 + Description 1 + 2. Option 2 + Description 2 + 3. Option 3 + Description 3 + 4. Option 4 + Description 4 + 5. Option 5 + Description 5 + 6. Option 6 + Description 6 + 7. Option 7 + Description 7 + 8. Option 8 + Description 8 + 9. Option 9 + Description 9 + 10. Option 10 + Description 10 + 11. Option 11 + Description 11 + 12. Option 12 + Description 12 + 13. Option 13 + Description 13 + 14. Option 14 + Description 14 + 15. Option 15 + Description 15 + 16. Enter a custom value + +Enter to select · ↑/↓ to navigate · Esc to cancel +" +`; + +exports[`AskUserDialog > Scroll Arrows (useAlternateBuffer: true) > shows scroll arrows correctly when useAlternateBuffer is true 2`] = ` +"Choose an option + ● 1. Option 1 Description 1 2. Option 2 @@ -122,8 +197,8 @@ Enter to submit · Tab/Shift+Tab to edit answers · Esc to cancel exports[`AskUserDialog > hides progress header for single question 1`] = ` "Which authentication method should we use? -● 1. OAuth 2.0 - Industry standard, supports SSO +● 1. OAuth 2.0 + Industry standard, supports SSO 2. JWT tokens Stateless, good for APIs 3. Enter a custom value @@ -135,8 +210,8 @@ Enter to select · ↑/↓ to navigate · Esc to cancel exports[`AskUserDialog > renders question and options 1`] = ` "Which authentication method should we use? -● 1. OAuth 2.0 - Industry standard, supports SSO +● 1. OAuth 2.0 + Industry standard, supports SSO 2. JWT tokens Stateless, good for APIs 3. Enter a custom value @@ -150,8 +225,8 @@ exports[`AskUserDialog > shows Review tab in progress header for multiple questi Which framework? -● 1. React - Component library +● 1. React + Component library 2. Vue Progressive framework 3. Enter a custom value @@ -163,8 +238,8 @@ Enter to select · ←/→ to switch questions · Esc to cancel exports[`AskUserDialog > shows keyboard hints 1`] = ` "Which authentication method should we use? -● 1. OAuth 2.0 - Industry standard, supports SSO +● 1. OAuth 2.0 + Industry standard, supports SSO 2. JWT tokens Stateless, good for APIs 3. Enter a custom value @@ -178,8 +253,8 @@ exports[`AskUserDialog > shows progress header for multiple questions 1`] = ` Which database should we use? -● 1. PostgreSQL - Relational database +● 1. PostgreSQL + Relational database 2. MongoDB Document database 3. Enter a custom value diff --git a/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap index db1b6d1ba5..9e210e3438 100644 --- a/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/ExitPlanModeDialog.test.tsx.snap @@ -19,14 +19,41 @@ Files to Modify 1. Yes, automatically accept edits Approves plan and allows tools to run automatically -● 2. Yes, manually accept edits - Approves plan but requires confirmation for each tool +● 2. Yes, manually accept edits + Approves plan but requires confirmation for each tool 3. Type your feedback... Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " `; +exports[`ExitPlanModeDialog > useAlternateBuffer: false > bubbles up Ctrl+C when feedback is empty while editing 2`] = ` +"Overview + +Add user authentication to the CLI application. + +Implementation Steps + + 1. Create src/auth/AuthService.ts with login/logout methods + 2. Add session storage in src/storage/SessionStore.ts + 3. Update src/commands/index.ts to check auth status + 4. Add tests in src/auth/__tests__/ + +Files to Modify + + - src/index.ts - Add auth middleware + - src/config.ts - Add auth configuration options + + 1. Yes, automatically accept edits + Approves plan and allows tools to run automatically + 2. Yes, manually accept edits + Approves plan but requires confirmation for each tool +● 3. Type your feedback... + +Enter to submit · Ctrl+X to edit plan · Esc to cancel +" +`; + exports[`ExitPlanModeDialog > useAlternateBuffer: false > calls onFeedback when feedback is typed and submitted 1`] = ` "Overview @@ -44,8 +71,8 @@ Files to Modify - src/index.ts - Add auth middleware - src/config.ts - Add auth configuration options -● 1. Yes, automatically accept edits - Approves plan and allows tools to run automatically +● 1. Yes, automatically accept edits + Approves plan and allows tools to run automatically 2. Yes, manually accept edits Approves plan but requires confirmation for each tool 3. Type your feedback... @@ -54,6 +81,33 @@ Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " `; +exports[`ExitPlanModeDialog > useAlternateBuffer: false > calls onFeedback when feedback is typed and submitted 2`] = ` +"Overview + +Add user authentication to the CLI application. + +Implementation Steps + + 1. Create src/auth/AuthService.ts with login/logout methods + 2. Add session storage in src/storage/SessionStore.ts + 3. Update src/commands/index.ts to check auth status + 4. Add tests in src/auth/__tests__/ + +Files to Modify + + - src/index.ts - Add auth middleware + - src/config.ts - Add auth configuration options + + 1. Yes, automatically accept edits + Approves plan and allows tools to run automatically + 2. Yes, manually accept edits + Approves plan but requires confirmation for each tool +● 3. Add tests + +Enter to submit · Ctrl+X to edit plan · Esc to cancel +" +`; + exports[`ExitPlanModeDialog > useAlternateBuffer: false > displays error state when file read fails 1`] = ` " Error reading plan: File not found " @@ -76,8 +130,8 @@ Implementation Steps 8. Add multi-factor authentication in src/auth/MFAService.ts ... last 22 lines hidden (Ctrl+O to show) ... -● 1. Yes, automatically accept edits - Approves plan and allows tools to run automatically +● 1. Yes, automatically accept edits + Approves plan and allows tools to run automatically 2. Yes, manually accept edits Approves plan but requires confirmation for each tool 3. Type your feedback... @@ -103,8 +157,8 @@ Files to Modify - src/index.ts - Add auth middleware - src/config.ts - Add auth configuration options -● 1. Yes, automatically accept edits - Approves plan and allows tools to run automatically +● 1. Yes, automatically accept edits + Approves plan and allows tools to run automatically 2. Yes, manually accept edits Approves plan but requires confirmation for each tool 3. Type your feedback... @@ -132,14 +186,41 @@ Files to Modify 1. Yes, automatically accept edits Approves plan and allows tools to run automatically -● 2. Yes, manually accept edits - Approves plan but requires confirmation for each tool +● 2. Yes, manually accept edits + Approves plan but requires confirmation for each tool 3. Type your feedback... Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " `; +exports[`ExitPlanModeDialog > useAlternateBuffer: true > bubbles up Ctrl+C when feedback is empty while editing 2`] = ` +"Overview + +Add user authentication to the CLI application. + +Implementation Steps + + 1. Create src/auth/AuthService.ts with login/logout methods + 2. Add session storage in src/storage/SessionStore.ts + 3. Update src/commands/index.ts to check auth status + 4. Add tests in src/auth/__tests__/ + +Files to Modify + + - src/index.ts - Add auth middleware + - src/config.ts - Add auth configuration options + + 1. Yes, automatically accept edits + Approves plan and allows tools to run automatically + 2. Yes, manually accept edits + Approves plan but requires confirmation for each tool +● 3. Type your feedback... + +Enter to submit · Ctrl+X to edit plan · Esc to cancel +" +`; + exports[`ExitPlanModeDialog > useAlternateBuffer: true > calls onFeedback when feedback is typed and submitted 1`] = ` "Overview @@ -157,8 +238,8 @@ Files to Modify - src/index.ts - Add auth middleware - src/config.ts - Add auth configuration options -● 1. Yes, automatically accept edits - Approves plan and allows tools to run automatically +● 1. Yes, automatically accept edits + Approves plan and allows tools to run automatically 2. Yes, manually accept edits Approves plan but requires confirmation for each tool 3. Type your feedback... @@ -167,6 +248,33 @@ Enter to select · ↑/↓ to navigate · Ctrl+X to edit plan · Esc to cancel " `; +exports[`ExitPlanModeDialog > useAlternateBuffer: true > calls onFeedback when feedback is typed and submitted 2`] = ` +"Overview + +Add user authentication to the CLI application. + +Implementation Steps + + 1. Create src/auth/AuthService.ts with login/logout methods + 2. Add session storage in src/storage/SessionStore.ts + 3. Update src/commands/index.ts to check auth status + 4. Add tests in src/auth/__tests__/ + +Files to Modify + + - src/index.ts - Add auth middleware + - src/config.ts - Add auth configuration options + + 1. Yes, automatically accept edits + Approves plan and allows tools to run automatically + 2. Yes, manually accept edits + Approves plan but requires confirmation for each tool +● 3. Add tests + +Enter to submit · Ctrl+X to edit plan · Esc to cancel +" +`; + exports[`ExitPlanModeDialog > useAlternateBuffer: true > displays error state when file read fails 1`] = ` " Error reading plan: File not found " @@ -210,8 +318,8 @@ Testing Strategy - Security penetration testing - Load testing for session management -● 1. Yes, automatically accept edits - Approves plan and allows tools to run automatically +● 1. Yes, automatically accept edits + Approves plan and allows tools to run automatically 2. Yes, manually accept edits Approves plan but requires confirmation for each tool 3. Type your feedback... @@ -237,8 +345,8 @@ Files to Modify - src/index.ts - Add auth middleware - src/config.ts - Add auth configuration options -● 1. Yes, automatically accept edits - Approves plan and allows tools to run automatically +● 1. Yes, automatically accept edits + Approves plan and allows tools to run automatically 2. Yes, manually accept edits Approves plan but requires confirmation for each tool 3. Type your feedback... diff --git a/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap index 88a1b0486f..f40887b3b9 100644 --- a/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/InputPrompt.test.tsx.snap @@ -12,8 +12,8 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and c (r:) Type your message or @path/to/file ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll → - lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll - ... + lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll + ... " `; @@ -22,8 +22,8 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > expands and c (r:) Type your message or @path/to/file ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll ← - lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll - llllllllllllllllllllllllllllllllllllllllllllllllll + lllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll + llllllllllllllllllllllllllllllllllllllllllllllllll " `; @@ -31,7 +31,7 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ (r:) commit ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ - git commit -m "feat: add search" in src/app + git commit -m "feat: add search" in src/app " `; @@ -39,7 +39,7 @@ exports[`InputPrompt > command search (Ctrl+R when not in shell) > renders match "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ (r:) commit ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ - git commit -m "feat: add search" in src/app + git commit -m "feat: add search" in src/app " `; @@ -78,6 +78,27 @@ exports[`InputPrompt > mouse interaction > should toggle paste expansion on doub " `; +exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 4`] = ` +"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + > [Pasted Text: 10 lines] +▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ +" +`; + +exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 5`] = ` +"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + > [Pasted Text: 10 lines] +▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ +" +`; + +exports[`InputPrompt > mouse interaction > should toggle paste expansion on double-click 6`] = ` +"▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + > [Pasted Text: 10 lines] +▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ +" +`; + exports[`InputPrompt > snapshots > should not show inverted cursor when shell is focused 1`] = ` "▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ > Type your message or @path/to/file diff --git a/packages/cli/src/ui/components/__snapshots__/LoadingIndicator.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/LoadingIndicator.test.tsx.snap index d70a278827..666525e720 100644 --- a/packages/cli/src/ui/components/__snapshots__/LoadingIndicator.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/LoadingIndicator.test.tsx.snap @@ -1,7 +1,7 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[` > should truncate long primary text instead of wrapping 1`] = ` -"MockRespondin This is an extremely long loading phrase that shoul… (esc to +"MockRespondin This is an extremely long loading phrase that shoul…(esc to gSpinner cancel, 5s) " `; diff --git a/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap index 0599e82f7c..d01043eee9 100644 --- a/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/MainContent.test.tsx.snap @@ -4,7 +4,7 @@ exports[`MainContent > MainContent Tool Output Height Logic > 'ASB mode - Focuse "ScrollableList AppHeader(full) ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ -│ ⊷ Shell Command Running a long command... │ +│ ⊶ Shell Command Running a long command... │ │ │ │ Line 10 │ │ Line 11 │ @@ -26,7 +26,7 @@ exports[`MainContent > MainContent Tool Output Height Logic > 'ASB mode - Unfocu "ScrollableList AppHeader(full) ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ -│ ⊷ Shell Command Running a long command... │ +│ ⊶ Shell Command Running a long command... │ │ │ │ Line 10 │ │ Line 11 │ @@ -47,7 +47,7 @@ ShowMoreLines exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Constrained height' 1`] = ` "AppHeader(full) ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ -│ ⊷ Shell Command Running a long command... │ +│ ⊶ Shell Command Running a long command... │ │ │ │ ... first 11 lines hidden (Ctrl+O to show) ... │ │ Line 12 │ @@ -67,7 +67,7 @@ ShowMoreLines exports[`MainContent > MainContent Tool Output Height Logic > 'Normal mode - Unconstrained height' 1`] = ` "AppHeader(full) ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ -│ ⊷ Shell Command Running a long command... │ +│ ⊶ Shell Command Running a long command... │ │ │ │ Line 1 │ │ Line 2 │ diff --git a/packages/cli/src/ui/components/__snapshots__/SessionBrowser.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/SessionBrowser.test.tsx.snap index 583d75d281..15cd8748ae 100644 --- a/packages/cli/src/ui/components/__snapshots__/SessionBrowser.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/SessionBrowser.test.tsx.snap @@ -6,7 +6,7 @@ exports[`SessionBrowser component > enters search mode, filters sessions, and re Search: query (Esc to cancel) Index │ Msgs │ Age │ Match - ❯ #1 │ 1 │ 10mo │ You: Query is here a… (+1 more) + ❯ #1 │ 1 │ 10mo │ You: Query is here a… (+1 more) ▼ " `; @@ -17,7 +17,7 @@ exports[`SessionBrowser component > renders a list of sessions and marks current Sort: s Reverse: r First/Last: g/G Index │ Msgs │ Age │ Name - ❯ #1 │ 5 │ 10mo │ Second conversation about dogs (current) + ❯ #1 │ 5 │ 10mo │ Second conversation about dogs (current) #2 │ 2 │ 10mo │ First conversation about cats ▼ " diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Initial-Rendering-should-render-settings-list-with-visual-indicators.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Initial-Rendering-should-render-settings-list-with-visual-indicators.snap.svg index b68e492f74..9b78352d03 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Initial-Rendering-should-render-settings-list-with-visual-indicators.snap.svg +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Initial-Rendering-should-render-settings-list-with-visual-indicators.snap.svg @@ -13,17 +13,17 @@ - ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ - + S earch to filter - + - ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ @@ -31,12 +31,20 @@ + + + Vim Mode + + false + + Enable Vim keybindings + @@ -112,8 +120,12 @@ Apply To + + + User Settings + Workspace Settings diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-accessibility-settings-enabled-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-accessibility-settings-enabled-correctly.snap.svg index 994055b1f7..4ea2a09cad 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-accessibility-settings-enabled-correctly.snap.svg +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-accessibility-settings-enabled-correctly.snap.svg @@ -13,17 +13,17 @@ - ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ - + S earch to filter - + - ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ @@ -31,12 +31,20 @@ + + + Vim Mode + + true* + + Enable Vim keybindings + @@ -112,8 +120,12 @@ Apply To + + + User Settings + Workspace Settings diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-all-boolean-settings-disabled-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-all-boolean-settings-disabled-correctly.snap.svg index f8a339d1d2..040e4cfcbe 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-all-boolean-settings-disabled-correctly.snap.svg +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-all-boolean-settings-disabled-correctly.snap.svg @@ -13,17 +13,17 @@ - ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ - + S earch to filter - + - ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ @@ -31,12 +31,20 @@ + + + Vim Mode + + false* + + Enable Vim keybindings + @@ -110,8 +118,12 @@ Apply To + + + User Settings + Workspace Settings diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-default-state-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-default-state-correctly.snap.svg index b68e492f74..9b78352d03 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-default-state-correctly.snap.svg +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-default-state-correctly.snap.svg @@ -13,17 +13,17 @@ - ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ - + S earch to filter - + - ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ @@ -31,12 +31,20 @@ + + + Vim Mode + + false + + Enable Vim keybindings + @@ -112,8 +120,12 @@ Apply To + + + User Settings + Workspace Settings diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-file-filtering-settings-configured-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-file-filtering-settings-configured-correctly.snap.svg index b68e492f74..9b78352d03 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-file-filtering-settings-configured-correctly.snap.svg +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-file-filtering-settings-configured-correctly.snap.svg @@ -13,17 +13,17 @@ - ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ - + S earch to filter - + - ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ @@ -31,12 +31,20 @@ + + + Vim Mode + + false + + Enable Vim keybindings + @@ -112,8 +120,12 @@ Apply To + + + User Settings + Workspace Settings diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-focused-on-scope-selector-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-focused-on-scope-selector-correctly.snap.svg index 4bc22d98ce..91471d9d51 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-focused-on-scope-selector-correctly.snap.svg +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-focused-on-scope-selector-correctly.snap.svg @@ -109,9 +109,15 @@ > Apply To + + + 1. + + User Settings + 2. Workspace Settings diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-mixed-boolean-and-number-settings-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-mixed-boolean-and-number-settings-correctly.snap.svg index cfc022959d..f39891212c 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-mixed-boolean-and-number-settings-correctly.snap.svg +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-mixed-boolean-and-number-settings-correctly.snap.svg @@ -13,17 +13,17 @@ - ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ - + S earch to filter - + - ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ @@ -31,12 +31,20 @@ + + + Vim Mode + + false* + + Enable Vim keybindings + @@ -111,8 +119,12 @@ Apply To + + + User Settings + Workspace Settings diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-tools-and-security-settings-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-tools-and-security-settings-correctly.snap.svg index b68e492f74..9b78352d03 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-tools-and-security-settings-correctly.snap.svg +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-tools-and-security-settings-correctly.snap.svg @@ -13,17 +13,17 @@ - ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ - + S earch to filter - + - ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ @@ -31,12 +31,20 @@ + + + Vim Mode + + false + + Enable Vim keybindings + @@ -112,8 +120,12 @@ Apply To + + + User Settings + Workspace Settings diff --git a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-various-boolean-settings-enabled-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-various-boolean-settings-enabled-correctly.snap.svg index 9f572f6582..600ace5560 100644 --- a/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-various-boolean-settings-enabled-correctly.snap.svg +++ b/packages/cli/src/ui/components/__snapshots__/SettingsDialog-SettingsDialog-Snapshot-Tests-should-render-various-boolean-settings-enabled-correctly.snap.svg @@ -13,17 +13,17 @@ - ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ - + S earch to filter - + - ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ @@ -31,12 +31,20 @@ + + + Vim Mode + + true* + + Enable Vim keybindings + @@ -110,8 +118,12 @@ Apply To + + + User Settings + Workspace Settings diff --git a/packages/cli/src/ui/components/__snapshots__/SuggestionsDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/SuggestionsDisplay.test.tsx.snap index 775233f30e..3c79a534a2 100644 --- a/packages/cli/src/ui/components/__snapshots__/SuggestionsDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/SuggestionsDisplay.test.tsx.snap @@ -7,7 +7,7 @@ exports[`SuggestionsDisplay > handles scrolling 1`] = ` Cmd 7 Description 7 Cmd 8 Description 8 Cmd 9 Description 9 - Cmd 10 Description 10 + Cmd 10 Description 10 Cmd 11 Description 11 Cmd 12 Description 12 ▼ @@ -17,13 +17,13 @@ exports[`SuggestionsDisplay > handles scrolling 1`] = ` exports[`SuggestionsDisplay > highlights active item 1`] = ` " command1 Description 1 - command2 Description 2 + command2 Description 2 command3 Description 3 " `; exports[`SuggestionsDisplay > renders MCP tag for MCP prompts 1`] = ` -" mcp-tool [MCP] +" mcp-tool [MCP] " `; @@ -33,7 +33,7 @@ exports[`SuggestionsDisplay > renders loading state 1`] = ` `; exports[`SuggestionsDisplay > renders suggestions list 1`] = ` -" command1 Description 1 +" command1 Description 1 command2 Description 2 command3 Description 3 " diff --git a/packages/cli/src/ui/components/__snapshots__/ThemeDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ThemeDialog.test.tsx.snap index 11f2af0a5c..0a5f4a08ae 100644 --- a/packages/cli/src/ui/components/__snapshots__/ThemeDialog.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/ThemeDialog.test.tsx.snap @@ -89,7 +89,7 @@ exports[`ThemeDialog Snapshots > should render correctly in scope selector mode " `; -exports[`ThemeDialog Snapshots > should render correctly in theme selection mode 1`] = ` +exports[`ThemeDialog Snapshots > should render correctly in theme selection mode (isDevelopment: false) 1`] = ` "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │ │ │ > Select Theme Preview │ @@ -113,3 +113,90 @@ exports[`ThemeDialog Snapshots > should render correctly in theme selection mode ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ " `; + +exports[`ThemeDialog Snapshots > should render correctly in theme selection mode (isDevelopment: true) 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ > Select Theme Preview │ +│ ▲ ┌─────────────────────────────────────────────────┐ │ +│ ● 1. ANSI Dark (Matches terminal) │ │ │ +│ 2. Atom One Dark │ 1 # function │ │ +│ 3. Ayu Dark │ 2 def fibonacci(n): │ │ +│ 4. Default Dark │ 3 a, b = 0, 1 │ │ +│ 5. Dracula Dark │ 4 for _ in range(n): │ │ +│ 6. GitHub Dark │ 5 a, b = b, a + b │ │ +│ 7. Holiday Dark │ 6 return a │ │ +│ 8. Shades Of Purple Dark │ │ │ +│ 9. Solarized Dark │ 1 - print("Hello, " + name) │ │ +│ 10. ANSI Light │ 1 + print(f"Hello, {name}!") │ │ +│ 11. Ayu Light │ │ │ +│ 12. Default Light └─────────────────────────────────────────────────┘ │ +│ ▼ │ +│ ╭─────────────────────────────────────────────────╮ │ +│ │ DEVELOPER TOOLS (Not visible to users) │ │ +│ │ │ │ +│ │ How do colors get applied? │ │ +│ │ • Hex: Rendered exactly by modern terminals. │ │ +│ │ Not overridden by app themes. │ │ +│ │ • Blank: Uses your terminal's default │ │ +│ │ foreground/background. │ │ +│ │ • Compatibility: On older terminals, hex is │ │ +│ │ approximated to the nearest ANSI color. │ │ +│ │ • ANSI Names: 'red', 'green', etc. are mapped │ │ +│ │ to your terminal app's palette. │ │ +│ │ │ │ +│ │ Value Name │ │ +│ │ #1E1E… backgroun Main terminal background │ │ +│ │ d.primary color │ │ +│ │ #313… backgroun Subtle background for │ │ +│ │ d.message message blocks │ │ +│ │ #313… backgroun Background for the input │ │ +│ │ d.input prompt │ │ +│ │ #39… background. Background highlight for │ │ +│ │ focus selected/focused items │ │ +│ │ #283… backgrou Background for added lines │ │ +│ │ nd.diff. in diffs │ │ +│ │ added │ │ +│ │ #430… backgroun Background for removed │ │ +│ │ d.diff.re lines in diffs │ │ +│ │ moved │ │ +│ │ (blank text.prim Primary text color (uses │ │ +│ │ ) ary terminal default if blank) │ │ +│ │ #6C7086 text.secon Secondary/dimmed text │ │ +│ │ dary color │ │ +│ │ #89B4FA text.link Hyperlink and highlighting │ │ +│ │ color │ │ +│ │ #CBA6F7 text.accen Accent color for │ │ +│ │ t emphasis │ │ +│ │ (blank) text.res Color for model response │ │ +│ │ ponse text (uses terminal default │ │ +│ │ if blank) │ │ +│ │ #3d3f51 border.def Standard border color │ │ +│ │ ault │ │ +│ │ #6C7086ui.comme Color for code comments and │ │ +│ │ nt metadata │ │ +│ │ #6C708 ui.symbol Color for technical symbols │ │ +│ │ 6 and UI icons │ │ +│ │ #89B4F ui.active Border color for active or │ │ +│ │ A running elements │ │ +│ │ #3d3f5 ui.dark Deeply dimmed color for │ │ +│ │ 1 subtle UI elements │ │ +│ │ #A6E3A ui.focus Color for focused elements │ │ +│ │ 1 (e.g. selected menu items, │ │ +│ │ focused borders) │ │ +│ │ #F38BA8status.err Color for error messages │ │ +│ │ or and critical status │ │ +│ │ #A6E3A1status.suc Color for success messages │ │ +│ │ cess and positive status │ │ +│ │ #F9E2A status.wa Color for warnings and │ │ +│ │ F rning cautionary status │ │ +│ │ #4796E4 ui.gradien │ │ +│ │ #847ACE t │ │ +│ │ #C3677F │ │ +│ ╰─────────────────────────────────────────────────╯ │ +│ │ +│ (Use Enter to select, Tab to configure scope, Esc to close) │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ +" +`; diff --git a/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx b/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx index 72ce8cec5f..233f905760 100644 --- a/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ShellToolMessage.test.tsx @@ -65,7 +65,7 @@ describe('', () => { ['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('', () => { await waitFor(() => { expect(mockSetEmbeddedShellFocused).toHaveBeenCalledWith(true); }); + unmount(); }); it('resets focus when shell finishes', async () => { let updateStatus: (s: CoreToolCallStatus) => void = () => {}; @@ -91,7 +92,7 @@ describe('', () => { return ; }; - const { lastFrame } = renderWithProviders(, { + const { lastFrame, unmount } = renderWithProviders(, { uiActions, uiState: { streamingState: StreamingState.Idle, @@ -115,6 +116,7 @@ describe('', () => { expect(mockSetEmbeddedShellFocused).toHaveBeenCalledWith(false); expect(lastFrame()).not.toContain('(Shift+Tab to unfocus)'); }); + unmount(); }); }); @@ -164,9 +166,13 @@ describe('', () => { }, ], ])('%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('', () => { 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('', () => { 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('', () => { // 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('', () => { 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('', () => { expect(frame.match(/Line \d+/g)?.length).toBe(15); }); expect(lastFrame()).toMatchSnapshot(); + unmount(); }); }); }); diff --git a/packages/cli/src/ui/components/messages/ShellToolMessage.tsx b/packages/cli/src/ui/components/messages/ShellToolMessage.tsx index 8e760b28e7..3a0cdb702e 100644 --- a/packages/cli/src/ui/components/messages/ShellToolMessage.tsx +++ b/packages/cli/src/ui/components/messages/ShellToolMessage.tsx @@ -125,7 +125,11 @@ export const ShellToolMessage: React.FC = ({ borderDimColor={borderDimColor} containerRef={headerRef} > - + = ({ borderColor={borderColor} borderDimColor={borderDimColor} > - + - + {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 = ({ 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 ( @@ -153,10 +159,9 @@ export const ToolStatusIndicator: React.FC = ({ {TOOL_STATUS.PENDING} )} {status === ToolCallStatus.Executing && ( - + + + )} {status === ToolCallStatus.Success && ( diff --git a/packages/cli/src/ui/components/messages/UserMessage.tsx b/packages/cli/src/ui/components/messages/UserMessage.tsx index 6453ab94c1..6609a7d1c4 100644 --- a/packages/cli/src/ui/components/messages/UserMessage.tsx +++ b/packages/cli/src/ui/components/messages/UserMessage.tsx @@ -29,7 +29,7 @@ export const UserMessage: React.FC = ({ 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; diff --git a/packages/cli/src/ui/components/messages/__snapshots__/RedirectionConfirmation.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/RedirectionConfirmation.test.tsx.snap index 4f89811121..f584e7f483 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/RedirectionConfirmation.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/RedirectionConfirmation.test.tsx.snap @@ -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) " diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ShellToolMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ShellToolMessage.test.tsx.snap index 0d34c7e49d..b51d7c435b 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ShellToolMessage.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ShellToolMessage.test.tsx.snap @@ -2,7 +2,7 @@ exports[` > 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[` > Height Constraints > fully expands in alternate exports[` > 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[` > Height Constraints > stays constrained in altern exports[` > 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[` > Height Constraints > uses ACTIVE_SHELL_MAX_LINES exports[` > 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[` > Height Constraints > uses full availableTerminal exports[` > 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[` > Snapshots > renders in Alternate Buffer mode whi exports[` > 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[` > Snapshots > renders in Error state 1`] = ` exports[` > Snapshots > renders in Executing state 1`] = ` "╭──────────────────────────────────────────────────────────────────────────────╮ -│ ⊷ Shell Command A shell command │ +│ ⊶ Shell Command A shell command │ │ │ │ Test result │ " diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage.test.tsx.snap index 72eda055d5..9e8dfe3a15 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage.test.tsx.snap @@ -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) diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap index 6adcb80a5c..29da4d5860 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap @@ -71,7 +71,7 @@ exports[` > Golden Snapshots > renders mixed tool calls incl │ │ │ Test result │ │ │ -│ ⊷ run_shell_command Run command │ +│ ⊶ run_shell_command Run command │ │ │ │ Test result │ ╰──────────────────────────────────────────────────────────────────────────╯ diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolMessage.test.tsx.snap index f31865874d..ec5643e773 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolMessage.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolMessage.test.tsx.snap @@ -29,7 +29,7 @@ exports[` > ToolStatusIndicator rendering > shows - for Canceled exports[` > 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[` > ToolStatusIndicator rendering > shows o for Pending s exports[` > 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[` > ToolStatusIndicator rendering > shows paused spinner exports[` > 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[` > renders DiffRenderer for diff results 1`] = ` exports[` > 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[` > renders emphasis correctly 2`] = ` exports[` > 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[` > renders indeterminate progress when total is missing exports[` > renders only percentage when progressMessage is missing 1`] = ` "╭──────────────────────────────────────────────────────────────────────────────╮ -│ MockRespondingSpinnertest-tool A tool for testing │ +│ ⊶ test-tool A tool for testing │ │ │ │ ███████████████░░░░░ 75% │ │ Test result │ diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolMessageFocusHint.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolMessageFocusHint.test.tsx.snap index fb4f1ec722..8da15d7fdb 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolMessageFocusHint.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolMessageFocusHint.test.tsx.snap @@ -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) │ │ │ " `; diff --git a/packages/cli/src/ui/components/shared/BaseSelectionList.test.tsx b/packages/cli/src/ui/components/shared/BaseSelectionList.test.tsx index 2444374c3e..8fffd4c5fc 100644 --- a/packages/cli/src/ui/components/shared/BaseSelectionList.test.tsx +++ b/packages/cli/src/ui/components/shared/BaseSelectionList.test.tsx @@ -19,13 +19,15 @@ vi.mock('../../hooks/useSelectionList.js'); const mockTheme = { text: { primary: 'COLOR_PRIMARY', secondary: 'COLOR_SECONDARY' }, - status: { success: 'COLOR_SUCCESS' }, + ui: { focus: 'COLOR_FOCUS' }, + background: { focus: 'COLOR_FOCUS_BG' }, } as typeof theme; vi.mock('../../semantic-colors.js', () => ({ theme: { text: { primary: 'COLOR_PRIMARY', secondary: 'COLOR_SECONDARY' }, - status: { success: 'COLOR_SUCCESS' }, + ui: { focus: 'COLOR_FOCUS' }, + background: { focus: 'COLOR_FOCUS_BG' }, }, })); @@ -161,8 +163,8 @@ describe('BaseSelectionList', () => { expect(mockRenderItem).toHaveBeenCalledWith( items[0], expect.objectContaining({ - titleColor: mockTheme.status.success, - numberColor: mockTheme.status.success, + titleColor: mockTheme.ui.focus, + numberColor: mockTheme.ui.focus, isSelected: true, }), ); @@ -207,8 +209,8 @@ describe('BaseSelectionList', () => { expect(mockRenderItem).toHaveBeenCalledWith( items[1], expect.objectContaining({ - titleColor: mockTheme.status.success, - numberColor: mockTheme.status.success, + titleColor: mockTheme.ui.focus, + numberColor: mockTheme.ui.focus, isSelected: true, }), ); @@ -267,7 +269,7 @@ describe('BaseSelectionList', () => { items[0], expect.objectContaining({ isSelected: true, - titleColor: mockTheme.status.success, + titleColor: mockTheme.ui.focus, numberColor: mockTheme.text.secondary, }), ); diff --git a/packages/cli/src/ui/components/shared/BaseSelectionList.tsx b/packages/cli/src/ui/components/shared/BaseSelectionList.tsx index db0d624a74..1467bb357e 100644 --- a/packages/cli/src/ui/components/shared/BaseSelectionList.tsx +++ b/packages/cli/src/ui/components/shared/BaseSelectionList.tsx @@ -117,8 +117,8 @@ export function BaseSelectionList< let numberColor = theme.text.primary; if (isSelected) { - titleColor = theme.status.success; - numberColor = theme.status.success; + titleColor = theme.ui.focus; + numberColor = theme.ui.focus; } else if (item.disabled) { titleColor = theme.text.secondary; numberColor = theme.text.secondary; @@ -137,11 +137,15 @@ export function BaseSelectionList< )}.`; return ( - + {/* Radio button indicator */} {isSelected ? '●' : ' '} diff --git a/packages/cli/src/ui/components/shared/BaseSettingsDialog.tsx b/packages/cli/src/ui/components/shared/BaseSettingsDialog.tsx index 58f15aa85a..c10104591d 100644 --- a/packages/cli/src/ui/components/shared/BaseSettingsDialog.tsx +++ b/packages/cli/src/ui/components/shared/BaseSettingsDialog.tsx @@ -459,7 +459,7 @@ export function BaseSettingsDialog({ editingKey ? theme.border.default : focusSection === 'settings' - ? theme.border.focused + ? theme.ui.focus : theme.border.default } paddingX={1} @@ -522,12 +522,17 @@ export function BaseSettingsDialog({ return ( - + {isActive ? '●' : ''} @@ -544,9 +549,7 @@ export function BaseSettingsDialog({ minWidth={0} > {item.label} {item.scopeMessage && ( @@ -565,7 +568,7 @@ export function BaseSettingsDialog({ ({ primary: 'COLOR_PRIMARY', secondary: 'COLOR_SECONDARY', }, + ui: { + focus: 'COLOR_FOCUS', + }, + background: { + focus: 'COLOR_FOCUS_BG', + }, status: { success: 'COLOR_SUCCESS', }, diff --git a/packages/cli/src/ui/components/shared/RadioButtonSelect.test.tsx b/packages/cli/src/ui/components/shared/RadioButtonSelect.test.tsx index 33c77f1a25..00607e522a 100644 --- a/packages/cli/src/ui/components/shared/RadioButtonSelect.test.tsx +++ b/packages/cli/src/ui/components/shared/RadioButtonSelect.test.tsx @@ -27,6 +27,8 @@ vi.mock('./BaseSelectionList.js', () => ({ vi.mock('../../semantic-colors.js', () => ({ theme: { text: { secondary: 'COLOR_SECONDARY' }, + ui: { focus: 'COLOR_FOCUS' }, + background: { focus: 'COLOR_FOCUS_BG' }, }, })); diff --git a/packages/cli/src/ui/constants.ts b/packages/cli/src/ui/constants.ts index 7a59645cef..448dc37523 100644 --- a/packages/cli/src/ui/constants.ts +++ b/packages/cli/src/ui/constants.ts @@ -37,6 +37,7 @@ export const EXPAND_HINT_DURATION_MS = 5000; export const DEFAULT_BACKGROUND_OPACITY = 0.16; export const DEFAULT_INPUT_BACKGROUND_OPACITY = 0.24; +export const DEFAULT_SELECTION_OPACITY = 0.2; export const DEFAULT_BORDER_OPACITY = 0.4; export const KEYBOARD_SHORTCUTS_URL = diff --git a/packages/cli/src/ui/hooks/useBanner.test.ts b/packages/cli/src/ui/hooks/useBanner.test.ts index 1d876c078c..cb5712bec4 100644 --- a/packages/cli/src/ui/hooks/useBanner.test.ts +++ b/packages/cli/src/ui/hooks/useBanner.test.ts @@ -29,6 +29,9 @@ vi.mock('../semantic-colors.js', () => ({ status: { warning: 'mock-warning-color', }, + ui: { + focus: 'mock-focus-color', + }, }, })); diff --git a/packages/cli/src/ui/themes/ansi.ts b/packages/cli/src/ui/themes/ansi.ts index 08c0a2c968..a8c788bf54 100644 --- a/packages/cli/src/ui/themes/ansi.ts +++ b/packages/cli/src/ui/themes/ansi.ts @@ -23,6 +23,7 @@ const ansiColors: ColorsTheme = { Comment: 'gray', Gray: 'gray', DarkGray: 'gray', + FocusBackground: 'black', GradientColors: ['cyan', 'green'], }; diff --git a/packages/cli/src/ui/themes/color-utils.ts b/packages/cli/src/ui/themes/color-utils.ts index 476703a7fc..2901bd6b2e 100644 --- a/packages/cli/src/ui/themes/color-utils.ts +++ b/packages/cli/src/ui/themes/color-utils.ts @@ -4,38 +4,25 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { debugLogger } from '@google/gemini-cli-core'; -import tinygradient from 'tinygradient'; -import tinycolor from 'tinycolor2'; +import { + resolveColor, + interpolateColor, + getThemeTypeFromBackgroundColor, + INK_SUPPORTED_NAMES, + INK_NAME_TO_HEX_MAP, + getLuminance, + CSS_NAME_TO_HEX_MAP, +} from './theme.js'; -// Define the set of Ink's named colors for quick lookup -export const INK_SUPPORTED_NAMES = new Set([ - 'black', - 'red', - 'green', - 'yellow', - 'blue', - 'cyan', - 'magenta', - 'white', - 'gray', - 'grey', - 'blackbright', - 'redbright', - 'greenbright', - 'yellowbright', - 'bluebright', - 'cyanbright', - 'magentabright', - 'whitebright', -]); - -// Use tinycolor's built-in names map for CSS colors, excluding ones Ink supports -export const CSS_NAME_TO_HEX_MAP = Object.fromEntries( - Object.entries(tinycolor.names) - .filter(([name]) => !INK_SUPPORTED_NAMES.has(name)) - .map(([name, hex]) => [name, `#${hex}`]), -); +export { + resolveColor, + interpolateColor, + getThemeTypeFromBackgroundColor, + INK_SUPPORTED_NAMES, + INK_NAME_TO_HEX_MAP, + getLuminance, + CSS_NAME_TO_HEX_MAP, +}; /** * Checks if a color string is valid (hex, Ink-supported color name, or CSS color name). @@ -66,45 +53,6 @@ export function isValidColor(color: string): boolean { return false; } -/** - * Resolves a CSS color value (name or hex) into an Ink-compatible color string. - * @param colorValue The raw color string (e.g., 'blue', '#ff0000', 'darkkhaki'). - * @returns An Ink-compatible color string (hex or name), or undefined if not resolvable. - */ -export function resolveColor(colorValue: string): string | undefined { - const lowerColor = colorValue.toLowerCase(); - - // 1. Check if it's already a hex code and valid - if (lowerColor.startsWith('#')) { - if (/^#[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$/.test(colorValue)) { - return lowerColor; - } else { - return undefined; - } - } - - // Handle hex codes without # - if (/^[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$/.test(colorValue)) { - return `#${lowerColor}`; - } - - // 2. Check if it's an Ink supported name (lowercase) - if (INK_SUPPORTED_NAMES.has(lowerColor)) { - return lowerColor; // Use Ink name directly - } - - // 3. Check if it's a known CSS name we can map to hex - if (CSS_NAME_TO_HEX_MAP[lowerColor]) { - return CSS_NAME_TO_HEX_MAP[lowerColor]; // Use mapped hex - } - - // 4. Could not resolve - debugLogger.warn( - `[ColorUtils] Could not resolve color "${colorValue}" to an Ink-compatible format.`, - ); - return undefined; -} - /** * Returns a "safe" background color to use in low-color terminals if the * terminal background is a standard black or white. @@ -132,73 +80,6 @@ export function getSafeLowColorBackground( return undefined; } -export function interpolateColor( - color1: string, - color2: string, - factor: number, -) { - if (factor <= 0 && color1) { - return color1; - } - if (factor >= 1 && color2) { - return color2; - } - if (!color1 || !color2) { - return ''; - } - const gradient = tinygradient(color1, color2); - const color = gradient.rgbAt(factor); - return color.toHexString(); -} - -export function getThemeTypeFromBackgroundColor( - backgroundColor: string | undefined, -): 'light' | 'dark' | undefined { - if (!backgroundColor) { - return undefined; - } - - const resolvedColor = resolveColor(backgroundColor); - if (!resolvedColor) { - return undefined; - } - - const luminance = getLuminance(resolvedColor); - return luminance > 128 ? 'light' : 'dark'; -} - -// Mapping for ANSI bright colors that are not in tinycolor's standard CSS names -export const INK_NAME_TO_HEX_MAP: Readonly> = { - blackbright: '#555555', - redbright: '#ff5555', - greenbright: '#55ff55', - yellowbright: '#ffff55', - bluebright: '#5555ff', - magentabright: '#ff55ff', - cyanbright: '#55ffff', - whitebright: '#ffffff', -}; - -/** - * Calculates the relative luminance of a color. - * See https://www.w3.org/TR/WCAG20/#relativeluminancedef - * - * @param color Color string (hex or Ink-supported name) - * @returns Luminance value (0-255) - */ -export function getLuminance(color: string): number { - const resolved = color.toLowerCase(); - const hex = INK_NAME_TO_HEX_MAP[resolved] || resolved; - - const colorObj = tinycolor(hex); - if (!colorObj.isValid()) { - return 0; - } - - // tinycolor returns 0-1, we need 0-255 - return colorObj.getLuminance() * 255; -} - // Hysteresis thresholds to prevent flickering when the background color // is ambiguous (near the midpoint). export const LIGHT_THEME_LUMINANCE_THRESHOLD = 140; diff --git a/packages/cli/src/ui/themes/github-light.ts b/packages/cli/src/ui/themes/github-light.ts index 264a9d7a88..18ac7a709e 100644 --- a/packages/cli/src/ui/themes/github-light.ts +++ b/packages/cli/src/ui/themes/github-light.ts @@ -23,6 +23,7 @@ const githubLightColors: ColorsTheme = { Comment: '#998', Gray: '#999', DarkGray: interpolateColor('#999', '#f8f8f8', 0.5), + FocusColor: '#458', // AccentBlue for GitHub branding GradientColors: ['#458', '#008080'], }; diff --git a/packages/cli/src/ui/themes/holiday.ts b/packages/cli/src/ui/themes/holiday.ts index b3e72b1cc1..9cd77b43f0 100644 --- a/packages/cli/src/ui/themes/holiday.ts +++ b/packages/cli/src/ui/themes/holiday.ts @@ -23,6 +23,7 @@ const holidayColors: ColorsTheme = { Comment: '#8FBC8F', Gray: '#D7F5D3', DarkGray: interpolateColor('#D7F5D3', '#151B18', 0.5), + FocusColor: '#33F9FF', // AccentCyan for neon pop GradientColors: ['#FF0000', '#FFFFFF', '#008000'], }; diff --git a/packages/cli/src/ui/themes/no-color.ts b/packages/cli/src/ui/themes/no-color.ts index 30e34c2c12..28b2a4e858 100644 --- a/packages/cli/src/ui/themes/no-color.ts +++ b/packages/cli/src/ui/themes/no-color.ts @@ -26,6 +26,7 @@ const noColorColorsTheme: ColorsTheme = { DarkGray: '', InputBackground: '', MessageBackground: '', + FocusBackground: '', }; const noColorSemanticColors: SemanticColors = { @@ -40,6 +41,7 @@ const noColorSemanticColors: SemanticColors = { primary: '', message: '', input: '', + focus: '', diff: { added: '', removed: '', @@ -47,12 +49,13 @@ const noColorSemanticColors: SemanticColors = { }, border: { default: '', - focused: '', }, ui: { comment: '', symbol: '', + active: '', dark: '', + focus: '', gradient: [], }, status: { diff --git a/packages/cli/src/ui/themes/semantic-tokens.ts b/packages/cli/src/ui/themes/semantic-tokens.ts index ca46fadb56..b5e9140156 100644 --- a/packages/cli/src/ui/themes/semantic-tokens.ts +++ b/packages/cli/src/ui/themes/semantic-tokens.ts @@ -18,6 +18,7 @@ export interface SemanticColors { primary: string; message: string; input: string; + focus: string; diff: { added: string; removed: string; @@ -25,12 +26,13 @@ export interface SemanticColors { }; border: { default: string; - focused: string; }; ui: { comment: string; symbol: string; + active: string; dark: string; + focus: string; gradient: string[] | undefined; }; status: { @@ -52,6 +54,7 @@ export const lightSemanticColors: SemanticColors = { primary: lightTheme.Background, message: lightTheme.MessageBackground!, input: lightTheme.InputBackground!, + focus: lightTheme.FocusBackground!, diff: { added: lightTheme.DiffAdded, removed: lightTheme.DiffRemoved, @@ -59,12 +62,13 @@ export const lightSemanticColors: SemanticColors = { }, border: { default: lightTheme.DarkGray, - focused: lightTheme.AccentBlue, }, ui: { comment: lightTheme.Comment, symbol: lightTheme.Gray, + active: lightTheme.AccentBlue, dark: lightTheme.DarkGray, + focus: lightTheme.AccentGreen, gradient: lightTheme.GradientColors, }, status: { @@ -86,6 +90,7 @@ export const darkSemanticColors: SemanticColors = { primary: darkTheme.Background, message: darkTheme.MessageBackground!, input: darkTheme.InputBackground!, + focus: darkTheme.FocusBackground!, diff: { added: darkTheme.DiffAdded, removed: darkTheme.DiffRemoved, @@ -93,12 +98,13 @@ export const darkSemanticColors: SemanticColors = { }, border: { default: darkTheme.DarkGray, - focused: darkTheme.AccentBlue, }, ui: { comment: darkTheme.Comment, symbol: darkTheme.Gray, + active: darkTheme.AccentBlue, dark: darkTheme.DarkGray, + focus: darkTheme.AccentGreen, gradient: darkTheme.GradientColors, }, status: { diff --git a/packages/cli/src/ui/themes/solarized-dark.ts b/packages/cli/src/ui/themes/solarized-dark.ts index c2bf3db34d..cef9fd9d22 100644 --- a/packages/cli/src/ui/themes/solarized-dark.ts +++ b/packages/cli/src/ui/themes/solarized-dark.ts @@ -4,8 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { type ColorsTheme, Theme } from './theme.js'; +import { type ColorsTheme, Theme, interpolateColor } from './theme.js'; import { type SemanticColors } from './semantic-tokens.js'; +import { DEFAULT_SELECTION_OPACITY } from '../constants.js'; const solarizedDarkColors: ColorsTheme = { type: 'dark', @@ -38,6 +39,7 @@ const semanticColors: SemanticColors = { primary: '#002b36', message: '#073642', input: '#073642', + focus: interpolateColor('#002b36', '#859900', DEFAULT_SELECTION_OPACITY), diff: { added: '#00382f', removed: '#3d0115', @@ -45,13 +47,14 @@ const semanticColors: SemanticColors = { }, border: { default: '#073642', - focused: '#586e75', }, ui: { comment: '#586e75', symbol: '#93a1a1', + active: '#268bd2', dark: '#073642', - gradient: ['#268bd2', '#2aa198'], + focus: '#859900', + gradient: ['#268bd2', '#2aa198', '#859900'], }, status: { success: '#859900', diff --git a/packages/cli/src/ui/themes/solarized-light.ts b/packages/cli/src/ui/themes/solarized-light.ts index 297238866d..b9ba313b1b 100644 --- a/packages/cli/src/ui/themes/solarized-light.ts +++ b/packages/cli/src/ui/themes/solarized-light.ts @@ -4,8 +4,9 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { type ColorsTheme, Theme } from './theme.js'; +import { type ColorsTheme, Theme, interpolateColor } from './theme.js'; import { type SemanticColors } from './semantic-tokens.js'; +import { DEFAULT_SELECTION_OPACITY } from '../constants.js'; const solarizedLightColors: ColorsTheme = { type: 'light', @@ -38,6 +39,7 @@ const semanticColors: SemanticColors = { primary: '#fdf6e3', message: '#eee8d5', input: '#eee8d5', + focus: interpolateColor('#fdf6e3', '#859900', DEFAULT_SELECTION_OPACITY), diff: { added: '#d7f2d7', removed: '#f2d7d7', @@ -45,13 +47,14 @@ const semanticColors: SemanticColors = { }, border: { default: '#eee8d5', - focused: '#93a1a1', }, ui: { comment: '#93a1a1', symbol: '#586e75', + active: '#268bd2', dark: '#eee8d5', - gradient: ['#268bd2', '#2aa198'], + focus: '#859900', + gradient: ['#268bd2', '#2aa198', '#859900'], }, status: { success: '#859900', diff --git a/packages/cli/src/ui/themes/theme-manager.ts b/packages/cli/src/ui/themes/theme-manager.ts index da54ba5d3e..775f085f6e 100644 --- a/packages/cli/src/ui/themes/theme-manager.ts +++ b/packages/cli/src/ui/themes/theme-manager.ts @@ -22,16 +22,18 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import type { Theme, ThemeType, ColorsTheme } from './theme.js'; import type { CustomTheme } from '@google/gemini-cli-core'; -import { createCustomTheme, validateCustomTheme } from './theme.js'; -import type { SemanticColors } from './semantic-tokens.js'; import { + createCustomTheme, + validateCustomTheme, interpolateColor, getThemeTypeFromBackgroundColor, resolveColor, -} from './color-utils.js'; +} from './theme.js'; +import type { SemanticColors } from './semantic-tokens.js'; import { DEFAULT_BACKGROUND_OPACITY, DEFAULT_INPUT_BACKGROUND_OPACITY, + DEFAULT_SELECTION_OPACITY, DEFAULT_BORDER_OPACITY, } from '../constants.js'; import { ANSI } from './ansi.js'; @@ -369,6 +371,11 @@ class ThemeManager { colors.Gray, DEFAULT_BACKGROUND_OPACITY, ), + FocusBackground: interpolateColor( + this.terminalBackground, + activeTheme.colors.FocusColor ?? activeTheme.colors.AccentGreen, + DEFAULT_SELECTION_OPACITY, + ), }; } else { this.cachedColors = colors; @@ -402,6 +409,7 @@ class ThemeManager { primary: this.terminalBackground, message: colors.MessageBackground!, input: colors.InputBackground!, + focus: colors.FocusBackground!, }, border: { ...semanticColors.border, @@ -410,6 +418,7 @@ class ThemeManager { ui: { ...semanticColors.ui, dark: colors.DarkGray, + focus: colors.FocusColor ?? colors.AccentGreen, }, }; } else { diff --git a/packages/cli/src/ui/themes/theme.ts b/packages/cli/src/ui/themes/theme.ts index c4277cd834..7785e9bda0 100644 --- a/packages/cli/src/ui/themes/theme.ts +++ b/packages/cli/src/ui/themes/theme.ts @@ -8,18 +8,153 @@ import type { CSSProperties } from 'react'; import type { SemanticColors } from './semantic-tokens.js'; -import { - resolveColor, - interpolateColor, - getThemeTypeFromBackgroundColor, -} from './color-utils.js'; - import type { CustomTheme } from '@google/gemini-cli-core'; import { - DEFAULT_BACKGROUND_OPACITY, DEFAULT_INPUT_BACKGROUND_OPACITY, + DEFAULT_SELECTION_OPACITY, DEFAULT_BORDER_OPACITY, } from '../constants.js'; +import tinygradient from 'tinygradient'; +import tinycolor from 'tinycolor2'; + +// Define the set of Ink's named colors for quick lookup +export const INK_SUPPORTED_NAMES = new Set([ + 'black', + 'red', + 'green', + 'yellow', + 'blue', + 'cyan', + 'magenta', + 'white', + 'gray', + 'grey', + 'blackbright', + 'redbright', + 'greenbright', + 'yellowbright', + 'bluebright', + 'cyanbright', + 'magentabright', + 'whitebright', +]); + +// Use tinycolor's built-in names map for CSS colors, excluding ones Ink supports +export const CSS_NAME_TO_HEX_MAP = Object.fromEntries( + Object.entries(tinycolor.names) + .filter(([name]) => !INK_SUPPORTED_NAMES.has(name)) + .map(([name, hex]) => [name, `#${hex}`]), +); + +// Mapping for ANSI bright colors that are not in tinycolor's standard CSS names +export const INK_NAME_TO_HEX_MAP: Readonly> = { + blackbright: '#555555', + redbright: '#ff5555', + greenbright: '#55ff55', + yellowbright: '#ffff55', + bluebright: '#5555ff', + magentabright: '#ff55ff', + cyanbright: '#55ffff', + whitebright: '#ffffff', +}; + +/** + * Calculates the relative luminance of a color. + * See https://www.w3.org/TR/WCAG20/#relativeluminancedef + * + * @param color Color string (hex or Ink-supported name) + * @returns Luminance value (0-255) + */ +export function getLuminance(color: string): number { + const resolved = color.toLowerCase(); + const hex = INK_NAME_TO_HEX_MAP[resolved] || resolved; + + const colorObj = tinycolor(hex); + if (!colorObj.isValid()) { + return 0; + } + + // tinycolor returns 0-1, we need 0-255 + return colorObj.getLuminance() * 255; +} + +/** + * Resolves a CSS color value (name or hex) into an Ink-compatible color string. + * @param colorValue The raw color string (e.g., 'blue', '#ff0000', 'darkkhaki'). + * @returns An Ink-compatible color string (hex or name), or undefined if not resolvable. + */ +export function resolveColor(colorValue: string): string | undefined { + const lowerColor = colorValue.toLowerCase(); + + // 1. Check if it's already a hex code and valid + if (lowerColor.startsWith('#')) { + if (/^#[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$/.test(colorValue)) { + return lowerColor; + } else { + return undefined; + } + } + + // Handle hex codes without # + if (/^[0-9A-Fa-f]{3}([0-9A-Fa-f]{3})?$/.test(colorValue)) { + return `#${lowerColor}`; + } + + // 2. Check if it's an Ink supported name (lowercase) + if (INK_SUPPORTED_NAMES.has(lowerColor)) { + return lowerColor; // Use Ink name directly + } + + // 3. Check if it's a known CSS name we can map to hex + // We can't import CSS_NAME_TO_HEX_MAP here due to circular deps, + // but we can use tinycolor directly for named colors. + const colorObj = tinycolor(lowerColor); + if (colorObj.isValid()) { + return colorObj.toHexString(); + } + + // 4. Could not resolve + return undefined; +} + +export function interpolateColor( + color1: string, + color2: string, + factor: number, +) { + if (factor <= 0 && color1) { + return color1; + } + if (factor >= 1 && color2) { + return color2; + } + if (!color1 || !color2) { + return ''; + } + try { + const gradient = tinygradient(color1, color2); + const color = gradient.rgbAt(factor); + return color.toHexString(); + } catch (_e) { + return color1; + } +} + +export function getThemeTypeFromBackgroundColor( + backgroundColor: string | undefined, +): 'light' | 'dark' | undefined { + if (!backgroundColor) { + return undefined; + } + + const resolvedColor = resolveColor(backgroundColor); + if (!resolvedColor) { + return undefined; + } + + const luminance = getLuminance(resolvedColor); + return luminance > 128 ? 'light' : 'dark'; +} export type { CustomTheme }; @@ -43,6 +178,8 @@ export interface ColorsTheme { DarkGray: string; InputBackground?: string; MessageBackground?: string; + FocusBackground?: string; + FocusColor?: string; GradientColors?: string[]; } @@ -70,7 +207,12 @@ export const lightTheme: ColorsTheme = { MessageBackground: interpolateColor( '#FAFAFA', '#97a0b0', - DEFAULT_BACKGROUND_OPACITY, + DEFAULT_INPUT_BACKGROUND_OPACITY, + ), + FocusBackground: interpolateColor( + '#FAFAFA', + '#3CA84B', + DEFAULT_SELECTION_OPACITY, ), GradientColors: ['#4796E4', '#847ACE', '#C3677F'], }; @@ -99,7 +241,12 @@ export const darkTheme: ColorsTheme = { MessageBackground: interpolateColor( '#1E1E2E', '#6C7086', - DEFAULT_BACKGROUND_OPACITY, + DEFAULT_INPUT_BACKGROUND_OPACITY, + ), + FocusBackground: interpolateColor( + '#1E1E2E', + '#A6E3A1', + DEFAULT_SELECTION_OPACITY, ), GradientColors: ['#4796E4', '#847ACE', '#C3677F'], }; @@ -122,6 +269,7 @@ export const ansiTheme: ColorsTheme = { DarkGray: 'gray', InputBackground: 'black', MessageBackground: 'black', + FocusBackground: 'black', }; export class Theme { @@ -164,7 +312,7 @@ export class Theme { interpolateColor( this.colors.Background, this.colors.Gray, - DEFAULT_BACKGROUND_OPACITY, + DEFAULT_INPUT_BACKGROUND_OPACITY, ), input: this.colors.InputBackground ?? @@ -173,6 +321,13 @@ export class Theme { this.colors.Gray, DEFAULT_INPUT_BACKGROUND_OPACITY, ), + focus: + this.colors.FocusBackground ?? + interpolateColor( + this.colors.Background, + this.colors.FocusColor ?? this.colors.AccentGreen, + DEFAULT_SELECTION_OPACITY, + ), diff: { added: this.colors.DiffAdded, removed: this.colors.DiffRemoved, @@ -180,12 +335,13 @@ export class Theme { }, border: { default: this.colors.DarkGray, - focused: this.colors.AccentBlue, }, ui: { comment: this.colors.Gray, symbol: this.colors.AccentCyan, + active: this.colors.AccentBlue, dark: this.colors.DarkGray, + focus: this.colors.FocusColor ?? this.colors.AccentGreen, gradient: this.colors.GradientColors, }, status: { @@ -292,8 +448,14 @@ export function createCustomTheme(customTheme: CustomTheme): Theme { MessageBackground: interpolateColor( customTheme.background?.primary ?? customTheme.Background ?? '', customTheme.text?.secondary ?? customTheme.Gray ?? '', - DEFAULT_BACKGROUND_OPACITY, + DEFAULT_INPUT_BACKGROUND_OPACITY, ), + FocusBackground: interpolateColor( + customTheme.background?.primary ?? customTheme.Background ?? '', + customTheme.status?.success ?? customTheme.AccentGreen ?? '#3CA84B', // Fallback to a default green if not found + DEFAULT_SELECTION_OPACITY, + ), + FocusColor: customTheme.ui?.focus ?? customTheme.AccentGreen, GradientColors: customTheme.ui?.gradient ?? customTheme.GradientColors, }; @@ -450,6 +612,7 @@ export function createCustomTheme(customTheme: CustomTheme): Theme { primary: customTheme.background?.primary ?? colors.Background, message: colors.MessageBackground!, input: colors.InputBackground!, + focus: colors.FocusBackground!, diff: { added: customTheme.background?.diff?.added ?? colors.DiffAdded, removed: customTheme.background?.diff?.removed ?? colors.DiffRemoved, @@ -457,12 +620,13 @@ export function createCustomTheme(customTheme: CustomTheme): Theme { }, border: { default: colors.DarkGray, - focused: customTheme.border?.focused ?? colors.AccentBlue, }, ui: { comment: customTheme.ui?.comment ?? colors.Comment, symbol: customTheme.ui?.symbol ?? colors.Gray, + active: customTheme.ui?.active ?? colors.AccentBlue, dark: colors.DarkGray, + focus: colors.FocusColor ?? colors.AccentGreen, gradient: customTheme.ui?.gradient ?? colors.GradientColors, }, status: { diff --git a/packages/cli/src/ui/themes/xcode.ts b/packages/cli/src/ui/themes/xcode.ts index 5d20f35c36..105c1d1a00 100644 --- a/packages/cli/src/ui/themes/xcode.ts +++ b/packages/cli/src/ui/themes/xcode.ts @@ -23,6 +23,7 @@ const xcodeColors: ColorsTheme = { Comment: '#007400', Gray: '#c0c0c0', DarkGray: interpolateColor('#c0c0c0', '#fff', 0.5), + FocusColor: '#1c00cf', // AccentBlue for more vibrance GradientColors: ['#1c00cf', '#007400'], }; diff --git a/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-a-pending-search-dialog-google_web_search-.snap.svg b/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-a-pending-search-dialog-google_web_search-.snap.svg index 4c1965c5df..fa207b48e5 100644 --- a/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-a-pending-search-dialog-google_web_search-.snap.svg +++ b/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-a-pending-search-dialog-google_web_search-.snap.svg @@ -19,7 +19,7 @@ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ - + google_web_search diff --git a/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-a-shell-tool.snap.svg b/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-a-shell-tool.snap.svg index 74e1e0d2b2..686698adaf 100644 --- a/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-a-shell-tool.snap.svg +++ b/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-a-shell-tool.snap.svg @@ -17,16 +17,16 @@ - ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ - - + ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ + + run_shell_command - - - - + + + + Running command... - - ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ + + ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-an-empty-slice-following-a-search-tool.snap.svg b/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-an-empty-slice-following-a-search-tool.snap.svg index 4c1965c5df..fa207b48e5 100644 --- a/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-an-empty-slice-following-a-search-tool.snap.svg +++ b/packages/cli/src/ui/utils/__snapshots__/borderStyles-MainContent-tool-group-border-SVG-snapshots-should-render-SVG-snapshot-for-an-empty-slice-following-a-search-tool.snap.svg @@ -19,7 +19,7 @@ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ - + google_web_search diff --git a/packages/cli/src/ui/utils/__snapshots__/borderStyles.test.tsx.snap b/packages/cli/src/ui/utils/__snapshots__/borderStyles.test.tsx.snap index d34d820236..bdf1e95332 100644 --- a/packages/cli/src/ui/utils/__snapshots__/borderStyles.test.tsx.snap +++ b/packages/cli/src/ui/utils/__snapshots__/borderStyles.test.tsx.snap @@ -8,7 +8,7 @@ exports[`MainContent tool group border SVG snapshots > should render SVG snapsho ▝▀ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ -│ ⊷ google_web_search │ +│ ⊶ google_web_search │ │ │ │ Searching... │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯" @@ -22,7 +22,7 @@ exports[`MainContent tool group border SVG snapshots > should render SVG snapsho ▝▀ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ -│ ⊷ run_shell_command │ +│ ⊶ run_shell_command │ │ │ │ Running command... │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯" @@ -36,7 +36,7 @@ exports[`MainContent tool group border SVG snapshots > should render SVG snapsho ▝▀ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ -│ ⊷ google_web_search │ +│ ⊶ google_web_search │ │ │ │ Searching... │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯" diff --git a/packages/cli/src/ui/utils/borderStyles.test.tsx b/packages/cli/src/ui/utils/borderStyles.test.tsx index 91b2497f7f..1852a0cb82 100644 --- a/packages/cli/src/ui/utils/borderStyles.test.tsx +++ b/packages/cli/src/ui/utils/borderStyles.test.tsx @@ -4,13 +4,18 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; import { getToolGroupBorderAppearance } from './borderStyles.js'; import { CoreToolCallStatus } from '@google/gemini-cli-core'; import { theme } from '../semantic-colors.js'; import type { IndividualToolCallDisplay } from '../types.js'; import { renderWithProviders } from '../../test-utils/render.js'; import { MainContent } from '../components/MainContent.js'; +import { Text } from 'ink'; + +vi.mock('../components/CliSpinner.js', () => ({ + CliSpinner: () => , +})); describe('getToolGroupBorderAppearance', () => { it('should use warning color for pending non-shell tools', () => { @@ -60,7 +65,7 @@ describe('getToolGroupBorderAppearance', () => { expect(appearance.borderDimColor).toBe(true); }); - it('should use symbol color for shell tools', () => { + it('should use active color for shell tools', () => { const item = { type: 'tool_group' as const, tools: [ @@ -73,9 +78,28 @@ describe('getToolGroupBorderAppearance', () => { ] as IndividualToolCallDisplay[], }; const appearance = getToolGroupBorderAppearance(item, undefined, false, []); - expect(appearance.borderColor).toBe(theme.ui.symbol); + expect(appearance.borderColor).toBe(theme.ui.active); expect(appearance.borderDimColor).toBe(true); }); + + it('should use focus color for focused shell tools', () => { + const ptyId = 123; + const item = { + type: 'tool_group' as const, + tools: [ + { + name: 'run_shell_command', + status: CoreToolCallStatus.Executing, + resultDisplay: '', + callId: 'call-1', + ptyId, + }, + ] as IndividualToolCallDisplay[], + }; + const appearance = getToolGroupBorderAppearance(item, ptyId, true, []); + expect(appearance.borderColor).toBe(theme.ui.focus); + expect(appearance.borderDimColor).toBe(false); + }); }); describe('MainContent tool group border SVG snapshots', () => { diff --git a/packages/cli/src/ui/utils/borderStyles.ts b/packages/cli/src/ui/utils/borderStyles.ts index 276d4a2502..7b7b767734 100644 --- a/packages/cli/src/ui/utils/borderStyles.ts +++ b/packages/cli/src/ui/utils/borderStyles.ts @@ -113,9 +113,10 @@ export function getToolGroupBorderAppearance( isCurrentlyInShellTurn && !!embeddedShellFocused); - const borderColor = - (isShell && isPending) || isEffectivelyFocused - ? theme.ui.symbol + const borderColor = isEffectivelyFocused + ? theme.ui.focus + : isShell && isPending + ? theme.ui.active : isPending ? theme.status.warning : theme.border.default; diff --git a/packages/cli/src/ui/utils/markdownParsingUtils.test.ts b/packages/cli/src/ui/utils/markdownParsingUtils.test.ts index a9ff96401f..c32bda58fa 100644 --- a/packages/cli/src/ui/utils/markdownParsingUtils.test.ts +++ b/packages/cli/src/ui/utils/markdownParsingUtils.test.ts @@ -17,6 +17,9 @@ vi.mock('../semantic-colors.js', () => ({ accent: 'cyan', link: 'blue', }, + ui: { + focus: 'green', + }, }, })); diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 258bd78f93..306e92e8d9 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -250,11 +250,12 @@ export interface CustomTheme { }; border?: { default?: string; - focused?: string; }; ui?: { comment?: string; symbol?: string; + active?: string; + focus?: string; gradient?: string[]; }; status?: {