feat: add highlighting to user prompt messages (#714)
This commit is contained in:
@@ -5,6 +5,7 @@ import { colors } from "./colors.js";
|
||||
interface InlineMarkdownProps {
|
||||
text: string;
|
||||
dimColor?: boolean;
|
||||
backgroundColor?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -15,6 +16,7 @@ interface InlineMarkdownProps {
|
||||
export const InlineMarkdown: React.FC<InlineMarkdownProps> = ({
|
||||
text,
|
||||
dimColor,
|
||||
backgroundColor,
|
||||
}) => {
|
||||
// Early return for plain text without markdown (treat underscores as plain text)
|
||||
if (!/[*~`[]/.test(text)) {
|
||||
@@ -47,7 +49,12 @@ export const InlineMarkdown: React.FC<InlineMarkdownProps> = ({
|
||||
) {
|
||||
// Bold
|
||||
nodes.push(
|
||||
<Text key={key} bold dimColor={dimColor}>
|
||||
<Text
|
||||
key={key}
|
||||
bold
|
||||
dimColor={dimColor}
|
||||
backgroundColor={backgroundColor}
|
||||
>
|
||||
{fullMatch.slice(2, -2)}
|
||||
</Text>,
|
||||
);
|
||||
@@ -58,7 +65,12 @@ export const InlineMarkdown: React.FC<InlineMarkdownProps> = ({
|
||||
) {
|
||||
// Italic
|
||||
nodes.push(
|
||||
<Text key={key} italic dimColor={dimColor}>
|
||||
<Text
|
||||
key={key}
|
||||
italic
|
||||
dimColor={dimColor}
|
||||
backgroundColor={backgroundColor}
|
||||
>
|
||||
{fullMatch.slice(1, -1)}
|
||||
</Text>,
|
||||
);
|
||||
@@ -69,14 +81,23 @@ export const InlineMarkdown: React.FC<InlineMarkdownProps> = ({
|
||||
) {
|
||||
// Strikethrough
|
||||
nodes.push(
|
||||
<Text key={key} strikethrough dimColor={dimColor}>
|
||||
<Text
|
||||
key={key}
|
||||
strikethrough
|
||||
dimColor={dimColor}
|
||||
backgroundColor={backgroundColor}
|
||||
>
|
||||
{fullMatch.slice(2, -2)}
|
||||
</Text>,
|
||||
);
|
||||
} else if (fullMatch.startsWith("`") && fullMatch.endsWith("`")) {
|
||||
// Inline code
|
||||
nodes.push(
|
||||
<Text key={key} color={colors.link.text}>
|
||||
<Text
|
||||
key={key}
|
||||
color={colors.link.text}
|
||||
backgroundColor={backgroundColor}
|
||||
>
|
||||
{fullMatch.slice(1, -1)}
|
||||
</Text>,
|
||||
);
|
||||
@@ -91,9 +112,12 @@ export const InlineMarkdown: React.FC<InlineMarkdownProps> = ({
|
||||
const linkText = linkMatch[1];
|
||||
const url = linkMatch[2];
|
||||
nodes.push(
|
||||
<Text key={key}>
|
||||
<Text key={key} backgroundColor={backgroundColor}>
|
||||
{linkText}
|
||||
<Text color={colors.link.url}> ({url})</Text>
|
||||
<Text color={colors.link.url} backgroundColor={backgroundColor}>
|
||||
{" "}
|
||||
({url})
|
||||
</Text>
|
||||
</Text>,
|
||||
);
|
||||
} else {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Box, Text, Transform } from "ink";
|
||||
import type React from "react";
|
||||
import { colors } from "./colors.js";
|
||||
import stringWidth from "string-width";
|
||||
import { colors, hexToBgAnsi } from "./colors.js";
|
||||
import { InlineMarkdown } from "./InlineMarkdownRenderer.js";
|
||||
|
||||
interface MarkdownDisplayProps {
|
||||
text: string;
|
||||
dimColor?: boolean;
|
||||
hangingIndent?: number; // indent for wrapped lines within a paragraph
|
||||
backgroundColor?: string; // background color for all text
|
||||
contentWidth?: number; // available width — used to pad lines to fill background
|
||||
}
|
||||
|
||||
// Regex patterns for markdown elements (defined outside component to avoid re-creation)
|
||||
@@ -38,9 +41,23 @@ export const MarkdownDisplay: React.FC<MarkdownDisplayProps> = ({
|
||||
text,
|
||||
dimColor,
|
||||
hangingIndent = 0,
|
||||
backgroundColor,
|
||||
contentWidth,
|
||||
}) => {
|
||||
if (!text) return null;
|
||||
|
||||
// Build ANSI background code and line-padding helper for full-width backgrounds.
|
||||
// Transform callbacks receive already-rendered text (with ANSI codes from child Text
|
||||
// components), so appended spaces need their own ANSI background coloring.
|
||||
const bgAnsi = backgroundColor ? hexToBgAnsi(backgroundColor) : "";
|
||||
const padLine = (ln: string): string => {
|
||||
if (!contentWidth || !backgroundColor) return ln;
|
||||
const visWidth = stringWidth(ln);
|
||||
const pad = Math.max(0, contentWidth - visWidth);
|
||||
if (pad <= 0) return ln;
|
||||
return `${ln}${bgAnsi}${" ".repeat(pad)}\x1b[0m`;
|
||||
};
|
||||
|
||||
const lines = text.split("\n");
|
||||
const contentBlocks: React.ReactNode[] = [];
|
||||
|
||||
@@ -78,26 +95,35 @@ export const MarkdownDisplay: React.FC<MarkdownDisplayProps> = ({
|
||||
<Box key={`table-${startIndex}`} flexDirection="column" marginY={0}>
|
||||
{/* Header row */}
|
||||
<Box flexDirection="row">
|
||||
<Text dimColor={dimColor}>│</Text>
|
||||
<Text dimColor={dimColor} backgroundColor={backgroundColor}>
|
||||
│
|
||||
</Text>
|
||||
{headerRow.map((cell, idx) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: static table content
|
||||
<Box key={`h-${idx}`} flexDirection="row">
|
||||
<Text bold dimColor={dimColor}>
|
||||
<Text bold dimColor={dimColor} backgroundColor={backgroundColor}>
|
||||
{" "}
|
||||
{cell.padEnd(colWidths[idx] ?? 3)}
|
||||
</Text>
|
||||
<Text dimColor={dimColor}> │</Text>
|
||||
<Text dimColor={dimColor} backgroundColor={backgroundColor}>
|
||||
{" "}
|
||||
│
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
{/* Separator */}
|
||||
<Box flexDirection="row">
|
||||
<Text dimColor={dimColor}>├</Text>
|
||||
<Text dimColor={dimColor} backgroundColor={backgroundColor}>
|
||||
├
|
||||
</Text>
|
||||
{colWidths.map((width, idx) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: static table content
|
||||
<Box key={`s-${idx}`} flexDirection="row">
|
||||
<Text dimColor={dimColor}>{"─".repeat(width + 2)}</Text>
|
||||
<Text dimColor={dimColor}>
|
||||
<Text dimColor={dimColor} backgroundColor={backgroundColor}>
|
||||
{"─".repeat(width + 2)}
|
||||
</Text>
|
||||
<Text dimColor={dimColor} backgroundColor={backgroundColor}>
|
||||
{idx < colWidths.length - 1 ? "┼" : "┤"}
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -107,15 +133,20 @@ export const MarkdownDisplay: React.FC<MarkdownDisplayProps> = ({
|
||||
{bodyRows.map((row, rowIdx) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: static table content
|
||||
<Box key={`r-${rowIdx}`} flexDirection="row">
|
||||
<Text dimColor={dimColor}>│</Text>
|
||||
<Text dimColor={dimColor} backgroundColor={backgroundColor}>
|
||||
│
|
||||
</Text>
|
||||
{row.map((cell, colIdx) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: static table content
|
||||
<Box key={`c-${colIdx}`} flexDirection="row">
|
||||
<Text dimColor={dimColor}>
|
||||
<Text dimColor={dimColor} backgroundColor={backgroundColor}>
|
||||
{" "}
|
||||
{(cell || "").padEnd(colWidths[colIdx] || 3)}
|
||||
</Text>
|
||||
<Text dimColor={dimColor}> │</Text>
|
||||
<Text dimColor={dimColor} backgroundColor={backgroundColor}>
|
||||
{" "}
|
||||
│
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
@@ -140,7 +171,10 @@ export const MarkdownDisplay: React.FC<MarkdownDisplayProps> = ({
|
||||
const code = codeBlockContent.join("\n");
|
||||
contentBlocks.push(
|
||||
<Box key={key} paddingLeft={2}>
|
||||
<Text color={colors.code.inline}>{code}</Text>
|
||||
<Text color={colors.code.inline} backgroundColor={backgroundColor}>
|
||||
{code}
|
||||
{backgroundColor ? " " : null}
|
||||
</Text>
|
||||
</Box>,
|
||||
);
|
||||
codeBlockContent = [];
|
||||
@@ -165,8 +199,13 @@ export const MarkdownDisplay: React.FC<MarkdownDisplayProps> = ({
|
||||
|
||||
contentBlocks.push(
|
||||
<Box key={key}>
|
||||
<Text {...style}>
|
||||
<InlineMarkdown text={content} dimColor={dimColor} />
|
||||
<Text {...style} backgroundColor={backgroundColor}>
|
||||
<InlineMarkdown
|
||||
text={content}
|
||||
dimColor={dimColor}
|
||||
backgroundColor={backgroundColor}
|
||||
/>
|
||||
{backgroundColor ? " " : null}
|
||||
</Text>
|
||||
</Box>,
|
||||
);
|
||||
@@ -193,11 +232,22 @@ export const MarkdownDisplay: React.FC<MarkdownDisplayProps> = ({
|
||||
contentBlocks.push(
|
||||
<Box key={key} paddingLeft={indent} flexDirection="row">
|
||||
<Box width={bulletWidth} flexShrink={0}>
|
||||
<Text dimColor={dimColor}>{bullet}</Text>
|
||||
<Text dimColor={dimColor} backgroundColor={backgroundColor}>
|
||||
{bullet}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box flexGrow={1}>
|
||||
<Text wrap="wrap" dimColor={dimColor}>
|
||||
<InlineMarkdown text={content} dimColor={dimColor} />
|
||||
<Text
|
||||
wrap="wrap"
|
||||
dimColor={dimColor}
|
||||
backgroundColor={backgroundColor}
|
||||
>
|
||||
<InlineMarkdown
|
||||
text={content}
|
||||
dimColor={dimColor}
|
||||
backgroundColor={backgroundColor}
|
||||
/>
|
||||
{backgroundColor ? " " : null}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>,
|
||||
@@ -211,9 +261,20 @@ export const MarkdownDisplay: React.FC<MarkdownDisplayProps> = ({
|
||||
if (blockquoteMatch && blockquoteMatch[1] !== undefined) {
|
||||
contentBlocks.push(
|
||||
<Box key={key} paddingLeft={2}>
|
||||
<Text dimColor>│ </Text>
|
||||
<Text wrap="wrap" dimColor={dimColor}>
|
||||
<InlineMarkdown text={blockquoteMatch[1]} dimColor={dimColor} />
|
||||
<Text dimColor backgroundColor={backgroundColor}>
|
||||
│{" "}
|
||||
</Text>
|
||||
<Text
|
||||
wrap="wrap"
|
||||
dimColor={dimColor}
|
||||
backgroundColor={backgroundColor}
|
||||
>
|
||||
<InlineMarkdown
|
||||
text={blockquoteMatch[1]}
|
||||
dimColor={dimColor}
|
||||
backgroundColor={backgroundColor}
|
||||
/>
|
||||
{backgroundColor ? " " : null}
|
||||
</Text>
|
||||
</Box>,
|
||||
);
|
||||
@@ -225,7 +286,9 @@ export const MarkdownDisplay: React.FC<MarkdownDisplayProps> = ({
|
||||
if (line.match(hrRegex)) {
|
||||
contentBlocks.push(
|
||||
<Box key={key}>
|
||||
<Text dimColor>───────────────────────────────</Text>
|
||||
<Text dimColor backgroundColor={backgroundColor}>
|
||||
───────────────────────────────
|
||||
</Text>
|
||||
</Box>,
|
||||
);
|
||||
index++;
|
||||
@@ -261,27 +324,58 @@ export const MarkdownDisplay: React.FC<MarkdownDisplayProps> = ({
|
||||
|
||||
// Empty lines
|
||||
if (line.trim() === "") {
|
||||
contentBlocks.push(<Box key={key} height={1} />);
|
||||
if (backgroundColor) {
|
||||
// Render a visible space so outer Transform can pad this line
|
||||
contentBlocks.push(
|
||||
<Box key={key}>
|
||||
<Text backgroundColor={backgroundColor}> </Text>
|
||||
</Box>,
|
||||
);
|
||||
} else {
|
||||
contentBlocks.push(<Box key={key} height={1} />);
|
||||
}
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regular paragraph text with optional hanging indent for wrapped lines
|
||||
// Regular paragraph text with optional hanging indent and line padding
|
||||
const needsTransform =
|
||||
hangingIndent > 0 || (contentWidth && backgroundColor);
|
||||
contentBlocks.push(
|
||||
<Box key={key}>
|
||||
{hangingIndent > 0 ? (
|
||||
{needsTransform ? (
|
||||
<Transform
|
||||
transform={(ln, i) =>
|
||||
i === 0 ? ln : " ".repeat(hangingIndent) + ln
|
||||
}
|
||||
transform={(ln, i) => {
|
||||
const indented =
|
||||
hangingIndent > 0 && i > 0
|
||||
? " ".repeat(hangingIndent) + ln
|
||||
: ln;
|
||||
return padLine(indented);
|
||||
}}
|
||||
>
|
||||
<Text wrap="wrap" dimColor={dimColor}>
|
||||
<InlineMarkdown text={line} dimColor={dimColor} />
|
||||
<Text
|
||||
wrap="wrap"
|
||||
dimColor={dimColor}
|
||||
backgroundColor={backgroundColor}
|
||||
>
|
||||
<InlineMarkdown
|
||||
text={line}
|
||||
dimColor={dimColor}
|
||||
backgroundColor={backgroundColor}
|
||||
/>
|
||||
</Text>
|
||||
</Transform>
|
||||
) : (
|
||||
<Text wrap="wrap" dimColor={dimColor}>
|
||||
<InlineMarkdown text={line} dimColor={dimColor} />
|
||||
<Text
|
||||
wrap="wrap"
|
||||
dimColor={dimColor}
|
||||
backgroundColor={backgroundColor}
|
||||
>
|
||||
<InlineMarkdown
|
||||
text={line}
|
||||
dimColor={dimColor}
|
||||
backgroundColor={backgroundColor}
|
||||
/>
|
||||
</Text>
|
||||
)}
|
||||
</Box>,
|
||||
@@ -294,7 +388,10 @@ export const MarkdownDisplay: React.FC<MarkdownDisplayProps> = ({
|
||||
const code = codeBlockContent.join("\n");
|
||||
contentBlocks.push(
|
||||
<Box key="unclosed-code" paddingLeft={2}>
|
||||
<Text color={colors.code.inline}>{code}</Text>
|
||||
<Text color={colors.code.inline} backgroundColor={backgroundColor}>
|
||||
{code}
|
||||
{backgroundColor ? " " : null}
|
||||
</Text>
|
||||
</Box>,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Box, Text } from "ink";
|
||||
import { Text } from "ink";
|
||||
import { memo } from "react";
|
||||
import stringWidth from "string-width";
|
||||
import { useTerminalWidth } from "../hooks/useTerminalWidth";
|
||||
import { MarkdownDisplay } from "./MarkdownDisplay.js";
|
||||
import { colors, hexToBgAnsi, hexToFgAnsi } from "./colors";
|
||||
|
||||
type UserLine = {
|
||||
kind: "user";
|
||||
@@ -10,28 +11,181 @@ type UserLine = {
|
||||
};
|
||||
|
||||
/**
|
||||
* UserMessageRich - Rich formatting version with two-column layout
|
||||
* This is a direct port from the old letta-code codebase to preserve the exact styling
|
||||
* Word-wrap plain text to a given visible width.
|
||||
* Returns an array of lines, each at most `width` visible characters wide.
|
||||
*/
|
||||
function wordWrap(text: string, width: number): string[] {
|
||||
if (width <= 0) return [text];
|
||||
const words = text.split(" ");
|
||||
const lines: string[] = [];
|
||||
let current = "";
|
||||
|
||||
for (const word of words) {
|
||||
if (current === "") {
|
||||
current = word;
|
||||
} else {
|
||||
const candidate = `${current} ${word}`;
|
||||
if (stringWidth(candidate) <= width) {
|
||||
current = candidate;
|
||||
} else {
|
||||
lines.push(current);
|
||||
current = word;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (current !== "") {
|
||||
lines.push(current);
|
||||
}
|
||||
return lines.length > 0 ? lines : [""];
|
||||
}
|
||||
|
||||
/** Right-padding (in characters) added after content on compact (single-line) messages. */
|
||||
const COMPACT_PAD = 1;
|
||||
|
||||
/**
|
||||
* Split text into system-reminder blocks and user content blocks.
|
||||
* System-reminder blocks are identified by <system-reminder>...</system-reminder> tags.
|
||||
* Returns array of { text, isSystemReminder } objects in order.
|
||||
*/
|
||||
function splitSystemReminderBlocks(
|
||||
text: string,
|
||||
): Array<{ text: string; isSystemReminder: boolean }> {
|
||||
const blocks: Array<{ text: string; isSystemReminder: boolean }> = [];
|
||||
const tagOpen = "<system-reminder>";
|
||||
const tagClose = "</system-reminder>";
|
||||
|
||||
let remaining = text;
|
||||
|
||||
while (remaining.length > 0) {
|
||||
const openIdx = remaining.indexOf(tagOpen);
|
||||
|
||||
if (openIdx === -1) {
|
||||
// No more system-reminder tags, rest is user content
|
||||
if (remaining.trim()) {
|
||||
blocks.push({ text: remaining.trim(), isSystemReminder: false });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Content before the tag is user content
|
||||
if (openIdx > 0) {
|
||||
const before = remaining.slice(0, openIdx).trim();
|
||||
if (before) {
|
||||
blocks.push({ text: before, isSystemReminder: false });
|
||||
}
|
||||
}
|
||||
|
||||
// Find the closing tag
|
||||
const closeIdx = remaining.indexOf(tagClose, openIdx);
|
||||
if (closeIdx === -1) {
|
||||
// Malformed - no closing tag, treat rest as system-reminder
|
||||
blocks.push({
|
||||
text: remaining.slice(openIdx).trim(),
|
||||
isSystemReminder: true,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// Extract the full system-reminder block (including tags)
|
||||
const sysBlock = remaining.slice(openIdx, closeIdx + tagClose.length);
|
||||
blocks.push({ text: sysBlock, isSystemReminder: true });
|
||||
|
||||
remaining = remaining.slice(closeIdx + tagClose.length);
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a block of text with "> " prefix (first line) and " " continuation.
|
||||
* If highlighted, applies background and foreground colors. Otherwise plain text.
|
||||
*/
|
||||
function renderBlock(
|
||||
text: string,
|
||||
contentWidth: number,
|
||||
columns: number,
|
||||
highlighted: boolean,
|
||||
colorAnsi: string, // combined bg + fg ANSI codes
|
||||
): string[] {
|
||||
const inputLines = text.split("\n");
|
||||
const outputLines: string[] = [];
|
||||
|
||||
for (const inputLine of inputLines) {
|
||||
if (inputLine.trim() === "") {
|
||||
outputLines.push("");
|
||||
continue;
|
||||
}
|
||||
const wrappedLines = wordWrap(inputLine, contentWidth);
|
||||
for (const wl of wrappedLines) {
|
||||
outputLines.push(wl);
|
||||
}
|
||||
}
|
||||
|
||||
if (outputLines.length === 0) return [];
|
||||
|
||||
const isSingleLine = outputLines.length === 1;
|
||||
|
||||
return outputLines.map((ol, i) => {
|
||||
const prefix = i === 0 ? "> " : " ";
|
||||
const content = prefix + ol;
|
||||
|
||||
if (!highlighted) {
|
||||
return content;
|
||||
}
|
||||
|
||||
const visWidth = stringWidth(content);
|
||||
if (isSingleLine) {
|
||||
return `${colorAnsi}${content}${" ".repeat(COMPACT_PAD)}\x1b[0m`;
|
||||
}
|
||||
const pad = Math.max(0, columns - visWidth);
|
||||
return `${colorAnsi}${content}${" ".repeat(pad)}\x1b[0m`;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* UserMessageRich - Rich formatting for user messages with background highlight
|
||||
*
|
||||
* Features:
|
||||
* - Left column (2 chars wide) with "> " prompt indicator
|
||||
* - Right column with wrapped text content
|
||||
* - Full markdown rendering support
|
||||
* Renders user messages as pre-formatted text with ANSI background codes:
|
||||
* - "> " prompt prefix on first line, " " continuation on subsequent lines
|
||||
* - Single-line messages: compact highlight (content + small padding)
|
||||
* - Multi-line messages: full-width highlight box extending to terminal edge
|
||||
* - Word wrapping respects the 2-char prefix width
|
||||
* - System-reminder parts are shown plain (no highlight), user parts highlighted
|
||||
*/
|
||||
export const UserMessage = memo(({ line }: { line: UserLine }) => {
|
||||
const columns = useTerminalWidth();
|
||||
const contentWidth = Math.max(0, columns - 2);
|
||||
const contentWidth = Math.max(1, columns - 2);
|
||||
|
||||
return (
|
||||
<Box flexDirection="row">
|
||||
<Box width={2} flexShrink={0}>
|
||||
<Text>{">"} </Text>
|
||||
</Box>
|
||||
<Box flexGrow={1} width={contentWidth}>
|
||||
<MarkdownDisplay text={line.text} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
// Build combined ANSI code for background + optional foreground
|
||||
const { background, text: textColor } = colors.userMessage;
|
||||
const bgAnsi = hexToBgAnsi(background);
|
||||
const fgAnsi = textColor ? hexToFgAnsi(textColor) : "";
|
||||
const colorAnsi = bgAnsi + fgAnsi;
|
||||
|
||||
// Split into system-reminder blocks and user content blocks
|
||||
const blocks = splitSystemReminderBlocks(line.text);
|
||||
|
||||
const allLines: string[] = [];
|
||||
|
||||
for (const block of blocks) {
|
||||
if (!block.text.trim()) continue;
|
||||
|
||||
// Add blank line between blocks (not before first)
|
||||
if (allLines.length > 0) {
|
||||
allLines.push("");
|
||||
}
|
||||
|
||||
const blockLines = renderBlock(
|
||||
block.text,
|
||||
contentWidth,
|
||||
columns,
|
||||
!block.isSystemReminder, // highlight user content, not system-reminder
|
||||
colorAnsi,
|
||||
);
|
||||
allLines.push(...blockLines);
|
||||
}
|
||||
|
||||
return <Text>{allLines.join("\n")}</Text>;
|
||||
});
|
||||
|
||||
UserMessage.displayName = "UserMessage";
|
||||
|
||||
@@ -5,6 +5,36 @@
|
||||
* No colors should be hardcoded in components - all should reference this file.
|
||||
*/
|
||||
|
||||
import { getTerminalTheme } from "../helpers/terminalTheme";
|
||||
|
||||
/**
|
||||
* Parse a hex color (#RRGGBB) to RGB components.
|
||||
*/
|
||||
function parseHex(hex: string): { r: number; g: number; b: number } {
|
||||
const h = hex.replace("#", "");
|
||||
return {
|
||||
r: parseInt(h.slice(0, 2), 16),
|
||||
g: parseInt(h.slice(2, 4), 16),
|
||||
b: parseInt(h.slice(4, 6), 16),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a hex color (#RRGGBB) to an ANSI 24-bit background escape sequence.
|
||||
*/
|
||||
export function hexToBgAnsi(hex: string): string {
|
||||
const { r, g, b } = parseHex(hex);
|
||||
return `\x1b[48;2;${r};${g};${b}m`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a hex color (#RRGGBB) to an ANSI 24-bit foreground escape sequence.
|
||||
*/
|
||||
export function hexToFgAnsi(hex: string): string {
|
||||
const { r, g, b } = parseHex(hex);
|
||||
return `\x1b[38;2;${r};${g};${b}m`;
|
||||
}
|
||||
|
||||
// Brand colors (dark mode)
|
||||
export const brandColors = {
|
||||
orange: "#FF5533", // dark orange
|
||||
@@ -38,7 +68,7 @@ export const brandColorsLight = {
|
||||
} as const;
|
||||
|
||||
// Semantic color system
|
||||
export const colors = {
|
||||
const _colors = {
|
||||
// Welcome screen
|
||||
welcome: {
|
||||
border: brandColors.primaryAccent,
|
||||
@@ -169,3 +199,18 @@ export const colors = {
|
||||
agentName: brandColors.primaryAccent,
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Combine static colors with theme-aware dynamic properties
|
||||
export const colors = {
|
||||
..._colors,
|
||||
|
||||
// User messages (past prompts) - theme-aware background
|
||||
// Uses getter to read theme at render time (after async init)
|
||||
get userMessage() {
|
||||
const theme = getTerminalTheme();
|
||||
return {
|
||||
background: theme === "light" ? "#dcddf2" : "#ffffff", // light purple for light, white for dark
|
||||
text: theme === "light" ? undefined : "#000000", // black text for dark terminals
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user