feat(nexus): design system phase 3 raw utility sweep

Third phase of the DESIGN.md migration. Removes every raw Tailwind
color palette utility (bg-red-*, text-amber-*, border-blue-*, etc.)
from component source and replaces them with the semantic tokens
introduced in phases 1 and 2.

Scope:
  - 84 files touched under ui/src/
  - ~420 raw palette utility instances replaced
  - 23 hardcoded hex fallbacks replaced with var(--token) refs
  - Zero raw palette utilities remain in component source
    (verified with rg '(bg|text|border|ring)-(red|blue|green|amber|
    yellow|cyan|violet|purple|pink|slate|zinc|neutral|sky|teal|
    emerald|indigo|rose|orange|fuchsia)-[0-9]+' ui/src)

Mapping rules applied:
  - red-* -> destructive
  - amber-/yellow-/orange-* -> warning
  - green-/emerald-* -> success
  - blue-/cyan-/sky-* -> primary (info/in-progress) or muted-foreground
  - slate-/gray-/zinc-/neutral-* -> muted / muted-foreground / border
  - violet-/purple-/pink-/indigo-/rose-/teal-* -> collapsed to
    primary or muted (most were one-off decorative choices, not
    role-bearing). Role-bearing uses go through lib/agent-role-colors
    which was rewritten in phase 2.
  - Opacity modifiers preserved (/10, /15, /20, etc.)
  - dark: variant duplicates removed (theme tokens auto-switch)

Hardcoded hex fallbacks fixed:
  - #6366f1 (indigo) -> var(--primary) / var(--volt)
  - #64748b (slate) -> var(--muted-foreground) / var(--silver)
  - #4f46e5 (indigo) -> var(--primary)
  - #89b4fa (old Catppuccin blue) -> var(--primary) / #faff69
  - OrgChart status dots (#22d3ee/#4ade80/#facc15/#f87171/#a3a3a3)
    -> var(--primary) / var(--success) / var(--warning) /
    var(--destructive) / var(--muted-foreground) per status
  - VoiceWaveform fallback #89b4fa -> #faff69 (volt)

Legitimate hex values left untouched (12 total):
  - lib/color-contrast.ts WCAG reference constants
  - lib/worktree-branding.ts contrast fallback references
  - lib/mention-chips.ts runtime-generated SVG fills
  - context/ThemeContext.tsx theme metadata brand hexes
  - components/ThemeSeedInput.tsx user-facing hex picker

Ambiguous decisions (flagged for visual QA):
  - AgentDetail.tsx invocation-source badges (timer/assignment/
    on_demand) collapsed to primary/muted — visual distinction
    is reduced, labels still differ. Consider chart-role slots
    if differentiation matters.
  - AgentDetail.tsx mixed-opacity amber banners: bg-warning/60
    against new warning base reads heavier than original amber-50
    base.
  - Live-state dots in KanbanBoard/AgentDetail: bg-blue-* ->
    bg-primary — will glow volt in dark mode, probably desirable.

Verification:
  - npx tsc --noEmit in ui/ — zero errors introduced. Pre-existing
    errors in AgentConfigForm, command.tsx, useKeyboardShortcuts,
    usePiperTts, useVadRecorder, PersonalAssistant remain, all
    unrelated to color work.
  - Dev server on :6100 returns 200.

Not changed in this commit:
  - ui/src/lib/company-routes.ts — separate routing fix for broken
    Assistant/ContentStudio/Convert links, committed next.
  - Test files — a few will need assertion updates but are out of
    phase 3 scope.

Phase 4 follow-ups (rounded-xl/2xl collapse, soft shadow removal,
gradient removal) noted in .planning/AUDIT-RADIUS-SHADOWS.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nexus Dev 2026-04-10 17:40:32 +00:00
parent 4b8f8178ee
commit 3a41ec7b9c
84 changed files with 1049 additions and 913 deletions

View file

@ -118,13 +118,13 @@ export function HermesLocalConfigFields({
}
>
{showInstallCallout && (
<div className="mb-2 rounded-md border border-amber-500/30 bg-amber-500/5 p-3 text-sm">
<span className="text-amber-200">Ollama is not detected. </span>
<div className="mb-2 rounded-md border border-warning/30 bg-warning/5 p-3 text-sm">
<span className="text-warning">Ollama is not detected. </span>
<a
href={ollamaStatus.installUrl ?? "https://ollama.com/download"}
target="_blank"
rel="noopener noreferrer"
className="text-amber-400 underline hover:text-amber-300"
className="text-warning underline hover:text-warning"
>
Install Ollama
</a>

View file

@ -8,21 +8,21 @@ const SURFACES = [
description: "Request-scoped usage and billed runs from cost_events.",
icon: Database,
points: ["tokens + billed dollars", "provider, biller, model", "subscription and overage aware"],
tone: "from-sky-500/12 via-sky-500/6 to-transparent",
tone: "from-primary/12 via-primary/6 to-transparent",
},
{
title: "Finance ledger",
description: "Account-level charges that are not one prompt-response pair.",
icon: ReceiptText,
points: ["top-ups, refunds, fees", "Bedrock provisioned or training charges", "credit expiries and adjustments"],
tone: "from-amber-500/14 via-amber-500/6 to-transparent",
tone: "from-warning/14 via-warning/6 to-transparent",
},
{
title: "Live quotas",
description: "Provider or biller windows that can stop traffic in real time.",
icon: Gauge,
points: ["provider quota windows", "biller credit systems", "errors surfaced directly"],
tone: "from-emerald-500/14 via-emerald-500/6 to-transparent",
tone: "from-success/14 via-success/6 to-transparent",
},
] as const;

View file

@ -93,7 +93,7 @@ function AgentRunCard({
<div className={cn(
"flex h-[320px] flex-col overflow-hidden rounded-xl border shadow-sm",
isActive
? "border-cyan-500/25 bg-cyan-500/[0.04] shadow-[0_16px_40px_rgba(6,182,212,0.08)]"
? "border-primary/25 bg-primary/[0.04] shadow-[0_16px_40px_rgba(6,182,212,0.08)]"
: "border-border bg-background/70",
)}>
<div className="border-b border-border/60 px-3 py-3">
@ -102,8 +102,8 @@ function AgentRunCard({
<div className="flex items-center gap-2">
{isActive ? (
<span className="relative flex h-2.5 w-2.5 shrink-0">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-cyan-400 opacity-70" />
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-cyan-500" />
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-70" />
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-primary" />
</span>
) : (
<span className="inline-flex h-2.5 w-2.5 rounded-full bg-muted-foreground/35" />
@ -129,7 +129,7 @@ function AgentRunCard({
to={`/issues/${issue?.identifier ?? run.issueId}`}
className={cn(
"line-clamp-2 hover:underline",
isActive ? "text-cyan-700 dark:text-cyan-300" : "text-muted-foreground hover:text-foreground",
isActive ? "text-primary" : "text-muted-foreground hover:text-foreground",
)}
title={issue?.title ? `${issue?.identifier ?? run.issueId.slice(0, 8)} - ${issue.title}` : issue?.identifier ?? run.issueId.slice(0, 8)}
>

View file

@ -88,9 +88,9 @@ export function RunActivityChart({ runs }: { runs: HeartbeatRun[] }) {
<div key={day} className="flex-1 h-full flex flex-col justify-end" title={`${day}: ${total} runs`}>
{total > 0 ? (
<div className="flex flex-col-reverse gap-px overflow-hidden" style={{ height: `${heightPct}%`, minHeight: 2 }}>
{entry.succeeded > 0 && <div className="bg-emerald-500" style={{ flex: entry.succeeded }} />}
{entry.failed > 0 && <div className="bg-red-500" style={{ flex: entry.failed }} />}
{entry.other > 0 && <div className="bg-neutral-500" style={{ flex: entry.other }} />}
{entry.succeeded > 0 && <div className="bg-success" style={{ flex: entry.succeeded }} />}
{entry.failed > 0 && <div className="bg-destructive" style={{ flex: entry.failed }} />}
{entry.other > 0 && <div className="bg-muted" style={{ flex: entry.other }} />}
</div>
) : (
<div className="bg-muted/30 rounded-sm" style={{ height: 2 }} />
@ -105,10 +105,10 @@ export function RunActivityChart({ runs }: { runs: HeartbeatRun[] }) {
}
const priorityColors: Record<string, string> = {
critical: "#ef4444",
high: "#f97316",
medium: "#eab308",
low: "#6b7280",
critical: "var(--destructive)",
high: "var(--chart-4)",
medium: "var(--chart-1)",
low: "var(--muted-foreground)",
};
const priorityOrder = ["critical", "high", "medium", "low"] as const;
@ -158,13 +158,13 @@ export function PriorityChart({ issues }: { issues: { priority: string; createdA
}
const statusColors: Record<string, string> = {
todo: "#3b82f6",
in_progress: "#8b5cf6",
in_review: "#a855f7",
done: "#10b981",
blocked: "#ef4444",
cancelled: "#6b7280",
backlog: "#64748b",
todo: "var(--chart-1)",
in_progress: "var(--chart-3)",
in_review: "var(--chart-4)",
done: "var(--chart-2)",
blocked: "var(--destructive)",
cancelled: "var(--muted-foreground)",
backlog: "var(--muted-foreground)",
};
const statusLabels: Record<string, string> = {
@ -208,7 +208,7 @@ export function IssueStatusChart({ issues }: { issues: { status: string; created
{total > 0 ? (
<div className="flex flex-col-reverse gap-px overflow-hidden" style={{ height: `${heightPct}%`, minHeight: 2 }}>
{statusOrder.map(s => (entry[s] ?? 0) > 0 ? (
<div key={s} style={{ flex: entry[s], backgroundColor: statusColors[s] ?? "#6b7280" }} />
<div key={s} style={{ flex: entry[s], backgroundColor: statusColors[s] ?? "var(--muted-foreground)" }} />
) : null)}
</div>
) : (
@ -219,7 +219,7 @@ export function IssueStatusChart({ issues }: { issues: { status: string; created
})}
</div>
<DateLabels days={days} />
<ChartLegend items={statusOrder.map(s => ({ color: statusColors[s] ?? "#6b7280", label: statusLabels[s] ?? s }))} />
<ChartLegend items={statusOrder.map(s => ({ color: statusColors[s] ?? "var(--muted-foreground)", label: statusLabels[s] ?? s }))} />
</div>
);
}
@ -245,7 +245,7 @@ export function SuccessRateChart({ runs }: { runs: HeartbeatRun[] }) {
{days.map(day => {
const entry = grouped.get(day)!;
const rate = entry.total > 0 ? entry.succeeded / entry.total : 0;
const color = entry.total === 0 ? undefined : rate >= 0.8 ? "#10b981" : rate >= 0.5 ? "#eab308" : "#ef4444";
const color = entry.total === 0 ? undefined : rate >= 0.8 ? "var(--chart-2)" : rate >= 0.5 ? "var(--chart-4)" : "var(--destructive)";
return (
<div key={day} className="flex-1 h-full flex flex-col justify-end" title={`${day}: ${entry.total > 0 ? Math.round(rate * 100) : 0}% (${entry.succeeded}/${entry.total})`}>
{entry.total > 0 ? (

View file

@ -550,7 +550,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
}}
/>
</Field>
<div className="rounded-md border border-amber-500/25 bg-amber-500/10 px-3 py-2 text-xs text-amber-100">
<div className="rounded-md border border-warning/25 bg-warning/10 px-3 py-2 text-xs text-warning">
Prompt template is replayed on every heartbeat. Keep it compact and dynamic to avoid recurring token cost and cache churn.
</div>
</>
@ -687,7 +687,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
}}
/>
</Field>
<div className="rounded-md border border-amber-500/25 bg-amber-500/10 px-3 py-2 text-xs text-amber-100">
<div className="rounded-md border border-warning/25 bg-warning/10 px-3 py-2 text-xs text-warning">
Prompt template is replayed on every heartbeat. Prefer small task framing and variables like <code>{"{{ context.* }}"}</code> or <code>{"{{ run.* }}"}</code>; avoid repeating stable instructions here.
</div>
</>
@ -786,7 +786,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
{adapterType === "codex_local" &&
codexSearchEnabled &&
currentThinkingEffort === "minimal" && (
<p className="text-xs text-amber-400">
<p className="text-xs text-warning">
Codex may reject `minimal` thinking when search is enabled.
</p>
)}
@ -813,7 +813,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
}}
/>
</Field>
<div className="rounded-md border border-amber-500/25 bg-amber-500/10 px-3 py-2 text-xs text-amber-200">
<div className="rounded-md border border-warning/25 bg-warning/10 px-3 py-2 text-xs text-warning">
Bootstrap prompt is legacy and will be removed in a future release. Consider moving this content into the agent&apos;s prompt template or instructions file instead.
</div>
</>
@ -993,10 +993,10 @@ function AdapterEnvironmentResult({ result }: { result: AdapterEnvironmentTestRe
result.status === "pass" ? "Passed" : result.status === "warn" ? "Warnings" : "Failed";
const statusClass =
result.status === "pass"
? "text-green-700 dark:text-green-300 border-green-300 dark:border-green-500/40 bg-green-50 dark:bg-green-500/10"
? "text-success border-success/30 bg-success/10"
: result.status === "warn"
? "text-amber-700 dark:text-amber-300 border-amber-300 dark:border-amber-500/40 bg-amber-50 dark:bg-amber-500/10"
: "text-red-700 dark:text-red-300 border-red-300 dark:border-red-500/40 bg-red-50 dark:bg-red-500/10";
? "text-warning border-warning/30 bg-warning/10"
: "text-destructive border-destructive/30 bg-destructive/10";
return (
<div className={`rounded-md border px-3 py-2 text-xs ${statusClass}`}>
@ -1504,7 +1504,7 @@ function ModelDropdown({
<span className="block w-full text-left truncate font-mono text-xs" title={value}>
{value}
</span>
<span className="shrink-0 ml-auto text-[9px] font-medium px-1.5 py-0.5 rounded-full bg-green-500/15 text-green-400 border border-green-500/20">
<span className="shrink-0 ml-auto text-[9px] font-medium px-1.5 py-0.5 rounded-full bg-success/15 text-success border border-success/20">
current
</span>
</button>
@ -1523,7 +1523,7 @@ function ModelDropdown({
<span className="block w-full text-left truncate font-mono text-xs" title={detectedModel}>
{detectedModel}
</span>
<span className="shrink-0 ml-auto text-[9px] font-medium px-1.5 py-0.5 rounded-full bg-blue-500/15 text-blue-400 border border-blue-500/20">
<span className="shrink-0 ml-auto text-[9px] font-medium px-1.5 py-0.5 rounded-full bg-primary/15 text-primary border border-primary/20">
detected
</span>
</button>

View file

@ -78,7 +78,7 @@ export function AgentProperties({ agent, runtimeState }: AgentPropertiesProps) {
)}
{runtimeState?.lastError && (
<PropertyRow label="Last error">
<span className="text-xs text-red-600 dark:text-red-400 truncate max-w-[160px]">{runtimeState.lastError}</span>
<span className="text-xs text-destructive truncate max-w-[160px]">{runtimeState.lastError}</span>
</PropertyRow>
)}
{agent.lastHeartbeatAt && (

View file

@ -7,10 +7,10 @@ import { timeAgo } from "../lib/timeAgo";
import type { Approval, Agent } from "@paperclipai/shared";
function statusIcon(status: string) {
if (status === "approved") return <CheckCircle2 className="h-3.5 w-3.5 text-green-600 dark:text-green-400" />;
if (status === "rejected") return <XCircle className="h-3.5 w-3.5 text-red-600 dark:text-red-400" />;
if (status === "revision_requested") return <Clock className="h-3.5 w-3.5 text-amber-600 dark:text-amber-400" />;
if (status === "pending") return <Clock className="h-3.5 w-3.5 text-yellow-600 dark:text-yellow-400" />;
if (status === "approved") return <CheckCircle2 className="h-3.5 w-3.5 text-success" />;
if (status === "rejected") return <XCircle className="h-3.5 w-3.5 text-destructive" />;
if (status === "revision_requested") return <Clock className="h-3.5 w-3.5 text-warning" />;
if (status === "pending") return <Clock className="h-3.5 w-3.5 text-warning" />;
return null;
}
@ -74,7 +74,7 @@ export function ApprovalCard({
<div className="flex gap-2 mt-4 pt-3 border-t border-border">
<Button
size="sm"
className="bg-green-700 hover:bg-green-600 text-white"
className="bg-success hover:bg-success text-white"
onClick={onApprove}
disabled={isPending}
>

View file

@ -33,25 +33,25 @@ export function BudgetIncidentCard({
const parsed = parseDollarInput(draftAmount);
return (
<Card className="overflow-hidden border-red-500/20 bg-[linear-gradient(180deg,rgba(255,70,70,0.10),rgba(255,255,255,0.02))]">
<Card className="overflow-hidden border-destructive/20 bg-[linear-gradient(180deg,rgba(255,70,70,0.10),rgba(255,255,255,0.02))]">
<CardHeader className="px-5 pt-5 pb-3">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-[11px] uppercase tracking-[0.22em] text-red-200/80">
<div className="text-[11px] uppercase tracking-[0.22em] text-destructive">
{incident.scopeType} hard stop
</div>
<CardTitle className="mt-1 text-base text-red-50">{incident.scopeName}</CardTitle>
<CardDescription className="mt-1 text-red-100/70">
<CardTitle className="mt-1 text-base text-destructive">{incident.scopeName}</CardTitle>
<CardDescription className="mt-1 text-destructive">
Spending reached {formatCents(incident.amountObserved)} against a limit of {formatCents(incident.amountLimit)}.
</CardDescription>
</div>
<div className="rounded-full border border-red-400/30 bg-red-500/10 p-2 text-red-200">
<div className="rounded-full border border-destructive/30 bg-destructive/10 p-2 text-destructive">
<AlertOctagon className="h-4 w-4" />
</div>
</div>
</CardHeader>
<CardContent className="space-y-4 px-5 pb-5 pt-0">
<div className="flex items-start gap-2 rounded-xl border border-red-400/20 bg-red-500/10 px-3 py-2 text-sm text-red-50/90">
<div className="flex items-start gap-2 rounded-xl border border-destructive/20 bg-destructive/10 px-3 py-2 text-sm text-destructive">
<PauseCircle className="mt-0.5 h-4 w-4 shrink-0" />
<div>
{incident.scopeType === "project"
@ -83,7 +83,7 @@ export function BudgetIncidentCard({
</Button>
</div>
{parsed !== null && parsed <= incident.amountObserved ? (
<p className="mt-2 text-xs text-red-200/80">
<p className="mt-2 text-xs text-destructive">
The new budget must exceed current observed spend.
</p>
) : null}

View file

@ -23,9 +23,9 @@ function windowLabel(windowKind: BudgetPolicySummary["windowKind"]) {
}
function statusTone(status: BudgetPolicySummary["status"]) {
if (status === "hard_stop") return "text-red-300 border-red-500/30 bg-red-500/10";
if (status === "warning") return "text-amber-200 border-amber-500/30 bg-amber-500/10";
return "text-emerald-200 border-emerald-500/30 bg-emerald-500/10";
if (status === "hard_stop") return "text-destructive border-destructive/30 bg-destructive/10";
if (status === "warning") return "text-warning border-warning/30 bg-warning/10";
return "text-success border-success/30 bg-success/10";
}
export function BudgetPolicyCard({
@ -104,10 +104,10 @@ export function BudgetPolicyCard({
className={cn(
"h-full rounded-full transition-[width,background-color] duration-200",
summary.status === "hard_stop"
? "bg-red-400"
? "bg-destructive"
: summary.status === "warning"
? "bg-amber-300"
: "bg-emerald-300",
? "bg-warning/15"
: "bg-success/15",
)}
style={{ width: `${progress}%` }}
/>
@ -116,7 +116,7 @@ export function BudgetPolicyCard({
);
const pausedPane = summary.paused ? (
<div className="flex items-start gap-2 rounded-xl border border-red-500/30 bg-red-500/10 px-3 py-2 text-sm text-red-100">
<div className="flex items-start gap-2 rounded-xl border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
<PauseCircle className="mt-0.5 h-4 w-4 shrink-0" />
<div>
{summary.scopeType === "project"
@ -166,9 +166,9 @@ export function BudgetPolicyCard({
className={cn(
"inline-flex items-center gap-2 text-[11px] uppercase tracking-[0.18em]",
summary.status === "hard_stop"
? "text-red-300"
? "text-destructive"
: summary.status === "warning"
? "text-amber-200"
? "text-warning"
: "text-muted-foreground",
)}
>

View file

@ -5,7 +5,7 @@ export function BudgetSidebarMarker({ title = "Paused by budget" }: { title?: st
<span
title={title}
aria-label={title}
className="ml-auto inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-red-500/90 text-white shadow-[0_0_0_1px_rgba(255,255,255,0.08)]"
className="ml-auto inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-destructive/90 text-white shadow-[0_0_0_1px_rgba(255,255,255,0.08)]"
>
<DollarSign className="h-3 w-3" />
</span>

View file

@ -26,7 +26,7 @@ export function ChatMessageIdentityBar({
<AgentIcon icon={agentIcon} className={`h-4 w-4 ${colorClass}`} />
<span className={`text-[13px] font-semibold ${colorClass}`}>{agentName}</span>
{isStreaming && (
<span className="h-1.5 w-1.5 rounded-full bg-cyan-400 animate-pulse" />
<span className="h-1.5 w-1.5 rounded-full bg-primary animate-pulse" />
)}
{timestamp && (
<span className="text-[11px] text-muted-foreground">

View file

@ -48,7 +48,7 @@ function HighlightedText({ text, query }: { text: string; query: string }) {
<>
{segments.map((seg, i) =>
seg.highlight ? (
<mark key={i} className="bg-yellow-200 dark:bg-yellow-800 rounded-sm">
<mark key={i} className="bg-warning/15 rounded-sm">
{seg.text}
</mark>
) : (

View file

@ -21,7 +21,7 @@ export function ChatStatusUpdateBadge({ agentName, taskId, taskTitle, taskUrl }:
)}
role="status"
>
<CheckCircle2 className="h-3.5 w-3.5 text-green-500 dark:text-green-400" />
<CheckCircle2 className="h-3.5 w-3.5 text-success" />
<span className="text-foreground">
{agentName} completed {taskId}
{displayTitle ? `: ${displayTitle}` : ""}

View file

@ -45,9 +45,9 @@ function orderedWindows(windows: QuotaWindow[]): QuotaWindow[] {
}
function fillClass(usedPercent: number | null): string {
if (usedPercent == null) return "bg-zinc-700";
if (usedPercent >= 90) return "bg-red-400";
if (usedPercent >= 70) return "bg-amber-400";
if (usedPercent == null) return "bg-muted";
if (usedPercent >= 90) return "bg-destructive";
if (usedPercent >= 70) return "bg-warning";
return "bg-primary/70";
}

View file

@ -41,9 +41,9 @@ function detailText(window: QuotaWindow): string | null {
}
function fillClass(usedPercent: number | null): string {
if (usedPercent == null) return "bg-zinc-700";
if (usedPercent >= 90) return "bg-red-400";
if (usedPercent >= 70) return "bg-amber-400";
if (usedPercent == null) return "bg-muted";
if (usedPercent >= 90) return "bg-destructive";
if (usedPercent >= 70) return "bg-warning";
return "bg-primary/70";
}

View file

@ -183,17 +183,17 @@ function runTimestamp(run: LinkedRunItem) {
function runStatusClass(status: string) {
switch (status) {
case "succeeded":
return "text-green-700 dark:text-green-300";
return "text-success";
case "failed":
case "error":
return "text-red-700 dark:text-red-300";
return "text-destructive";
case "timed_out":
return "text-orange-700 dark:text-orange-300";
return "text-warning";
case "running":
return "text-cyan-700 dark:text-cyan-300";
return "text-primary";
case "queued":
case "pending":
return "text-amber-700 dark:text-amber-300";
return "text-warning";
case "cancelled":
return "text-muted-foreground";
default:
@ -258,7 +258,7 @@ function CommentCard({
id={`comment-${comment.id}`}
className={`border p-3 overflow-hidden min-w-0 rounded-sm transition-colors duration-1000 ${
isQueued
? "border-amber-300/70 bg-amber-50/70 dark:border-amber-500/40 dark:bg-amber-500/10"
? "border-warning/70 bg-warning/70"
: isHighlighted
? "border-primary/50 bg-primary/5"
: "border-border"
@ -277,7 +277,7 @@ function CommentCard({
)}
<span className="flex items-center gap-1.5">
{isQueued ? (
<span className="inline-flex items-center rounded-full border border-amber-400/60 bg-amber-100/70 px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.14em] text-amber-800 dark:border-amber-400/40 dark:bg-amber-500/20 dark:text-amber-200">
<span className="inline-flex items-center rounded-full border border-warning/60 bg-warning/70 px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.14em] text-warning">
Queued
</span>
) : null}
@ -765,14 +765,14 @@ export function CommentThread({
{queuedComments.length > 0 && (
<div className="space-y-3">
<div className="flex items-center justify-between gap-2">
<h4 className="text-xs font-semibold uppercase tracking-[0.14em] text-amber-700 dark:text-amber-300">
<h4 className="text-xs font-semibold uppercase tracking-[0.14em] text-warning">
Queued Comments ({queuedComments.length})
</h4>
{onInterruptQueued && queuedComments[0]?.queueTargetRunId ? (
<Button
size="sm"
variant="outline"
className="border-red-300 text-red-700 hover:bg-red-50 hover:text-red-800 dark:border-red-500/40 dark:text-red-300 dark:hover:bg-red-500/10"
className="border-destructive/30 text-destructive hover:bg-destructive/10 hover:text-destructive hover:bg-destructive/10"
disabled={interruptingQueuedRunId === queuedComments[0].queueTargetRunId}
onClick={() => void onInterruptQueued(queuedComments[0]!.queueTargetRunId!)}
>

View file

@ -134,13 +134,13 @@ function SortableCompanyItem({
{hasLiveAgents && (
<span className="pointer-events-none absolute -right-0.5 -top-0.5 z-10">
<span className="relative flex h-2.5 w-2.5">
<span className="absolute inline-flex h-full w-full animate-pulse rounded-full bg-blue-400 opacity-80" />
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-blue-500 ring-2 ring-background" />
<span className="absolute inline-flex h-full w-full animate-pulse rounded-full bg-primary opacity-80" />
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-primary ring-2 ring-background" />
</span>
</span>
)}
{hasUnreadInbox && (
<span className="pointer-events-none absolute -bottom-0.5 -right-0.5 z-10 h-2.5 w-2.5 rounded-full bg-red-500 ring-2 ring-background" />
<span className="pointer-events-none absolute -bottom-0.5 -right-0.5 z-10 h-2.5 w-2.5 rounded-full bg-destructive ring-2 ring-background" />
)}
</div>
</a>

View file

@ -15,13 +15,13 @@ import { Button } from "@/components/ui/button";
function statusDotColor(status?: string): string {
switch (status) {
case "active":
return "bg-green-400";
return "bg-success";
case "paused":
return "bg-yellow-400";
return "bg-warning";
case "archived":
return "bg-neutral-400";
return "bg-muted";
default:
return "bg-green-400";
return "bg-success";
}
}

View file

@ -33,14 +33,14 @@ export function DevRestartBanner({ devServer }: { devServer?: DevServerHealthSta
const sample = devServer.changedPathsSample.slice(0, 3);
return (
<div className="border-b border-amber-300/60 bg-amber-50 text-amber-950 dark:border-amber-500/25 dark:bg-amber-500/10 dark:text-amber-100">
<div className="border-b border-warning/60 bg-warning/10 text-warning">
<div className="flex flex-col gap-3 px-3 py-2.5 md:flex-row md:items-center md:justify-between">
<div className="min-w-0">
<div className="flex items-center gap-2 text-[12px] font-semibold uppercase tracking-[0.18em]">
<AlertTriangle className="h-3.5 w-3.5 shrink-0" />
<span>Restart Required</span>
{devServer.autoRestartEnabled ? (
<span className="rounded-full bg-amber-900/10 px-2 py-0.5 text-[10px] tracking-[0.14em] dark:bg-amber-100/10">
<span className="rounded-full bg-warning/10 px-2 py-0.5 text-[10px] tracking-[0.14em]">
Auto-Restart On
</span>
) : null}
@ -49,7 +49,7 @@ export function DevRestartBanner({ devServer }: { devServer?: DevServerHealthSta
{describeReason(devServer)}
{changedAt ? ` · updated ${changedAt}` : ""}
</p>
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-amber-900/80 dark:text-amber-100/75">
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-warning">
{sample.length > 0 ? (
<span>
Changed: {sample.join(", ")}
@ -67,17 +67,17 @@ export function DevRestartBanner({ devServer }: { devServer?: DevServerHealthSta
<div className="flex shrink-0 items-center gap-2 text-xs font-medium">
{devServer.waitingForIdle ? (
<div className="inline-flex items-center gap-2 rounded-full bg-amber-900/10 px-3 py-1.5 dark:bg-amber-100/10">
<div className="inline-flex items-center gap-2 rounded-full bg-warning/10 px-3 py-1.5">
<TimerReset className="h-3.5 w-3.5" />
<span>Waiting for {devServer.activeRunCount} live run{devServer.activeRunCount === 1 ? "" : "s"} to finish</span>
</div>
) : devServer.autoRestartEnabled ? (
<div className="inline-flex items-center gap-2 rounded-full bg-amber-900/10 px-3 py-1.5 dark:bg-amber-100/10">
<div className="inline-flex items-center gap-2 rounded-full bg-warning/10 px-3 py-1.5">
<RotateCcw className="h-3.5 w-3.5" />
<span>Auto-restart will trigger when the instance is idle</span>
</div>
) : (
<div className="inline-flex items-center gap-2 rounded-full bg-amber-900/10 px-3 py-1.5 dark:bg-amber-100/10">
<div className="inline-flex items-center gap-2 rounded-full bg-warning/10 px-3 py-1.5">
<RotateCcw className="h-3.5 w-3.5" />
<span>Restart <code>pnpm dev:once</code> after the active work is safe to interrupt</span>
</div>

View file

@ -30,9 +30,9 @@ function readinessTone(state: "ready" | "ready_with_warnings" | "blocked") {
return "border-destructive/30 bg-destructive/5 text-destructive";
}
if (state === "ready_with_warnings") {
return "border-amber-500/30 bg-amber-500/10 text-amber-800 dark:text-amber-300";
return "border-warning/30 bg-warning/10 text-warning";
}
return "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300";
return "border-success/30 bg-success/10 text-success";
}
export function ExecutionWorkspaceCloseDialog({
@ -163,7 +163,7 @@ export function ExecutionWorkspaceCloseDialog({
<h3 className="text-sm font-medium">Warnings</h3>
<ul className="space-y-2 text-sm text-muted-foreground">
{readiness.warnings.map((warning, idx) => (
<li key={`warning-${idx}`} className="break-words rounded-lg border border-amber-500/20 bg-amber-500/5 px-3 py-2">
<li key={`warning-${idx}`} className="break-words rounded-lg border border-warning/20 bg-warning/5 px-3 py-2">
{warning}
</li>
))}
@ -262,7 +262,7 @@ export function ExecutionWorkspaceCloseDialog({
</section>
{currentStatus === "cleanup_failed" ? (
<div className="rounded-xl border border-amber-500/20 bg-amber-500/5 px-4 py-3 text-sm text-muted-foreground">
<div className="rounded-xl border border-warning/20 bg-warning/5 px-4 py-3 text-sm text-muted-foreground">
Cleanup previously failed on this workspace. Retrying close will rerun the cleanup flow and update the
workspace status if it succeeds.
</div>

View file

@ -59,7 +59,7 @@ export function FinanceTimelineCard({
<div className="text-right tabular-nums">
<div className="text-sm font-semibold">{formatCents(row.amountCents)}</div>
<div className="text-xs text-muted-foreground">{row.currency}</div>
{row.estimated ? <div className="text-[11px] uppercase tracking-[0.12em] text-amber-600">estimated</div> : null}
{row.estimated ? <div className="text-[11px] uppercase tracking-[0.12em] text-warning">estimated</div> : null}
</div>
</div>
</div>

View file

@ -764,13 +764,13 @@ export function IssueDocumentsSection({
<div
id="document-plan"
className={cn(
"rounded-lg border border-amber-500/30 bg-amber-500/5 p-3 transition-colors duration-1000",
"rounded-lg border border-warning/30 bg-warning/5 p-3 transition-colors duration-1000",
highlightDocumentKey === "plan" && "border-primary/50 bg-primary/5",
)}
>
<div className="mb-2 flex items-center gap-2">
<FileText className="h-4 w-4 text-amber-600" />
<span className="rounded-full border border-amber-500/30 px-2 py-0.5 font-mono text-[10px] uppercase tracking-[0.16em] text-amber-700 dark:text-amber-300">
<FileText className="h-4 w-4 text-warning" />
<span className="rounded-full border border-warning/30 px-2 py-0.5 font-mono text-[10px] uppercase tracking-[0.16em] text-warning">
PLAN
</span>
</div>
@ -834,7 +834,7 @@ export function IssueDocumentsSection({
size="sm"
className={cn(
"h-auto px-1.5 py-0 text-[11px] font-normal text-muted-foreground hover:text-foreground",
isHistoricalPreview && "text-amber-300 hover:text-amber-200",
isHistoricalPreview && "text-warning hover:text-warning",
)}
>
rev {displayedRevisionNumber}
@ -963,10 +963,10 @@ export function IssueDocumentsSection({
: undefined}
>
{isHistoricalPreview && selectedHistoricalRevision && (
<div className="rounded-md border border-amber-500/30 bg-amber-500/5 px-3 py-3">
<div className="rounded-md border border-warning/30 bg-warning/5 px-3 py-3">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-1">
<p className="text-sm font-medium text-amber-200">
<p className="text-sm font-medium text-warning">
Viewing revision {selectedHistoricalRevision.revisionNumber}
</p>
<p className="text-xs text-muted-foreground">
@ -998,10 +998,10 @@ export function IssueDocumentsSection({
</div>
)}
{activeConflict && !isHistoricalPreview && (
<div className="rounded-md border border-amber-500/30 bg-amber-500/5 px-3 py-3">
<div className="rounded-md border border-warning/30 bg-warning/5 px-3 py-3">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="space-y-1">
<p className="text-sm font-medium text-amber-200">Out of date</p>
<p className="text-sm font-medium text-warning">Out of date</p>
<p className="text-xs text-muted-foreground">
This document changed while you were editing. Your local draft is preserved and autosave is paused.
</p>
@ -1074,7 +1074,7 @@ export function IssueDocumentsSection({
}`}
>
{isHistoricalPreview ? (
<div className="rounded-md border border-amber-500/20 bg-background/50 p-3">
<div className="rounded-md border border-warning/20 bg-background/50 p-3">
{renderBody(displayedBody, documentBodyContentClassName)}
</div>
) : activeDraft ? (
@ -1107,9 +1107,9 @@ export function IssueDocumentsSection({
<span
className={`text-[11px] transition-opacity duration-150 ${
isHistoricalPreview
? "text-amber-300"
? "text-warning"
: activeConflict
? "text-amber-300"
? "text-warning"
: autosaveState === "error"
? "text-destructive"
: "text-muted-foreground"

View file

@ -128,7 +128,7 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
const [labelsOpen, setLabelsOpen] = useState(false);
const [labelSearch, setLabelSearch] = useState("");
const [newLabelName, setNewLabelName] = useState("");
const [newLabelColor, setNewLabelColor] = useState("#6366f1");
const [newLabelColor, setNewLabelColor] = useState("var(--primary)");
const { data: session } = useQuery({
queryKey: queryKeys.auth.session,
@ -411,7 +411,7 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
<>
<span
className="shrink-0 h-3 w-3 rounded-sm"
style={{ backgroundColor: orderedProjects.find((p) => p.id === issue.projectId)?.color ?? "#6366f1" }}
style={{ backgroundColor: orderedProjects.find((p) => p.id === issue.projectId)?.color ?? "var(--primary)" }}
/>
<span className="text-sm truncate">{projectName(issue.projectId)}</span>
</>
@ -479,7 +479,7 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
>
<span
className="shrink-0 h-3 w-3 rounded-sm"
style={{ backgroundColor: p.color ?? "#6366f1" }}
style={{ backgroundColor: p.color ?? "var(--primary)" }}
/>
{p.name}
</button>

View file

@ -116,14 +116,14 @@ export function IssueRow({
}}
className={cn(
"inline-flex h-4 w-4 items-center justify-center rounded-full transition-colors",
selected ? "hover:bg-muted/80" : "hover:bg-blue-500/20",
selected ? "hover:bg-muted/80" : "hover:bg-primary/20",
)}
aria-label="Mark as read"
>
<span
className={cn(
"block h-2 w-2 rounded-full transition-opacity duration-300",
selected ? "bg-muted-foreground/70" : "bg-blue-600 dark:bg-blue-400",
selected ? "bg-muted-foreground/70" : "bg-primary",
unreadState === "fading" ? "opacity-0" : "opacity-100",
)}
/>

View file

@ -87,7 +87,7 @@ function CopyableInline({ value, label, mono }: { value: string; label?: string;
onClick={handleCopy}
title={copied ? "Copied!" : "Copy"}
>
{copied ? <Check className="h-3 w-3 text-green-500" /> : <Copy className="h-3 w-3" />}
{copied ? <Check className="h-3 w-3 text-success" /> : <Copy className="h-3 w-3" />}
</button>
</span>
);
@ -144,9 +144,9 @@ function workspaceDetailLink(input: {
function statusBadge(status: string) {
const colors: Record<string, string> = {
active: "bg-green-500/15 text-green-700 dark:text-green-400",
active: "bg-success/15 text-success",
idle: "bg-muted text-muted-foreground",
in_review: "bg-blue-500/15 text-blue-700 dark:text-blue-400",
in_review: "bg-primary/15 text-primary",
archived: "bg-muted text-muted-foreground",
};
return (

View file

@ -400,7 +400,7 @@ export function IssuesList({
{/* Filter */}
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="sm" className={`text-xs ${activeFilterCount > 0 ? "text-blue-600 dark:text-blue-400" : ""}`}>
<Button variant="ghost" size="sm" className={`text-xs ${activeFilterCount > 0 ? "text-primary" : ""}`}>
<Filter className="h-3.5 w-3.5 sm:h-3 sm:w-3 sm:mr-1" />
<span className="hidden sm:inline">{activeFilterCount > 0 ? `Filters: ${activeFilterCount}` : "Filter"}</span>
{activeFilterCount > 0 && (
@ -734,12 +734,12 @@ export function IssuesList({
{issue.identifier ?? issue.id.slice(0, 8)}
</span>
{liveIssueIds?.has(issue.id) && (
<span className="inline-flex items-center gap-1 rounded-full bg-blue-500/10 px-1.5 py-0.5 sm:gap-1.5 sm:px-2">
<span className="inline-flex items-center gap-1 rounded-full bg-primary/10 px-1.5 py-0.5 sm:gap-1.5 sm:px-2">
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-pulse rounded-full bg-blue-400 opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-blue-500" />
<span className="absolute inline-flex h-full w-full animate-pulse rounded-full bg-primary opacity-75" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-primary" />
</span>
<span className="hidden text-[11px] font-medium text-blue-600 dark:text-blue-400 sm:inline">
<span className="hidden text-[11px] font-medium text-primary sm:inline">
Live
</span>
</span>

View file

@ -154,8 +154,8 @@ function KanbanCard({
</span>
{isLive && (
<span className="relative flex h-2 w-2 shrink-0 mt-0.5">
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500" />
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
</span>
)}
</div>

View file

@ -87,9 +87,9 @@ export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
if (runs.length === 0) return null;
return (
<div className="overflow-hidden rounded-xl border border-cyan-500/25 bg-background/80 shadow-[0_18px_50px_rgba(6,182,212,0.08)]">
<div className="border-b border-border/60 bg-cyan-500/[0.04] px-4 py-3">
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-cyan-700 dark:text-cyan-300">
<div className="overflow-hidden rounded-xl border border-primary/25 bg-background/80 shadow-[0_18px_50px_rgba(6,182,212,0.08)]">
<div className="border-b border-border/60 bg-primary/[0.04] px-4 py-3">
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-primary">
Live Runs
</div>
<div className="mt-1 text-xs text-muted-foreground">
@ -111,7 +111,7 @@ export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<Link
to={`/agents/${run.agentId}/runs/${run.id}`}
className="inline-flex items-center rounded-full border border-border/70 bg-background/70 px-2 py-1 font-mono hover:border-cyan-500/30 hover:text-foreground"
className="inline-flex items-center rounded-full border border-border/70 bg-background/70 px-2 py-1 font-mono hover:border-primary/30 hover:text-foreground"
>
{run.id.slice(0, 8)}
</Link>
@ -125,7 +125,7 @@ export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
<button
onClick={() => handleCancelRun(run.id)}
disabled={cancellingRunIds.has(run.id)}
className="inline-flex items-center gap-1 rounded-full border border-red-500/20 bg-red-500/[0.06] px-2.5 py-1 text-[11px] font-medium text-red-700 transition-colors hover:bg-red-500/[0.12] dark:text-red-300 disabled:opacity-50"
className="inline-flex items-center gap-1 rounded-full border border-destructive/20 bg-destructive/[0.06] px-2.5 py-1 text-[11px] font-medium text-destructive transition-colors hover:bg-destructive/[0.12] disabled:opacity-50"
>
<Square className="h-2.5 w-2.5" fill="currentColor" />
{cancellingRunIds.has(run.id) ? "Stopping…" : "Stop"}
@ -133,7 +133,7 @@ export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
)}
<Link
to={`/agents/${run.agentId}/runs/${run.id}`}
className="inline-flex items-center gap-1 rounded-full border border-border/70 bg-background/70 px-2.5 py-1 text-[11px] font-medium text-cyan-700 transition-colors hover:border-cyan-500/30 hover:text-cyan-600 dark:text-cyan-300"
className="inline-flex items-center gap-1 rounded-full border border-border/70 bg-background/70 px-2.5 py-1 text-[11px] font-medium text-primary transition-colors hover:border-primary/30 hover:text-primary"
>
Open run
<ExternalLink className="h-3 w-3" />

View file

@ -663,7 +663,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
{option.kind === "project" && option.projectId ? (
<span
className="inline-flex h-2 w-2 rounded-full border border-border/50"
style={{ backgroundColor: option.projectColor ?? "#64748b" }}
style={{ backgroundColor: option.projectColor ?? "var(--muted-foreground)" }}
/>
) : (
<AgentIcon

View file

@ -233,7 +233,7 @@ export function NewAgentDialog() {
onClick={() => handleAdvancedAdapterPick(opt.value)}
>
{opt.recommended && (
<span className="absolute -top-1.5 right-1.5 bg-green-500 text-white text-[9px] font-semibold px-1.5 py-0.5 rounded-full leading-none">
<span className="absolute -top-1.5 right-1.5 bg-success text-white text-[9px] font-semibold px-1.5 py-0.5 rounded-full leading-none">
Recommended
</span>
)}

View file

@ -1092,7 +1092,7 @@ export function NewIssueDialog() {
<>
<span
className="h-3.5 w-3.5 shrink-0 rounded-sm"
style={{ backgroundColor: currentProject.color ?? "#6366f1" }}
style={{ backgroundColor: currentProject.color ?? "var(--primary)" }}
/>
<span className="truncate">{option.label}</span>
</>
@ -1107,7 +1107,7 @@ export function NewIssueDialog() {
<>
<span
className="h-3.5 w-3.5 shrink-0 rounded-sm"
style={{ backgroundColor: project?.color ?? "#6366f1" }}
style={{ backgroundColor: project?.color ?? "var(--primary)" }}
/>
<span className="truncate">{option.label}</span>
</>
@ -1212,7 +1212,7 @@ export function NewIssueDialog() {
data-slot="toggle"
className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors",
assigneeChrome ? "bg-green-600" : "bg-muted"
assigneeChrome ? "bg-success" : "bg-muted"
)}
onClick={() => setAssigneeChrome((value) => !value)}
>

View file

@ -1,4 +1,4 @@
// [nexus] Replacement onboarding wizard — 5-step flow: hardware detection, mode selection, provider selection, root directory, summary
// [nexus] Replacement onboarding wizard — 6-step full-page flow: hardware detection, mode selection, provider selection, voice, phone access, summary
// Exports `OnboardingWizard` to match the named import in App.tsx.
// Wired via Vite alias: all imports of ./components/OnboardingWizard are
// redirected here at build time; the original file is preserved for upstream rebase.
@ -14,7 +14,6 @@ import { agentsApi } from "../api/agents";
import { puterProxyApi } from "../api/puter-proxy";
import { queryKeys } from "../lib/queryKeys";
import { resolveRouteOnboardingOptions } from "../lib/onboarding-route";
import { Dialog, DialogPortal } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { cn } from "../lib/utils";
@ -27,19 +26,54 @@ import { TelegramStep } from "./onboarding/TelegramStep";
import { useHardwareInfo } from "../hooks/useHardwareInfo";
import { updateNexusSettings, type NexusMode } from "../api/hardware";
import { useChatPanel } from "../context/ChatPanelContext";
import {
Cpu,
LayoutGrid,
Cloud,
Mic,
Smartphone,
CheckCircle,
Check,
} from "lucide-react";
import type { LucideIcon } from "lucide-react";
const ADAPTER_LABELS: Record<string, string> = {
claude_local: "Claude Code (detected)",
hermes_local: "Hermes (detected)",
openclaw_gateway: "OpenClaw Gateway (detected)",
};
function deriveProviderLabel(
puterToken: string | null,
googleOAuthStateId: string | null,
apiKeyData: { provider: string; apiKey: string } | null,
selectedAdapterChoice: string | null,
): string {
if (selectedAdapterChoice) return ADAPTER_LABELS[selectedAdapterChoice] ?? selectedAdapterChoice;
if (puterToken) return "Puter (free, zero-config)";
if (googleOAuthStateId) return "Google Gemini (free tier)";
if (apiKeyData) return `API key — ${apiKeyData.provider}`;
return "None selected";
}
// [nexus] 7-step onboarding wizard: hardware detection → mode selection → provider selection → voice → telegram → root directory → summary
interface StepDef {
id: number;
name: string;
icon: LucideIcon;
title: string;
description: string;
}
const STEPS: StepDef[] = [
{ id: 1, name: "Hardware", icon: Cpu, title: "Your hardware", description: "Detecting what your machine can do." },
{ id: 2, name: "Mode", icon: LayoutGrid, title: "Choose your mode", description: "How do you want to use Nexus?" },
{ id: 3, name: "Provider", icon: Cloud, title: "Choose a provider", description: "No API keys needed for the zero-config path." },
{ id: 4, name: "Voice", icon: Mic, title: "Voice features", description: "Speak to your assistant and hear responses read aloud. Runs entirely on your device." },
{ id: 5, name: "Phone Access", icon: Smartphone, title: "Phone Access", description: "Get notifications and send quick replies from your phone." },
{ id: 6, name: "Summary", icon: CheckCircle, title: "Ready to go", description: "Review your setup before starting." },
];
// [nexus] 6-step onboarding wizard: hardware detection -> mode selection -> provider selection -> voice -> phone access -> summary
export function OnboardingWizard() {
const { onboardingOpen, onboardingOptions, closeOnboarding } = useDialog();
const { companies, setSelectedCompanyId, loading: companiesLoading } = useCompany();
@ -67,7 +101,7 @@ export function OnboardingWizard() {
setRouteDismissed(false);
}, [location.pathname]);
// Step state: 1 = hardware detection, 2 = mode selection, 3 = provider selection, 4 = voice, 5 = telegram, 6 = root directory, 7 = summary
// Step state: 1 = hardware, 2 = mode, 3 = provider, 4 = voice, 5 = phone access, 6 = summary
const [step, setStep] = useState(1);
// Mode state: "both" pre-selected per UI-SPEC
@ -80,6 +114,7 @@ export function OnboardingWizard() {
const [puterToken, setPuterToken] = useState<string | null>(null);
const [googleOAuthStateId, setGoogleOAuthStateId] = useState<string | null>(null);
const [apiKeyData, setApiKeyData] = useState<{ provider: string; apiKey: string } | null>(null);
const [selectedAdapterChoice, setSelectedAdapterChoice] = useState<string | null>(null);
// Form state
const [rootDir, setRootDir] = useState("");
@ -131,12 +166,7 @@ export function OnboardingWizard() {
}).catch(() => {}).finally(() => setProbing(false));
}, [effectiveOnboardingOpen]);
function handleClose() {
setRouteDismissed(true);
closeOnboarding();
}
// [nexus] Shared workspace creation logic used by both handleSubmit (step 4 direct) and handleStartChat (step 6)
// [nexus] Shared workspace creation logic used by both handleSubmit and handleStartChat
async function createWorkspace() {
// Step 1: Create workspace (company) named after VOCAB.appName
const company = await companiesApi.create({ name: VOCAB.appName });
@ -263,7 +293,7 @@ export function OnboardingWizard() {
async function handleStartChat() {
// Guard: claude_local requires rootDir
if (defaultAdapter === "claude_local" && !rootDir.trim()) {
setError("Root directory is required for Claude Code. Go back to step 6 to set it.");
setError("Root directory is required for Claude Code. Go back to step 5 to set it.");
return;
}
@ -283,265 +313,182 @@ export function OnboardingWizard() {
}
}
// Current step definition
const currentStep = STEPS.find(s => s.id === step) ?? STEPS[0];
// Navigation helpers
function goBack() {
if (step > 1) setStep(step - 1);
}
function goNext() {
if (step < 6) setStep(step + 1);
}
// Render step content
function renderStepContent() {
switch (step) {
case 1:
return (
<HardwareSummaryStep
hardwareInfo={hardwareInfo}
isLoading={hwLoading}
isError={hwError}
/>
);
case 2:
return <ModeSelector value={selectedMode} onChange={setSelectedMode} />;
case 3:
return (
<ProviderSelectionStep
onPuterToken={(token) => { setPuterToken(token); setSelectedAdapterChoice(null); }}
onGoogleOAuthState={(id) => { setGoogleOAuthStateId(id); setSelectedAdapterChoice(null); }}
onApiKey={(provider, apiKey) => { setApiKeyData({ provider, apiKey }); setSelectedAdapterChoice(null); }}
onAdapterSelected={(adapter) => { setSelectedAdapterChoice(adapter); setPuterToken(null); setGoogleOAuthStateId(null); setApiKeyData(null); }}
onSkip={goNext}
onContinue={goNext}
detectedAdapters={detectedAdapters}
probing={probing}
/>
);
case 4:
return (
<VoiceStep
onEnable={() => {
setVoiceEnabled(true);
goNext();
}}
onSkip={goNext}
voiceCapability={hardwareInfo?.voiceCapability}
/>
);
case 5:
return (
<TelegramStep
onNext={goNext}
onBack={goBack}
/>
);
case 6:
return (
<OnboardingSummaryStep
hardwareInfo={hardwareInfo}
selectedMode={selectedMode}
providerLabel={deriveProviderLabel(puterToken, googleOAuthStateId, apiKeyData, selectedAdapterChoice)}
rootDir={rootDir}
onRootDirChange={setRootDir}
loading={loading}
error={error}
onStartChat={handleStartChat}
onBack={goBack}
voiceEnabled={voiceEnabled}
defaultAdapter={defaultAdapter}
/>
);
default:
return null;
}
}
// Steps 3, 4, 5 have their own navigation — don't render the shared footer
const stepHasOwnNav = step === 3 || step === 4 || step === 5 || step === 6;
if (!effectiveOnboardingOpen) return null;
return (
<DialogPortal>
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
onClick={handleClose}
/>
<div className="fixed inset-0 z-50 flex bg-background">
{/* Left sidebar - step navigation (hidden on mobile) */}
<aside className="hidden md:flex w-[260px] flex-col border-r border-border bg-card p-6">
{/* Logo/title at top */}
<div className="mb-8">
<h1 className="text-lg font-bold">Nexus Setup</h1>
<p className="text-xs text-muted-foreground mt-1">Configure your workspace</p>
</div>
{/* Card */}
<div
className={cn(
"relative z-10 w-full max-w-md mx-4 rounded-xl border bg-card text-card-foreground shadow-2xl",
"p-8 flex flex-col gap-6"
)}
>
{/* Step indicator */}
<p className="text-xs text-muted-foreground text-center">
{step === 7 ? "Summary" : `Step ${step} of 6`}
</p>
{/* Step list */}
<nav className="flex flex-col gap-1">
{STEPS.map((s) => {
const isCompleted = s.id < step;
const isCurrent = s.id === step;
const isUpcoming = s.id > step;
const Icon = s.icon;
{/* Step 1 — Hardware Detection */}
{step === 1 && (
<>
<div className="flex flex-col gap-2 text-center">
<h1 className="text-2xl font-semibold tracking-tight">
{hwLoading ? "Detecting your hardware..." : "Your hardware"}
</h1>
</div>
<HardwareSummaryStep
hardwareInfo={hardwareInfo}
isLoading={hwLoading}
isError={hwError}
/>
<div className="flex flex-col gap-2">
<Button
type="button"
onClick={() => setStep(2)}
className="w-full"
>
Continue
</Button>
<Button
type="button"
variant="ghost"
onClick={() => setStep(2)}
className="w-full"
>
Skip
</Button>
</div>
</>
)}
{/* Step 2 — Mode Selection */}
{step === 2 && (
<>
<div className="flex flex-col gap-2 text-center">
<h1 className="text-2xl font-semibold tracking-tight">
Choose your mode
</h1>
</div>
<ModeSelector value={selectedMode} onChange={setSelectedMode} />
<div className="flex flex-col gap-2">
<Button
type="button"
onClick={() => setStep(3)}
className="w-full"
>
Continue
</Button>
<Button
type="button"
variant="ghost"
onClick={() => setStep(1)}
className="w-full"
>
Back
</Button>
<Button
type="button"
variant="ghost"
onClick={() => setStep(3)}
className="w-full"
>
Skip
</Button>
</div>
</>
)}
{/* Step 3 — Provider Selection (NEW) */}
{step === 3 && (
<>
<div className="flex flex-col gap-2 text-center">
<h1 className="text-2xl font-semibold tracking-tight">
Choose a provider
</h1>
<p className="text-sm text-muted-foreground">
No API keys needed for the zero-config path.
</p>
</div>
<ProviderSelectionStep
onPuterToken={setPuterToken}
onGoogleOAuthState={setGoogleOAuthStateId}
onApiKey={(provider, apiKey) => setApiKeyData({ provider, apiKey })}
onSkip={() => setStep(4)}
onContinue={() => setStep(4)}
detectedAdapters={detectedAdapters}
/>
<Button
return (
<button
key={s.id}
type="button"
variant="ghost"
onClick={() => setStep(2)}
className="w-full"
>
Back
</Button>
</>
)}
{/* Step 4 — Voice */}
{step === 4 && (
<>
<div className="flex flex-col gap-2 text-center">
<h1 className="text-2xl font-semibold tracking-tight">
Voice features
</h1>
<p className="text-sm text-muted-foreground">
Speak to your assistant and hear responses read aloud. Runs entirely on your device.
</p>
</div>
<VoiceStep
onEnable={() => {
setVoiceEnabled(true);
setStep(5);
onClick={() => {
if (isCompleted) setStep(s.id);
}}
onSkip={() => setStep(5)}
voiceCapability={hardwareInfo?.voiceCapability}
/>
<Button
type="button"
variant="ghost"
onClick={() => setStep(3)}
className="w-full"
>
Back
</Button>
</>
)}
{/* Step 5 — Telegram Bridge */}
{step === 5 && (
<TelegramStep
onNext={() => setStep(6)}
onBack={() => setStep(4)}
/>
)}
{/* Step 6 — Root Directory (was step 5, now step 6) */}
{step === 6 && (
<>
{/* Header */}
<div className="flex flex-col gap-2 text-center">
<h1 className="text-2xl font-semibold tracking-tight">
Welcome to {VOCAB.appName}
</h1>
<p className="text-sm text-muted-foreground">
{defaultAdapter === "hermes_local"
? `${VOCAB.appName} will set up a local AI workspace with a ${VOCAB.ceo.toLowerCase()}, engineer, and generalist — no API key needed.`
: `Choose a project root directory. ${VOCAB.appName} will set up a ${VOCAB.ceo.toLowerCase()} and engineer to start working.`}
</p>
</div>
{/* Form */}
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<label
htmlFor="nexus-root-dir"
className="text-sm font-medium leading-none"
>
Project root directory{defaultAdapter === "hermes_local" ? " (optional)" : ""}
</label>
<Input
id="nexus-root-dir"
type="text"
placeholder="~/projects/my-project"
value={rootDir}
onChange={(e) => setRootDir(e.target.value)}
disabled={loading}
autoFocus
autoComplete="off"
className="font-mono text-sm"
/>
</div>
{error && (
<p className="text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2">
{error}
</p>
disabled={isUpcoming}
className={cn(
"flex items-center gap-3 px-3 py-2.5 text-sm rounded-lg transition-colors text-left",
isCurrent && "bg-primary/10 text-primary font-medium",
isCompleted && "text-foreground hover:bg-muted/50 cursor-pointer",
isUpcoming && "text-muted-foreground/50 cursor-default"
)}
>
{isCompleted ? (
<Check className="h-4 w-4 shrink-0 text-[color:var(--chart-2)]" />
) : (
<Icon className={cn(
"h-4 w-4 shrink-0",
isCurrent ? "text-primary" : "text-muted-foreground/50"
)} />
)}
<span>{s.name}</span>
</button>
);
})}
</nav>
</aside>
<Button
type="button"
onClick={() => setStep(7)}
disabled={loading || probing}
className="w-full"
>
Review &amp; finish
</Button>
<Button
type="button"
variant="ghost"
onClick={() => setStep(5)}
className="w-full"
disabled={loading}
>
Back
</Button>
<Button
type="button"
variant="ghost"
onClick={() => setStep(7)}
className="w-full"
disabled={loading}
>
Skip to summary
</Button>
</div>
</>
)}
{/* Step 7 — Summary (was step 6) */}
{step === 7 && (
<OnboardingSummaryStep
hardwareInfo={hardwareInfo}
selectedMode={selectedMode}
providerLabel={deriveProviderLabel(puterToken, googleOAuthStateId, apiKeyData)}
rootDir={rootDir}
loading={loading}
error={error}
onStartChat={handleStartChat}
onBack={() => setStep(6)}
{/* Mobile step indicator - horizontal bar at top */}
<div className="md:hidden fixed top-0 left-0 right-0 z-50 bg-card border-b border-border px-4 py-3">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium">Nexus Setup</span>
<span className="text-xs text-muted-foreground">Step {step} of 6</span>
</div>
<div className="flex gap-1.5">
{STEPS.map((s) => (
<div
key={s.id}
className={cn(
"h-1 flex-1 rounded-full transition-colors",
s.id < step && "bg-[color:var(--chart-2)]",
s.id === step && "bg-primary",
s.id > step && "bg-muted"
)}
/>
)}
))}
</div>
</div>
</DialogPortal>
{/* Right content area */}
<main className="flex-1 flex flex-col overflow-y-auto">
<div className="flex-1 flex items-start justify-center p-8 md:p-12 pt-20 md:pt-12">
<div className="w-full max-w-[640px]">
{/* Step heading */}
<h2 className="text-2xl md:text-3xl font-bold mb-2">{currentStep.title}</h2>
<p className="text-base text-muted-foreground mb-8">{currentStep.description}</p>
{/* Step content */}
{renderStepContent()}
{/* Shared navigation footer — only for steps 1 and 2 */}
{!stepHasOwnNav && (
<div className="flex items-center justify-between mt-8 pt-6 border-t border-border">
{step > 1 ? (
<Button variant="ghost" size="sm" onClick={goBack}>Back</Button>
) : (
<div />
)}
<Button className="ml-auto" onClick={goNext}>Continue</Button>
</div>
)}
</div>
</div>
</main>
</div>
);
}

View file

@ -35,7 +35,7 @@ export function OfflineBanner({ queuedCount = 0 }: OfflineBannerProps) {
return (
<div
className="fixed top-0 left-0 right-0 z-50 px-4 py-2 text-sm flex items-center gap-2 bg-amber-50 text-amber-800 border-b border-amber-200 dark:bg-amber-900/40 dark:text-amber-200 dark:border-amber-800"
className="fixed top-0 left-0 right-0 z-50 px-4 py-2 text-sm flex items-center gap-2 bg-warning/10 text-warning border-b border-warning/30"
role="status"
aria-live="polite"
>

View file

@ -796,7 +796,7 @@ export function OnboardingWizard() {
}}
>
{opt.recommended && (
<span className="absolute -top-1.5 right-1.5 bg-green-500 text-white text-[9px] font-semibold px-1.5 py-0.5 rounded-full leading-none">
<span className="absolute -top-1.5 right-1.5 bg-success text-white text-[9px] font-semibold px-1.5 py-0.5 rounded-full leading-none">
Recommended
</span>
)}
@ -1050,7 +1050,7 @@ export function OnboardingWizard() {
{adapterEnvResult &&
adapterEnvResult.status === "pass" ? (
<div className="flex items-center gap-2 rounded-md border border-green-300 dark:border-green-500/40 bg-green-50 dark:bg-green-500/10 px-3 py-2 text-xs text-green-700 dark:text-green-300 animate-in fade-in slide-in-from-bottom-1 duration-300">
<div className="flex items-center gap-2 rounded-md border border-success/30 bg-success/10 px-3 py-2 text-xs text-success animate-in fade-in slide-in-from-bottom-1 duration-300">
<Check className="h-3.5 w-3.5 shrink-0" />
<span className="font-medium">Passed</span>
</div>
@ -1059,8 +1059,8 @@ export function OnboardingWizard() {
) : null}
{shouldSuggestUnsetAnthropicApiKey && (
<div className="rounded-md border border-amber-300/60 bg-amber-50/40 px-2.5 py-2 space-y-2">
<p className="text-[11px] text-amber-900/90 leading-relaxed">
<div className="rounded-md border border-warning/60 bg-warning/40 px-2.5 py-2 space-y-2">
<p className="text-[11px] text-warning leading-relaxed">
Claude failed while{" "}
<span className="font-mono">ANTHROPIC_API_KEY</span>{" "}
is set. You can clear it in this {VOCAB.ceo} adapter config
@ -1224,7 +1224,7 @@ export function OnboardingWizard() {
</p>
<p className="text-xs text-muted-foreground">{VOCAB.company}</p>
</div>
<Check className="h-4 w-4 text-green-500 shrink-0" />
<Check className="h-4 w-4 text-success shrink-0" />
</div>
<div className="flex items-center gap-3 px-3 py-2.5">
<Bot className="h-4 w-4 text-muted-foreground shrink-0" />
@ -1236,7 +1236,7 @@ export function OnboardingWizard() {
{getUIAdapter(adapterType).label}
</p>
</div>
<Check className="h-4 w-4 text-green-500 shrink-0" />
<Check className="h-4 w-4 text-success shrink-0" />
</div>
<div className="flex items-center gap-3 px-3 py-2.5">
<ListTodo className="h-4 w-4 text-muted-foreground shrink-0" />
@ -1246,7 +1246,7 @@ export function OnboardingWizard() {
</p>
<p className="text-xs text-muted-foreground">Task</p>
</div>
<Check className="h-4 w-4 text-green-500 shrink-0" />
<Check className="h-4 w-4 text-success shrink-0" />
</div>
</div>
</div>
@ -1337,7 +1337,7 @@ export function OnboardingWizard() {
{/* Right half — ASCII art (hidden on mobile) */}
<div
className={cn(
"hidden md:block overflow-hidden bg-[#1d1d1d] transition-[width,opacity] duration-500 ease-in-out",
"hidden md:block overflow-hidden bg-card transition-[width,opacity] duration-500 ease-in-out",
step === 1 ? "w-1/2 opacity-100" : "w-0 opacity-0"
)}
>
@ -1362,10 +1362,10 @@ function AdapterEnvironmentResult({
: "Failed";
const statusClass =
result.status === "pass"
? "text-green-700 dark:text-green-300 border-green-300 dark:border-green-500/40 bg-green-50 dark:bg-green-500/10"
? "text-success border-success/30 bg-success/10"
: result.status === "warn"
? "text-amber-700 dark:text-amber-300 border-amber-300 dark:border-amber-500/40 bg-amber-50 dark:bg-amber-500/10"
: "text-red-700 dark:text-red-300 border-red-300 dark:border-red-500/40 bg-red-50 dark:bg-red-500/10";
? "text-warning border-warning/30 bg-warning/10"
: "text-destructive border-destructive/30 bg-destructive/10";
return (
<div className={`rounded-md border px-2.5 py-2 text-[11px] ${statusClass}`}>

View file

@ -115,7 +115,7 @@ export function OutputFeedbackButtons({
size="sm"
variant="outline"
disabled={disabled || isSaving}
className={cn(visibleVote === "up" && "border-green-600/50 bg-green-500/10 text-green-700")}
className={cn(visibleVote === "up" && "border-success/50 bg-success/10 text-success")}
onClick={() => handleVote("up")}
>
<ThumbsUp className="mr-1.5 h-3.5 w-3.5" />
@ -126,7 +126,7 @@ export function OutputFeedbackButtons({
size="sm"
variant="outline"
disabled={disabled || isSaving}
className={cn(visibleVote === "down" && "border-amber-600/50 bg-amber-500/10 text-amber-800")}
className={cn(visibleVote === "down" && "border-warning/50 bg-warning/10 text-warning")}
onClick={() => handleVote("down")}
>
<ThumbsDown className="mr-1.5 h-3.5 w-3.5" />

View file

@ -62,7 +62,7 @@ function SaveIndicator({ state }: { state: ProjectFieldSaveState }) {
}
if (state === "saved") {
return (
<span className="inline-flex items-center gap-1 text-[11px] text-green-600 dark:text-green-400">
<span className="inline-flex items-center gap-1 text-[11px] text-success">
<Check className="h-3 w-3" />
Saved
</span>
@ -738,9 +738,9 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
className={cn(
"rounded-full px-1.5 py-0.5 text-[10px] uppercase tracking-wide",
service.status === "running"
? "bg-green-500/15 text-green-700 dark:text-green-300"
? "bg-success/15 text-success"
: service.status === "failed"
? "bg-red-500/15 text-red-700 dark:text-red-300"
? "bg-destructive/15 text-destructive"
: "bg-muted text-muted-foreground",
)}
>
@ -891,7 +891,7 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
data-slot="toggle"
className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors",
executionWorkspacesEnabled ? "bg-green-600" : "bg-muted",
executionWorkspacesEnabled ? "bg-success" : "bg-muted",
)}
type="button"
onClick={() =>
@ -930,7 +930,7 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
data-slot="toggle"
className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors",
executionWorkspaceDefaultMode === "isolated_workspace" ? "bg-green-600" : "bg-muted",
executionWorkspaceDefaultMode === "isolated_workspace" ? "bg-success" : "bg-muted",
)}
type="button"
onClick={() =>

View file

@ -338,10 +338,10 @@ export function ProviderQuotaCard({
qw.usedPercent == null
? null
: qw.usedPercent >= 90
? "bg-red-400"
? "bg-destructive"
: qw.usedPercent >= 70
? "bg-yellow-400"
: "bg-green-400";
? "bg-warning"
: "bg-success";
return (
<div key={qw.label} className="space-y-1">
<div className="flex items-center justify-between gap-2 text-xs">

View file

@ -12,9 +12,9 @@ interface QuotaBarProps {
}
function fillColor(pct: number): string {
if (pct > 90) return "bg-red-400";
if (pct > 70) return "bg-yellow-400";
return "bg-green-400";
if (pct > 90) return "bg-destructive";
if (pct > 70) return "bg-warning";
return "bg-success";
}
export function QuotaBar({

View file

@ -45,7 +45,7 @@ export function ReportsToPicker({
type="button"
className={cn(
"inline-flex max-w-full min-w-0 items-center gap-1.5 overflow-hidden rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors",
terminatedManager && "border-amber-600/45 bg-amber-500/5",
terminatedManager && "border-warning/45 bg-warning/5",
disabled && "opacity-60 cursor-not-allowed",
)}
disabled={disabled}
@ -61,7 +61,7 @@ export function ReportsToPicker({
<span
className={cn(
"min-w-0 truncate",
terminatedManager && "text-amber-900 dark:text-amber-200",
terminatedManager && "text-warning",
)}
>
{`Reports to ${current.name}${terminatedManager ? " (terminated)" : ""}`}

View file

@ -274,11 +274,11 @@ export function RoutineRunVariablesDialog({
<DialogFooter showCloseButton={false}>
{missingRequired.length > 0 ? (
<p className="mr-auto text-xs text-amber-600">
<p className="mr-auto text-xs text-warning">
Missing: {missingRequired.join(", ")}
</p>
) : workspaceSelectionEnabled && !workspaceConfigValid ? (
<p className="mr-auto text-xs text-amber-600">
<p className="mr-auto text-xs text-warning">
Choose an existing workspace before running.
</p>
) : (

View file

@ -124,12 +124,12 @@ export function SidebarAgents() {
) : null}
{runCount > 0 ? (
<span className="relative flex h-2 w-2">
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500" />
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
</span>
) : null}
{runCount > 0 ? (
<span className="text-[11px] font-medium text-blue-600 dark:text-blue-400">
<span className="text-[11px] font-medium text-primary">
{runCount} live
</span>
) : null}

View file

@ -50,7 +50,7 @@ export function SidebarNavItem({
<span className="relative shrink-0">
<Icon className="h-4 w-4" />
{alert && (
<span className="absolute -right-0.5 -top-0.5 h-2 w-2 rounded-full bg-red-500 shadow-[0_0_0_2px_hsl(var(--background))]" />
<span className="absolute -right-0.5 -top-0.5 h-2 w-2 rounded-full bg-destructive shadow-[0_0_0_2px_hsl(var(--background))]" />
)}
</span>
<span className="flex-1 truncate">{label}</span>
@ -59,7 +59,7 @@ export function SidebarNavItem({
className={cn(
"ml-auto rounded-full px-1.5 py-0.5 text-[10px] font-medium leading-none",
textBadgeTone === "amber"
? "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400"
? "bg-warning/10 text-warning"
: "bg-muted text-muted-foreground",
)}
>
@ -69,10 +69,10 @@ export function SidebarNavItem({
{liveCount != null && liveCount > 0 && (
<span className="ml-auto flex items-center gap-1.5">
<span className="relative flex h-2 w-2">
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500" />
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
</span>
<span className="text-[11px] font-medium text-blue-600 dark:text-blue-400">{liveCount} live</span>
<span className="text-[11px] font-medium text-primary">{liveCount} live</span>
</span>
)}
{badge != null && badge > 0 && (
@ -80,7 +80,7 @@ export function SidebarNavItem({
className={cn(
"ml-auto rounded-full px-1.5 py-0.5 text-xs leading-none",
badgeTone === "danger"
? "bg-red-600/90 text-red-50"
? "bg-destructive/90 text-destructive"
: "bg-primary text-primary-foreground",
)}
>

View file

@ -86,7 +86,7 @@ function SortableProjectItem({
>
<span
className="shrink-0 h-3.5 w-3.5 rounded-sm"
style={{ backgroundColor: project.color ?? "#6366f1" }}
style={{ backgroundColor: project.color ?? "var(--primary)" }}
/>
<span className="flex-1 truncate">{project.name}</span>
{project.pauseReason === "budget" ? <BudgetSidebarMarker title="Project paused by budget" /> : null}

View file

@ -65,7 +65,7 @@ export function SkillCard({
{hasUpdate && !isReadOnly && (
<Badge
variant="outline"
className="text-xs text-amber-600 border-amber-500"
className="text-xs text-warning border-warning/30"
aria-label="Update available"
>
Update
@ -91,7 +91,7 @@ export function SkillCard({
<Badge variant="secondary" className="text-xs">{skill.sourceId}</Badge>
{skill.averageRating != null && (
<span className="flex items-center gap-1 text-xs text-muted-foreground">
<Star className="h-3 w-3 fill-amber-400 text-amber-400" />
<Star className="h-3 w-3 fill-amber-400 text-warning" />
{skill.averageRating.toFixed(1)}
</span>
)}

View file

@ -41,7 +41,7 @@ export function StarRating({
<Star
className={cn(
iconClass,
filled ? "fill-amber-400 text-amber-400" : "text-muted-foreground",
filled ? "fill-amber-400 text-warning" : "text-muted-foreground",
)}
/>
</button>

View file

@ -141,7 +141,7 @@ export function SwipeToArchive({
>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 flex items-center justify-end bg-emerald-600 px-4 text-white"
className="pointer-events-none absolute inset-0 flex items-center justify-end bg-success px-4 text-white"
style={{ opacity: Math.max(archiveReveal, 0.2) }}
>
<span className="inline-flex items-center gap-2 text-sm font-medium">
@ -153,7 +153,7 @@ export function SwipeToArchive({
data-inbox-row-surface
className={cn(
"relative will-change-transform",
selected ? "bg-zinc-100 dark:bg-zinc-800" : "bg-card",
selected ? "bg-muted" : "bg-card",
)}
style={{
transform: `translate3d(${offsetX}px, 0, 0)`,

View file

@ -5,17 +5,17 @@ import { useToast, type ToastItem, type ToastTone } from "../context/ToastContex
import { cn } from "../lib/utils";
const toneClasses: Record<ToastTone, string> = {
info: "border-sky-300 bg-sky-50 text-sky-900 dark:border-sky-500/25 dark:bg-sky-950/60 dark:text-sky-100",
success: "border-emerald-300 bg-emerald-50 text-emerald-900 dark:border-emerald-500/25 dark:bg-emerald-950/60 dark:text-emerald-100",
warn: "border-amber-300 bg-amber-50 text-amber-900 dark:border-amber-500/25 dark:bg-amber-950/60 dark:text-amber-100",
error: "border-red-300 bg-red-50 text-red-900 dark:border-red-500/30 dark:bg-red-950/60 dark:text-red-100",
info: "border-primary/30 bg-primary/10 text-primary",
success: "border-success/30 bg-success/10 text-success",
warn: "border-warning/30 bg-warning/10 text-warning",
error: "border-destructive/30 bg-destructive/10 text-destructive",
};
const toneDotClasses: Record<ToastTone, string> = {
info: "bg-sky-500 dark:bg-sky-400",
success: "bg-emerald-500 dark:bg-emerald-400",
warn: "bg-amber-500 dark:bg-amber-400",
error: "bg-red-500 dark:bg-red-400",
info: "bg-primary",
success: "bg-success",
warn: "bg-warning",
error: "bg-destructive",
};
function AnimatedToast({

View file

@ -40,10 +40,10 @@ export function VoiceWaveform({ stream, active }: VoiceWaveformProps) {
const canvasHeight = canvas.height;
const ctx2d = canvas.getContext("2d");
// Get primary color from CSS variable
// Get primary color from CSS variable; fall back to volt (the dark-mode brand accent).
const primaryColor =
getComputedStyle(document.documentElement).getPropertyValue("--primary").trim() ||
"#1e66f5";
"#faff69";
const draw = () => {
analyser.getByteFrequencyData(dataArray);

View file

@ -126,7 +126,7 @@ export function ToggleField({
type="button"
className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors",
checked ? "bg-green-600" : "bg-muted"
checked ? "bg-success" : "bg-muted"
)}
onClick={() => onChange(!checked)}
>
@ -175,7 +175,7 @@ export function ToggleWithNumber({
data-slot="toggle"
className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors shrink-0",
checked ? "bg-green-600" : "bg-muted"
checked ? "bg-success" : "bg-muted"
)}
onClick={() => onCheckedChange(!checked)}
>

View file

@ -1,5 +1,6 @@
// [nexus] Hardware summary display for onboarding wizard step 1
import { Skeleton } from "@/components/ui/skeleton";
import { Badge } from "@/components/ui/badge";
import type { HardwareInfo } from "@/api/hardware";
interface HardwareSummaryStepProps {
@ -16,28 +17,39 @@ interface StatRowProps {
function StatRow({ label, value }: StatRowProps) {
return (
<div className="flex items-center justify-between gap-2">
<span className="text-xs text-muted-foreground">{label}</span>
<span className="text-sm font-medium">{value}</span>
<span className="text-sm text-muted-foreground">{label}</span>
<span className="text-base font-medium">{value}</span>
</div>
);
}
const TIER_BADGE: Record<string, { label: string; color: string }> = {
apple_silicon: { label: "Apple Silicon", color: "text-[color:var(--chart-2)] border-[color:var(--chart-2)]/30 bg-[color:var(--chart-2)]/10" },
gpu: { label: "GPU Available", color: "text-primary border-primary/30 bg-primary/10" },
cpu_only: { label: "CPU Only", color: "text-[color:var(--chart-4)] border-[color:var(--chart-4)]/30 bg-[color:var(--chart-4)]/10" },
};
export function HardwareSummaryStep({ hardwareInfo, isLoading, isError }: HardwareSummaryStepProps) {
if (isLoading) {
return (
<div className="flex flex-col gap-2">
<Skeleton className="h-4 w-full rounded" />
<Skeleton className="h-4 w-full rounded" />
<Skeleton className="h-4 w-full rounded" />
<div className="flex flex-col gap-3">
<Skeleton className="h-5 w-full rounded" />
<Skeleton className="h-5 w-full rounded" />
<Skeleton className="h-5 w-full rounded" />
</div>
);
}
if (isError) {
return (
<p className="text-sm text-muted-foreground">
Could not detect hardware. You can still continue.
</p>
<div className="flex flex-col gap-2">
<p className="text-sm text-muted-foreground">
Could not detect hardware. You can still continue.
</p>
<p className="text-xs text-destructive/70">
GET /api/system/providers failed -- check server logs
</p>
</div>
);
}
@ -50,10 +62,24 @@ export function HardwareSummaryStep({ hardwareInfo, isLoading, isError }: Hardwa
}
const { hardwareTier } = hardwareInfo;
const tierBadge = TIER_BADGE[hardwareTier];
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-5">
{/* Detection status + tier badge */}
<div className="flex items-center gap-3 flex-wrap">
<Badge variant="outline" className="text-[color:var(--chart-2)] border-[color:var(--chart-2)]/30 bg-[color:var(--chart-2)]/10">
Detected
</Badge>
{tierBadge && (
<Badge variant="outline" className={tierBadge.color}>
{tierBadge.label}
</Badge>
)}
</div>
{/* Hardware stats */}
<div className="flex flex-col gap-3">
{hardwareTier === "apple_silicon" && (
<>
<StatRow label="Unified memory" value={`${hardwareInfo.totalGb} GB`} />
@ -74,7 +100,7 @@ export function HardwareSummaryStep({ hardwareInfo, isLoading, isError }: Hardwa
<>
<StatRow label="System RAM" value={`${hardwareInfo.totalGb} GB`} />
<StatRow label="CPU" value={hardwareInfo.cpuModel} />
<p className="text-xs text-muted-foreground">
<p className="text-sm text-muted-foreground">
Slower than GPU-accelerated models -- cloud AI recommended
</p>
</>
@ -83,10 +109,9 @@ export function HardwareSummaryStep({ hardwareInfo, isLoading, isError }: Hardwa
{hardwareTier !== "cpu_only" && (
<div className="flex flex-col gap-1 pt-2">
<span className="text-sm font-medium">Local AI (recommended for privacy)</span>
<span className="text-xs text-muted-foreground">
Runs entirely on your machine.{"\n"}
No accounts. No tracking. Works offline.
<span className="text-base font-medium">Local AI (recommended for privacy)</span>
<span className="text-sm text-muted-foreground">
Runs entirely on your machine. No accounts. No tracking. Works offline.
</span>
</div>
)}

View file

@ -1,49 +1,74 @@
// [nexus] Three-card mode selector for onboarding wizard step 2
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { MessageSquare, Hammer, Layers } from "lucide-react";
import type { NexusMode } from "@/api/hardware";
import type { LucideIcon } from "lucide-react";
interface ModeSelectorProps {
value: NexusMode;
onChange: (mode: NexusMode) => void;
}
const MODES: { id: NexusMode; label: string; description: string }[] = [
const MODES: { id: NexusMode; label: string; description: string; icon: LucideIcon; accent: string; recommended?: boolean }[] = [
{
id: "personal_ai",
label: "Personal AI Assistant",
description: "Always available, persistent memory, private.",
icon: MessageSquare,
accent: "text-[color:var(--chart-3)]",
},
{
id: "project_builder",
label: "Project Builder",
description: "Brainstorm -> PM -> Engineer -> shipped product.",
icon: Hammer,
accent: "text-[color:var(--chart-4)]",
},
{
id: "both",
label: "Both (recommended)",
label: "Both",
description: "A conversation becomes a project with one click.",
icon: Layers,
accent: "text-primary",
recommended: true,
},
];
export function ModeSelector({ value, onChange }: ModeSelectorProps) {
return (
<div className="grid gap-3">
{MODES.map((mode) => (
<button
key={mode.id}
type="button"
onClick={() => onChange(mode.id)}
className={cn(
"flex flex-col gap-1 rounded-lg border p-4 text-left transition-colors",
value === mode.id
? "border-primary bg-primary/5"
: "border-border hover:border-muted-foreground/50"
)}
>
<span className="font-medium text-sm">{mode.label}</span>
<span className="text-xs text-muted-foreground">{mode.description}</span>
</button>
))}
{MODES.map((mode) => {
const Icon = mode.icon;
const isSelected = value === mode.id;
return (
<button
key={mode.id}
type="button"
onClick={() => onChange(mode.id)}
className={cn(
"flex items-start gap-4 rounded-lg border p-5 text-left transition-colors",
isSelected
? "border-l-2 border-l-primary border-t border-r border-b border-border bg-primary/5"
: "border-border hover:border-muted-foreground/50"
)}
>
<Icon className={cn("h-5 w-5 shrink-0 mt-0.5", mode.accent)} />
<div className="flex flex-col gap-1 flex-1">
<div className="flex items-center gap-2">
<span className="text-base font-medium">{mode.label}</span>
{mode.recommended && (
<Badge variant="outline" className="text-primary border-primary/30 bg-primary/10 text-xs">
Recommended
</Badge>
)}
</div>
<span className="text-sm text-muted-foreground">{mode.description}</span>
</div>
</button>
);
})}
</div>
);
}

View file

@ -1,4 +1,6 @@
// [nexus] Summary screen for onboarding wizard step 5 — read-only review before starting
// [nexus] Summary screen for onboarding wizard step 6 — read-only review before starting
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import type { HardwareInfo, HardwareTier, NexusMode } from "@/api/hardware";
import { Button } from "@/components/ui/button";
@ -7,23 +9,35 @@ interface OnboardingSummaryStepProps {
selectedMode: NexusMode;
providerLabel: string;
rootDir: string;
onRootDirChange: (value: string) => void;
loading: boolean;
error: string | null;
onStartChat: () => void;
onBack: () => void;
voiceEnabled?: boolean;
defaultAdapter?: string;
}
interface SummaryRowProps {
label: string;
value: string;
mono?: boolean;
warn?: boolean;
}
function SummaryRow({ label, value, mono }: SummaryRowProps) {
function SummaryRow({ label, value, mono, warn }: SummaryRowProps) {
return (
<div className="flex items-start justify-between gap-4">
<span className="text-sm text-muted-foreground shrink-0">{label}</span>
<span className={mono ? "font-mono text-sm text-right" : "text-sm text-right"}>{value}</span>
<span
className={cn(
"text-base font-medium text-right",
mono && "font-mono text-sm",
warn && "text-[color:var(--chart-4)]"
)}
>
{value}
</span>
</div>
);
}
@ -45,32 +59,54 @@ export function OnboardingSummaryStep({
selectedMode,
providerLabel,
rootDir,
onRootDirChange,
loading,
error,
onStartChat,
onBack,
voiceEnabled,
defaultAdapter,
}: OnboardingSummaryStepProps) {
const hardwareLabel = hardwareInfo
? (HARDWARE_TIER_LABELS[hardwareInfo.hardwareTier] ?? "Unknown")
: "Unknown";
const modeLabel = MODE_LABELS[selectedMode];
const isProviderNone = providerLabel === "None selected";
const isVoiceNone = voiceEnabled === false || voiceEnabled === undefined;
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2 text-center">
<h1 className="text-2xl font-semibold tracking-tight">Ready to go</h1>
<p className="text-sm text-muted-foreground">Review your setup before starting.</p>
</div>
{/* Summary card */}
<div className="rounded-lg border border-border p-4 flex flex-col gap-3">
<div className="rounded-lg border border-border p-5 flex flex-col gap-4">
<SummaryRow label="Hardware" value={hardwareLabel} />
<SummaryRow label="Mode" value={modeLabel} />
<SummaryRow label="Provider" value={providerLabel} />
{rootDir && <SummaryRow label="Root directory" value={rootDir} mono />}
<SummaryRow label="Provider" value={providerLabel} warn={isProviderNone} />
<SummaryRow label="Voice" value={voiceEnabled ? "Enabled" : "None selected"} warn={isVoiceNone} />
</div>
{/* Root directory — editable input for claude_local, read-only row for others */}
{defaultAdapter === "claude_local" ? (
<div className="flex flex-col gap-2">
<label htmlFor="summary-root-dir" className="text-sm text-muted-foreground">
Root directory (required for Claude Code)
</label>
<Input
id="summary-root-dir"
type="text"
placeholder="~/projects/my-project"
className="font-mono text-sm"
autoComplete="off"
value={rootDir}
onChange={(e) => onRootDirChange(e.target.value)}
/>
</div>
) : rootDir ? (
<div className="rounded-lg border border-border p-5">
<SummaryRow label="Root directory" value={rootDir} mono />
</div>
) : null}
{/* Error message */}
{error && (
<p className="text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2">
@ -84,7 +120,7 @@ export function OnboardingSummaryStep({
type="button"
onClick={onStartChat}
disabled={loading}
className="w-full"
className="w-full h-12 text-base font-medium"
>
{loading ? (
<span className="flex items-center gap-2">

View file

@ -1,8 +1,8 @@
// [nexus] Provider selection step — Step 3 of 4 in the onboarding wizard
// Heading: "Choose a provider" / Subheading: "No API keys needed for the zero-config path."
// Three provider cards (Puter, Google, API key) with adapter badges and skip button
// [nexus] Provider selection step — Step 3 of 6 in the onboarding wizard
// Separated detected adapters section from provider cards
import { useState } from "react";
import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { PuterAuthButton } from "./PuterAuthButton";
import { GoogleOAuthButton } from "./GoogleOAuthButton";
@ -12,68 +12,139 @@ interface ProviderSelectionStepProps {
onPuterToken: (token: string) => void;
onGoogleOAuthState: (stateId: string) => void;
onApiKey: (provider: string, apiKey: string) => void;
onAdapterSelected: (adapter: string | null) => void;
onSkip: () => void;
onContinue: () => void;
detectedAdapters: Record<string, boolean>;
probing?: boolean;
}
type ProviderChoice = "puter" | "google" | "apikey" | null;
type AdapterChoice = "claude_local" | "hermes_local" | "openclaw_gateway" | null;
const ADAPTER_INFO: Record<string, { name: string; description: string }> = {
claude_local: { name: "Claude Code", description: "CLI agent — uses your Anthropic API key" },
hermes_local: { name: "Hermes", description: "Free local agent — no API key needed" },
openclaw_gateway: { name: "OpenClaw Gateway", description: "Multi-model gateway" },
};
export function ProviderSelectionStep({
onPuterToken,
onGoogleOAuthState,
onApiKey,
onAdapterSelected,
onSkip,
onContinue,
detectedAdapters,
probing,
}: ProviderSelectionStepProps) {
const [selectedProvider, setSelectedProvider] = useState<ProviderChoice>(null);
const [selectedAdapter, setSelectedAdapter] = useState<AdapterChoice>(null);
const [providerReady, setProviderReady] = useState(false);
const [error, setError] = useState<string | null>(null);
function handleSelect(provider: ProviderChoice) {
function handleSelectProvider(provider: ProviderChoice) {
setSelectedProvider(provider);
setSelectedAdapter(null);
setProviderReady(false);
setError(null);
onAdapterSelected(null);
}
function handleSelectAdapter(adapter: AdapterChoice) {
setSelectedAdapter(adapter);
setSelectedProvider(null);
setProviderReady(false);
setError(null);
onAdapterSelected(adapter);
}
const hermesDetected = detectedAdapters["hermes_local"] === true;
const claudeDetected = detectedAdapters["claude_local"] === true;
const openclawDetected = detectedAdapters["openclaw_gateway"] === true;
const hasDetectedAdapters = hermesDetected || claudeDetected || openclawDetected;
// Determine if continue should be active
const canContinue = selectedAdapter !== null || (selectedProvider !== null && providerReady);
const hasSelection = selectedAdapter !== null || selectedProvider !== null;
function handleContinue() {
if (selectedAdapter) {
onContinue();
} else if (selectedProvider && providerReady) {
onContinue();
}
}
return (
<div className="flex flex-col gap-4">
{/* Three provider cards */}
<div className="flex flex-col gap-6">
{/* Detected local adapters section */}
{(probing || hasDetectedAdapters) && (
<div className="flex flex-col gap-3">
<p className="text-sm font-medium text-muted-foreground">Detected on this machine</p>
{probing ? (
<div className="flex items-center gap-3 rounded-lg border border-border p-4">
<svg className="h-4 w-4 animate-spin text-muted-foreground" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
<span className="text-sm text-muted-foreground">Checking for installed tools...</span>
</div>
) : (
<div className="flex flex-col gap-2">
{(["claude_local", "hermes_local", "openclaw_gateway"] as const).map((key) => {
if (!detectedAdapters[key]) return null;
const info = ADAPTER_INFO[key];
const isSelected = selectedAdapter === key;
return (
<button
key={key}
type="button"
onClick={() => handleSelectAdapter(key)}
className={cn(
"flex items-center justify-between rounded-lg border p-4 text-left transition-colors w-full",
isSelected
? "border-l-2 border-l-primary bg-primary/5"
: "border-border hover:border-muted-foreground/50"
)}
>
<div className="flex flex-col gap-0.5">
<span className="text-sm font-medium">{info.name}</span>
<span className="text-xs text-muted-foreground">{info.description}</span>
</div>
<Badge variant="outline" className="text-[color:var(--chart-2)] border-[color:var(--chart-2)]/30 bg-[color:var(--chart-2)]/10 shrink-0 ml-3">
Detected
</Badge>
</button>
);
})}
</div>
)}
</div>
)}
{/* Cloud provider cards */}
<div className="flex flex-col gap-3">
<p className="text-sm font-medium text-muted-foreground">Cloud providers</p>
{/* Puter card */}
<button
type="button"
onClick={() => handleSelect("puter")}
onClick={() => handleSelectProvider("puter")}
className={cn(
"flex flex-col gap-1 rounded-lg border p-4 text-left transition-colors",
"flex flex-col gap-1 rounded-lg border p-4 text-left transition-colors w-full",
selectedProvider === "puter"
? "border-primary bg-primary/5"
? "border-l-2 border-l-[var(--primary)] bg-[var(--primary)]/5"
: "border-border hover:border-muted-foreground/50"
)}
>
<div className="flex items-center gap-2 flex-wrap">
<span className="font-medium text-sm">Puter -- free, zero-config</span>
{hermesDetected && (
<span className="text-xs text-primary">Hermes detected</span>
)}
{claudeDetected && (
<span className="text-xs text-primary">Claude Code detected</span>
)}
{openclawDetected && (
<span className="text-xs text-primary">OpenClaw detected</span>
)}
</div>
<span className="text-xs text-muted-foreground">
<span className="text-base font-medium">Puter -- free, zero-config</span>
<span className="text-sm text-muted-foreground">
Free AI powered by your Puter.com account. No API key needed.
</span>
</button>
{/* Puter auth component — shown when Puter is selected */}
{/* Puter auth component */}
{selectedProvider === "puter" && (
<PuterAuthButton
onSuccess={(token) => {
@ -87,21 +158,21 @@ export function ProviderSelectionStep({
{/* Google card */}
<button
type="button"
onClick={() => handleSelect("google")}
onClick={() => handleSelectProvider("google")}
className={cn(
"flex flex-col gap-1 rounded-lg border p-4 text-left transition-colors",
"flex flex-col gap-1 rounded-lg border p-4 text-left transition-colors w-full",
selectedProvider === "google"
? "border-primary bg-primary/5"
? "border-l-2 border-l-[var(--primary)] bg-[var(--primary)]/5"
: "border-border hover:border-muted-foreground/50"
)}
>
<span className="font-medium text-sm">Google -- Gemini free tier</span>
<span className="text-xs text-muted-foreground">
<span className="text-base font-medium">Google Gemini -- free tier</span>
<span className="text-sm text-muted-foreground">
Sign in with Google to access Gemini via your Google account.
</span>
</button>
{/* Google OAuth component — shown when Google is selected */}
{/* Google OAuth component */}
{selectedProvider === "google" && (
<GoogleOAuthButton
onSuccess={(stateId) => {
@ -115,21 +186,21 @@ export function ProviderSelectionStep({
{/* API key card */}
<button
type="button"
onClick={() => handleSelect("apikey")}
onClick={() => handleSelectProvider("apikey")}
className={cn(
"flex flex-col gap-1 rounded-lg border p-4 text-left transition-colors",
"flex flex-col gap-1 rounded-lg border p-4 text-left transition-colors w-full",
selectedProvider === "apikey"
? "border-primary bg-primary/5"
? "border-l-2 border-l-[var(--primary)] bg-[var(--primary)]/5"
: "border-border hover:border-muted-foreground/50"
)}
>
<span className="font-medium text-sm">API key -- subscription provider</span>
<span className="text-xs text-muted-foreground">
<span className="text-base font-medium">API key -- subscription provider</span>
<span className="text-sm text-muted-foreground">
Use your own OpenAI, Anthropic, or Groq API key.
</span>
</button>
{/* API key form — shown when API key is selected */}
{/* API key form */}
{selectedProvider === "apikey" && (
<ApiKeyEntryForm
onSave={(prov, key) => {
@ -148,23 +219,22 @@ export function ProviderSelectionStep({
</p>
)}
{/* Continue button — shown when provider auth is complete */}
{providerReady && (
<Button type="button" onClick={onContinue} className="w-full">
Continue
</Button>
)}
{/* Skip button — always visible */}
<Button
type="button"
variant="ghost"
onClick={onSkip}
aria-label="Skip provider setup for now"
className="w-full"
>
Skip for now
</Button>
{/* Single bottom action — contextual based on selection state */}
<div className="flex items-center justify-center mt-2">
{canContinue ? (
<Button type="button" onClick={handleContinue} className="w-full">
Continue
</Button>
) : (
<button
type="button"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
onClick={onSkip}
>
Continue without a provider
</button>
)}
</div>
</div>
);
}

View file

@ -1,4 +1,4 @@
// [nexus] Telegram bridge onboarding step — BotFather guided setup with token validation
// [nexus] Phone access onboarding step — Telegram bridge with BotFather guided setup
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@ -50,68 +50,70 @@ export function TelegramStep({ onNext, onBack }: TelegramStepProps) {
return (
<div className="flex flex-col gap-6">
{/* Header */}
<div className="flex flex-col gap-2 text-center">
<h1 className="text-2xl font-semibold tracking-tight">Connect Telegram</h1>
<p className="text-sm text-muted-foreground">
Get instant notifications and interact with your agents via Telegram.
</p>
{/* Telegram as current option */}
<div className="flex flex-col gap-4">
<p className="text-sm font-medium">Telegram Bot</p>
{/* BotFather instructions */}
<div className="flex flex-col gap-3">
<p className="text-sm text-muted-foreground">Set up your bot in 4 steps:</p>
<ol className="flex flex-col gap-3 list-none pl-0">
{[
<>Open Telegram and search for <span className="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">@BotFather</span></>,
<>Send <span className="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">/newbot</span> and follow the prompts to create a bot</>,
<>Copy the bot token -- it looks like <span className="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">123456:ABC-DEF...</span></>,
"Paste the token below and click Validate",
].map((instruction, i) => (
<li key={i} className="flex items-start gap-3 text-sm text-muted-foreground">
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-medium text-foreground">
{i + 1}
</span>
<span className="mt-0.5">{instruction}</span>
</li>
))}
</ol>
</div>
{/* Token input */}
<div className="flex flex-col gap-2">
<label htmlFor="telegram-token" className="text-sm font-medium leading-none">
Bot token
</label>
<Input
id="telegram-token"
type="text"
placeholder="Paste bot token here"
value={token}
onChange={(e) => {
setToken(e.target.value);
setBotUsername(null);
setError(null);
}}
disabled={validating}
autoComplete="off"
className="font-mono text-sm"
/>
{/* Success state */}
{botUsername && (
<p className={cn("text-sm", "text-[color:var(--chart-2)]")}>
Connected to @{botUsername}
</p>
)}
{/* Error state */}
{error && (
<p className="text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2">
{error}
</p>
)}
</div>
</div>
{/* BotFather instructions */}
<div className="flex flex-col gap-2">
<p className="text-sm font-medium">Set up your bot in 4 steps:</p>
<ol className="flex flex-col gap-2 list-none pl-0">
{[
<>Open Telegram and search for <span className="font-mono text-xs bg-muted px-1 py-0.5 rounded">@BotFather</span></>,
<>Send <span className="font-mono text-xs bg-muted px-1 py-0.5 rounded">/newbot</span> and follow the prompts to create a bot</>,
<>Copy the bot token it looks like <span className="font-mono text-xs bg-muted px-1 py-0.5 rounded">123456:ABC-DEF...</span></>,
"Paste the token below and click Validate",
].map((instruction, i) => (
<li key={i} className="flex items-start gap-3 text-sm text-muted-foreground">
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-medium text-foreground">
{i + 1}
</span>
<span className="mt-0.5">{instruction}</span>
</li>
))}
</ol>
</div>
{/* Token input */}
<div className="flex flex-col gap-2">
<label htmlFor="telegram-token" className="text-sm font-medium leading-none">
Bot token
</label>
<Input
id="telegram-token"
type="text"
placeholder="Paste bot token here"
value={token}
onChange={(e) => {
setToken(e.target.value);
setBotUsername(null);
setError(null);
}}
disabled={validating}
autoComplete="off"
className="font-mono text-sm"
/>
{/* Success state */}
{botUsername && (
<p className={cn("text-sm", "text-green-600 dark:text-green-400")}>
Connected to @{botUsername}
</p>
)}
{/* Error state */}
{error && (
<p className="text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2">
{error}
</p>
)}
</div>
{/* Future bridges note */}
<p className="text-sm text-muted-foreground">
Discord and WhatsApp bridges coming in a future update.
</p>
{/* Actions */}
<div className="flex flex-col gap-2">
@ -122,7 +124,7 @@ export function TelegramStep({ onNext, onBack }: TelegramStepProps) {
variant="outline"
className="w-full"
>
{validating ? "Validating" : "Validate Token"}
{validating ? "Validating..." : "Validate Token"}
</Button>
<Button

View file

@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
import { Mic, Volume2, CheckCircle2, AlertTriangle, Info } from "lucide-react";
import { Mic, Volume2, CheckCircle2, Info } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import type { VoiceCapability } from "../../hooks/useHardwareInfo";
@ -21,55 +22,73 @@ export function VoiceStep({ onEnable, onSkip, voiceCapability }: VoiceStepProps)
// Determine STT status label
function whisperStatusLabel(): string {
if (!voiceCapability) {
// Fall back to mic-only check
if (micAvailable === false) return "No microphone detected — unavailable";
if (micAvailable === true) return "Microphone detected — speak to your assistant";
if (micAvailable === false) return "No microphone detected -- unavailable";
if (micAvailable === true) return "Microphone detected -- speak to your assistant";
return "Checking microphone...";
}
if (voiceCapability.whisperAvailable) return "Whisper detected speech recognition ready";
return "Whisper not found install whisper-cpp for voice input";
if (voiceCapability.whisperAvailable) return "Whisper detected -- speech recognition ready";
return "Whisper not found -- install whisper-cpp for voice input";
}
function whisperStatusIcon() {
function whisperBadge() {
if (!voiceCapability) return null;
if (voiceCapability.whisperAvailable) {
return <CheckCircle2 className="h-4 w-4 text-green-500 shrink-0" />;
return (
<Badge variant="outline" className="text-[color:var(--chart-2)] border-[color:var(--chart-2)]/30 bg-[color:var(--chart-2)]/10 text-xs">
Available
</Badge>
);
}
return <AlertTriangle className="h-4 w-4 text-amber-500 shrink-0" />;
return (
<Badge variant="outline" className="text-[color:var(--chart-4)] border-[color:var(--chart-4)]/30 bg-[color:var(--chart-4)]/10 text-xs">
Install needed
</Badge>
);
}
// Determine TTS status label
function piperStatusLabel(): string {
if (!voiceCapability) {
return "Hear responses read aloud. Runs entirely on your device no server needed.";
return "Hear responses read aloud. Runs entirely on your device -- no server needed.";
}
if (voiceCapability.piperAvailable) return "Piper detected text-to-speech ready";
return "Piper not found install piper for voice output";
if (voiceCapability.piperAvailable) return "Piper detected -- text-to-speech ready";
return "Piper not found -- install piper for voice output";
}
function piperStatusIcon() {
function piperBadge() {
if (!voiceCapability) return null;
if (voiceCapability.piperAvailable) {
return <CheckCircle2 className="h-4 w-4 text-green-500 shrink-0" />;
return (
<Badge variant="outline" className="text-[color:var(--chart-2)] border-[color:var(--chart-2)]/30 bg-[color:var(--chart-2)]/10 text-xs">
Available
</Badge>
);
}
return <AlertTriangle className="h-4 w-4 text-amber-500 shrink-0" />;
return (
<Badge variant="outline" className="text-[color:var(--chart-4)] border-[color:var(--chart-4)]/30 bg-[color:var(--chart-4)]/10 text-xs">
Install needed
</Badge>
);
}
// Insufficient hardware: show note + skip only
// Insufficient hardware: show note but still allow enabling
if (voiceCapability && !voiceCapability.voiceTierSufficient) {
return (
<div className="flex flex-col gap-4">
<div className="flex items-start gap-3 rounded-lg border border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950/20 p-3">
<Info className="h-5 w-5 text-amber-500 shrink-0 mt-0.5" />
<div className="flex items-start gap-3 rounded-lg border border-[color:var(--chart-4)]/30 bg-[color:var(--chart-4)]/5 p-4">
<Info className="h-5 w-5 text-[color:var(--chart-4)] shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-amber-800 dark:text-amber-300">Hardware may not support voice</p>
<p className="text-xs text-amber-700 dark:text-amber-400 mt-1">
Voice features require at least 4GB free RAM. Your system currently has insufficient free memory for local voice processing.
<p className="text-sm font-medium">Limited hardware for voice</p>
<p className="text-sm text-muted-foreground mt-1">
Voice features require at least 4GB free RAM. Transcription may be slower on your system.
</p>
</div>
</div>
<div className="flex flex-col gap-2">
<Button onClick={onEnable} variant="outline" className="w-full">
Enable voice anyway
</Button>
<Button variant="ghost" onClick={onSkip} className="w-full">
Skip voice setup
</Button>
@ -78,45 +97,55 @@ export function VoiceStep({ onEnable, onSkip, voiceCapability }: VoiceStepProps)
);
}
// Sufficient hardware or unknown show full UI
// Sufficient hardware or unknown -- show full UI
const neitherBinaryFound =
voiceCapability &&
!voiceCapability.whisperAvailable &&
!voiceCapability.piperAvailable;
// CPU-only note
const isCpuOnly = voiceCapability && !voiceCapability.whisperAvailable && !voiceCapability.piperAvailable;
return (
<div className="flex flex-col gap-4">
{/* Install note when tier is sufficient but binaries are missing */}
{neitherBinaryFound && (
<div className="flex items-start gap-3 rounded-lg border border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950/20 p-3">
<Info className="h-5 w-5 text-blue-500 shrink-0 mt-0.5" />
<p className="text-xs text-blue-700 dark:text-blue-400">
<div className="flex items-start gap-3 rounded-lg border border-primary/20 bg-primary/5 p-4">
<Info className="h-5 w-5 text-primary shrink-0 mt-0.5" />
<p className="text-sm text-muted-foreground">
Install whisper-cpp and piper for local voice features. You can enable voice now and configure binaries later.
</p>
</div>
)}
{/* CPU-only note */}
{voiceCapability?.voiceTierSufficient && !voiceCapability.whisperAvailable && (
<p className="text-sm text-muted-foreground">
Works on CPU -- transcription may take a few extra seconds.
</p>
)}
<div className="flex flex-col gap-3">
<div className="flex items-center gap-3 rounded-lg border p-3">
<div className="flex items-center gap-3 rounded-lg border border-border p-4">
<Mic className="h-5 w-5 text-primary shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium">Speech-to-Text (Whisper)</p>
<p className="text-xs text-muted-foreground">
<p className="text-xs text-muted-foreground mt-0.5">
{whisperStatusLabel()}
</p>
</div>
{whisperStatusIcon()}
{whisperBadge()}
</div>
<div className="flex items-center gap-3 rounded-lg border p-3">
<div className="flex items-center gap-3 rounded-lg border border-border p-4">
<Volume2 className="h-5 w-5 text-primary shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium">Text-to-Speech (Piper)</p>
<p className="text-xs text-muted-foreground">
<p className="text-xs text-muted-foreground mt-0.5">
{piperStatusLabel()}
</p>
</div>
{piperStatusIcon()}
{piperBadge()}
</div>
</div>

View file

@ -658,21 +658,21 @@ function TranscriptToolCard({
: "Completed";
const statusTone =
block.status === "running"
? "text-cyan-700 dark:text-cyan-300"
? "text-primary"
: block.status === "error"
? "text-red-700 dark:text-red-300"
: "text-emerald-700 dark:text-emerald-300";
? "text-destructive"
: "text-success";
const detailsClass = cn(
"space-y-3",
block.status === "error" && "rounded-xl border border-red-500/20 bg-red-500/[0.06] p-3",
block.status === "error" && "rounded-xl border border-destructive/20 bg-destructive/[0.06] p-3",
);
const iconClass = cn(
"mt-0.5 h-3.5 w-3.5 shrink-0",
block.status === "error"
? "text-red-600 dark:text-red-300"
? "text-destructive"
: block.status === "completed"
? "text-emerald-600 dark:text-emerald-300"
: "text-cyan-600 dark:text-cyan-300",
? "text-success"
: "text-primary",
);
const summary = block.status === "running"
? summarizeToolInput(block.name, block.input, density)
@ -681,7 +681,7 @@ function TranscriptToolCard({
: summarizeToolResult(block.result, block.isError, density);
return (
<div className={cn(block.status === "error" && "rounded-xl border border-red-500/20 bg-red-500/[0.04] p-3")}>
<div className={cn(block.status === "error" && "rounded-xl border border-destructive/20 bg-destructive/[0.04] p-3")}>
<div className="flex items-start gap-2">
{block.status === "error" ? (
<CircleAlert className={iconClass} />
@ -730,7 +730,7 @@ function TranscriptToolCard({
</div>
<pre className={cn(
"overflow-x-auto whitespace-pre-wrap break-words font-mono text-[11px]",
block.status === "error" ? "text-red-700 dark:text-red-300" : "text-foreground/80",
block.status === "error" ? "text-destructive" : "text-foreground/80",
)}>
{block.result ? formatToolPayload(block.result) : "Waiting for result..."}
</pre>
@ -771,11 +771,11 @@ function TranscriptCommandGroup({
? summarizeToolInput("command_execution", runningItem.input, density)
: null;
const statusTone = isRunning
? "text-cyan-700 dark:text-cyan-300"
? "text-primary"
: "text-foreground/70";
return (
<div className={cn(showExpandedErrorState && "rounded-xl border border-red-500/20 bg-red-500/[0.04] p-3")}>
<div className={cn(showExpandedErrorState && "rounded-xl border border-destructive/20 bg-destructive/[0.04] p-3")}>
<div
role="button"
tabIndex={0}
@ -799,7 +799,7 @@ function TranscriptCommandGroup({
"inline-flex h-6 w-6 items-center justify-center rounded-full border shadow-sm",
index > 0 && "-ml-1.5",
isRunning
? "border-cyan-500/25 bg-cyan-500/[0.08] text-cyan-600 dark:text-cyan-300"
? "border-primary/25 bg-primary/[0.08] text-primary"
: "border-border/70 bg-background text-foreground/55",
isRunning && "animate-pulse",
)}
@ -839,16 +839,16 @@ function TranscriptCommandGroup({
</button>
</div>
{open && (
<div className={cn("mt-3 space-y-3", hasError && "rounded-xl border border-red-500/20 bg-red-500/[0.06] p-3")}>
<div className={cn("mt-3 space-y-3", hasError && "rounded-xl border border-destructive/20 bg-destructive/[0.06] p-3")}>
{block.items.map((item, index) => (
<div key={`${item.ts}-${index}`} className="space-y-2">
<div className="flex items-center gap-2">
<span className={cn(
"inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full border",
item.status === "error"
? "border-red-500/25 bg-red-500/[0.08] text-red-600 dark:text-red-300"
? "border-destructive/25 bg-destructive/[0.08] text-destructive"
: item.status === "running"
? "border-cyan-500/25 bg-cyan-500/[0.08] text-cyan-600 dark:text-cyan-300"
? "border-primary/25 bg-primary/[0.08] text-primary"
: "border-border/70 bg-background text-foreground/55",
)}>
<TerminalSquare className="h-3 w-3" />
@ -860,7 +860,7 @@ function TranscriptCommandGroup({
{item.result && (
<pre className={cn(
"overflow-x-auto whitespace-pre-wrap break-words font-mono text-[11px]",
item.status === "error" ? "text-red-700 dark:text-red-300" : "text-foreground/80",
item.status === "error" ? "text-destructive" : "text-foreground/80",
)}>
{formatToolPayload(item.result)}
</pre>
@ -899,7 +899,7 @@ function TranscriptToolGroup({
? summarizeToolInput(runningItem.name, runningItem.input, density)
: null;
const statusTone = isRunning
? "text-cyan-700 dark:text-cyan-300"
? "text-primary"
: "text-foreground/70";
return (
@ -922,9 +922,9 @@ function TranscriptToolGroup({
"inline-flex h-6 w-6 items-center justify-center rounded-full border shadow-sm",
index > 0 && "-ml-1.5",
isItemRunning
? "border-cyan-500/25 bg-cyan-500/[0.08] text-cyan-600 dark:text-cyan-300"
? "border-primary/25 bg-primary/[0.08] text-primary"
: isItemError
? "border-red-500/25 bg-red-500/[0.08] text-red-600 dark:text-red-300"
? "border-destructive/25 bg-destructive/[0.08] text-destructive"
: "border-border/70 bg-background text-foreground/55",
isItemRunning && "animate-pulse",
)}
@ -961,9 +961,9 @@ function TranscriptToolGroup({
<span className={cn(
"inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full border",
item.status === "error"
? "border-red-500/25 bg-red-500/[0.08] text-red-600 dark:text-red-300"
? "border-destructive/25 bg-destructive/[0.08] text-destructive"
: item.status === "running"
? "border-cyan-500/25 bg-cyan-500/[0.08] text-cyan-600 dark:text-cyan-300"
? "border-primary/25 bg-primary/[0.08] text-primary"
: "border-border/70 bg-background text-foreground/55",
)}>
<Wrench className="h-3 w-3" />
@ -972,9 +972,9 @@ function TranscriptToolGroup({
{humanizeLabel(item.name)}
</span>
<span className={cn("text-[10px] font-semibold uppercase tracking-[0.14em]",
item.status === "running" ? "text-cyan-700 dark:text-cyan-300"
: item.status === "error" ? "text-red-700 dark:text-red-300"
: "text-emerald-700 dark:text-emerald-300"
item.status === "running" ? "text-primary"
: item.status === "error" ? "text-destructive"
: "text-success"
)}>
{item.status === "running" ? "Running" : item.status === "error" ? "Errored" : "Completed"}
</span>
@ -991,7 +991,7 @@ function TranscriptToolGroup({
<div className="mb-0.5 text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">Result</div>
<pre className={cn(
"overflow-x-auto whitespace-pre-wrap break-words font-mono text-[11px]",
item.status === "error" ? "text-red-700 dark:text-red-300" : "text-foreground/80",
item.status === "error" ? "text-destructive" : "text-foreground/80",
)}>
{formatToolPayload(item.result)}
</pre>
@ -1016,11 +1016,11 @@ function TranscriptActivityRow({
return (
<div className="flex items-start gap-2">
{block.status === "completed" ? (
<Check className="mt-0.5 h-3.5 w-3.5 shrink-0 text-emerald-600 dark:text-emerald-300" />
<Check className="mt-0.5 h-3.5 w-3.5 shrink-0 text-success" />
) : (
<span className="relative mt-1 flex h-2.5 w-2.5 shrink-0">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-cyan-400 opacity-70" />
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-cyan-500" />
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-70" />
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-primary" />
</span>
)}
<div className={cn(
@ -1043,11 +1043,11 @@ function TranscriptEventRow({
const compact = density === "compact";
const toneClasses =
block.tone === "error"
? "rounded-xl border border-red-500/20 bg-red-500/[0.06] p-3 text-red-700 dark:text-red-300"
? "rounded-xl border border-destructive/20 bg-destructive/[0.06] p-3 text-destructive"
: block.tone === "warn"
? "text-amber-700 dark:text-amber-300"
? "text-warning"
: block.tone === "info"
? "text-sky-700 dark:text-sky-300"
? "text-primary"
: "text-foreground/75";
return (
@ -1062,7 +1062,7 @@ function TranscriptEventRow({
)}
<div className="min-w-0 flex-1">
{block.label === "result" && block.tone !== "error" ? (
<div className={cn("whitespace-pre-wrap break-words text-sky-700 dark:text-sky-300", compact ? "text-[11px]" : "text-xs")}>
<div className={cn("whitespace-pre-wrap break-words text-primary", compact ? "text-[11px]" : "text-xs")}>
{block.text}
</div>
) : (
@ -1094,7 +1094,7 @@ function TranscriptStderrGroup({
const [open, setOpen] = useState(false);
const compact = density === "compact";
return (
<div className="rounded-xl border border-amber-500/20 bg-amber-500/[0.06] p-2 text-amber-700 dark:text-amber-300">
<div className="rounded-xl border border-warning/20 bg-warning/[0.06] p-2 text-warning">
<div
role="button"
tabIndex={0}
@ -1108,10 +1108,10 @@ function TranscriptStderrGroup({
{open ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
</div>
{open && (
<pre className="mt-2 overflow-x-auto whitespace-pre-wrap break-words font-mono text-[11px] text-amber-700/80 dark:text-amber-300/80 pl-5">
<pre className="mt-2 overflow-x-auto whitespace-pre-wrap break-words font-mono text-[11px] text-warning pl-5">
{block.lines.map((line, i) => (
<span key={`${line.ts}-${i}`}>
<span className="select-none text-amber-500/50 dark:text-amber-400/40">{i > 0 ? "\n" : ""}</span>
<span className="select-none text-warning">{i > 0 ? "\n" : ""}</span>
{line.text}
</span>
))}

View file

@ -99,12 +99,12 @@ import {
} from "../lib/agent-skills-state";
const runStatusIcons: Record<string, { icon: typeof CheckCircle2; color: string }> = {
succeeded: { icon: CheckCircle2, color: "text-green-600 dark:text-green-400" },
failed: { icon: XCircle, color: "text-red-600 dark:text-red-400" },
running: { icon: Loader2, color: "text-cyan-600 dark:text-cyan-400" },
queued: { icon: Clock, color: "text-yellow-600 dark:text-yellow-400" },
timed_out: { icon: Timer, color: "text-orange-600 dark:text-orange-400" },
cancelled: { icon: Slash, color: "text-neutral-500 dark:text-neutral-400" },
succeeded: { icon: CheckCircle2, color: "text-success" },
failed: { icon: XCircle, color: "text-destructive" },
running: { icon: Loader2, color: "text-primary" },
queued: { icon: Clock, color: "text-warning" },
timed_out: { icon: Timer, color: "text-warning" },
cancelled: { icon: Slash, color: "text-muted-foreground" },
};
const REDACTED_ENV_VALUE = "***REDACTED***";
@ -324,13 +324,13 @@ function workspaceOperationPhaseLabel(phase: WorkspaceOperation["phase"]) {
function workspaceOperationStatusTone(status: WorkspaceOperation["status"]) {
switch (status) {
case "succeeded":
return "border-green-500/20 bg-green-500/10 text-green-700 dark:text-green-300";
return "border-success/20 bg-success/10 text-success";
case "failed":
return "border-red-500/20 bg-red-500/10 text-red-700 dark:text-red-300";
return "border-destructive/20 bg-destructive/10 text-destructive";
case "running":
return "border-cyan-500/20 bg-cyan-500/10 text-cyan-700 dark:text-cyan-300";
return "border-primary/20 bg-primary/10 text-primary";
case "skipped":
return "border-yellow-500/20 bg-yellow-500/10 text-yellow-700 dark:text-yellow-300";
return "border-warning/20 bg-warning/10 text-warning";
default:
return "border-border bg-muted/40 text-muted-foreground";
}
@ -390,19 +390,19 @@ function WorkspaceOperationLogViewer({
<div className="text-xs text-muted-foreground">No persisted log lines.</div>
)}
{chunks.length > 0 && (
<div className="max-h-64 overflow-y-auto rounded bg-neutral-100 p-2 font-mono text-xs dark:bg-neutral-950">
<div className="max-h-64 overflow-y-auto rounded bg-muted p-2 font-mono text-xs">
{chunks.map((chunk, index) => (
<div key={`${chunk.ts}-${index}`} className="flex gap-2">
<span className="shrink-0 text-neutral-500">
<span className="shrink-0 text-muted-foreground">
{new Date(chunk.ts).toLocaleTimeString("en-US", { hour12: false })}
</span>
<span
className={cn(
"shrink-0 w-14",
chunk.stream === "stderr"
? "text-red-600 dark:text-red-300"
? "text-destructive"
: chunk.stream === "system"
? "text-blue-600 dark:text-blue-300"
? "text-primary"
: "text-muted-foreground",
)}
>
@ -488,8 +488,8 @@ function WorkspaceOperationsSection({
)}
{operation.stderrExcerpt && operation.stderrExcerpt.trim() && (
<div>
<div className="mb-1 text-xs text-red-700 dark:text-red-300">stderr excerpt</div>
<pre className="rounded-md bg-red-50 p-2 text-xs whitespace-pre-wrap break-all text-red-800 dark:bg-neutral-950 dark:text-red-100">
<div className="mb-1 text-xs text-destructive">stderr excerpt</div>
<pre className="rounded-md bg-destructive/10 p-2 text-xs whitespace-pre-wrap break-all text-destructive">
{redactPathText(operation.stderrExcerpt, censorUsernameInLogs)}
</pre>
</div>
@ -497,7 +497,7 @@ function WorkspaceOperationsSection({
{operation.stdoutExcerpt && operation.stdoutExcerpt.trim() && (
<div>
<div className="mb-1 text-xs text-muted-foreground">stdout excerpt</div>
<pre className="rounded-md bg-neutral-100 p-2 text-xs whitespace-pre-wrap break-all dark:bg-neutral-950">
<pre className="rounded-md bg-muted p-2 text-xs whitespace-pre-wrap break-all">
{redactPathText(operation.stdoutExcerpt, censorUsernameInLogs)}
</pre>
</div>
@ -849,13 +849,13 @@ export function AgentDetail() {
{mobileLiveRun && (
<Link
to={`/agents/${canonicalAgentRef}/runs/${mobileLiveRun.id}`}
className="sm:hidden flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-blue-500/10 hover:bg-blue-500/20 transition-colors no-underline"
className="sm:hidden flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-primary/10 hover:bg-primary/20 transition-colors no-underline"
>
<span className="relative flex h-2 w-2">
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500" />
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
</span>
<span className="text-[11px] font-medium text-blue-600 dark:text-blue-400">Live</span>
<span className="text-[11px] font-medium text-primary">Live</span>
</Link>
)}
@ -924,7 +924,7 @@ export function AgentDetail() {
{actionError && <p className="text-sm text-destructive">{actionError}</p>}
{isPendingApproval && (
<p className="text-sm text-amber-500">
<p className="text-sm text-warning">
This agent is pending board approval and cannot be invoked yet.
</p>
)}
@ -1074,7 +1074,7 @@ function LatestRunCard({ runs, agentId }: { runs: HeartbeatRun[]; agentId: strin
const liveRun = sorted.find((r) => r.status === "running" || r.status === "queued");
const run = liveRun ?? sorted[0];
const isLive = run.status === "running" || run.status === "queued";
const statusInfo = runStatusIcons[run.status] ?? { icon: Clock, color: "text-neutral-400" };
const statusInfo = runStatusIcons[run.status] ?? { icon: Clock, color: "text-muted-foreground" };
const StatusIcon = statusInfo.icon;
const summary = run.resultJson
? String((run.resultJson as Record<string, unknown>).summary ?? (run.resultJson as Record<string, unknown>).result ?? "")
@ -1086,8 +1086,8 @@ function LatestRunCard({ runs, agentId }: { runs: HeartbeatRun[]; agentId: strin
<h3 className="flex items-center gap-2 text-sm font-medium">
{isLive && (
<span className="relative flex h-2 w-2">
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-cyan-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-cyan-400" />
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
</span>
)}
{isLive ? "Live Run" : "Latest Run"}
@ -1104,7 +1104,7 @@ function LatestRunCard({ runs, agentId }: { runs: HeartbeatRun[]; agentId: strin
to={`/agents/${agentId}/runs/${run.id}`}
className={cn(
"block border rounded-lg p-4 space-y-2 w-full no-underline transition-colors hover:bg-muted/50 cursor-pointer",
isLive ? "border-cyan-500/30 shadow-[0_0_12px_rgba(6,182,212,0.08)]" : "border-border"
isLive ? "border-primary/30 shadow-[0_0_12px_rgba(6,182,212,0.08)]" : "border-border"
)}
>
<div className="flex items-center gap-2">
@ -1113,9 +1113,9 @@ function LatestRunCard({ runs, agentId }: { runs: HeartbeatRun[]; agentId: strin
<span className="font-mono text-xs text-muted-foreground">{run.id.slice(0, 8)}</span>
<span className={cn(
"inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-medium",
run.invocationSource === "timer" ? "bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300"
: run.invocationSource === "assignment" ? "bg-violet-100 text-violet-700 dark:bg-violet-900/50 dark:text-violet-300"
: run.invocationSource === "on_demand" ? "bg-cyan-100 text-cyan-700 dark:bg-cyan-900/50 dark:text-cyan-300"
run.invocationSource === "timer" ? "bg-primary/10 text-primary"
: run.invocationSource === "assignment" ? "bg-muted text-primary"
: run.invocationSource === "on_demand" ? "bg-primary/10 text-primary"
: "bg-muted text-muted-foreground"
)}>
{sourceLabels[run.invocationSource] ?? run.invocationSource}
@ -1571,7 +1571,7 @@ function ConfigurationTab({
aria-checked={canCreateAgents}
className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors shrink-0 disabled:cursor-not-allowed disabled:opacity-50",
canCreateAgents ? "bg-green-600" : "bg-muted",
canCreateAgents ? "bg-success" : "bg-muted",
)}
onClick={() =>
updatePermissions.mutate({
@ -1603,7 +1603,7 @@ function ConfigurationTab({
aria-checked={canAssignTasks}
className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors shrink-0 disabled:cursor-not-allowed disabled:opacity-50",
canAssignTasks ? "bg-green-600" : "bg-muted",
canAssignTasks ? "bg-success" : "bg-muted",
)}
onClick={() =>
updatePermissions.mutate({
@ -1955,7 +1955,7 @@ function PromptsTab({
{(bundle?.warnings ?? []).length > 0 && (
<div className="space-y-2">
{(bundle?.warnings ?? []).map((warning) => (
<div key={warning} className="rounded-md border border-sky-500/25 bg-sky-500/10 px-3 py-2 text-xs text-sky-100">
<div key={warning} className="rounded-md border border-primary/25 bg-primary/10 px-3 py-2 text-xs text-primary">
{warning}
</div>
))}
@ -2222,7 +2222,7 @@ function PromptsTab({
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="ml-3 shrink-0 rounded border border-amber-500/40 bg-amber-500/10 text-amber-200 px-1.5 py-0.5 text-[10px] uppercase tracking-wide cursor-help">
<span className="ml-3 shrink-0 rounded border border-warning/40 bg-warning/10 text-warning px-1.5 py-0.5 text-[10px] uppercase tracking-wide cursor-help">
virtual file
</span>
</TooltipTrigger>
@ -2592,7 +2592,7 @@ function AgentSkillsTab({
</div>
{skillSnapshot?.warnings.length ? (
<div className="space-y-1 rounded-xl border border-amber-300/60 bg-amber-50/60 px-4 py-3 text-sm text-amber-800 dark:border-amber-500/30 dark:bg-amber-950/20 dark:text-amber-200">
<div className="space-y-1 rounded-xl border border-warning/60 bg-warning/60 px-4 py-3 text-sm text-warning">
{skillSnapshot.warnings.map((warning) => (
<div key={warning}>{warning}</div>
))}
@ -2745,7 +2745,7 @@ function AgentSkillsTab({
})()}
{desiredOnlyMissingSkills.length > 0 && (
<div className="rounded-xl border border-amber-300/60 bg-amber-50/60 px-4 py-3 text-sm text-amber-800 dark:border-amber-500/30 dark:bg-amber-950/20 dark:text-amber-200">
<div className="rounded-xl border border-warning/60 bg-warning/60 px-4 py-3 text-sm text-warning">
<div className="font-medium">Requested skills missing from the company library</div>
<div className="mt-1 text-xs">
{desiredOnlyMissingSkills.join(", ")}
@ -2784,7 +2784,7 @@ function AgentSkillsTab({
/* ---- Runs Tab ---- */
function RunListItem({ run, isSelected, agentId }: { run: HeartbeatRun; isSelected: boolean; agentId: string }) {
const statusInfo = runStatusIcons[run.status] ?? { icon: Clock, color: "text-neutral-400" };
const statusInfo = runStatusIcons[run.status] ?? { icon: Clock, color: "text-muted-foreground" };
const StatusIcon = statusInfo.icon;
const metrics = runMetrics(run);
const summary = run.resultJson
@ -2806,9 +2806,9 @@ function RunListItem({ run, isSelected, agentId }: { run: HeartbeatRun; isSelect
</span>
<span className={cn(
"inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-medium shrink-0",
run.invocationSource === "timer" ? "bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300"
: run.invocationSource === "assignment" ? "bg-violet-100 text-violet-700 dark:bg-violet-900/50 dark:text-violet-300"
: run.invocationSource === "on_demand" ? "bg-cyan-100 text-cyan-700 dark:bg-cyan-900/50 dark:text-cyan-300"
run.invocationSource === "timer" ? "bg-primary/10 text-primary"
: run.invocationSource === "assignment" ? "bg-muted text-primary"
: run.invocationSource === "on_demand" ? "bg-primary/10 text-primary"
: "bg-muted text-muted-foreground"
)}>
{sourceLabels[run.invocationSource] ?? run.invocationSource}
@ -3138,7 +3138,7 @@ function RunDetail({ run: initialRun, agentRouteId, adapterType }: { run: Heartb
)}
{run.error && (
<div className="text-xs">
<span className="text-red-600 dark:text-red-400">{run.error}</span>
<span className="text-destructive">{run.error}</span>
{run.errorCode && <span className="text-muted-foreground ml-1">({run.errorCode})</span>}
</div>
)}
@ -3165,7 +3165,7 @@ function RunDetail({ run: initialRun, agentRouteId, adapterType }: { run: Heartb
Login URL:
<a
href={claudeLoginResult.loginUrl}
className="text-blue-600 underline underline-offset-2 ml-1 break-all dark:text-blue-400"
className="text-primary underline underline-offset-2 ml-1 break-all"
target="_blank"
rel="noreferrer"
>
@ -3176,12 +3176,12 @@ function RunDetail({ run: initialRun, agentRouteId, adapterType }: { run: Heartb
{claudeLoginResult && (
<>
{!!claudeLoginResult.stdout && (
<pre className="bg-neutral-100 dark:bg-neutral-950 rounded-md p-3 text-xs font-mono text-foreground overflow-x-auto whitespace-pre-wrap">
<pre className="bg-muted rounded-md p-3 text-xs font-mono text-foreground overflow-x-auto whitespace-pre-wrap">
{claudeLoginResult.stdout}
</pre>
)}
{!!claudeLoginResult.stderr && (
<pre className="bg-neutral-100 dark:bg-neutral-950 rounded-md p-3 text-xs font-mono text-red-700 dark:text-red-300 overflow-x-auto whitespace-pre-wrap">
<pre className="bg-muted rounded-md p-3 text-xs font-mono text-destructive overflow-x-auto whitespace-pre-wrap">
{claudeLoginResult.stderr}
</pre>
)}
@ -3190,7 +3190,7 @@ function RunDetail({ run: initialRun, agentRouteId, adapterType }: { run: Heartb
</div>
)}
{hasNonZeroExit && (
<div className="text-xs text-red-600 dark:text-red-400">
<div className="text-xs text-destructive">
Exit code {run.exitCode}
{run.signal && <span className="text-muted-foreground ml-1">(signal: {run.signal})</span>}
</div>
@ -3229,7 +3229,7 @@ function RunDetail({ run: initialRun, agentRouteId, adapterType }: { run: Heartb
>
<ChevronRight className={cn("h-3 w-3 transition-transform", sessionOpen && "rotate-90")} />
Session
{sessionChanged && <span className="text-yellow-400 ml-1">(changed)</span>}
{sessionChanged && <span className="text-warning ml-1">(changed)</span>}
</button>
{sessionOpen && (
<div className="px-4 pb-3 space-y-1 text-xs">
@ -3304,8 +3304,8 @@ function RunDetail({ run: initialRun, agentRouteId, adapterType }: { run: Heartb
{/* stderr excerpt for failed runs */}
{run.stderrExcerpt && (
<div className="space-y-1">
<span className="text-xs font-medium text-red-600 dark:text-red-400">stderr</span>
<pre className="bg-neutral-100 dark:bg-neutral-950 rounded-md p-3 text-xs font-mono text-red-700 dark:text-red-300 overflow-x-auto whitespace-pre-wrap">{run.stderrExcerpt}</pre>
<span className="text-xs font-medium text-destructive">stderr</span>
<pre className="bg-muted rounded-md p-3 text-xs font-mono text-destructive overflow-x-auto whitespace-pre-wrap">{run.stderrExcerpt}</pre>
</div>
)}
@ -3313,7 +3313,7 @@ function RunDetail({ run: initialRun, agentRouteId, adapterType }: { run: Heartb
{run.stdoutExcerpt && !run.logRef && (
<div className="space-y-1">
<span className="text-xs font-medium text-muted-foreground">stdout</span>
<pre className="bg-neutral-100 dark:bg-neutral-950 rounded-md p-3 text-xs font-mono text-foreground overflow-x-auto whitespace-pre-wrap">{run.stdoutExcerpt}</pre>
<pre className="bg-muted rounded-md p-3 text-xs font-mono text-foreground overflow-x-auto whitespace-pre-wrap">{run.stdoutExcerpt}</pre>
</div>
)}
@ -3716,14 +3716,14 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
const levelColors: Record<string, string> = {
info: "text-foreground",
warn: "text-yellow-600 dark:text-yellow-400",
error: "text-red-600 dark:text-red-400",
warn: "text-warning",
error: "text-destructive",
};
const streamColors: Record<string, string> = {
stdout: "text-foreground",
stderr: "text-red-600 dark:text-red-300",
system: "text-blue-600 dark:text-blue-300",
stderr: "text-destructive",
system: "text-primary",
};
return (
@ -3771,7 +3771,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
{adapterInvokePayload.prompt !== undefined && (
<div>
<div className="text-xs text-muted-foreground mb-1">Prompt</div>
<pre className="bg-neutral-100 dark:bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap">
<pre className="bg-muted rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap">
{typeof adapterInvokePayload.prompt === "string"
? redactPathText(adapterInvokePayload.prompt, censorUsernameInLogs)
: JSON.stringify(redactPathValue(adapterInvokePayload.prompt, censorUsernameInLogs), null, 2)}
@ -3781,7 +3781,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
{adapterInvokePayload.context !== undefined && (
<div>
<div className="text-xs text-muted-foreground mb-1">Context</div>
<pre className="bg-neutral-100 dark:bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap">
<pre className="bg-muted rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap">
{JSON.stringify(redactPathValue(adapterInvokePayload.context, censorUsernameInLogs), null, 2)}
</pre>
</div>
@ -3789,7 +3789,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
{adapterInvokePayload.env !== undefined && (
<div>
<div className="text-xs text-muted-foreground mb-1">Environment</div>
<pre className="bg-neutral-100 dark:bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap font-mono">
<pre className="bg-muted rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap font-mono">
{formatEnvForDisplay(adapterInvokePayload.env, censorUsernameInLogs)}
</pre>
</div>
@ -3835,10 +3835,10 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
</Button>
)}
{isLive && (
<span className="flex items-center gap-1 text-xs text-cyan-400">
<span className="flex items-center gap-1 text-xs text-primary">
<span className="relative flex h-2 w-2">
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-cyan-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-cyan-400" />
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
</span>
Live
</span>
@ -3853,7 +3853,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
emptyMessage={run.logRef ? "Waiting for transcript..." : "No persisted transcript for this run."}
/>
{logError && (
<div className="mt-3 rounded-xl border border-red-500/20 bg-red-500/[0.06] px-3 py-2 text-xs text-red-700 dark:text-red-300">
<div className="mt-3 rounded-xl border border-destructive/20 bg-destructive/[0.06] px-3 py-2 text-xs text-destructive">
{logError}
</div>
)}
@ -3861,34 +3861,34 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
</div>
{(run.status === "failed" || run.status === "timed_out") && (
<div className="rounded-lg border border-red-300 dark:border-red-500/30 bg-red-50 dark:bg-red-950/20 p-3 space-y-2">
<div className="text-xs font-medium text-red-700 dark:text-red-300">Failure details</div>
<div className="rounded-lg border border-destructive/30 bg-destructive/10 p-3 space-y-2">
<div className="text-xs font-medium text-destructive">Failure details</div>
{run.error && (
<div className="text-xs text-red-600 dark:text-red-200">
<span className="text-red-700 dark:text-red-300">Error: </span>
<div className="text-xs text-destructive">
<span className="text-destructive">Error: </span>
{redactPathText(run.error, censorUsernameInLogs)}
</div>
)}
{run.stderrExcerpt && run.stderrExcerpt.trim() && (
<div>
<div className="text-xs text-red-700 dark:text-red-300 mb-1">stderr excerpt</div>
<pre className="bg-red-50 dark:bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap text-red-800 dark:text-red-100">
<div className="text-xs text-destructive mb-1">stderr excerpt</div>
<pre className="bg-destructive/10 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap text-destructive">
{redactPathText(run.stderrExcerpt, censorUsernameInLogs)}
</pre>
</div>
)}
{run.resultJson && (
<div>
<div className="text-xs text-red-700 dark:text-red-300 mb-1">adapter result JSON</div>
<pre className="bg-red-50 dark:bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap text-red-800 dark:text-red-100">
<div className="text-xs text-destructive mb-1">adapter result JSON</div>
<pre className="bg-destructive/10 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap text-destructive">
{JSON.stringify(redactPathValue(run.resultJson, censorUsernameInLogs), null, 2)}
</pre>
</div>
)}
{run.stdoutExcerpt && run.stdoutExcerpt.trim() && !run.resultJson && (
<div>
<div className="text-xs text-red-700 dark:text-red-300 mb-1">stdout excerpt</div>
<pre className="bg-red-50 dark:bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap text-red-800 dark:text-red-100">
<div className="text-xs text-destructive mb-1">stdout excerpt</div>
<pre className="bg-destructive/10 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap text-destructive">
{redactPathText(run.stdoutExcerpt, censorUsernameInLogs)}
</pre>
</div>
@ -3899,7 +3899,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
{events.length > 0 && (
<div>
<div className="mb-2 text-xs font-medium text-muted-foreground">Events ({events.length})</div>
<div className="bg-neutral-100 dark:bg-neutral-950 rounded-lg p-3 font-mono text-xs space-y-0.5">
<div className="bg-muted rounded-lg p-3 font-mono text-xs space-y-0.5">
{events.map((evt) => {
const color = evt.color
?? (evt.level ? levelColors[evt.level] : null)
@ -3908,10 +3908,10 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
return (
<div key={evt.id} className="flex gap-2">
<span className="text-neutral-400 dark:text-neutral-600 shrink-0 select-none w-16">
<span className="text-muted-foreground shrink-0 select-none w-16">
{new Date(evt.createdAt).toLocaleTimeString("en-US", { hour12: false })}
</span>
<span className={cn("shrink-0 w-14", evt.stream ? (streamColors[evt.stream] ?? "text-neutral-500") : "text-neutral-500")}>
<span className={cn("shrink-0 w-14", evt.stream ? (streamColors[evt.stream] ?? "text-muted-foreground") : "text-muted-foreground")}>
{evt.stream ? `[${evt.stream}]` : ""}
</span>
<span className={cn("break-all", color)}>
@ -3976,12 +3976,12 @@ function KeysTab({ agentId, companyId }: { agentId: string; companyId?: string }
<div className="space-y-6">
{/* New token banner */}
{newToken && (
<div className="border border-yellow-300 dark:border-yellow-600/40 bg-yellow-50 dark:bg-yellow-500/5 rounded-lg p-4 space-y-2">
<p className="text-sm font-medium text-yellow-700 dark:text-yellow-400">
<div className="border border-warning/30 bg-warning/10 rounded-lg p-4 space-y-2">
<p className="text-sm font-medium text-warning">
API key created copy it now, it will not be shown again.
</p>
<div className="flex items-center gap-2">
<code className="flex-1 bg-neutral-100 dark:bg-neutral-950 rounded px-3 py-1.5 text-xs font-mono text-green-700 dark:text-green-300 truncate">
<code className="flex-1 bg-muted rounded px-3 py-1.5 text-xs font-mono text-success truncate">
{tokenVisible ? newToken : newToken.replace(/./g, "•")}
</code>
<Button
@ -4000,7 +4000,7 @@ function KeysTab({ agentId, companyId }: { agentId: string; companyId?: string }
>
<Copy className="h-3.5 w-3.5" />
</Button>
{copied && <span className="text-xs text-green-400">Copied!</span>}
{copied && <span className="text-xs text-success">Copied!</span>}
</div>
<Button
variant="ghost"

View file

@ -401,14 +401,14 @@ function LiveRunIndicator({
return (
<Link
to={`/agents/${agentRef}/runs/${runId}`}
className="flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-blue-500/10 hover:bg-blue-500/20 transition-colors no-underline"
className="flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-primary/10 hover:bg-primary/20 transition-colors no-underline"
onClick={(e) => e.stopPropagation()}
>
<span className="relative flex h-2 w-2">
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500" />
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
</span>
<span className="text-[11px] font-medium text-blue-600 dark:text-blue-400">
<span className="text-[11px] font-medium text-primary">
Live{liveCount > 1 ? ` (${liveCount})` : ""}
</span>
</Link>

View file

@ -174,16 +174,16 @@ export function ApprovalDetail() {
return (
<div className="space-y-6 max-w-3xl">
{showApprovedBanner && (
<div className="border border-green-300 dark:border-green-700/40 bg-green-50 dark:bg-green-900/20 rounded-lg px-4 py-3 animate-in fade-in zoom-in-95 duration-300">
<div className="border border-success/30 bg-success/10 rounded-lg px-4 py-3 animate-in fade-in zoom-in-95 duration-300">
<div className="flex items-start justify-between gap-3">
<div className="flex items-start gap-2">
<div className="relative mt-0.5">
<CheckCircle2 className="h-4 w-4 text-green-600 dark:text-green-300" />
<Sparkles className="h-3 w-3 text-green-500 dark:text-green-200 absolute -right-2 -top-1 animate-pulse" />
<CheckCircle2 className="h-4 w-4 text-success" />
<Sparkles className="h-3 w-3 text-success absolute -right-2 -top-1 animate-pulse" />
</div>
<div>
<p className="text-sm text-green-800 dark:text-green-100 font-medium">Approval confirmed</p>
<p className="text-xs text-green-700 dark:text-green-200/90">
<p className="text-sm text-success font-medium">Approval confirmed</p>
<p className="text-xs text-success">
Requesting agent was notified to review this approval and linked issues.
</p>
</div>
@ -191,7 +191,7 @@ export function ApprovalDetail() {
<Button
size="sm"
variant="outline"
className="border-green-400 dark:border-green-600/50 text-green-800 dark:text-green-100 hover:bg-green-100 dark:hover:bg-green-900/30"
className="border-success/30 text-success hover:bg-success/10 hover:bg-success/30"
onClick={() => navigate(resolvedCta.to)}
>
{resolvedCta.label}
@ -266,7 +266,7 @@ export function ApprovalDetail() {
<>
<Button
size="sm"
className="bg-green-700 hover:bg-green-600 text-white"
className="bg-success hover:bg-success text-white"
onClick={() => approveMutation.mutate()}
disabled={approveMutation.isPending}
>

View file

@ -91,7 +91,7 @@ export function Approvals() {
{ value: "pending", label: <>Pending{pendingCount > 0 && (
<span className={cn(
"ml-1.5 rounded-full px-1.5 py-0.5 text-[10px] font-medium",
"bg-yellow-500/20 text-yellow-500"
"bg-warning/20 text-warning"
)}>
{pendingCount}
</span>

View file

@ -159,7 +159,7 @@ export function Companies() {
onClick={saveEdit}
disabled={editMutation.isPending}
>
<Check className="h-3.5 w-3.5 text-green-500" />
<Check className="h-3.5 w-3.5 text-success" />
</Button>
<Button variant="ghost" size="icon-xs" onClick={cancelEdit}>
<X className="h-3.5 w-3.5 text-muted-foreground" />
@ -171,9 +171,9 @@ export function Companies() {
<span
className={`inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium ${
company.status === "active"
? "bg-green-500/10 text-green-600 dark:text-green-400"
? "bg-success/10 text-success"
: company.status === "paused"
? "bg-yellow-500/10 text-yellow-600 dark:text-yellow-400"
? "bg-warning/10 text-warning"
: "bg-muted text-muted-foreground"
}`}
>

View file

@ -942,7 +942,7 @@ export function CompanyExport() {
{selectedCount} / {totalFiles} file{totalFiles === 1 ? "" : "s"} selected
</span>
{warnings.length > 0 && (
<span className="text-amber-500">
<span className="text-warning">
{warnings.length} warning{warnings.length === 1 ? "" : "s"}
</span>
)}
@ -962,9 +962,9 @@ export function CompanyExport() {
{/* Warnings */}
{warnings.length > 0 && (
<div className="mx-5 mt-3 rounded-md border border-amber-500/30 bg-amber-500/5 px-4 py-3">
<div className="mx-5 mt-3 rounded-md border border-warning/30 bg-warning/5 px-4 py-3">
{warnings.map((w) => (
<div key={w} className="text-xs text-amber-500">{w}</div>
<div key={w} className="text-xs text-warning">{w}</div>
))}
</div>
)}

View file

@ -104,10 +104,10 @@ function ensureMarkdownPath(p: string): string {
}
const ACTION_COLORS: Record<string, string> = {
create: "text-emerald-500 border-emerald-500/30",
update: "text-amber-500 border-amber-500/30",
overwrite: "text-red-500 border-red-500/30",
replace: "text-red-500 border-red-500/30",
create: "text-success border-success/30",
update: "text-warning border-warning/30",
overwrite: "text-destructive border-destructive/30",
replace: "text-destructive border-destructive/30",
skip: "text-muted-foreground border-border",
none: "text-muted-foreground border-border",
};
@ -163,7 +163,7 @@ function renderImportFileExtra(node: FileTreeNode, checked: boolean, renameMap:
return (
<span className="inline-flex items-center gap-1.5 shrink-0">
{renamedTo && checked && (
<span className="text-[10px] text-cyan-500 font-mono truncate max-w-[7rem]" title={renamedTo}>
<span className="text-[10px] text-primary font-mono truncate max-w-[7rem]" title={renamedTo}>
&rarr; {renamedTo}
</span>
)}
@ -222,7 +222,7 @@ function ImportPreviewPane({
<div className="min-w-0 flex items-center gap-2">
<span className="truncate font-mono text-sm">{selectedFile}</span>
{renamedTo && (
<span className="shrink-0 font-mono text-sm text-cyan-500">
<span className="shrink-0 font-mono text-sm text-primary">
&rarr; {renamedTo}
</span>
)}
@ -428,7 +428,7 @@ function ConflictResolutionList({
className={cn(
"flex items-center gap-3 px-4 py-2.5 text-sm",
isSkipped && "opacity-40",
isConfirmed && !isSkipped && "bg-emerald-500/5",
isConfirmed && !isSkipped && "bg-success/5",
)}
>
{/* Skip button on the left */}
@ -450,8 +450,8 @@ function ConflictResolutionList({
isSkipped
? "text-muted-foreground border-border"
: isConfirmed
? "text-emerald-500 border-emerald-500/30"
: "text-amber-500 border-amber-500/30",
? "text-success border-success/30"
: "text-warning border-warning/30",
)}>
{item.kind}
</span>
@ -467,7 +467,7 @@ function ConflictResolutionList({
<>
<ArrowRight className="h-3 w-3 shrink-0 text-muted-foreground" />
{isConfirmed ? (
<span className="min-w-0 flex-1 font-mono text-xs text-emerald-500">
<span className="min-w-0 flex-1 font-mono text-xs text-success">
{currentName}
</span>
) : (
@ -487,7 +487,7 @@ function ConflictResolutionList({
className={cn(
"ml-auto shrink-0 rounded-md border px-2.5 py-1 text-xs transition-colors inline-flex items-center gap-1.5",
isConfirmed
? "border-emerald-500/30 bg-emerald-500/10 text-emerald-500"
? "border-success/30 bg-success/10 text-success"
: "border-border text-muted-foreground hover:bg-accent/50",
)}
onClick={() => onToggleConfirm(item.slug)}
@ -565,7 +565,7 @@ function AdapterPickerList({
<div className="flex items-center gap-3 px-4 py-2.5 text-sm">
<span className={cn(
"shrink-0 rounded-full border px-2 py-0.5 text-[10px] uppercase tracking-wide",
"text-blue-500 border-blue-500/30",
"text-primary border-primary/30",
)}>
agent
</span>
@ -1252,7 +1252,7 @@ export function CompanyImport() {
{selectedCount} / {totalFiles} file{totalFiles === 1 ? "" : "s"} selected
</span>
{conflicts.length > 0 && (
<span className="text-amber-500">
<span className="text-warning">
{conflicts.length} conflict{conflicts.length === 1 ? "" : "s"}
</span>
)}
@ -1302,9 +1302,9 @@ export function CompanyImport() {
{/* Warnings */}
{importPreview.warnings.length > 0 && (
<div className="mx-5 mt-3 rounded-md border border-amber-500/30 bg-amber-500/5 px-4 py-3">
<div className="mx-5 mt-3 rounded-md border border-warning/30 bg-warning/5 px-4 py-3">
{importPreview.warnings.map((w) => (
<div key={w} className="text-xs text-amber-500">{w}</div>
<div key={w} className="text-xs text-warning">{w}</div>
))}
</div>
)}

View file

@ -345,7 +345,7 @@ export function CompanySettings() {
<div className="flex items-center gap-2">
<input
type="color"
value={brandColor || "#6366f1"}
value={brandColor || "var(--primary)"}
onChange={(e) => setBrandColor(e.target.value)}
className="h-8 w-8 cursor-pointer rounded border border-border bg-transparent p-0"
/>
@ -498,7 +498,7 @@ export function CompanySettings() {
{snippetCopied && (
<span
key={snippetCopyDelightId}
className="flex items-center gap-1 text-xs text-green-600 animate-pulse"
className="flex items-center gap-1 text-xs text-success animate-pulse"
>
<Check className="h-3 w-3" />
Copied

View file

@ -20,7 +20,7 @@ export function ContentStudio() {
const companyId = selectedCompanyId ?? "";
const themeJob = useContentJob(companyId);
const [showApplyDialog, setShowApplyDialog] = useState(false);
const [seedColor, setSeedColor] = useState("#4f46e5");
const [seedColor, setSeedColor] = useState("var(--primary)");
const [themeBundle, setThemeBundle] = useState<{
palette: PaletteRole[];
exports: { css: string; tailwind: string; vscode: string; json: string };

View file

@ -695,10 +695,10 @@ export function Costs() {
className={cn(
"h-full transition-[width,background-color] duration-150",
spendData.summary.utilizationPercent > 90
? "bg-red-400"
? "bg-destructive"
: spendData.summary.utilizationPercent > 70
? "bg-yellow-400"
: "bg-emerald-400",
? "bg-warning"
: "bg-success",
)}
style={{ width: `${Math.min(100, spendData.summary.utilizationPercent)}%` }}
/>

View file

@ -191,16 +191,16 @@ export function Dashboard() {
{error && <p className="text-sm text-destructive">{error.message}</p>}
{hasNoAgents && (
<div className="flex items-center justify-between gap-3 rounded-md border border-amber-300 bg-amber-50 px-4 py-3 dark:border-amber-500/25 dark:bg-amber-950/60">
<div className="flex items-center justify-between gap-3 rounded-md border border-warning/30 bg-warning/10 px-4 py-3">
<div className="flex items-center gap-2.5">
<Bot className="h-4 w-4 text-amber-600 dark:text-amber-400 shrink-0" />
<p className="text-sm text-amber-900 dark:text-amber-100">
<Bot className="h-4 w-4 text-warning shrink-0" />
<p className="text-sm text-warning">
You have no agents.
</p>
</div>
<button
onClick={() => openOnboarding({ initialStep: 2, companyId: selectedCompanyId! })}
className="text-sm font-medium text-amber-700 hover:text-amber-900 dark:text-amber-300 dark:hover:text-amber-100 underline underline-offset-2 shrink-0"
className="text-sm font-medium text-warning hover:text-warning hover:text-warning underline underline-offset-2 shrink-0"
>
Create one here
</button>
@ -212,19 +212,19 @@ export function Dashboard() {
{data && (
<>
{data.budgets.activeIncidents > 0 ? (
<div className="flex items-start justify-between gap-3 rounded-xl border border-red-500/20 bg-[linear-gradient(180deg,rgba(255,80,80,0.12),rgba(255,255,255,0.02))] px-4 py-3">
<div className="flex items-start justify-between gap-3 rounded-xl border border-destructive/20 bg-[linear-gradient(180deg,rgba(255,80,80,0.12),rgba(255,255,255,0.02))] px-4 py-3">
<div className="flex items-start gap-2.5">
<PauseCircle className="mt-0.5 h-4 w-4 shrink-0 text-red-300" />
<PauseCircle className="mt-0.5 h-4 w-4 shrink-0 text-destructive" />
<div>
<p className="text-sm font-medium text-red-50">
<p className="text-sm font-medium text-destructive">
{data.budgets.activeIncidents} active budget incident{data.budgets.activeIncidents === 1 ? "" : "s"}
</p>
<p className="text-xs text-red-100/70">
<p className="text-xs text-destructive">
{data.budgets.pausedAgents} agents paused · {data.budgets.pausedProjects} projects paused · {data.budgets.pendingApprovals} pending budget approvals
</p>
</div>
</div>
<Link to="/costs" className="text-sm underline underline-offset-2 text-red-100">
<Link to="/costs" className="text-sm underline underline-offset-2 text-destructive">
Open budgets
</Link>
</div>

View file

@ -461,9 +461,9 @@ export function DesignGuide() {
<SubSection title="Run invocation badges">
<div className="flex items-center gap-2 flex-wrap">
{[
["timer", "bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300"],
["assignment", "bg-violet-100 text-violet-700 dark:bg-violet-900/50 dark:text-violet-300"],
["on_demand", "bg-cyan-100 text-cyan-700 dark:bg-cyan-900/50 dark:text-cyan-300"],
["timer", "bg-primary/10 text-primary"],
["assignment", "bg-muted text-primary"],
["on_demand", "bg-primary/10 text-primary"],
["automation", "bg-muted text-muted-foreground"],
].map(([label, cls]) => (
<span key={label} className={`rounded-full px-1.5 py-0.5 text-[10px] font-medium ${cls}`}>
@ -1033,9 +1033,9 @@ export function DesignGuide() {
<Section title="Progress Bars (Budget)">
<div className="space-y-3">
{[
{ label: "Under budget (40%)", pct: 40, color: "bg-green-400" },
{ label: "Warning (75%)", pct: 75, color: "bg-yellow-400" },
{ label: "Over budget (95%)", pct: 95, color: "bg-red-400" },
{ label: "Under budget (40%)", pct: 40, color: "bg-success" },
{ label: "Warning (75%)", pct: 75, color: "bg-warning" },
{ label: "Over budget (95%)", pct: 95, color: "bg-destructive" },
].map(({ label, pct, color }) => (
<div key={label} className="space-y-1">
<div className="flex items-center justify-between">
@ -1057,20 +1057,20 @@ export function DesignGuide() {
{/* LOG VIEWER */}
{/* ============================================================ */}
<Section title="Log Viewer">
<div className="bg-neutral-950 rounded-lg p-3 font-mono text-xs max-h-80 overflow-y-auto">
<div className="bg-muted rounded-lg p-3 font-mono text-xs max-h-80 overflow-y-auto">
<div className="text-foreground">[12:00:01] INFO Agent started successfully</div>
<div className="text-foreground">[12:00:02] INFO Processing task PAP-001</div>
<div className="text-yellow-400">[12:00:05] WARN Rate limit approaching (80%)</div>
<div className="text-warning">[12:00:05] WARN Rate limit approaching (80%)</div>
<div className="text-foreground">[12:00:08] INFO Task PAP-001 completed</div>
<div className="text-red-400">[12:00:12] ERROR Connection timeout to upstream service</div>
<div className="text-blue-300">[12:00:12] SYS Retrying connection in 5s...</div>
<div className="text-destructive">[12:00:12] ERROR Connection timeout to upstream service</div>
<div className="text-primary">[12:00:12] SYS Retrying connection in 5s...</div>
<div className="text-foreground">[12:00:17] INFO Reconnected successfully</div>
<div className="flex items-center gap-1.5">
<span className="relative flex h-1.5 w-1.5">
<span className="absolute inline-flex h-full w-full rounded-full bg-cyan-400 animate-pulse" />
<span className="inline-flex h-full w-full rounded-full bg-cyan-400" />
<span className="absolute inline-flex h-full w-full rounded-full bg-primary animate-pulse" />
<span className="inline-flex h-full w-full rounded-full bg-primary" />
</span>
<span className="text-cyan-400">Live</span>
<span className="text-primary">Live</span>
</div>
</div>
</Section>

View file

@ -384,7 +384,7 @@ export function ExecutionWorkspaceDetail() {
</Button>
<StatusPill>{workspace.mode}</StatusPill>
<StatusPill>{workspace.providerType}</StatusPill>
<StatusPill className={workspace.status === "active" ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300" : undefined}>
<StatusPill className={workspace.status === "active" ? "border-success/30 bg-success/10 text-success" : undefined}>
{workspace.status}
</StatusPill>
</div>

View file

@ -188,22 +188,22 @@ export function InboxIssueMetaLeading({
<span
className={cn(
"inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 sm:gap-1.5 sm:px-2",
"bg-blue-500/10",
"bg-primary/10",
)}
>
<span className="relative flex h-2 w-2">
<span className="absolute inline-flex h-full w-full animate-pulse rounded-full bg-blue-400 opacity-75" />
<span className="absolute inline-flex h-full w-full animate-pulse rounded-full bg-primary opacity-75" />
<span
className={cn(
"relative inline-flex h-2 w-2 rounded-full",
"bg-blue-500",
"bg-primary",
)}
/>
</span>
<span
className={cn(
"hidden text-[11px] font-medium sm:inline",
"text-blue-600 dark:text-blue-400",
"text-primary",
)}
>
Live
@ -286,7 +286,7 @@ export function InboxIssueTrailingColumns({
if (column === "project") {
if (projectName) {
const accentColor = projectColor ?? "#64748b";
const accentColor = projectColor ?? "var(--muted-foreground)";
return (
<span
key={column}
@ -407,13 +407,13 @@ export function FailedRunInboxRow({
onClick={onMarkRead}
className={cn(
"inline-flex h-4 w-4 items-center justify-center rounded-full transition-colors",
"hover:bg-blue-500/20",
"hover:bg-primary/20",
)}
aria-label="Mark as read"
>
<span className={cn(
"block h-2 w-2 rounded-full transition-opacity duration-300",
"bg-blue-600 dark:bg-blue-400",
"bg-primary",
unreadState === "fading" ? "opacity-0" : "opacity-100",
)} />
</button>
@ -441,8 +441,8 @@ export function FailedRunInboxRow({
>
{!showUnreadSlot && <span className="hidden h-2 w-2 shrink-0 sm:inline-flex" aria-hidden="true" />}
<span className="hidden h-3.5 w-3.5 shrink-0 sm:inline-flex" aria-hidden="true" />
<span className="mt-0.5 shrink-0 rounded-md bg-red-500/20 p-1.5 sm:mt-0">
<XCircle className="h-4 w-4 text-red-600 dark:text-red-400" />
<span className="mt-0.5 shrink-0 rounded-md bg-destructive/20 p-1.5 sm:mt-0">
<XCircle className="h-4 w-4 text-destructive" />
</span>
<span className="min-w-0 flex-1">
<span className="line-clamp-2 text-sm font-medium sm:truncate sm:line-clamp-none">
@ -563,13 +563,13 @@ function ApprovalInboxRow({
onClick={onMarkRead}
className={cn(
"inline-flex h-4 w-4 items-center justify-center rounded-full transition-colors",
"hover:bg-blue-500/20",
"hover:bg-primary/20",
)}
aria-label="Mark as read"
>
<span className={cn(
"block h-2 w-2 rounded-full transition-opacity duration-300",
"bg-blue-600 dark:bg-blue-400",
"bg-primary",
unreadState === "fading" ? "opacity-0" : "opacity-100",
)} />
</button>
@ -615,7 +615,7 @@ function ApprovalInboxRow({
<div className="hidden shrink-0 items-center gap-2 sm:flex">
<Button
size="sm"
className="h-8 bg-green-700 px-3 text-white hover:bg-green-600"
className="h-8 bg-success px-3 text-white hover:bg-success"
onClick={onApprove}
disabled={isPending}
>
@ -637,7 +637,7 @@ function ApprovalInboxRow({
<div className="mt-3 flex gap-2 sm:hidden">
<Button
size="sm"
className="h-8 bg-green-700 px-3 text-white hover:bg-green-600"
className="h-8 bg-success px-3 text-white hover:bg-success"
onClick={onApprove}
disabled={isPending}
>
@ -702,13 +702,13 @@ function JoinRequestInboxRow({
onClick={onMarkRead}
className={cn(
"inline-flex h-4 w-4 items-center justify-center rounded-full transition-colors",
"hover:bg-blue-500/20",
"hover:bg-primary/20",
)}
aria-label="Mark as read"
>
<span className={cn(
"block h-2 w-2 rounded-full transition-opacity duration-300",
"bg-blue-600 dark:bg-blue-400",
"bg-primary",
unreadState === "fading" ? "opacity-0" : "opacity-100",
)} />
</button>
@ -746,7 +746,7 @@ function JoinRequestInboxRow({
<div className="hidden shrink-0 items-center gap-2 sm:flex">
<Button
size="sm"
className="h-8 bg-green-700 px-3 text-white hover:bg-green-600"
className="h-8 bg-success px-3 text-white hover:bg-success"
onClick={onApprove}
disabled={isPending}
>
@ -766,7 +766,7 @@ function JoinRequestInboxRow({
<div className="mt-3 flex gap-2 sm:hidden">
<Button
size="sm"
className="h-8 bg-green-700 px-3 text-white hover:bg-green-600"
className="h-8 bg-success px-3 text-white hover:bg-success"
onClick={onApprove}
disabled={isPending}
>
@ -1775,8 +1775,8 @@ export function Inbox() {
if (showTodayDivider) {
elements.push(
<div key="today-divider" className="flex items-center gap-3 px-4 my-2">
<div className="flex-1 border-t border-zinc-600" />
<span className="shrink-0 text-[11px] font-medium uppercase tracking-wider text-zinc-500">
<div className="flex-1 border-t border-border" />
<span className="shrink-0 text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
Earlier
</span>
</div>,
@ -1979,7 +1979,7 @@ export function Inbox() {
to="/agents"
className="flex flex-1 cursor-pointer items-center gap-3 no-underline text-inherit"
>
<AlertTriangle className="h-4 w-4 shrink-0 text-red-600 dark:text-red-400" />
<AlertTriangle className="h-4 w-4 shrink-0 text-destructive" />
<span className="text-sm">
<span className="font-medium">{dashboard!.agents.error}</span>{" "}
{dashboard!.agents.error === 1 ? "agent has" : "agents have"} errors
@ -2001,7 +2001,7 @@ export function Inbox() {
to="/costs"
className="flex flex-1 cursor-pointer items-center gap-3 no-underline text-inherit"
>
<AlertTriangle className="h-4 w-4 shrink-0 text-yellow-400" />
<AlertTriangle className="h-4 w-4 shrink-0 text-warning" />
<span className="text-sm">
Budget at{" "}
<span className="font-medium">{dashboard!.costs.monthUtilizationPercent}%</span>{" "}

View file

@ -89,7 +89,7 @@ export function InstanceExperimentalSettings() {
disabled={toggleMutation.isPending}
className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-60",
enableIsolatedWorkspaces ? "bg-green-600" : "bg-muted",
enableIsolatedWorkspaces ? "bg-success" : "bg-muted",
)}
onClick={() => toggleMutation.mutate({ enableIsolatedWorkspaces: !enableIsolatedWorkspaces })}
>
@ -119,7 +119,7 @@ export function InstanceExperimentalSettings() {
disabled={toggleMutation.isPending}
className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-60",
autoRestartDevServerWhenIdle ? "bg-green-600" : "bg-muted",
autoRestartDevServerWhenIdle ? "bg-success" : "bg-muted",
)}
onClick={() =>
toggleMutation.mutate({ autoRestartDevServerWhenIdle: !autoRestartDevServerWhenIdle })

View file

@ -123,7 +123,7 @@ export function InstanceGeneralSettings() {
disabled={updateGeneralMutation.isPending}
className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-60",
censorUsernameInLogs ? "bg-green-600" : "bg-muted",
censorUsernameInLogs ? "bg-success" : "bg-muted",
)}
onClick={() =>
updateGeneralMutation.mutate({
@ -157,7 +157,7 @@ export function InstanceGeneralSettings() {
disabled={updateGeneralMutation.isPending}
className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-60",
keyboardShortcuts ? "bg-green-600" : "bg-muted",
keyboardShortcuts ? "bg-success" : "bg-muted",
)}
onClick={() => updateGeneralMutation.mutate({ keyboardShortcuts: !keyboardShortcuts })}
>

View file

@ -218,7 +218,7 @@ export function InviteLandingPage() {
<p className="font-medium text-foreground">Connectivity diagnostics</p>
{diagnostics.map((diag, idx) => (
<div key={`${diag.code}:${idx}`} className="space-y-0.5">
<p className={diag.level === "warn" ? "text-amber-600 dark:text-amber-400" : undefined}>
<p className={diag.level === "warn" ? "text-warning" : undefined}>
[{diag.level}] {diag.message}
</p>
{diag.hint && <p className="font-mono break-all">{diag.hint}</p>}

View file

@ -1177,10 +1177,10 @@ export function IssueDetail() {
<span className="text-sm font-mono text-muted-foreground shrink-0">{issue.identifier ?? issue.id.slice(0, 8)}</span>
{hasLiveRuns && (
<span className="inline-flex items-center gap-1.5 rounded-full bg-cyan-500/10 border border-cyan-500/30 px-2 py-0.5 text-[10px] font-medium text-cyan-600 dark:text-cyan-400 shrink-0">
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary/10 border border-primary/30 px-2 py-0.5 text-[10px] font-medium text-primary shrink-0">
<span className="relative flex h-1.5 w-1.5">
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-cyan-400 opacity-75" />
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-cyan-400" />
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-primary" />
</span>
Live
</span>
@ -1189,7 +1189,7 @@ export function IssueDetail() {
{issue.originKind === "routine_execution" && issue.originId && (
<Link
to={`/routines/${issue.originId}`}
className="inline-flex items-center gap-1 rounded-full bg-violet-500/10 border border-violet-500/30 px-2 py-0.5 text-[10px] font-medium text-violet-600 dark:text-violet-400 shrink-0 hover:bg-violet-500/20 transition-colors"
className="inline-flex items-center gap-1 rounded-full bg-primary/10 border border-primary/30 px-2 py-0.5 text-[10px] font-medium text-primary shrink-0 hover:bg-primary/20 transition-colors"
>
<Repeat className="h-3 w-3" />
Routine
@ -1239,7 +1239,7 @@ export function IssueDetail() {
onClick={copyIssueToClipboard}
title="Copy issue as markdown"
>
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
{copied ? <Check className="h-4 w-4 text-success" /> : <Copy className="h-4 w-4" />}
</Button>
<Button
variant="ghost"
@ -1258,7 +1258,7 @@ export function IssueDetail() {
onClick={copyIssueToClipboard}
title="Copy issue as markdown"
>
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
{copied ? <Check className="h-4 w-4 text-success" /> : <Copy className="h-4 w-4" />}
</Button>
<Button
variant="ghost"

View file

@ -69,14 +69,14 @@ function OrgTreeNode({
className={cn(
"h-2 w-2 rounded-full shrink-0",
node.status === "active"
? "bg-green-400"
? "bg-success"
: node.status === "paused"
? "bg-yellow-400"
? "bg-warning"
: node.status === "pending_approval"
? "bg-amber-400"
? "bg-warning"
: node.status === "error"
? "bg-red-400"
: "bg-neutral-400"
? "bg-destructive"
: "bg-muted"
)}
/>
<span className="font-medium flex-1">{node.name}</span>

View file

@ -129,15 +129,17 @@ const adapterLabels: Record<string, string> = {
http: "HTTP",
};
// [nexus] Design system migration Phase 3 — status colors now reference
// semantic CSS variables so they auto-switch with light/dark themes.
const statusDotColor: Record<string, string> = {
running: "#22d3ee",
active: "#4ade80",
paused: "#facc15",
idle: "#facc15",
error: "#f87171",
terminated: "#a3a3a3",
running: "var(--primary)",
active: "var(--success)",
paused: "var(--warning)",
idle: "var(--muted-foreground)",
error: "var(--destructive)",
terminated: "var(--muted-foreground)",
};
const defaultDotColor = "#a3a3a3";
const defaultDotColor = "var(--muted-foreground)";
// ── Main component ──────────────────────────────────────────────────────

View file

@ -205,9 +205,9 @@ export function PluginManager() {
</Dialog>
</div>
<div className="rounded-lg border border-amber-500/30 bg-amber-500/5 px-4 py-3">
<div className="rounded-lg border border-warning/30 bg-warning/5 px-4 py-3">
<div className="flex items-start gap-3">
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-700" />
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-warning" />
<div className="space-y-1 text-sm">
<p className="font-medium text-foreground">Plugins are alpha.</p>
<p className="text-muted-foreground">
@ -251,7 +251,7 @@ export function PluginManager() {
{installedPlugin ? (
<Badge
variant={installedPlugin.status === "ready" ? "default" : "secondary"}
className={installedPlugin.status === "ready" ? "bg-green-600 hover:bg-green-700" : ""}
className={installedPlugin.status === "ready" ? "bg-success hover:bg-success" : ""}
>
{installedPlugin.status}
</Badge>
@ -347,15 +347,15 @@ export function PluginManager() {
{plugin.manifestJson.description || "No description provided."}
</p>
{plugin.status === "error" && (
<div className="mt-3 rounded-md border border-red-500/25 bg-red-500/[0.06] px-3 py-2">
<div className="mt-3 rounded-md border border-destructive/25 bg-destructive/[0.06] px-3 py-2">
<div className="flex flex-wrap items-start gap-3">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 text-sm font-medium text-red-700 dark:text-red-300">
<div className="flex items-center gap-2 text-sm font-medium text-destructive">
<AlertTriangle className="h-4 w-4 shrink-0" />
<span>Plugin error</span>
</div>
<p
className="mt-1 text-sm text-red-700/90 dark:text-red-200/90 break-words"
className="mt-1 text-sm text-destructive break-words"
title={plugin.lastError ?? undefined}
>
{errorSummaryByPluginId.get(plugin.id)}
@ -364,7 +364,7 @@ export function PluginManager() {
<Button
variant="outline"
size="sm"
className="border-red-500/30 bg-background/60 text-red-700 hover:bg-red-500/10 hover:text-red-800 dark:text-red-200 dark:hover:text-red-100"
className="border-destructive/30 bg-background/60 text-destructive hover:bg-destructive/10 hover:text-destructive hover:text-destructive"
onClick={() => setErrorDetailsPlugin(plugin)}
>
View full error
@ -386,7 +386,7 @@ export function PluginManager() {
}
className={cn(
"shrink-0",
plugin.status === "ready" ? "bg-green-600 hover:bg-green-700" : ""
plugin.status === "ready" ? "bg-success hover:bg-success" : ""
)}
>
{plugin.status}
@ -405,7 +405,7 @@ export function PluginManager() {
}}
disabled={enableMutation.isPending || disableMutation.isPending}
>
<Power className={cn("h-4 w-4", plugin.status === "ready" ? "text-green-600" : "")} />
<Power className={cn("h-4 w-4", plugin.status === "ready" ? "text-success" : "")} />
</Button>
<Button
variant="outline"
@ -478,14 +478,14 @@ export function PluginManager() {
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="rounded-md border border-red-500/25 bg-red-500/[0.06] px-4 py-3">
<div className="rounded-md border border-destructive/25 bg-destructive/[0.06] px-4 py-3">
<div className="flex items-start gap-3">
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-red-700 dark:text-red-300" />
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-destructive" />
<div className="space-y-1 text-sm">
<p className="font-medium text-red-700 dark:text-red-300">
<p className="font-medium text-destructive">
What errored
</p>
<p className="text-red-700/90 dark:text-red-200/90 break-words">
<p className="text-destructive break-words">
{errorDetailsPlugin ? getPluginErrorSummary(errorDetailsPlugin) : "No error summary available."}
</p>
</div>

View file

@ -290,7 +290,7 @@ export function PluginSettings() {
<>
<div className="flex justify-between col-span-2">
<span className="text-muted-foreground flex items-center gap-1">
<AlertTriangle className="h-3 w-3 text-amber-500" />
<AlertTriangle className="h-3 w-3 text-warning" />
Crashes
</span>
<span className="text-xs">
@ -409,7 +409,7 @@ export function PluginSettings() {
entry.level === "error"
? "text-destructive"
: entry.level === "warn"
? "text-yellow-600 dark:text-yellow-400"
? "text-warning"
: entry.level === "debug"
? "text-muted-foreground/60"
: "text-muted-foreground"
@ -454,7 +454,7 @@ export function PluginSettings() {
{check.name}
</span>
{check.passed ? (
<CheckCircle className="h-4 w-4 shrink-0 text-green-500" />
<CheckCircle className="h-4 w-4 shrink-0 text-success" />
) : (
<XCircle className="h-4 w-4 shrink-0 text-destructive" />
)}
@ -682,7 +682,7 @@ function PluginConfigForm({ pluginId, schema, initialValues, isLoading, pluginSt
<div
className={`text-sm p-2 rounded border ${
saveMessage.type === "success"
? "text-green-700 bg-green-50 border-green-200 dark:text-green-400 dark:bg-green-950/30 dark:border-green-900"
? "text-success bg-success/10 border-success/30"
: "text-destructive bg-destructive/10 border-destructive/20"
}`}
>
@ -694,7 +694,7 @@ function PluginConfigForm({ pluginId, schema, initialValues, isLoading, pluginSt
<div
className={`text-sm p-2 rounded border ${
testResult.type === "success"
? "text-green-700 bg-green-50 border-green-200 dark:text-green-400 dark:bg-green-950/30 dark:border-green-900"
? "text-success bg-success/10 border-success/30"
: "text-destructive bg-destructive/10 border-destructive/20"
}`}
>
@ -800,14 +800,14 @@ function formatTimestamp(epochMs: number): string {
function JobStatusDot({ status }: { status: string }) {
const colorClass =
status === "success" || status === "succeeded"
? "bg-green-500"
? "bg-success"
: status === "failed"
? "bg-red-500"
? "bg-destructive"
: status === "running"
? "bg-blue-500 animate-pulse"
? "bg-primary animate-pulse"
: status === "cancelled"
? "bg-gray-400"
: "bg-amber-500"; // queued, pending
? "bg-muted"
: "bg-warning"; // queued, pending
return (
<span
className={`inline-block h-2 w-2 rounded-full shrink-0 ${colorClass}`}
@ -822,12 +822,12 @@ function JobStatusDot({ status }: { status: string }) {
function DeliveryStatusDot({ status }: { status: string }) {
const colorClass =
status === "processed" || status === "success"
? "bg-green-500"
? "bg-success"
: status === "failed"
? "bg-red-500"
? "bg-destructive"
: status === "received"
? "bg-blue-500"
: "bg-amber-500"; // pending
? "bg-primary"
: "bg-warning"; // pending
return (
<span
className={`inline-block h-2 w-2 rounded-full shrink-0 ${colorClass}`}

View file

@ -287,8 +287,8 @@ function ProjectWorkspacesContent({
<div className="flex shrink-0 items-center gap-2 text-xs text-muted-foreground">
{summary.serviceCount > 0 ? (
<span className={`inline-flex items-center gap-1 ${hasRunningServices ? "text-emerald-500" : ""}`}>
<span className={`inline-block h-1.5 w-1.5 rounded-full ${hasRunningServices ? "bg-emerald-500" : "bg-muted-foreground/40"}`} />
<span className={`inline-flex items-center gap-1 ${hasRunningServices ? "text-success" : ""}`}>
<span className={`inline-block h-1.5 w-1.5 rounded-full ${hasRunningServices ? "bg-success" : "bg-muted-foreground/40"}`} />
{summary.runningServiceCount}/{summary.serviceCount}
</span>
) : null}
@ -410,7 +410,7 @@ function ProjectWorkspacesContent({
<div className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">
Cleanup attention needed
</div>
<div className="overflow-hidden rounded-xl border border-amber-500/20 bg-amber-500/5">
<div className="overflow-hidden rounded-xl border border-warning/20 bg-warning/5">
{cleanupFailedSummaries.map(renderSummaryRow)}
</div>
</div>
@ -796,7 +796,7 @@ export function ProjectDetail() {
<div className="flex items-start gap-3">
<div className="h-7 flex items-center">
<ColorPicker
currentColor={project.color ?? "#6366f1"}
currentColor={project.color ?? "var(--primary)"}
onSelect={(color) => updateProject.mutate({ color })}
/>
</div>
@ -808,8 +808,8 @@ export function ProjectDetail() {
className="text-xl font-bold"
/>
{project.pauseReason === "budget" ? (
<div className="inline-flex items-center gap-2 rounded-full border border-red-500/30 bg-red-500/10 px-3 py-1 text-[11px] font-medium uppercase tracking-[0.18em] text-red-200">
<span className="h-2 w-2 rounded-full bg-red-400" />
<div className="inline-flex items-center gap-2 rounded-full border border-destructive/30 bg-destructive/10 px-3 py-1 text-[11px] font-medium uppercase tracking-[0.18em] text-destructive">
<span className="h-2 w-2 rounded-full bg-destructive" />
Paused by budget hard stop
</div>
) : null}

View file

@ -389,7 +389,7 @@ export function ProjectWorkspaceDetail() {
Make primary
</Button>
) : (
<div className="inline-flex items-center gap-2 rounded-xl border border-emerald-500/25 bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700 dark:text-emerald-300 sm:max-w-sm">
<div className="inline-flex items-center gap-2 rounded-xl border border-success/25 bg-success/10 px-3 py-2 text-sm text-success sm:max-w-sm">
<Sparkles className="h-4 w-4" />
This is the projects primary codebase workspace.
</div>

View file

@ -663,7 +663,7 @@ export function RoutineDetail() {
const automationLabelClassName = routine.status === "archived"
? "text-muted-foreground"
: automationEnabled
? "text-emerald-400"
? "text-success"
: "text-muted-foreground";
return (
@ -719,7 +719,7 @@ export function RoutineDetail() {
aria-label={automationEnabled ? "Pause automatic triggers" : "Enable automatic triggers"}
disabled={automationToggleDisabled}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
automationEnabled ? "bg-emerald-500" : "bg-muted"
automationEnabled ? "bg-success" : "bg-muted"
} ${automationToggleDisabled ? "cursor-not-allowed opacity-50" : ""}`}
onClick={() => updateRoutineStatus.mutate(automationEnabled ? "paused" : "active")}
>
@ -737,7 +737,7 @@ export function RoutineDetail() {
{/* Secret message banner */}
{secretMessage && (
<div className="rounded-lg border border-blue-500/30 bg-blue-500/5 p-4 space-y-3 text-sm">
<div className="rounded-lg border border-primary/30 bg-primary/5 p-4 space-y-3 text-sm">
<div>
<p className="font-medium">{secretMessage.title}</p>
<p className="text-xs text-muted-foreground">{`Save this now. ${VOCAB.appName} will not show the secret value again.`}</p>
@ -825,7 +825,7 @@ export function RoutineDetail() {
<>
<span
className="h-3.5 w-3.5 shrink-0 rounded-sm"
style={{ backgroundColor: currentProject.color ?? "#64748b" }}
style={{ backgroundColor: currentProject.color ?? "var(--muted-foreground)" }}
/>
<span className="truncate">{option.label}</span>
</>
@ -840,7 +840,7 @@ export function RoutineDetail() {
<>
<span
className="h-3.5 w-3.5 shrink-0 rounded-sm"
style={{ backgroundColor: project?.color ?? "#64748b" }}
style={{ backgroundColor: project?.color ?? "var(--muted-foreground)" }}
/>
<span className="truncate">{option.label}</span>
</>
@ -920,7 +920,7 @@ export function RoutineDetail() {
{/* Save bar */}
<div className="flex items-center justify-between">
{isEditDirty ? (
<span className="text-xs text-amber-600">Unsaved changes</span>
<span className="text-xs text-warning">Unsaved changes</span>
) : (
<span />
)}
@ -945,7 +945,7 @@ export function RoutineDetail() {
<TabsTrigger value="runs" className="gap-1.5">
<Play className="h-3.5 w-3.5" />
Runs
{hasLiveRun && <span className="h-2 w-2 rounded-full bg-blue-500 animate-pulse" />}
{hasLiveRun && <span className="h-2 w-2 rounded-full bg-primary animate-pulse" />}
</TabsTrigger>
<TabsTrigger value="activity" className="gap-1.5">
<ActivityIcon className="h-3.5 w-3.5" />

View file

@ -282,7 +282,7 @@ export function Routines() {
<div className="space-y-1">
<h1 className="text-2xl font-semibold tracking-tight flex items-center gap-2">
Routines
<span className="rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-800 dark:bg-amber-900/30 dark:text-amber-400">Beta</span>
<span className="rounded-full bg-warning/10 px-2 py-0.5 text-xs font-medium text-warning">Beta</span>
</h1>
<p className="text-sm text-muted-foreground">
Recurring work definitions that materialize into auditable execution issues.
@ -425,7 +425,7 @@ export function Routines() {
<>
<span
className="h-3.5 w-3.5 shrink-0 rounded-sm"
style={{ backgroundColor: currentProject.color ?? "#64748b" }}
style={{ backgroundColor: currentProject.color ?? "var(--muted-foreground)" }}
/>
<span className="truncate">{option.label}</span>
</>
@ -440,7 +440,7 @@ export function Routines() {
<>
<span
className="h-3.5 w-3.5 shrink-0 rounded-sm"
style={{ backgroundColor: project?.color ?? "#64748b" }}
style={{ backgroundColor: project?.color ?? "var(--muted-foreground)" }}
/>
<span className="truncate">{option.label}</span>
</>
@ -610,7 +610,7 @@ export function Routines() {
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span
className="shrink-0 h-3 w-3 rounded-sm"
style={{ backgroundColor: projectById.get(routine.projectId)?.color ?? "#6366f1" }}
style={{ backgroundColor: projectById.get(routine.projectId)?.color ?? "var(--primary)" }}
/>
<span className="truncate">{projectById.get(routine.projectId)?.name ?? "Unknown"}</span>
</div>

View file

@ -97,9 +97,9 @@ function LiveWidgetPreview({
density: TranscriptDensity;
}) {
return (
<div className="overflow-hidden rounded-xl border border-cyan-500/25 bg-background/85 shadow-[0_20px_50px_rgba(6,182,212,0.10)]">
<div className="border-b border-border/60 bg-cyan-500/[0.05] px-5 py-4">
<div className="text-xs font-semibold uppercase tracking-[0.2em] text-cyan-700 dark:text-cyan-300">
<div className="overflow-hidden rounded-xl border border-primary/25 bg-background/85 shadow-[0_20px_50px_rgba(6,182,212,0.10)]">
<div className="border-b border-border/60 bg-primary/[0.05] px-5 py-4">
<div className="text-xs font-semibold uppercase tracking-[0.2em] text-primary">
Live Runs
</div>
<div className="mt-1 text-xs text-muted-foreground">
@ -151,7 +151,7 @@ function DashboardPreview({
<div className={cn(
"flex h-[320px] flex-col overflow-hidden rounded-xl border shadow-[0_20px_40px_rgba(15,23,42,0.10)]",
streaming
? "border-cyan-500/25 bg-cyan-500/[0.04]"
? "border-primary/25 bg-primary/[0.04]"
: "border-border bg-background/75",
)}>
<div className="border-b border-border/60 px-4 py-4">
@ -160,7 +160,7 @@ function DashboardPreview({
<div className="flex items-center gap-2">
<span className={cn(
"inline-flex h-2.5 w-2.5 rounded-full",
streaming ? "bg-cyan-500 shadow-[0_0_0_6px_rgba(34,211,238,0.12)]" : "bg-muted-foreground/35",
streaming ? "bg-primary shadow-[0_0_0_6px_rgba(34,211,238,0.12)]" : "bg-muted-foreground/35",
)} />
<Identity name={runTranscriptFixtureMeta.agentName} size="sm" />
</div>
@ -172,7 +172,7 @@ function DashboardPreview({
<ExternalLink className="h-2.5 w-2.5" />
</span>
</div>
<div className="mt-3 rounded-lg border border-border/60 bg-background/60 px-3 py-2 text-xs text-cyan-700 dark:text-cyan-300">
<div className="mt-3 rounded-lg border border-border/60 bg-background/60 px-3 py-2 text-xs text-primary">
{runTranscriptFixtureMeta.issueIdentifier} - {runTranscriptFixtureMeta.issueTitle}
</div>
</div>
@ -204,7 +204,7 @@ export function RunTranscriptUxLab() {
<div className="grid gap-6 lg:grid-cols-[260px_minmax(0,1fr)]">
<aside className="border-b border-border/60 bg-background/75 p-5 lg:border-b-0 lg:border-r">
<div className="mb-5">
<div className="inline-flex items-center gap-2 rounded-full border border-cyan-500/25 bg-cyan-500/[0.08] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.24em] text-cyan-700 dark:text-cyan-300">
<div className="inline-flex items-center gap-2 rounded-full border border-primary/25 bg-primary/[0.08] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.24em] text-primary">
<FlaskConical className="h-3.5 w-3.5" />
UX Lab
</div>
@ -225,12 +225,12 @@ export function RunTranscriptUxLab() {
className={cn(
"w-full rounded-xl border px-4 py-3 text-left transition-all",
selectedSurface === option.id
? "border-cyan-500/35 bg-cyan-500/[0.10] shadow-[0_12px_24px_rgba(6,182,212,0.12)]"
: "border-border/70 bg-background/70 hover:border-cyan-500/20 hover:bg-cyan-500/[0.04]",
? "border-primary/35 bg-primary/[0.10] shadow-[0_12px_24px_rgba(6,182,212,0.12)]"
: "border-border/70 bg-background/70 hover:border-primary/20 hover:bg-primary/[0.04]",
)}
>
<div className="flex items-start gap-3">
<span className="rounded-lg border border-current/15 p-2 text-cyan-700 dark:text-cyan-300">
<span className="rounded-lg border border-current/15 p-2 text-primary">
<Icon className="h-4 w-4" />
</span>
<span className="min-w-0">

View file

@ -58,8 +58,8 @@ function VersionDiff({ oldContent, newContent }: { oldContent: string; newConten
<span
key={i}
className={cn(
part.added && "bg-green-500/20 text-green-700 dark:text-green-400",
part.removed && "bg-red-500/20 text-red-700 dark:text-red-400",
part.added && "bg-success/20 text-success",
part.removed && "bg-destructive/20 text-destructive",
!part.added && !part.removed && "text-muted-foreground",
)}
>
@ -343,7 +343,7 @@ export function SkillDetail() {
<span className="flex items-center gap-1 text-sm font-semibold">
{skill.averageRating != null ? (
<>
<Star className="h-3.5 w-3.5 fill-amber-400 text-amber-400" />
<Star className="h-3.5 w-3.5 fill-amber-400 text-warning" />
{skill.averageRating.toFixed(1)}
</>
) : (