feat(theme): dark/light mode with a Radix Colors-based palette

Adds a working Dark/Light toggle (next to the language switcher, same row
as EN/VI) and re-themes the whole dashboard, not just the handful of
components that already used semantic tokens.

- Tailwind darkMode:"class" + CSS-variable color tokens (client/src/index.css,
  tailwind.config.js): surface.0-5, border/border-light, accent/accent-hover,
  fg.primary/secondary/muted, status.success/danger/warning. One class flip
  on <html> re-themes everything — no per-element dark: variant pairs.
- useTheme() hook: localStorage-persisted, defaults to dark, no
  prefers-color-scheme fallback (client/src/hooks/useTheme.ts).
- Mechanical, table-driven migration (scripts/migrate-color-tokens.mjs,
  scripts/tokenize-status-colors.mjs, scripts/darken-status-colors.mjs) of
  every raw neutral/gray/slate + emerald/red/amber Tailwind utility across
  client/src onto the new tokens, so every badge/button/component pulls the
  same shade per status/role instead of each picking its own.
- Palette values are the literal Radix Colors (radix-ui.com/colors) scale
  constants — slate/blue/green/red/amber steps 1-12 — adopted after three
  rounds of hand-picked values that kept overshooting (flat, then too dark,
  then glaring); see docs/superpowers/specs/2026-07-31-color-redesign-
  dark-light-mode-design.md for the full history and role mapping.
- PipelineMap: done/current/failed/passed-no-evidence/detected share one
  visual language (border + text + translucent wash of the same status
  color); `current` alone stays a solid accent fill, the one state that
  gets to look bolder ("you are here").
- LaneCard: removed the stage/kind/auto-stage chips that duplicated the
  Workspace lane-detail header already showing them.

Categorical/decorative hues (violet, indigo, cyan, teal, sky, rose, pink,
orange, yellow, and blue where it plays a role-coloring part e.g. message
bubbles) are deliberately out of scope — collapsing those onto shared
tokens would erase the distinction between different kinds of thing, not
a status.
This commit is contained in:
2026-07-31 10:54:31 +07:00
parent 4905d63b97
commit b673363351
82 changed files with 3776 additions and 2578 deletions
@@ -142,20 +142,20 @@ export function CodeBlock({
const palette =
tone === "danger"
? {
wrapper: "border-red-500/30 bg-red-500/5",
chrome: "bg-red-500/10 border-b border-red-500/20",
label: "text-red-300",
wrapper: "border-status-danger/30 bg-status-danger/5",
chrome: "bg-status-danger/10 border-b border-status-danger/20",
label: "text-status-danger",
}
: tone === "success"
? {
wrapper: "border-emerald-500/30 bg-emerald-500/5",
chrome: "bg-emerald-500/10 border-b border-emerald-500/20",
label: "text-emerald-300",
wrapper: "border-status-success/30 bg-status-success/5",
chrome: "bg-status-success/10 border-b border-status-success/20",
label: "text-status-success",
}
: {
wrapper: "border-surface-3 bg-surface-4/50",
chrome: "bg-surface-3/70 border-b border-surface-3",
label: "text-gray-400",
label: "text-fg-secondary",
};
const preStyle: React.CSSProperties = {};
@@ -177,7 +177,7 @@ export function CodeBlock({
{/* Filename + lang together when both are set */}
{filename && !label && (
<span className="text-gray-600 font-mono lowercase">{langDisplay(lang)}</span>
<span className="text-fg-muted font-mono lowercase">{langDisplay(lang)}</span>
)}
{filename && label && (
<span className={`font-mono uppercase tracking-wider ${palette.label}`}>· {label}</span>
@@ -186,7 +186,7 @@ export function CodeBlock({
{/* Right side: line count + copy */}
<div className="ml-auto flex items-center gap-3">
{totalLines > 1 && (
<span className="text-gray-600 font-mono">
<span className="text-fg-muted font-mono">
{totalLines} {totalLines === 1 ? "line" : "lines"}
</span>
)}
@@ -194,7 +194,7 @@ export function CodeBlock({
type="button"
onClick={handleCopy}
className={`inline-flex items-center gap-1 transition-colors ${
copied ? "text-emerald-300" : "text-gray-500 hover:text-gray-200"
copied ? "text-status-success" : "text-fg-muted hover:text-fg-secondary"
}`}
aria-label="Copy code"
>
@@ -221,7 +221,7 @@ export function CodeBlock({
{lineTokens.map((line, i) => (
<tr key={i} className="align-top">
<td
className="select-none text-right pl-3 pr-3 text-gray-600 font-mono text-[11px] leading-[1.6] sticky left-0 bg-inherit"
className="select-none text-right pl-3 pr-3 text-fg-muted font-mono text-[11px] leading-[1.6] sticky left-0 bg-inherit"
style={{ width: "1%", whiteSpace: "nowrap" }}
>
{i + 1}
@@ -399,7 +399,7 @@ export function ConversationView({ sessionId, initialTranscriptId }: Conversatio
<select
value={selectedTranscript || ""}
onChange={(e) => setSelectedTranscript(e.target.value || null)}
className="appearance-none bg-surface-2 border border-surface-3 rounded-lg px-3 py-1.5 pr-8 text-sm text-gray-300 focus:outline-none focus:border-violet-500/50 hover:border-violet-500/30 cursor-pointer transition-colors"
className="appearance-none bg-surface-2 border border-surface-3 rounded-lg px-3 py-1.5 pr-8 text-sm text-fg-secondary focus:outline-none focus:border-violet-500/50 hover:border-violet-500/30 cursor-pointer transition-colors"
>
{transcripts.map((t) => (
<option key={t.id} value={t.id}>
@@ -407,10 +407,10 @@ export function ConversationView({ sessionId, initialTranscriptId }: Conversatio
</option>
))}
</select>
<ChevronDown className="w-3.5 h-3.5 text-gray-500 absolute right-2.5 top-1/2 -translate-y-1/2 pointer-events-none" />
<ChevronDown className="w-3.5 h-3.5 text-fg-muted absolute right-2.5 top-1/2 -translate-y-1/2 pointer-events-none" />
</div>
)}
<span className="inline-flex items-center gap-1.5 text-[11px] text-gray-500 font-mono bg-surface-2 border border-surface-3 rounded-md px-2 py-1">
<span className="inline-flex items-center gap-1.5 text-[11px] text-fg-muted font-mono bg-surface-2 border border-surface-3 rounded-md px-2 py-1">
<MessagesSquare className="w-3 h-3" />
{total} message{total !== 1 ? "s" : ""}
</span>
@@ -420,7 +420,7 @@ export function ConversationView({ sessionId, initialTranscriptId }: Conversatio
disabled={refreshing || loading}
title="Refresh conversation"
aria-label="Refresh conversation"
className="inline-flex items-center gap-1.5 text-[11px] text-gray-400 hover:text-gray-200 bg-surface-2 border border-surface-3 hover:border-violet-500/30 rounded-md px-2 py-1 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
className="inline-flex items-center gap-1.5 text-[11px] text-fg-secondary hover:text-fg-primary bg-surface-2 border border-surface-3 hover:border-violet-500/30 rounded-md px-2 py-1 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<RefreshCw className={`w-3 h-3 ${refreshing ? "animate-spin" : ""}`} />
Refresh
@@ -430,7 +430,7 @@ export function ConversationView({ sessionId, initialTranscriptId }: Conversatio
{/* Error alert */}
{error && (
<div className="text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-lg px-4 py-3 flex-shrink-0">
<div className="text-sm text-status-danger bg-status-danger/10 border border-status-danger/20 rounded-lg px-4 py-3 flex-shrink-0">
{error}
</div>
)}
@@ -445,31 +445,31 @@ export function ConversationView({ sessionId, initialTranscriptId }: Conversatio
{/* History loading indicator */}
{loadingHistory && (
<div className="flex justify-center py-3">
<Loader2 className="w-4 h-4 text-gray-500 animate-spin" />
<span className="text-xs text-gray-500 ml-2">Loading history...</span>
<Loader2 className="w-4 h-4 text-fg-muted animate-spin" />
<span className="text-xs text-fg-muted ml-2">Loading history...</span>
</div>
)}
{/* Scroll-up for history hint */}
{hasMore && !loadingHistory && !loading && (
<div className="flex justify-center py-2">
<span className="text-[11px] text-gray-600"> Scroll up for older messages</span>
<span className="text-[11px] text-fg-muted"> Scroll up for older messages</span>
</div>
)}
{loading ? (
<div className="flex items-center justify-center py-12 text-gray-500 text-sm">
<div className="flex items-center justify-center py-12 text-fg-muted text-sm">
Loading conversation...
</div>
) : messages.length === 0 ? (
<div className="mx-auto max-w-md py-12 text-center">
<p className="text-sm text-gray-400">No conversation records found.</p>
<p className="mt-2 text-xs leading-relaxed text-gray-500">
<p className="text-sm text-fg-secondary">No conversation records found.</p>
<p className="mt-2 text-xs leading-relaxed text-fg-muted">
This session's metadata was imported, but its transcript file is no longer on disk.
Claude Code automatically deletes inactive session transcripts after a retention
period (<code className="text-gray-400">cleanupPeriodDays</code>, default 30 days), so
older conversations may already be gone. Sessions imported from now on are snapshotted
and kept even after Claude Code prunes the originals.
period (<code className="text-fg-secondary">cleanupPeriodDays</code>, default 30
days), so older conversations may already be gone. Sessions imported from now on are
snapshotted and kept even after Claude Code prunes the originals.
</p>
</div>
) : (
@@ -274,7 +274,7 @@ function renderInline(text: string, baseKey = ""): React.ReactNode[] {
const codeM = rest.match(/^`([^`\n]+)`/);
if (codeM) {
push(
<code className="rounded bg-surface-4 border border-surface-3 px-1.5 py-0.5 font-mono text-[12.5px] text-amber-200">
<code className="rounded bg-surface-4 border border-surface-3 px-1.5 py-0.5 font-mono text-[12.5px] text-status-warning">
{codeM[1]}
</code>
);
@@ -286,7 +286,7 @@ function renderInline(text: string, baseKey = ""): React.ReactNode[] {
const boldM = rest.match(/^(\*\*|__)(.+?)\1/);
if (boldM) {
push(
<strong className="font-semibold text-gray-50">
<strong className="font-semibold text-fg-primary">
{renderInline(boldM[2]!, `${baseKey}-b${n}`)}
</strong>
);
@@ -298,7 +298,9 @@ function renderInline(text: string, baseKey = ""): React.ReactNode[] {
const italicM = rest.match(/^(\*|_)([^*_\n]+?)\1/);
if (italicM) {
push(
<em className="italic text-gray-200">{renderInline(italicM[2]!, `${baseKey}-i${n}`)}</em>
<em className="italic text-fg-secondary">
{renderInline(italicM[2]!, `${baseKey}-i${n}`)}
</em>
);
i += italicM[0].length;
continue;
@@ -308,7 +310,7 @@ function renderInline(text: string, baseKey = ""): React.ReactNode[] {
const strikeM = rest.match(/^~~(.+?)~~/);
if (strikeM) {
push(
<span className="line-through text-gray-500">
<span className="line-through text-fg-muted">
{renderInline(strikeM[1]!, `${baseKey}-s${n}`)}
</span>
);
@@ -366,11 +368,13 @@ function renderListItem(item: string, key: string): React.ReactNode {
<span className="inline-flex items-baseline gap-2">
<span
className={`inline-block w-3 h-3 rounded-sm border flex-shrink-0 translate-y-0.5 ${
checked ? "bg-emerald-500/40 border-emerald-400/60" : "bg-surface-4 border-surface-3"
checked
? "bg-status-success/40 border-status-success/60"
: "bg-surface-4 border-surface-3"
}`}
aria-hidden="true"
/>
<span className={checked ? "text-gray-500 line-through" : ""}>
<span className={checked ? "text-fg-muted line-through" : ""}>
{renderInline(taskMatch[2]!, key)}
</span>
</span>
@@ -386,12 +390,12 @@ interface MarkdownContentProps {
}
const HEADING_STYLES = [
"text-[18px] font-semibold text-gray-50 mt-2 pb-1 border-b border-surface-3",
"text-[16px] font-semibold text-gray-50 mt-2",
"text-[15px] font-semibold text-gray-100",
"text-sm font-semibold text-gray-100",
"text-sm font-medium text-gray-200",
"text-xs font-medium text-gray-300 uppercase tracking-wider",
"text-[18px] font-semibold text-fg-primary mt-2 pb-1 border-b border-surface-3",
"text-[16px] font-semibold text-fg-primary mt-2",
"text-[15px] font-semibold text-fg-primary",
"text-sm font-semibold text-fg-primary",
"text-sm font-medium text-fg-secondary",
"text-xs font-medium text-fg-secondary uppercase tracking-wider",
];
export function MarkdownContent({ text, dense = false }: MarkdownContentProps) {
@@ -399,7 +403,7 @@ export function MarkdownContent({ text, dense = false }: MarkdownContentProps) {
const gap = dense ? "space-y-1.5" : "space-y-2.5";
return (
<div className={`text-sm text-gray-300 leading-relaxed ${gap}`}>
<div className={`text-sm text-fg-secondary leading-relaxed ${gap}`}>
{blocks.map((b, idx) => {
switch (b.kind) {
case "code":
@@ -419,10 +423,10 @@ export function MarkdownContent({ text, dense = false }: MarkdownContentProps) {
return (
<ol
key={idx}
className="list-decimal pl-5 space-y-1 marker:text-gray-500 marker:font-mono marker:text-xs"
className="list-decimal pl-5 space-y-1 marker:text-fg-muted marker:font-mono marker:text-xs"
>
{b.items.map((item, i) => (
<li key={i} className="text-sm text-gray-300">
<li key={i} className="text-sm text-fg-secondary">
{renderListItem(item, `li${idx}-${i}`)}
</li>
))}
@@ -432,7 +436,7 @@ export function MarkdownContent({ text, dense = false }: MarkdownContentProps) {
return (
<ul key={idx} className="list-disc pl-5 space-y-1 marker:text-violet-400/60">
{b.items.map((item, i) => (
<li key={i} className="text-sm text-gray-300">
<li key={i} className="text-sm text-fg-secondary">
{renderListItem(item, `li${idx}-${i}`)}
</li>
))}
@@ -443,7 +447,7 @@ export function MarkdownContent({ text, dense = false }: MarkdownContentProps) {
return (
<blockquote
key={idx}
className="relative border-l-2 border-violet-400/50 pl-3 pr-2 py-1 text-gray-400 italic bg-violet-500/[0.04] rounded-r"
className="relative border-l-2 border-violet-400/50 pl-3 pr-2 py-1 text-fg-secondary italic bg-violet-500/[0.04] rounded-r"
>
{renderInline(b.text, `q${idx}`)}
</blockquote>
@@ -471,7 +475,7 @@ export function MarkdownContent({ text, dense = false }: MarkdownContentProps) {
{b.header.map((cell, i) => (
<th
key={i}
className={`px-3 py-1.5 font-semibold text-gray-200 border-b border-surface-3 ${alignClass(b.aligns[i] ?? null)}`}
className={`px-3 py-1.5 font-semibold text-fg-secondary border-b border-surface-3 ${alignClass(b.aligns[i] ?? null)}`}
>
{renderInline(cell, `th${idx}-${i}`)}
</th>
@@ -487,7 +491,7 @@ export function MarkdownContent({ text, dense = false }: MarkdownContentProps) {
{row.map((cell, ci) => (
<td
key={ci}
className={`px-3 py-1.5 text-gray-300 align-top ${alignClass(b.aligns[ci] ?? null)}`}
className={`px-3 py-1.5 text-fg-secondary align-top ${alignClass(b.aligns[ci] ?? null)}`}
>
{renderInline(cell, `td${idx}-${ri}-${ci}`)}
</td>
@@ -502,7 +506,7 @@ export function MarkdownContent({ text, dense = false }: MarkdownContentProps) {
case "para":
return (
<p key={idx} className="text-sm text-gray-300 whitespace-pre-wrap break-words">
<p key={idx} className="text-sm text-fg-secondary whitespace-pre-wrap break-words">
{renderInline(b.text, `p${idx}`)}
</p>
);
@@ -88,9 +88,9 @@ const SENDER_STYLES: Record<
label: "User",
icon: User,
avatarRing:
"bg-gradient-to-br from-blue-500/30 to-cyan-500/20 text-blue-200 ring-1 ring-blue-400/30",
accentBar: "before:bg-blue-500/40",
headerText: "text-blue-200",
"bg-gradient-to-br from-blue-600/30 to-cyan-500/20 text-blue-300 ring-1 ring-blue-500/30",
accentBar: "before:bg-blue-600/40",
headerText: "text-blue-300",
},
assistant: {
label: "Assistant",
@@ -104,7 +104,7 @@ const SENDER_STYLES: Record<
label: "Main agent",
icon: Workflow,
avatarRing:
"bg-gradient-to-br from-teal-500/30 to-emerald-500/20 text-teal-200 ring-1 ring-teal-400/30",
"bg-gradient-to-br from-teal-500/30 to-status-success/20 text-teal-200 ring-1 ring-teal-400/30",
accentBar: "before:bg-teal-500/40",
headerText: "text-teal-200",
},
@@ -112,17 +112,17 @@ const SENDER_STYLES: Record<
label: "System",
icon: Cog,
avatarRing:
"bg-gradient-to-br from-slate-500/30 to-gray-500/20 text-gray-300 ring-1 ring-slate-400/30",
accentBar: "before:bg-slate-500/40",
headerText: "text-gray-300",
"bg-gradient-to-br from-slate-500/30 to-gray-500/20 text-fg-secondary ring-1 ring-border-light/30",
accentBar: "before:bg-surface-4/40",
headerText: "text-fg-secondary",
},
tool: {
label: "Tool",
icon: Terminal,
avatarRing:
"bg-gradient-to-br from-amber-500/30 to-orange-500/20 text-amber-200 ring-1 ring-amber-400/30",
accentBar: "before:bg-amber-500/40",
headerText: "text-amber-200",
"bg-gradient-to-br from-status-warning/30 to-orange-500/20 text-status-warning ring-1 ring-status-warning/30",
accentBar: "before:bg-status-warning/40",
headerText: "text-status-warning",
},
};
import { ToolCallBlock } from "./ToolCallBlock";
@@ -174,12 +174,12 @@ function formatLocalTime(iso: string): string {
function SessionEventRow({ title, timestamp }: { title?: string; timestamp: string | null }) {
return (
<div className="flex items-center justify-center py-1">
<div className="inline-flex items-center gap-2 text-[11px] text-gray-400 bg-surface-2/70 border border-surface-3 rounded-full px-3 py-1 max-w-full">
<div className="inline-flex items-center gap-2 text-[11px] text-fg-secondary bg-surface-2/70 border border-surface-3 rounded-full px-3 py-1 max-w-full">
<Pencil className="w-3 h-3 text-violet-300/70 flex-shrink-0" />
<span className="text-gray-500">Renamed session </span>
<span className="text-gray-200 font-medium truncate">{title || "(untitled)"}</span>
<span className="text-fg-muted">Renamed session </span>
<span className="text-fg-secondary font-medium truncate">{title || "(untitled)"}</span>
{timestamp && (
<span className="text-[10px] text-gray-600 font-mono flex-shrink-0">
<span className="text-[10px] text-fg-muted font-mono flex-shrink-0">
{formatLocalTime(timestamp)}
</span>
)}
@@ -191,8 +191,8 @@ function SessionEventRow({ title, timestamp }: { title?: string; timestamp: stri
/** Compact pill for /command invocations parsed out of TUI markup. */
function CommandPill({ display }: { display: string }) {
return (
<div className="inline-flex items-center gap-2 text-sm text-emerald-300 font-mono bg-emerald-500/10 border border-emerald-500/20 rounded-md px-3 py-1.5 max-w-full">
<span className="text-emerald-500/70"></span>
<div className="inline-flex items-center gap-2 text-sm text-status-success font-mono bg-status-success/10 border border-status-success/20 rounded-md px-3 py-1.5 max-w-full">
<span className="text-status-success/70"></span>
<span className="break-all">{display}</span>
</div>
);
@@ -203,9 +203,9 @@ function TerminalBlock({ text, stream }: { text: string; stream: "stdout" | "std
const cleaned = stripAnsi(text).replace(/^\n+|\n+$/g, "");
const isErr = stream === "stderr";
const accent = isErr
? "border-red-500/30 bg-red-950/30 text-red-200/90"
: "border-surface-3 bg-surface-4/60 text-gray-200";
const labelColor = isErr ? "text-red-300/80" : "text-gray-400";
? "border-status-danger/30 bg-status-danger/30 text-status-danger/90"
: "border-surface-3 bg-surface-4/60 text-fg-secondary";
const labelColor = isErr ? "text-status-danger/80" : "text-fg-secondary";
return (
<div className={`rounded-lg border ${accent} overflow-hidden`}>
<div
@@ -224,7 +224,7 @@ function TerminalBlock({ text, stream }: { text: string; stream: "stdout" | "std
/** Subtle inline note for the local-command-caveat banner. */
function CaveatBlock({ text }: { text: string }) {
return (
<div className="flex items-start gap-2 rounded-md border border-amber-500/15 bg-amber-500/[0.05] px-3 py-1.5 text-[11px] text-amber-200/70">
<div className="flex items-start gap-2 rounded-md border border-status-warning/15 bg-status-warning/[0.05] px-3 py-1.5 text-[11px] text-status-warning/70">
<Info className="w-3.5 h-3.5 mt-px flex-shrink-0 opacity-60" />
<span className="leading-relaxed italic">{stripAnsi(text).trim()}</span>
</div>
@@ -247,11 +247,11 @@ function renderSegment(seg: TuiSegment, key: number): React.ReactNode {
<CollapsibleBlock
key={key}
text={seg.text}
icon={<AlertTriangle className="w-3.5 h-3.5 text-amber-400/70 flex-shrink-0" />}
icon={<AlertTriangle className="w-3.5 h-3.5 text-status-warning/70 flex-shrink-0" />}
title="System reminder"
borderClass="border-amber-500/20"
bgClass="bg-amber-500/5"
textClass="text-amber-300/80"
borderClass="border-status-warning/20"
bgClass="bg-status-warning/5"
textClass="text-status-warning/80"
/>
);
case "persisted-output":
@@ -326,7 +326,7 @@ export function MessageList({ messages, loading }: MessageListProps) {
if (loading) {
return (
<div className="flex items-center justify-center py-12 text-gray-500 text-sm">
<div className="flex items-center justify-center py-12 text-fg-muted text-sm">
Loading conversation...
</div>
);
@@ -334,7 +334,7 @@ export function MessageList({ messages, loading }: MessageListProps) {
if (messages.length === 0) {
return (
<div className="text-center py-12 text-gray-500 text-sm">No conversation records found.</div>
<div className="text-center py-12 text-fg-muted text-sm">No conversation records found.</div>
);
}
@@ -392,19 +392,19 @@ export function MessageList({ messages, loading }: MessageListProps) {
{style.label}
</span>
{msg.model && (
<span className="text-[10px] text-gray-400 font-mono bg-surface-3/60 border border-surface-3 rounded px-1.5 py-0.5">
<span className="text-[10px] text-fg-secondary font-mono bg-surface-3/60 border border-surface-3 rounded px-1.5 py-0.5">
{formatModelName(msg.model)}
</span>
)}
{msg.usage && (
<span className="text-[10px] text-gray-500 font-mono inline-flex items-center gap-1">
<span className="text-emerald-300/70"> {fmt(msg.usage.input_tokens)}</span>
<span className="text-gray-700">·</span>
<span className="text-[10px] text-fg-muted font-mono inline-flex items-center gap-1">
<span className="text-status-success/70"> {fmt(msg.usage.input_tokens)}</span>
<span className="text-fg-muted">·</span>
<span className="text-orange-300/70"> {fmt(msg.usage.output_tokens)}</span>
</span>
)}
{msg.timestamp && (
<span className="text-[10px] text-gray-600 ml-auto font-mono">
<span className="text-[10px] text-fg-muted ml-auto font-mono">
{formatLocalTime(msg.timestamp)}
</span>
)}
@@ -436,11 +436,11 @@ export function MessageList({ messages, loading }: MessageListProps) {
<CollapsibleBlock
key={bIdx}
text={block.text}
icon={<ScrollText className="w-3.5 h-3.5 text-blue-400/60 flex-shrink-0" />}
icon={<ScrollText className="w-3.5 h-3.5 text-blue-500/60 flex-shrink-0" />}
title={skillPath}
borderClass="border-blue-500/20"
bgClass="bg-blue-500/5"
textClass="text-blue-400/80"
borderClass="border-blue-600/20"
bgClass="bg-blue-600/5"
textClass="text-blue-500/80"
/>
);
}
@@ -470,7 +470,7 @@ export function MessageList({ messages, loading }: MessageListProps) {
return (
<div
key={bIdx}
className="rounded-lg border border-amber-500/20 bg-amber-500/5 overflow-hidden"
className="rounded-lg border border-status-warning/20 bg-status-warning/5 overflow-hidden"
>
<button
onClick={() =>
@@ -481,23 +481,23 @@ export function MessageList({ messages, loading }: MessageListProps) {
return next;
})
}
className="w-full flex items-center gap-2 px-3 py-1.5 text-left hover:bg-amber-500/10 transition-colors"
className="w-full flex items-center gap-2 px-3 py-1.5 text-left hover:bg-status-warning/10 transition-colors"
>
<ChevronRight
className={`w-3.5 h-3.5 text-amber-500/60 transition-transform duration-150 ${
className={`w-3.5 h-3.5 text-status-warning/60 transition-transform duration-150 ${
isExpanded ? "rotate-90" : ""
}`}
/>
<Brain className="w-3.5 h-3.5 text-amber-400/80" />
<span className="text-xs text-amber-200/90 font-medium">Thinking</span>
<Brain className="w-3.5 h-3.5 text-status-warning/80" />
<span className="text-xs text-status-warning/90 font-medium">Thinking</span>
{!isExpanded && (
<span className="text-[10px] text-amber-300/40 font-mono ml-auto">
<span className="text-[10px] text-status-warning/40 font-mono ml-auto">
{block.text.length.toLocaleString()} chars
</span>
)}
</button>
{isExpanded && (
<div className="border-t border-amber-500/10 px-3 py-2 text-amber-100/80">
<div className="border-t border-status-warning/10 px-3 py-2 text-status-warning/80">
<MarkdownContent text={block.text} dense />
</div>
)}
@@ -138,7 +138,7 @@ function renderInput(toolUse: TranscriptContent) {
<div className="space-y-2">
<CodeBlock code={obj.command} lang="bash" label="Command" />
{typeof obj.description === "string" && (
<p className="text-xs text-gray-500 italic px-1">{obj.description}</p>
<p className="text-xs text-fg-muted italic px-1">{obj.description}</p>
)}
</div>
);
@@ -161,11 +161,11 @@ function renderInput(toolUse: TranscriptContent) {
const lang = langFromPath(obj.file_path);
return (
<div className="space-y-2">
<div className="flex items-center gap-1.5 text-xs text-gray-400">
<div className="flex items-center gap-1.5 text-xs text-fg-secondary">
<FileText className="w-3.5 h-3.5 text-violet-400" />
<span className="font-mono">{obj.file_path}</span>
{obj.replace_all === true && (
<span className="text-[10px] uppercase tracking-wider text-amber-300/80 bg-amber-500/10 border border-amber-500/20 rounded px-1.5 py-0.5">
<span className="text-[10px] uppercase tracking-wider text-status-warning/80 bg-status-warning/10 border border-status-warning/20 rounded px-1.5 py-0.5">
replace all
</span>
)}
@@ -183,11 +183,11 @@ function renderInput(toolUse: TranscriptContent) {
// Read: just show the path with offset/limit
if (tool === "read" && typeof obj.file_path === "string") {
return (
<div className="flex items-center gap-1.5 text-xs text-gray-300 bg-surface-4/40 border border-surface-3 rounded-md px-3 py-2">
<div className="flex items-center gap-1.5 text-xs text-fg-secondary bg-surface-4/40 border border-surface-3 rounded-md px-3 py-2">
<FileText className="w-3.5 h-3.5 text-sky-400 flex-shrink-0" />
<span className="font-mono break-all">{obj.file_path}</span>
{(typeof obj.offset === "number" || typeof obj.limit === "number") && (
<span className="text-gray-500 font-mono ml-auto flex-shrink-0">
<span className="text-fg-muted font-mono ml-auto flex-shrink-0">
{typeof obj.offset === "number" ? `:${obj.offset}` : ""}
{typeof obj.limit === "number" ? `+${obj.limit}` : ""}
</span>
@@ -201,7 +201,7 @@ function renderInput(toolUse: TranscriptContent) {
return (
<div className="space-y-1.5">
<div className="flex items-center gap-2 text-xs">
<span className="text-gray-500 font-mono uppercase tracking-wider text-[10px]">
<span className="text-fg-muted font-mono uppercase tracking-wider text-[10px]">
Pattern
</span>
<code className="font-mono text-cyan-300 bg-surface-4 border border-surface-3 rounded px-1.5 py-0.5">
@@ -210,18 +210,18 @@ function renderInput(toolUse: TranscriptContent) {
</div>
{typeof obj.path === "string" && (
<div className="flex items-center gap-2 text-xs">
<span className="text-gray-500 font-mono uppercase tracking-wider text-[10px]">
<span className="text-fg-muted font-mono uppercase tracking-wider text-[10px]">
Path
</span>
<code className="font-mono text-gray-300">{obj.path}</code>
<code className="font-mono text-fg-secondary">{obj.path}</code>
</div>
)}
{typeof obj.glob === "string" && (
<div className="flex items-center gap-2 text-xs">
<span className="text-gray-500 font-mono uppercase tracking-wider text-[10px]">
<span className="text-fg-muted font-mono uppercase tracking-wider text-[10px]">
Glob
</span>
<code className="font-mono text-gray-300">{obj.glob}</code>
<code className="font-mono text-fg-secondary">{obj.glob}</code>
</div>
)}
</div>
@@ -235,7 +235,7 @@ function renderInput(toolUse: TranscriptContent) {
/** Render the result pane: detect diff/json/text. */
function renderResult(toolResult: TranscriptContent, toolName: string) {
const text = toolResult.output ?? "";
if (text.length === 0) return <div className="text-xs text-gray-500 italic px-1">(empty)</div>;
if (text.length === 0) return <div className="text-xs text-fg-muted italic px-1">(empty)</div>;
const isError = !!toolResult.is_error;
const tool = toolName.toLowerCase();
@@ -269,8 +269,8 @@ export function ToolCallBlock({ toolUse, toolResult }: ToolCallBlockProps) {
const style = styleForTool(toolUse.name);
const Icon = style.Icon;
const wrapperBorder = isError ? "border-red-500/30" : style.border;
const wrapperBg = isError ? "bg-red-500/5" : "bg-surface-2/60";
const wrapperBorder = isError ? "border-status-danger/30" : style.border;
const wrapperBg = isError ? "bg-status-danger/5" : "bg-surface-2/60";
return (
<div
@@ -282,7 +282,7 @@ export function ToolCallBlock({ toolUse, toolResult }: ToolCallBlockProps) {
className="w-full flex items-center gap-2.5 px-3 py-2 text-left hover:bg-surface-3/40 transition-colors"
>
<ChevronRight
className={`w-3.5 h-3.5 text-gray-500 flex-shrink-0 transition-transform duration-150 ${
className={`w-3.5 h-3.5 text-fg-muted flex-shrink-0 transition-transform duration-150 ${
expanded ? "rotate-90" : ""
}`}
/>
@@ -295,23 +295,23 @@ export function ToolCallBlock({ toolUse, toolResult }: ToolCallBlockProps) {
{toolUse.name}
</span>
{summary && (
<span className="text-gray-500 text-xs font-mono truncate min-w-0" title={summary}>
<span className="text-fg-muted text-xs font-mono truncate min-w-0" title={summary}>
{summary}
</span>
)}
<span className="ml-auto flex-shrink-0">
{isError ? (
<span className="inline-flex items-center gap-1 text-[10px] uppercase tracking-wider text-red-300 bg-red-500/15 border border-red-500/20 rounded px-1.5 py-0.5">
<span className="inline-flex items-center gap-1 text-[10px] uppercase tracking-wider text-status-danger bg-status-danger/15 border border-status-danger/20 rounded px-1.5 py-0.5">
<AlertCircle className="w-3 h-3" />
error
</span>
) : hasResult ? (
<span className="inline-flex items-center gap-1 text-[10px] uppercase tracking-wider text-emerald-300/80 bg-emerald-500/10 border border-emerald-500/20 rounded px-1.5 py-0.5">
<span className="inline-flex items-center gap-1 text-[10px] uppercase tracking-wider text-status-success/80 bg-status-success/10 border border-status-success/20 rounded px-1.5 py-0.5">
<CheckCircle2 className="w-3 h-3" />
ok
</span>
) : (
<span className="text-gray-600 text-[10px] uppercase tracking-wider font-mono">
<span className="text-fg-muted text-[10px] uppercase tracking-wider font-mono">
pending
</span>
)}