diff --git a/docs/cli/themes.md b/docs/cli/themes.md index ee708d29ed..14697be5da 100644 --- a/docs/cli/themes.md +++ b/docs/cli/themes.md @@ -88,6 +88,11 @@ color keys. For example: - `DiffRemoved` (optional, for removed 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:** - `name` (must match the key in the `customThemes` object and be a string) diff --git a/packages/cli/src/ui/components/ShellConfirmationDialog.tsx b/packages/cli/src/ui/components/ShellConfirmationDialog.tsx index 18613d888f..fb3ebace6e 100644 --- a/packages/cli/src/ui/components/ShellConfirmationDialog.tsx +++ b/packages/cli/src/ui/components/ShellConfirmationDialog.tsx @@ -93,7 +93,7 @@ export const ShellConfirmationDialog: React.FC< > {commands.map((cmd) => ( - + ))} diff --git a/packages/cli/src/ui/components/messages/InfoMessage.tsx b/packages/cli/src/ui/components/messages/InfoMessage.tsx index e8d09d637e..b8da1a4e20 100644 --- a/packages/cli/src/ui/components/messages/InfoMessage.tsx +++ b/packages/cli/src/ui/components/messages/InfoMessage.tsx @@ -23,8 +23,8 @@ export const InfoMessage: React.FC = ({ text }) => { {prefix} - - + + diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx index 612ba40076..61880deb67 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx @@ -249,9 +249,7 @@ export const ToolConfirmationMessage: React.FC< bodyContent = ( - - - + {displayUrls && infoProps.urls && infoProps.urls.length > 0 && ( URLs to fetch: diff --git a/packages/cli/src/ui/components/messages/WarningMessage.tsx b/packages/cli/src/ui/components/messages/WarningMessage.tsx index adc86b6f12..c255bc1a47 100644 --- a/packages/cli/src/ui/components/messages/WarningMessage.tsx +++ b/packages/cli/src/ui/components/messages/WarningMessage.tsx @@ -6,7 +6,7 @@ import type React from 'react'; import { Box, Text } from 'ink'; -import { Colors } from '../../colors.js'; +import { theme } from '../../semantic-colors.js'; import { RenderInline } from '../../utils/InlineMarkdownRenderer.js'; interface WarningMessageProps { @@ -20,11 +20,11 @@ export const WarningMessage: React.FC = ({ text }) => { return ( - {prefix} + {prefix} - - + + diff --git a/packages/cli/src/ui/themes/no-color.ts b/packages/cli/src/ui/themes/no-color.ts index 9accca8851..7c22e68b9a 100644 --- a/packages/cli/src/ui/themes/no-color.ts +++ b/packages/cli/src/ui/themes/no-color.ts @@ -32,6 +32,7 @@ const noColorSemanticColors: SemanticColors = { secondary: '', link: '', accent: '', + response: '', }, background: { primary: '', diff --git a/packages/cli/src/ui/themes/semantic-tokens.ts b/packages/cli/src/ui/themes/semantic-tokens.ts index 75059d2e0c..794ce745b6 100644 --- a/packages/cli/src/ui/themes/semantic-tokens.ts +++ b/packages/cli/src/ui/themes/semantic-tokens.ts @@ -12,6 +12,7 @@ export interface SemanticColors { secondary: string; link: string; accent: string; + response: string; }; background: { primary: string; @@ -43,6 +44,7 @@ export const lightSemanticColors: SemanticColors = { secondary: lightTheme.Gray, link: lightTheme.AccentBlue, accent: lightTheme.AccentPurple, + response: lightTheme.Foreground, }, background: { primary: lightTheme.Background, @@ -74,6 +76,7 @@ export const darkSemanticColors: SemanticColors = { secondary: darkTheme.Gray, link: darkTheme.AccentBlue, accent: darkTheme.AccentPurple, + response: darkTheme.Foreground, }, background: { primary: darkTheme.Background, @@ -105,6 +108,7 @@ export const ansiSemanticColors: SemanticColors = { secondary: ansiTheme.Gray, link: ansiTheme.AccentBlue, accent: ansiTheme.AccentPurple, + response: ansiTheme.Foreground, }, background: { primary: ansiTheme.Background, diff --git a/packages/cli/src/ui/themes/theme.ts b/packages/cli/src/ui/themes/theme.ts index 5dc5cfaae4..e2762773dc 100644 --- a/packages/cli/src/ui/themes/theme.ts +++ b/packages/cli/src/ui/themes/theme.ts @@ -38,6 +38,7 @@ export interface CustomTheme { secondary?: string; link?: string; accent?: string; + response?: string; }; background?: { primary?: string; @@ -166,6 +167,7 @@ export class Theme { secondary: this.colors.Gray, link: this.colors.AccentBlue, accent: this.colors.AccentPurple, + response: this.colors.Foreground, }, background: { primary: this.colors.Background, @@ -427,6 +429,10 @@ export function createCustomTheme(customTheme: CustomTheme): Theme { secondary: customTheme.text?.secondary ?? colors.Gray, link: customTheme.text?.link ?? colors.AccentBlue, accent: customTheme.text?.accent ?? colors.AccentPurple, + response: + customTheme.text?.response ?? + customTheme.text?.primary ?? + colors.Foreground, }, background: { primary: customTheme.background?.primary ?? colors.Background, diff --git a/packages/cli/src/ui/utils/InlineMarkdownRenderer.tsx b/packages/cli/src/ui/utils/InlineMarkdownRenderer.tsx index 4320c51967..1b35c62bcc 100644 --- a/packages/cli/src/ui/utils/InlineMarkdownRenderer.tsx +++ b/packages/cli/src/ui/utils/InlineMarkdownRenderer.tsx @@ -19,12 +19,17 @@ const UNDERLINE_TAG_END_LENGTH = 4; // For "" interface RenderInlineProps { text: string; + defaultColor?: string; } -const RenderInlineInternal: React.FC = ({ text }) => { +const RenderInlineInternal: React.FC = ({ + text, + defaultColor, +}) => { + const baseColor = defaultColor ?? theme.text.primary; // Early return for plain text without markdown or URLs if (!/[*_~`<[https?:]/.test(text)) { - return {text}; + return {text}; } const nodes: React.ReactNode[] = []; @@ -36,7 +41,7 @@ const RenderInlineInternal: React.FC = ({ text }) => { while ((match = inlineRegex.exec(text)) !== null) { if (match.index > lastIndex) { nodes.push( - + {text.slice(lastIndex, match.index)} , ); @@ -53,7 +58,7 @@ const RenderInlineInternal: React.FC = ({ text }) => { fullMatch.length > BOLD_MARKER_LENGTH * 2 ) { renderedNode = ( - + {fullMatch.slice(BOLD_MARKER_LENGTH, -BOLD_MARKER_LENGTH)} ); @@ -71,7 +76,7 @@ const RenderInlineInternal: React.FC = ({ text }) => { ) ) { renderedNode = ( - + {fullMatch.slice(ITALIC_MARKER_LENGTH, -ITALIC_MARKER_LENGTH)} ); @@ -81,7 +86,7 @@ const RenderInlineInternal: React.FC = ({ text }) => { fullMatch.length > STRIKETHROUGH_MARKER_LENGTH * 2 ) { renderedNode = ( - + {fullMatch.slice( STRIKETHROUGH_MARKER_LENGTH, -STRIKETHROUGH_MARKER_LENGTH, @@ -111,7 +116,7 @@ const RenderInlineInternal: React.FC = ({ text }) => { const linkText = linkMatch[1]; const url = linkMatch[2]; renderedNode = ( - + {linkText} ({url}) @@ -124,7 +129,7 @@ const RenderInlineInternal: React.FC = ({ text }) => { UNDERLINE_TAG_START_LENGTH + UNDERLINE_TAG_END_LENGTH - 1 // -1 because length is compared to combined length of start and end tags ) { renderedNode = ( - + {fullMatch.slice( UNDERLINE_TAG_START_LENGTH, -UNDERLINE_TAG_END_LENGTH, @@ -143,12 +148,22 @@ const RenderInlineInternal: React.FC = ({ text }) => { renderedNode = null; } - nodes.push(renderedNode ?? {fullMatch}); + nodes.push( + renderedNode ?? ( + + {fullMatch} + + ), + ); lastIndex = inlineRegex.lastIndex; } if (lastIndex < text.length) { - nodes.push({text.slice(lastIndex)}); + nodes.push( + + {text.slice(lastIndex)} + , + ); } return <>{nodes.filter((node) => node !== null)}; diff --git a/packages/cli/src/ui/utils/MarkdownDisplay.tsx b/packages/cli/src/ui/utils/MarkdownDisplay.tsx index 7a95157521..6d7197ea89 100644 --- a/packages/cli/src/ui/utils/MarkdownDisplay.tsx +++ b/packages/cli/src/ui/utils/MarkdownDisplay.tsx @@ -35,6 +35,7 @@ const MarkdownDisplayInternal: React.FC = ({ renderMarkdown = true, }) => { const settings = useSettings(); + const responseColor = theme.text.response ?? theme.text.primary; if (!text) return <>; @@ -138,8 +139,8 @@ const MarkdownDisplayInternal: React.FC = ({ // Not a table, treat as regular text addContentBlock( - - + + , ); @@ -177,8 +178,8 @@ const MarkdownDisplayInternal: React.FC = ({ if (line.trim().length > 0) { addContentBlock( - - + + , ); @@ -197,35 +198,38 @@ const MarkdownDisplayInternal: React.FC = ({ case 1: headerNode = ( - + ); break; case 2: headerNode = ( - + ); break; case 3: headerNode = ( - - + + ); break; case 4: headerNode = ( - + ); break; default: headerNode = ( - - + + ); break; @@ -268,8 +272,8 @@ const MarkdownDisplayInternal: React.FC = ({ } else { addContentBlock( - - + + , ); @@ -401,6 +405,7 @@ const RenderListItemInternal: React.FC = ({ const prefix = type === 'ol' ? `${marker}. ` : `${marker} `; const prefixWidth = prefix.length; const indentation = leadingWhitespace.length; + const listResponseColor = theme.text.response ?? theme.text.primary; return ( = ({ flexDirection="row" > - {prefix} + {prefix} - - + +