mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-23 16:20:57 -07:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3d11e09e1 | |||
| 9ae8f7f3ac | |||
| 9c91f25d62 | |||
| 85446fdfd3 | |||
| 861a72906d | |||
| 6f20f8a300 | |||
| 544ccdbe1e | |||
| 8d9f45efeb | |||
| 47d51fb4bd | |||
| 6fd45b50f6 | |||
| 4596ced82e | |||
| 1ecfcc963e | |||
| cc78073add | |||
| 89b9c4fe63 | |||
| a1ff30b6fe | |||
| 5dfcab609a | |||
| 254bdbc027 | |||
| d3f328dc5b | |||
| 0a385dbf97 | |||
| 70c9975296 |
@@ -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' │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user