Compare commits

...

20 Commits

Author SHA1 Message Date
Mark McLaughlin c3d11e09e1 Merge branch 'main' into feat/card-component 2026-02-10 14:22:34 -08:00
Mark McLaughlin 9ae8f7f3ac refactor: Update Card snapshots to improve formatting and add new card states 2026-02-10 12:00:30 -08:00
Mark McLaughlin 9c91f25d62 refactor: Update Card component to adjust colors for Executing status 2026-02-10 12:00:22 -08:00
Mark McLaughlin 85446fdfd3 refactor: Update Card tests to use renderWithProviders and add new card states 2026-02-10 12:00:13 -08:00
Mark McLaughlin 861a72906d refactor: Simplify CardDemo example by removing redundant cards and enhancing clarity 2026-02-10 12:00:05 -08:00
Mark McLaughlin 6f20f8a300 refactor: Update Card component snapshots to use showStatusIndicator and improve clarity 2026-02-10 11:36:23 -08:00
Mark McLaughlin 544ccdbe1e refactor: Replace prefix prop with showStatusIndicator in Card component tests for clarity 2026-02-10 11:36:01 -08:00
Mark McLaughlin 8d9f45efeb feat: Add CardDemo example showcasing various Card component states and configurations 2026-02-10 11:35:29 -08:00
Mark McLaughlin 47d51fb4bd refactor: Rename prefix prop to showStatusIndicator for clarity in Card component 2026-02-10 11:35:19 -08:00
Mark McLaughlin 6fd45b50f6 refactor: Integrate checkExhaustive function and ensure Card component width is configurable 2026-02-10 10:56:09 -08:00
Mark McLaughlin 4596ced82e test: Add fixed width card case and restore mocks after each test 2026-02-10 10:55:51 -08:00
Mark McLaughlin 1ecfcc963e refactor: Update Card component to use a status prop and integrate ToolStatusIndicator. 2026-02-05 15:37:24 -08:00
Mark McLaughlin cc78073add test: Remove width prop from Card component test case. 2026-02-05 11:18:14 -08:00
Mark McLaughlin 89b9c4fe63 refactor: export Card component directly and explicitly define styles for the information variant. 2026-02-05 11:17:13 -08:00
Mark McLaughlin a1ff30b6fe test: Remove debug console.log from Card test and add snapshot. 2026-02-05 11:09:08 -08:00
Mark McLaughlin 5dfcab609a refactor: remove width prop from Card component and add as const to test array. 2026-02-05 11:07:19 -08:00
Mark McLaughlin 254bdbc027 Merge branch 'main' into feat/card-component 2026-02-05 08:27:46 -08:00
Mark McLaughlin d3f328dc5b fix: removed unnecessary export 2026-02-04 22:30:05 -08:00
Mark McLaughlin 0a385dbf97 feat(card): implement Card component with rendering and glyph support 2026-02-04 22:29:26 -08:00
Mark McLaughlin 70c9975296 chore: added WARNING and INFORMATION glyphs 2026-02-04 16:29:00 -08:00
5 changed files with 365 additions and 0 deletions
+86
View File
@@ -0,0 +1,86 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render, Box, Text } from 'ink';
import { Card } from '../src/ui/components/shared/Card.js';
import { ToolCallStatus, StreamingState } from '../src/ui/types.js';
import { StreamingContext } from '../src/ui/contexts/StreamingContext.js';
const CardDemo = () => {
return (
<StreamingContext.Provider value={StreamingState.Idle}>
<Box flexDirection="column" padding={1} gap={1}>
<Text bold>Card Component Demo</Text>
<Box flexDirection="column" gap={1}>
<Card
title="FindFiles"
suffix="**/*.ts"
status={ToolCallStatus.Success}
>
<Text>Found 37 files</Text>
</Card>
<Card
title="SearchText"
suffix="nano-banana"
status={ToolCallStatus.Canceled}
>
<Text>No matches found</Text>
</Card>
<Card
title="ReadFile"
suffix='{"file_path":"this_file_does_not_exist.txt"}'
status={ToolCallStatus.Error}
>
<Text>
{
'File not found:\n/users/root/src/gemini/this_file_does_not_exist.txt'
}
</Text>
</Card>
<Card
title="Shell"
suffix="npm test -w @google/gemini-cli"
status={ToolCallStatus.Confirming}
>
<Text>Apply this change?</Text>
</Card>
<Card
title="Executing"
suffix="Lorem ipsum dolor sit amet"
status={ToolCallStatus.Executing}
>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat.
</Text>
</Card>
<Card
title="Executing"
suffix="Lorem ipsum dolor sit amet"
status={ToolCallStatus.Executing}
width={60}
>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut
enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat.
</Text>
</Card>
</Box>
<Box marginTop={1}>
<Text color="dim">Press Ctrl+C to exit</Text>
</Box>
</Box>
</StreamingContext.Provider>
);
};
render(<CardDemo />);
@@ -0,0 +1,100 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { renderWithProviders } from '../../../test-utils/render.js';
import { Card } from './Card.js';
import { Text } from 'ink';
import { describe, it, expect, afterEach, vi } from 'vitest';
import { ToolCallStatus, StreamingState } from '../../types.js';
describe('Card', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it.each([
{
status: ToolCallStatus.Pending,
title: 'Gemini CLI update available',
suffix: '0.26.0 → 0.27.0',
showStatusIndicator: true,
body: 'Installed via Homebrew. Please update with "brew upgrade gemini-cli".',
},
{
status: ToolCallStatus.Canceled,
title: 'Delegate to agent',
suffix: "Delegating to agent 'cli_help'",
showStatusIndicator: true,
body: '🤖💭 Execution limit reached (ERROR_NO_COMPLETE_TASK_CALL). Attempting one final recovery turn with a grace period.',
},
{
status: ToolCallStatus.Error,
title: 'Error',
suffix: '429 You exceeded your current quota',
showStatusIndicator: true,
body: 'Go to https://aistudio.google.com/apikey to upgrade your quota tier, or submit a quota increase request in https://ai.google.dev/gemini-api/docs/rate-limits',
},
{
status: ToolCallStatus.Confirming,
title: 'Shell',
suffix: 'node -v && which gemini',
showStatusIndicator: true,
body: "ls /usr/local/bin | grep 'xattr'",
},
{
status: ToolCallStatus.Success,
title: 'ReadFolder',
suffix: '/usr/local/bin',
showStatusIndicator: true,
body: 'Listed 39 item(s).',
},
{
status: ToolCallStatus.Executing,
title: 'Searching',
suffix: 'ripgrep',
showStatusIndicator: true,
body: 'Searching for pattern in directory...',
},
{
status: ToolCallStatus.Pending,
title: 'No Status Indicator',
suffix: 'Hidden',
showStatusIndicator: false,
body: 'The status indicator should be hidden.',
},
{
status: ToolCallStatus.Pending,
title: 'Fixed Width Card',
suffix: undefined,
showStatusIndicator: true,
width: 40,
body: 'This card has a fixed width of 40 characters.',
},
] as const)(
"renders '$title' card with status=$status and showStatusIndicator=$showStatusIndicator",
({ status, title, suffix, showStatusIndicator, body, width }) => {
const { lastFrame } = renderWithProviders(
<Card
status={status}
title={title}
suffix={suffix}
showStatusIndicator={showStatusIndicator}
width={width}
>
<Text>{body}</Text>
</Card>,
{
uiState: {
streamingState: StreamingState.Responding,
},
},
);
const output = lastFrame();
expect(output).toMatchSnapshot();
},
);
});
@@ -0,0 +1,126 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { Box, Text } from 'ink';
import { theme } from '../../semantic-colors.js';
import { ToolStatusIndicator } from '../messages/ToolShared.js';
import { ToolCallStatus } from '../../types.js';
import { checkExhaustive } from '@google/gemini-cli-core';
/**
* Props for the Card component.
*/
export interface CardProps {
/** The main title of the card. */
title: string;
/** Optional text to display after the title (e.g., version, status). */
suffix?: string;
/** Optional indicator to display before the title. */
showStatusIndicator?: boolean;
/** The content to be displayed inside the card. */
children?: React.ReactNode;
/** The styling and intent of the card. */
status?: ToolCallStatus;
/** The width of the card. Defaults to 100%. */
width?: string | number;
}
export const Card: React.FC<CardProps> = ({
status = ToolCallStatus.Pending,
title,
showStatusIndicator = true,
suffix,
children,
width = '100%',
}) => {
const getColors = () => {
switch (status) {
case ToolCallStatus.Pending:
return { border: theme.border.default, text: theme.text.accent };
case ToolCallStatus.Confirming:
return { border: theme.border.focused, text: theme.text.link };
case ToolCallStatus.Error:
return { border: theme.status.error, text: theme.status.error };
case ToolCallStatus.Success:
return { border: theme.border.default, text: theme.status.success };
case ToolCallStatus.Canceled:
return { border: theme.status.warning, text: theme.status.warning };
case ToolCallStatus.Executing:
return { border: theme.border.default, text: theme.text.accent };
default:
checkExhaustive(status);
return { border: theme.border.default, text: theme.text.primary };
}
};
const colors = getColors();
return (
<Box flexDirection="column" width={width}>
<Box width="100%" flexDirection="row">
{/* Top border section */}
<Box
borderStyle="round"
borderBottom={false}
borderRight={false}
paddingLeft={0}
borderColor={colors.border}
/>
{/* Label */}
<Box flexGrow={1}>
<Box
paddingX={1}
flexDirection="row"
gap={1}
justifyContent="flex-start"
>
<Box marginRight={showStatusIndicator ? -2 : -1}>
{showStatusIndicator && (
<ToolStatusIndicator status={status} name={title} />
)}
</Box>
<Text bold color={colors.text}>
{title}
</Text>
{suffix && (
<Text color={colors.text} dimColor wrap="truncate-end">
{suffix}
</Text>
)}
</Box>
{/* Top border after text */}
<Box
borderStyle="single"
flexGrow={1}
borderBottom={false}
borderLeft={false}
borderRight={false}
borderColor={colors.border}
/>
</Box>
{/* Right border */}
<Box
borderStyle="round"
borderBottom={false}
borderLeft={false}
paddingRight={1}
borderColor={colors.border}
></Box>
</Box>
<Box
borderStyle="round"
borderTop={false}
flexDirection="column"
paddingX={1}
paddingY={0}
borderColor={colors.border}
>
{children}
</Box>
</Box>
);
};
@@ -0,0 +1,51 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Card > renders ''Delegate to agent'' card with status='Canceled' and showStatusIndicator=true 1`] = `
"╭ - Delegate to agent Delegating to agent 'cli_help' ──────────────────────────────────────────────────────────────────╮
│ 🤖💭 Execution limit reached (ERROR_NO_COMPLETE_TASK_CALL). Attempting one final recovery turn with a grace period. │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`Card > renders ''Error'' card with status='Error' and showStatusIndicator=true 1`] = `
"╭ x Error 429 You exceeded your current quota ─────────────────────────────────────────────────────────────────────────╮
│ Go to https://aistudio.google.com/apikey to upgrade your quota tier, or submit a quota increase request in │
│ https://ai.google.dev/gemini-api/docs/rate-limits │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`Card > renders ''Fixed Width Card'' card with status='Pending' and showStatusIndicator=true 1`] = `
"╭ o Fixed Width Card ──────────────────╮
│ This card has a fixed width of 40 │
│ characters. │
╰──────────────────────────────────────╯"
`;
exports[`Card > renders ''Gemini CLI update available'' card with status='Pending' and showStatusIndicator=true 1`] = `
"╭ o Gemini CLI update available 0.26.0 → 0.27.0 ───────────────────────────────────────────────────────────────────────╮
│ Installed via Homebrew. Please update with "brew upgrade gemini-cli". │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`Card > renders ''No Status Indicator'' card with status='Pending' and showStatusIndicator=false 1`] = `
"╭ No Status Indicator Hidden ──────────────────────────────────────────────────────────────────────────────────────────╮
│ The status indicator should be hidden. │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`Card > renders ''ReadFolder'' card with status='Success' and showStatusIndicator=true 1`] = `
"╭ ✓ ReadFolder /usr/local/bin ─────────────────────────────────────────────────────────────────────────────────────────╮
│ Listed 39 item(s). │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`Card > renders ''Searching'' card with status='Executing' and showStatusIndicator=true 1`] = `
"╭ ⊶ Searching ripgrep ─────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Searching for pattern in directory... │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`Card > renders ''Shell'' card with status='Confirming' and showStatusIndicator=true 1`] = `
"╭ ? Shell node -v && which gemini ─────────────────────────────────────────────────────────────────────────────────────╮
│ ls /usr/local/bin | grep 'xattr' │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;
+2
View File
@@ -24,6 +24,8 @@ export const TOOL_STATUS = {
CONFIRMING: '?',
CANCELED: '-',
ERROR: 'x',
WARNING: '⚠',
INFORMATION: '',
} as const;
// Maximum number of MCP resources to display per server before truncating