diff --git a/.changeset/tui-color-scheme.md b/.changeset/tui-color-scheme.md new file mode 100644 index 00000000..651b5558 --- /dev/null +++ b/.changeset/tui-color-scheme.md @@ -0,0 +1,5 @@ +--- +"@perstack/tui-components": patch +--- + +Centralize TUI color scheme into a single `colors.ts` module with semantic tokens, replacing ad-hoc color string literals across all components diff --git a/packages/tui-components/src/colors.ts b/packages/tui-components/src/colors.ts new file mode 100644 index 00000000..a32bd0de --- /dev/null +++ b/packages/tui-components/src/colors.ts @@ -0,0 +1,10 @@ +export const colors = { + primary: "white", + muted: "gray", + accent: "cyan", + success: "green", + warn: "yellow", + destructive: "red", +} as const + +export type ThemeColor = (typeof colors)[keyof typeof colors] diff --git a/packages/tui-components/src/components/action-row.tsx b/packages/tui-components/src/components/action-row.tsx index 1b8230e8..ccb18553 100644 --- a/packages/tui-components/src/components/action-row.tsx +++ b/packages/tui-components/src/components/action-row.tsx @@ -1,10 +1,10 @@ import { Box, Text } from "ink" import type React from "react" +import { colors, type ThemeColor } from "../colors.js" import { INDICATOR } from "../constants.js" -export type StatusColor = "green" | "red" | "yellow" | "white" | "gray" | "cyan" | "blue" type ActionRowSimpleProps = { - indicatorColor: StatusColor + indicatorColor: ThemeColor text: string textDimColor?: boolean } @@ -16,7 +16,7 @@ export const ActionRowSimple = ({ {INDICATOR.BULLET} - + {text} @@ -24,7 +24,7 @@ export const ActionRowSimple = ({ ) type ActionRowProps = { - indicatorColor: StatusColor + indicatorColor: ThemeColor label: string summary?: string children: React.ReactNode @@ -33,9 +33,9 @@ export const ActionRow = ({ indicatorColor, label, summary, children }: ActionRo {INDICATOR.BULLET} - {label} + {label} {summary && ( - + {summary} )} diff --git a/packages/tui-components/src/components/checkpoint-action-row.tsx b/packages/tui-components/src/components/checkpoint-action-row.tsx index 02c4ef84..3dc3f998 100644 --- a/packages/tui-components/src/components/checkpoint-action-row.tsx +++ b/packages/tui-components/src/components/checkpoint-action-row.tsx @@ -1,9 +1,10 @@ import type { Activity, ActivityOrGroup, ParallelActivitiesGroup } from "@perstack/core" import { Box, Text } from "ink" import type React from "react" +import { colors, type ThemeColor } from "../colors.js" import { RENDER_CONSTANTS, UI_CONSTANTS } from "../constants.js" import { shortenPath, summarizeOutput, truncateText } from "../helpers.js" -import { ActionRow, ActionRowSimple, type StatusColor } from "./action-row.js" +import { ActionRow, ActionRowSimple } from "./action-row.js" type CheckpointActionRowProps = { action: ActivityOrGroup @@ -46,7 +47,7 @@ function renderParallelGroup(group: ParallelActivitiesGroup): React.ReactNode { function renderReasoning(text: string): React.ReactNode { const lines = text.split("\n") return ( - + {lines.map((line, idx) => ( @@ -59,8 +60,10 @@ function renderReasoning(text: string): React.ReactNode { } function renderAction(action: Activity): React.ReactNode { - const color: StatusColor = - action.type === "error" || ("error" in action && action.error) ? "red" : "green" + const color: ThemeColor = + action.type === "error" || ("error" in action && action.error) + ? colors.destructive + : colors.success switch (action.type) { case "query": @@ -68,14 +71,14 @@ function renderAction(action: Activity): React.ReactNode { case "retry": return ( - + {action.message || action.error} ) case "complete": return ( - + {action.text} ) @@ -84,22 +87,22 @@ function renderAction(action: Activity): React.ReactNode { // Show status of completion attempt if (action.error) { return ( - - {action.error} + + {action.error} ) } const remaining = action.remainingTodos?.filter((t) => !t.completed) ?? [] if (remaining.length > 0) { return ( - + {remaining.length} remaining task{remaining.length > 1 ? "s" : ""} ) } - return + return } case "todo": @@ -136,7 +139,7 @@ function renderAction(action: Activity): React.ReactNode { case "delegate": return ( - + {`{"query":"${truncateText(action.query, UI_CONSTANTS.TRUNCATE_TEXT_MEDIUM)}"}`} @@ -146,14 +149,14 @@ function renderAction(action: Activity): React.ReactNode { case "delegationComplete": return ( 1 ? "s" : ""} returned)`} /> ) case "interactiveTool": return ( - + {truncateText(JSON.stringify(action.args), UI_CONSTANTS.TRUNCATE_TEXT_MEDIUM)} @@ -171,8 +174,8 @@ function renderAction(action: Activity): React.ReactNode { case "error": return ( - - {action.error ?? "Unknown error"} + + {action.error ?? "Unknown error"} ) @@ -186,7 +189,7 @@ function renderAction(action: Activity): React.ReactNode { function renderTodo( action: Extract, - color: StatusColor, + color: ThemeColor, ): React.ReactNode { const { newTodos, completedTodos, todos } = action @@ -250,7 +253,7 @@ function renderTodo( function renderReadTextFile( action: Extract, - color: StatusColor, + color: ThemeColor, ): React.ReactNode { const { path, content, from, to } = action const lineRange = from !== undefined && to !== undefined ? `#${from}-${to}` : "" @@ -264,7 +267,7 @@ function renderReadTextFile( {lines.map((line, idx) => ( - + {line} @@ -276,7 +279,7 @@ function renderReadTextFile( function renderWriteTextFile( action: Extract, - color: StatusColor, + color: ThemeColor, ): React.ReactNode { const { path, text } = action const lines = text.split("\n") @@ -285,10 +288,10 @@ function renderWriteTextFile( {lines.map((line, idx) => ( - + + - + {line} @@ -300,7 +303,7 @@ function renderWriteTextFile( function renderEditTextFile( action: Extract, - color: StatusColor, + color: ThemeColor, ): React.ReactNode { const { path, oldText, newText } = action const oldLines = oldText.split("\n") @@ -310,17 +313,17 @@ function renderEditTextFile( {oldLines.map((line, idx) => ( - + - - + {line} ))} {newLines.map((line, idx) => ( - + + {line} @@ -333,7 +336,7 @@ function renderEditTextFile( function renderExec( action: Extract, - color: StatusColor, + color: ThemeColor, ): React.ReactNode { const { command, args, cwd, output } = action const cwdPart = cwd ? ` ${shortenPath(cwd, 40)}` : "" @@ -365,7 +368,7 @@ function renderQuery(text: string, runId: string): React.ReactNode { // Show abbreviated runId (first 8 chars) to help identify different runs const shortRunId = runId.slice(0, 8) return ( - + {lines.map((line, idx) => ( diff --git a/packages/tui-components/src/components/expert-list.tsx b/packages/tui-components/src/components/expert-list.tsx index eed220c1..fe911d11 100644 --- a/packages/tui-components/src/components/expert-list.tsx +++ b/packages/tui-components/src/components/expert-list.tsx @@ -1,4 +1,5 @@ import { Box, Text } from "ink" +import { colors } from "../colors.js" import type { ExpertOption } from "../types/index.js" export type ExpertListProps = { @@ -17,15 +18,15 @@ export const ExpertList = ({ }: ExpertListProps) => { const displayExperts = maxItems ? experts.slice(0, maxItems) : experts if (displayExperts.length === 0) { - return No experts found. + return No experts found. } const items = displayExperts.map((expert, index) => ( - + {index === selectedIndex ? ">" : " "} {showSource ? expert.key : expert.name} {showSource && expert.source && ( <> {" "} - + [{expert.source === "configured" ? "config" : "recent"}] diff --git a/packages/tui-components/src/components/expert-selector-base.tsx b/packages/tui-components/src/components/expert-selector-base.tsx index 150d5840..1e1996eb 100644 --- a/packages/tui-components/src/components/expert-selector-base.tsx +++ b/packages/tui-components/src/components/expert-selector-base.tsx @@ -1,5 +1,6 @@ import type { Key } from "ink" import { Box, Text, useInput } from "ink" +import { colors } from "../colors.js" import { UI_CONSTANTS } from "../constants.js" import { useExpertSelector } from "../hooks/index.js" import type { ExpertOption } from "../types/index.js" @@ -34,7 +35,7 @@ export const ExpertSelectorBase = ({ {!inputMode && ( - {hint} + {hint} - Expert: - {input} - _ + Expert: + {input} + _ )} diff --git a/packages/tui-components/src/components/input-areas/browsing-checkpoints.tsx b/packages/tui-components/src/components/input-areas/browsing-checkpoints.tsx index 29e872e6..9c95f7b1 100644 --- a/packages/tui-components/src/components/input-areas/browsing-checkpoints.tsx +++ b/packages/tui-components/src/components/input-areas/browsing-checkpoints.tsx @@ -1,4 +1,5 @@ import { Text } from "ink" +import { colors } from "../../colors.js" import { KEY_HINTS } from "../../constants.js" import type { CheckpointHistoryItem, JobHistoryItem } from "../../types/index.js" import { ListBrowser } from "../list-browser.js" @@ -33,7 +34,7 @@ export const BrowsingCheckpointsInput = ({ return false }} renderItem={(cp, isSelected) => ( - + {isSelected ? ">" : " "} Step {cp.stepNumber} ({cp.id}) )} diff --git a/packages/tui-components/src/components/input-areas/browsing-event-detail.tsx b/packages/tui-components/src/components/input-areas/browsing-event-detail.tsx index 9a6b7d6c..90a4fed5 100644 --- a/packages/tui-components/src/components/input-areas/browsing-event-detail.tsx +++ b/packages/tui-components/src/components/input-areas/browsing-event-detail.tsx @@ -1,4 +1,5 @@ import { Box, Text, useInput } from "ink" +import { colors } from "../../colors.js" import { KEY_HINTS } from "../../constants.js" import { formatTimestamp } from "../../helpers.js" import type { EventHistoryItem } from "../../types/index.js" @@ -21,23 +22,23 @@ export const BrowsingEventDetailInput = ({ event, onBack }: BrowsingEventDetailI - Type: - {event.type} + Type: + {event.type} - Step: + Step: {event.stepNumber} - Timestamp: + Timestamp: {formatTimestamp(event.timestamp)} - ID: + ID: {event.id} - Run ID: + Run ID: {event.runId} diff --git a/packages/tui-components/src/components/input-areas/browsing-events.tsx b/packages/tui-components/src/components/input-areas/browsing-events.tsx index 56edb810..466a1901 100644 --- a/packages/tui-components/src/components/input-areas/browsing-events.tsx +++ b/packages/tui-components/src/components/input-areas/browsing-events.tsx @@ -1,4 +1,5 @@ import { Text } from "ink" +import { colors } from "../../colors.js" import { KEY_HINTS } from "../../constants.js" import { formatTimestamp } from "../../helpers.js" import type { CheckpointHistoryItem, EventHistoryItem } from "../../types/index.js" @@ -23,7 +24,7 @@ export const BrowsingEventsInput = ({ onBack={onBack} emptyMessage="No events found" renderItem={(ev, isSelected) => ( - + {isSelected ? ">" : " "} [{ev.type}] Step {ev.stepNumber} ({formatTimestamp(ev.timestamp)}) )} diff --git a/packages/tui-components/src/components/input-areas/browsing-history.tsx b/packages/tui-components/src/components/input-areas/browsing-history.tsx index f2805a74..a2eac6da 100644 --- a/packages/tui-components/src/components/input-areas/browsing-history.tsx +++ b/packages/tui-components/src/components/input-areas/browsing-history.tsx @@ -1,4 +1,5 @@ import { Text } from "ink" +import { colors } from "../../colors.js" import { KEY_HINTS } from "../../constants.js" import { formatTimestamp } from "../../helpers.js" import type { JobHistoryItem } from "../../types/index.js" @@ -33,7 +34,7 @@ export const BrowsingHistoryInput = ({ return false }} renderItem={(job, isSelected) => ( - + {isSelected ? ">" : " "} {job.expertKey} - {job.totalSteps} steps ({job.jobId}) ( {formatTimestamp(job.startedAt)}) diff --git a/packages/tui-components/src/components/list-browser.tsx b/packages/tui-components/src/components/list-browser.tsx index 57810fbc..b7f72fd2 100644 --- a/packages/tui-components/src/components/list-browser.tsx +++ b/packages/tui-components/src/components/list-browser.tsx @@ -2,6 +2,7 @@ import type { Key } from "ink" import { Box, Text, useInput } from "ink" import type React from "react" import { useMemo } from "react" +import { colors } from "../colors.js" import { INDICATOR, UI_CONSTANTS } from "../constants.js" import { useListNavigation } from "../hooks/use-list-navigation.js" @@ -46,18 +47,18 @@ export const ListBrowser = ({ return ( - {title} + {title} {items.length > maxItems && ( - + {" "} ({selectedIndex + 1}/{items.length}) )} - {hasMoreAbove && {INDICATOR.ELLIPSIS}} + {hasMoreAbove && {INDICATOR.ELLIPSIS}} {displayItems.length === 0 ? ( - {emptyMessage} + {emptyMessage} ) : ( displayItems.map((item, index) => { const actualIndex = scrollOffset + index @@ -65,7 +66,7 @@ export const ListBrowser = ({ }) )} - {hasMoreBelow && {INDICATOR.ELLIPSIS}} + {hasMoreBelow && {INDICATOR.ELLIPSIS}} ) } diff --git a/packages/tui-components/src/execution/components/interface-panel.tsx b/packages/tui-components/src/execution/components/interface-panel.tsx index e209906c..1cca47fc 100644 --- a/packages/tui-components/src/execution/components/interface-panel.tsx +++ b/packages/tui-components/src/execution/components/interface-panel.tsx @@ -1,6 +1,7 @@ import type { StreamingState } from "@perstack/react" import { Box, Text, useInput } from "ink" import type React from "react" +import { colors } from "../../colors.js" import { USAGE_INDICATORS } from "../../constants.js" import { useTextInput } from "../../hooks/use-text-input.js" import type { RuntimeInfo } from "../../types/index.js" @@ -42,11 +43,11 @@ export const InterfacePanel = ({ // Derive status label let statusLabel: React.ReactNode if (runStatus === "waiting") { - statusLabel = Waiting for query... + statusLabel = Waiting for query... } else if (runStatus === "completed") { - statusLabel = Completed + statusLabel = Completed } else if (runStatus === "stopped") { - statusLabel = Stopped + statusLabel = Stopped } else if (streamingPhase === "reasoning") { statusLabel = Streaming Reasoning... } else if (streamingPhase === "generating") { @@ -63,14 +64,14 @@ export const InterfacePanel = ({ - {spinner ? {spinner} : null} + {spinner ? {spinner} : null} {statusLabel} @@ -80,9 +81,9 @@ export const InterfacePanel = ({ {usagePercent}% - > + > {input} - _ + _ )