Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tui-color-scheme.md
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions packages/tui-components/src/colors.ts
Original file line number Diff line number Diff line change
@@ -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]
12 changes: 6 additions & 6 deletions packages/tui-components/src/components/action-row.tsx
Original file line number Diff line number Diff line change
@@ -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
}
Expand All @@ -16,15 +16,15 @@ export const ActionRowSimple = ({
<Box flexDirection="column" marginBottom={1}>
<Box flexDirection="row" gap={1}>
<Text color={indicatorColor}>{INDICATOR.BULLET}</Text>
<Text color="white" dimColor={textDimColor}>
<Text color={colors.primary} dimColor={textDimColor}>
{text}
</Text>
</Box>
</Box>
)

type ActionRowProps = {
indicatorColor: StatusColor
indicatorColor: ThemeColor
label: string
summary?: string
children: React.ReactNode
Expand All @@ -33,9 +33,9 @@ export const ActionRow = ({ indicatorColor, label, summary, children }: ActionRo
<Box flexDirection="column" marginBottom={1}>
<Box flexDirection="row" gap={1}>
<Text color={indicatorColor}>{INDICATOR.BULLET}</Text>
<Text color="white">{label}</Text>
<Text color={colors.primary}>{label}</Text>
{summary && (
<Text color="white" dimColor>
<Text color={colors.primary} dimColor>
{summary}
</Text>
)}
Expand Down
57 changes: 30 additions & 27 deletions packages/tui-components/src/components/checkpoint-action-row.tsx
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -46,7 +47,7 @@ function renderParallelGroup(group: ParallelActivitiesGroup): React.ReactNode {
function renderReasoning(text: string): React.ReactNode {
const lines = text.split("\n")
return (
<ActionRow indicatorColor="white" label="Reasoning">
<ActionRow indicatorColor={colors.primary} label="Reasoning">
<Box flexDirection="column">
{lines.map((line, idx) => (
<Text key={`reasoning-${idx}`} dimColor wrap="wrap">
Expand All @@ -59,23 +60,25 @@ 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":
return renderQuery(action.text, action.runId)

case "retry":
return (
<ActionRow indicatorColor="yellow" label="Retry">
<ActionRow indicatorColor={colors.warn} label="Retry">
<Text dimColor>{action.message || action.error}</Text>
</ActionRow>
)

case "complete":
return (
<ActionRow indicatorColor="green" label="Run Results">
<ActionRow indicatorColor={colors.success} label="Run Results">
<Text>{action.text}</Text>
</ActionRow>
)
Expand All @@ -84,22 +87,22 @@ function renderAction(action: Activity): React.ReactNode {
// Show status of completion attempt
if (action.error) {
return (
<ActionRow indicatorColor="red" label="Completion Failed">
<Text color="red">{action.error}</Text>
<ActionRow indicatorColor={colors.destructive} label="Completion Failed">
<Text color={colors.destructive}>{action.error}</Text>
</ActionRow>
)
}
const remaining = action.remainingTodos?.filter((t) => !t.completed) ?? []
if (remaining.length > 0) {
return (
<ActionRow indicatorColor="yellow" label="Completion Blocked">
<ActionRow indicatorColor={colors.warn} label="Completion Blocked">
<Text dimColor>
{remaining.length} remaining task{remaining.length > 1 ? "s" : ""}
</Text>
</ActionRow>
)
}
return <ActionRowSimple indicatorColor="green" text="Completion Accepted" />
return <ActionRowSimple indicatorColor={colors.success} text="Completion Accepted" />
}

case "todo":
Expand Down Expand Up @@ -136,7 +139,7 @@ function renderAction(action: Activity): React.ReactNode {

case "delegate":
return (
<ActionRow indicatorColor="yellow" label={action.delegateExpertKey}>
<ActionRow indicatorColor={colors.warn} label={action.delegateExpertKey}>
<Text
dimColor
>{`{"query":"${truncateText(action.query, UI_CONSTANTS.TRUNCATE_TEXT_MEDIUM)}"}`}</Text>
Expand All @@ -146,14 +149,14 @@ function renderAction(action: Activity): React.ReactNode {
case "delegationComplete":
return (
<ActionRowSimple
indicatorColor="green"
indicatorColor={colors.success}
text={`Delegation Complete (${action.count} delegate${action.count > 1 ? "s" : ""} returned)`}
/>
)

case "interactiveTool":
return (
<ActionRow indicatorColor="yellow" label={`Interactive: ${action.toolName}`}>
<ActionRow indicatorColor={colors.warn} label={`Interactive: ${action.toolName}`}>
<Text dimColor>
{truncateText(JSON.stringify(action.args), UI_CONSTANTS.TRUNCATE_TEXT_MEDIUM)}
</Text>
Expand All @@ -171,8 +174,8 @@ function renderAction(action: Activity): React.ReactNode {

case "error":
return (
<ActionRow indicatorColor="red" label={action.errorName ?? "Error"}>
<Text color="red">{action.error ?? "Unknown error"}</Text>
<ActionRow indicatorColor={colors.destructive} label={action.errorName ?? "Error"}>
<Text color={colors.destructive}>{action.error ?? "Unknown error"}</Text>
</ActionRow>
)

Expand All @@ -186,7 +189,7 @@ function renderAction(action: Activity): React.ReactNode {

function renderTodo(
action: Extract<Activity, { type: "todo" }>,
color: StatusColor,
color: ThemeColor,
): React.ReactNode {
const { newTodos, completedTodos, todos } = action

Expand Down Expand Up @@ -250,7 +253,7 @@ function renderTodo(

function renderReadTextFile(
action: Extract<Activity, { type: "readTextFile" }>,
color: StatusColor,
color: ThemeColor,
): React.ReactNode {
const { path, content, from, to } = action
const lineRange = from !== undefined && to !== undefined ? `#${from}-${to}` : ""
Expand All @@ -264,7 +267,7 @@ function renderReadTextFile(
<Box flexDirection="column">
{lines.map((line, idx) => (
<Box flexDirection="row" key={`read-${idx}`} gap={1}>
<Text color="white" dimColor>
<Text color={colors.primary} dimColor>
{line}
</Text>
</Box>
Expand All @@ -276,7 +279,7 @@ function renderReadTextFile(

function renderWriteTextFile(
action: Extract<Activity, { type: "writeTextFile" }>,
color: StatusColor,
color: ThemeColor,
): React.ReactNode {
const { path, text } = action
const lines = text.split("\n")
Expand All @@ -285,10 +288,10 @@ function renderWriteTextFile(
<Box flexDirection="column">
{lines.map((line, idx) => (
<Box flexDirection="row" key={`write-${idx}`} gap={1}>
<Text color="green" dimColor>
<Text color={colors.success} dimColor>
+
</Text>
<Text color="white" dimColor>
<Text color={colors.primary} dimColor>
{line}
</Text>
</Box>
Expand All @@ -300,7 +303,7 @@ function renderWriteTextFile(

function renderEditTextFile(
action: Extract<Activity, { type: "editTextFile" }>,
color: StatusColor,
color: ThemeColor,
): React.ReactNode {
const { path, oldText, newText } = action
const oldLines = oldText.split("\n")
Expand All @@ -310,17 +313,17 @@ function renderEditTextFile(
<Box flexDirection="column">
{oldLines.map((line, idx) => (
<Box flexDirection="row" key={`old-${idx}`} gap={1}>
<Text color="red" dimColor>
<Text color={colors.destructive} dimColor>
-
</Text>
<Text color="white" dimColor>
<Text color={colors.primary} dimColor>
{line}
</Text>
</Box>
))}
{newLines.map((line, idx) => (
<Box flexDirection="row" key={`new-${idx}`} gap={1}>
<Text color="green" dimColor>
<Text color={colors.success} dimColor>
+
</Text>
<Text dimColor>{line}</Text>
Expand All @@ -333,7 +336,7 @@ function renderEditTextFile(

function renderExec(
action: Extract<Activity, { type: "exec" }>,
color: StatusColor,
color: ThemeColor,
): React.ReactNode {
const { command, args, cwd, output } = action
const cwdPart = cwd ? ` ${shortenPath(cwd, 40)}` : ""
Expand Down Expand Up @@ -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 (
<ActionRow indicatorColor="cyan" label="Query" summary={`(${shortRunId})`}>
<ActionRow indicatorColor={colors.accent} label="Query" summary={`(${shortRunId})`}>
<Box flexDirection="column">
{lines.map((line, idx) => (
<Text key={`query-${idx}`} dimColor wrap="wrap">
Expand Down
7 changes: 4 additions & 3 deletions packages/tui-components/src/components/expert-list.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Box, Text } from "ink"
import { colors } from "../colors.js"
import type { ExpertOption } from "../types/index.js"

export type ExpertListProps = {
Expand All @@ -17,15 +18,15 @@ export const ExpertList = ({
}: ExpertListProps) => {
const displayExperts = maxItems ? experts.slice(0, maxItems) : experts
if (displayExperts.length === 0) {
return <Text color="gray">No experts found.</Text>
return <Text color={colors.muted}>No experts found.</Text>
}
const items = displayExperts.map((expert, index) => (
<Text key={expert.key} color={index === selectedIndex ? "cyan" : "gray"}>
<Text key={expert.key} color={index === selectedIndex ? colors.accent : colors.muted}>
{index === selectedIndex ? ">" : " "} {showSource ? expert.key : expert.name}
{showSource && expert.source && (
<>
{" "}
<Text color={expert.source === "configured" ? "green" : "yellow"}>
<Text color={expert.source === "configured" ? colors.success : colors.warn}>
[{expert.source === "configured" ? "config" : "recent"}]
</Text>
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -34,7 +35,7 @@ export const ExpertSelectorBase = ({
{!inputMode && (
<Box flexDirection="column">
<Box>
<Text color="cyan">{hint}</Text>
<Text color={colors.accent}>{hint}</Text>
</Box>
<ExpertList
experts={experts}
Expand All @@ -47,9 +48,9 @@ export const ExpertSelectorBase = ({
)}
{inputMode && (
<Box>
<Text color="gray">Expert: </Text>
<Text color="white">{input}</Text>
<Text color="cyan">_</Text>
<Text color={colors.muted}>Expert: </Text>
<Text color={colors.primary}>{input}</Text>
<Text color={colors.accent}>_</Text>
</Box>
)}
</Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -33,7 +34,7 @@ export const BrowsingCheckpointsInput = ({
return false
}}
renderItem={(cp, isSelected) => (
<Text key={cp.id} color={isSelected ? "cyan" : "gray"}>
<Text key={cp.id} color={isSelected ? colors.accent : colors.muted}>
{isSelected ? ">" : " "} Step {cp.stepNumber} ({cp.id})
</Text>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -21,23 +22,23 @@ export const BrowsingEventDetailInput = ({ event, onBack }: BrowsingEventDetailI
</Box>
<Box flexDirection="column" marginLeft={2}>
<Text>
<Text color="gray">Type: </Text>
<Text color="cyan">{event.type}</Text>
<Text color={colors.muted}>Type: </Text>
<Text color={colors.accent}>{event.type}</Text>
</Text>
<Text>
<Text color="gray">Step: </Text>
<Text color={colors.muted}>Step: </Text>
<Text>{event.stepNumber}</Text>
</Text>
<Text>
<Text color="gray">Timestamp: </Text>
<Text color={colors.muted}>Timestamp: </Text>
<Text>{formatTimestamp(event.timestamp)}</Text>
</Text>
<Text>
<Text color="gray">ID: </Text>
<Text color={colors.muted}>ID: </Text>
<Text dimColor>{event.id}</Text>
</Text>
<Text>
<Text color="gray">Run ID: </Text>
<Text color={colors.muted}>Run ID: </Text>
<Text dimColor>{event.runId}</Text>
</Text>
</Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -23,7 +24,7 @@ export const BrowsingEventsInput = ({
onBack={onBack}
emptyMessage="No events found"
renderItem={(ev, isSelected) => (
<Text key={ev.id} color={isSelected ? "cyan" : "gray"}>
<Text key={ev.id} color={isSelected ? colors.accent : colors.muted}>
{isSelected ? ">" : " "} [{ev.type}] Step {ev.stepNumber} ({formatTimestamp(ev.timestamp)})
</Text>
)}
Expand Down
Loading