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:
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user