mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-08-02 21:21:09 -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: '?',
|
CONFIRMING: '?',
|
||||||
CANCELED: '-',
|
CANCELED: '-',
|
||||||
ERROR: 'x',
|
ERROR: 'x',
|
||||||
|
WARNING: '⚠',
|
||||||
|
INFORMATION: 'ℹ',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// Maximum number of MCP resources to display per server before truncating
|
// Maximum number of MCP resources to display per server before truncating
|
||||||
|
|||||||
Reference in New Issue
Block a user