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
+5 -5
View File
@@ -194,14 +194,14 @@ export function AgentCard({ agent, session, label, onClick }: AgentCardProps) {
{isMain ? <Bot className="w-3.5 h-3.5" /> : <GitBranch className="w-3.5 h-3.5" />}
</div>
<div className="min-w-0 overflow-hidden">
<p className="text-sm font-medium text-gray-200 truncate">
<p className="text-sm font-medium text-fg-secondary truncate">
{/* Auto-generated main-agent titles (e.g. "Main Agent - Session
229d93fd" or "Main Agent - work - e3f8e613") swap the
placeholder for the real session name when one exists; custom
(sub)agent names are left untouched. */}
{isMain ? mainAgentDisplayName(agent.name, realSessionName) : agent.name}
</p>
{subtitle && <p className="text-[11px] text-gray-500 truncate">{subtitle}</p>}
{subtitle && <p className="text-[11px] text-fg-muted truncate">{subtitle}</p>}
</div>
</div>
{/* compact: cards are narrow — inline reason chip would squeeze the
@@ -210,10 +210,10 @@ export function AgentCard({ agent, session, label, onClick }: AgentCardProps) {
</div>
{agent.task && (
<p className="text-xs text-gray-400 mb-3 line-clamp-2 leading-relaxed">{agent.task}</p>
<p className="text-xs text-fg-secondary mb-3 line-clamp-2 leading-relaxed">{agent.task}</p>
)}
<div className="flex items-center gap-3 text-[11px] text-gray-500 min-w-0 overflow-hidden flex-wrap">
<div className="flex items-center gap-3 text-[11px] text-fg-muted min-w-0 overflow-hidden flex-wrap">
{agent.current_tool && (
<span className="flex items-center gap-1 flex-shrink-0">
<Wrench className="w-3 h-3" />
@@ -243,7 +243,7 @@ export function AgentCard({ agent, session, label, onClick }: AgentCardProps) {
{t("ran")}
{formatDuration(agent.started_at, agent.ended_at)}
</span>
<span className="text-gray-600 flex-shrink-0">{timeAgo(agent.ended_at)}</span>
<span className="text-fg-muted flex-shrink-0">{timeAgo(agent.ended_at)}</span>
</>
) : (
<span className="flex items-center gap-1 flex-shrink-0">
+33 -33
View File
@@ -403,8 +403,8 @@ export function AlertsNotifications() {
onClick={() => setTab(tb.key)}
className={`inline-flex items-center gap-2 text-xs font-medium px-3.5 py-2 rounded-lg transition-colors ${
active
? "bg-surface-4 text-gray-100 shadow-sm"
: "text-gray-500 hover:text-gray-300 hover:bg-surface-3"
? "bg-surface-4 text-fg-primary shadow-sm"
: "text-fg-muted hover:text-fg-secondary hover:bg-surface-3"
}`}
>
<Icon className="w-3.5 h-3.5" />
@@ -413,10 +413,10 @@ export function AlertsNotifications() {
<span
className={`text-[10px] font-semibold rounded-full px-1.5 min-w-[18px] text-center ${
tb.key === "activity"
? "text-amber-300 bg-amber-500/15"
? "text-status-warning bg-status-warning/15"
: active
? "text-accent bg-accent/15"
: "text-gray-400 bg-surface-2"
: "text-fg-secondary bg-surface-2"
}`}
>
{tb.badge}
@@ -432,8 +432,8 @@ export function AlertsNotifications() {
<div className="card p-4">
<div className="flex items-center justify-between gap-3 mb-3">
<div>
<h4 className="text-sm font-semibold text-gray-200">{t("rules.title")}</h4>
<p className="text-xs text-gray-500 mt-0.5">{ts("alertsHub.rulesHint")}</p>
<h4 className="text-sm font-semibold text-fg-secondary">{t("rules.title")}</h4>
<p className="text-xs text-fg-muted mt-0.5">{ts("alertsHub.rulesHint")}</p>
</div>
<button
onClick={() => {
@@ -450,7 +450,7 @@ export function AlertsNotifications() {
{formOpen && (
<div className="rounded-lg border border-border bg-surface-2 p-3 mb-3 space-y-3">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<label className="block text-xs text-gray-400">
<label className="block text-xs text-fg-secondary">
<span className="inline-flex items-center gap-1">
{t("rules.form.name")}
<FieldHelp description={t("rules.help.name")} />
@@ -463,7 +463,7 @@ export function AlertsNotifications() {
className="input mt-1 w-full"
/>
</label>
<label className="block text-xs text-gray-400">
<label className="block text-xs text-fg-secondary">
<span className="inline-flex items-center gap-1">
{t("rules.form.type")}
<FieldHelp title={t("rules.form.type")} description={t("rules.help.type")} />
@@ -480,16 +480,16 @@ export function AlertsNotifications() {
</option>
))}
</select>
<ChevronDown className="w-3.5 h-3.5 absolute right-2.5 top-1/2 -translate-y-1/2 text-gray-500 pointer-events-none" />
<ChevronDown className="w-3.5 h-3.5 absolute right-2.5 top-1/2 -translate-y-1/2 text-fg-muted pointer-events-none" />
</div>
</label>
</div>
<p className="text-[11px] text-gray-500">{t(`ruleTypeHints.${form.rule_type}`)}</p>
<p className="text-[11px] text-fg-muted">{t(`ruleTypeHints.${form.rule_type}`)}</p>
{form.rule_type === "event_pattern" && (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<label className="block text-xs text-gray-400">
<label className="block text-xs text-fg-secondary">
<span className="inline-flex items-center gap-1">
{t("rules.form.eventType")}
<FieldHelp
@@ -506,7 +506,7 @@ export function AlertsNotifications() {
className="input mt-1 w-full"
/>
</label>
<label className="block text-xs text-gray-400">
<label className="block text-xs text-fg-secondary">
<span className="inline-flex items-center gap-1">
{t("rules.form.toolName")}
<FieldHelp
@@ -523,7 +523,7 @@ export function AlertsNotifications() {
className="input mt-1 w-full"
/>
</label>
<label className="block text-xs text-gray-400">
<label className="block text-xs text-fg-secondary">
<span className="inline-flex items-center gap-1">
{t("rules.form.summaryContains")}
<FieldHelp
@@ -540,7 +540,7 @@ export function AlertsNotifications() {
className="input mt-1 w-full"
/>
</label>
<label className="block text-xs text-gray-400">
<label className="block text-xs text-fg-secondary">
<span className="inline-flex items-center gap-1">
{t("rules.form.count")}
<FieldHelp description={t("rules.help.count")} />
@@ -554,7 +554,7 @@ export function AlertsNotifications() {
/>
</label>
{parseInt(form.count, 10) > 1 && (
<label className="block text-xs text-gray-400">
<label className="block text-xs text-fg-secondary">
<span className="inline-flex items-center gap-1">
{t("rules.form.windowMinutes")}
<FieldHelp description={t("rules.help.window")} />
@@ -574,7 +574,7 @@ export function AlertsNotifications() {
{(form.rule_type === "inactivity" || form.rule_type === "status_duration") && (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
{form.rule_type === "status_duration" && (
<label className="block text-xs text-gray-400">
<label className="block text-xs text-fg-secondary">
<span className="inline-flex items-center gap-1">
{t("rules.form.agentStatus")}
<FieldHelp description={t("rules.help.status")} />
@@ -588,11 +588,11 @@ export function AlertsNotifications() {
<option value="working">working</option>
<option value="waiting">waiting</option>
</select>
<ChevronDown className="w-3.5 h-3.5 absolute right-2.5 top-1/2 -translate-y-1/2 text-gray-500 pointer-events-none" />
<ChevronDown className="w-3.5 h-3.5 absolute right-2.5 top-1/2 -translate-y-1/2 text-fg-muted pointer-events-none" />
</div>
</label>
)}
<label className="block text-xs text-gray-400">
<label className="block text-xs text-fg-secondary">
<span className="inline-flex items-center gap-1">
{t("rules.form.minutes")}
<FieldHelp
@@ -616,7 +616,7 @@ export function AlertsNotifications() {
{form.rule_type === "token_threshold" && (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<label className="block text-xs text-gray-400">
<label className="block text-xs text-fg-secondary">
<span className="inline-flex items-center gap-1">
{t("rules.form.totalTokens")}
<FieldHelp description={t("rules.help.totalTokens")} />
@@ -633,7 +633,7 @@ export function AlertsNotifications() {
)}
<div className="flex flex-wrap items-end justify-between gap-3">
<label className="block text-xs text-gray-400">
<label className="block text-xs text-fg-secondary">
<span className="mb-1.5 flex items-center gap-1">
{t("rules.form.cooldown")}
<FieldHelp description={t("rules.help.cooldown")} />
@@ -655,7 +655,7 @@ export function AlertsNotifications() {
{saving ? t("rules.saving") : t("rules.create")}
</button>
</div>
{formError && <p className="text-xs text-red-400">{formError}</p>}
{formError && <p className="text-xs text-status-danger">{formError}</p>}
</div>
)}
@@ -680,7 +680,7 @@ export function AlertsNotifications() {
<div className="min-w-0">
<div className="flex items-center gap-2">
<span
className={`text-sm font-medium truncate ${rule.enabled ? "text-gray-200" : "text-gray-500 line-through"}`}
className={`text-sm font-medium truncate ${rule.enabled ? "text-fg-secondary" : "text-fg-muted line-through"}`}
>
{rule.name}
</span>
@@ -688,7 +688,7 @@ export function AlertsNotifications() {
{t(`ruleTypes.${rule.rule_type}`)}
</span>
</div>
<p className="text-xs text-gray-500 mt-0.5 truncate">
<p className="text-xs text-fg-muted mt-0.5 truncate">
{describeRule(rule, t)} ·{" "}
{t("rules.cooldown", { seconds: rule.cooldown_seconds })}
</p>
@@ -698,8 +698,8 @@ export function AlertsNotifications() {
onClick={() => onToggleRule(rule)}
className={`text-xs px-2.5 py-1.5 rounded-md border transition-colors ${
rule.enabled
? "border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/10"
: "border-border text-gray-500 hover:text-gray-300 hover:bg-surface-3"
? "border-status-success/30 text-status-success hover:bg-status-success/10"
: "border-border text-fg-muted hover:text-fg-secondary hover:bg-surface-3"
}`}
title={rule.enabled ? t("rules.disable") : t("rules.enable")}
>
@@ -707,7 +707,7 @@ export function AlertsNotifications() {
</button>
<button
onClick={() => setConfirmRule(rule)}
className="p-1.5 rounded-md text-gray-500 hover:text-red-400 hover:bg-red-500/10 transition-colors"
className="p-1.5 rounded-md text-fg-muted hover:text-status-danger hover:bg-status-danger/10 transition-colors"
title={t("rules.delete")}
aria-label={t("rules.delete")}
>
@@ -728,10 +728,10 @@ export function AlertsNotifications() {
{tab === "activity" && (
<div className="card p-4">
<div className="flex flex-wrap items-center justify-between gap-3 mb-3">
<h4 className="text-sm font-semibold text-gray-200">
<h4 className="text-sm font-semibold text-fg-secondary">
{t("feed.title")}
{unacked > 0 && (
<span className="ml-2 text-[10px] font-semibold text-amber-300 bg-amber-500/10 border border-amber-500/30 rounded-full px-2 py-0.5">
<span className="ml-2 text-[10px] font-semibold text-status-warning bg-status-warning/10 border border-status-warning/30 rounded-full px-2 py-0.5">
{t("feed.unackedCount", { count: unacked })}
</span>
)}
@@ -782,17 +782,17 @@ export function AlertsNotifications() {
className={`flex flex-wrap items-center justify-between gap-3 rounded-lg border px-3 py-2.5 ${
alert.acknowledged_at
? "border-border bg-surface-2 opacity-70"
: "border-amber-500/30 bg-amber-500/5"
: "border-status-warning/30 bg-status-warning/5"
}`}
>
<div className="min-w-0">
<div className="flex items-center gap-2">
<BellRing
className={`w-3.5 h-3.5 flex-shrink-0 ${alert.acknowledged_at ? "text-gray-500" : "text-amber-400"}`}
className={`w-3.5 h-3.5 flex-shrink-0 ${alert.acknowledged_at ? "text-fg-muted" : "text-status-warning"}`}
/>
<span className="text-sm text-gray-200 truncate">{alert.message}</span>
<span className="text-sm text-fg-secondary truncate">{alert.message}</span>
</div>
<p className="text-[11px] text-gray-500 mt-0.5 font-mono">
<p className="text-[11px] text-fg-muted mt-0.5 font-mono">
{timeAgo(alert.triggered_at)} · {alert.rule_name}
{alert.session_id && (
<>
@@ -810,7 +810,7 @@ export function AlertsNotifications() {
{!alert.acknowledged_at && (
<button
onClick={() => onAck(alert.id)}
className="inline-flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md border border-border text-gray-300 hover:text-gray-100 hover:bg-surface-3 transition-colors flex-shrink-0"
className="inline-flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md border border-border text-fg-secondary hover:text-fg-primary hover:bg-surface-3 transition-colors flex-shrink-0"
>
<Check className="w-3.5 h-3.5" />
{t("feed.ack")}
+3 -1
View File
@@ -105,7 +105,9 @@ export function Checkbox({ checked, onChange, label, className, labelClassName }
{checked && <Check className="h-3 w-3 text-white" strokeWidth={3} />}
</span>
{label != null && (
<span className={labelClassName ?? "text-xs text-gray-400 group-hover:text-gray-300"}>
<span
className={labelClassName ?? "text-xs text-fg-secondary group-hover:text-fg-secondary"}
>
{label}
</span>
)}
+6 -6
View File
@@ -133,16 +133,16 @@ export function ConfirmModal({
>
<div className="flex items-start gap-3 p-5">
{destructive && (
<div className="w-9 h-9 rounded-lg bg-red-500/10 border border-red-500/20 flex items-center justify-center flex-shrink-0">
<AlertTriangle className="w-4.5 h-4.5 text-red-400" />
<div className="w-9 h-9 rounded-lg bg-status-danger/10 border border-status-danger/20 flex items-center justify-center flex-shrink-0">
<AlertTriangle className="w-4.5 h-4.5 text-status-danger" />
</div>
)}
<div className="min-w-0 flex-1">
<h3 id={titleId} className="text-sm font-semibold text-gray-100">
<h3 id={titleId} className="text-sm font-semibold text-fg-primary">
{title}
</h3>
{message && (
<p id={messageId} className="text-xs text-gray-400 mt-1 leading-relaxed">
<p id={messageId} className="text-xs text-fg-secondary mt-1 leading-relaxed">
{message}
</p>
)}
@@ -151,7 +151,7 @@ export function ConfirmModal({
<button
type="button"
onClick={onCancel}
className="text-gray-500 hover:text-gray-300 p-1 -mt-1 -mr-1"
className="text-fg-muted hover:text-fg-secondary p-1 -mt-1 -mr-1"
aria-label={cancelLabel}
>
<X className="w-4 h-4" />
@@ -172,7 +172,7 @@ export function ConfirmModal({
disabled={busy || disabled}
className={`inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-md transition-colors disabled:opacity-50 ${
destructive
? "text-red-200 bg-red-500/15 border border-red-500/30 hover:bg-red-500/25"
? "text-status-danger bg-status-danger/15 border border-status-danger/30 hover:bg-status-danger/25"
: "btn-primary"
}`}
>
+10 -10
View File
@@ -180,7 +180,7 @@ export function DateTimePicker({
? "bg-accent text-white font-medium"
: isToday
? "bg-surface-3 text-accent font-medium"
: "hover:bg-surface-2 text-gray-300 hover:text-white"
: "hover:bg-surface-2 text-fg-secondary hover:text-white"
}`}
>
{i}
@@ -202,12 +202,12 @@ export function DateTimePicker({
title={title}
className={`flex items-center gap-2 bg-surface-2 border ${isOpen ? "border-accent" : "border-border"} rounded px-2 py-1.5 min-w-[150px] text-xs focus:outline-none focus:border-accent transition-colors w-full text-left`}
>
<Calendar className="w-3.5 h-3.5 text-gray-400 shrink-0" />
<span className={`flex-1 truncate ${!dateObj ? "text-gray-500" : "text-gray-200"}`}>
<Calendar className="w-3.5 h-3.5 text-fg-secondary shrink-0" />
<span className={`flex-1 truncate ${!dateObj ? "text-fg-muted" : "text-fg-secondary"}`}>
{dateObj ? formatDisplay(dateObj) : placeholder}
</span>
{dateObj && (
<X className="w-3 h-3 text-gray-500 hover:text-white shrink-0" onClick={clearValue} />
<X className="w-3 h-3 text-fg-muted hover:text-white shrink-0" onClick={clearValue} />
)}
</button>
@@ -222,11 +222,11 @@ export function DateTimePicker({
onClick={() =>
setViewDate(new Date(viewDate.getFullYear(), viewDate.getMonth() - 1, 1))
}
className="p-1 hover:bg-surface-2 rounded text-gray-400 hover:text-white"
className="p-1 hover:bg-surface-2 rounded text-fg-secondary hover:text-white"
>
<ChevronLeft className="w-4 h-4" />
</button>
<span className="text-xs font-medium text-gray-200">
<span className="text-xs font-medium text-fg-secondary">
{viewDate.toLocaleString(undefined, { month: "long", year: "numeric" })}
</span>
<button
@@ -234,7 +234,7 @@ export function DateTimePicker({
onClick={() =>
setViewDate(new Date(viewDate.getFullYear(), viewDate.getMonth() + 1, 1))
}
className="p-1 hover:bg-surface-2 rounded text-gray-400 hover:text-white"
className="p-1 hover:bg-surface-2 rounded text-fg-secondary hover:text-white"
>
<ChevronRight className="w-4 h-4" />
</button>
@@ -244,7 +244,7 @@ export function DateTimePicker({
<div>
<div className="grid grid-cols-7 gap-1 mb-1">
{["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"].map((day) => (
<div key={day} className="w-6 text-center text-[10px] font-medium text-gray-500">
<div key={day} className="w-6 text-center text-[10px] font-medium text-fg-muted">
{day}
</div>
))}
@@ -254,7 +254,7 @@ export function DateTimePicker({
{/* Time Picker */}
<div className="pt-3 border-t border-border flex items-center justify-between">
<div className="flex items-center gap-1.5 text-gray-400">
<div className="flex items-center gap-1.5 text-fg-secondary">
<Clock className="w-3.5 h-3.5" />
<span className="text-[11px] font-medium">Time</span>
</div>
@@ -262,7 +262,7 @@ export function DateTimePicker({
type="time"
value={timeValue}
onChange={handleTimeChange}
className="bg-surface-2 border border-border rounded px-2 py-1 text-xs text-gray-200 focus:outline-none focus:border-accent w-[85px] time-input-custom"
className="bg-surface-2 border border-border rounded px-2 py-1 text-xs text-fg-secondary focus:outline-none focus:border-accent w-[85px] time-input-custom"
/>
</div>
</div>
+3 -3
View File
@@ -91,10 +91,10 @@ export function EmptyState({ icon: Icon, title, description, action }: EmptyStat
return (
<div className="flex flex-col items-center justify-center py-20 text-center">
<div className="w-14 h-14 rounded-2xl bg-surface-4 flex items-center justify-center mb-5">
<Icon className="w-6 h-6 text-gray-500" />
<Icon className="w-6 h-6 text-fg-muted" />
</div>
<h3 className="text-base font-medium text-gray-300 mb-2">{title}</h3>
<p className="text-sm text-gray-500 max-w-md mb-6">{description}</p>
<h3 className="text-base font-medium text-fg-secondary mb-2">{title}</h3>
<p className="text-sm text-fg-muted max-w-md mb-6">{description}</p>
{action}
</div>
);
+13 -13
View File
@@ -284,7 +284,7 @@ function SummaryBlock({
return (
<div className="border border-border rounded overflow-hidden bg-surface-3/30">
<div className="px-3 py-1 border-b border-border bg-black/20">
<span className="text-gray-500 text-[10px] uppercase tracking-wide font-semibold">
<span className="text-fg-muted text-[10px] uppercase tracking-wide font-semibold">
{t("eventDetail.summary")}
</span>
</div>
@@ -293,19 +293,19 @@ function SummaryBlock({
<span className="text-base leading-none" aria-hidden="true">
{summary.icon}
</span>
<span className="text-[12px] text-gray-100 font-medium break-words">
<span className="text-[12px] text-fg-primary font-medium break-words">
{summary.headline}
</span>
</div>
{summary.bullets.length > 0 && (
<ul className="list-disc pl-6 space-y-0.5 text-[11px] text-gray-400">
<ul className="list-disc pl-6 space-y-0.5 text-[11px] text-fg-secondary">
{summary.bullets.map((b, i) => (
<li key={i}>{b}</li>
))}
</ul>
)}
{hint && (
<div className="text-[11px] text-gray-500 italic pt-1 border-t border-border/40">
<div className="text-[11px] text-fg-muted italic pt-1 border-t border-border/40">
{hint}
</div>
)}
@@ -337,7 +337,7 @@ function FieldRow({
if (view) {
return (
<div className="grid grid-cols-[160px_1fr] gap-x-4 items-start text-[11px]">
<div className="text-gray-500 font-mono pt-2">{label}</div>
<div className="text-fg-muted font-mono pt-2">{label}</div>
<div>{view}</div>
</div>
);
@@ -348,7 +348,7 @@ function FieldRow({
if (view) {
return (
<div className="grid grid-cols-[160px_1fr] gap-x-4 items-start text-[11px]">
<div className="text-gray-500 font-mono pt-2">{label}</div>
<div className="text-fg-muted font-mono pt-2">{label}</div>
<div>{view}</div>
</div>
);
@@ -358,8 +358,8 @@ function FieldRow({
if (isInlineScalar(value)) {
return (
<div className="grid grid-cols-[160px_1fr] gap-x-4 items-start text-[11px]">
<div className="text-gray-500 font-mono pt-0.5">{label}</div>
<div className="text-gray-300 font-mono break-all">
<div className="text-fg-muted font-mono pt-0.5">{label}</div>
<div className="text-fg-secondary font-mono break-all">
<ScalarValue value={value} />
</div>
</div>
@@ -368,7 +368,7 @@ function FieldRow({
return (
<div className="grid grid-cols-[160px_1fr] gap-x-4 items-start text-[11px]">
<div className="text-gray-500 font-mono pt-2">{label}</div>
<div className="text-fg-muted font-mono pt-2">{label}</div>
<CodeView value={value} />
</div>
);
@@ -382,11 +382,11 @@ function isInlineScalar(value: unknown): boolean {
}
function ScalarValue({ value }: { value: unknown }) {
if (value == null) return <span className="text-gray-500 italic">null</span>;
if (value == null) return <span className="text-fg-muted italic">null</span>;
if (typeof value === "boolean") {
const color = value
? "text-green-400 border-green-500/30 bg-green-500/10"
: "text-gray-400 border-gray-500/30 bg-gray-500/10";
: "text-fg-secondary border-border-light/30 bg-surface-4/10";
return (
<span className={`inline-block px-2 py-0.5 rounded border ${color}`}>{String(value)}</span>
);
@@ -402,12 +402,12 @@ function CodeView({ value }: { value: unknown }) {
return (
<div className="relative bg-black/70 border border-border rounded font-mono text-[11px] overflow-hidden">
<div className="flex items-center justify-between px-3 py-1.5 border-b border-border bg-black/40">
<span className="text-gray-500 text-[10px] uppercase tracking-wide">
<span className="text-fg-muted text-[10px] uppercase tracking-wide">
{typeof value === "string" ? "text" : Array.isArray(value) ? "array" : "json"}
</span>
<CopyButton text={text} />
</div>
<pre className="px-3 py-2 text-gray-200 whitespace-pre-wrap break-words max-h-96 overflow-auto">
<pre className="px-3 py-2 text-fg-secondary whitespace-pre-wrap break-words max-h-96 overflow-auto">
{text}
</pre>
</div>
+8 -8
View File
@@ -232,16 +232,16 @@ export function EventFilters({
return (
<div className="card p-3 space-y-2">
<div className="flex flex-wrap items-center gap-2">
<Filter className="w-3.5 h-3.5 text-gray-500 flex-shrink-0" />
<Filter className="w-3.5 h-3.5 text-fg-muted flex-shrink-0" />
<div className="relative flex-1 min-w-[180px]">
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-gray-500" />
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-fg-muted" />
<input
type="text"
value={searchDraft}
onChange={(e) => setSearchDraft(e.target.value)}
placeholder={t("eventFilters.searchPlaceholder")}
aria-label={t("eventFilters.searchPlaceholder")}
className="w-full bg-surface-2 border border-border rounded pl-7 pr-2 py-1.5 text-xs text-gray-200 placeholder-gray-500 focus:outline-none focus:border-accent"
className="w-full bg-surface-2 border border-border rounded pl-7 pr-2 py-1.5 text-xs text-fg-secondary placeholder-fg-muted focus:outline-none focus:border-accent"
/>
</div>
<DateTimePicker
@@ -251,7 +251,7 @@ export function EventFilters({
title={t("eventFilters.from")}
placeholder={t("eventFilters.from")}
/>
<span className="text-xs text-gray-600"></span>
<span className="text-xs text-fg-muted"></span>
<DateTimePicker
value={value.to}
onChange={(val: string) => onChange({ ...value, to: val })}
@@ -263,7 +263,7 @@ export function EventFilters({
<button
type="button"
onClick={() => onChange(EMPTY_FILTERS)}
className="flex items-center gap-1 text-[11px] px-2 py-1 rounded text-gray-400 hover:text-gray-200 hover:bg-surface-2 cursor-pointer"
className="flex items-center gap-1 text-[11px] px-2 py-1 rounded text-fg-secondary hover:text-fg-primary hover:bg-surface-2 cursor-pointer"
aria-label={t("eventFilters.clearAll")}
>
<X className="w-3 h-3" />
@@ -359,7 +359,7 @@ function ChipGroup({
className={`text-[11px] px-2 py-1 rounded border cursor-pointer flex items-center gap-1.5 ${
selectedCount > 0
? "border-accent/40 bg-accent/10 text-accent"
: "border-border bg-surface-2 text-gray-400 hover:text-gray-200"
: "border-border bg-surface-2 text-fg-secondary hover:text-fg-primary"
}`}
>
<span>{label}</span>
@@ -374,7 +374,7 @@ function ChipGroup({
className="absolute left-0 mt-1 z-20 min-w-[220px] max-h-64 overflow-auto bg-surface-1 border border-border rounded shadow-xl p-1.5"
>
{options.length === 0 ? (
<p className="text-[11px] text-gray-500 px-2 py-1 italic">
<p className="text-[11px] text-fg-muted px-2 py-1 italic">
{t("eventFilters.noOptions")}
</p>
) : (
@@ -384,7 +384,7 @@ function ChipGroup({
return (
<label
key={opt}
className="flex items-center gap-2 px-2 py-1 text-[11px] text-gray-300 rounded hover:bg-surface-3 cursor-pointer"
className="flex items-center gap-2 px-2 py-1 text-[11px] text-fg-secondary rounded hover:bg-surface-3 cursor-pointer"
>
<input
type="checkbox"
+14 -12
View File
@@ -71,17 +71,17 @@ export function EventFiltersInfo() {
const { t } = useTranslation("common");
return (
<details className="card bg-surface-2/40 border border-border rounded overflow-hidden">
<summary className="cursor-pointer select-none px-3 py-2 flex items-center text-[11px] text-gray-400 hover:text-gray-200 hover:bg-surface-2/80">
<summary className="cursor-pointer select-none px-3 py-2 flex items-center text-[11px] text-fg-secondary hover:text-fg-primary hover:bg-surface-2/80">
<Info className="w-3.5 h-3.5 mr-2" />
<span className="font-semibold uppercase tracking-wide mr-1.5">
{t("eventFilters.help.title")}
</span>
<span className="text-gray-500 font-normal">- {t("eventFilters.help.subtitle")}</span>
<span className="text-fg-muted font-normal">- {t("eventFilters.help.subtitle")}</span>
</summary>
<div className="divide-y divide-border">
<Section title={t("eventFilters.help.statusesTitle")}>
<p className="text-[11px] text-gray-500 mb-2">{t("eventFilters.help.statusesIntro")}</p>
<p className="text-[11px] text-fg-muted mb-2">{t("eventFilters.help.statusesIntro")}</p>
<dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1.5 text-[11px]">
<dt>
<AgentStatusBadge status="working" />
@@ -103,17 +103,19 @@ export function EventFiltersInfo() {
</Section>
<Section title={t("eventFilters.help.lifecycleTitle")}>
<p className="text-[11px] text-gray-400 mb-2">{t("eventFilters.help.lifecycleDesc")}</p>
<code className="block bg-black/40 border border-border rounded p-2 text-[11px] font-mono text-gray-300 whitespace-pre-wrap">
<p className="text-[11px] text-fg-secondary mb-2">
{t("eventFilters.help.lifecycleDesc")}
</p>
<code className="block bg-black/40 border border-border rounded p-2 text-[11px] font-mono text-fg-secondary whitespace-pre-wrap">
{t("eventFilters.help.lifecycleFlow")}
</code>
</Section>
<Section title={t("eventFilters.help.filtersTitle")}>
<ul className="list-disc pl-5 space-y-1 text-[11px] text-gray-400">
<ul className="list-disc pl-5 space-y-1 text-[11px] text-fg-secondary">
<li>{t("eventFilters.help.filterTip1")}</li>
<li>{t("eventFilters.help.filterTip2")}</li>
<li className="text-amber-300/90">{t("eventFilters.help.filterTipGrouping")}</li>
<li className="text-status-warning/90">{t("eventFilters.help.filterTipGrouping")}</li>
<li>{t("eventFilters.help.filterTip3")}</li>
<li>{t("eventFilters.help.filterTip4")}</li>
</ul>
@@ -157,9 +159,9 @@ export function EventFiltersInfo() {
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<details className="group" open>
<summary className="cursor-pointer select-none px-3 py-1.5 text-[11px] text-gray-300 hover:bg-surface-2/60 flex items-center gap-2">
<span className="text-gray-500 transition-transform group-open:rotate-90"></span>
<span className="font-semibold uppercase tracking-wide text-gray-400">{title}</span>
<summary className="cursor-pointer select-none px-3 py-1.5 text-[11px] text-fg-secondary hover:bg-surface-2/60 flex items-center gap-2">
<span className="text-fg-muted transition-transform group-open:rotate-90"></span>
<span className="font-semibold uppercase tracking-wide text-fg-secondary">{title}</span>
</summary>
<div className="px-3 pb-3 pt-1">{children}</div>
</details>
@@ -170,8 +172,8 @@ function Section({ title, children }: { title: string; children: React.ReactNode
function Field({ label, desc }: { label: string; desc: string }) {
return (
<>
<dt className="font-semibold text-gray-300 whitespace-nowrap">{label}</dt>
<dd className="text-gray-400">{desc}</dd>
<dt className="font-semibold text-fg-secondary whitespace-nowrap">{label}</dt>
<dd className="text-fg-secondary">{desc}</dd>
</>
);
}
+5 -5
View File
@@ -136,7 +136,7 @@ export function FieldHelp({ title, description, examples, note }: FieldHelpProps
e.preventDefault();
setOpen((v) => !v);
}}
className="text-gray-500 hover:text-gray-300 transition-colors"
className="text-fg-muted hover:text-fg-secondary transition-colors"
>
<HelpCircle className="w-3.5 h-3.5" />
</button>
@@ -154,11 +154,11 @@ export function FieldHelp({ title, description, examples, note }: FieldHelpProps
}}
className="rounded-lg border border-border bg-surface-1 shadow-xl shadow-black/40 p-3 w-[300px] pointer-events-none"
>
{title && <p className="text-xs font-semibold text-gray-200 mb-1">{title}</p>}
<p className="text-[11px] leading-relaxed text-gray-400">{description}</p>
{title && <p className="text-xs font-semibold text-fg-secondary mb-1">{title}</p>}
<p className="text-[11px] leading-relaxed text-fg-secondary">{description}</p>
{examples && examples.length > 0 && (
<div className="mt-2">
<p className="text-[10px] uppercase tracking-wider text-gray-600 mb-1">
<p className="text-[10px] uppercase tracking-wider text-fg-muted mb-1">
{t("examples")}
</p>
<div className="flex flex-wrap gap-1">
@@ -173,7 +173,7 @@ export function FieldHelp({ title, description, examples, note }: FieldHelpProps
</div>
</div>
)}
{note && <p className="text-[10px] text-gray-500 mt-2 leading-relaxed">{note}</p>}
{note && <p className="text-[10px] text-fg-muted mt-2 leading-relaxed">{note}</p>}
</div>,
document.body
)}
+61 -61
View File
@@ -285,12 +285,12 @@ export function ImportHistory() {
return (
<section>
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2 mb-1">
<History className="w-4 h-4 text-gray-500" />
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
<History className="w-4 h-4 text-fg-muted" />
{t("import.title")}
</h3>
<p className="text-xs text-gray-500 mb-1">{t("import.description")}</p>
<p className="text-[11px] text-gray-600 italic mb-4 leading-snug">{t("cursorPathsNote")}</p>
<p className="text-xs text-fg-muted mb-1">{t("import.description")}</p>
<p className="text-[11px] text-fg-muted italic mb-4 leading-snug">{t("cursorPathsNote")}</p>
<div className="card p-5 space-y-5">
{/* Step-by-step instructions */}
@@ -299,33 +299,33 @@ export function ImportHistory() {
onClick={() => setInstructionsOpen((v) => !v)}
className="w-full flex items-center justify-between px-4 py-3 bg-surface-2 hover:bg-surface-3 transition-colors"
>
<span className="flex items-center gap-2 text-xs font-semibold text-gray-300 uppercase tracking-wider">
<ListChecks className="w-3.5 h-3.5 text-blue-400" />
<span className="flex items-center gap-2 text-xs font-semibold text-fg-secondary uppercase tracking-wider">
<ListChecks className="w-3.5 h-3.5 text-blue-500" />
{t("import.instructions")}
</span>
<span className="text-[11px] text-gray-500">{instructionsOpen ? "▾" : "▸"}</span>
<span className="text-[11px] text-fg-muted">{instructionsOpen ? "▾" : "▸"}</span>
</button>
{instructionsOpen && (
<div className="px-4 py-4 space-y-4 text-sm text-gray-300 bg-surface-1">
<div className="px-4 py-4 space-y-4 text-sm text-fg-secondary bg-surface-1">
{/* Default location card */}
{guide && (
<div className="flex flex-wrap items-center gap-2 text-xs bg-surface-2 border border-border rounded-md px-3 py-2">
<HardDrive className="w-3.5 h-3.5 text-gray-500 flex-shrink-0" />
<span className="text-gray-400">{t("import.defaultLocation")}:</span>
<code className="font-mono text-gray-200 truncate">
<HardDrive className="w-3.5 h-3.5 text-fg-muted flex-shrink-0" />
<span className="text-fg-secondary">{t("import.defaultLocation")}:</span>
<code className="font-mono text-fg-secondary truncate">
{guide.default_projects_dir_display}
</code>
{guide.default_projects_dir_exists ? (
<span className="inline-flex items-center gap-1 text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 px-2 py-0.5 rounded-full">
<span className="inline-flex items-center gap-1 text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full">
<CheckCircle2 className="w-3 h-3" />
{t("import.locationFound")}
<span className="text-gray-500 ml-1">
<span className="text-fg-muted ml-1">
· {guide.default_projects_dir_stats.projects} {t("import.projectsLabel")},{" "}
{guide.default_projects_dir_stats.jsonl_files} {t("import.jsonlLabel")}
</span>
</span>
) : (
<span className="inline-flex items-center gap-1 text-amber-400 bg-amber-500/10 border border-amber-500/20 px-2 py-0.5 rounded-full">
<span className="inline-flex items-center gap-1 text-status-warning bg-status-warning/10 border border-status-warning/20 px-2 py-0.5 rounded-full">
<AlertTriangle className="w-3 h-3" />
{t("import.locationMissing")}
</span>
@@ -339,13 +339,13 @@ export function ImportHistory() {
<Step title={t("import.stepArchive")} body={t("import.stepArchiveBody")}>
{guide && (
<div className="mt-2 flex items-center gap-2 bg-surface-2 border border-border rounded-md px-3 py-2">
<Terminal className="w-3.5 h-3.5 text-gray-500 flex-shrink-0" />
<code className="flex-1 text-xs font-mono text-gray-200 truncate">
<Terminal className="w-3.5 h-3.5 text-fg-muted flex-shrink-0" />
<code className="flex-1 text-xs font-mono text-fg-secondary truncate">
{guide.archive_command}
</code>
<button
onClick={copyArchiveCmd}
className="text-xs text-gray-400 hover:text-gray-200 flex items-center gap-1 flex-shrink-0"
className="text-xs text-fg-secondary hover:text-fg-primary flex items-center gap-1 flex-shrink-0"
>
{copied ? (
<>
@@ -364,7 +364,7 @@ export function ImportHistory() {
<Step title={t("import.stepVerify")} body={t("import.stepVerifyBody")} />
</div>
<div className="text-[11px] text-gray-500 flex items-start gap-2 pt-2 border-t border-border">
<div className="text-[11px] text-fg-muted flex items-start gap-2 pt-2 border-t border-border">
<Info className="w-3 h-3 mt-0.5 flex-shrink-0" />
<span>{t("import.accuracyNote")}</span>
</div>
@@ -409,8 +409,8 @@ export function ImportHistory() {
{mode === "rescan" && (
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-3 min-w-0">
<FolderOpen className="w-4 h-4 text-gray-500 flex-shrink-0" />
<code className="font-mono text-xs text-gray-300 truncate">
<FolderOpen className="w-4 h-4 text-fg-muted flex-shrink-0" />
<code className="font-mono text-xs text-fg-secondary truncate">
{guide?.default_projects_dir_display || "~/.claude/projects"}
</code>
</div>
@@ -440,7 +440,7 @@ export function ImportHistory() {
className="input w-full text-sm font-mono"
spellCheck={false}
/>
<p className="text-[11px] text-gray-500 mt-1.5">{t("import.folderHelper")}</p>
<p className="text-[11px] text-fg-muted mt-1.5">{t("import.folderHelper")}</p>
</div>
<div className="flex justify-end">
<button
@@ -475,13 +475,13 @@ export function ImportHistory() {
}}
className={`border-2 border-dashed rounded-lg px-4 py-8 text-center cursor-pointer transition-colors ${
dragging
? "border-blue-400 bg-blue-500/5"
: "border-border hover:border-gray-500 bg-surface-1"
? "border-blue-500 bg-blue-600/5"
: "border-border hover:border-border-light bg-surface-1"
}`}
>
<UploadCloud className="w-6 h-6 text-gray-500 mx-auto mb-2" />
<p className="text-sm text-gray-300">{t("import.dropzoneHint")}</p>
<p className="text-[11px] text-gray-500 mt-1">{t("import.dropzoneSub")}</p>
<UploadCloud className="w-6 h-6 text-fg-muted mx-auto mb-2" />
<p className="text-sm text-fg-secondary">{t("import.dropzoneHint")}</p>
<p className="text-[11px] text-fg-muted mt-1">{t("import.dropzoneSub")}</p>
<input
ref={fileInputRef}
type="file"
@@ -493,17 +493,17 @@ export function ImportHistory() {
</div>
{files.length > 0 && (
<div className="flex flex-wrap items-center justify-between gap-2 text-xs bg-surface-3 rounded-md px-3 py-2">
<span className="text-gray-400">
<FileArchive className="w-3.5 h-3.5 inline mr-1.5 text-gray-500" />
<span className="text-fg-secondary">
<FileArchive className="w-3.5 h-3.5 inline mr-1.5 text-fg-muted" />
{t("import.filesSelected", { count: files.length })}
<span className="text-gray-600 ml-2">({formatBytes(totalSize)})</span>
<span className="text-fg-muted ml-2">({formatBytes(totalSize)})</span>
</span>
<button
onClick={() => {
setFiles([]);
if (fileInputRef.current) fileInputRef.current.value = "";
}}
className="text-gray-500 hover:text-gray-300 text-[11px]"
className="text-fg-muted hover:text-fg-secondary text-[11px]"
>
{t("import.clearSelection")}
</button>
@@ -543,13 +543,13 @@ export function ImportHistory() {
}}
className={`border-2 border-dashed rounded-lg px-4 py-8 text-center cursor-pointer transition-colors ${
dragging
? "border-blue-400 bg-blue-500/5"
: "border-border hover:border-gray-500 bg-surface-1"
? "border-blue-500 bg-blue-600/5"
: "border-border hover:border-border-light bg-surface-1"
}`}
>
<DatabaseBackup className="w-6 h-6 text-gray-500 mx-auto mb-2" />
<p className="text-sm text-gray-300">{t("import.backupHint")}</p>
<p className="text-[11px] text-gray-500 mt-1">{t("import.backupSub")}</p>
<DatabaseBackup className="w-6 h-6 text-fg-muted mx-auto mb-2" />
<p className="text-sm text-fg-secondary">{t("import.backupHint")}</p>
<p className="text-[11px] text-fg-muted mt-1">{t("import.backupSub")}</p>
<input
ref={backupInputRef}
type="file"
@@ -560,17 +560,17 @@ export function ImportHistory() {
</div>
{backupFile && (
<div className="flex flex-wrap items-center justify-between gap-2 text-xs bg-surface-3 rounded-md px-3 py-2">
<span className="text-gray-400 min-w-0">
<FileArchive className="w-3.5 h-3.5 inline mr-1.5 text-gray-500" />
<span className="text-fg-secondary min-w-0">
<FileArchive className="w-3.5 h-3.5 inline mr-1.5 text-fg-muted" />
<span className="font-mono truncate">{backupFile.name}</span>
<span className="text-gray-600 ml-2">({formatBytes(backupFile.size)})</span>
<span className="text-fg-muted ml-2">({formatBytes(backupFile.size)})</span>
</span>
<button
onClick={() => {
setBackupFile(null);
if (backupInputRef.current) backupInputRef.current.value = "";
}}
className="text-gray-500 hover:text-gray-300 text-[11px]"
className="text-fg-muted hover:text-fg-secondary text-[11px]"
>
{t("import.clearSelection")}
</button>
@@ -596,11 +596,11 @@ export function ImportHistory() {
{/* In-flight progress */}
{running && progressText && (
<div className="flex items-center gap-2 text-xs text-gray-400 bg-surface-2 border border-border rounded-md px-3 py-2">
<Loader2 className="w-3.5 h-3.5 animate-spin text-blue-400 flex-shrink-0" />
<div className="flex items-center gap-2 text-xs text-fg-secondary bg-surface-2 border border-border rounded-md px-3 py-2">
<Loader2 className="w-3.5 h-3.5 animate-spin text-blue-500 flex-shrink-0" />
<span className="truncate">{progressText}</span>
{progress?.current && (
<code className="font-mono text-[11px] text-gray-600 truncate">
<code className="font-mono text-[11px] text-fg-muted truncate">
· {progress.current.split("/").slice(-2).join("/")}
</code>
)}
@@ -609,7 +609,7 @@ export function ImportHistory() {
{/* Errors */}
{errorMsg && (
<div className="flex items-start gap-2 text-xs text-red-400 bg-red-500/10 border border-red-500/20 rounded-md px-3 py-2">
<div className="flex items-start gap-2 text-xs text-status-danger bg-status-danger/10 border border-status-danger/20 rounded-md px-3 py-2">
<XCircle className="w-3.5 h-3.5 mt-0.5 flex-shrink-0" />
<span>{errorMsg}</span>
</div>
@@ -617,8 +617,8 @@ export function ImportHistory() {
{/* Result summary */}
{result && !running && (
<div className="border border-emerald-500/20 bg-emerald-500/5 rounded-lg px-4 py-3 space-y-2">
<div className="flex items-center gap-2 text-xs font-semibold text-emerald-400 uppercase tracking-wider">
<div className="border border-status-success/20 bg-status-success/5 rounded-lg px-4 py-3 space-y-2">
<div className="flex items-center gap-2 text-xs font-semibold text-status-success uppercase tracking-wider">
<CheckCircle2 className="w-3.5 h-3.5" />
{t("import.result.title")}
</div>
@@ -626,26 +626,26 @@ export function ImportHistory() {
<ResultStat
label={t("import.result.imported", { count: result.imported })}
value={result.imported}
color="text-emerald-300"
color="text-status-success"
/>
<ResultStat
label={t("import.result.backfilled", { count: result.backfilled ?? 0 })}
value={result.backfilled ?? 0}
color="text-blue-300"
color="text-blue-400"
/>
<ResultStat
label={t("import.result.skipped", { count: result.skipped })}
value={result.skipped}
color="text-gray-400"
color="text-fg-secondary"
/>
<ResultStat
label={t("import.result.errors", { count: result.errors })}
value={result.errors}
color={result.errors > 0 ? "text-red-300" : "text-gray-500"}
color={result.errors > 0 ? "text-status-danger" : "text-fg-muted"}
/>
</div>
{typeof result.files_scanned === "number" && (
<p className="text-[11px] text-gray-500">
<p className="text-[11px] text-fg-muted">
{t("import.result.filesScanned", { count: result.files_scanned })}
{result.path ? ` · ${result.path}` : ""}
</p>
@@ -655,8 +655,8 @@ export function ImportHistory() {
{/* Restore-from-backup result summary */}
{backupResult && !running && (
<div className="border border-emerald-500/20 bg-emerald-500/5 rounded-lg px-4 py-3 space-y-2">
<div className="flex items-center gap-2 text-xs font-semibold text-emerald-400 uppercase tracking-wider">
<div className="border border-status-success/20 bg-status-success/5 rounded-lg px-4 py-3 space-y-2">
<div className="flex items-center gap-2 text-xs font-semibold text-status-success uppercase tracking-wider">
<CheckCircle2 className="w-3.5 h-3.5" />
{t("import.backupResult.title")}
</div>
@@ -666,14 +666,14 @@ export function ImportHistory() {
count: backupResult.sessions_imported,
})}
value={backupResult.sessions_imported}
color="text-emerald-300"
color="text-status-success"
/>
<ResultStat
label={t("import.backupResult.sessionsSkipped", {
count: backupResult.sessions_skipped,
})}
value={backupResult.sessions_skipped}
color="text-gray-400"
color="text-fg-secondary"
/>
<ResultStat
label={t("import.backupResult.events", { count: backupResult.events })}
@@ -686,7 +686,7 @@ export function ImportHistory() {
color="text-cyan-300"
/>
</div>
<p className="text-[11px] text-gray-500">
<p className="text-[11px] text-fg-muted">
{t("import.backupResult.detail", {
agents: backupResult.agents,
workflows: backupResult.workflows,
@@ -712,8 +712,8 @@ function Step({
}) {
return (
<div>
<p className="text-sm font-medium text-gray-200">{title}</p>
<p className="text-xs text-gray-400 mt-1 whitespace-pre-line">{body}</p>
<p className="text-sm font-medium text-fg-secondary">{title}</p>
<p className="text-xs text-fg-secondary mt-1 whitespace-pre-line">{body}</p>
{children}
</div>
);
@@ -737,19 +737,19 @@ function ModeButton({
onClick={onClick}
className={`text-left p-3 rounded-lg border transition-colors ${
active
? "border-blue-500/40 bg-blue-500/10"
? "border-blue-600/40 bg-blue-600/10"
: "border-border bg-surface-2 hover:bg-surface-3"
}`}
>
<div
className={`flex items-center gap-1.5 text-xs font-medium mb-1 ${
active ? "text-blue-300" : "text-gray-300"
active ? "text-blue-400" : "text-fg-secondary"
}`}
>
{icon}
{title}
</div>
<p className="text-[11px] text-gray-500 leading-snug">{desc}</p>
<p className="text-[11px] text-fg-muted leading-snug">{desc}</p>
</button>
);
}
@@ -758,7 +758,7 @@ function ResultStat({ label, value, color }: { label: string; value: number; col
return (
<div className="bg-surface-2 rounded-md px-2.5 py-2">
<p className={`text-sm font-semibold ${color}`}>{value.toLocaleString()}</p>
<p className="text-[10px] text-gray-500 uppercase tracking-wider mt-0.5">{label}</p>
<p className="text-[10px] text-fg-muted uppercase tracking-wider mt-0.5">{label}</p>
</div>
);
}
+43 -40
View File
@@ -101,14 +101,17 @@ const EMPTY_FORM: RemoteSourceInput = {
/** Compact status pill for a source's last-known sync state. */
function StatusPill({ status }: { status: RemoteSource["status"] }) {
const map: Record<RemoteSource["status"], { cls: string; label: string; pulse?: boolean }> = {
idle: { cls: "text-gray-400 bg-gray-500/10 border-gray-500/20", label: "Idle" },
idle: { cls: "text-fg-secondary bg-surface-4/10 border-border-light/20", label: "Idle" },
syncing: {
cls: "text-amber-300 bg-amber-500/10 border-amber-500/25",
cls: "text-status-warning bg-status-warning/10 border-status-warning/25",
label: "Syncing",
pulse: true,
},
ok: { cls: "text-emerald-300 bg-emerald-500/10 border-emerald-500/25", label: "OK" },
error: { cls: "text-red-300 bg-red-500/10 border-red-500/25", label: "Error" },
ok: { cls: "text-status-success bg-status-success/10 border-status-success/25", label: "OK" },
error: {
cls: "text-status-danger bg-status-danger/10 border-status-danger/25",
label: "Error",
},
};
const s = map[status] || map.idle;
return (
@@ -318,17 +321,17 @@ export function RemoteSources() {
return (
<div>
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2 mb-1">
<Cloud className="w-4 h-4 text-gray-500" />
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
<Cloud className="w-4 h-4 text-fg-muted" />
{t("remoteSources.title", "Remote Data Sources")}
</h3>
<p className="text-xs text-gray-500 mb-4">
<p className="text-xs text-fg-muted mb-4">
{t(
"remoteSources.description",
"Collect Claude Code usage from other machines over SSH — e.g. a dev box or cloud VM you drive over SSH while running this dashboard locally. Authentication uses your own SSH setup (~/.ssh/config, keys, agent); no passwords are stored here."
)}
</p>
<p className="text-[11px] text-gray-600 italic mb-4 leading-snug">
<p className="text-[11px] text-fg-muted italic mb-4 leading-snug">
{t(
"cursorPathsNote",
"Informational: Cursor sessions count here too — Cursor happens to use the same ~/.claude paths as Claude Code (locally and on synced remotes)."
@@ -339,11 +342,11 @@ export function RemoteSources() {
<div className="card p-5 mb-4">
<div className="flex items-center gap-2 mb-1">
<Wifi className="w-4 h-4 text-accent" />
<span className="text-sm font-medium text-gray-200">
<span className="text-sm font-medium text-fg-secondary">
{t("remoteSources.scopeTitle", "Data scope")}
</span>
</div>
<p className="text-xs text-gray-500 mb-3">
<p className="text-xs text-fg-muted mb-3">
{t(
"remoteSources.scopeDesc",
"Choose which machines' data the whole dashboard shows. Changes apply immediately across every page — sessions, analytics, and cost."
@@ -399,13 +402,13 @@ export function RemoteSources() {
{active && (
<CheckCircle className="w-4 h-4 text-accent absolute top-2.5 right-2.5" />
)}
<Icon className={`w-5 h-5 mb-2 ${active ? "text-accent" : "text-gray-400"}`} />
<Icon className={`w-5 h-5 mb-2 ${active ? "text-accent" : "text-fg-secondary"}`} />
<div
className={`text-sm font-medium ${active ? "text-gray-100" : "text-gray-300"}`}
className={`text-sm font-medium ${active ? "text-fg-primary" : "text-fg-secondary"}`}
>
{title}
</div>
<div className="text-[11px] text-gray-500 mt-0.5 leading-snug">{desc}</div>
<div className="text-[11px] text-fg-muted mt-0.5 leading-snug">{desc}</div>
{mode === "selected" && scope.mode === "selected" && (
<div className="text-[11px] text-accent mt-1">
{t("remoteSources.scopeSelectedCount", "{{n}} of {{total}} selected", {
@@ -420,7 +423,7 @@ export function RemoteSources() {
</div>
{scope.mode === "selected" && (
<div className="mt-3 pt-3 border-t border-border">
<div className="text-[11px] uppercase tracking-wider text-gray-500 mb-2">
<div className="text-[11px] uppercase tracking-wider text-fg-muted mb-2">
{t("remoteSources.scopePickMachines", "Machines to include")}
</div>
<div className="flex flex-wrap gap-2">
@@ -434,7 +437,7 @@ export function RemoteSources() {
className={`inline-flex items-center gap-1.5 text-xs px-2.5 py-1 rounded-full border transition-colors ${
on
? "bg-accent/15 border-accent/40 text-accent"
: "bg-surface-2 border-border text-gray-400 hover:text-gray-200"
: "bg-surface-2 border-border text-fg-secondary hover:text-fg-primary"
}`}
>
{on ? <Check className="w-3 h-3" /> : <Server className="w-3 h-3" />}
@@ -449,7 +452,7 @@ export function RemoteSources() {
{/* Sources list header + add button */}
<div className="flex items-center justify-between mb-3">
<span className="text-xs font-medium text-gray-400 uppercase tracking-wider">
<span className="text-xs font-medium text-fg-secondary uppercase tracking-wider">
{t("remoteSources.listTitle", "Configured sources")}
</span>
<div className="flex items-center gap-2">
@@ -473,14 +476,14 @@ export function RemoteSources() {
{/* Add/Edit form */}
{showForm && (
<div className="card p-5 mb-4 space-y-3 border-accent/30">
<div className="text-sm font-medium text-gray-200">
<div className="text-sm font-medium text-fg-secondary">
{editingId
? t("remoteSources.editTitle", "Edit source")
: t("remoteSources.addTitle", "Add a remote source")}
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label className="block text-xs text-gray-500 mb-1">
<label className="block text-xs text-fg-muted mb-1">
{t("remoteSources.fieldLabel", "Label")} *
</label>
<input
@@ -491,7 +494,7 @@ export function RemoteSources() {
/>
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">
<label className="block text-xs text-fg-muted mb-1">
{t("remoteSources.fieldHost", "SSH host")} *
</label>
<input
@@ -502,7 +505,7 @@ export function RemoteSources() {
/>
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">
<label className="block text-xs text-fg-muted mb-1">
{t("remoteSources.fieldPort", "Port (optional)")}
</label>
<input
@@ -519,7 +522,7 @@ export function RemoteSources() {
/>
</div>
<div>
<label className="block text-xs text-gray-500 mb-1">
<label className="block text-xs text-fg-muted mb-1">
{t("remoteSources.fieldIdentity", "Identity file (optional)")}
</label>
<input
@@ -530,7 +533,7 @@ export function RemoteSources() {
/>
</div>
<div className="sm:col-span-2">
<label className="block text-xs text-gray-500 mb-1">
<label className="block text-xs text-fg-muted mb-1">
{t("remoteSources.fieldRemoteHome", "Remote Claude home (optional)")}
</label>
<input
@@ -539,7 +542,7 @@ export function RemoteSources() {
value={form.remote_home ?? ""}
onChange={(e) => setForm((f) => ({ ...f, remote_home: e.target.value }))}
/>
<p className="mt-1 text-[11px] text-gray-500 leading-snug">
<p className="mt-1 text-[11px] text-fg-muted leading-snug">
{t(
"remoteSources.fieldRemoteHomeHint",
"Linux/macOS: default ~/.claude (or an absolute path like /home/you/.claude). Windows SSH + Claude in WSL: leave blank (auto-detect) or use wsl:~/.claude. Native Windows: C:/Users/you/.claude."
@@ -554,12 +557,12 @@ export function RemoteSources() {
checked={!!form.enabled}
onChange={(e) => setForm((f) => ({ ...f, enabled: e.target.checked }))}
/>
<span className="text-sm text-gray-300">
<span className="text-sm text-fg-secondary">
{t("remoteSources.fieldEnabled", "Sync automatically in the background")}
</span>
</label>
{formError && (
<div className="text-xs text-red-300 bg-red-500/10 border border-red-500/25 rounded-lg px-3 py-2">
<div className="text-xs text-status-danger bg-status-danger/10 border border-status-danger/25 rounded-lg px-3 py-2">
{formError}
</div>
)}
@@ -585,14 +588,14 @@ export function RemoteSources() {
{/* Sources list */}
{loading ? (
<div className="text-xs text-gray-500">{t("common:loading", "Loading…")}</div>
<div className="text-xs text-fg-muted">{t("common:loading", "Loading…")}</div>
) : sources.length === 0 ? (
<div className="card p-6 text-center">
<Server className="w-6 h-6 text-gray-600 mx-auto mb-2" />
<p className="text-sm text-gray-400">
<Server className="w-6 h-6 text-fg-muted mx-auto mb-2" />
<p className="text-sm text-fg-secondary">
{t("remoteSources.empty", "No remote sources yet.")}
</p>
<p className="text-xs text-gray-600 mt-1">
<p className="text-xs text-fg-muted mt-1">
{t(
"remoteSources.emptyHint",
"Add a machine you reach over SSH to pull its Claude Code usage in."
@@ -610,16 +613,16 @@ export function RemoteSources() {
<div className="min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<Server className="w-4 h-4 text-accent shrink-0" />
<span className="text-sm font-medium text-gray-200">{s.label}</span>
<span className="text-sm font-medium text-fg-secondary">{s.label}</span>
<StatusPill status={s.status} />
{!s.enabled && (
<span className="text-[10px] text-gray-500 bg-surface-2 border border-border px-1.5 py-0.5 rounded-full">
<span className="text-[10px] text-fg-muted bg-surface-2 border border-border px-1.5 py-0.5 rounded-full">
{t("remoteSources.paused", "Auto-sync off")}
</span>
)}
{s.session_count != null && s.session_count > 0 && (
<span
className="text-[10px] text-gray-400 bg-surface-2 border border-border px-1.5 py-0.5 rounded-full"
className="text-[10px] text-fg-secondary bg-surface-2 border border-border px-1.5 py-0.5 rounded-full"
title={t(
"remoteSources.sessionCountHint",
"Sessions linked to this source (not every session visible when data scope is All)"
@@ -631,12 +634,12 @@ export function RemoteSources() {
</span>
)}
</div>
<div className="text-[11px] text-gray-500 font-mono mt-1 truncate">
<div className="text-[11px] text-fg-muted font-mono mt-1 truncate">
{s.host}
{s.ssh_port ? `:${s.ssh_port}` : ""}
{s.remote_home ? ` · ${s.remote_home}` : ""}
</div>
<div className="text-[11px] text-gray-600 mt-1">
<div className="text-[11px] text-fg-muted mt-1">
{s.last_sync_at
? t("remoteSources.lastSync", "Last sync: {{when}}", {
when: new Date(s.last_sync_at).toLocaleString(),
@@ -652,14 +655,14 @@ export function RemoteSources() {
})}`}
</div>
{s.status === "error" && s.last_error && (
<div className="text-[11px] text-red-300 mt-1 break-words">
<div className="text-[11px] text-status-danger mt-1 break-words">
{s.last_error}
</div>
)}
{test && test.message && (
<div
className={`flex items-start gap-1.5 text-[11px] mt-2 ${
test.ok ? "text-emerald-300" : "text-red-300"
test.ok ? "text-status-success" : "text-status-danger"
}`}
>
{test.ok ? (
@@ -712,7 +715,7 @@ export function RemoteSources() {
</button>
<button
onClick={() => setConfirmDelete({ id: s.id, purge: false })}
className="btn-ghost text-xs text-red-300 hover:text-red-200"
className="btn-ghost text-xs text-status-danger hover:text-status-danger"
title={t("common:delete", "Delete")}
>
<Trash2 className="w-3.5 h-3.5" />
@@ -723,7 +726,7 @@ export function RemoteSources() {
{/* Inline delete confirmation */}
{confirmDelete?.id === s.id && (
<div className="mt-3 pt-3 border-t border-border">
<p className="text-xs text-gray-300 mb-2">
<p className="text-xs text-fg-secondary mb-2">
{t("remoteSources.confirmDelete", "Remove this source?")}
</p>
<label className="flex items-center gap-2 mb-3 cursor-pointer">
@@ -735,7 +738,7 @@ export function RemoteSources() {
setConfirmDelete((c) => c && { ...c, purge: e.target.checked })
}
/>
<span className="text-xs text-gray-400">
<span className="text-xs text-fg-secondary">
{t(
"remoteSources.purgeData",
"Also delete the sessions imported from this source (cannot be undone)"
@@ -746,7 +749,7 @@ export function RemoteSources() {
<button
onClick={doDelete}
disabled={busy}
className="btn-primary text-xs bg-red-600 hover:bg-red-500 disabled:opacity-40"
className="btn-primary text-xs bg-status-danger hover:bg-status-danger disabled:opacity-40"
>
{busy ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" />
+4 -4
View File
@@ -179,10 +179,10 @@ export function Select<T extends string>({ value, onChange, options, disabled }:
disabled={disabled}
onClick={() => setOpen((v) => !v)}
onKeyDown={onKey}
className="w-full flex items-center justify-between gap-2 bg-surface-2 border border-border rounded-md px-3 py-1.5 text-[11px] text-gray-100 focus:outline-none focus:border-accent/50 hover:bg-surface-3 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
className="w-full flex items-center justify-between gap-2 bg-surface-2 border border-border rounded-md px-3 py-1.5 text-[11px] text-fg-primary focus:outline-none focus:border-accent/50 hover:bg-surface-3 transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
>
<span className="truncate">{current?.label ?? "-"}</span>
<ChevronDown className="w-3.5 h-3.5 text-gray-500 flex-shrink-0" />
<ChevronDown className="w-3.5 h-3.5 text-fg-muted flex-shrink-0" />
</button>
{open && (
<div
@@ -207,7 +207,7 @@ export function Select<T extends string>({ value, onChange, options, disabled }:
<div className="flex items-center gap-2">
<span
className={`text-[11px] flex-1 truncate ${
isSelected ? "text-accent font-medium" : "text-gray-200"
isSelected ? "text-accent font-medium" : "text-fg-secondary"
}`}
>
{opt.label}
@@ -215,7 +215,7 @@ export function Select<T extends string>({ value, onChange, options, disabled }:
{isSelected && <Check className="w-3 h-3 text-accent flex-shrink-0" />}
</div>
{opt.hint && (
<div className="text-[10px] text-gray-500 truncate mt-0.5">{opt.hint}</div>
<div className="text-[10px] text-fg-muted truncate mt-0.5">{opt.hint}</div>
)}
</button>
);
+5 -5
View File
@@ -113,8 +113,8 @@ export function SessionCard({ session, onClick }: SessionCardProps) {
<FolderOpen className="w-3.5 h-3.5" />
</div>
<div className="min-w-0 overflow-hidden">
<p className="text-sm font-medium text-gray-200 truncate">{title}</p>
<p className="text-[11px] text-gray-500 font-mono truncate">
<p className="text-sm font-medium text-fg-secondary truncate">{title}</p>
<p className="text-[11px] text-fg-muted font-mono truncate">
{session.id.slice(0, 12)}
</p>
</div>
@@ -125,12 +125,12 @@ export function SessionCard({ session, onClick }: SessionCardProps) {
</div>
{session.cwd && (
<p className="text-xs text-gray-400 mb-3 truncate font-mono leading-relaxed">
<p className="text-xs text-fg-secondary mb-3 truncate font-mono leading-relaxed">
{session.cwd}
</p>
)}
<div className="flex items-center gap-3 text-[11px] text-gray-500 min-w-0 overflow-hidden flex-wrap">
<div className="flex items-center gap-3 text-[11px] text-fg-muted min-w-0 overflow-hidden flex-wrap">
<span className="flex items-center gap-1 flex-shrink-0">
<Bot className="w-3 h-3" />
{t("session.agentSummary", { count: agentCount })}
@@ -153,7 +153,7 @@ export function SessionCard({ session, onClick }: SessionCardProps) {
? `${t("ran")}${formatDuration(session.started_at, session.ended_at)}`
: `${t("running")}${formatDuration(session.started_at, new Date().toISOString())}`}
</span>
<span className="text-gray-600 flex-shrink-0 ml-auto">
<span className="text-fg-muted flex-shrink-0 ml-auto">
{timeAgo(session.ended_at || lastActivity)}
</span>
</div>
+45 -42
View File
@@ -100,35 +100,35 @@ function StatTile({
tone?: "default" | "violet" | "emerald" | "amber" | "rose" | "cyan" | "blue";
}) {
const palette = {
default: "border-surface-3 bg-surface-2 text-gray-200",
default: "border-surface-3 bg-surface-2 text-fg-secondary",
violet: "border-violet-500/20 bg-violet-500/5 text-violet-200",
emerald: "border-emerald-500/20 bg-emerald-500/5 text-emerald-200",
amber: "border-amber-500/20 bg-amber-500/5 text-amber-200",
emerald: "border-status-success/20 bg-status-success/5 text-status-success",
amber: "border-status-warning/20 bg-status-warning/5 text-status-warning",
rose: "border-rose-500/20 bg-rose-500/5 text-rose-200",
cyan: "border-cyan-500/20 bg-cyan-500/5 text-cyan-200",
blue: "border-blue-500/20 bg-blue-500/5 text-blue-200",
blue: "border-blue-600/20 bg-blue-600/5 text-blue-300",
}[tone];
const iconTone = {
default: "text-gray-500",
default: "text-fg-muted",
violet: "text-violet-400",
emerald: "text-emerald-400",
amber: "text-amber-400",
emerald: "text-status-success",
amber: "text-status-warning",
rose: "text-rose-400",
cyan: "text-cyan-400",
blue: "text-blue-400",
blue: "text-blue-500",
}[tone];
return (
<div className={`rounded-lg border px-3 py-2.5 ${palette}`}>
<div className="flex items-center gap-1.5 text-[10px] uppercase tracking-wider text-gray-500">
<div className="flex items-center gap-1.5 text-[10px] uppercase tracking-wider text-fg-muted">
<span className={iconTone}>{icon}</span>
{label}
</div>
<div className="mt-1 font-mono text-lg font-semibold text-gray-100 leading-tight">
<div className="mt-1 font-mono text-lg font-semibold text-fg-primary leading-tight">
{value}
</div>
{hint && <div className="text-[10px] text-gray-500 mt-0.5">{hint}</div>}
{hint && <div className="text-[10px] text-fg-muted mt-0.5">{hint}</div>}
</div>
);
}
@@ -156,7 +156,7 @@ function ToolUsageRow({ toolName, count, max }: { toolName: string; count: numbe
style={{ width: `${pct}%` }}
/>
</div>
<span className="font-mono text-xs text-gray-400 w-14 text-right flex-shrink-0">
<span className="font-mono text-xs text-fg-secondary w-14 text-right flex-shrink-0">
{count.toLocaleString()}
</span>
</div>
@@ -286,23 +286,26 @@ export function SessionOverview({ session, agents }: SessionOverviewProps) {
<div className="space-y-5 mb-6">
{/* Active-agent banner - only shows when session is running */}
{activeAgent && (
<div className="flex items-center gap-2.5 px-3 py-2 rounded-lg border border-emerald-500/20 bg-emerald-500/5">
<div className="flex items-center gap-2.5 px-3 py-2 rounded-lg border border-status-success/20 bg-status-success/5">
<span className="relative flex h-2 w-2 flex-shrink-0">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-60" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-400" />
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-status-success opacity-60" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-status-success" />
</span>
<Bot className="w-3.5 h-3.5 text-emerald-300 flex-shrink-0" />
<span className="text-xs text-emerald-200 font-medium flex-shrink-0">
<Bot className="w-3.5 h-3.5 text-status-success flex-shrink-0" />
<span className="text-xs text-status-success font-medium flex-shrink-0">
{activeAgent.name || "Agent"}
</span>
{activeAgent.current_tool && (
<span className="text-[11px] text-gray-400 font-mono inline-flex items-center gap-1">
<span className="text-gray-600">running</span>
<span className="text-emerald-300">{activeAgent.current_tool}</span>
<span className="text-[11px] text-fg-secondary font-mono inline-flex items-center gap-1">
<span className="text-fg-muted">running</span>
<span className="text-status-success">{activeAgent.current_tool}</span>
</span>
)}
{activeAgent.task && (
<span className="text-[11px] text-gray-400 truncate min-w-0" title={activeAgent.task}>
<span
className="text-[11px] text-fg-secondary truncate min-w-0"
title={activeAgent.task}
>
· {activeAgent.task}
</span>
)}
@@ -361,16 +364,16 @@ export function SessionOverview({ session, agents }: SessionOverviewProps) {
{/* Tool usage */}
<div className="lg:col-span-2 rounded-lg border border-surface-3 bg-surface-2/60 p-3.5">
<div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-semibold text-gray-300 uppercase tracking-wider flex items-center gap-1.5">
<h3 className="text-xs font-semibold text-fg-secondary uppercase tracking-wider flex items-center gap-1.5">
<Wrench className="w-3.5 h-3.5 text-violet-400" />
Top tools
</h3>
<span className="text-[10px] text-gray-500 font-mono">
<span className="text-[10px] text-fg-muted font-mono">
{stats.tools_used.length} total
</span>
</div>
{stats.tools_used.length === 0 ? (
<div className="text-center py-6 text-xs text-gray-500">No tool calls yet.</div>
<div className="text-center py-6 text-xs text-fg-muted">No tool calls yet.</div>
) : (
<div className="space-y-1.5">
{stats.tools_used.slice(0, 8).map((t) => (
@@ -420,26 +423,26 @@ export function SessionOverview({ session, agents }: SessionOverviewProps) {
return (
<>
<div className="flex items-center justify-between mb-3">
<h3 className="text-xs font-semibold text-gray-300 uppercase tracking-wider flex items-center gap-1.5">
<h3 className="text-xs font-semibold text-fg-secondary uppercase tracking-wider flex items-center gap-1.5">
<GitBranch className="w-3.5 h-3.5 text-cyan-400" />
Subagents
</h3>
<span className="text-[10px] text-gray-500 font-mono">{totalRuns} runs</span>
<span className="text-[10px] text-fg-muted font-mono">{totalRuns} runs</span>
</div>
{rows.length === 0 ? (
<div className="text-center py-6 text-xs text-gray-500">
<div className="text-center py-6 text-xs text-fg-muted">
No subagents in this session.
</div>
) : (
<div className="space-y-1.5">
{rows.slice(0, 8).map((r) => {
const pct = max > 0 ? Math.max(4, Math.round((r.count / max) * 100)) : 0;
const barClass = r.isCompaction ? "bg-amber-500/60" : "bg-cyan-500/60";
const barClass = r.isCompaction ? "bg-status-warning/60" : "bg-cyan-500/60";
return (
<div key={r.key} className="flex items-center gap-2">
<span
className={`font-mono text-xs truncate flex-1 min-w-0 ${
r.isCompaction ? "text-amber-300" : "text-gray-300"
r.isCompaction ? "text-status-warning" : "text-fg-secondary"
}`}
title={r.label}
>
@@ -451,7 +454,7 @@ export function SessionOverview({ session, agents }: SessionOverviewProps) {
style={{ width: `${pct}%` }}
/>
</div>
<span className="font-mono text-xs text-gray-400 w-8 text-right flex-shrink-0">
<span className="font-mono text-xs text-fg-secondary w-8 text-right flex-shrink-0">
{r.count}
</span>
</div>
@@ -469,11 +472,11 @@ export function SessionOverview({ session, agents }: SessionOverviewProps) {
{totalTokens > 0 && (
<div className="rounded-lg border border-surface-3 bg-surface-2/60 p-3.5">
<div className="flex items-center justify-between mb-2.5">
<h3 className="text-xs font-semibold text-gray-300 uppercase tracking-wider flex items-center gap-1.5">
<Coins className="w-3.5 h-3.5 text-amber-400" />
<h3 className="text-xs font-semibold text-fg-secondary uppercase tracking-wider flex items-center gap-1.5">
<Coins className="w-3.5 h-3.5 text-status-warning" />
Token flow
</h3>
<span className="text-[10px] text-gray-500 font-mono">{fmt(totalTokens)} total</span>
<span className="text-[10px] text-fg-muted font-mono">{fmt(totalTokens)} total</span>
</div>
<TokenFlowBar tokens={tokens} total={totalTokens} />
</div>
@@ -482,8 +485,8 @@ export function SessionOverview({ session, agents }: SessionOverviewProps) {
{/* Event-type breakdown - secondary, only top 6 */}
{stats.events_by_type.length > 0 && (
<div className="rounded-lg border border-surface-3 bg-surface-2/60 p-3.5">
<h3 className="text-xs font-semibold text-gray-300 uppercase tracking-wider mb-3 flex items-center gap-1.5">
<Activity className="w-3.5 h-3.5 text-gray-400" />
<h3 className="text-xs font-semibold text-fg-secondary uppercase tracking-wider mb-3 flex items-center gap-1.5">
<Activity className="w-3.5 h-3.5 text-fg-secondary" />
Event mix
</h3>
<div className="flex flex-wrap gap-1.5">
@@ -492,9 +495,9 @@ export function SessionOverview({ session, agents }: SessionOverviewProps) {
key={e.event_type}
className="inline-flex items-center gap-1.5 text-[11px] font-mono bg-surface-3/60 border border-surface-3 rounded-md px-2 py-0.5"
>
<span className="text-gray-400">{e.event_type}</span>
<span className="text-gray-500">·</span>
<span className="text-gray-200">{e.count.toLocaleString()}</span>
<span className="text-fg-secondary">{e.event_type}</span>
<span className="text-fg-muted">·</span>
<span className="text-fg-secondary">{e.count.toLocaleString()}</span>
</span>
))}
</div>
@@ -524,8 +527,8 @@ function TokenFlowBar({ tokens, total }: { tokens: SessionStats["tokens"]; total
key: "input",
label: "Input",
value: tokens.input_tokens,
cls: "bg-emerald-500",
text: "text-emerald-300",
cls: "bg-status-success",
text: "text-status-success",
},
{
key: "output",
@@ -558,11 +561,11 @@ function TokenFlowBar({ tokens, total }: { tokens: SessionStats["tokens"]; total
return (
<div key={s.key} className="flex items-center gap-2">
<span className={`block w-2 h-2 rounded-full ${s.cls}`} />
<span className="text-gray-500 text-[11px]">{s.label}</span>
<span className="text-fg-muted text-[11px]">{s.label}</span>
<span className={`font-mono ml-auto ${s.text}`}>
{fmt(s.value)}
{pct > 0 && (
<span className="text-gray-600 text-[10px] ml-1">
<span className="text-fg-muted text-[10px] ml-1">
{pct >= 1 ? Math.round(pct) : pct.toFixed(1)}%
</span>
)}
+135 -75
View File
@@ -79,10 +79,13 @@ import {
Gauge,
ChevronUp,
ChevronDown,
Sun,
Moon,
} from "lucide-react";
import type { LucideIcon } from "lucide-react";
import { api } from "../lib/api";
import { eventBus } from "../lib/eventBus";
import { useTheme } from "../hooks/useTheme";
import type { UpdateStatusPayload, WSMessage } from "../lib/types";
function isUpdatePayload(x: unknown): x is UpdateStatusPayload {
@@ -180,6 +183,7 @@ interface SidebarProps {
export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
const { t, i18n } = useTranslation();
const { theme, setTheme, toggleTheme } = useTheme();
// Track whether nav items are clipped by overflow so we can render
// chevron affordances pointing toward the hidden items. Recomputed on
// scroll, resize, and any structural change (e.g. collapse toggle).
@@ -376,6 +380,8 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
const switchLanguageTitle = t("nav:switchLanguage", {
language: t(`nav:languageNames.${nextLanguage}`),
});
const nextTheme = theme === "dark" ? "light" : "dark";
const switchThemeTitle = t("nav:switchTheme", { theme: t(`nav:themeNames.${nextTheme}`) });
const toggleLang = () => {
i18n.changeLanguage(nextLanguage);
@@ -401,8 +407,8 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
</div>
{!collapsed && (
<div className="min-w-0">
<h1 className="text-sm font-semibold text-gray-100 truncate">{t("nav:brand")}</h1>
<p className="text-[11px] text-gray-500">{t("nav:brandSub")}</p>
<h1 className="text-sm font-semibold text-fg-primary truncate">{t("nav:brand")}</h1>
<p className="text-[11px] text-fg-muted">{t("nav:brandSub")}</p>
</div>
)}
</div>
@@ -428,7 +434,7 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
} ${
isActive
? "bg-accent/10 text-accent border border-accent/20"
: "text-gray-400 hover:text-gray-200 hover:bg-surface-3 border border-transparent"
: "text-fg-secondary hover:text-fg-primary hover:bg-surface-3 border border-transparent"
}`
}
>
@@ -444,7 +450,7 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
onClick={() => scrollNavBy(-160)}
aria-label={t("nav:scrollUp")}
title={t("nav:scrollUp")}
className="absolute top-1.5 right-[7px] z-10 inline-flex items-center justify-center w-6 h-6 rounded-md border border-border bg-surface-2/90 text-gray-300 hover:text-gray-50 hover:bg-surface-3 shadow-md backdrop-blur-sm transition-colors animate-fade-in"
className="absolute top-1.5 right-[7px] z-10 inline-flex items-center justify-center w-6 h-6 rounded-md border border-border bg-surface-2/90 text-fg-secondary hover:text-fg-primary hover:bg-surface-3 shadow-md backdrop-blur-sm transition-colors animate-fade-in"
>
<ChevronUp className="w-3.5 h-3.5" aria-hidden />
</button>
@@ -455,52 +461,106 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
onClick={() => scrollNavBy(160)}
aria-label={t("nav:scrollDown")}
title={t("nav:scrollDown")}
className="absolute bottom-1.5 right-[7px] z-10 inline-flex items-center justify-center w-6 h-6 rounded-md border border-border bg-surface-2/90 text-gray-300 hover:text-gray-50 hover:bg-surface-3 shadow-md backdrop-blur-sm transition-colors animate-fade-in"
className="absolute bottom-1.5 right-[7px] z-10 inline-flex items-center justify-center w-6 h-6 rounded-md border border-border bg-surface-2/90 text-fg-secondary hover:text-fg-primary hover:bg-surface-3 shadow-md backdrop-blur-sm transition-colors animate-fade-in"
>
<ChevronDown className="w-3.5 h-3.5" aria-hidden />
</button>
)}
</div>
{/* Language controls */}
{/* Language + theme controls. Theme sits next to language: same icon
row when collapsed, same row as the EN/VI buttons when expanded -
both are 2-state click-to-flip toggles, so they read as one family
of controls rather than two unrelated settings. */}
<div className="px-2 pb-2 flex-shrink-0">
{collapsed ? (
<button
onClick={toggleLang}
className="w-full h-9 rounded-lg border border-border bg-surface-2 text-gray-300 hover:bg-surface-3 hover:text-gray-100 transition-colors flex flex-col items-center justify-center gap-0.5"
title={switchLanguageTitle}
aria-label={switchLanguageTitle}
>
<Languages className="w-3.5 h-3.5" />
<span className="text-[10px] font-semibold leading-none">
{t(`nav:languageShort.${currentLanguage}`)}
</span>
</button>
<div className="flex flex-col gap-1">
<button
onClick={toggleLang}
className="w-full h-9 rounded-lg border border-border bg-surface-2 text-fg-secondary hover:bg-surface-3 hover:text-fg-primary transition-colors flex flex-col items-center justify-center gap-0.5"
title={switchLanguageTitle}
aria-label={switchLanguageTitle}
>
<Languages className="w-3.5 h-3.5" />
<span className="text-[10px] font-semibold leading-none">
{t(`nav:languageShort.${currentLanguage}`)}
</span>
</button>
<button
onClick={toggleTheme}
className="w-full h-9 rounded-lg border border-border bg-surface-2 text-fg-secondary hover:bg-surface-3 hover:text-fg-primary transition-colors flex flex-col items-center justify-center gap-0.5"
title={switchThemeTitle}
aria-label={switchThemeTitle}
>
{theme === "dark" ? (
<Moon className="w-3.5 h-3.5" />
) : (
<Sun className="w-3.5 h-3.5" />
)}
<span className="text-[10px] font-semibold leading-none">
{t(`nav:themeNames.${theme}`).slice(0, 1)}
</span>
</button>
</div>
) : (
<div className="rounded-lg border border-border bg-surface-2 p-2">
<p className="px-1 text-[10px] font-semibold uppercase tracking-wider text-gray-500">
{t("nav:language")}
</p>
<div className="mt-2 grid grid-cols-4 gap-1">
{SUPPORTED_LANGUAGES.map((language) => {
const active = language === currentLanguage;
return (
<button
key={language}
onClick={() => changeLanguage(language)}
aria-pressed={active}
aria-label={t(`nav:languageNames.${language}`)}
title={t(`nav:languageNames.${language}`)}
className={`rounded-md px-2 py-1.5 text-[11px] font-semibold transition-colors ${
active
? "bg-accent/20 text-accent border border-accent/30"
: "bg-surface-1 text-gray-400 border border-border hover:bg-surface-3 hover:text-gray-200"
}`}
>
{t(`nav:languageShort.${language}`)}
</button>
);
})}
<div className="rounded-lg border border-border bg-surface-2 p-2 space-y-2">
<div className="flex items-center justify-between gap-2">
<div className="flex-1">
<p className="px-1 text-[10px] font-semibold uppercase tracking-wider text-fg-muted">
{t("nav:language")}
</p>
<div className="mt-2 grid grid-cols-4 gap-1">
{SUPPORTED_LANGUAGES.map((language) => {
const active = language === currentLanguage;
return (
<button
key={language}
onClick={() => changeLanguage(language)}
aria-pressed={active}
aria-label={t(`nav:languageNames.${language}`)}
title={t(`nav:languageNames.${language}`)}
className={`rounded-md px-2 py-1.5 text-[11px] font-semibold transition-colors ${
active
? "bg-accent/20 text-accent border border-accent/30"
: "bg-surface-1 text-fg-secondary border border-border hover:bg-surface-3 hover:text-fg-primary"
}`}
>
{t(`nav:languageShort.${language}`)}
</button>
);
})}
</div>
</div>
<div>
<p className="px-1 text-[10px] font-semibold uppercase tracking-wider text-fg-muted">
{t("nav:theme")}
</p>
<div className="mt-2 grid grid-cols-2 gap-1">
{(["dark", "light"] as const).map((option) => {
const active = option === theme;
return (
<button
key={option}
onClick={() => setTheme(option)}
aria-pressed={active}
aria-label={t(`nav:themeNames.${option}`)}
title={t(`nav:themeNames.${option}`)}
className={`rounded-md px-2 py-1.5 text-[11px] font-semibold transition-colors flex items-center justify-center ${
active
? "bg-accent/20 text-accent border border-accent/30"
: "bg-surface-1 text-fg-secondary border border-border hover:bg-surface-3 hover:text-fg-primary"
}`}
>
{option === "dark" ? (
<Moon className="w-3.5 h-3.5" />
) : (
<Sun className="w-3.5 h-3.5" />
)}
</button>
);
})}
</div>
</div>
</div>
</div>
)}
@@ -512,8 +572,8 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
onClick={onToggle}
className={`w-full h-10 rounded-lg border border-border bg-surface-2 transition-colors ${
collapsed
? "flex items-center justify-center text-gray-400 hover:text-gray-200 hover:bg-surface-3"
: "flex items-center gap-2.5 px-3 text-gray-300 hover:text-gray-100 hover:bg-surface-3"
? "flex items-center justify-center text-fg-secondary hover:text-fg-primary hover:bg-surface-3"
: "flex items-center gap-2.5 px-3 text-fg-secondary hover:text-fg-primary hover:bg-surface-3"
}`}
title={collapsed ? t("nav:expand") : t("nav:collapse")}
aria-label={collapsed ? t("nav:expand") : t("nav:collapse")}
@@ -551,7 +611,7 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
>
<span
className={`inline-flex items-center gap-2 ${
wsConnected ? "text-emerald-400" : "text-gray-500"
wsConnected ? "text-status-success" : "text-fg-muted"
}`}
>
{wsConnected ? (
@@ -566,7 +626,7 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
)}
</span>
{!collapsed && (
<span className="text-[11px] font-medium text-gray-600">{`v${__APP_VERSION__}`}</span>
<span className="text-[11px] font-medium text-fg-muted">{`v${__APP_VERSION__}`}</span>
)}
</div>
</button>
@@ -579,15 +639,15 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
aria-label={checkTitle}
className={`relative w-8 h-8 mx-auto flex items-center justify-center rounded-lg border bg-surface-2 transition-colors disabled:opacity-60 ${
updateAvailable
? "border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/10"
? "border-status-success/40 text-status-success hover:bg-status-success/10"
: checkError
? "border-amber-500/40 text-amber-300 hover:bg-amber-500/10"
: "border-border text-gray-400 hover:text-gray-200 hover:bg-surface-3"
? "border-status-warning/40 text-status-warning hover:bg-status-warning/10"
: "border-border text-fg-secondary hover:text-fg-primary hover:bg-surface-3"
}`}
>
<RefreshCw className={`w-3.5 h-3.5 ${checking ? "animate-spin" : ""}`} aria-hidden />
{updateAvailable && !checking && (
<span className="absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-emerald-400" />
<span className="absolute top-1 right-1 w-1.5 h-1.5 rounded-full bg-status-success" />
)}
</button>
) : (
@@ -598,10 +658,10 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
title={checkTitle}
className={`w-full rounded-lg border bg-surface-2 px-2.5 py-2 text-xs transition-colors disabled:opacity-60 flex items-center justify-between gap-2 ${
updateAvailable
? "border-emerald-500/40 text-emerald-300 hover:bg-emerald-500/10"
? "border-status-success/40 text-status-success hover:bg-status-success/10"
: checkError
? "border-amber-500/40 text-amber-300 hover:bg-amber-500/10"
: "border-border text-gray-300 hover:text-gray-100 hover:bg-surface-3"
? "border-status-warning/40 text-status-warning hover:bg-status-warning/10"
: "border-border text-fg-secondary hover:text-fg-primary hover:bg-surface-3"
}`}
>
<span className="inline-flex items-center gap-2 truncate">
@@ -612,7 +672,7 @@ export function Sidebar({ wsConnected, collapsed, onToggle }: SidebarProps) {
<span className="font-medium truncate">{checkTitle}</span>
</span>
{updateAvailable && !checking && (
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 flex-shrink-0" />
<span className="w-1.5 h-1.5 rounded-full bg-status-success flex-shrink-0" />
)}
</button>
)}
@@ -741,8 +801,8 @@ function ConnectionStatusModal({
<div
className={`w-9 h-9 rounded-lg border flex items-center justify-center flex-shrink-0 ${
wsConnected
? "bg-emerald-500/10 border-emerald-500/30 text-emerald-400"
: "bg-surface-3 border-border text-gray-400"
? "bg-status-success/10 border-status-success/30 text-status-success"
: "bg-surface-3 border-border text-fg-secondary"
}`}
>
{wsConnected ? (
@@ -754,19 +814,19 @@ function ConnectionStatusModal({
<div className="min-w-0">
<h2
id="connection-status-title"
className="text-base font-semibold text-gray-50 truncate tracking-tight leading-tight"
className="text-base font-semibold text-fg-primary truncate tracking-tight leading-tight"
>
{t("nav:connectionDetails")}
</h2>
<p
className={`text-[11px] font-medium inline-flex items-center gap-1.5 leading-tight ${
wsConnected ? "text-emerald-400" : "text-gray-500"
wsConnected ? "text-status-success" : "text-fg-muted"
}`}
>
{wsConnected && (
<span className="relative flex w-1.5 h-1.5">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-emerald-400" />
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-status-success opacity-75" />
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-status-success" />
</span>
)}
{wsConnected ? t("nav:live") : t("nav:disconnected")}
@@ -777,7 +837,7 @@ function ConnectionStatusModal({
type="button"
onClick={close}
aria-label={t("nav:close")}
className="p-1.5 -m-1 rounded-lg text-gray-500 hover:text-gray-200 hover:bg-surface-4 transition-colors flex-shrink-0"
className="p-1.5 -m-1 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-4 transition-colors flex-shrink-0"
>
<X className="w-4 h-4" />
</button>
@@ -835,7 +895,7 @@ function ConnectionStatusModal({
{/* Top event types */}
<Section title={t("nav:topEventTypes")} icon={BarChart3}>
{topTypes.length === 0 ? (
<p className="text-xs text-gray-500 italic">{t("nav:noEventsYet")}</p>
<p className="text-xs text-fg-muted italic">{t("nav:noEventsYet")}</p>
) : (
<div className="space-y-1.5">
{topTypes.map(([type, count]) => (
@@ -848,7 +908,7 @@ function ConnectionStatusModal({
{/* Recent activity */}
<Section title={t("nav:recentActivity")} icon={Clock}>
{recentEvents.length === 0 ? (
<p className="text-xs text-gray-500 italic">{t("nav:noEventsYet")}</p>
<p className="text-xs text-fg-muted italic">{t("nav:noEventsYet")}</p>
) : (
<ul className="space-y-1">
{recentEvents.map((evt, i) => (
@@ -856,8 +916,8 @@ function ConnectionStatusModal({
key={`${evt.at}-${i}`}
className="flex items-center justify-between gap-3 text-[11px] font-mono px-2 py-1 rounded bg-surface-2/50"
>
<span className="text-gray-200 truncate">{evt.type}</span>
<span className="text-gray-500 flex-shrink-0">{formatRelative(evt.at, t)}</span>
<span className="text-fg-secondary truncate">{evt.type}</span>
<span className="text-fg-muted flex-shrink-0">{formatRelative(evt.at, t)}</span>
</li>
))}
</ul>
@@ -866,11 +926,11 @@ function ConnectionStatusModal({
</div>
<div className="flex items-center justify-between gap-2 px-5 py-3 border-t border-border bg-surface-2/40">
<span className="text-[10px] text-gray-500">{t("nav:statsPersisted")}</span>
<span className="text-[10px] text-fg-muted">{t("nav:statsPersisted")}</span>
<button
type="button"
onClick={onResetStats}
className="text-[11px] font-medium text-gray-400 hover:text-gray-100 hover:bg-surface-3 px-2 py-1 rounded transition-colors"
className="text-[11px] font-medium text-fg-secondary hover:text-fg-primary hover:bg-surface-3 px-2 py-1 rounded transition-colors"
>
{t("nav:resetStats")}
</button>
@@ -896,7 +956,7 @@ function Section({
<span className="w-5 h-5 rounded-md bg-accent/15 border border-accent/25 flex items-center justify-center flex-shrink-0">
<Icon className="w-3 h-3 text-accent" aria-hidden />
</span>
<h3 className="text-[13px] font-semibold text-gray-100 tracking-tight">{title}</h3>
<h3 className="text-[13px] font-semibold text-fg-primary tracking-tight">{title}</h3>
</div>
{children}
</section>
@@ -906,12 +966,12 @@ function Section({
function KpiTile({ label, value, unit }: { label: string; value: string; unit: string }) {
return (
<div className="rounded-lg border border-border bg-surface-2 px-2.5 py-2">
<div className="text-[9px] font-semibold uppercase tracking-wider text-gray-500 truncate">
<div className="text-[9px] font-semibold uppercase tracking-wider text-fg-muted truncate">
{label}
</div>
<div className="mt-0.5 flex items-baseline gap-1 truncate">
<span className="text-base font-semibold text-gray-100 font-mono">{value}</span>
<span className="text-[10px] font-medium text-gray-500 truncate">{unit}</span>
<span className="text-base font-semibold text-fg-primary font-mono">{value}</span>
<span className="text-[10px] font-medium text-fg-muted truncate">{unit}</span>
</div>
</div>
);
@@ -920,10 +980,10 @@ function KpiTile({ label, value, unit }: { label: string; value: string; unit: s
function DetailRow({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
return (
<div className="flex items-start justify-between gap-3 text-xs">
<span className="text-gray-500 font-medium uppercase tracking-wider text-[10px] pt-0.5">
<span className="text-fg-muted font-medium uppercase tracking-wider text-[10px] pt-0.5">
{label}
</span>
<span className={`text-gray-200 text-right break-all min-w-0 ${mono ? "font-mono" : ""}`}>
<span className={`text-fg-secondary text-right break-all min-w-0 ${mono ? "font-mono" : ""}`}>
{value}
</span>
</div>
@@ -946,8 +1006,8 @@ function TypeBar({
return (
<div className="text-[11px]">
<div className="flex items-center justify-between gap-2 mb-0.5">
<span className="font-mono text-gray-200 truncate">{type}</span>
<span className="text-gray-500 flex-shrink-0 font-mono">
<span className="font-mono text-fg-secondary truncate">{type}</span>
<span className="text-fg-muted flex-shrink-0 font-mono">
{count} · {sharePct}%
</span>
</div>
@@ -1005,7 +1065,7 @@ function Sparkline({
/>
)}
</svg>
<div className="flex items-center justify-between mt-1.5 text-[10px] text-gray-500 font-mono">
<div className="flex items-center justify-between mt-1.5 text-[10px] text-fg-muted font-mono">
<span>60s</span>
<span>{avgLabel}</span>
<span>{"now"}</span>
+3 -3
View File
@@ -98,7 +98,7 @@ export function StatCard({
return (
<div className="card p-5">
<div className="flex items-center justify-between gap-3 mb-3">
<span className="text-xs font-medium text-gray-500 uppercase tracking-wider truncate">
<span className="text-xs font-medium text-fg-muted uppercase tracking-wider truncate">
{label}
</span>
<Icon className={`w-5 h-5 flex-shrink-0 ${accentColor}`} />
@@ -108,11 +108,11 @@ export function StatCard({
<StatValueSkeleton />
) : (
<Tip raw={raw}>
<span className="text-2xl font-semibold text-gray-100 truncate">{value}</span>
<span className="text-2xl font-semibold text-fg-primary truncate">{value}</span>
</Tip>
)}
{!loading && trend && (
<span className="text-xs text-gray-500 mb-1 flex-shrink-0">{trend}</span>
<span className="text-xs text-fg-muted mb-1 flex-shrink-0">{trend}</span>
)}
</div>
</div>
+1 -1
View File
@@ -96,7 +96,7 @@ function ReasonChip({ reason }: { reason: AwaitingReason }) {
<span
className={`inline-flex items-center gap-1 rounded-full border px-1.5 text-[10px] font-medium leading-4 ${
cfg.urgent
? "bg-amber-500/15 border-amber-500/25 text-amber-300"
? "bg-status-warning/15 border-status-warning/25 text-status-warning"
: "bg-yellow-500/10 border-yellow-500/20 text-yellow-400/90"
}`}
>
+21 -15
View File
@@ -113,15 +113,17 @@ export function TabbyPanel({
<span className="text-base leading-none" aria-hidden>
🐾
</span>
<span className="text-sm font-semibold text-gray-100">Tabby</span>
<span className="text-sm font-semibold text-fg-primary">Tabby</span>
<span
className={`ml-0.5 inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 text-[10px] font-medium ${
status.connected ? "bg-emerald-500/15 text-emerald-300" : "bg-red-500/15 text-red-300"
status.connected
? "bg-status-success/15 text-status-success"
: "bg-status-danger/15 text-status-danger"
}`}
>
<span
className={`inline-block h-1.5 w-1.5 rounded-full ${
status.connected ? "bg-emerald-400" : "bg-red-500"
status.connected ? "bg-status-success" : "bg-status-danger"
}`}
aria-hidden
/>
@@ -129,7 +131,7 @@ export function TabbyPanel({
</span>
</div>
<button
className="rounded-md p-1 text-gray-500 transition-colors hover:bg-surface-4 hover:text-gray-200"
className="rounded-md p-1 text-fg-muted transition-colors hover:bg-surface-4 hover:text-fg-secondary"
onClick={onClose}
aria-label="Close Tabby"
>
@@ -191,7 +193,7 @@ export function TabbyPanel({
{/* ask */}
<form onSubmit={submit} className="flex items-center gap-1.5">
<input
className="flex-1 rounded-lg border border-border bg-surface-1 px-2.5 py-1.5 text-xs text-gray-200 placeholder-gray-500 transition-colors focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent/30"
className="flex-1 rounded-lg border border-border bg-surface-1 px-2.5 py-1.5 text-xs text-fg-secondary placeholder-fg-muted transition-colors focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent/30"
placeholder="Ask Tabby… (e.g. any errors?)"
value={query}
onChange={(e) => setQuery(e.target.value)}
@@ -206,7 +208,7 @@ export function TabbyPanel({
</button>
</form>
{answer && (
<p className="mt-2 rounded-lg bg-surface-1/70 px-2.5 py-2 text-xs leading-relaxed text-gray-300">
<p className="mt-2 rounded-lg bg-surface-1/70 px-2.5 py-2 text-xs leading-relaxed text-fg-secondary">
{answer}
</p>
)}
@@ -223,18 +225,22 @@ interface Tone {
const TONE_MUTED: Tone = {
wrap: "border-border bg-surface-1",
value: "text-gray-300",
icon: "text-gray-500",
value: "text-fg-secondary",
icon: "text-fg-muted",
};
const TONES: Record<string, Tone> = {
accent: { wrap: "border-accent/30 bg-accent/10", value: "text-gray-100", icon: "text-accent" },
accent: { wrap: "border-accent/30 bg-accent/10", value: "text-fg-primary", icon: "text-accent" },
amber: {
wrap: "border-amber-500/30 bg-amber-500/10",
value: "text-amber-200",
icon: "text-amber-400",
wrap: "border-status-warning/30 bg-status-warning/10",
value: "text-status-warning",
icon: "text-status-warning",
},
red: {
wrap: "border-status-danger/30 bg-status-danger/10",
value: "text-status-danger",
icon: "text-status-danger",
},
red: { wrap: "border-red-500/30 bg-red-500/10", value: "text-red-200", icon: "text-red-400" },
muted: TONE_MUTED,
};
@@ -256,7 +262,7 @@ function StatChip({
<span className={`text-base font-semibold leading-none tabular-nums ${t.value}`}>
{value}
</span>
<span className="text-[9px] uppercase tracking-wider text-gray-500">{label}</span>
<span className="text-[9px] uppercase tracking-wider text-fg-muted">{label}</span>
</div>
);
}
@@ -274,7 +280,7 @@ function ActionButton({
}) {
return (
<button
className="flex items-center gap-1.5 rounded-lg bg-surface-1 px-2 py-1.5 text-xs text-gray-300 transition-colors hover:bg-surface-4 hover:text-gray-100 disabled:cursor-not-allowed disabled:opacity-40"
className="flex items-center gap-1.5 rounded-lg bg-surface-1 px-2 py-1.5 text-xs text-fg-secondary transition-colors hover:bg-surface-4 hover:text-fg-primary disabled:cursor-not-allowed disabled:opacity-40"
onClick={onClick}
disabled={disabled}
>
+1 -1
View File
@@ -144,7 +144,7 @@ export function Tip({ raw, children, maxWidth = 320, block = false }: TipProps)
<div
ref={tipRef}
style={tipStyle}
className="px-2.5 py-1.5 text-[11px] leading-relaxed font-mono text-gray-100 bg-[#12121f] border border-[#2a2a4a] rounded-lg shadow-xl pointer-events-none whitespace-pre-wrap break-words"
className="px-2.5 py-1.5 text-[11px] leading-relaxed font-mono text-fg-primary bg-[#12121f] border border-[#2a2a4a] rounded-lg shadow-xl pointer-events-none whitespace-pre-wrap break-words"
>
{raw}
</div>,
+10 -10
View File
@@ -213,11 +213,11 @@ export function UpdateNotifier() {
<div className="min-w-0">
<h2
id="update-notifier-title"
className="text-sm font-semibold text-gray-100 truncate"
className="text-sm font-semibold text-fg-primary truncate"
>
{t("title")}
</h2>
<p className="text-[11px] text-gray-500 mt-0.5 font-mono truncate">
<p className="text-[11px] text-fg-muted mt-0.5 font-mono truncate">
{t("commitsBehind", { count: behind, ref: refLabel })}
</p>
</div>
@@ -226,7 +226,7 @@ export function UpdateNotifier() {
type="button"
onClick={dismiss}
aria-label={t("dismiss")}
className="p-1.5 -m-1 rounded-lg text-gray-500 hover:text-gray-200 hover:bg-surface-4 transition-colors flex-shrink-0"
className="p-1.5 -m-1 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-4 transition-colors flex-shrink-0"
>
<X className="w-4 h-4" />
</button>
@@ -234,29 +234,29 @@ export function UpdateNotifier() {
{/* Body */}
<div className="px-5 py-4 space-y-3">
<p className="text-sm text-gray-300 leading-relaxed">{t("lead")}</p>
<p className="text-sm text-fg-secondary leading-relaxed">{t("lead")}</p>
{status.fetch_error ? (
<div className="text-xs text-amber-300/90 bg-amber-500/5 border border-amber-500/20 rounded-lg px-3 py-2">
<div className="text-xs text-status-warning/90 bg-status-warning/5 border border-status-warning/20 rounded-lg px-3 py-2">
{t("fetchError")}
</div>
) : null}
{!status.git_repo ? (
<div className="text-xs text-gray-400 bg-surface-2 border border-border rounded-lg px-3 py-2">
<div className="text-xs text-fg-secondary bg-surface-2 border border-border rounded-lg px-3 py-2">
{t("notGit")}
</div>
) : null}
{status.situation_note ? (
<div className="text-xs text-amber-200/90 bg-amber-500/5 border border-amber-500/20 rounded-lg px-3 py-2 leading-relaxed">
<div className="text-xs text-status-warning/90 bg-status-warning/5 border border-status-warning/20 rounded-lg px-3 py-2 leading-relaxed">
{status.situation_note}
</div>
) : null}
{status.manual_command ? (
<pre
className="bg-surface-1 border border-border rounded-lg px-3 py-2.5 text-[11px] font-mono text-gray-200 whitespace-pre-wrap break-all leading-relaxed"
className="bg-surface-1 border border-border rounded-lg px-3 py-2.5 text-[11px] font-mono text-fg-secondary whitespace-pre-wrap break-all leading-relaxed"
aria-label={t("commandLabel")}
>
{status.manual_command}
@@ -268,11 +268,11 @@ export function UpdateNotifier() {
* are fetch-only - restarting the dashboard would change nothing. */}
{status.situation === "tracking_canonical" ||
status.situation === "fork_or_diverged_tracking" ? (
<p className="text-[11px] text-gray-500 leading-relaxed">{t("restartNote")}</p>
<p className="text-[11px] text-fg-muted leading-relaxed">{t("restartNote")}</p>
) : null}
{error ? (
<p className="text-xs text-red-400" role="alert">
<p className="text-xs text-status-danger" role="alert">
{error}
</p>
) : null}
+39 -39
View File
@@ -110,7 +110,7 @@ const TYPE_STYLES: Partial<Record<WebhookType, string>> = {
opsgenie: "text-[#2684FF] bg-[#2684FF]/10 border-[#2684FF]/20",
splunk_oncall: "text-[#F99D1C] bg-[#F99D1C]/10 border-[#F99D1C]/20",
};
const NEUTRAL_STYLE = "text-gray-300 bg-surface-2 border-border";
const NEUTRAL_STYLE = "text-fg-secondary bg-surface-2 border-border";
interface HeaderRow {
key: string;
@@ -155,7 +155,7 @@ function Toggle({
aria-label={label}
onClick={() => onChange(!checked)}
className={`relative inline-flex h-5 w-9 flex-shrink-0 items-center rounded-full transition-colors ${
checked ? "bg-blue-500" : "bg-surface-4"
checked ? "bg-blue-600" : "bg-surface-4"
}`}
>
<span
@@ -390,7 +390,7 @@ export function WebhookSettings() {
return (
<div className="card p-5 space-y-4">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2 text-xs text-gray-500">
<div className="flex items-center gap-2 text-xs text-fg-muted">
<Webhook className="w-3.5 h-3.5" />
{t("webhooks.count", { count: targets.length })}
</div>
@@ -408,9 +408,9 @@ export function WebhookSettings() {
{/* Target list */}
{loading ? (
<p className="text-xs text-gray-500">{t("webhooks.loading")}</p>
<p className="text-xs text-fg-muted">{t("webhooks.loading")}</p>
) : targets.length === 0 && !formOpen ? (
<div className="flex items-center gap-2 text-xs text-gray-500 py-2">
<div className="flex items-center gap-2 text-xs text-fg-muted py-2">
<Webhook className="w-3.5 h-3.5" />
{t("webhooks.empty")}
</div>
@@ -431,12 +431,12 @@ export function WebhookSettings() {
>
{labelOf(target.type)}
</span>
<span className="text-sm text-gray-200 font-medium">{target.name}</span>
<code className="text-[11px] text-gray-500 font-mono truncate max-w-[220px]">
<span className="text-sm text-fg-secondary font-medium">{target.name}</span>
<code className="text-[11px] text-fg-muted font-mono truncate max-w-[220px]">
{target.url_preview}
</code>
{target.rule_ids && target.rule_ids.length > 0 && (
<span className="text-[10px] text-amber-400/80">
<span className="text-[10px] text-status-warning/80">
{t("webhooks.scopedTo", { count: target.rule_ids.length })}
</span>
)}
@@ -446,8 +446,8 @@ export function WebhookSettings() {
title={target.last_delivery.error || undefined}
className={`inline-flex items-center gap-1 text-[10px] px-1.5 py-0.5 rounded-full ${
target.last_delivery.status === "success"
? "text-emerald-400 bg-emerald-500/10"
: "text-red-400 bg-red-500/10"
? "text-status-success bg-status-success/10"
: "text-status-danger bg-status-danger/10"
}`}
>
{target.last_delivery.status === "success" ? (
@@ -470,7 +470,7 @@ export function WebhookSettings() {
<button
onClick={() => onTest(target.id)}
disabled={testing === target.id}
className="inline-flex items-center gap-1.5 text-[11px] px-2.5 py-1 rounded-md text-gray-400 hover:text-gray-200 hover:bg-surface-4 border border-border transition-colors disabled:opacity-50"
className="inline-flex items-center gap-1.5 text-[11px] px-2.5 py-1 rounded-md text-fg-secondary hover:text-fg-primary hover:bg-surface-4 border border-border transition-colors disabled:opacity-50"
>
{testing === target.id ? (
<Loader2 className="w-3 h-3 animate-spin" />
@@ -481,14 +481,14 @@ export function WebhookSettings() {
</button>
<button
onClick={() => openEdit(target)}
className="inline-flex items-center gap-1.5 text-[11px] px-2.5 py-1 rounded-md text-gray-400 hover:text-gray-200 hover:bg-surface-4 border border-border transition-colors"
className="inline-flex items-center gap-1.5 text-[11px] px-2.5 py-1 rounded-md text-fg-secondary hover:text-fg-primary hover:bg-surface-4 border border-border transition-colors"
>
<Pencil className="w-3 h-3" />
{t("webhooks.edit")}
</button>
<button
onClick={() => setConfirmDelete(target.id)}
className="inline-flex items-center gap-1.5 text-[11px] px-2.5 py-1 rounded-md text-gray-500 hover:text-red-400 hover:bg-red-500/10 border border-border transition-colors"
className="inline-flex items-center gap-1.5 text-[11px] px-2.5 py-1 rounded-md text-fg-muted hover:text-status-danger hover:bg-status-danger/10 border border-border transition-colors"
>
<Trash2 className="w-3 h-3" />
{t("webhooks.delete")}
@@ -496,7 +496,7 @@ export function WebhookSettings() {
{result && (
<span
className={`inline-flex items-center gap-1 text-[11px] ${
result.ok ? "text-emerald-400" : "text-red-400"
result.ok ? "text-status-success" : "text-status-danger"
}`}
>
{result.ok ? (
@@ -522,17 +522,17 @@ export function WebhookSettings() {
{formOpen && form && provider && (
<div className="border border-border rounded-lg p-4 space-y-3 bg-surface-1">
<div className="flex items-center justify-between">
<h4 className="text-xs font-semibold text-gray-300 uppercase tracking-wide">
<h4 className="text-xs font-semibold text-fg-secondary uppercase tracking-wide">
{isEdit ? t("webhooks.editTitle") : t("webhooks.addTitle")}
</h4>
<button onClick={closeForm} className="text-gray-500 hover:text-gray-300">
<button onClick={closeForm} className="text-fg-muted hover:text-fg-secondary">
<X className="w-4 h-4" />
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<label className="block">
<span className="text-[11px] text-gray-500">{t("webhooks.fieldName")}</span>
<span className="text-[11px] text-fg-muted">{t("webhooks.fieldName")}</span>
<input
value={form.name}
onChange={(e) => set({ name: e.target.value })}
@@ -541,7 +541,7 @@ export function WebhookSettings() {
/>
</label>
<label className="block">
<span className="text-[11px] text-gray-500">{t("webhooks.fieldType")}</span>
<span className="text-[11px] text-fg-muted">{t("webhooks.fieldType")}</span>
<div className="mt-1">
<Select<WebhookType>
value={form.type}
@@ -564,12 +564,12 @@ export function WebhookSettings() {
{/* URL (hidden for providers that derive their own URL) */}
{showUrl && (
<label className="block">
<span className="text-[11px] text-gray-500">
<span className="text-[11px] text-fg-muted">
{t("webhooks.fieldUrl")}
{isEdit ? (
<span className="text-gray-600"> - {t("webhooks.urlKeepHint")}</span>
<span className="text-fg-muted"> - {t("webhooks.urlKeepHint")}</span>
) : urlOptional ? (
<span className="text-gray-600"> - {t("webhooks.urlOptional")}</span>
<span className="text-fg-muted"> - {t("webhooks.urlOptional")}</span>
) : null}
</span>
<input
@@ -583,7 +583,7 @@ export function WebhookSettings() {
</label>
)}
{!showUrl && (
<p className="text-[11px] text-gray-600 flex items-center gap-1.5">
<p className="text-[11px] text-fg-muted flex items-center gap-1.5">
<Webhook className="w-3 h-3" />
{t("webhooks.urlAuto")}
</p>
@@ -594,19 +594,19 @@ export function WebhookSettings() {
<button
type="button"
onClick={() => setGuideOpen((o) => !o)}
className="w-full flex items-center justify-between gap-2 px-3 py-2 text-[11px] text-gray-300 hover:bg-surface-3 transition-colors"
className="w-full flex items-center justify-between gap-2 px-3 py-2 text-[11px] text-fg-secondary hover:bg-surface-3 transition-colors"
>
<span className="inline-flex items-center gap-1.5">
<BookOpen className="w-3.5 h-3.5 text-gray-500" />
<BookOpen className="w-3.5 h-3.5 text-fg-muted" />
{t("webhooks.guideToggle", { provider: provider.label })}
</span>
<ChevronDown
className={`w-3.5 h-3.5 text-gray-500 transition-transform ${guideOpen ? "rotate-180" : ""}`}
className={`w-3.5 h-3.5 text-fg-muted transition-transform ${guideOpen ? "rotate-180" : ""}`}
/>
</button>
{guideOpen && (
<div className="px-3 pb-3 pt-2 space-y-2.5 border-t border-border">
<ol className="list-decimal list-inside space-y-1 text-[11px] leading-relaxed text-gray-400 marker:text-gray-600">
<ol className="list-decimal list-inside space-y-1 text-[11px] leading-relaxed text-fg-secondary marker:text-fg-muted">
{(t(`webhookGuides.${form.type}.steps`, { returnObjects: true }) as string[]).map(
(s, i) => (
<li key={i}>{s}</li>
@@ -624,7 +624,7 @@ export function WebhookSettings() {
{t("webhooks.guideDocs", { provider: provider.label })}
</a>
)}
<p className="flex items-start gap-1.5 text-[10px] text-gray-500 leading-relaxed pt-2 border-t border-border">
<p className="flex items-start gap-1.5 text-[10px] text-fg-muted leading-relaxed pt-2 border-t border-border">
<Info className="w-3 h-3 mt-0.5 flex-shrink-0" />
{t("webhooks.guideStaleNote")}
</p>
@@ -637,9 +637,9 @@ export function WebhookSettings() {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 pt-1 border-t border-border">
{provider.fields.map((f) => (
<label key={f.key} className="block">
<span className="text-[11px] text-gray-500">
<span className="text-[11px] text-fg-muted">
{f.label}
{f.required && <span className="text-red-400"> *</span>}
{f.required && <span className="text-status-danger"> *</span>}
</span>
{f.type === "enum" && f.options ? (
<div className="mt-1">
@@ -667,7 +667,7 @@ export function WebhookSettings() {
{provider.supports_secret && (
<div className="space-y-3 pt-1 border-t border-border">
<label className="block">
<span className="text-[11px] text-gray-500">{t("webhooks.fieldSecret")}</span>
<span className="text-[11px] text-fg-muted">{t("webhooks.fieldSecret")}</span>
<input
type="password"
value={form.secret}
@@ -677,18 +677,18 @@ export function WebhookSettings() {
}
className="input w-full mt-1 py-1.5 text-[11px] leading-normal font-mono"
/>
<span className="text-[10px] text-gray-600">{t("webhooks.secretHint")}</span>
<span className="text-[10px] text-fg-muted">{t("webhooks.secretHint")}</span>
</label>
<div>
<div className="flex items-center justify-between">
<span className="text-[11px] text-gray-500">{t("webhooks.fieldHeaders")}</span>
<span className="text-[11px] text-fg-muted">{t("webhooks.fieldHeaders")}</span>
{isEdit && (
<Checkbox
checked={form.replaceHeaders}
onChange={(v) => set({ replaceHeaders: v })}
label={t("webhooks.replaceHeaders")}
labelClassName="text-[10px] text-gray-500 group-hover:text-gray-400"
labelClassName="text-[10px] text-fg-muted group-hover:text-fg-secondary"
/>
)}
</div>
@@ -724,7 +724,7 @@ export function WebhookSettings() {
onClick={() =>
set({ headerRows: form.headerRows.filter((_, j) => j !== i) })
}
className="text-gray-600 hover:text-red-400 p-1"
className="text-fg-muted hover:text-status-danger p-1"
>
<X className="w-3.5 h-3.5" />
</button>
@@ -734,7 +734,7 @@ export function WebhookSettings() {
onClick={() =>
set({ headerRows: [...form.headerRows, { key: "", value: "" }] })
}
className="inline-flex items-center gap-1 text-[10px] text-gray-500 hover:text-gray-300"
className="inline-flex items-center gap-1 text-[10px] text-fg-muted hover:text-fg-secondary"
>
<Plus className="w-3 h-3" />
{t("webhooks.addHeader")}
@@ -752,7 +752,7 @@ export function WebhookSettings() {
checked={form.scopeAll}
onChange={(v) => set({ scopeAll: v })}
label={t("webhooks.scopeAll")}
labelClassName="text-[11px] text-gray-400 group-hover:text-gray-300"
labelClassName="text-[11px] text-fg-secondary group-hover:text-fg-secondary"
/>
{!form.scopeAll && (
<div className="mt-2 grid grid-cols-1 sm:grid-cols-2 gap-1">
@@ -768,7 +768,7 @@ export function WebhookSettings() {
})
}
label={rule.name}
labelClassName="text-[11px] text-gray-400 group-hover:text-gray-300 truncate"
labelClassName="text-[11px] text-fg-secondary group-hover:text-fg-secondary truncate"
/>
))}
</div>
@@ -777,14 +777,14 @@ export function WebhookSettings() {
)}
<div className="flex items-center gap-3 pt-1">
<label className="inline-flex items-center gap-2 text-[11px] text-gray-400 cursor-pointer">
<label className="inline-flex items-center gap-2 text-[11px] text-fg-secondary cursor-pointer">
<Toggle checked={form.enabled} onChange={(v) => set({ enabled: v })} />
{t("webhooks.enabledOnSave")}
</label>
</div>
{formError && (
<div className="flex items-center gap-1.5 text-[11px] text-red-400">
<div className="flex items-center gap-1.5 text-[11px] text-status-danger">
<AlertTriangle className="w-3.5 h-3.5" />
{formError}
</div>
@@ -206,7 +206,7 @@ describe("AgentCard", () => {
it("should not render subagent_type when null", () => {
const { container } = renderCard(<AgentCard agent={makeAgent({ subagent_type: null })} />);
// Only the name should be in the name container, no subagent type
expect(container.querySelectorAll(".text-\\[11px\\].text-gray-500.truncate")).toHaveLength(0);
expect(container.querySelectorAll(".text-\\[11px\\].text-fg-muted.truncate")).toHaveLength(0);
});
it("should render task when present", () => {
@@ -44,10 +44,10 @@ describe("StatCard", () => {
it("should apply custom accent color", () => {
const { container } = render(
<StatCard label="Test" value={0} icon={Activity} accentColor="text-emerald-400" />
<StatCard label="Test" value={0} icon={Activity} accentColor="text-status-success" />
);
const svg = container.querySelector("svg");
expect(svg?.className?.baseVal ?? svg?.getAttribute("class")).toContain("text-emerald-400");
expect(svg?.className?.baseVal ?? svg?.getAttribute("class")).toContain("text-status-success");
});
it("should apply default accent color when not specified", () => {
@@ -136,9 +136,9 @@ describe("awaiting-reason suffix", () => {
it("marks urgent reasons with the hotter amber tint", () => {
const { container } = render(<AgentStatusBadge status="waiting" reason="notification" />);
expect(container.querySelector(".text-amber-300")).toBeInTheDocument();
expect(container.querySelector(".text-status-warning")).toBeInTheDocument();
const { container: calm } = render(<AgentStatusBadge status="waiting" reason="stop" />);
expect(calm.querySelector(".text-amber-300")).not.toBeInTheDocument();
expect(calm.querySelector(".text-status-warning")).not.toBeInTheDocument();
});
it("compact mode suppresses the inline chip but keeps the hover tooltip", () => {
@@ -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>
)}
@@ -130,7 +130,7 @@ export function CopyButton({ text }: { text: string }) {
<button
type="button"
onClick={copy}
className="flex items-center gap-1 text-[10px] py-0.5 px-1.5 rounded text-gray-400 hover:text-gray-200 hover:bg-surface-2 cursor-pointer"
className="flex items-center gap-1 text-[10px] py-0.5 px-1.5 rounded text-fg-secondary hover:text-fg-primary hover:bg-surface-2 cursor-pointer"
aria-label={t("eventDetail.copy")}
>
{copied ? <Check className="w-3 h-3" /> : <Copy className="w-3 h-3" />}
@@ -145,13 +145,13 @@ export function Terminal({ command, description }: { command: string; descriptio
return (
<div className="relative bg-black/70 border border-border rounded font-mono text-[11px] overflow-hidden">
<div className="flex items-center justify-between px-3 py-1 border-b border-border bg-black/40">
<span className="text-gray-500 text-[10px] uppercase tracking-wide">terminal</span>
<span className="text-fg-muted text-[10px] uppercase tracking-wide">terminal</span>
<CopyButton text={command} />
</div>
<pre className="px-3 py-2 text-gray-200 whitespace-pre-wrap break-words">
{description && <div className="text-gray-500 mb-1"># {description}</div>}
<pre className="px-3 py-2 text-fg-secondary whitespace-pre-wrap break-words">
{description && <div className="text-fg-muted mb-1"># {description}</div>}
<div>
<span className="text-emerald-400 select-none">$ </span>
<span className="text-status-success select-none">$ </span>
{command}
</div>
</pre>
@@ -176,11 +176,14 @@ export function TerminalOutput({
const hasStderr = typeof stderr === "string" && stderr.length > 0;
const flag =
interrupted === true
? { label: "interrupted", color: "text-red-400 border-red-500/40 bg-red-500/10" }
? {
label: "interrupted",
color: "text-status-danger border-status-danger/40 bg-status-danger/10",
}
: typeof exitCode === "number" && exitCode !== 0
? {
label: `exit ${exitCode}`,
color: "text-red-400 border-red-500/40 bg-red-500/10",
color: "text-status-danger border-status-danger/40 bg-status-danger/10",
}
: null;
@@ -208,11 +211,11 @@ function OutputBlock({
text: string;
variant: "out" | "err";
}) {
const color = variant === "err" ? "text-red-300" : "text-gray-200";
const color = variant === "err" ? "text-status-danger" : "text-fg-secondary";
return (
<div className="relative bg-black/70 border border-border rounded font-mono text-[11px] overflow-hidden">
<div className="flex items-center justify-between px-3 py-1 border-b border-border bg-black/40">
<span className="text-gray-500 text-[10px] uppercase tracking-wide">{label}</span>
<span className="text-fg-muted text-[10px] uppercase tracking-wide">{label}</span>
<CopyButton text={text} />
</div>
<pre className={`px-3 py-2 whitespace-pre-wrap break-words max-h-96 overflow-auto ${color}`}>
@@ -240,7 +243,7 @@ export function LineNumberedCode({
<div className="relative bg-black/70 border border-border rounded font-mono text-[11px] overflow-hidden">
{label && (
<div className="flex items-center justify-between px-3 py-1 border-b border-border bg-black/40">
<span className="text-gray-500 text-[10px] uppercase tracking-wide">{label}</span>
<span className="text-fg-muted text-[10px] uppercase tracking-wide">{label}</span>
<CopyButton text={text} />
</div>
)}
@@ -249,10 +252,10 @@ export function LineNumberedCode({
<tbody>
{lines.map((line, i) => (
<tr key={i}>
<td className="px-2 text-right text-gray-600 select-none bg-black/40 border-r border-border align-top">
<td className="px-2 text-right text-fg-muted select-none bg-black/40 border-r border-border align-top">
{i + startLine}
</td>
<td className="px-3 text-gray-200 whitespace-pre-wrap break-words">{line}</td>
<td className="px-3 text-fg-secondary whitespace-pre-wrap break-words">{line}</td>
</tr>
))}
</tbody>
@@ -274,7 +277,7 @@ export type DiffHunk = {
export function UnifiedDiff({ hunks }: { hunks: DiffHunk[] }) {
if (hunks.length === 0) {
return <p className="text-[11px] text-gray-500 italic">no diff</p>;
return <p className="text-[11px] text-fg-muted italic">no diff</p>;
}
return (
<div className="relative bg-black/70 border border-border rounded font-mono text-[11px] overflow-hidden">
@@ -306,17 +309,17 @@ function HunkView({ hunk }: { hunk: DiffHunk }) {
kind === "add"
? "bg-green-500/10 text-green-200"
: kind === "remove"
? "bg-red-500/10 text-red-200"
: "text-gray-300";
? "bg-status-danger/10 text-status-danger"
: "text-fg-secondary";
const oldCell = showOld ? oldLine++ : "";
const newCell = showNew ? newLine++ : "";
const sign = kind === "add" ? "+" : kind === "remove" ? "-" : " ";
return (
<tr key={i} className={rowBg}>
<td className="px-2 text-right text-gray-600 select-none border-r border-border/40 w-[36px]">
<td className="px-2 text-right text-fg-muted select-none border-r border-border/40 w-[36px]">
{oldCell}
</td>
<td className="px-2 text-right text-gray-600 select-none border-r border-border/40 w-[36px]">
<td className="px-2 text-right text-fg-muted select-none border-r border-border/40 w-[36px]">
{newCell}
</td>
<td className="px-1 text-center select-none w-[16px]">{sign}</td>
@@ -347,7 +350,7 @@ export function KeyValueCard({
const ordered = [...priorityEntries, ...restEntries];
if (ordered.length === 0) {
return <p className="text-[11px] text-gray-500 italic">empty</p>;
return <p className="text-[11px] text-fg-muted italic">empty</p>;
}
return (
@@ -355,10 +358,10 @@ export function KeyValueCard({
<tbody>
{ordered.map(([k, v], i) => (
<tr key={k} className={i > 0 ? "border-t border-border" : ""}>
<td className="text-gray-500 align-top py-1.5 px-2 font-mono bg-surface-3/60 w-[28%] break-all">
<td className="text-fg-muted align-top py-1.5 px-2 font-mono bg-surface-3/60 w-[28%] break-all">
{k}
</td>
<td className="text-gray-300 align-top py-1.5 px-2">
<td className="text-fg-secondary align-top py-1.5 px-2">
<ValueCell value={v} />
</td>
</tr>
@@ -369,32 +372,33 @@ export function KeyValueCard({
}
function ValueCell({ value }: { value: unknown }) {
if (value == null) return <span className="text-gray-500 italic">null</span>;
if (value == null) return <span className="text-fg-muted italic">null</span>;
if (typeof value === "boolean")
return (
<span
className={`inline-block px-2 py-0.5 rounded border text-[11px] font-mono ${
value
? "text-green-400 border-green-500/30 bg-green-500/10"
: "text-gray-400 border-gray-500/30 bg-gray-500/10"
: "text-fg-secondary border-border-light/30 bg-surface-4/10"
}`}
>
{String(value)}
</span>
);
if (typeof value === "number") return <span className="font-mono text-gray-300">{value}</span>;
if (typeof value === "number")
return <span className="font-mono text-fg-secondary">{value}</span>;
if (typeof value === "string") {
if (value.length > 120 || value.includes("\n")) {
return (
<pre className="bg-surface-3 text-gray-300 text-[11px] font-mono p-2 rounded border border-border whitespace-pre-wrap break-words max-h-48 overflow-auto">
<pre className="bg-surface-3 text-fg-secondary text-[11px] font-mono p-2 rounded border border-border whitespace-pre-wrap break-words max-h-48 overflow-auto">
{value}
</pre>
);
}
return <span className="font-mono text-gray-300 break-all">{value}</span>;
return <span className="font-mono text-fg-secondary break-all">{value}</span>;
}
if (Array.isArray(value)) {
if (value.length === 0) return <span className="text-gray-500 italic">[]</span>;
if (value.length === 0) return <span className="text-fg-muted italic">[]</span>;
return (
<ol className="list-decimal pl-4 space-y-1">
{value.map((item, i) => (
@@ -406,7 +410,7 @@ function ValueCell({ value }: { value: unknown }) {
);
}
return (
<pre className="bg-surface-3 text-gray-300 text-[11px] font-mono p-2 rounded border border-border whitespace-pre-wrap break-words max-h-48 overflow-auto">
<pre className="bg-surface-3 text-fg-secondary text-[11px] font-mono p-2 rounded border border-border whitespace-pre-wrap break-words max-h-48 overflow-auto">
{safeStringify(value)}
</pre>
);
@@ -423,11 +427,11 @@ function safeStringify(value: unknown): string {
// ───────────────────────── File list / match list ─────────────────────────
export function FileList({ paths }: { paths: string[] }) {
if (paths.length === 0) return <p className="text-[11px] text-gray-500 italic">no files</p>;
if (paths.length === 0) return <p className="text-[11px] text-fg-muted italic">no files</p>;
return (
<ul className="divide-y divide-border border border-border rounded overflow-hidden text-[11px] max-h-80 overflow-y-auto">
{paths.map((p, i) => (
<li key={i} className="px-3 py-1 font-mono text-gray-300 break-all">
<li key={i} className="px-3 py-1 font-mono text-fg-secondary break-all">
{p}
</li>
))}
@@ -442,14 +446,14 @@ export type GrepMatch = {
};
export function MatchList({ matches }: { matches: GrepMatch[] }) {
if (matches.length === 0) return <p className="text-[11px] text-gray-500 italic">no matches</p>;
if (matches.length === 0) return <p className="text-[11px] text-fg-muted italic">no matches</p>;
return (
<ul className="divide-y divide-border border border-border rounded overflow-hidden text-[11px] max-h-80 overflow-y-auto font-mono">
{matches.map((m, i) => (
<li key={i} className="px-3 py-1 text-gray-300 break-all">
<li key={i} className="px-3 py-1 text-fg-secondary break-all">
{m.file && <span className="text-cyan-300">{m.file}</span>}
{m.line != null && <span className="text-gray-500">:{m.line}</span>}
{m.text && <span className="text-gray-400">: {m.text}</span>}
{m.line != null && <span className="text-fg-muted">:{m.line}</span>}
{m.text && <span className="text-fg-secondary">: {m.text}</span>}
</li>
))}
</ul>
@@ -397,9 +397,9 @@ export function ToolResponseView({
{hunks.length > 0 && <UnifiedDiff hunks={hunks} />}
{originalFile && (
<details className="bg-surface-2/40 border border-border rounded overflow-hidden">
<summary className="cursor-pointer select-none px-3 py-1.5 text-[11px] text-gray-400 hover:text-gray-200 hover:bg-surface-2">
<summary className="cursor-pointer select-none px-3 py-1.5 text-[11px] text-fg-secondary hover:text-fg-primary hover:bg-surface-2">
<span className="font-semibold uppercase tracking-wide">original file</span>
<span className="text-gray-500 font-normal ml-2">
<span className="text-fg-muted font-normal ml-2">
({originalFile.split(/\r?\n/).length} lines)
</span>
</summary>
+9 -9
View File
@@ -130,7 +130,7 @@ export function AddLaneModal({
>
<div className="space-y-3">
<div>
<label className="mb-1 block text-xs text-neutral-400" htmlFor="add-lane-repo">
<label className="mb-1 block text-xs text-fg-secondary" htmlFor="add-lane-repo">
{t("addLaneRepoLabel")}
</label>
<CwdAutocomplete
@@ -139,11 +139,11 @@ export function AddLaneModal({
onChange={setSourceRepo}
suggestions={cwdSuggestions}
/>
<p className="mt-1 text-[10px] text-neutral-500">{t("addLaneRepoHint")}</p>
<p className="mt-1 text-[10px] text-fg-muted">{t("addLaneRepoHint")}</p>
</div>
<div>
<label className="mb-1 block text-xs text-neutral-400" htmlFor="add-lane-title">
<label className="mb-1 block text-xs text-fg-secondary" htmlFor="add-lane-title">
{t("addLaneTitleLabel")}
</label>
<input
@@ -151,23 +151,23 @@ export function AddLaneModal({
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder={t("addLaneTitlePlaceholder")}
className="w-full rounded-md border border-neutral-700 bg-neutral-900 px-3 py-1.5 text-xs text-neutral-100 placeholder:text-neutral-600 focus:border-blue-400 focus:outline-none"
className="w-full rounded-md border border-border-light bg-surface-0 px-3 py-1.5 text-xs text-fg-primary placeholder:text-fg-muted focus:border-blue-500 focus:outline-none"
/>
</div>
{branches && (
<div>
<label className="mb-1 block text-xs text-neutral-400" htmlFor="add-lane-base">
<label className="mb-1 block text-xs text-fg-secondary" htmlFor="add-lane-base">
{t("addLaneBaseLabel")}
</label>
{branches.length === 0 ? (
<p className="text-[10px] text-neutral-500">{t("addLaneNoBranches")}</p>
<p className="text-[10px] text-fg-muted">{t("addLaneNoBranches")}</p>
) : (
<select
id="add-lane-base"
value={base}
onChange={(e) => setBase(e.target.value)}
className="w-full rounded-md border border-neutral-700 bg-neutral-900 px-3 py-1.5 text-xs text-neutral-100 focus:border-blue-400 focus:outline-none"
className="w-full rounded-md border border-border-light bg-surface-0 px-3 py-1.5 text-xs text-fg-primary focus:border-blue-500 focus:outline-none"
>
{branches.map((b) => (
<option key={b} value={b}>
@@ -179,11 +179,11 @@ export function AddLaneModal({
</div>
)}
{branchesError && !branches && (
<p className="text-[10px] text-amber-400">{branchesError}</p>
<p className="text-[10px] text-status-warning">{branchesError}</p>
)}
{error && (
<p role="alert" className="text-xs text-red-400">
<p role="alert" className="text-xs text-status-danger">
{error}
</p>
)}
@@ -153,18 +153,18 @@ export function DestructiveLaneModal({
onConfirm({ expect: expectFor(preflight), ...(force ? { force: true as const } : {}) });
}}
>
{loading && <p className="mt-3 text-xs text-neutral-400">{t("destructive.loading")}</p>}
{loading && <p className="mt-3 text-xs text-fg-secondary">{t("destructive.loading")}</p>}
{error && (
<p className="mt-3 text-xs text-red-300">
<p className="mt-3 text-xs text-status-danger">
{t("preflightErrorWithMessage", { message: error })}
</p>
)}
{preflight && (
<table className="mt-3 w-full text-left text-xs text-neutral-300">
<table className="mt-3 w-full text-left text-xs text-fg-secondary">
<tbody>
{facts.map(([name, value]) => (
<tr key={name} className="border-t border-neutral-800">
<th scope="row" className="py-1.5 font-medium text-neutral-400">
<tr key={name} className="border-t border-border">
<th scope="row" className="py-1.5 font-medium text-fg-secondary">
{t(`destructive.count.${name}`)}
</th>
<td className="py-1.5 text-right tabular-nums">{value ?? "—"}</td>
@@ -172,8 +172,8 @@ export function DestructiveLaneModal({
))}
{/* Not part of `expect`: an estimate the server never verifies. */}
{purge && (
<tr className="border-t border-neutral-800">
<th scope="row" className="py-1.5 font-medium text-neutral-400">
<tr className="border-t border-border">
<th scope="row" className="py-1.5 font-medium text-fg-secondary">
{t("destructive.count.bytesEstimate")}
</th>
<td className="py-1.5 text-right tabular-nums">
@@ -185,19 +185,19 @@ export function DestructiveLaneModal({
</table>
)}
{blocked && (
<p className="mt-3 rounded border border-amber-700/60 bg-amber-950/30 px-2 py-1.5 text-xs text-amber-200">
<p className="mt-3 rounded border border-status-warning/60 bg-status-warning/30 px-2 py-1.5 text-xs text-status-warning">
{t(`destructive.blocked.${blocked}`)}
</p>
)}
{purge?.activeSessionSkipped && (
<p className="mt-3 rounded border border-neutral-700 bg-neutral-800/50 px-2 py-1.5 text-xs text-neutral-300">
<p className="mt-3 rounded border border-border-light bg-surface-2/50 px-2 py-1.5 text-xs text-fg-secondary">
{t("destructive.notice.activeSessionSkipped")}
</p>
)}
{noticesFor(preflight).map((notice) => (
<p
key={notice}
className="mt-3 rounded border border-neutral-700 bg-neutral-800/50 px-2 py-1.5 text-xs text-neutral-300"
className="mt-3 rounded border border-border-light bg-surface-2/50 px-2 py-1.5 text-xs text-fg-secondary"
>
{t(`destructive.notice.${notice}`)}
</p>
@@ -205,13 +205,13 @@ export function DestructiveLaneModal({
{warningsFor(preflight).map((warning) => (
<p
key={warning}
className="mt-3 rounded border border-neutral-700 bg-neutral-800/50 px-2 py-1.5 text-xs text-neutral-300"
className="mt-3 rounded border border-border-light bg-surface-2/50 px-2 py-1.5 text-xs text-fg-secondary"
>
{t(`destructive.warning.${warning}`)}
</p>
))}
{requiresForce && (
<label className="mt-3 flex items-start gap-2 text-xs text-amber-200">
<label className="mt-3 flex items-start gap-2 text-xs text-status-warning">
<input
type="checkbox"
checked={force}
@@ -222,7 +222,7 @@ export function DestructiveLaneModal({
</label>
)}
{action === "reset" && (
<p className="mt-3 text-xs text-neutral-400">{t("destructive.reset.survives")}</p>
<p className="mt-3 text-xs text-fg-secondary">{t("destructive.reset.survives")}</p>
)}
</ConfirmModal>
);
+96 -122
View File
@@ -1,11 +1,13 @@
/**
* @file One lane's card: title, stage badge with time-on-phase, progress bar,
* branch/CI/PR facts, the "needs you" banner sourced from Claude Code's
* Notification hook, and the control row. A dead lane (its driving session went
* silent while it should have been working) is called out loudly — that is the
* failure this view exists to catch. An "auto: <stage>" chip appears only when
* the server's detected stage is ahead of the agent's own declaration — when the
* declaration leads or matches, it stays the sole headline.
* @file One lane's card: title, progress bar with time-on-phase, branch/CI/PR
* facts, the "needs you" banner sourced from Claude Code's Notification hook,
* and the control row — which ends in the two deletions (lane, history) as
* plain buttons, each gated by DestructiveLaneModal. A dead lane (its driving
* session went silent while it should have been working) is called out
* loudly — that is the failure this view exists to catch. Stage, kind, and
* the "auto: <stage>" chip are NOT repeated here — this card only ever
* renders inside the Workspace lane-detail section, whose header above it
* already shows them.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
@@ -54,9 +56,9 @@ function useLaneGitFacts(laneId: number): LaneGitFacts | null {
}
const LIVENESS_DOT: Record<Lane["liveness"], string> = {
active: "bg-emerald-400",
idle: "bg-neutral-500",
dead: "bg-red-500",
active: "bg-status-success",
idle: "bg-surface-4",
dead: "bg-status-danger",
};
function since(sec: number | null): string {
@@ -66,15 +68,6 @@ function since(sec: number | null): string {
return `${Math.floor(sec / 3600)}h ${Math.floor((sec % 3600) / 60)}m`;
}
/** Whether `lane.detected_stage` is strictly ahead of `lane.stage` in the
* pipeline's node order. Unknown ids sort as "not found" (-1), so an unmatched
* detected stage never outranks a matched declaration. */
function detectionLeadsDeclaration(lane: Lane): boolean {
if (!lane.detected_stage) return false;
const ids = lane.pipeline_nodes.map((n) => n.id);
return ids.indexOf(lane.detected_stage) > ids.indexOf(lane.stage);
}
export default function LaneCard({
lane,
onAction,
@@ -93,15 +86,15 @@ export default function LaneCard({
<>
<div
data-testid={`lane-card-${lane.id}`}
className="flex flex-col rounded-xl border border-neutral-800 bg-neutral-900/70 p-4 transition-colors hover:border-neutral-700"
className="flex flex-col rounded-xl border border-border bg-surface-4 p-4 transition-colors hover:border-border-light"
>
{/* Identity strip: which lane, and is it alive. Kept on one line and in
uppercase so a wall of cards can be scanned vertically. */}
<div className="mb-3 flex items-center justify-between">
<span className="text-[11px] font-semibold uppercase tracking-widest text-neutral-500">
<span className="text-[11px] font-semibold uppercase tracking-widest text-fg-muted">
{t("cardId", { id: lane.id })}
</span>
<span className="flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-neutral-300">
<span className="flex items-center gap-1.5 text-[11px] font-medium uppercase tracking-wide text-fg-secondary">
<span className={`h-2 w-2 rounded-full ${LIVENESS_DOT[lane.liveness]}`} />
{/* i18next returns the KEY on a miss, so `|| raw` never fires and a
non-standard status rendered as the literal "status.foo".
@@ -112,81 +105,57 @@ export default function LaneCard({
</span>
</div>
<h3 className="mb-3 text-[15px] font-semibold leading-snug text-neutral-50">
<h3 className="mb-3 text-[15px] font-semibold leading-snug text-fg-primary">
{lane.title || lane.cwd}
</h3>
{/* Stage line: the declared stage, how far through, and how long it has
been sitting there — the three facts that say whether a lane is
moving. The inferred chip sits beside them, never instead of them. */}
{/* Progress line: how far through the pipeline and how long the
current stage has been sitting there. The stage's own name is
already in the Workspace header above; not repeated here. */}
<div className="mb-2 flex items-center gap-2 text-xs">
<span
data-testid="lane-stage"
className="rounded bg-neutral-800 px-2 py-0.5 font-medium text-neutral-200"
>
{lane.stage}
</span>
<div
className={`h-1 flex-1 overflow-hidden rounded-full ${
lane.progress > 0 ? "bg-neutral-800" : "bg-transparent"
lane.progress > 0 ? "bg-surface-2" : "bg-transparent"
}`}
>
<div
data-testid="lane-progress-fill"
className="h-full rounded-full bg-blue-500 transition-[width]"
className="h-full rounded-full bg-blue-600 transition-[width]"
style={{ width: `${lane.progress}%` }}
/>
</div>
{lane.progress > 0 && (
<span className="tabular-nums text-neutral-400">{lane.progress}%</span>
<span className="tabular-nums text-fg-secondary">{lane.progress}%</span>
)}
<span className="tabular-nums text-neutral-600">{since(lane.stage_seconds)}</span>
<span className="tabular-nums text-fg-muted">{since(lane.stage_seconds)}</span>
</div>
<div className="mb-3 flex flex-wrap items-center gap-1.5 text-[11px]">
<span
className={`rounded px-1.5 py-0.5 ${
lane.kind === "managed"
? "bg-blue-500/15 text-blue-300"
: "bg-violet-500/15 text-violet-300"
}`}
>
{t(`kind.${lane.kind}`)}
</span>
{detectionLeadsDeclaration(lane) && (
<span
data-testid="lane-auto-stage"
title={lane.detected_signal || undefined}
className="rounded border border-dashed border-amber-500 px-1.5 py-0.5 text-amber-300"
>
{t("autoStage", { stage: lane.detected_stage })}
</span>
)}
{lane.ci_status && (
<span className="rounded bg-neutral-800 px-1.5 py-0.5 text-neutral-300">
{lane.ci_status && (
<div className="mb-3 flex flex-wrap items-center gap-1.5 text-[11px]">
<span className="rounded bg-surface-2 px-1.5 py-0.5 text-fg-secondary">
CI {lane.ci_status}
</span>
)}
</div>
</div>
)}
{lane.needs_action && (
<div className="mb-3 rounded border border-amber-600/50 bg-amber-500/10 px-2 py-1.5 text-xs text-amber-300">
<div className="mb-3 rounded border border-status-warning/50 bg-status-warning/10 px-2 py-1.5 text-xs text-status-warning">
{lane.needs_action}
</div>
)}
<dl className="mb-3 space-y-1 font-mono text-[11px] text-neutral-400">
<dl className="mb-3 space-y-1 font-mono text-[11px] text-fg-secondary">
{git?.available && (
<div data-testid="lane-git" className="space-y-1">
<div className="truncate">
{git.branch}
<span className="ml-2 text-neutral-500">{git.head}</span>
<span className="ml-2 text-fg-muted">{git.head}</span>
</div>
<div className="truncate text-neutral-500" title={git.subject}>
<div className="truncate text-fg-muted" title={git.subject}>
{git.subject}
</div>
{(git.dirty > 0 || git.untracked > 0) && (
<div className="text-amber-400/80">
<div className="text-status-warning/80">
{t("git.uncommitted", { dirty: git.dirty, untracked: git.untracked })}
</div>
)}
@@ -195,14 +164,14 @@ export default function LaneCard({
{/* The lane's own recorded branch, shown only when git could not be
read — otherwise it duplicates the live branch above. */}
{!git?.available && lane.branch && <div className="truncate"> {lane.branch}</div>}
<div className="truncate text-neutral-500" title={lane.cwd}>
<div className="truncate text-fg-muted" title={lane.cwd}>
{lane.cwd}
</div>
</dl>
{/* mt-auto pins the controls to the bottom so cards of differing height
in one grid row still line their buttons up. */}
<div className="mt-auto flex items-center gap-1 border-t border-neutral-800 pt-2 text-xs">
<div className="mt-auto flex items-center gap-1 border-t border-border pt-2 text-xs">
{(["start", "stop", "clear"] as const).map((a) => (
<button
key={a}
@@ -214,77 +183,82 @@ export default function LaneCard({
}}
className={`rounded px-2 py-1 transition-colors ${
a === "start"
? "bg-blue-500/15 text-blue-300 hover:bg-blue-500/25"
: "text-neutral-400 hover:bg-neutral-800 hover:text-neutral-200"
? "bg-blue-600/15 text-blue-400 hover:bg-blue-600/25"
: "text-fg-secondary hover:bg-surface-2 hover:text-fg-secondary"
}`}
title={a === "start" ? t("tooltipStart") : undefined}
>
{t(`action.${a}`)}
</button>
))}
{/* Destructive verbs sit behind a menu so the card is not a wall of
red. Red is kept for the items inside, where it means something. */}
<div className="relative ml-auto">
{/* Deleting the lane and deleting its history are each their own
button, by request: hiding "delete" behind a ⋯ made it unfindable,
and the one label that read as "delete" was `clear`. Neither fires
directly — both open DestructiveLaneModal, which shows the exact
counts and demands a confirmation. `reset` stays in the menu; it
is the rare one and only exists for managed lanes. */}
<div className="ml-auto flex items-center gap-1">
<button
type="button"
data-testid="lane-more"
aria-haspopup="menu"
aria-expanded={menuOpen}
data-testid="lane-action-purge"
onClick={(e) => {
e.stopPropagation();
setMenuOpen((v) => !v);
setDestructiveAction("purge");
}}
className="rounded px-2 py-1 text-neutral-500 hover:bg-neutral-800 hover:text-neutral-200"
title={t("moreActions")}
className="rounded px-2 py-1 text-status-danger/80 transition-colors hover:bg-status-danger/10 hover:text-status-danger"
>
{t("action.purge")}
</button>
{menuOpen && (
<div
role="menu"
className="absolute right-0 z-10 mt-1 min-w-40 overflow-hidden rounded-md border border-neutral-700 bg-neutral-900 py-1 shadow-lg shadow-black/40"
>
{lane.kind === "managed" && (
<button
type="button"
role="menuitem"
data-testid="lane-action-reset"
onClick={(e) => {
e.stopPropagation();
setMenuOpen(false);
setDestructiveAction("reset");
}}
className="block w-full px-3 py-1.5 text-left text-amber-300 hover:bg-amber-500/10"
<button
type="button"
data-testid="lane-action-remove"
onClick={(e) => {
e.stopPropagation();
setDestructiveAction("remove");
}}
className="rounded px-2 py-1 text-status-danger transition-colors hover:bg-status-danger/15 hover:text-status-danger"
>
{lane.kind === "adopted" ? t("action.forget") : t("action.remove")}
</button>
{/* Adopted lanes have no worktree to reset, so the menu would be
empty — it is not rendered at all rather than opening onto
nothing. */}
{lane.kind === "managed" && (
<div className="relative">
<button
type="button"
data-testid="lane-more"
aria-haspopup="menu"
aria-expanded={menuOpen}
onClick={(e) => {
e.stopPropagation();
setMenuOpen((v) => !v);
}}
className="rounded px-2 py-1 text-fg-muted hover:bg-surface-2 hover:text-fg-secondary"
title={t("moreActions")}
>
</button>
{menuOpen && (
<div
role="menu"
className="absolute right-0 z-10 mt-1 min-w-40 overflow-hidden rounded-md border border-border-light bg-surface-0 py-1 shadow-lg shadow-black/40"
>
{t("action.reset")}
</button>
<button
type="button"
role="menuitem"
data-testid="lane-action-reset"
onClick={(e) => {
e.stopPropagation();
setMenuOpen(false);
setDestructiveAction("reset");
}}
className="block w-full px-3 py-1.5 text-left text-status-warning hover:bg-status-warning/10"
>
{t("action.reset")}
</button>
</div>
)}
<button
type="button"
role="menuitem"
data-testid="lane-action-remove"
onClick={(e) => {
e.stopPropagation();
setMenuOpen(false);
setDestructiveAction("remove");
}}
className="block w-full px-3 py-1.5 text-left text-red-400 hover:bg-red-500/10"
>
{lane.kind === "adopted" ? t("action.forget") : t("action.remove")}
</button>
<button
type="button"
role="menuitem"
data-testid="lane-action-purge"
onClick={(e) => {
e.stopPropagation();
setMenuOpen(false);
setDestructiveAction("purge");
}}
className="block w-full px-3 py-1.5 text-left text-red-400 hover:bg-red-500/10"
>
{t("action.purge")}
</button>
</div>
)}
</div>
+14 -14
View File
@@ -11,9 +11,9 @@ import { useTranslation } from "react-i18next";
import type { Lane } from "../../lib/types";
const LIVENESS_DOT: Record<Lane["liveness"], string> = {
active: "bg-emerald-400",
idle: "bg-neutral-500",
dead: "bg-red-500",
active: "bg-status-success",
idle: "bg-surface-4",
dead: "bg-status-danger",
};
/** Whether the inferred stage sits ahead of the declared one in node order. */
@@ -42,49 +42,49 @@ export default function LaneStripCard({
aria-pressed={selected}
onClick={onSelect}
title={lane.cwd}
className={`w-56 shrink-0 snap-start rounded-lg border p-3 text-left transition-colors ${
className={`w-56 shrink-0 snap-start rounded-lg border p-3 text-left shadow-sm transition-colors ${
selected
? "border-blue-400/70 bg-blue-500/[0.07]"
: "border-neutral-800 bg-neutral-900/60 hover:border-neutral-700"
? "border-accent bg-accent/10"
: "border-border bg-surface-2 hover:border-border-light hover:bg-surface-3"
}`}
>
<div className="mb-1.5 flex items-center gap-1.5">
<span className={`h-2 w-2 shrink-0 rounded-full ${LIVENESS_DOT[lane.liveness]}`} />
<span className="text-[10px] font-semibold uppercase tracking-widest text-neutral-500">
<span className="text-[10px] font-semibold uppercase tracking-widest text-fg-muted">
{t("cardId", { id: lane.id })}
</span>
{lane.needs_action && (
<span className="ml-auto text-amber-400" title={lane.needs_action}>
<span className="ml-auto text-status-warning" title={lane.needs_action}>
</span>
)}
</div>
<div className="mb-2 truncate text-[13px] font-medium text-neutral-100">
<div className="mb-2 truncate text-[13px] font-medium text-fg-primary">
{lane.title || lane.cwd}
</div>
<div className="flex items-center gap-1.5 text-[11px]">
<span className="truncate rounded bg-neutral-800 px-1.5 py-0.5 text-neutral-300">
<span className="truncate rounded bg-surface-4 px-1.5 py-0.5 text-fg-secondary">
{lane.stage}
</span>
{detectionLeads(lane) && (
<span
data-testid={`lane-tile-auto-${lane.id}`}
title={lane.detected_signal || undefined}
className="shrink-0 rounded border border-dashed border-amber-500 px-1 text-amber-300"
className="shrink-0 rounded border border-dashed border-status-warning/50 px-1 text-status-warning"
>
{lane.detected_stage}
</span>
)}
{lane.progress > 0 && (
<span className="ml-auto shrink-0 tabular-nums text-neutral-500">{lane.progress}%</span>
<span className="ml-auto shrink-0 tabular-nums text-fg-muted">{lane.progress}%</span>
)}
</div>
{lane.progress > 0 && (
<div className="mt-1.5 h-0.5 overflow-hidden rounded-full bg-neutral-800">
<div className="h-full rounded-full bg-blue-500" style={{ width: `${lane.progress}%` }} />
<div className="mt-1.5 h-0.5 overflow-hidden rounded-full bg-surface-4">
<div className="h-full rounded-full bg-accent" style={{ width: `${lane.progress}%` }} />
</div>
)}
</button>
+21 -16
View File
@@ -2,28 +2,33 @@
* @file The lane pipeline map: a horizontal chain of stage nodes coloured by
* state. Layout is computed from the node list (flex + connectors), never from
* hardcoded coordinates, so a lane can use a longer or shorter template without
* touching this component. "passed without evidence" is deliberately its own
* colour: a stage the agent claimed but left no artifact for is not the same as
* a stage that is genuinely done. A `detected` node (the server's heuristic saw
* tool-event evidence but the agent never declared it) gets a FOURTH treatment —
* dashed amber, overriding whatever `state` it carries — because it must never
* be mistaken for the solid green of a real "done".
* touching this component. Every state but `current` shares one visual
* language — coloured border, coloured text, a translucent wash of the same
* colour — so status colour means the same thing everywhere in the app, not
* a different shade per component. `current` is the sole solid fill, in the
* app's own accent colour: the one state that gets to look bolder than the
* rest, because it answers "where am I right now". "passed without
* evidence" is its own colour: a stage the agent claimed but left no
* artifact for is not the same as a stage that is genuinely done. A
* `detected` node (the server's heuristic saw tool-event evidence but the
* agent never declared it) gets a FOURTH treatment — thin dashed warning,
* no fill — because it must never be mistaken for a real declaration.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import type { LaneNode } from "../../lib/types";
const STATE_CLASS: Record<LaneNode["state"], string> = {
done: "border-emerald-500 text-emerald-400 bg-emerald-500/10",
current: "border-blue-400 text-blue-300 bg-blue-500/20 ring-2 ring-blue-400/40",
"passed-no-evidence": "border-amber-500 text-amber-400 bg-amber-500/10",
failed: "border-red-500 text-red-400 bg-red-500/10",
pending: "border-neutral-700 text-neutral-500 bg-transparent",
done: "border-status-success/60 text-status-success bg-status-success/10",
current: "border-accent bg-accent text-white ring-2 ring-accent/30 shadow-sm",
"passed-no-evidence": "border-status-warning/60 text-status-warning bg-status-warning/10",
failed: "border-status-danger/60 text-status-danger bg-status-danger/10",
pending: "border-border-light text-fg-muted bg-transparent",
};
// Dashed border distinguishes an inferred stage from every other class above,
// including the solid amber of "passed-no-evidence" — never let it read as done.
const DETECTED_CLASS = "border-dashed border-amber-400 text-amber-300 bg-amber-500/5";
// Thin dashed border and no fill keep an inference visually lighter than
// every outlined state above — so it never reads as more certain than a claim.
const DETECTED_CLASS = "border-dashed border-status-warning/50 text-status-warning bg-transparent";
export default function PipelineMap({
nodes,
@@ -32,7 +37,7 @@ export default function PipelineMap({
nodes: LaneNode[];
detectedSignal?: string | null;
}) {
if (!nodes.length) return <div className="text-xs text-neutral-500">no pipeline</div>;
if (!nodes.length) return <div className="text-xs text-fg-muted">no pipeline</div>;
return (
// The map spans the panel: every node takes an equal share and the
// connectors absorb the slack, so the pipeline reads as one track across
@@ -54,7 +59,7 @@ export default function PipelineMap({
<span className="shrink-0 text-[13px] leading-none">{n.icon}</span>
<span className="truncate">{n.label}</span>
</div>
{i < nodes.length - 1 && <div className="h-px w-2 shrink-0 bg-neutral-700 sm:w-3" />}
{i < nodes.length - 1 && <div className="h-px w-2 shrink-0 bg-surface-3 sm:w-3" />}
</div>
))}
</div>
@@ -74,52 +74,6 @@ const pipelineNodes: Lane["pipeline_nodes"] = [
{ id: "tests", label: "tests", icon: "🧪", gate: false, state: "pending" },
];
describe("LaneCard auto: chip", () => {
it("shows the auto chip when the detected stage is ahead of the declared stage", () => {
render(
<LaneCard
lane={makeLane({ stage: "plan", pipeline_nodes: pipelineNodes, detected_stage: "tests" })}
onAction={vi.fn()}
/>
);
expect(screen.getByText("auto: tests")).toBeInTheDocument();
});
it("hides the auto chip when the detected stage matches the declared stage", () => {
render(
<LaneCard
lane={makeLane({ stage: "plan", pipeline_nodes: pipelineNodes, detected_stage: "plan" })}
onAction={vi.fn()}
/>
);
expect(screen.queryByText("auto: plan")).not.toBeInTheDocument();
});
it("hides the auto chip when the detected stage trails the declared stage", () => {
render(
<LaneCard
lane={makeLane({
stage: "implement",
pipeline_nodes: pipelineNodes,
detected_stage: "intake",
})}
onAction={vi.fn()}
/>
);
expect(screen.queryByText("auto: intake")).not.toBeInTheDocument();
});
it("hides the auto chip when nothing is detected", () => {
render(
<LaneCard
lane={makeLane({ stage: "plan", pipeline_nodes: pipelineNodes, detected_stage: null })}
onAction={vi.fn()}
/>
);
expect(screen.queryByText(/^auto:/)).not.toBeInTheDocument();
});
});
describe("LaneCard rebuilt layout", () => {
const full = (over: Partial<Lane> = {}) =>
makeLane({
@@ -141,9 +95,8 @@ describe("LaneCard rebuilt layout", () => {
expect(screen.getByTestId("lane-card-7")).toBeInTheDocument();
});
it("shows the declared stage, the progress percentage and the time on stage", () => {
it("shows the progress percentage and the time on stage", () => {
render(<LaneCard lane={full()} onAction={vi.fn()} />);
expect(screen.getByTestId("lane-stage").textContent).toBe("plan");
expect(screen.getByText("48%")).toBeInTheDocument();
expect(screen.getByText("2m 32s")).toBeInTheDocument();
});
@@ -165,16 +118,26 @@ describe("LaneCard rebuilt layout", () => {
expect(onAction).toHaveBeenCalledWith("stop");
});
it("keeps the destructive verbs out of the card until the menu is opened", async () => {
it("shows both deletions as buttons, and keeps reset behind the menu", async () => {
render(<LaneCard lane={full()} onAction={vi.fn()} />);
// A wall of red buttons makes none of them read as the dangerous one, so
// reset/remove/purge live behind the ⋯ menu.
// Deleting the lane and deleting its history are the two the user goes
// looking for, so they are visible without opening anything. Reset is not.
expect(screen.getByTestId("lane-action-remove")).toBeInTheDocument();
expect(screen.getByTestId("lane-action-purge")).toBeInTheDocument();
expect(screen.queryByTestId("lane-action-reset")).toBeNull();
expect(screen.queryByTestId("lane-action-remove")).toBeNull();
await userEvent.setup().click(screen.getByTestId("lane-more"));
expect(screen.getByTestId("lane-action-reset")).toBeInTheDocument();
expect(screen.getByTestId("lane-action-remove")).toBeInTheDocument();
});
it("routes both deletions through the modal rather than firing them", async () => {
const onAction = vi.fn();
render(<LaneCard lane={full()} onAction={onAction} />);
const user = userEvent.setup();
await user.click(screen.getByTestId("lane-action-remove"));
expect(onAction).not.toHaveBeenCalled();
await user.click(screen.getByTestId("lane-action-purge"));
expect(onAction).not.toHaveBeenCalled();
});
it("routes reset through the confirmation modal rather than firing it", async () => {
@@ -186,16 +149,18 @@ describe("LaneCard rebuilt layout", () => {
expect(onAction).not.toHaveBeenCalled();
});
it("offers reset only for a managed lane", async () => {
it("offers reset only for a managed lane, and drops the menu entirely without it", async () => {
const user = userEvent.setup();
const { unmount } = render(<LaneCard lane={full()} onAction={vi.fn()} />);
await user.click(screen.getByTestId("lane-more"));
expect(screen.getByTestId("lane-action-reset")).toBeInTheDocument();
unmount();
// An adopted lane has nothing left in the menu, so there is no ⋯ to open.
render(<LaneCard lane={full({ kind: "adopted" })} onAction={vi.fn()} />);
await user.click(screen.getByTestId("lane-more"));
expect(screen.queryByTestId("lane-more")).toBeNull();
expect(screen.queryByTestId("lane-action-reset")).toBeNull();
expect(screen.getByTestId("lane-action-remove")).toBeInTheDocument();
});
});
@@ -36,9 +36,10 @@ describe("PipelineMap", () => {
render(<PipelineMap nodes={nodes} />);
const done = screen.getByTestId("pipeline-node-plan").className;
const amber = screen.getByTestId("pipeline-node-implement").className;
// The done node must contain emerald colour token and the amber node must contain amber token.
expect(done).toContain("emerald");
expect(amber).toContain("amber");
// The done node must contain the success status token and the amber node
// must contain the warning status token.
expect(done).toContain("status-success");
expect(amber).toContain("status-warning");
expect(done).not.toEqual(amber);
});
@@ -94,8 +95,8 @@ describe("PipelineMap", () => {
render(<PipelineMap nodes={impossible} detectedSignal="npm run test:server" />);
const node = screen.getByTestId("pipeline-node-tests");
expect(node.className).toContain("border-dashed");
expect(node.className).toContain("amber");
expect(node.className).not.toContain("emerald");
expect(node.className).toContain("status-warning");
expect(node.className).not.toContain("status-success");
});
it("non-detected nodes carry no data-detected attribute", () => {
+59 -43
View File
@@ -256,21 +256,22 @@ function TokenMeter({ stats }: { stats: TokenStats }) {
const pct = Math.min(100, Math.round((total / cap) * 100));
// Colour is the whole warning mechanism here - the meter is one status line,
// so there is no room for a bar plus five labelled figures.
const tone = pct >= 95 ? "text-red-300" : pct >= 80 ? "text-amber-300" : "text-gray-400";
const tone =
pct >= 95 ? "text-status-danger" : pct >= 80 ? "text-status-warning" : "text-fg-secondary";
return (
<div className="flex flex-wrap items-center gap-x-3 gap-y-0.5 border-t border-border px-3 py-1.5 font-mono text-[11.5px] text-gray-500">
<div className="flex flex-wrap items-center gap-x-3 gap-y-0.5 border-t border-border px-3 py-1.5 font-mono text-[11.5px] text-fg-muted">
<span className="select-none opacity-60" aria-hidden>
</span>
<span className={tone}>{`${formatNum(total)} / ${formatNum(cap)} (${pct}%)`}</span>
<span title={t("tokens.output")}>{formatNum(stats.outputTokens)}</span>
{stats.cacheReadTokens > 0 && (
<span className="text-emerald-400/70" title={t("tokens.cacheRead")}>
<span className="text-status-success/70" title={t("tokens.cacheRead")}>
{formatNum(stats.cacheReadTokens)}
</span>
)}
{stats.costUsd != null && (
<span className="text-gray-400">${stats.costUsd.toFixed(4)}</span>
<span className="text-fg-secondary">${stats.costUsd.toFixed(4)}</span>
)}
</div>
);
@@ -323,11 +324,11 @@ function commandSourceLabel(s: SlashCommand["source"]): string {
function commandSourceTone(s: SlashCommand["source"]): string {
return s === "builtin"
? "bg-gray-500/10 text-gray-400 border-gray-500/30"
? "bg-surface-4/10 text-fg-secondary border-border-light/30"
: s === "user"
? "bg-sky-500/10 text-sky-300 border-sky-500/30"
: s === "project"
? "bg-emerald-500/10 text-emerald-300 border-emerald-500/30"
? "bg-status-success/10 text-status-success border-status-success/30"
: "bg-violet-500/10 text-violet-300 border-violet-500/30";
}
@@ -565,11 +566,11 @@ export function PromptEditor({
placeholder={placeholder}
rows={rows}
spellCheck={false}
className="w-full bg-surface-2 border border-border rounded-md px-3 py-2 text-sm text-gray-100 placeholder:text-gray-500 focus:outline-none focus:border-accent/50 resize-y font-sans leading-relaxed"
className="w-full bg-surface-2 border border-border rounded-md px-3 py-2 text-sm text-fg-primary placeholder:text-fg-muted focus:outline-none focus:border-accent/50 resize-y font-sans leading-relaxed"
/>
{state && (
<div className="absolute z-30 left-0 right-0 bottom-full mb-1 rounded-md border border-border bg-surface-1 shadow-lg shadow-black/40 max-h-72 overflow-auto py-1">
<div className="px-3 py-1.5 border-b border-border text-[10px] font-semibold uppercase tracking-wider text-gray-500 inline-flex items-center gap-1.5">
<div className="px-3 py-1.5 border-b border-border text-[10px] font-semibold uppercase tracking-wider text-fg-muted inline-flex items-center gap-1.5">
{state.kind === "slash" ? (
<>
<SlashIcon className="w-3 h-3" />
@@ -583,7 +584,7 @@ export function PromptEditor({
)}
</div>
{items.length === 0 ? (
<div className="px-3 py-2 text-[11px] text-gray-500">{t("autocomplete.noMatches")}</div>
<div className="px-3 py-2 text-[11px] text-fg-muted">{t("autocomplete.noMatches")}</div>
) : state.kind === "slash" ? (
(items as SlashCommand[]).map((c, idx) => (
<button
@@ -597,7 +598,7 @@ export function PromptEditor({
}`}
>
<div className="flex items-center gap-2">
<span className="font-mono text-[12px] text-gray-100">/{c.name}</span>
<span className="font-mono text-[12px] text-fg-primary">/{c.name}</span>
<span
className={`text-[9px] font-mono px-1.5 py-0.5 rounded border ${commandSourceTone(c.source)}`}
>
@@ -605,7 +606,7 @@ export function PromptEditor({
</span>
</div>
{c.description && (
<div className="text-[10.5px] text-gray-500 truncate mt-0.5">{c.description}</div>
<div className="text-[10.5px] text-fg-muted truncate mt-0.5">{c.description}</div>
)}
</button>
))
@@ -621,8 +622,8 @@ export function PromptEditor({
idx === active ? "bg-accent/15" : "hover:bg-surface-3"
}`}
>
<FileCode className="w-3 h-3 text-gray-500 flex-shrink-0" />
<span className="font-mono text-[11px] text-gray-200 truncate">{p}</span>
<FileCode className="w-3 h-3 text-fg-muted flex-shrink-0" />
<span className="font-mono text-[11px] text-fg-secondary truncate">{p}</span>
</button>
))
)}
@@ -693,16 +694,16 @@ export function RunConsole(props: RunConsoleProps) {
<div className="flex flex-wrap items-center gap-2 border-b border-border px-3 py-1.5 font-mono text-[11.5px]">
<StatusPill status={props.handle.status} />
<ModeBadge mode={props.mode} />
{init?.model && <span className="text-gray-500">{init.model}</span>}
{init?.model && <span className="text-fg-muted">{init.model}</span>}
{props.handle.sessionId && (
<span className="truncate text-gray-600">{props.handle.sessionId.slice(0, 8)}</span>
<span className="truncate text-fg-muted">{props.handle.sessionId.slice(0, 8)}</span>
)}
<div className="flex-1" />
{props.isLive && (
<button
onClick={props.onStop}
disabled={props.busy === "stop"}
className="inline-flex items-center gap-1 text-red-300 hover:text-red-200 disabled:opacity-60 transition-colors"
className="inline-flex items-center gap-1 text-status-danger hover:text-status-danger disabled:opacity-60 transition-colors"
>
<Square className="w-3 h-3" />
{props.busy === "stop" ? t("actions.stopping") : t("actions.stop")}
@@ -711,7 +712,7 @@ export function RunConsole(props: RunConsoleProps) {
{props.handle.sessionId && (
<Link
to={`/sessions/${encodeURIComponent(props.handle.sessionId)}`}
className="inline-flex items-center gap-1 text-gray-400 hover:text-gray-200 transition-colors"
className="inline-flex items-center gap-1 text-fg-secondary hover:text-fg-primary transition-colors"
>
<ExternalLink className="w-3 h-3" />
{t("actions.viewSession")}
@@ -756,7 +757,7 @@ export function RunConsole(props: RunConsoleProps) {
fileCwd={props.handle.cwd}
/>
<div className="mt-2 flex items-center justify-between">
<div className="text-[10px] text-gray-600">{t("hint.shortcut")} · / · @</div>
<div className="text-[10px] text-fg-muted">{t("hint.shortcut")} · / · @</div>
<button
onClick={props.onSend}
disabled={!props.followUp.trim() || props.busy === "send"}
@@ -775,7 +776,7 @@ function EmptyStream({ isLive }: { isLive: boolean }) {
const { t } = useTranslation("run");
if (isLive) {
return (
<div className="text-center py-12 text-gray-500 flex flex-col items-center gap-2">
<div className="text-center py-12 text-fg-muted flex flex-col items-center gap-2">
<RefreshCw className="w-5 h-5 animate-spin" />
<span className="text-xs">{t("status.spawning")}</span>
</div>
@@ -783,25 +784,37 @@ function EmptyStream({ isLive }: { isLive: boolean }) {
}
return (
<div className="text-center py-12 flex flex-col items-center gap-2">
<Sparkles className="w-6 h-6 text-gray-600" />
<div className="text-sm font-medium text-gray-400">{t("empty.title")}</div>
<div className="text-xs text-gray-500 max-w-md">{t("empty.body")}</div>
<Sparkles className="w-6 h-6 text-fg-muted" />
<div className="text-sm font-medium text-fg-secondary">{t("empty.title")}</div>
<div className="text-xs text-fg-muted max-w-md">{t("empty.body")}</div>
</div>
);
}
export function StatusPill({ status }: { status: string }) {
const { t } = useTranslation("run");
const idle = { color: "bg-surface-3 text-gray-400 border-border", icon: Clock as typeof Play };
const idle = {
color: "bg-surface-3 text-fg-secondary border-border",
icon: Clock as typeof Play,
};
const config: Record<string, { color: string; icon: typeof Play }> = {
spawning: { color: "bg-amber-500/15 text-amber-300 border-amber-500/30", icon: RefreshCw },
running: { color: "bg-emerald-500/15 text-emerald-300 border-emerald-500/30", icon: Sparkles },
spawning: {
color: "bg-status-warning/15 text-status-warning border-status-warning/30",
icon: RefreshCw,
},
running: {
color: "bg-status-success/15 text-status-success border-status-success/30",
icon: Sparkles,
},
completed: {
color: "bg-emerald-500/15 text-emerald-300 border-emerald-500/30",
color: "bg-status-success/15 text-status-success border-status-success/30",
icon: CheckCircle2,
},
error: { color: "bg-red-500/15 text-red-300 border-red-500/30", icon: XCircle },
killed: { color: "bg-gray-500/15 text-gray-400 border-gray-500/30", icon: Square },
error: {
color: "bg-status-danger/15 text-status-danger border-status-danger/30",
icon: XCircle,
},
killed: { color: "bg-surface-4/15 text-fg-secondary border-border-light/30", icon: Square },
abandoned: {
color: "bg-orange-500/10 text-orange-300 border-orange-500/30",
icon: Square,
@@ -826,7 +839,7 @@ export function StatusPill({ status }: { status: string }) {
export function ModeBadge({ mode }: { mode: RunMode }) {
const { t } = useTranslation("run");
return (
<span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-surface-3 text-gray-400 border border-border inline-flex items-center gap-1">
<span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-surface-3 text-fg-secondary border border-border inline-flex items-center gap-1">
{mode === "conversation" ? (
<Terminal className="w-3 h-3" />
) : (
@@ -894,7 +907,7 @@ function UserTurn({ env }: { env: UserMessage }) {
<span className="select-none text-indigo-400" aria-hidden>
&gt;
</span>
<span className="min-w-0 flex-1 whitespace-pre-wrap break-words text-gray-200">
<span className="min-w-0 flex-1 whitespace-pre-wrap break-words text-fg-secondary">
{text || "-"}
</span>
</div>
@@ -928,10 +941,13 @@ function AssistantTurn({ env }: { env: AssistantMessage }) {
// The gutter glyph is the only speaker marker - no avatar, no label
// row. Prose keeps its markdown rendering; only the chrome is gone.
<div className="flex gap-2">
<span className="select-none font-mono text-[12.5px] leading-relaxed text-accent" aria-hidden>
<span
className="select-none font-mono text-[12.5px] leading-relaxed text-accent"
aria-hidden
>
</span>
<div className="min-w-0 flex-1 text-[13px] leading-relaxed text-gray-200 prose-claude">
<div className="min-w-0 flex-1 text-[13px] leading-relaxed text-fg-secondary prose-claude">
<MarkdownContent text={text} />
</div>
</div>
@@ -978,14 +994,14 @@ function ToolUseBlock({ toolUse }: { toolUse: Extract<ContentBlock, { type: "too
className="flex w-full items-baseline gap-2 text-left hover:bg-white/[0.03] transition-colors"
title={t("events.tool")}
>
<span className="select-none text-amber-400" aria-hidden>
<span className="select-none text-status-warning" aria-hidden>
</span>
<span className="font-medium text-amber-200">{toolUse.name}</span>
{summary && <span className="truncate text-gray-500">{summary}</span>}
<span className="font-medium text-status-warning">{toolUse.name}</span>
{summary && <span className="truncate text-fg-muted">{summary}</span>}
</button>
{open && (
<pre className="mt-0.5 ml-1.5 max-h-72 overflow-auto border-l border-amber-500/25 pl-3 whitespace-pre-wrap break-words text-gray-400">
<pre className="mt-0.5 ml-1.5 max-h-72 overflow-auto border-l border-status-warning/25 pl-3 whitespace-pre-wrap break-words text-fg-secondary">
{JSON.stringify(toolUse.input, null, 2)}
</pre>
)}
@@ -1010,7 +1026,7 @@ function ToolResultBlock({ result }: { result: Extract<ContentBlock, { type: "to
.join("\n")
: JSON.stringify(result.content);
const lines = text.split("\n").length;
const tone = result.is_error ? "text-red-300" : "text-emerald-300/90";
const tone = result.is_error ? "text-status-danger" : "text-status-success/90";
// First line is the useful one nine times out of ten, so it doubles as the
// collapsed summary instead of a generic "tool result" label.
const firstLine = text.split("\n").find((l) => l.trim()) || "";
@@ -1037,9 +1053,9 @@ function ToolResultBlock({ result }: { result: Extract<ContentBlock, { type: "to
function UnknownTurn({ env }: { env: Envelope }) {
return (
<details className="font-mono text-[11.5px] leading-relaxed text-gray-500">
<details className="font-mono text-[11.5px] leading-relaxed text-fg-muted">
<summary className="cursor-pointer">? {(env.type as string) || "unknown"}</summary>
<pre className="mt-0.5 ml-1.5 max-h-48 overflow-auto border-l border-border pl-3 whitespace-pre-wrap break-words text-gray-500">
<pre className="mt-0.5 ml-1.5 max-h-48 overflow-auto border-l border-border pl-3 whitespace-pre-wrap break-words text-fg-muted">
{JSON.stringify(env, null, 2)}
</pre>
</details>
@@ -1061,21 +1077,21 @@ function ResultFooter({ result }: { result: ResultEnvelope }) {
const { t } = useTranslation("run");
const isError = result.is_error;
const parts: string[] = [];
if (typeof result.duration_ms === "number") parts.push(`${(result.duration_ms / 1000).toFixed(1)}s`);
if (typeof result.duration_ms === "number")
parts.push(`${(result.duration_ms / 1000).toFixed(1)}s`);
if (typeof result.total_cost_usd === "number") parts.push(`$${result.total_cost_usd.toFixed(4)}`);
if (typeof result.num_turns === "number") parts.push(t("footer.turns") + " " + result.num_turns);
return (
<div
className={`border-t border-border px-3 py-1.5 font-mono text-[11.5px] ${
isError ? "text-red-300" : "text-emerald-300/90"
isError ? "text-status-danger" : "text-status-success/90"
}`}
>
<span className="select-none opacity-60" aria-hidden>
{" "}
</span>
{isError ? t("status.error") : t("status.completed")}
{parts.length > 0 && <span className="text-gray-500"> · {parts.join(" · ")}</span>}
{parts.length > 0 && <span className="text-fg-muted"> · {parts.join(" · ")}</span>}
</div>
);
}
+27 -27
View File
@@ -148,20 +148,20 @@ export function ActiveRunsSwitcher({
disabled={totalCount === 0}
className={`inline-flex items-center gap-2 rounded-lg border px-3 py-1.5 text-xs font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
liveCount > 0
? "border-emerald-500/40 bg-emerald-500/10 text-emerald-200 hover:bg-emerald-500/15"
: "border-border bg-surface-2 text-gray-300 hover:bg-surface-3"
? "border-status-success/40 bg-status-success/10 text-status-success hover:bg-status-success/15"
: "border-border bg-surface-2 text-fg-secondary hover:bg-surface-3"
}`}
>
<ListOrdered className="w-3.5 h-3.5" />
{liveCount > 0 ? (
<>
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
<span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse" />
{t("runs.viewActive_other", { count: liveCount })}
</>
) : (
<>
{t("runs.switcher")}
{totalCount > 0 && <span className="text-gray-500 font-mono">{totalCount}</span>}
{totalCount > 0 && <span className="text-fg-muted font-mono">{totalCount}</span>}
</>
)}
</button>
@@ -277,10 +277,10 @@ export function RunsModal({
<ListOrdered className="w-4 h-4 text-accent" />
</div>
<div className="min-w-0 flex-1">
<h2 className="text-sm font-semibold text-gray-100">
<h2 className="text-sm font-semibold text-fg-primary">
{t("runs.modalTitle", "Dashboard runs")}
</h2>
<p className="text-[11px] text-gray-500">
<p className="text-[11px] text-fg-muted">
{t(
"runs.modalSubtitle",
"Every run started from this dashboard, regardless of status"
@@ -289,7 +289,7 @@ export function RunsModal({
</div>
<button
onClick={onRefresh}
className="w-7 h-7 rounded-md text-gray-500 hover:text-gray-200 hover:bg-surface-3 inline-flex items-center justify-center"
className="w-7 h-7 rounded-md text-fg-muted hover:text-fg-secondary hover:bg-surface-3 inline-flex items-center justify-center"
aria-label={t("runs.refresh", "Refresh")}
title={t("runs.refresh", "Refresh")}
>
@@ -304,7 +304,7 @@ export function RunsModal({
</Link>
<button
onClick={onClose}
className="w-7 h-7 rounded-md text-gray-500 hover:text-gray-200 hover:bg-surface-3 inline-flex items-center justify-center"
className="w-7 h-7 rounded-md text-fg-muted hover:text-fg-secondary hover:bg-surface-3 inline-flex items-center justify-center"
aria-label={t("limitations.dismiss")}
>
<X className="w-4 h-4" />
@@ -314,18 +314,18 @@ export function RunsModal({
{/* Filter bar */}
<div className="px-5 py-3 border-b border-border flex flex-col gap-2.5 flex-shrink-0">
<div className="flex items-center gap-2 bg-surface-2 border border-border rounded-md px-2.5 py-1.5">
<Search className="w-3.5 h-3.5 text-gray-500 flex-shrink-0" />
<Search className="w-3.5 h-3.5 text-fg-muted flex-shrink-0" />
<input
autoFocus
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder={t("runs.searchPlaceholder", "Search prompt, cwd, model, or session id…")}
className="flex-1 bg-transparent text-[12px] text-gray-100 placeholder:text-gray-600 focus:outline-none"
className="flex-1 bg-transparent text-[12px] text-fg-primary placeholder:text-fg-muted focus:outline-none"
/>
{search && (
<button
onClick={() => setSearch("")}
className="text-gray-500 hover:text-gray-200 text-[10px]"
className="text-fg-muted hover:text-fg-secondary text-[10px]"
aria-label="Clear"
>
<X className="w-3 h-3" />
@@ -359,7 +359,7 @@ export function RunsModal({
{/* List */}
<div className="flex-1 min-h-0 overflow-auto divide-y divide-border">
{filtered.length === 0 ? (
<div className="px-5 py-12 text-center text-[12px] text-gray-500">
<div className="px-5 py-12 text-center text-[12px] text-fg-muted">
{rows.length === 0
? t(
"runs.modalEmpty",
@@ -392,11 +392,11 @@ export function RunsModal({
{/* Footer */}
<div className="px-5 py-2.5 border-t border-border bg-surface-2/40 flex items-center gap-2 flex-shrink-0">
<Info className="w-3 h-3 text-gray-500 flex-shrink-0" />
<span className="text-[10.5px] text-gray-500 leading-relaxed flex-1">
<Info className="w-3 h-3 text-fg-muted flex-shrink-0" />
<span className="text-[10.5px] text-fg-muted leading-relaxed flex-1">
{t("runs.scopeNote")}
</span>
<span className="text-[10.5px] text-gray-500 font-mono">
<span className="text-[10.5px] text-fg-muted font-mono">
{filtered.length} / {rows.length}
</span>
</div>
@@ -418,7 +418,7 @@ function FilterChipGroup<T extends string>({
}) {
return (
<div className="flex items-center gap-1.5 flex-wrap">
<span className="text-[10px] uppercase tracking-wider font-semibold text-gray-500 mr-1">
<span className="text-[10px] uppercase tracking-wider font-semibold text-fg-muted mr-1">
{label}
</span>
{options.map((opt) => {
@@ -432,11 +432,11 @@ function FilterChipGroup<T extends string>({
className={`text-[10.5px] font-medium px-2 py-0.5 rounded-full border transition-colors disabled:opacity-40 ${
active
? "bg-accent/15 border-accent/50 text-accent"
: "bg-surface-2 border-border text-gray-300 hover:bg-surface-3 hover:border-border-strong"
: "bg-surface-2 border-border text-fg-secondary hover:bg-surface-3 hover:border-border-strong"
}`}
>
{opt.label}
<span className="ml-1 text-gray-500 font-mono">{opt.count}</span>
<span className="ml-1 text-fg-muted font-mono">{opt.count}</span>
</button>
);
})}
@@ -481,8 +481,8 @@ function UnifiedRunRowView({
<StatusPill status={row.status} />
<ModeBadge mode={row.mode} />
{row.isLive && (
<span className="text-[10px] font-semibold text-emerald-300 bg-emerald-500/10 border border-emerald-500/25 px-1.5 py-0.5 rounded-full inline-flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
<span className="text-[10px] font-semibold text-status-success bg-status-success/10 border border-status-success/25 px-1.5 py-0.5 rounded-full inline-flex items-center gap-1">
<span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse" />
{t("runs.liveBadge", "live")}
</span>
)}
@@ -495,7 +495,7 @@ function UnifiedRunRowView({
{row.isLive && !isCurrent && (
<button
onClick={onAttach}
className="inline-flex items-center gap-1 rounded-md border border-emerald-500/40 bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-200 px-2 py-0.5 text-[10.5px] font-medium transition-colors"
className="inline-flex items-center gap-1 rounded-md border border-status-success/40 bg-status-success/10 hover:bg-status-success/20 text-status-success px-2 py-0.5 text-[10.5px] font-medium transition-colors"
>
<Play className="w-3 h-3" />
{t("runs.attachLabel", "Attach")}
@@ -513,7 +513,7 @@ function UnifiedRunRowView({
{canView && (
<button
onClick={onView}
className="inline-flex items-center gap-1 rounded-md border border-border bg-surface-2 hover:bg-surface-3 text-gray-300 hover:text-gray-100 px-2 py-0.5 text-[10.5px] font-medium transition-colors"
className="inline-flex items-center gap-1 rounded-md border border-border bg-surface-2 hover:bg-surface-3 text-fg-secondary hover:text-fg-primary px-2 py-0.5 text-[10.5px] font-medium transition-colors"
>
<Eye className="w-3 h-3" />
{t("runs.viewLabel", "View")}
@@ -522,18 +522,18 @@ function UnifiedRunRowView({
</span>
</div>
{row.promptPreview && (
<div className="text-[12px] text-gray-300 line-clamp-2 leading-snug">
<div className="text-[12px] text-fg-secondary line-clamp-2 leading-snug">
{row.promptPreview}
</div>
)}
<div className="font-mono text-[10px] text-gray-500 truncate mt-1">{row.cwd}</div>
<div className="text-[10px] text-gray-600 mt-0.5 flex items-center gap-2 flex-wrap">
<div className="font-mono text-[10px] text-fg-muted truncate mt-1">{row.cwd}</div>
<div className="text-[10px] text-fg-muted mt-0.5 flex items-center gap-2 flex-wrap">
<span>{startedLabel}</span>
{row.model && <span className="font-mono text-gray-500">· {row.model}</span>}
{row.model && <span className="font-mono text-fg-muted">· {row.model}</span>}
{row.sessionId && (
<Link
to={`/sessions/${encodeURIComponent(row.sessionId)}`}
className="inline-flex items-center gap-1 text-gray-500 hover:text-gray-300 transition-colors"
className="inline-flex items-center gap-1 text-fg-muted hover:text-fg-secondary transition-colors"
title={t("actions.viewSession")}
>
<ExternalLink className="w-2.5 h-2.5" />
+34 -33
View File
@@ -54,7 +54,6 @@ import type { SlashCommand } from "./RunConsole";
// ── Limitations banner (above the config card) ────────────────────────
interface RunSetupProps {
mode: RunMode;
onModeChange: (m: RunMode) => void;
@@ -159,7 +158,7 @@ export function RunSetup(props: RunSetupProps) {
{/* Prompt */}
<div className="px-4 py-3 border-b border-border">
<label className="block text-[11px] font-semibold uppercase tracking-wider text-gray-500 mb-1.5">
<label className="block text-[11px] font-semibold uppercase tracking-wider text-fg-muted mb-1.5">
{t("fields.prompt")}
</label>
<PromptEditor
@@ -171,7 +170,7 @@ export function RunSetup(props: RunSetupProps) {
slashCommands={props.slashCommands}
fileCwd={props.resumeSession?.cwd || props.cwd}
/>
<div className="mt-1 text-[10px] text-gray-600">
<div className="mt-1 text-[10px] text-fg-muted">
{t("hint.shortcut")} · / for slash commands · @ for file references
</div>
</div>
@@ -180,8 +179,8 @@ export function RunSetup(props: RunSetupProps) {
<div className="grid grid-cols-1 gap-3 px-4 py-3 sm:grid-cols-2 lg:grid-cols-4">
<Field label={t("fields.cwd")}>
{isResume && props.resumeSession ? (
<div className="bg-surface-2 border border-border rounded-md px-3 py-1.5 text-[11px] font-mono text-gray-300 flex items-center gap-2">
<Lock className="w-3 h-3 text-gray-500 flex-shrink-0" />
<div className="bg-surface-2 border border-border rounded-md px-3 py-1.5 text-[11px] font-mono text-fg-secondary flex items-center gap-2">
<Lock className="w-3 h-3 text-fg-muted flex-shrink-0" />
<span className="truncate">{props.resumeSession.cwd}</span>
</div>
) : (
@@ -223,7 +222,7 @@ export function RunSetup(props: RunSetupProps) {
</div>
{props.permissionMode === "bypassPermissions" && (
<div className="mx-4 mb-3 rounded-md border border-red-500/40 bg-red-500/10 px-3 py-2 text-[11px] text-red-200 flex items-start gap-2">
<div className="mx-4 mb-3 rounded-md border border-status-danger/40 bg-status-danger/10 px-3 py-2 text-[11px] text-status-danger flex items-start gap-2">
<ShieldAlert className="w-3.5 h-3.5 flex-shrink-0 mt-0.5" />
<span>{t("hint.permissionWarning")}</span>
</div>
@@ -233,13 +232,13 @@ export function RunSetup(props: RunSetupProps) {
<div className="border-t border-border px-4 py-3 flex items-center justify-between gap-3 flex-wrap">
<div className="flex items-center gap-3 text-[11px] min-w-0">
{atCap ? (
<span className="inline-flex items-center gap-1.5 text-amber-300">
<span className="inline-flex items-center gap-1.5 text-status-warning">
<AlertCircle className="w-3.5 h-3.5" />
{t("concurrency.atCap", { max: props.activeRuns?.maxConcurrent ?? 0 })}
</span>
) : props.activeRuns && props.activeRuns.activeCount > 0 ? (
<span className="inline-flex items-center gap-1.5 text-gray-400">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
<span className="inline-flex items-center gap-1.5 text-fg-secondary">
<span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse" />
{t("concurrency.active", { count: props.activeRuns.activeCount })}
</span>
) : null}
@@ -291,7 +290,7 @@ function Seg({
title={title}
aria-pressed={active}
className={`rounded px-2 py-0.5 font-medium transition-colors ${
active ? "bg-accent/20 text-accent" : "text-gray-400 hover:text-gray-200"
active ? "bg-accent/20 text-accent" : "text-fg-secondary hover:text-fg-primary"
}`}
>
{label}
@@ -302,7 +301,7 @@ function Seg({
function Field({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div>
<label className="block text-[10px] font-semibold uppercase tracking-wider text-gray-500 mb-1">
<label className="block text-[10px] font-semibold uppercase tracking-wider text-fg-muted mb-1">
{label}
</label>
{children}
@@ -397,7 +396,7 @@ export function CwdAutocomplete({
return (
<div ref={containerRef} className="relative">
<div className="relative">
<FolderOpen className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-gray-500 pointer-events-none" />
<FolderOpen className="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-fg-muted pointer-events-none" />
<input
ref={inputRef}
id={inputId}
@@ -413,17 +412,17 @@ export function CwdAutocomplete({
placeholder={t("fields.cwdPlaceholder")}
autoComplete="off"
spellCheck={false}
className="w-full bg-surface-2 border border-border rounded-md pl-7 pr-3 py-1.5 text-[11px] font-mono text-gray-100 placeholder:text-gray-500 focus:outline-none focus:border-accent/50"
className="w-full bg-surface-2 border border-border rounded-md pl-7 pr-3 py-1.5 text-[11px] font-mono text-fg-primary placeholder:text-fg-muted focus:outline-none focus:border-accent/50"
/>
</div>
{open && (
<div className="absolute z-30 left-0 right-0 mt-1 rounded-md border border-border bg-surface-1 shadow-lg shadow-black/40 max-h-72 overflow-auto py-1">
{groups.length === 0 ? (
<div className="px-3 py-2 text-[11px] text-gray-500">{t("fields.cwdNoMatches")}</div>
<div className="px-3 py-2 text-[11px] text-fg-muted">{t("fields.cwdNoMatches")}</div>
) : (
groups.map((g) => (
<div key={g.kind}>
<div className="px-3 pt-1.5 pb-0.5 text-[10px] font-semibold uppercase tracking-wider text-gray-500 flex items-center gap-1.5">
<div className="px-3 pt-1.5 pb-0.5 text-[10px] font-semibold uppercase tracking-wider text-fg-muted flex items-center gap-1.5">
{g.kind === "dashboard" ? (
<FolderOpen className="w-3 h-3" />
) : g.kind === "home" ? (
@@ -447,8 +446,8 @@ export function CwdAutocomplete({
isActive ? "bg-accent/15" : "hover:bg-surface-3"
}`}
>
<div className="text-[11px] text-gray-200 truncate">{s.label}</div>
<div className="font-mono text-[10px] text-gray-500 truncate">{s.path}</div>
<div className="text-[11px] text-fg-secondary truncate">{s.label}</div>
<div className="font-mono text-[10px] text-fg-muted truncate">{s.path}</div>
</button>
);
})}
@@ -527,13 +526,13 @@ function SessionPicker({
<span className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-accent/15 text-accent border border-accent/30">
{t("resume.selectedBadge")}
</span>
<span className="font-mono text-[11px] text-gray-200 truncate">{selected.id}</span>
<span className="font-mono text-[11px] text-fg-secondary truncate">{selected.id}</span>
</div>
<div className="font-mono text-[10px] text-gray-500 truncate mt-0.5">{selected.cwd}</div>
<div className="font-mono text-[10px] text-fg-muted truncate mt-0.5">{selected.cwd}</div>
</div>
<button
onClick={() => onSelect(null)}
className="text-[10px] font-medium px-2 py-0.5 rounded border border-border bg-surface-2 hover:bg-surface-3 text-gray-300 inline-flex items-center gap-1 flex-shrink-0"
className="text-[10px] font-medium px-2 py-0.5 rounded border border-border bg-surface-2 hover:bg-surface-3 text-fg-secondary inline-flex items-center gap-1 flex-shrink-0"
>
<X className="w-3 h-3" />
{t("resume.clear")}
@@ -546,7 +545,7 @@ function SessionPicker({
<div ref={containerRef} className="relative mt-2">
<button
onClick={() => setOpen((v) => !v)}
className="w-full text-left rounded-md border border-dashed border-border bg-surface-2 hover:bg-surface-3 px-3 py-2 text-[11px] text-gray-400 inline-flex items-center gap-2"
className="w-full text-left rounded-md border border-dashed border-border bg-surface-2 hover:bg-surface-3 px-3 py-2 text-[11px] text-fg-secondary inline-flex items-center gap-2"
>
<RotateCcw className="w-3.5 h-3.5" />
{t("resume.pickSession")}
@@ -555,20 +554,20 @@ function SessionPicker({
{open && (
<div className="absolute z-30 left-0 right-0 mt-1 rounded-md border border-border bg-surface-1 shadow-lg shadow-black/40 overflow-hidden">
<div className="px-3 py-2 border-b border-border flex items-center gap-2">
<Search className="w-3.5 h-3.5 text-gray-500 flex-shrink-0" />
<Search className="w-3.5 h-3.5 text-fg-muted flex-shrink-0" />
<input
autoFocus
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={t("resume.search")}
className="bg-transparent text-[11px] text-gray-100 placeholder:text-gray-500 focus:outline-none w-full"
className="bg-transparent text-[11px] text-fg-primary placeholder:text-fg-muted focus:outline-none w-full"
/>
</div>
<div className="max-h-72 overflow-auto py-1">
{sessions === null ? (
<div className="px-3 py-2 text-[11px] text-gray-500"></div>
<div className="px-3 py-2 text-[11px] text-fg-muted"></div>
) : filtered.length === 0 ? (
<div className="px-3 py-2 text-[11px] text-gray-500">{t("resume.noSessions")}</div>
<div className="px-3 py-2 text-[11px] text-fg-muted">{t("resume.noSessions")}</div>
) : (
filtered.map((s) => (
<button
@@ -584,27 +583,29 @@ function SessionPicker({
<span
className={`text-[10px] font-mono px-1.5 py-0.5 rounded border ${
s.status === "active"
? "bg-emerald-500/10 text-emerald-300 border-emerald-500/30"
? "bg-status-success/10 text-status-success border-status-success/30"
: s.status === "completed"
? "bg-sky-500/10 text-sky-300 border-sky-500/30"
: s.status === "error"
? "bg-red-500/10 text-red-300 border-red-500/30"
: "bg-surface-3 text-gray-400 border-border"
? "bg-status-danger/10 text-status-danger border-status-danger/30"
: "bg-surface-3 text-fg-secondary border-border"
}`}
>
{s.status}
</span>
{s.name?.trim() && (
<span className="text-[11px] text-gray-200 truncate">{s.name.trim()}</span>
<span className="text-[11px] text-fg-secondary truncate">
{s.name.trim()}
</span>
)}
<span className="font-mono text-[11px] text-gray-400 truncate flex-shrink-0">
<span className="font-mono text-[11px] text-fg-secondary truncate flex-shrink-0">
{s.id.slice(0, 12)}
</span>
<span className="text-[10px] text-gray-600 ml-auto flex-shrink-0">
<span className="text-[10px] text-fg-muted ml-auto flex-shrink-0">
{new Date(s.started_at).toLocaleString()}
</span>
</div>
<div className="font-mono text-[10px] text-gray-500 truncate">{s.cwd}</div>
<div className="font-mono text-[10px] text-fg-muted truncate">{s.cwd}</div>
</button>
))
)}
@@ -666,7 +667,7 @@ function ModelPicker({ value, onChange }: { value: string; onChange: (s: string)
placeholder={t("fields.modelCustomPlaceholder")}
autoComplete="off"
spellCheck={false}
className="w-full bg-surface-2 border border-border rounded-md px-3 py-1.5 text-[11px] font-mono text-gray-100 placeholder:text-gray-500 focus:outline-none focus:border-accent/50"
className="w-full bg-surface-2 border border-border rounded-md px-3 py-1.5 text-[11px] font-mono text-fg-primary placeholder:text-fg-muted focus:outline-none focus:border-accent/50"
/>
)}
</div>
@@ -581,8 +581,8 @@ export function AgentCollaborationNetwork({
if (isEmpty) {
return (
<div className="flex flex-col items-center justify-center py-16 text-center">
<p className="text-sm font-medium text-gray-400">{t("pipeline.noData")}</p>
<p className="text-xs text-gray-600 mt-1">{t("pipeline.noDataDesc")}</p>
<p className="text-sm font-medium text-fg-secondary">{t("pipeline.noData")}</p>
<p className="text-xs text-fg-muted mt-1">{t("pipeline.noDataDesc")}</p>
</div>
);
}
@@ -615,7 +615,7 @@ export function AgentCollaborationNetwork({
}}
/>
<div className="flex flex-wrap items-center gap-3 mt-3 px-1">
<span className="text-[10px] text-gray-600 uppercase tracking-widest font-medium">
<span className="text-[10px] text-fg-muted uppercase tracking-widest font-medium">
{t("pipeline.legend")}
</span>
{nodes.map((n) => (
@@ -627,7 +627,7 @@ export function AgentCollaborationNetwork({
border: `1.5px solid ${STROKE_PALETTE[n.colorIndex] ?? STROKE_PALETTE[0]}`,
}}
/>
<span className="text-[11px] text-gray-500">{n.id}</span>
<span className="text-[11px] text-fg-muted">{n.id}</span>
</div>
))}
<div className="flex items-center gap-1.5 ml-2">
@@ -635,7 +635,7 @@ export function AgentCollaborationNetwork({
<line x1="0" y1="4" x2="14" y2="4" stroke="#64748b" strokeWidth="1.5" />
<polygon points="14,1 20,4 14,7" fill="#64748b" />
</svg>
<span className="text-[11px] text-gray-500">{t("pipeline.legendDesc")}</span>
<span className="text-[11px] text-fg-muted">{t("pipeline.legendDesc")}</span>
</div>
</div>
</div>
@@ -284,10 +284,10 @@ function StatBox({ label, value, sub, accent = "text-accent" }: StatBoxProps) {
return (
<div className="flex flex-col gap-1 bg-surface-3 border border-border rounded-xl px-4 py-3.5 flex-1 min-w-0">
<span className={`text-2xl font-semibold tabular-nums ${accent}`}>{value}</span>
<span className="text-[11px] font-medium text-gray-500 uppercase tracking-wider leading-tight">
<span className="text-[11px] font-medium text-fg-muted uppercase tracking-wider leading-tight">
{label}
</span>
{sub && <span className="text-[11px] text-gray-600 tabular-nums">{sub}</span>}
{sub && <span className="text-[11px] text-fg-muted tabular-nums">{sub}</span>}
</div>
);
}
@@ -334,7 +334,7 @@ export function CompactionImpact({ data }: CompactionImpactProps) {
if (!hasData) {
return (
<div className="flex flex-col items-center justify-center py-16 gap-3 text-gray-500">
<div className="flex flex-col items-center justify-center py-16 gap-3 text-fg-muted">
<svg
width="40"
height="40"
@@ -357,7 +357,7 @@ export function CompactionImpact({ data }: CompactionImpactProps) {
return (
<div className="flex flex-col gap-5">
{/* What compaction is - one line so the numbers below make sense */}
<p className="text-xs text-gray-500 leading-relaxed">{t("compaction.help")}</p>
<p className="text-xs text-fg-muted leading-relaxed">{t("compaction.help")}</p>
{/* Stat tiles */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
@@ -375,18 +375,18 @@ export function CompactionImpact({ data }: CompactionImpactProps) {
<StatBox
label={t("compaction.avgPerSession")}
value={avgPerSession.toFixed(1)}
accent="text-blue-300"
accent="text-blue-400"
/>
<StatBox
label={t("compaction.peakSession")}
value={peak.toLocaleString()}
accent="text-emerald-400"
accent="text-status-success"
/>
</div>
{/* Histogram: sessions by compaction count */}
<div className="w-full overflow-hidden">
<p className="text-xs font-medium text-gray-500 uppercase tracking-wider mb-2">
<p className="text-xs font-medium text-fg-muted uppercase tracking-wider mb-2">
{t("compaction.distribution")}
</p>
<svg
@@ -399,7 +399,7 @@ export function CompactionImpact({ data }: CompactionImpactProps) {
</div>
{/* Plain-English summary + (when present) tokens freed */}
<p className="text-xs text-gray-500 leading-relaxed">
<p className="text-xs text-fg-muted leading-relaxed">
{t("compaction.summary", {
affected: affected.toLocaleString(),
total: data.totalSessions.toLocaleString(),
@@ -420,8 +420,8 @@ export function CompactionImpact({ data }: CompactionImpactProps) {
transform: tip.x > window.innerWidth - 220 ? "translateX(-100%)" : undefined,
}}
>
<div className="font-medium text-gray-100">{tip.title}</div>
<div className="mt-0.5 text-gray-400">{tip.detail}</div>
<div className="font-medium text-fg-primary">{tip.title}</div>
<div className="mt-0.5 text-fg-secondary">{tip.detail}</div>
</div>
)}
</div>
@@ -121,7 +121,7 @@ function LaneRow({ lane, color, maxCount, onShowTip, onHideTip }: LaneRowProps)
<div className="flex items-center gap-3 py-1.5 group">
{/* Label column */}
<div className="flex-shrink-0 w-[140px] text-right" title={displayName}>
<span className="text-xs font-medium text-gray-400 truncate block group-hover:text-gray-200 transition-colors">
<span className="text-xs font-medium text-fg-secondary truncate block group-hover:text-fg-secondary transition-colors">
{displayName}
</span>
</div>
@@ -154,7 +154,7 @@ function LaneRow({ lane, color, maxCount, onShowTip, onHideTip }: LaneRowProps)
</div>
{/* Timing range */}
<div className="flex-shrink-0 w-[72px] text-[11px] text-gray-600 tabular-nums">
<div className="flex-shrink-0 w-[72px] text-[11px] text-fg-muted tabular-nums">
{startPct}%&ndash;{endPct}%
</div>
</div>
@@ -229,7 +229,7 @@ function EmptyState() {
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="w-10 h-10 rounded-xl bg-surface-4 flex items-center justify-center mb-3">
<svg
className="w-5 h-5 text-gray-600"
className="w-5 h-5 text-fg-muted"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -240,8 +240,8 @@ function EmptyState() {
<rect x="3" y="16" width="15" height="4" rx="1" />
</svg>
</div>
<p className="text-sm font-medium text-gray-400">{t("concurrency.noData")}</p>
<p className="text-xs text-gray-600 mt-1">{t("concurrency.noDataDesc")}</p>
<p className="text-sm font-medium text-fg-secondary">{t("concurrency.noData")}</p>
<p className="text-xs text-fg-muted mt-1">{t("concurrency.noDataDesc")}</p>
</div>
);
}
@@ -314,15 +314,15 @@ export function ConcurrencyTimeline({ data }: ConcurrencyTimelineProps) {
<div className="flex items-center gap-3 mb-2">
<div className="flex-shrink-0 w-[140px]" />
<div className="flex-1 flex items-center justify-between">
<span className="text-[10px] text-gray-600 uppercase tracking-wider">
<span className="text-[10px] text-fg-muted uppercase tracking-wider">
{t("concurrency.sessions")}
</span>
<span className="text-[10px] text-gray-600 tabular-nums">
<span className="text-[10px] text-fg-muted tabular-nums">
{maxCount}
{t("concurrency.max")}
</span>
</div>
<div className="flex-shrink-0 w-[72px] text-[10px] text-gray-600 uppercase tracking-wider">
<div className="flex-shrink-0 w-[72px] text-[10px] text-fg-muted uppercase tracking-wider">
{t("concurrency.timing")}
</div>
</div>
@@ -92,7 +92,7 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
if (!hasErrors) {
return (
<div className="flex flex-col items-center justify-center py-16 gap-3">
<div className="w-12 h-12 rounded-full bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center">
<div className="w-12 h-12 rounded-full bg-status-success/10 border border-status-success/20 flex items-center justify-center">
<svg
width="24"
height="24"
@@ -107,10 +107,10 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
<polyline points="22 4 12 14.01 9 11.01" />
</svg>
</div>
<span className="text-sm text-emerald-400 font-medium">
<span className="text-sm text-status-success font-medium">
{t("errorPropagation.noErrors")}
</span>
<span className="text-xs text-gray-600">{t("errorPropagation.allSuccess")}</span>
<span className="text-xs text-fg-muted">{t("errorPropagation.allSuccess")}</span>
</div>
);
}
@@ -124,20 +124,20 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
return (
<div className="flex flex-col gap-4">
{/* Error rate summary bar */}
<div className="flex items-center gap-3 p-3 rounded-xl bg-red-500/5 border border-red-500/15">
<div className="flex-shrink-0 min-w-[2.75rem] h-10 px-2 rounded-lg bg-red-500/10 border border-red-500/20 flex items-center justify-center">
<span className="text-[13px] font-bold text-red-400 tabular-nums whitespace-nowrap">
<div className="flex items-center gap-3 p-3 rounded-xl bg-status-danger/5 border border-status-danger/15">
<div className="flex-shrink-0 min-w-[2.75rem] h-10 px-2 rounded-lg bg-status-danger/10 border border-status-danger/20 flex items-center justify-center">
<span className="text-[13px] font-bold text-status-danger tabular-nums whitespace-nowrap">
{errorRatePct}%
</span>
</div>
<div className="min-w-0 flex-1">
<p className="text-xs font-medium text-red-300">
<p className="text-xs font-medium text-status-danger">
{t("errorPropagation.sessionsErrorSummary", {
errorSessions: data.sessionsWithErrors,
totalSessions: data.totalSessions,
})}
</p>
<p className="text-[11px] text-gray-500 mt-0.5">
<p className="text-[11px] text-fg-muted mt-0.5">
{totalErrors > 0
? `${totalErrors}${t("errorPropagation.agentErrors")}`
: t("errorPropagation.sessionErrorsOnly")}
@@ -148,7 +148,7 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
{/* Errors by depth - horizontal bars */}
{hasDepthData && (
<div>
<p className="text-[10px] font-medium text-gray-500 uppercase tracking-wider mb-2.5">
<p className="text-[10px] font-medium text-fg-muted uppercase tracking-wider mb-2.5">
{t("errorPropagation.errorsByDepth")}
</p>
<div className="flex flex-col gap-1.5">
@@ -165,7 +165,7 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
onMouseEnter={() => setHoveredDepth(d.depth)}
onMouseLeave={() => setHoveredDepth(null)}
>
<span className="text-[11px] text-gray-500 w-24 flex-shrink-0 text-right truncate">
<span className="text-[11px] text-fg-muted w-24 flex-shrink-0 text-right truncate">
{depthLabel(d.depth)}
</span>
<div className="flex-1 h-5 bg-surface-3 rounded overflow-hidden relative">
@@ -194,7 +194,7 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
{/* Error-prone agent types */}
{topTypes.length > 0 && (
<div>
<p className="text-[10px] font-medium text-gray-500 uppercase tracking-wider mb-2.5">
<p className="text-[10px] font-medium text-fg-muted uppercase tracking-wider mb-2.5">
{t("errorPropagation.errorProneTypes")}
</p>
<div className="flex flex-col gap-1">
@@ -210,16 +210,16 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
className="w-1.5 h-1.5 rounded-full flex-shrink-0"
style={{ backgroundColor: DEPTH_COLORS[Math.min(i, DEPTH_COLORS.length - 1)] }}
/>
<span className="text-xs text-gray-300 truncate flex-1 min-w-0">
<span className="text-xs text-fg-secondary truncate flex-1 min-w-0">
{t.subagent_type}
</span>
<div className="w-16 h-1.5 bg-surface-4 rounded-full overflow-hidden flex-shrink-0">
<div
className="h-full rounded-full bg-red-400/60"
className="h-full rounded-full bg-status-danger/60"
style={{ width: `${Math.max(pct, 8)}%` }}
/>
</div>
<span className="text-[11px] font-semibold text-red-300 tabular-nums w-5 text-right flex-shrink-0">
<span className="text-[11px] font-semibold text-status-danger tabular-nums w-5 text-right flex-shrink-0">
{t.count}
</span>
</div>
@@ -232,14 +232,14 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
{/* API & session errors */}
{data.eventErrors && data.eventErrors.length > 0 && (
<div>
<p className="text-[10px] font-medium text-gray-500 uppercase tracking-wider mb-2.5">
<p className="text-[10px] font-medium text-fg-muted uppercase tracking-wider mb-2.5">
{t("errorPropagation.apiSessionErrors")}
</p>
<div className="flex flex-col gap-1">
{data.eventErrors.map((e) => (
<div
key={e.summary}
className="flex items-center gap-2.5 px-2.5 py-2 rounded-lg bg-amber-500/5 border border-amber-500/10 hover:border-amber-500/20 transition-colors"
className="flex items-center gap-2.5 px-2.5 py-2 rounded-lg bg-status-warning/5 border border-status-warning/10 hover:border-status-warning/20 transition-colors"
>
<svg
width="14"
@@ -256,10 +256,13 @@ export function ErrorPropagationMap({ data }: ErrorPropagationMapProps) {
<line x1="12" y1="9" x2="12" y2="13" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
<span className="text-xs text-gray-300 truncate flex-1 min-w-0" title={e.summary}>
<span
className="text-xs text-fg-secondary truncate flex-1 min-w-0"
title={e.summary}
>
{e.summary}
</span>
<span className="flex-shrink-0 text-[11px] font-semibold text-amber-400 tabular-nums">
<span className="flex-shrink-0 text-[11px] font-semibold text-status-warning tabular-nums">
{e.count}x
</span>
</div>
@@ -402,7 +402,7 @@ export function ModelDelegationFlow({ data }: ModelDelegationFlowProps) {
if (!hasData) {
return (
<div className="flex flex-col items-center justify-center py-16 gap-3 text-gray-500">
<div className="flex flex-col items-center justify-center py-16 gap-3 text-fg-muted">
<svg
width="40"
height="40"
@@ -787,7 +787,7 @@ export function OrchestrationDAG({ data, onNodeClick, selectedNode }: Orchestrat
fill="none"
stroke="currentColor"
strokeWidth={1.5}
className="w-6 h-6 text-gray-500"
className="w-6 h-6 text-fg-muted"
>
<circle cx="6" cy="12" r="2" />
<circle cx="18" cy="6" r="2" />
@@ -796,8 +796,10 @@ export function OrchestrationDAG({ data, onNodeClick, selectedNode }: Orchestrat
<line x1="8" y1="13" x2="16" y2="17" />
</svg>
</div>
<h3 className="text-base font-medium text-gray-300 mb-2">{t("orchestration.noData")}</h3>
<p className="text-sm text-gray-500 max-w-sm">{t("orchestration.noDataDesc")}</p>
<h3 className="text-base font-medium text-fg-secondary mb-2">
{t("orchestration.noData")}
</h3>
<p className="text-sm text-fg-muted max-w-sm">{t("orchestration.noDataDesc")}</p>
</div>
);
}
@@ -831,7 +833,7 @@ export function OrchestrationDAG({ data, onNodeClick, selectedNode }: Orchestrat
{/* Legend */}
<div className="flex flex-wrap items-center gap-x-5 gap-y-2 px-1 mt-4">
<span className="text-[10px] text-gray-600 uppercase tracking-widest font-medium mr-1">
<span className="text-[10px] text-fg-muted uppercase tracking-widest font-medium mr-1">
{t("orchestration.legend")}
</span>
{LEGEND_ITEMS.map((item) => (
@@ -840,7 +842,7 @@ export function OrchestrationDAG({ data, onNodeClick, selectedNode }: Orchestrat
className="inline-block w-3 h-3 rounded-sm flex-shrink-0"
style={{ background: item.color, border: `1px solid ${item.border}` }}
/>
<span className="text-[11px] text-gray-500">{item.label}</span>
<span className="text-[11px] text-fg-muted">{item.label}</span>
</div>
))}
<div className="flex items-center gap-1.5 ml-2">
@@ -848,7 +850,7 @@ export function OrchestrationDAG({ data, onNodeClick, selectedNode }: Orchestrat
className="inline-block h-[2px] w-8 rounded flex-shrink-0"
style={{ background: "linear-gradient(to right, #312e81, #4f46e5)" }}
/>
<span className="text-[11px] text-gray-500">{t("orchestration.edgeWeight")}</span>
<span className="text-[11px] text-fg-muted">{t("orchestration.edgeWeight")}</span>
</div>
</div>
@@ -882,12 +884,12 @@ function buildDAGTooltipContent(el: HTMLDivElement, node: DAGNode, t: TFn) {
const meta = describeNode(node, t);
const title = document.createElement("p");
title.className = "text-xs font-semibold text-gray-200";
title.className = "text-xs font-semibold text-fg-secondary";
title.textContent = node.label;
el.appendChild(title);
const subtitle = document.createElement("p");
subtitle.className = "text-[10px] uppercase tracking-wider text-gray-600 mt-0.5 mb-2";
subtitle.className = "text-[10px] uppercase tracking-wider text-fg-muted mt-0.5 mb-2";
subtitle.textContent = meta.layer;
el.appendChild(subtitle);
@@ -121,17 +121,17 @@ function Tooltip({ state }: { state: TooltipState }) {
return (
<div
className="fixed z-50 px-3 py-2 text-xs bg-[#12121f] border border-[#2a2a4a] rounded-lg shadow-xl text-gray-200 pointer-events-none whitespace-nowrap"
className="fixed z-50 px-3 py-2 text-xs bg-[#12121f] border border-[#2a2a4a] rounded-lg shadow-xl text-fg-secondary pointer-events-none whitespace-nowrap"
style={{
left: nearRight ? state.x - 12 : state.x + 12,
top: state.y - 10,
transform: nearRight ? "translateX(-100%)" : undefined,
}}
>
<p className="font-semibold text-gray-100 mb-1 truncate max-w-[180px]">
<p className="font-semibold text-fg-primary mb-1 truncate max-w-[180px]">
{state.item.name ?? state.item.id.slice(0, 12)}
</p>
<div className="flex flex-col gap-0.5 text-gray-400">
<div className="flex flex-col gap-0.5 text-fg-secondary">
<span>
{t("complexity.tooltip.duration")} {formatDurationSec(state.item.duration)}
</span>
@@ -173,7 +173,7 @@ function Legend() {
className="w-3 h-3 rounded-full flex-shrink-0"
style={{ backgroundColor: statusColor(s) }}
/>
<span className="text-xs text-gray-500">
<span className="text-xs text-fg-muted">
{t(`common:status.${s}`, { defaultValue: s })}
</span>
</div>
@@ -190,7 +190,7 @@ function EmptyState() {
<div className="flex flex-col items-center justify-center py-16 text-center">
<div className="w-10 h-10 rounded-xl bg-surface-4 flex items-center justify-center mb-3">
<svg
className="w-5 h-5 text-gray-600"
className="w-5 h-5 text-fg-muted"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -201,8 +201,8 @@ function EmptyState() {
<circle cx="14" cy="17" r="4" />
</svg>
</div>
<p className="text-sm font-medium text-gray-400">{t("complexity.noData")}</p>
<p className="text-xs text-gray-600 mt-1">{t("complexity.noDataDesc")}</p>
<p className="text-sm font-medium text-fg-secondary">{t("complexity.noData")}</p>
<p className="text-xs text-fg-muted mt-1">{t("complexity.noDataDesc")}</p>
</div>
);
}
@@ -83,15 +83,15 @@ function statusColor(status: string): string {
case "completed":
return "text-violet-400 bg-violet-500/10 border-violet-500/20";
case "working":
return "text-emerald-400 bg-emerald-500/10 border-emerald-500/20";
return "text-status-success bg-status-success/10 border-status-success/20";
case "error":
return "text-red-400 bg-red-500/10 border-red-500/20";
return "text-status-danger bg-status-danger/10 border-status-danger/20";
case "active":
return "text-emerald-400 bg-emerald-500/10 border-emerald-500/20";
return "text-status-success bg-status-success/10 border-status-success/20";
case "waiting":
return "text-yellow-400 bg-yellow-500/10 border-yellow-500/20";
default:
return "text-gray-400 bg-gray-500/10 border-gray-500/20";
return "text-fg-secondary bg-surface-4/10 border-border-light/20";
}
}
@@ -140,8 +140,8 @@ function TabBar({ active, onChange }: TabBarProps) {
className={[
"flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors duration-150",
active === tab.id
? "bg-surface-5 text-gray-100 shadow-sm"
: "text-gray-500 hover:text-gray-300",
? "bg-surface-5 text-fg-primary shadow-sm"
: "text-fg-muted hover:text-fg-secondary",
].join(" ")}
>
{tab.icon}
@@ -192,20 +192,20 @@ function TreeNode({ node, depth }: TreeNodeProps) {
{/* Name */}
<span
className={`text-sm font-medium truncate ${isMain ? "text-indigo-300" : "text-gray-200"}`}
className={`text-sm font-medium truncate ${isMain ? "text-indigo-300" : "text-fg-secondary"}`}
>
{node.name}
</span>
{/* Subagent type */}
{node.subagent_type && (
<span className="text-xs text-gray-500 truncate flex-shrink-0">
<span className="text-xs text-fg-muted truncate flex-shrink-0">
[{node.subagent_type}]
</span>
)}
{/* Duration */}
<span className="ml-auto flex-shrink-0 text-xs text-gray-600 tabular-nums">{dur}</span>
<span className="ml-auto flex-shrink-0 text-xs text-fg-muted tabular-nums">{dur}</span>
</div>
{node.children.length > 0 && (
@@ -226,7 +226,7 @@ interface AgentTreeProps {
function AgentTree({ tree }: AgentTreeProps) {
const { t } = useTranslation("workflows");
if (tree.length === 0) {
return <p className="text-sm text-gray-500 text-center py-8">{t("drillIn.noAgentTree")}</p>;
return <p className="text-sm text-fg-muted text-center py-8">{t("drillIn.noAgentTree")}</p>;
}
return (
@@ -249,7 +249,7 @@ interface ToolTimelineProps {
function ToolTimeline({ events }: ToolTimelineProps) {
const { t } = useTranslation("workflows");
if (events.length === 0) {
return <p className="text-sm text-gray-500 text-center py-8">{t("drillIn.noToolEvents")}</p>;
return <p className="text-sm text-fg-muted text-center py-8">{t("drillIn.noToolEvents")}</p>;
}
return (
@@ -267,11 +267,13 @@ function ToolTimeline({ events }: ToolTimelineProps) {
{/* Summary */}
{ev.summary && (
<span className="text-xs text-gray-400 truncate flex-1 min-w-0">{ev.summary}</span>
<span className="text-xs text-fg-secondary truncate flex-1 min-w-0">
{ev.summary}
</span>
)}
{/* Timestamp */}
<span className="flex-shrink-0 text-[10px] text-gray-600 tabular-nums ml-auto">
<span className="flex-shrink-0 text-[10px] text-fg-muted tabular-nums ml-auto">
{safeTimestamp(ev.created_at)}
</span>
</div>
@@ -288,22 +290,22 @@ interface EventSequenceProps {
}
const EVENT_TYPE_COLOR: Record<string, string> = {
tool_use: "text-blue-400",
tool_result: "text-emerald-400",
tool_use: "text-blue-500",
tool_result: "text-status-success",
agent_start: "text-indigo-400",
agent_stop: "text-violet-400",
compaction: "text-amber-400",
error: "text-red-400",
compaction: "text-status-warning",
error: "text-status-danger",
};
function eventTypeColor(type: string): string {
return EVENT_TYPE_COLOR[type] ?? "text-gray-400";
return EVENT_TYPE_COLOR[type] ?? "text-fg-secondary";
}
function EventSequence({ events }: EventSequenceProps) {
const { t } = useTranslation("workflows");
if (events.length === 0) {
return <p className="text-sm text-gray-500 text-center py-8">{t("drillIn.noEvents")}</p>;
return <p className="text-sm text-fg-muted text-center py-8">{t("drillIn.noEvents")}</p>;
}
const recent = events.slice(0, 100);
@@ -325,18 +327,18 @@ function EventSequence({ events }: EventSequenceProps) {
</span>
{/* Summary */}
<span className="text-xs text-gray-400 flex-1 min-w-0 truncate">
<span className="text-xs text-fg-secondary flex-1 min-w-0 truncate">
{ev.summary ?? ev.tool_name ?? "-"}
</span>
{/* Timestamp */}
<span className="flex-shrink-0 text-[10px] text-gray-600 tabular-nums opacity-0 group-hover:opacity-100 transition-opacity">
<span className="flex-shrink-0 text-[10px] text-fg-muted tabular-nums opacity-0 group-hover:opacity-100 transition-opacity">
{safeTimestamp(ev.created_at)}
</span>
</div>
))}
{events.length > 100 && (
<p className="text-xs text-gray-600 text-center py-2">
<p className="text-xs text-fg-muted text-center py-2">
{t("drillIn.showingOf", { total: events.length })}
</p>
)}
@@ -365,11 +367,11 @@ function ErrorState({ message }: ErrorStateProps) {
const { t } = useTranslation("workflows");
return (
<div className="flex flex-col items-center justify-center py-10 text-center px-4">
<div className="w-9 h-9 rounded-xl bg-red-500/10 border border-red-500/20 flex items-center justify-center mb-3">
<X className="w-4 h-4 text-red-400" />
<div className="w-9 h-9 rounded-xl bg-status-danger/10 border border-status-danger/20 flex items-center justify-center mb-3">
<X className="w-4 h-4 text-status-danger" />
</div>
<p className="text-sm font-medium text-red-400">{t("drillIn.failedLoad")}</p>
<p className="text-xs text-gray-600 mt-1 max-w-xs">{message}</p>
<p className="text-sm font-medium text-status-danger">{t("drillIn.failedLoad")}</p>
<p className="text-xs text-fg-muted mt-1 max-w-xs">{message}</p>
</div>
);
}
@@ -405,17 +407,19 @@ function NoSessionState({ onSelectSession }: NoSessionStateProps) {
<div className="flex flex-col items-center text-center mt-2">
<div className="w-10 h-10 rounded-xl bg-surface-4 flex items-center justify-center mb-4">
<GitFork className="w-5 h-5 text-gray-600" />
<GitFork className="w-5 h-5 text-fg-muted" />
</div>
<p className="text-sm font-medium text-gray-400 mb-1">{t("drillIn.noSessionSelected")}</p>
<p className="text-xs text-gray-600 max-w-xs">{t("drillIn.noSessionDesc")}</p>
<p className="text-sm font-medium text-fg-secondary mb-1">
{t("drillIn.noSessionSelected")}
</p>
<p className="text-xs text-fg-muted max-w-xs">{t("drillIn.noSessionDesc")}</p>
{/* Preview tab pills */}
<div className="flex gap-2 mt-5">
{tabs.map((tab) => (
<div
key={tab.id}
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium text-gray-600 bg-surface-3 border border-border"
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md text-xs font-medium text-fg-muted bg-surface-3 border border-border"
>
{tab.icon}
{tab.label}
@@ -444,10 +448,10 @@ function SessionHeader({ drillIn, onClose, activeTab, onTabChange }: SessionHead
<div className="flex flex-col gap-3 mb-4">
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
<p className="text-sm font-semibold text-gray-100 truncate">
<p className="text-sm font-semibold text-fg-primary truncate">
{session.name ?? session.id}
</p>
<p className="text-xs text-gray-500 mt-0.5 truncate">
<p className="text-xs text-fg-muted mt-0.5 truncate">
{formatModelName(session.model) ?? t("drillIn.unknownModel")} &middot;{" "}
{t(`common:status.${session.status}`, { defaultValue: session.status })}
{session.started_at && ` \u00b7 ${safeTimestamp(session.started_at)}`}
@@ -456,7 +460,7 @@ function SessionHeader({ drillIn, onClose, activeTab, onTabChange }: SessionHead
<button
type="button"
onClick={onClose}
className="flex-shrink-0 w-7 h-7 rounded-md flex items-center justify-center text-gray-500 hover:text-gray-200 hover:bg-white/10 transition-colors"
className="flex-shrink-0 w-7 h-7 rounded-md flex items-center justify-center text-fg-muted hover:text-fg-secondary hover:bg-white/10 transition-colors"
aria-label={t("drillIn.closePanel")}
>
<X className="w-4 h-4" />
@@ -569,13 +573,13 @@ function SessionSelector({ onSelectSession }: SessionSelectorProps) {
inputRef.current?.focus();
}}
>
<Search className="w-3.5 h-3.5 text-gray-500 flex-shrink-0" />
<Search className="w-3.5 h-3.5 text-fg-muted flex-shrink-0" />
<input
ref={inputRef}
type="text"
value={search}
placeholder={t("drillIn.searchPlaceholder")}
className="flex-1 bg-transparent text-xs text-gray-200 placeholder-gray-600 outline-none min-w-0"
className="flex-1 bg-transparent text-xs text-fg-secondary placeholder-fg-muted outline-none min-w-0"
onFocus={() => setOpen(true)}
onChange={(e) => {
setSearch(e.target.value);
@@ -584,7 +588,7 @@ function SessionSelector({ onSelectSession }: SessionSelectorProps) {
/>
<ChevronDown
className={[
"w-3.5 h-3.5 text-gray-600 flex-shrink-0 transition-transform duration-150",
"w-3.5 h-3.5 text-fg-muted flex-shrink-0 transition-transform duration-150",
open ? "rotate-180" : "",
].join(" ")}
/>
@@ -605,7 +609,7 @@ function SessionSelector({ onSelectSession }: SessionSelectorProps) {
))}
</div>
) : filtered.length === 0 ? (
<p className="text-xs text-gray-600 text-center py-6 px-3">
<p className="text-xs text-fg-muted text-center py-6 px-3">
{search.trim() ? t("drillIn.noMatch") : t("drillIn.notFound")}
</p>
) : (
@@ -625,22 +629,22 @@ function SessionSelector({ onSelectSession }: SessionSelectorProps) {
{s.status}
</span>
<span className="flex-1 min-w-0">
<span className="block text-xs font-medium text-gray-200 truncate">
<span className="block text-xs font-medium text-fg-secondary truncate">
{s.name ?? s.id}
</span>
{s.name && (
<span className="block text-[10px] text-gray-600 font-mono truncate">
<span className="block text-[10px] text-fg-muted font-mono truncate">
{s.id}
</span>
)}
</span>
{s.model && (
<span className="flex-shrink-0 text-[10px] text-gray-500 truncate max-w-[80px]">
<span className="flex-shrink-0 text-[10px] text-fg-muted truncate max-w-[80px]">
{formatModelName(s.model)}
</span>
)}
{s.started_at && (
<span className="flex-shrink-0 text-[10px] text-gray-600 tabular-nums">
<span className="flex-shrink-0 text-[10px] text-fg-muted tabular-nums">
{safeTimestamp(s.started_at)}
</span>
)}
@@ -656,7 +660,7 @@ function SessionSelector({ onSelectSession }: SessionSelectorProps) {
type="button"
onClick={handleLoadMore}
disabled={loading}
className="w-full px-3 py-2 text-xs text-gray-500 hover:text-gray-300 hover:bg-white/5 transition-colors border-t border-border/50 disabled:opacity-50"
className="w-full px-3 py-2 text-xs text-fg-muted hover:text-fg-secondary hover:bg-white/5 transition-colors border-t border-border/50 disabled:opacity-50"
>
{loading ? t("drillIn.loading") : t("drillIn.loadMore")}
</button>
@@ -737,11 +741,11 @@ export function SessionDrillIn({ sessionId, onClose, onSelectSession }: SessionD
<div className="bg-surface-2 border border-border rounded-xl p-4">
<SessionSelector onSelectSession={onSelectSession} />
<div className="flex items-center justify-between mb-2">
<p className="text-xs text-gray-600 font-mono truncate">{sessionId}</p>
<p className="text-xs text-fg-muted font-mono truncate">{sessionId}</p>
<button
type="button"
onClick={onClose}
className="w-6 h-6 flex items-center justify-center rounded text-gray-600 hover:text-gray-300 hover:bg-white/10 transition-colors"
className="w-6 h-6 flex items-center justify-center rounded text-fg-muted hover:text-fg-secondary hover:bg-white/10 transition-colors"
aria-label={t("drillIn.close")}
>
<X className="w-3.5 h-3.5" />
@@ -146,7 +146,7 @@ function SuccessRing({ rate, color }: SuccessRingProps) {
{clampedRate.toFixed(0)}%
</text>
</svg>
<span className="text-[10px] font-medium text-gray-500 uppercase tracking-wider">
<span className="text-[10px] font-medium text-fg-muted uppercase tracking-wider">
{t("effectiveness.success")}
</span>
</div>
@@ -213,7 +213,7 @@ function Sparkline({ data, color }: SparklineProps) {
{bars.map((_, i) => (
<span
key={i}
className="flex-1 text-center text-[8px] text-gray-600 leading-none select-none"
className="flex-1 text-center text-[8px] text-fg-muted leading-none select-none"
>
{dayLabels[i % dayLabels.length] ?? ""}
</span>
@@ -281,11 +281,11 @@ function SparklineTooltip({
<div
ref={ref}
role="tooltip"
className="fixed z-[60] px-2 py-1 bg-[#12121f] border border-[#2a2a4a] rounded-md shadow-xl text-[10px] text-gray-200 whitespace-nowrap pointer-events-none"
className="fixed z-[60] px-2 py-1 bg-[#12121f] border border-[#2a2a4a] rounded-md shadow-xl text-[10px] text-fg-secondary whitespace-nowrap pointer-events-none"
style={{ left: pos.left, top: pos.top }}
>
<span className="font-medium">{label}</span>
<span className="text-gray-400 mx-1">·</span>
<span className="text-fg-secondary mx-1">·</span>
<span className="tabular-nums" style={{ color }}>
{t("effectiveness.sessionCount", { count: value })}
</span>
@@ -302,10 +302,10 @@ interface MetricBoxProps {
function MetricBox({ label, value }: MetricBoxProps) {
return (
<div className="flex flex-col items-center gap-0.5 bg-surface-3 rounded-lg px-2 py-2 flex-1 min-w-0 overflow-hidden">
<span className="text-xs font-semibold text-gray-200 tabular-nums truncate w-full text-center">
<span className="text-xs font-semibold text-fg-secondary tabular-nums truncate w-full text-center">
{value}
</span>
<span className="text-[9px] text-gray-500 uppercase tracking-wider truncate w-full text-center">
<span className="text-[9px] text-fg-muted uppercase tracking-wider truncate w-full text-center">
{label}
</span>
</div>
@@ -337,7 +337,7 @@ function ScoreCard({ item, colorIndex }: ScoreCardProps) {
style={{ backgroundColor: color }}
aria-hidden="true"
/>
<span className="text-sm font-medium text-gray-200 truncate" title={item.subagent_type}>
<span className="text-sm font-medium text-fg-secondary truncate" title={item.subagent_type}>
{item.subagent_type}
</span>
</div>
@@ -358,7 +358,7 @@ function ScoreCard({ item, colorIndex }: ScoreCardProps) {
{/* Sparkline */}
<div className="flex flex-col gap-1">
<span className="text-[10px] text-gray-500 uppercase tracking-wider">
<span className="text-[10px] text-fg-muted uppercase tracking-wider">
{t("effectiveness.weeklyActivity")}
</span>
<Sparkline data={item.trend} color={color} />
@@ -375,7 +375,7 @@ export function SubagentEffectiveness({ data }: SubagentEffectivenessProps) {
const { t } = useTranslation("workflows");
if (data.length === 0) {
return (
<div className="flex items-center justify-center py-16 text-gray-500 text-sm">
<div className="flex items-center justify-center py-16 text-fg-muted text-sm">
{t("effectiveness.noData")}
</div>
);
@@ -507,7 +507,7 @@ export function ToolExecutionFlow({
<div className="relative" ref={containerRef} onMouseLeave={hideTip}>
{isEmpty ? (
<div className="flex items-center justify-center" style={{ height: dimensions.height }}>
<span className="text-sm text-gray-500">{t("toolFlow.noData")}</span>
<span className="text-sm text-fg-muted">{t("toolFlow.noData")}</span>
</div>
) : (
<svg
@@ -663,7 +663,7 @@ function Legend() {
style={{ background: color, opacity: 0.9 }}
className="inline-block w-2.5 h-2.5 rounded-sm flex-shrink-0"
/>
<span className="text-xs text-gray-400">{t(`toolLegend.${key}`)}</span>
<span className="text-xs text-fg-secondary">{t(`toolLegend.${key}`)}</span>
</div>
))}
</div>
@@ -159,12 +159,12 @@ function StepFlow({ steps }: { steps: string[] }) {
<span key={idx} className="flex items-center gap-1">
<StepPill label={step} />
{(idx < visible.length - 1 || overflow > 0) && (
<ChevronRight className="w-3.5 h-3.5 flex-shrink-0 text-gray-600" />
<ChevronRight className="w-3.5 h-3.5 flex-shrink-0 text-fg-muted" />
)}
</span>
))}
{overflow > 0 && (
<span className="inline-flex items-center px-2 py-1 rounded-lg text-xs font-medium bg-gray-700/50 text-gray-400 border border-gray-600/20 whitespace-nowrap">
<span className="inline-flex items-center px-2 py-1 rounded-lg text-xs font-medium bg-surface-3/50 text-fg-secondary border border-border-light/20 whitespace-nowrap">
{t("common:plusMore", { count: overflow })}
</span>
)}
@@ -176,8 +176,8 @@ function PatternFrequency({ count, percentage }: { count: number; percentage: nu
const { t } = useTranslation("workflows");
return (
<div className="flex-shrink-0 text-right">
<p className="text-sm font-semibold text-gray-100">{count.toLocaleString()}</p>
<p className="text-xs text-gray-500">
<p className="text-sm font-semibold text-fg-primary">{count.toLocaleString()}</p>
<p className="text-xs text-fg-muted">
{percentage.toFixed(1)}% {t("common:ofSessions", { defaultValue: "of sessions" })}
</p>
</div>
@@ -230,7 +230,7 @@ function PatternItem({ pattern, rank, isSelected, onClick }: PatternItemProps) {
{/* Click affordance - visible only when not yet expanded so users know the row is interactive. */}
{!isSelected && (
<Info className="hidden sm:block w-3.5 h-3.5 text-gray-600 flex-shrink-0" />
<Info className="hidden sm:block w-3.5 h-3.5 text-fg-muted flex-shrink-0" />
)}
</button>
@@ -249,7 +249,7 @@ function PatternDetail({ pattern }: { pattern: WorkflowPattern }) {
<div className="border-t border-indigo-500/20 bg-surface-1/40 px-4 py-3.5 space-y-3.5">
{/* Full step sequence (no truncation) */}
<div>
<p className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider mb-2">
<p className="text-[10px] font-semibold text-fg-muted uppercase tracking-wider mb-2">
{t("patterns.detail.stepsHeading")}
</p>
<div className="flex items-center flex-wrap gap-1.5">
@@ -260,7 +260,7 @@ function PatternDetail({ pattern }: { pattern: WorkflowPattern }) {
{step}
</span>
{i < pattern.steps.length - 1 && (
<ChevronRight className="w-3.5 h-3.5 flex-shrink-0 text-gray-600" />
<ChevronRight className="w-3.5 h-3.5 flex-shrink-0 text-fg-muted" />
)}
</span>
))}
@@ -286,11 +286,11 @@ function PatternDetail({ pattern }: { pattern: WorkflowPattern }) {
{/* Narrative - what this means */}
<div>
<p className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider mb-1.5 flex items-center gap-1.5">
<p className="text-[10px] font-semibold text-fg-muted uppercase tracking-wider mb-1.5 flex items-center gap-1.5">
<Info className="w-3 h-3 text-indigo-400" />
{t("patterns.detail.narrativeHeading")}
</p>
<p className="text-xs text-gray-300 leading-relaxed">{narrative}</p>
<p className="text-xs text-fg-secondary leading-relaxed">{narrative}</p>
</div>
{/* Suggestion */}
@@ -299,7 +299,7 @@ function PatternDetail({ pattern }: { pattern: WorkflowPattern }) {
<Lightbulb className="w-3 h-3" />
{t("patterns.detail.suggestionHeading")}
</p>
<p className="text-xs text-gray-300 leading-relaxed">{suggestion}</p>
<p className="text-xs text-fg-secondary leading-relaxed">{suggestion}</p>
</div>
</div>
);
@@ -308,8 +308,8 @@ function PatternDetail({ pattern }: { pattern: WorkflowPattern }) {
function DetailStat({ label, value }: { label: string; value: string }) {
return (
<div className="bg-surface-2 border border-border rounded-md px-2.5 py-2">
<p className="text-sm font-semibold text-gray-100 tabular-nums">{value}</p>
<p className="text-[10px] text-gray-500 uppercase tracking-wider mt-0.5 truncate">{label}</p>
<p className="text-sm font-semibold text-fg-primary tabular-nums">{value}</p>
<p className="text-[10px] text-fg-muted uppercase tracking-wider mt-0.5 truncate">{label}</p>
</div>
);
}
@@ -327,8 +327,8 @@ function SoloSessionItem({ count, percentage }: { count: number; percentage: num
</span>
</div>
<div className="flex-shrink-0 text-right">
<p className="text-sm font-semibold text-gray-100">{count.toLocaleString()}</p>
<p className="text-xs text-gray-500">
<p className="text-sm font-semibold text-fg-primary">{count.toLocaleString()}</p>
<p className="text-xs text-fg-muted">
{percentage.toFixed(1)}% {t("common:ofSessions", { defaultValue: "of sessions" })}
</p>
</div>
@@ -341,10 +341,10 @@ function EmptyPatterns() {
return (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="w-10 h-10 rounded-xl bg-surface-4 flex items-center justify-center mb-3">
<Zap className="w-5 h-5 text-gray-600" />
<Zap className="w-5 h-5 text-fg-muted" />
</div>
<p className="text-sm font-medium text-gray-400">{t("patterns.noData")}</p>
<p className="text-xs text-gray-600 mt-1">{t("patterns.noDataDesc")}</p>
<p className="text-sm font-medium text-fg-secondary">{t("patterns.noData")}</p>
<p className="text-xs text-fg-muted mt-1">{t("patterns.noDataDesc")}</p>
</div>
);
}
@@ -372,7 +372,7 @@ export function WorkflowPatterns({ data, onPatternClick }: WorkflowPatternsProps
return (
<div className="card p-5">
<h2 className="text-sm font-semibold text-gray-300 uppercase tracking-wider mb-4">
<h2 className="text-sm font-semibold text-fg-secondary uppercase tracking-wider mb-4">
{t("patterns.label")}
</h2>
@@ -109,18 +109,18 @@ interface Props {
}
const STATUS_STYLES: Record<string, string> = {
running: "bg-amber-500/15 text-amber-400 border-amber-500/30",
working: "bg-amber-500/15 text-amber-400 border-amber-500/30",
queued: "bg-gray-500/15 text-gray-400 border-gray-500/30",
completed: "bg-emerald-500/15 text-emerald-400 border-emerald-500/30",
done: "bg-emerald-500/15 text-emerald-400 border-emerald-500/30",
success: "bg-emerald-500/15 text-emerald-400 border-emerald-500/30",
error: "bg-red-500/15 text-red-400 border-red-500/30",
failed: "bg-red-500/15 text-red-400 border-red-500/30",
running: "bg-status-warning/15 text-status-warning border-status-warning/30",
working: "bg-status-warning/15 text-status-warning border-status-warning/30",
queued: "bg-surface-4/15 text-fg-secondary border-border-light/30",
completed: "bg-status-success/15 text-status-success border-status-success/30",
done: "bg-status-success/15 text-status-success border-status-success/30",
success: "bg-status-success/15 text-status-success border-status-success/30",
error: "bg-status-danger/15 text-status-danger border-status-danger/30",
failed: "bg-status-danger/15 text-status-danger border-status-danger/30",
};
function statusClass(status: string): string {
return STATUS_STYLES[status] || "bg-gray-500/15 text-gray-400 border-gray-500/30";
return STATUS_STYLES[status] || "bg-surface-4/15 text-fg-secondary border-border-light/30";
}
// Distinct per-phase chip colors, cycled by phase index so every phase
@@ -129,15 +129,15 @@ function statusClass(status: string): string {
const PHASE_PALETTE = [
"bg-violet-500/15 text-violet-300 border-violet-500/40",
"bg-sky-500/15 text-sky-300 border-sky-500/40",
"bg-amber-500/15 text-amber-300 border-amber-500/40",
"bg-emerald-500/15 text-emerald-300 border-emerald-500/40",
"bg-status-warning/15 text-status-warning border-status-warning/40",
"bg-status-success/15 text-status-success border-status-success/40",
"bg-rose-500/15 text-rose-300 border-rose-500/40",
"bg-cyan-500/15 text-cyan-300 border-cyan-500/40",
"bg-fuchsia-500/15 text-fuchsia-300 border-fuchsia-500/40",
];
function phaseColor(phaseTitles: string[], title: string | null | undefined): string {
if (!title) return "bg-gray-500/15 text-gray-300 border-gray-500/40";
if (!title) return "bg-surface-4/15 text-fg-secondary border-border-light/40";
const i = phaseTitles.indexOf(title);
const idx = i >= 0 ? i : Math.abs(hashStr(title)) % PHASE_PALETTE.length;
return PHASE_PALETTE[idx % PHASE_PALETTE.length] as string;
@@ -326,7 +326,7 @@ export function WorkflowRunsPanel({
if (!controlled && loading) {
return (
<div className="flex items-center justify-center gap-2 py-8 text-sm text-gray-500">
<div className="flex items-center justify-center gap-2 py-8 text-sm text-fg-muted">
<Loader2 className="w-4 h-4 animate-spin text-violet-400" />
<span className="animate-pulse">{t("runs.loading")}</span>
</div>
@@ -334,8 +334,8 @@ export function WorkflowRunsPanel({
}
if (runs.length === 0) {
return (
<div className="text-sm text-gray-500 flex items-center gap-2">
<Workflow className="w-4 h-4 text-gray-600" />
<div className="text-sm text-fg-muted flex items-center gap-2">
<Workflow className="w-4 h-4 text-fg-muted" />
{t("runs.empty")}
</div>
);
@@ -356,36 +356,36 @@ export function WorkflowRunsPanel({
return (
<div
key={run.run_id}
className="rounded-lg border border-gray-800 bg-card/40 overflow-hidden"
className="rounded-lg border border-border bg-card/40 overflow-hidden"
>
<button
onClick={() => toggle(run.run_id)}
className="w-full flex items-center gap-3 px-3 py-2.5 text-left hover:bg-gray-800/30 transition-colors"
className="w-full flex items-center gap-3 px-3 py-2.5 text-left hover:bg-surface-2/30 transition-colors"
aria-expanded={isOpen}
>
{isOpen ? (
<ChevronDown className="w-4 h-4 text-gray-500 flex-shrink-0" />
<ChevronDown className="w-4 h-4 text-fg-muted flex-shrink-0" />
) : (
<ChevronRight className="w-4 h-4 text-gray-500 flex-shrink-0" />
<ChevronRight className="w-4 h-4 text-fg-muted flex-shrink-0" />
)}
{running ? (
<Loader2 className="w-4 h-4 text-amber-400 flex-shrink-0 animate-spin" />
<Loader2 className="w-4 h-4 text-status-warning flex-shrink-0 animate-spin" />
) : (
<Workflow className="w-4 h-4 text-violet-400 flex-shrink-0" />
)}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm font-medium text-gray-200 truncate">
<span className="text-sm font-medium text-fg-secondary truncate">
{run.name || run.run_id}
</span>
<span className={`badge text-[10px] border ${statusClass(run.status)}`}>
{t(`runs.status.${run.status}`, run.status)}
</span>
{run.default_model && (
<span className="text-[10px] font-mono text-gray-500">{run.default_model}</span>
<span className="text-[10px] font-mono text-fg-muted">{run.default_model}</span>
)}
</div>
<div className="flex items-center gap-3 mt-0.5 text-[11px] text-gray-500 flex-wrap">
<div className="flex items-center gap-3 mt-0.5 text-[11px] text-fg-muted flex-wrap">
<span>{t("runs.agents", { count: run.agent_count })}</span>
<span>{t("runs.tools", { count: run.total_tool_calls })}</span>
<span>
@@ -399,7 +399,7 @@ export function WorkflowRunsPanel({
<Link
to={`/sessions/${encodeURIComponent(run.session_id)}`}
onClick={(e) => e.stopPropagation()}
className="text-gray-500 hover:text-violet-400 transition-colors flex-shrink-0"
className="text-fg-muted hover:text-violet-400 transition-colors flex-shrink-0"
title={t("runs.openSession")}
>
<ExternalLink className="w-3.5 h-3.5" />
@@ -408,11 +408,11 @@ export function WorkflowRunsPanel({
</button>
{isOpen && (
<div className="px-3 pb-3 pt-3 border-t border-gray-800/60 space-y-3">
<div className="px-3 pb-3 pt-3 border-t border-border/60 space-y-3">
{/* Clickable, colored phase filters */}
{phaseTitles.length > 0 && (
<div className="flex items-center gap-1.5 flex-wrap">
<Layers className="w-3.5 h-3.5 text-gray-500" />
<Layers className="w-3.5 h-3.5 text-fg-muted" />
{phaseTitles.map((title, i) => {
const active = sel === title;
return (
@@ -435,7 +435,7 @@ export function WorkflowRunsPanel({
{sel && (
<button
onClick={() => setPhase(run.run_id, sel)}
className="text-[10px] text-gray-500 hover:text-gray-300 underline"
className="text-[10px] text-fg-muted hover:text-fg-secondary underline"
>
{t("runs.clearFilter")}
</button>
@@ -447,7 +447,7 @@ export function WorkflowRunsPanel({
<div className="overflow-x-auto">
<table className="w-full text-[11px]">
<thead>
<tr className="text-gray-500 text-left border-b border-gray-800">
<tr className="text-fg-muted text-left border-b border-border">
<th className="py-1 pr-3 font-medium">{t("runs.col.agent")}</th>
<th className="py-1 pr-3 font-medium">{t("runs.col.phase")}</th>
<th className="py-1 pr-3 font-medium">{t("runs.col.state")}</th>
@@ -464,11 +464,11 @@ export function WorkflowRunsPanel({
</thead>
<tbody>
{shown.map((a: WorkflowProgressEntry, i) => (
<tr key={a.agentId || i} className="border-b border-gray-800/40">
<td className="py-1 pr-3 text-gray-300">
<tr key={a.agentId || i} className="border-b border-border/40">
<td className="py-1 pr-3 text-fg-secondary">
{a.label || a.agentType || a.agentId}
{a.lastToolName && (
<span className="text-gray-600 font-mono ml-1">
<span className="text-fg-muted font-mono ml-1">
· {a.lastToolName}
</span>
)}
@@ -487,13 +487,13 @@ export function WorkflowRunsPanel({
{t(`runs.status.${a.state}`, String(a.state || "-"))}
</span>
</td>
<td className="py-1 pr-3 text-right text-gray-400">
<td className="py-1 pr-3 text-right text-fg-secondary">
{fmt(a.tokens || 0)}
</td>
<td className="py-1 pr-3 text-right text-gray-400">
<td className="py-1 pr-3 text-right text-fg-secondary">
{a.toolCalls || 0}
</td>
<td className="py-1 pr-3 text-right text-gray-400">
<td className="py-1 pr-3 text-right text-fg-secondary">
{a.durationMs != null ? formatMs(a.durationMs) : "-"}
</td>
</tr>
@@ -502,15 +502,15 @@ export function WorkflowRunsPanel({
</table>
</div>
) : (
<p className="text-[11px] text-gray-600">{t("runs.noAgents")}</p>
<p className="text-[11px] text-fg-muted">{t("runs.noAgents")}</p>
)}
{/* Clickable, colored, expandable results - full content on click */}
{resultRows.length > 0 && (
<div className="space-y-1.5">
<div className="text-[10px] font-medium uppercase tracking-wider text-gray-600">
<div className="text-[10px] font-medium uppercase tracking-wider text-fg-muted">
{t("runs.resultsLabel")}
<span className="ml-1 text-gray-700">· {resultRows.length}</span>
<span className="ml-1 text-fg-muted">· {resultRows.length}</span>
</div>
{resultRows.map((a, i) => {
const key = `${run.run_id}::${a.agentId || i}`;
@@ -528,7 +528,7 @@ export function WorkflowRunsPanel({
return (
<div
key={key}
className="rounded border border-gray-800/70 bg-gray-900/30 overflow-hidden"
className="rounded border border-border/70 bg-surface-0/30 overflow-hidden"
>
<button
onClick={() => {
@@ -537,13 +537,13 @@ export function WorkflowRunsPanel({
}
toggleResult(key);
}}
className="w-full flex items-center gap-2 px-2 py-1.5 text-left hover:bg-gray-800/40 transition-colors"
className="w-full flex items-center gap-2 px-2 py-1.5 text-left hover:bg-surface-2/40 transition-colors"
aria-expanded={open}
>
{open ? (
<ChevronDown className="w-3.5 h-3.5 text-gray-500 flex-shrink-0" />
<ChevronDown className="w-3.5 h-3.5 text-fg-muted flex-shrink-0" />
) : (
<ChevronRight className="w-3.5 h-3.5 text-gray-500 flex-shrink-0" />
<ChevronRight className="w-3.5 h-3.5 text-fg-muted flex-shrink-0" />
)}
<span
className={`badge text-[10px] border flex-shrink-0 ${phaseColor(phaseTitles, a.phaseTitle)}`}
@@ -551,14 +551,14 @@ export function WorkflowRunsPanel({
{a.label || a.agentType || a.agentId}
</span>
{!open && (
<span className="text-[11px] text-gray-500 leading-snug min-w-0">
<span className="text-[11px] text-fg-muted leading-snug min-w-0">
{truncate(friendlyPreview(a.resultPreview), 160)}
</span>
)}
</button>
{open && (
<div className="px-2.5 pb-2.5 pt-0.5 space-y-2">
<div className="flex flex-wrap items-center gap-2 text-[10px] text-gray-500">
<div className="flex flex-wrap items-center gap-2 text-[10px] text-fg-muted">
{a.model && <span className="font-mono">{a.model}</span>}
<span
className={`badge border ${statusClass(String(a.state || ""))}`}
@@ -579,19 +579,19 @@ export function WorkflowRunsPanel({
</div>
{fullPrompt && (
<div>
<div className="text-[10px] uppercase tracking-wider text-gray-600 mb-0.5">
<div className="text-[10px] uppercase tracking-wider text-fg-muted mb-0.5">
{t("runs.promptLabel")}
</div>
<pre className="text-[11px] text-gray-400 whitespace-pre-wrap break-words bg-black/30 rounded p-2 max-h-48 overflow-auto">
<pre className="text-[11px] text-fg-secondary whitespace-pre-wrap break-words bg-black/30 rounded p-2 max-h-48 overflow-auto">
{fullPrompt}
</pre>
</div>
)}
<div>
<div className="text-[10px] uppercase tracking-wider text-gray-600 mb-0.5">
<div className="text-[10px] uppercase tracking-wider text-fg-muted mb-0.5">
{t("runs.resultLabel")}
</div>
<pre className="text-[11px] text-gray-300 whitespace-pre-wrap break-words bg-black/30 rounded p-2 max-h-96 overflow-auto">
<pre className="text-[11px] text-fg-secondary whitespace-pre-wrap break-words bg-black/30 rounded p-2 max-h-96 overflow-auto">
{fullResult}
</pre>
</div>
@@ -76,9 +76,9 @@ function formatDurationSec(sec: number): string {
}
function successRateColor(rate: number): string {
if (rate > 90) return "text-emerald-400";
if (rate > 90) return "text-status-success";
if (rate > 70) return "text-yellow-400";
return "text-red-400";
return "text-status-danger";
}
// ── Deterministic interpreters - return an i18n key + params ─────────────────
@@ -228,7 +228,7 @@ function InfoPopover({ calculationKey, interp, valueDisplay, metricPhraseKey }:
onMouseLeave={() => setOpen(false)}
onFocus={() => setOpen(true)}
onBlur={() => setOpen(false)}
className="flex items-center justify-center rounded-full p-0.5 -m-0.5 text-gray-600 hover:text-gray-300 focus:outline-none focus:ring-1 focus:ring-accent/40"
className="flex items-center justify-center rounded-full p-0.5 -m-0.5 text-fg-muted hover:text-fg-secondary focus:outline-none focus:ring-1 focus:ring-accent/40"
>
<Info className="w-4 h-4" />
</button>
@@ -236,27 +236,27 @@ function InfoPopover({ calculationKey, interp, valueDisplay, metricPhraseKey }:
<div
ref={popoverRef}
role="tooltip"
className="fixed z-50 p-3 bg-[#12121f] border border-[#2a2a4a] rounded-lg shadow-2xl text-[11px] text-gray-300 pointer-events-none"
className="fixed z-50 p-3 bg-[#12121f] border border-[#2a2a4a] rounded-lg shadow-2xl text-[11px] text-fg-secondary pointer-events-none"
style={{ left: coords.left, top: coords.top, width: POPOVER_W }}
>
<div className="flex items-baseline gap-2 mb-2 pb-2 border-b border-[#2a2a4a]">
<span className="text-base font-semibold text-gray-100 tabular-nums">
<span className="text-base font-semibold text-fg-primary tabular-nums">
{valueDisplay}
</span>
<span className="text-[10px] uppercase tracking-wider text-gray-500">
<span className="text-[10px] uppercase tracking-wider text-fg-muted">
{metricPhrase}
</span>
</div>
<p className="font-semibold text-gray-200 uppercase tracking-wider text-[9px] mb-1">
<p className="font-semibold text-fg-secondary uppercase tracking-wider text-[9px] mb-1">
{t("stats.tooltip.howCalc")}
</p>
<p className="text-gray-400 leading-snug mb-2.5">{t(calculationKey)}</p>
<p className="text-fg-secondary leading-snug mb-2.5">{t(calculationKey)}</p>
<p className="font-semibold text-gray-200 uppercase tracking-wider text-[9px] mb-1">
<p className="font-semibold text-fg-secondary uppercase tracking-wider text-[9px] mb-1">
{t("stats.tooltip.whatItMeans")}
</p>
<p className="text-gray-400 leading-snug">{valueMeans}</p>
<p className="text-fg-secondary leading-snug">{valueMeans}</p>
</div>
)}
</>
@@ -287,7 +287,7 @@ function StatCard({
return (
<div className="bg-surface-2 border border-border rounded-xl p-4 flex flex-col gap-3">
<div className="flex items-center justify-between gap-2">
<span className="text-[10px] font-semibold text-gray-500 uppercase tracking-wider leading-none">
<span className="text-[10px] font-semibold text-fg-muted uppercase tracking-wider leading-none">
{label}
</span>
<Icon className={`w-4 h-4 flex-shrink-0 ${accentClass}`} />
@@ -340,7 +340,7 @@ export function WorkflowStats({ stats }: WorkflowStatsProps) {
label={t("stats.avgSubagentsPerSession")}
value={stats.avgSubagents.toFixed(1)}
icon={Users}
accentClass="text-blue-400"
accentClass="text-blue-500"
calculationKey="stats.tooltip.calc.subagents"
interp={interpAvgSubagents(stats.avgSubagents)}
metricPhraseKey="stats.tooltip.phrase.subagents"
@@ -376,7 +376,7 @@ export function WorkflowStats({ stats }: WorkflowStatsProps) {
label={t("stats.avgDuration")}
value={formatDurationSec(stats.avgDurationSec)}
icon={Clock}
accentClass="text-amber-400"
accentClass="text-status-warning"
calculationKey="stats.tooltip.calc.duration"
interp={interpAvgDuration(stats.avgDurationSec)}
metricPhraseKey="stats.tooltip.phrase.duration"