diff --git a/packages/cli/src/ui/components/shared/Card.test.tsx b/packages/cli/src/ui/components/shared/Card.test.tsx new file mode 100644 index 0000000000..579a0f1e94 --- /dev/null +++ b/packages/cli/src/ui/components/shared/Card.test.tsx @@ -0,0 +1,74 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { render } from '../../../test-utils/render.js'; +import { Card } from './Card.js'; +import { Text } from 'ink'; +import { describe, it, expect } from 'vitest'; + +describe('Card', () => { + it.each([ + { + variant: 'warning', + title: 'Gemini CLI update available', + suffix: '0.26.0 → 0.27.0', + prefix: true, + body: 'Installed via Homebrew. Please update with "brew upgrade gemini-cli".', + }, + { + variant: 'information', + title: 'Delegate to agent', + suffix: "Delegating to agent 'cli_help'", + prefix: true, + body: '🤖💭 Execution limit reached (ERROR_NO_COMPLETE_TASK_CALL). Attempting one final recovery turn with a grace period.', + }, + { + variant: 'error', + title: 'Error', + suffix: '429 You exceeded your current quota', + prefix: 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', + }, + { + variant: 'confirmation', + title: 'Shell', + suffix: 'node -v && which gemini', + prefix: true, + body: "ls /usr/local/bin | grep 'xattr'", + }, + { + variant: 'success', + title: 'ReadFolder', + suffix: '/usr/local/bin', + prefix: true, + body: 'Listed 39 item(s).', + }, + ])( + 'renders a $variant card with prefix=$prefix', + ({ variant, title, suffix, prefix, body }) => { + const { lastFrame } = render( + + {body} + , + ); + + const output = lastFrame(); + + // eslint-disable-next-line no-console + console.log(output); + + expect(output).toMatchSnapshot(); + }, + ); +}); + +export type { CardProps }; diff --git a/packages/cli/src/ui/components/shared/Card.tsx b/packages/cli/src/ui/components/shared/Card.tsx new file mode 100644 index 0000000000..6e21079351 --- /dev/null +++ b/packages/cli/src/ui/components/shared/Card.tsx @@ -0,0 +1,142 @@ +/** + * @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 { TOOL_STATUS } from '../../constants.js'; + +/** + * 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 icon or text to display before the title. */ + prefix?: boolean; + /** The content to be displayed inside the card. */ + children?: React.ReactNode; + /** The styling and intent of the card. */ + variant?: 'information' | 'success' | 'warning' | 'error' | 'confirmation'; + width?: number | string; +} + +const CardDisplay: React.FC = ({ + variant = 'information', + title, + prefix = true, + suffix, + children, +}) => { + const getColors = () => { + switch (variant) { + case 'error': + return { border: theme.status.error, text: theme.status.error }; + case 'warning': + return { border: theme.status.warning, text: theme.status.warning }; + case 'success': + return { border: theme.status.success, text: theme.status.success }; + case 'confirmation': + return { border: theme.border.focused, text: theme.text.link }; + default: + return { border: theme.border.default, text: theme.text.primary }; + } + }; + + const getGlyph = () => { + switch (variant) { + case 'error': + return TOOL_STATUS.ERROR; + case 'success': + return TOOL_STATUS.SUCCESS; + case 'warning': + return TOOL_STATUS.WARNING; + case 'confirmation': + return TOOL_STATUS.CONFIRMING; + default: + return TOOL_STATUS.INFORMATION; + } + }; + + const colors = getColors(); + const glyph = getGlyph(); + + return ( + + + {/* Top border section */} + + {/* Label */} + + + {prefix && {glyph}} + + {title} + + {suffix && ( + + {suffix} + + )} + + {/* Top border after text */} + + + {/* Right border */} + + + {/* Content area */} + + {children} + + + ); +}; + +export const Card: React.FC = ({ + title, + prefix, + suffix, + children, + variant, +}) => ( + + {children} + +);