mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-12 12:54:07 -07:00
feat(ui): add response semantic color (#12450)
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: shambhu-hegde <143840542+shambhu-hegde@users.noreply.github.com>
This commit is contained in:
@@ -88,6 +88,11 @@ color keys. For example:
|
|||||||
- `DiffRemoved` (optional, for removed lines in diffs)
|
- `DiffRemoved` (optional, for removed lines in diffs)
|
||||||
- `DiffModified` (optional, for modified lines in diffs)
|
- `DiffModified` (optional, for modified lines in diffs)
|
||||||
|
|
||||||
|
You can also override individual UI text roles by adding a nested `text` object.
|
||||||
|
This object supports the keys `primary`, `secondary`, `link`, `accent`, and
|
||||||
|
`response`. When `text.response` is provided it takes precedence over
|
||||||
|
`text.primary` for rendering model responses in chat.
|
||||||
|
|
||||||
**Required Properties:**
|
**Required Properties:**
|
||||||
|
|
||||||
- `name` (must match the key in the `customThemes` object and be a string)
|
- `name` (must match the key in the `customThemes` object and be a string)
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export const ShellConfirmationDialog: React.FC<
|
|||||||
>
|
>
|
||||||
{commands.map((cmd) => (
|
{commands.map((cmd) => (
|
||||||
<Text key={cmd} color={theme.text.link}>
|
<Text key={cmd} color={theme.text.link}>
|
||||||
<RenderInline text={cmd} />
|
<RenderInline text={cmd} defaultColor={theme.text.link} />
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ export const InfoMessage: React.FC<InfoMessageProps> = ({ text }) => {
|
|||||||
<Text color={theme.status.warning}>{prefix}</Text>
|
<Text color={theme.status.warning}>{prefix}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box flexGrow={1}>
|
<Box flexGrow={1}>
|
||||||
<Text wrap="wrap" color={theme.status.warning}>
|
<Text wrap="wrap">
|
||||||
<RenderInline text={text} />
|
<RenderInline text={text} defaultColor={theme.status.warning} />
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -249,9 +249,7 @@ export const ToolConfirmationMessage: React.FC<
|
|||||||
|
|
||||||
bodyContent = (
|
bodyContent = (
|
||||||
<Box flexDirection="column" paddingX={1} marginLeft={1}>
|
<Box flexDirection="column" paddingX={1} marginLeft={1}>
|
||||||
<Text color={theme.text.link}>
|
<RenderInline text={infoProps.prompt} defaultColor={theme.text.link} />
|
||||||
<RenderInline text={infoProps.prompt} />
|
|
||||||
</Text>
|
|
||||||
{displayUrls && infoProps.urls && infoProps.urls.length > 0 && (
|
{displayUrls && infoProps.urls && infoProps.urls.length > 0 && (
|
||||||
<Box flexDirection="column" marginTop={1}>
|
<Box flexDirection="column" marginTop={1}>
|
||||||
<Text color={theme.text.primary}>URLs to fetch:</Text>
|
<Text color={theme.text.primary}>URLs to fetch:</Text>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { Colors } from '../../colors.js';
|
import { theme } from '../../semantic-colors.js';
|
||||||
import { RenderInline } from '../../utils/InlineMarkdownRenderer.js';
|
import { RenderInline } from '../../utils/InlineMarkdownRenderer.js';
|
||||||
|
|
||||||
interface WarningMessageProps {
|
interface WarningMessageProps {
|
||||||
@@ -20,11 +20,11 @@ export const WarningMessage: React.FC<WarningMessageProps> = ({ text }) => {
|
|||||||
return (
|
return (
|
||||||
<Box flexDirection="row" marginTop={1}>
|
<Box flexDirection="row" marginTop={1}>
|
||||||
<Box width={prefixWidth}>
|
<Box width={prefixWidth}>
|
||||||
<Text color={Colors.AccentYellow}>{prefix}</Text>
|
<Text color={theme.status.warning}>{prefix}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box flexGrow={1}>
|
<Box flexGrow={1}>
|
||||||
<Text wrap="wrap" color={Colors.AccentYellow}>
|
<Text wrap="wrap">
|
||||||
<RenderInline text={text} />
|
<RenderInline text={text} defaultColor={theme.status.warning} />
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const noColorSemanticColors: SemanticColors = {
|
|||||||
secondary: '',
|
secondary: '',
|
||||||
link: '',
|
link: '',
|
||||||
accent: '',
|
accent: '',
|
||||||
|
response: '',
|
||||||
},
|
},
|
||||||
background: {
|
background: {
|
||||||
primary: '',
|
primary: '',
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface SemanticColors {
|
|||||||
secondary: string;
|
secondary: string;
|
||||||
link: string;
|
link: string;
|
||||||
accent: string;
|
accent: string;
|
||||||
|
response: string;
|
||||||
};
|
};
|
||||||
background: {
|
background: {
|
||||||
primary: string;
|
primary: string;
|
||||||
@@ -43,6 +44,7 @@ export const lightSemanticColors: SemanticColors = {
|
|||||||
secondary: lightTheme.Gray,
|
secondary: lightTheme.Gray,
|
||||||
link: lightTheme.AccentBlue,
|
link: lightTheme.AccentBlue,
|
||||||
accent: lightTheme.AccentPurple,
|
accent: lightTheme.AccentPurple,
|
||||||
|
response: lightTheme.Foreground,
|
||||||
},
|
},
|
||||||
background: {
|
background: {
|
||||||
primary: lightTheme.Background,
|
primary: lightTheme.Background,
|
||||||
@@ -74,6 +76,7 @@ export const darkSemanticColors: SemanticColors = {
|
|||||||
secondary: darkTheme.Gray,
|
secondary: darkTheme.Gray,
|
||||||
link: darkTheme.AccentBlue,
|
link: darkTheme.AccentBlue,
|
||||||
accent: darkTheme.AccentPurple,
|
accent: darkTheme.AccentPurple,
|
||||||
|
response: darkTheme.Foreground,
|
||||||
},
|
},
|
||||||
background: {
|
background: {
|
||||||
primary: darkTheme.Background,
|
primary: darkTheme.Background,
|
||||||
@@ -105,6 +108,7 @@ export const ansiSemanticColors: SemanticColors = {
|
|||||||
secondary: ansiTheme.Gray,
|
secondary: ansiTheme.Gray,
|
||||||
link: ansiTheme.AccentBlue,
|
link: ansiTheme.AccentBlue,
|
||||||
accent: ansiTheme.AccentPurple,
|
accent: ansiTheme.AccentPurple,
|
||||||
|
response: ansiTheme.Foreground,
|
||||||
},
|
},
|
||||||
background: {
|
background: {
|
||||||
primary: ansiTheme.Background,
|
primary: ansiTheme.Background,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface CustomTheme {
|
|||||||
secondary?: string;
|
secondary?: string;
|
||||||
link?: string;
|
link?: string;
|
||||||
accent?: string;
|
accent?: string;
|
||||||
|
response?: string;
|
||||||
};
|
};
|
||||||
background?: {
|
background?: {
|
||||||
primary?: string;
|
primary?: string;
|
||||||
@@ -166,6 +167,7 @@ export class Theme {
|
|||||||
secondary: this.colors.Gray,
|
secondary: this.colors.Gray,
|
||||||
link: this.colors.AccentBlue,
|
link: this.colors.AccentBlue,
|
||||||
accent: this.colors.AccentPurple,
|
accent: this.colors.AccentPurple,
|
||||||
|
response: this.colors.Foreground,
|
||||||
},
|
},
|
||||||
background: {
|
background: {
|
||||||
primary: this.colors.Background,
|
primary: this.colors.Background,
|
||||||
@@ -427,6 +429,10 @@ export function createCustomTheme(customTheme: CustomTheme): Theme {
|
|||||||
secondary: customTheme.text?.secondary ?? colors.Gray,
|
secondary: customTheme.text?.secondary ?? colors.Gray,
|
||||||
link: customTheme.text?.link ?? colors.AccentBlue,
|
link: customTheme.text?.link ?? colors.AccentBlue,
|
||||||
accent: customTheme.text?.accent ?? colors.AccentPurple,
|
accent: customTheme.text?.accent ?? colors.AccentPurple,
|
||||||
|
response:
|
||||||
|
customTheme.text?.response ??
|
||||||
|
customTheme.text?.primary ??
|
||||||
|
colors.Foreground,
|
||||||
},
|
},
|
||||||
background: {
|
background: {
|
||||||
primary: customTheme.background?.primary ?? colors.Background,
|
primary: customTheme.background?.primary ?? colors.Background,
|
||||||
|
|||||||
@@ -19,12 +19,17 @@ const UNDERLINE_TAG_END_LENGTH = 4; // For "</u>"
|
|||||||
|
|
||||||
interface RenderInlineProps {
|
interface RenderInlineProps {
|
||||||
text: string;
|
text: string;
|
||||||
|
defaultColor?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RenderInlineInternal: React.FC<RenderInlineProps> = ({ text }) => {
|
const RenderInlineInternal: React.FC<RenderInlineProps> = ({
|
||||||
|
text,
|
||||||
|
defaultColor,
|
||||||
|
}) => {
|
||||||
|
const baseColor = defaultColor ?? theme.text.primary;
|
||||||
// Early return for plain text without markdown or URLs
|
// Early return for plain text without markdown or URLs
|
||||||
if (!/[*_~`<[https?:]/.test(text)) {
|
if (!/[*_~`<[https?:]/.test(text)) {
|
||||||
return <Text color={theme.text.primary}>{text}</Text>;
|
return <Text color={baseColor}>{text}</Text>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodes: React.ReactNode[] = [];
|
const nodes: React.ReactNode[] = [];
|
||||||
@@ -36,7 +41,7 @@ const RenderInlineInternal: React.FC<RenderInlineProps> = ({ text }) => {
|
|||||||
while ((match = inlineRegex.exec(text)) !== null) {
|
while ((match = inlineRegex.exec(text)) !== null) {
|
||||||
if (match.index > lastIndex) {
|
if (match.index > lastIndex) {
|
||||||
nodes.push(
|
nodes.push(
|
||||||
<Text key={`t-${lastIndex}`}>
|
<Text key={`t-${lastIndex}`} color={baseColor}>
|
||||||
{text.slice(lastIndex, match.index)}
|
{text.slice(lastIndex, match.index)}
|
||||||
</Text>,
|
</Text>,
|
||||||
);
|
);
|
||||||
@@ -53,7 +58,7 @@ const RenderInlineInternal: React.FC<RenderInlineProps> = ({ text }) => {
|
|||||||
fullMatch.length > BOLD_MARKER_LENGTH * 2
|
fullMatch.length > BOLD_MARKER_LENGTH * 2
|
||||||
) {
|
) {
|
||||||
renderedNode = (
|
renderedNode = (
|
||||||
<Text key={key} bold>
|
<Text key={key} bold color={baseColor}>
|
||||||
{fullMatch.slice(BOLD_MARKER_LENGTH, -BOLD_MARKER_LENGTH)}
|
{fullMatch.slice(BOLD_MARKER_LENGTH, -BOLD_MARKER_LENGTH)}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
@@ -71,7 +76,7 @@ const RenderInlineInternal: React.FC<RenderInlineProps> = ({ text }) => {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
renderedNode = (
|
renderedNode = (
|
||||||
<Text key={key} italic>
|
<Text key={key} italic color={baseColor}>
|
||||||
{fullMatch.slice(ITALIC_MARKER_LENGTH, -ITALIC_MARKER_LENGTH)}
|
{fullMatch.slice(ITALIC_MARKER_LENGTH, -ITALIC_MARKER_LENGTH)}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
@@ -81,7 +86,7 @@ const RenderInlineInternal: React.FC<RenderInlineProps> = ({ text }) => {
|
|||||||
fullMatch.length > STRIKETHROUGH_MARKER_LENGTH * 2
|
fullMatch.length > STRIKETHROUGH_MARKER_LENGTH * 2
|
||||||
) {
|
) {
|
||||||
renderedNode = (
|
renderedNode = (
|
||||||
<Text key={key} strikethrough>
|
<Text key={key} strikethrough color={baseColor}>
|
||||||
{fullMatch.slice(
|
{fullMatch.slice(
|
||||||
STRIKETHROUGH_MARKER_LENGTH,
|
STRIKETHROUGH_MARKER_LENGTH,
|
||||||
-STRIKETHROUGH_MARKER_LENGTH,
|
-STRIKETHROUGH_MARKER_LENGTH,
|
||||||
@@ -111,7 +116,7 @@ const RenderInlineInternal: React.FC<RenderInlineProps> = ({ text }) => {
|
|||||||
const linkText = linkMatch[1];
|
const linkText = linkMatch[1];
|
||||||
const url = linkMatch[2];
|
const url = linkMatch[2];
|
||||||
renderedNode = (
|
renderedNode = (
|
||||||
<Text key={key}>
|
<Text key={key} color={baseColor}>
|
||||||
{linkText}
|
{linkText}
|
||||||
<Text color={theme.text.link}> ({url})</Text>
|
<Text color={theme.text.link}> ({url})</Text>
|
||||||
</Text>
|
</Text>
|
||||||
@@ -124,7 +129,7 @@ const RenderInlineInternal: React.FC<RenderInlineProps> = ({ text }) => {
|
|||||||
UNDERLINE_TAG_START_LENGTH + UNDERLINE_TAG_END_LENGTH - 1 // -1 because length is compared to combined length of start and end tags
|
UNDERLINE_TAG_START_LENGTH + UNDERLINE_TAG_END_LENGTH - 1 // -1 because length is compared to combined length of start and end tags
|
||||||
) {
|
) {
|
||||||
renderedNode = (
|
renderedNode = (
|
||||||
<Text key={key} underline>
|
<Text key={key} underline color={baseColor}>
|
||||||
{fullMatch.slice(
|
{fullMatch.slice(
|
||||||
UNDERLINE_TAG_START_LENGTH,
|
UNDERLINE_TAG_START_LENGTH,
|
||||||
-UNDERLINE_TAG_END_LENGTH,
|
-UNDERLINE_TAG_END_LENGTH,
|
||||||
@@ -143,12 +148,22 @@ const RenderInlineInternal: React.FC<RenderInlineProps> = ({ text }) => {
|
|||||||
renderedNode = null;
|
renderedNode = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
nodes.push(renderedNode ?? <Text key={key}>{fullMatch}</Text>);
|
nodes.push(
|
||||||
|
renderedNode ?? (
|
||||||
|
<Text key={key} color={baseColor}>
|
||||||
|
{fullMatch}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
);
|
||||||
lastIndex = inlineRegex.lastIndex;
|
lastIndex = inlineRegex.lastIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastIndex < text.length) {
|
if (lastIndex < text.length) {
|
||||||
nodes.push(<Text key={`t-${lastIndex}`}>{text.slice(lastIndex)}</Text>);
|
nodes.push(
|
||||||
|
<Text key={`t-${lastIndex}`} color={baseColor}>
|
||||||
|
{text.slice(lastIndex)}
|
||||||
|
</Text>,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{nodes.filter((node) => node !== null)}</>;
|
return <>{nodes.filter((node) => node !== null)}</>;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
|
|||||||
renderMarkdown = true,
|
renderMarkdown = true,
|
||||||
}) => {
|
}) => {
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
|
const responseColor = theme.text.response ?? theme.text.primary;
|
||||||
|
|
||||||
if (!text) return <></>;
|
if (!text) return <></>;
|
||||||
|
|
||||||
@@ -138,8 +139,8 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
|
|||||||
// Not a table, treat as regular text
|
// Not a table, treat as regular text
|
||||||
addContentBlock(
|
addContentBlock(
|
||||||
<Box key={key}>
|
<Box key={key}>
|
||||||
<Text wrap="wrap">
|
<Text wrap="wrap" color={responseColor}>
|
||||||
<RenderInline text={line} />
|
<RenderInline text={line} defaultColor={responseColor} />
|
||||||
</Text>
|
</Text>
|
||||||
</Box>,
|
</Box>,
|
||||||
);
|
);
|
||||||
@@ -177,8 +178,8 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
|
|||||||
if (line.trim().length > 0) {
|
if (line.trim().length > 0) {
|
||||||
addContentBlock(
|
addContentBlock(
|
||||||
<Box key={key}>
|
<Box key={key}>
|
||||||
<Text wrap="wrap">
|
<Text wrap="wrap" color={responseColor}>
|
||||||
<RenderInline text={line} />
|
<RenderInline text={line} defaultColor={responseColor} />
|
||||||
</Text>
|
</Text>
|
||||||
</Box>,
|
</Box>,
|
||||||
);
|
);
|
||||||
@@ -197,35 +198,38 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
|
|||||||
case 1:
|
case 1:
|
||||||
headerNode = (
|
headerNode = (
|
||||||
<Text bold color={theme.text.link}>
|
<Text bold color={theme.text.link}>
|
||||||
<RenderInline text={headerText} />
|
<RenderInline text={headerText} defaultColor={theme.text.link} />
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
headerNode = (
|
headerNode = (
|
||||||
<Text bold color={theme.text.link}>
|
<Text bold color={theme.text.link}>
|
||||||
<RenderInline text={headerText} />
|
<RenderInline text={headerText} defaultColor={theme.text.link} />
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
headerNode = (
|
headerNode = (
|
||||||
<Text bold color={theme.text.primary}>
|
<Text bold color={responseColor}>
|
||||||
<RenderInline text={headerText} />
|
<RenderInline text={headerText} defaultColor={responseColor} />
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
headerNode = (
|
headerNode = (
|
||||||
<Text italic color={theme.text.secondary}>
|
<Text italic color={theme.text.secondary}>
|
||||||
<RenderInline text={headerText} />
|
<RenderInline
|
||||||
|
text={headerText}
|
||||||
|
defaultColor={theme.text.secondary}
|
||||||
|
/>
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
headerNode = (
|
headerNode = (
|
||||||
<Text color={theme.text.primary}>
|
<Text color={responseColor}>
|
||||||
<RenderInline text={headerText} />
|
<RenderInline text={headerText} defaultColor={responseColor} />
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@@ -268,8 +272,8 @@ const MarkdownDisplayInternal: React.FC<MarkdownDisplayProps> = ({
|
|||||||
} else {
|
} else {
|
||||||
addContentBlock(
|
addContentBlock(
|
||||||
<Box key={key}>
|
<Box key={key}>
|
||||||
<Text wrap="wrap" color={theme.text.primary}>
|
<Text wrap="wrap" color={responseColor}>
|
||||||
<RenderInline text={line} />
|
<RenderInline text={line} defaultColor={responseColor} />
|
||||||
</Text>
|
</Text>
|
||||||
</Box>,
|
</Box>,
|
||||||
);
|
);
|
||||||
@@ -401,6 +405,7 @@ const RenderListItemInternal: React.FC<RenderListItemProps> = ({
|
|||||||
const prefix = type === 'ol' ? `${marker}. ` : `${marker} `;
|
const prefix = type === 'ol' ? `${marker}. ` : `${marker} `;
|
||||||
const prefixWidth = prefix.length;
|
const prefixWidth = prefix.length;
|
||||||
const indentation = leadingWhitespace.length;
|
const indentation = leadingWhitespace.length;
|
||||||
|
const listResponseColor = theme.text.response ?? theme.text.primary;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -408,11 +413,11 @@ const RenderListItemInternal: React.FC<RenderListItemProps> = ({
|
|||||||
flexDirection="row"
|
flexDirection="row"
|
||||||
>
|
>
|
||||||
<Box width={prefixWidth}>
|
<Box width={prefixWidth}>
|
||||||
<Text color={theme.text.primary}>{prefix}</Text>
|
<Text color={listResponseColor}>{prefix}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box flexGrow={LIST_ITEM_TEXT_FLEX_GROW}>
|
<Box flexGrow={LIST_ITEM_TEXT_FLEX_GROW}>
|
||||||
<Text wrap="wrap" color={theme.text.primary}>
|
<Text wrap="wrap" color={listResponseColor}>
|
||||||
<RenderInline text={itemText} />
|
<RenderInline text={itemText} defaultColor={listResponseColor} />
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user