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

View file

@ -8,21 +8,21 @@ const SURFACES = [
description: "Request-scoped usage and billed runs from cost_events.", description: "Request-scoped usage and billed runs from cost_events.",
icon: Database, icon: Database,
points: ["tokens + billed dollars", "provider, biller, model", "subscription and overage aware"], 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", title: "Finance ledger",
description: "Account-level charges that are not one prompt-response pair.", description: "Account-level charges that are not one prompt-response pair.",
icon: ReceiptText, icon: ReceiptText,
points: ["top-ups, refunds, fees", "Bedrock provisioned or training charges", "credit expiries and adjustments"], 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", title: "Live quotas",
description: "Provider or biller windows that can stop traffic in real time.", description: "Provider or biller windows that can stop traffic in real time.",
icon: Gauge, icon: Gauge,
points: ["provider quota windows", "biller credit systems", "errors surfaced directly"], 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; ] as const;

View file

@ -93,7 +93,7 @@ function AgentRunCard({
<div className={cn( <div className={cn(
"flex h-[320px] flex-col overflow-hidden rounded-xl border shadow-sm", "flex h-[320px] flex-col overflow-hidden rounded-xl border shadow-sm",
isActive 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", : "border-border bg-background/70",
)}> )}>
<div className="border-b border-border/60 px-3 py-3"> <div className="border-b border-border/60 px-3 py-3">
@ -102,8 +102,8 @@ function AgentRunCard({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{isActive ? ( {isActive ? (
<span className="relative flex h-2.5 w-2.5 shrink-0"> <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="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-cyan-500" /> <span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-primary" />
</span> </span>
) : ( ) : (
<span className="inline-flex h-2.5 w-2.5 rounded-full bg-muted-foreground/35" /> <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}`} to={`/issues/${issue?.identifier ?? run.issueId}`}
className={cn( className={cn(
"line-clamp-2 hover:underline", "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)} 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`}> <div key={day} className="flex-1 h-full flex flex-col justify-end" title={`${day}: ${total} runs`}>
{total > 0 ? ( {total > 0 ? (
<div className="flex flex-col-reverse gap-px overflow-hidden" style={{ height: `${heightPct}%`, minHeight: 2 }}> <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.succeeded > 0 && <div className="bg-success" style={{ flex: entry.succeeded }} />}
{entry.failed > 0 && <div className="bg-red-500" style={{ flex: entry.failed }} />} {entry.failed > 0 && <div className="bg-destructive" style={{ flex: entry.failed }} />}
{entry.other > 0 && <div className="bg-neutral-500" style={{ flex: entry.other }} />} {entry.other > 0 && <div className="bg-muted" style={{ flex: entry.other }} />}
</div> </div>
) : ( ) : (
<div className="bg-muted/30 rounded-sm" style={{ height: 2 }} /> <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> = { const priorityColors: Record<string, string> = {
critical: "#ef4444", critical: "var(--destructive)",
high: "#f97316", high: "var(--chart-4)",
medium: "#eab308", medium: "var(--chart-1)",
low: "#6b7280", low: "var(--muted-foreground)",
}; };
const priorityOrder = ["critical", "high", "medium", "low"] as const; 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> = { const statusColors: Record<string, string> = {
todo: "#3b82f6", todo: "var(--chart-1)",
in_progress: "#8b5cf6", in_progress: "var(--chart-3)",
in_review: "#a855f7", in_review: "var(--chart-4)",
done: "#10b981", done: "var(--chart-2)",
blocked: "#ef4444", blocked: "var(--destructive)",
cancelled: "#6b7280", cancelled: "var(--muted-foreground)",
backlog: "#64748b", backlog: "var(--muted-foreground)",
}; };
const statusLabels: Record<string, string> = { const statusLabels: Record<string, string> = {
@ -208,7 +208,7 @@ export function IssueStatusChart({ issues }: { issues: { status: string; created
{total > 0 ? ( {total > 0 ? (
<div className="flex flex-col-reverse gap-px overflow-hidden" style={{ height: `${heightPct}%`, minHeight: 2 }}> <div className="flex flex-col-reverse gap-px overflow-hidden" style={{ height: `${heightPct}%`, minHeight: 2 }}>
{statusOrder.map(s => (entry[s] ?? 0) > 0 ? ( {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)} ) : null)}
</div> </div>
) : ( ) : (
@ -219,7 +219,7 @@ export function IssueStatusChart({ issues }: { issues: { status: string; created
})} })}
</div> </div>
<DateLabels days={days} /> <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> </div>
); );
} }
@ -245,7 +245,7 @@ export function SuccessRateChart({ runs }: { runs: HeartbeatRun[] }) {
{days.map(day => { {days.map(day => {
const entry = grouped.get(day)!; const entry = grouped.get(day)!;
const rate = entry.total > 0 ? entry.succeeded / entry.total : 0; 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 ( 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})`}> <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 ? ( {entry.total > 0 ? (

View file

@ -550,7 +550,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
}} }}
/> />
</Field> </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. Prompt template is replayed on every heartbeat. Keep it compact and dynamic to avoid recurring token cost and cache churn.
</div> </div>
</> </>
@ -687,7 +687,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
}} }}
/> />
</Field> </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. 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> </div>
</> </>
@ -786,7 +786,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
{adapterType === "codex_local" && {adapterType === "codex_local" &&
codexSearchEnabled && codexSearchEnabled &&
currentThinkingEffort === "minimal" && ( currentThinkingEffort === "minimal" && (
<p className="text-xs text-amber-400"> <p className="text-xs text-warning">
Codex may reject `minimal` thinking when search is enabled. Codex may reject `minimal` thinking when search is enabled.
</p> </p>
)} )}
@ -813,7 +813,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
}} }}
/> />
</Field> </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. 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> </div>
</> </>
@ -993,10 +993,10 @@ function AdapterEnvironmentResult({ result }: { result: AdapterEnvironmentTestRe
result.status === "pass" ? "Passed" : result.status === "warn" ? "Warnings" : "Failed"; result.status === "pass" ? "Passed" : result.status === "warn" ? "Warnings" : "Failed";
const statusClass = const statusClass =
result.status === "pass" 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" : 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-warning border-warning/30 bg-warning/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-destructive border-destructive/30 bg-destructive/10";
return ( return (
<div className={`rounded-md border px-3 py-2 text-xs ${statusClass}`}> <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}> <span className="block w-full text-left truncate font-mono text-xs" title={value}>
{value} {value}
</span> </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 current
</span> </span>
</button> </button>
@ -1523,7 +1523,7 @@ function ModelDropdown({
<span className="block w-full text-left truncate font-mono text-xs" title={detectedModel}> <span className="block w-full text-left truncate font-mono text-xs" title={detectedModel}>
{detectedModel} {detectedModel}
</span> </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 detected
</span> </span>
</button> </button>

View file

@ -78,7 +78,7 @@ export function AgentProperties({ agent, runtimeState }: AgentPropertiesProps) {
)} )}
{runtimeState?.lastError && ( {runtimeState?.lastError && (
<PropertyRow label="Last error"> <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> </PropertyRow>
)} )}
{agent.lastHeartbeatAt && ( {agent.lastHeartbeatAt && (

View file

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

View file

@ -33,25 +33,25 @@ export function BudgetIncidentCard({
const parsed = parseDollarInput(draftAmount); const parsed = parseDollarInput(draftAmount);
return ( 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"> <CardHeader className="px-5 pt-5 pb-3">
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<div> <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 {incident.scopeType} hard stop
</div> </div>
<CardTitle className="mt-1 text-base text-red-50">{incident.scopeName}</CardTitle> <CardTitle className="mt-1 text-base text-destructive">{incident.scopeName}</CardTitle>
<CardDescription className="mt-1 text-red-100/70"> <CardDescription className="mt-1 text-destructive">
Spending reached {formatCents(incident.amountObserved)} against a limit of {formatCents(incident.amountLimit)}. Spending reached {formatCents(incident.amountObserved)} against a limit of {formatCents(incident.amountLimit)}.
</CardDescription> </CardDescription>
</div> </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" /> <AlertOctagon className="h-4 w-4" />
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4 px-5 pb-5 pt-0"> <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" /> <PauseCircle className="mt-0.5 h-4 w-4 shrink-0" />
<div> <div>
{incident.scopeType === "project" {incident.scopeType === "project"
@ -83,7 +83,7 @@ export function BudgetIncidentCard({
</Button> </Button>
</div> </div>
{parsed !== null && parsed <= incident.amountObserved ? ( {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. The new budget must exceed current observed spend.
</p> </p>
) : null} ) : null}

View file

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

View file

@ -5,7 +5,7 @@ export function BudgetSidebarMarker({ title = "Paused by budget" }: { title?: st
<span <span
title={title} title={title}
aria-label={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" /> <DollarSign className="h-3 w-3" />
</span> </span>

View file

@ -26,7 +26,7 @@ export function ChatMessageIdentityBar({
<AgentIcon icon={agentIcon} className={`h-4 w-4 ${colorClass}`} /> <AgentIcon icon={agentIcon} className={`h-4 w-4 ${colorClass}`} />
<span className={`text-[13px] font-semibold ${colorClass}`}>{agentName}</span> <span className={`text-[13px] font-semibold ${colorClass}`}>{agentName}</span>
{isStreaming && ( {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 && ( {timestamp && (
<span className="text-[11px] text-muted-foreground"> <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) => {segments.map((seg, i) =>
seg.highlight ? ( 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} {seg.text}
</mark> </mark>
) : ( ) : (

View file

@ -21,7 +21,7 @@ export function ChatStatusUpdateBadge({ agentName, taskId, taskTitle, taskUrl }:
)} )}
role="status" 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"> <span className="text-foreground">
{agentName} completed {taskId} {agentName} completed {taskId}
{displayTitle ? `: ${displayTitle}` : ""} {displayTitle ? `: ${displayTitle}` : ""}

View file

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

View file

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

View file

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

View file

@ -134,13 +134,13 @@ function SortableCompanyItem({
{hasLiveAgents && ( {hasLiveAgents && (
<span className="pointer-events-none absolute -right-0.5 -top-0.5 z-10"> <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="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="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-blue-500 ring-2 ring-background" /> <span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-primary ring-2 ring-background" />
</span> </span>
</span> </span>
)} )}
{hasUnreadInbox && ( {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> </div>
</a> </a>

View file

@ -15,13 +15,13 @@ import { Button } from "@/components/ui/button";
function statusDotColor(status?: string): string { function statusDotColor(status?: string): string {
switch (status) { switch (status) {
case "active": case "active":
return "bg-green-400"; return "bg-success";
case "paused": case "paused":
return "bg-yellow-400"; return "bg-warning";
case "archived": case "archived":
return "bg-neutral-400"; return "bg-muted";
default: 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); const sample = devServer.changedPathsSample.slice(0, 3);
return ( 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="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="min-w-0">
<div className="flex items-center gap-2 text-[12px] font-semibold uppercase tracking-[0.18em]"> <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" /> <AlertTriangle className="h-3.5 w-3.5 shrink-0" />
<span>Restart Required</span> <span>Restart Required</span>
{devServer.autoRestartEnabled ? ( {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 Auto-Restart On
</span> </span>
) : null} ) : null}
@ -49,7 +49,7 @@ export function DevRestartBanner({ devServer }: { devServer?: DevServerHealthSta
{describeReason(devServer)} {describeReason(devServer)}
{changedAt ? ` · updated ${changedAt}` : ""} {changedAt ? ` · updated ${changedAt}` : ""}
</p> </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 ? ( {sample.length > 0 ? (
<span> <span>
Changed: {sample.join(", ")} 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"> <div className="flex shrink-0 items-center gap-2 text-xs font-medium">
{devServer.waitingForIdle ? ( {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" /> <TimerReset className="h-3.5 w-3.5" />
<span>Waiting for {devServer.activeRunCount} live run{devServer.activeRunCount === 1 ? "" : "s"} to finish</span> <span>Waiting for {devServer.activeRunCount} live run{devServer.activeRunCount === 1 ? "" : "s"} to finish</span>
</div> </div>
) : devServer.autoRestartEnabled ? ( ) : 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" /> <RotateCcw className="h-3.5 w-3.5" />
<span>Auto-restart will trigger when the instance is idle</span> <span>Auto-restart will trigger when the instance is idle</span>
</div> </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" /> <RotateCcw className="h-3.5 w-3.5" />
<span>Restart <code>pnpm dev:once</code> after the active work is safe to interrupt</span> <span>Restart <code>pnpm dev:once</code> after the active work is safe to interrupt</span>
</div> </div>

View file

@ -30,9 +30,9 @@ function readinessTone(state: "ready" | "ready_with_warnings" | "blocked") {
return "border-destructive/30 bg-destructive/5 text-destructive"; return "border-destructive/30 bg-destructive/5 text-destructive";
} }
if (state === "ready_with_warnings") { 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({ export function ExecutionWorkspaceCloseDialog({
@ -163,7 +163,7 @@ export function ExecutionWorkspaceCloseDialog({
<h3 className="text-sm font-medium">Warnings</h3> <h3 className="text-sm font-medium">Warnings</h3>
<ul className="space-y-2 text-sm text-muted-foreground"> <ul className="space-y-2 text-sm text-muted-foreground">
{readiness.warnings.map((warning, idx) => ( {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} {warning}
</li> </li>
))} ))}
@ -262,7 +262,7 @@ export function ExecutionWorkspaceCloseDialog({
</section> </section>
{currentStatus === "cleanup_failed" ? ( {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 Cleanup previously failed on this workspace. Retrying close will rerun the cleanup flow and update the
workspace status if it succeeds. workspace status if it succeeds.
</div> </div>

View file

@ -59,7 +59,7 @@ export function FinanceTimelineCard({
<div className="text-right tabular-nums"> <div className="text-right tabular-nums">
<div className="text-sm font-semibold">{formatCents(row.amountCents)}</div> <div className="text-sm font-semibold">{formatCents(row.amountCents)}</div>
<div className="text-xs text-muted-foreground">{row.currency}</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> </div>
</div> </div>

View file

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

View file

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

View file

@ -116,14 +116,14 @@ export function IssueRow({
}} }}
className={cn( className={cn(
"inline-flex h-4 w-4 items-center justify-center rounded-full transition-colors", "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" aria-label="Mark as read"
> >
<span <span
className={cn( className={cn(
"block h-2 w-2 rounded-full transition-opacity duration-300", "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", unreadState === "fading" ? "opacity-0" : "opacity-100",
)} )}
/> />

View file

@ -87,7 +87,7 @@ function CopyableInline({ value, label, mono }: { value: string; label?: string;
onClick={handleCopy} onClick={handleCopy}
title={copied ? "Copied!" : "Copy"} 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> </button>
</span> </span>
); );
@ -144,9 +144,9 @@ function workspaceDetailLink(input: {
function statusBadge(status: string) { function statusBadge(status: string) {
const colors: Record<string, 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", 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", archived: "bg-muted text-muted-foreground",
}; };
return ( return (

View file

@ -400,7 +400,7 @@ export function IssuesList({
{/* Filter */} {/* Filter */}
<Popover> <Popover>
<PopoverTrigger asChild> <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" /> <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> <span className="hidden sm:inline">{activeFilterCount > 0 ? `Filters: ${activeFilterCount}` : "Filter"}</span>
{activeFilterCount > 0 && ( {activeFilterCount > 0 && (
@ -734,12 +734,12 @@ export function IssuesList({
{issue.identifier ?? issue.id.slice(0, 8)} {issue.identifier ?? issue.id.slice(0, 8)}
</span> </span>
{liveIssueIds?.has(issue.id) && ( {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="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="relative inline-flex h-2 w-2 rounded-full bg-blue-500" /> <span className="relative inline-flex h-2 w-2 rounded-full bg-primary" />
</span> </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 Live
</span> </span>
</span> </span>

View file

@ -154,8 +154,8 @@ function KanbanCard({
</span> </span>
{isLive && ( {isLive && (
<span className="relative flex h-2 w-2 shrink-0 mt-0.5"> <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="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-blue-500" /> <span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
</span> </span>
)} )}
</div> </div>

View file

@ -87,9 +87,9 @@ export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
if (runs.length === 0) return null; if (runs.length === 0) return null;
return ( 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="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-cyan-500/[0.04] px-4 py-3"> <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-cyan-700 dark:text-cyan-300"> <div className="text-xs font-semibold uppercase tracking-[0.18em] text-primary">
Live Runs Live Runs
</div> </div>
<div className="mt-1 text-xs text-muted-foreground"> <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"> <div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<Link <Link
to={`/agents/${run.agentId}/runs/${run.id}`} 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)} {run.id.slice(0, 8)}
</Link> </Link>
@ -125,7 +125,7 @@ export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
<button <button
onClick={() => handleCancelRun(run.id)} onClick={() => handleCancelRun(run.id)}
disabled={cancellingRunIds.has(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" /> <Square className="h-2.5 w-2.5" fill="currentColor" />
{cancellingRunIds.has(run.id) ? "Stopping…" : "Stop"} {cancellingRunIds.has(run.id) ? "Stopping…" : "Stop"}
@ -133,7 +133,7 @@ export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
)} )}
<Link <Link
to={`/agents/${run.agentId}/runs/${run.id}`} 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 Open run
<ExternalLink className="h-3 w-3" /> <ExternalLink className="h-3 w-3" />

View file

@ -663,7 +663,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
{option.kind === "project" && option.projectId ? ( {option.kind === "project" && option.projectId ? (
<span <span
className="inline-flex h-2 w-2 rounded-full border border-border/50" 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 <AgentIcon

View file

@ -233,7 +233,7 @@ export function NewAgentDialog() {
onClick={() => handleAdvancedAdapterPick(opt.value)} onClick={() => handleAdvancedAdapterPick(opt.value)}
> >
{opt.recommended && ( {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 Recommended
</span> </span>
)} )}

View file

@ -1092,7 +1092,7 @@ export function NewIssueDialog() {
<> <>
<span <span
className="h-3.5 w-3.5 shrink-0 rounded-sm" 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> <span className="truncate">{option.label}</span>
</> </>
@ -1107,7 +1107,7 @@ export function NewIssueDialog() {
<> <>
<span <span
className="h-3.5 w-3.5 shrink-0 rounded-sm" 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> <span className="truncate">{option.label}</span>
</> </>
@ -1212,7 +1212,7 @@ export function NewIssueDialog() {
data-slot="toggle" data-slot="toggle"
className={cn( className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors", "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)} 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. // Exports `OnboardingWizard` to match the named import in App.tsx.
// Wired via Vite alias: all imports of ./components/OnboardingWizard are // Wired via Vite alias: all imports of ./components/OnboardingWizard are
// redirected here at build time; the original file is preserved for upstream rebase. // 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 { puterProxyApi } from "../api/puter-proxy";
import { queryKeys } from "../lib/queryKeys"; import { queryKeys } from "../lib/queryKeys";
import { resolveRouteOnboardingOptions } from "../lib/onboarding-route"; import { resolveRouteOnboardingOptions } from "../lib/onboarding-route";
import { Dialog, DialogPortal } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { cn } from "../lib/utils"; import { cn } from "../lib/utils";
@ -27,19 +26,54 @@ import { TelegramStep } from "./onboarding/TelegramStep";
import { useHardwareInfo } from "../hooks/useHardwareInfo"; import { useHardwareInfo } from "../hooks/useHardwareInfo";
import { updateNexusSettings, type NexusMode } from "../api/hardware"; import { updateNexusSettings, type NexusMode } from "../api/hardware";
import { useChatPanel } from "../context/ChatPanelContext"; 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( function deriveProviderLabel(
puterToken: string | null, puterToken: string | null,
googleOAuthStateId: string | null, googleOAuthStateId: string | null,
apiKeyData: { provider: string; apiKey: string } | null, apiKeyData: { provider: string; apiKey: string } | null,
selectedAdapterChoice: string | null,
): string { ): string {
if (selectedAdapterChoice) return ADAPTER_LABELS[selectedAdapterChoice] ?? selectedAdapterChoice;
if (puterToken) return "Puter (free, zero-config)"; if (puterToken) return "Puter (free, zero-config)";
if (googleOAuthStateId) return "Google Gemini (free tier)"; if (googleOAuthStateId) return "Google Gemini (free tier)";
if (apiKeyData) return `API key — ${apiKeyData.provider}`; if (apiKeyData) return `API key — ${apiKeyData.provider}`;
return "None selected"; 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() { export function OnboardingWizard() {
const { onboardingOpen, onboardingOptions, closeOnboarding } = useDialog(); const { onboardingOpen, onboardingOptions, closeOnboarding } = useDialog();
const { companies, setSelectedCompanyId, loading: companiesLoading } = useCompany(); const { companies, setSelectedCompanyId, loading: companiesLoading } = useCompany();
@ -67,7 +101,7 @@ export function OnboardingWizard() {
setRouteDismissed(false); setRouteDismissed(false);
}, [location.pathname]); }, [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); const [step, setStep] = useState(1);
// Mode state: "both" pre-selected per UI-SPEC // Mode state: "both" pre-selected per UI-SPEC
@ -80,6 +114,7 @@ export function OnboardingWizard() {
const [puterToken, setPuterToken] = useState<string | null>(null); const [puterToken, setPuterToken] = useState<string | null>(null);
const [googleOAuthStateId, setGoogleOAuthStateId] = useState<string | null>(null); const [googleOAuthStateId, setGoogleOAuthStateId] = useState<string | null>(null);
const [apiKeyData, setApiKeyData] = useState<{ provider: string; apiKey: string } | null>(null); const [apiKeyData, setApiKeyData] = useState<{ provider: string; apiKey: string } | null>(null);
const [selectedAdapterChoice, setSelectedAdapterChoice] = useState<string | null>(null);
// Form state // Form state
const [rootDir, setRootDir] = useState(""); const [rootDir, setRootDir] = useState("");
@ -131,12 +166,7 @@ export function OnboardingWizard() {
}).catch(() => {}).finally(() => setProbing(false)); }).catch(() => {}).finally(() => setProbing(false));
}, [effectiveOnboardingOpen]); }, [effectiveOnboardingOpen]);
function handleClose() { // [nexus] Shared workspace creation logic used by both handleSubmit and handleStartChat
setRouteDismissed(true);
closeOnboarding();
}
// [nexus] Shared workspace creation logic used by both handleSubmit (step 4 direct) and handleStartChat (step 6)
async function createWorkspace() { async function createWorkspace() {
// Step 1: Create workspace (company) named after VOCAB.appName // Step 1: Create workspace (company) named after VOCAB.appName
const company = await companiesApi.create({ name: VOCAB.appName }); const company = await companiesApi.create({ name: VOCAB.appName });
@ -263,7 +293,7 @@ export function OnboardingWizard() {
async function handleStartChat() { async function handleStartChat() {
// Guard: claude_local requires rootDir // Guard: claude_local requires rootDir
if (defaultAdapter === "claude_local" && !rootDir.trim()) { 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; 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; if (!effectiveOnboardingOpen) return null;
return ( return (
<DialogPortal> <div className="fixed inset-0 z-50 flex bg-background">
<div className="fixed inset-0 z-50 flex items-center justify-center"> {/* Left sidebar - step navigation (hidden on mobile) */}
{/* Backdrop */} <aside className="hidden md:flex w-[260px] flex-col border-r border-border bg-card p-6">
<div {/* Logo/title at top */}
className="absolute inset-0 bg-black/60 backdrop-blur-sm" <div className="mb-8">
onClick={handleClose} <h1 className="text-lg font-bold">Nexus Setup</h1>
/> <p className="text-xs text-muted-foreground mt-1">Configure your workspace</p>
</div>
{/* Card */} {/* Step list */}
<div <nav className="flex flex-col gap-1">
className={cn( {STEPS.map((s) => {
"relative z-10 w-full max-w-md mx-4 rounded-xl border bg-card text-card-foreground shadow-2xl", const isCompleted = s.id < step;
"p-8 flex flex-col gap-6" const isCurrent = s.id === step;
)} const isUpcoming = s.id > step;
> const Icon = s.icon;
{/* Step indicator */}
<p className="text-xs text-muted-foreground text-center">
{step === 7 ? "Summary" : `Step ${step} of 6`}
</p>
{/* Step 1 — Hardware Detection */} return (
{step === 1 && ( <button
<> key={s.id}
<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
type="button" type="button"
variant="ghost" onClick={() => {
onClick={() => setStep(2)} if (isCompleted) setStep(s.id);
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);
}} }}
onSkip={() => setStep(5)} disabled={isUpcoming}
voiceCapability={hardwareInfo?.voiceCapability} 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",
<Button isCompleted && "text-foreground hover:bg-muted/50 cursor-pointer",
type="button" isUpcoming && "text-muted-foreground/50 cursor-default"
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>
)} )}
>
{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 {/* Mobile step indicator - horizontal bar at top */}
type="button" <div className="md:hidden fixed top-0 left-0 right-0 z-50 bg-card border-b border-border px-4 py-3">
onClick={() => setStep(7)} <div className="flex items-center justify-between mb-2">
disabled={loading || probing} <span className="text-sm font-medium">Nexus Setup</span>
className="w-full" <span className="text-xs text-muted-foreground">Step {step} of 6</span>
> </div>
Review &amp; finish <div className="flex gap-1.5">
</Button> {STEPS.map((s) => (
<div
<Button key={s.id}
type="button" className={cn(
variant="ghost" "h-1 flex-1 rounded-full transition-colors",
onClick={() => setStep(5)} s.id < step && "bg-[color:var(--chart-2)]",
className="w-full" s.id === step && "bg-primary",
disabled={loading} s.id > step && "bg-muted"
> )}
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)}
/> />
)} ))}
</div> </div>
</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 ( return (
<div <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" role="status"
aria-live="polite" aria-live="polite"
> >

View file

@ -796,7 +796,7 @@ export function OnboardingWizard() {
}} }}
> >
{opt.recommended && ( {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 Recommended
</span> </span>
)} )}
@ -1050,7 +1050,7 @@ export function OnboardingWizard() {
{adapterEnvResult && {adapterEnvResult &&
adapterEnvResult.status === "pass" ? ( 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" /> <Check className="h-3.5 w-3.5 shrink-0" />
<span className="font-medium">Passed</span> <span className="font-medium">Passed</span>
</div> </div>
@ -1059,8 +1059,8 @@ export function OnboardingWizard() {
) : null} ) : null}
{shouldSuggestUnsetAnthropicApiKey && ( {shouldSuggestUnsetAnthropicApiKey && (
<div className="rounded-md border border-amber-300/60 bg-amber-50/40 px-2.5 py-2 space-y-2"> <div className="rounded-md border border-warning/60 bg-warning/40 px-2.5 py-2 space-y-2">
<p className="text-[11px] text-amber-900/90 leading-relaxed"> <p className="text-[11px] text-warning leading-relaxed">
Claude failed while{" "} Claude failed while{" "}
<span className="font-mono">ANTHROPIC_API_KEY</span>{" "} <span className="font-mono">ANTHROPIC_API_KEY</span>{" "}
is set. You can clear it in this {VOCAB.ceo} adapter config is set. You can clear it in this {VOCAB.ceo} adapter config
@ -1224,7 +1224,7 @@ export function OnboardingWizard() {
</p> </p>
<p className="text-xs text-muted-foreground">{VOCAB.company}</p> <p className="text-xs text-muted-foreground">{VOCAB.company}</p>
</div> </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 className="flex items-center gap-3 px-3 py-2.5"> <div className="flex items-center gap-3 px-3 py-2.5">
<Bot className="h-4 w-4 text-muted-foreground shrink-0" /> <Bot className="h-4 w-4 text-muted-foreground shrink-0" />
@ -1236,7 +1236,7 @@ export function OnboardingWizard() {
{getUIAdapter(adapterType).label} {getUIAdapter(adapterType).label}
</p> </p>
</div> </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 className="flex items-center gap-3 px-3 py-2.5"> <div className="flex items-center gap-3 px-3 py-2.5">
<ListTodo className="h-4 w-4 text-muted-foreground shrink-0" /> <ListTodo className="h-4 w-4 text-muted-foreground shrink-0" />
@ -1246,7 +1246,7 @@ export function OnboardingWizard() {
</p> </p>
<p className="text-xs text-muted-foreground">Task</p> <p className="text-xs text-muted-foreground">Task</p>
</div> </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> </div>
</div> </div>
@ -1337,7 +1337,7 @@ export function OnboardingWizard() {
{/* Right half — ASCII art (hidden on mobile) */} {/* Right half — ASCII art (hidden on mobile) */}
<div <div
className={cn( 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" step === 1 ? "w-1/2 opacity-100" : "w-0 opacity-0"
)} )}
> >
@ -1362,10 +1362,10 @@ function AdapterEnvironmentResult({
: "Failed"; : "Failed";
const statusClass = const statusClass =
result.status === "pass" 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" : 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-warning border-warning/30 bg-warning/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-destructive border-destructive/30 bg-destructive/10";
return ( return (
<div className={`rounded-md border px-2.5 py-2 text-[11px] ${statusClass}`}> <div className={`rounded-md border px-2.5 py-2 text-[11px] ${statusClass}`}>

View file

@ -115,7 +115,7 @@ export function OutputFeedbackButtons({
size="sm" size="sm"
variant="outline" variant="outline"
disabled={disabled || isSaving} 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")} onClick={() => handleVote("up")}
> >
<ThumbsUp className="mr-1.5 h-3.5 w-3.5" /> <ThumbsUp className="mr-1.5 h-3.5 w-3.5" />
@ -126,7 +126,7 @@ export function OutputFeedbackButtons({
size="sm" size="sm"
variant="outline" variant="outline"
disabled={disabled || isSaving} 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")} onClick={() => handleVote("down")}
> >
<ThumbsDown className="mr-1.5 h-3.5 w-3.5" /> <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") { if (state === "saved") {
return ( 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" /> <Check className="h-3 w-3" />
Saved Saved
</span> </span>
@ -738,9 +738,9 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
className={cn( className={cn(
"rounded-full px-1.5 py-0.5 text-[10px] uppercase tracking-wide", "rounded-full px-1.5 py-0.5 text-[10px] uppercase tracking-wide",
service.status === "running" service.status === "running"
? "bg-green-500/15 text-green-700 dark:text-green-300" ? "bg-success/15 text-success"
: service.status === "failed" : service.status === "failed"
? "bg-red-500/15 text-red-700 dark:text-red-300" ? "bg-destructive/15 text-destructive"
: "bg-muted text-muted-foreground", : "bg-muted text-muted-foreground",
)} )}
> >
@ -891,7 +891,7 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
data-slot="toggle" data-slot="toggle"
className={cn( className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors", "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" type="button"
onClick={() => onClick={() =>
@ -930,7 +930,7 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
data-slot="toggle" data-slot="toggle"
className={cn( className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors", "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" type="button"
onClick={() => onClick={() =>

View file

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

View file

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

View file

@ -45,7 +45,7 @@ export function ReportsToPicker({
type="button" type="button"
className={cn( 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", "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 && "opacity-60 cursor-not-allowed",
)} )}
disabled={disabled} disabled={disabled}
@ -61,7 +61,7 @@ export function ReportsToPicker({
<span <span
className={cn( className={cn(
"min-w-0 truncate", "min-w-0 truncate",
terminatedManager && "text-amber-900 dark:text-amber-200", terminatedManager && "text-warning",
)} )}
> >
{`Reports to ${current.name}${terminatedManager ? " (terminated)" : ""}`} {`Reports to ${current.name}${terminatedManager ? " (terminated)" : ""}`}

View file

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

View file

@ -124,12 +124,12 @@ export function SidebarAgents() {
) : null} ) : null}
{runCount > 0 ? ( {runCount > 0 ? (
<span className="relative flex h-2 w-2"> <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="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-blue-500" /> <span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
</span> </span>
) : null} ) : null}
{runCount > 0 ? ( {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 {runCount} live
</span> </span>
) : null} ) : null}

View file

@ -50,7 +50,7 @@ export function SidebarNavItem({
<span className="relative shrink-0"> <span className="relative shrink-0">
<Icon className="h-4 w-4" /> <Icon className="h-4 w-4" />
{alert && ( {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>
<span className="flex-1 truncate">{label}</span> <span className="flex-1 truncate">{label}</span>
@ -59,7 +59,7 @@ export function SidebarNavItem({
className={cn( className={cn(
"ml-auto rounded-full px-1.5 py-0.5 text-[10px] font-medium leading-none", "ml-auto rounded-full px-1.5 py-0.5 text-[10px] font-medium leading-none",
textBadgeTone === "amber" 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", : "bg-muted text-muted-foreground",
)} )}
> >
@ -69,10 +69,10 @@ export function SidebarNavItem({
{liveCount != null && liveCount > 0 && ( {liveCount != null && liveCount > 0 && (
<span className="ml-auto flex items-center gap-1.5"> <span className="ml-auto flex items-center gap-1.5">
<span className="relative flex h-2 w-2"> <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="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-blue-500" /> <span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
</span> </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> </span>
)} )}
{badge != null && badge > 0 && ( {badge != null && badge > 0 && (
@ -80,7 +80,7 @@ export function SidebarNavItem({
className={cn( className={cn(
"ml-auto rounded-full px-1.5 py-0.5 text-xs leading-none", "ml-auto rounded-full px-1.5 py-0.5 text-xs leading-none",
badgeTone === "danger" badgeTone === "danger"
? "bg-red-600/90 text-red-50" ? "bg-destructive/90 text-destructive"
: "bg-primary text-primary-foreground", : "bg-primary text-primary-foreground",
)} )}
> >

View file

@ -86,7 +86,7 @@ function SortableProjectItem({
> >
<span <span
className="shrink-0 h-3.5 w-3.5 rounded-sm" 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> <span className="flex-1 truncate">{project.name}</span>
{project.pauseReason === "budget" ? <BudgetSidebarMarker title="Project paused by budget" /> : null} {project.pauseReason === "budget" ? <BudgetSidebarMarker title="Project paused by budget" /> : null}

View file

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

View file

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

View file

@ -141,7 +141,7 @@ export function SwipeToArchive({
> >
<div <div
aria-hidden="true" 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) }} style={{ opacity: Math.max(archiveReveal, 0.2) }}
> >
<span className="inline-flex items-center gap-2 text-sm font-medium"> <span className="inline-flex items-center gap-2 text-sm font-medium">
@ -153,7 +153,7 @@ export function SwipeToArchive({
data-inbox-row-surface data-inbox-row-surface
className={cn( className={cn(
"relative will-change-transform", "relative will-change-transform",
selected ? "bg-zinc-100 dark:bg-zinc-800" : "bg-card", selected ? "bg-muted" : "bg-card",
)} )}
style={{ style={{
transform: `translate3d(${offsetX}px, 0, 0)`, 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"; import { cn } from "../lib/utils";
const toneClasses: Record<ToastTone, string> = { 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", info: "border-primary/30 bg-primary/10 text-primary",
success: "border-emerald-300 bg-emerald-50 text-emerald-900 dark:border-emerald-500/25 dark:bg-emerald-950/60 dark:text-emerald-100", success: "border-success/30 bg-success/10 text-success",
warn: "border-amber-300 bg-amber-50 text-amber-900 dark:border-amber-500/25 dark:bg-amber-950/60 dark:text-amber-100", warn: "border-warning/30 bg-warning/10 text-warning",
error: "border-red-300 bg-red-50 text-red-900 dark:border-red-500/30 dark:bg-red-950/60 dark:text-red-100", error: "border-destructive/30 bg-destructive/10 text-destructive",
}; };
const toneDotClasses: Record<ToastTone, string> = { const toneDotClasses: Record<ToastTone, string> = {
info: "bg-sky-500 dark:bg-sky-400", info: "bg-primary",
success: "bg-emerald-500 dark:bg-emerald-400", success: "bg-success",
warn: "bg-amber-500 dark:bg-amber-400", warn: "bg-warning",
error: "bg-red-500 dark:bg-red-400", error: "bg-destructive",
}; };
function AnimatedToast({ function AnimatedToast({

View file

@ -40,10 +40,10 @@ export function VoiceWaveform({ stream, active }: VoiceWaveformProps) {
const canvasHeight = canvas.height; const canvasHeight = canvas.height;
const ctx2d = canvas.getContext("2d"); 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 = const primaryColor =
getComputedStyle(document.documentElement).getPropertyValue("--primary").trim() || getComputedStyle(document.documentElement).getPropertyValue("--primary").trim() ||
"#1e66f5"; "#faff69";
const draw = () => { const draw = () => {
analyser.getByteFrequencyData(dataArray); analyser.getByteFrequencyData(dataArray);

View file

@ -126,7 +126,7 @@ export function ToggleField({
type="button" type="button"
className={cn( className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors", "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)} onClick={() => onChange(!checked)}
> >
@ -175,7 +175,7 @@ export function ToggleWithNumber({
data-slot="toggle" data-slot="toggle"
className={cn( className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors shrink-0", "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)} onClick={() => onCheckedChange(!checked)}
> >

View file

@ -1,5 +1,6 @@
// [nexus] Hardware summary display for onboarding wizard step 1 // [nexus] Hardware summary display for onboarding wizard step 1
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { Badge } from "@/components/ui/badge";
import type { HardwareInfo } from "@/api/hardware"; import type { HardwareInfo } from "@/api/hardware";
interface HardwareSummaryStepProps { interface HardwareSummaryStepProps {
@ -16,28 +17,39 @@ interface StatRowProps {
function StatRow({ label, value }: StatRowProps) { function StatRow({ label, value }: StatRowProps) {
return ( return (
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<span className="text-xs text-muted-foreground">{label}</span> <span className="text-sm text-muted-foreground">{label}</span>
<span className="text-sm font-medium">{value}</span> <span className="text-base font-medium">{value}</span>
</div> </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) { export function HardwareSummaryStep({ hardwareInfo, isLoading, isError }: HardwareSummaryStepProps) {
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-3">
<Skeleton className="h-4 w-full rounded" /> <Skeleton className="h-5 w-full rounded" />
<Skeleton className="h-4 w-full rounded" /> <Skeleton className="h-5 w-full rounded" />
<Skeleton className="h-4 w-full rounded" /> <Skeleton className="h-5 w-full rounded" />
</div> </div>
); );
} }
if (isError) { if (isError) {
return ( return (
<p className="text-sm text-muted-foreground"> <div className="flex flex-col gap-2">
Could not detect hardware. You can still continue. <p className="text-sm text-muted-foreground">
</p> 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 { hardwareTier } = hardwareInfo;
const tierBadge = TIER_BADGE[hardwareTier];
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-5">
<div className="flex flex-col gap-2"> {/* 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" && ( {hardwareTier === "apple_silicon" && (
<> <>
<StatRow label="Unified memory" value={`${hardwareInfo.totalGb} GB`} /> <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="System RAM" value={`${hardwareInfo.totalGb} GB`} />
<StatRow label="CPU" value={hardwareInfo.cpuModel} /> <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 Slower than GPU-accelerated models -- cloud AI recommended
</p> </p>
</> </>
@ -83,10 +109,9 @@ export function HardwareSummaryStep({ hardwareInfo, isLoading, isError }: Hardwa
{hardwareTier !== "cpu_only" && ( {hardwareTier !== "cpu_only" && (
<div className="flex flex-col gap-1 pt-2"> <div className="flex flex-col gap-1 pt-2">
<span className="text-sm font-medium">Local AI (recommended for privacy)</span> <span className="text-base font-medium">Local AI (recommended for privacy)</span>
<span className="text-xs text-muted-foreground"> <span className="text-sm text-muted-foreground">
Runs entirely on your machine.{"\n"} Runs entirely on your machine. No accounts. No tracking. Works offline.
No accounts. No tracking. Works offline.
</span> </span>
</div> </div>
)} )}

View file

@ -1,49 +1,74 @@
// [nexus] Three-card mode selector for onboarding wizard step 2 // [nexus] Three-card mode selector for onboarding wizard step 2
import { cn } from "@/lib/utils"; 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 { NexusMode } from "@/api/hardware";
import type { LucideIcon } from "lucide-react";
interface ModeSelectorProps { interface ModeSelectorProps {
value: NexusMode; value: NexusMode;
onChange: (mode: NexusMode) => void; 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", id: "personal_ai",
label: "Personal AI Assistant", label: "Personal AI Assistant",
description: "Always available, persistent memory, private.", description: "Always available, persistent memory, private.",
icon: MessageSquare,
accent: "text-[color:var(--chart-3)]",
}, },
{ {
id: "project_builder", id: "project_builder",
label: "Project Builder", label: "Project Builder",
description: "Brainstorm -> PM -> Engineer -> shipped product.", description: "Brainstorm -> PM -> Engineer -> shipped product.",
icon: Hammer,
accent: "text-[color:var(--chart-4)]",
}, },
{ {
id: "both", id: "both",
label: "Both (recommended)", label: "Both",
description: "A conversation becomes a project with one click.", description: "A conversation becomes a project with one click.",
icon: Layers,
accent: "text-primary",
recommended: true,
}, },
]; ];
export function ModeSelector({ value, onChange }: ModeSelectorProps) { export function ModeSelector({ value, onChange }: ModeSelectorProps) {
return ( return (
<div className="grid gap-3"> <div className="grid gap-3">
{MODES.map((mode) => ( {MODES.map((mode) => {
<button const Icon = mode.icon;
key={mode.id} const isSelected = value === mode.id;
type="button"
onClick={() => onChange(mode.id)} return (
className={cn( <button
"flex flex-col gap-1 rounded-lg border p-4 text-left transition-colors", key={mode.id}
value === mode.id type="button"
? "border-primary bg-primary/5" onClick={() => onChange(mode.id)}
: "border-border hover:border-muted-foreground/50" className={cn(
)} "flex items-start gap-4 rounded-lg border p-5 text-left transition-colors",
> isSelected
<span className="font-medium text-sm">{mode.label}</span> ? "border-l-2 border-l-primary border-t border-r border-b border-border bg-primary/5"
<span className="text-xs text-muted-foreground">{mode.description}</span> : "border-border hover:border-muted-foreground/50"
</button> )}
))} >
<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> </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 type { HardwareInfo, HardwareTier, NexusMode } from "@/api/hardware";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -7,23 +9,35 @@ interface OnboardingSummaryStepProps {
selectedMode: NexusMode; selectedMode: NexusMode;
providerLabel: string; providerLabel: string;
rootDir: string; rootDir: string;
onRootDirChange: (value: string) => void;
loading: boolean; loading: boolean;
error: string | null; error: string | null;
onStartChat: () => void; onStartChat: () => void;
onBack: () => void; onBack: () => void;
voiceEnabled?: boolean;
defaultAdapter?: string;
} }
interface SummaryRowProps { interface SummaryRowProps {
label: string; label: string;
value: string; value: string;
mono?: boolean; mono?: boolean;
warn?: boolean;
} }
function SummaryRow({ label, value, mono }: SummaryRowProps) { function SummaryRow({ label, value, mono, warn }: SummaryRowProps) {
return ( return (
<div className="flex items-start justify-between gap-4"> <div className="flex items-start justify-between gap-4">
<span className="text-sm text-muted-foreground shrink-0">{label}</span> <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> </div>
); );
} }
@ -45,32 +59,54 @@ export function OnboardingSummaryStep({
selectedMode, selectedMode,
providerLabel, providerLabel,
rootDir, rootDir,
onRootDirChange,
loading, loading,
error, error,
onStartChat, onStartChat,
onBack, onBack,
voiceEnabled,
defaultAdapter,
}: OnboardingSummaryStepProps) { }: OnboardingSummaryStepProps) {
const hardwareLabel = hardwareInfo const hardwareLabel = hardwareInfo
? (HARDWARE_TIER_LABELS[hardwareInfo.hardwareTier] ?? "Unknown") ? (HARDWARE_TIER_LABELS[hardwareInfo.hardwareTier] ?? "Unknown")
: "Unknown"; : "Unknown";
const modeLabel = MODE_LABELS[selectedMode]; const modeLabel = MODE_LABELS[selectedMode];
const isProviderNone = providerLabel === "None selected";
const isVoiceNone = voiceEnabled === false || voiceEnabled === undefined;
return ( return (
<div className="flex flex-col gap-6"> <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 */} {/* 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="Hardware" value={hardwareLabel} />
<SummaryRow label="Mode" value={modeLabel} /> <SummaryRow label="Mode" value={modeLabel} />
<SummaryRow label="Provider" value={providerLabel} /> <SummaryRow label="Provider" value={providerLabel} warn={isProviderNone} />
{rootDir && <SummaryRow label="Root directory" value={rootDir} mono />} <SummaryRow label="Voice" value={voiceEnabled ? "Enabled" : "None selected"} warn={isVoiceNone} />
</div> </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 message */}
{error && ( {error && (
<p className="text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2"> <p className="text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2">
@ -84,7 +120,7 @@ export function OnboardingSummaryStep({
type="button" type="button"
onClick={onStartChat} onClick={onStartChat}
disabled={loading} disabled={loading}
className="w-full" className="w-full h-12 text-base font-medium"
> >
{loading ? ( {loading ? (
<span className="flex items-center gap-2"> <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 // [nexus] Provider selection step — Step 3 of 6 in the onboarding wizard
// Heading: "Choose a provider" / Subheading: "No API keys needed for the zero-config path." // Separated detected adapters section from provider cards
// Three provider cards (Puter, Google, API key) with adapter badges and skip button
import { useState } from "react"; import { useState } from "react";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { PuterAuthButton } from "./PuterAuthButton"; import { PuterAuthButton } from "./PuterAuthButton";
import { GoogleOAuthButton } from "./GoogleOAuthButton"; import { GoogleOAuthButton } from "./GoogleOAuthButton";
@ -12,68 +12,139 @@ interface ProviderSelectionStepProps {
onPuterToken: (token: string) => void; onPuterToken: (token: string) => void;
onGoogleOAuthState: (stateId: string) => void; onGoogleOAuthState: (stateId: string) => void;
onApiKey: (provider: string, apiKey: string) => void; onApiKey: (provider: string, apiKey: string) => void;
onAdapterSelected: (adapter: string | null) => void;
onSkip: () => void; onSkip: () => void;
onContinue: () => void; onContinue: () => void;
detectedAdapters: Record<string, boolean>; detectedAdapters: Record<string, boolean>;
probing?: boolean;
} }
type ProviderChoice = "puter" | "google" | "apikey" | null; 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({ export function ProviderSelectionStep({
onPuterToken, onPuterToken,
onGoogleOAuthState, onGoogleOAuthState,
onApiKey, onApiKey,
onAdapterSelected,
onSkip, onSkip,
onContinue, onContinue,
detectedAdapters, detectedAdapters,
probing,
}: ProviderSelectionStepProps) { }: ProviderSelectionStepProps) {
const [selectedProvider, setSelectedProvider] = useState<ProviderChoice>(null); const [selectedProvider, setSelectedProvider] = useState<ProviderChoice>(null);
const [selectedAdapter, setSelectedAdapter] = useState<AdapterChoice>(null);
const [providerReady, setProviderReady] = useState(false); const [providerReady, setProviderReady] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
function handleSelect(provider: ProviderChoice) { function handleSelectProvider(provider: ProviderChoice) {
setSelectedProvider(provider); setSelectedProvider(provider);
setSelectedAdapter(null);
setProviderReady(false); setProviderReady(false);
setError(null); 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 hermesDetected = detectedAdapters["hermes_local"] === true;
const claudeDetected = detectedAdapters["claude_local"] === true; const claudeDetected = detectedAdapters["claude_local"] === true;
const openclawDetected = detectedAdapters["openclaw_gateway"] === 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 ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-6">
{/* Three provider cards */} {/* 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"> <div className="flex flex-col gap-3">
<p className="text-sm font-medium text-muted-foreground">Cloud providers</p>
{/* Puter card */} {/* Puter card */}
<button <button
type="button" type="button"
onClick={() => handleSelect("puter")} onClick={() => handleSelectProvider("puter")}
className={cn( 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" 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" : "border-border hover:border-muted-foreground/50"
)} )}
> >
<div className="flex items-center gap-2 flex-wrap"> <span className="text-base font-medium">Puter -- free, zero-config</span>
<span className="font-medium text-sm">Puter -- free, zero-config</span> <span className="text-sm text-muted-foreground">
{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">
Free AI powered by your Puter.com account. No API key needed. Free AI powered by your Puter.com account. No API key needed.
</span> </span>
</button> </button>
{/* Puter auth component — shown when Puter is selected */} {/* Puter auth component */}
{selectedProvider === "puter" && ( {selectedProvider === "puter" && (
<PuterAuthButton <PuterAuthButton
onSuccess={(token) => { onSuccess={(token) => {
@ -87,21 +158,21 @@ export function ProviderSelectionStep({
{/* Google card */} {/* Google card */}
<button <button
type="button" type="button"
onClick={() => handleSelect("google")} onClick={() => handleSelectProvider("google")}
className={cn( 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" 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" : "border-border hover:border-muted-foreground/50"
)} )}
> >
<span className="font-medium text-sm">Google -- Gemini free tier</span> <span className="text-base font-medium">Google Gemini -- free tier</span>
<span className="text-xs text-muted-foreground"> <span className="text-sm text-muted-foreground">
Sign in with Google to access Gemini via your Google account. Sign in with Google to access Gemini via your Google account.
</span> </span>
</button> </button>
{/* Google OAuth component — shown when Google is selected */} {/* Google OAuth component */}
{selectedProvider === "google" && ( {selectedProvider === "google" && (
<GoogleOAuthButton <GoogleOAuthButton
onSuccess={(stateId) => { onSuccess={(stateId) => {
@ -115,21 +186,21 @@ export function ProviderSelectionStep({
{/* API key card */} {/* API key card */}
<button <button
type="button" type="button"
onClick={() => handleSelect("apikey")} onClick={() => handleSelectProvider("apikey")}
className={cn( 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" 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" : "border-border hover:border-muted-foreground/50"
)} )}
> >
<span className="font-medium text-sm">API key -- subscription provider</span> <span className="text-base font-medium">API key -- subscription provider</span>
<span className="text-xs text-muted-foreground"> <span className="text-sm text-muted-foreground">
Use your own OpenAI, Anthropic, or Groq API key. Use your own OpenAI, Anthropic, or Groq API key.
</span> </span>
</button> </button>
{/* API key form — shown when API key is selected */} {/* API key form */}
{selectedProvider === "apikey" && ( {selectedProvider === "apikey" && (
<ApiKeyEntryForm <ApiKeyEntryForm
onSave={(prov, key) => { onSave={(prov, key) => {
@ -148,23 +219,22 @@ export function ProviderSelectionStep({
</p> </p>
)} )}
{/* Continue button — shown when provider auth is complete */} {/* Single bottom action — contextual based on selection state */}
{providerReady && ( <div className="flex items-center justify-center mt-2">
<Button type="button" onClick={onContinue} className="w-full"> {canContinue ? (
Continue <Button type="button" onClick={handleContinue} className="w-full">
</Button> Continue
)} </Button>
) : (
{/* Skip button — always visible */} <button
<Button type="button"
type="button" className="text-sm text-muted-foreground hover:text-foreground transition-colors"
variant="ghost" onClick={onSkip}
onClick={onSkip} >
aria-label="Skip provider setup for now" Continue without a provider
className="w-full" </button>
> )}
Skip for now </div>
</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 { useState } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@ -50,68 +50,70 @@ export function TelegramStep({ onNext, onBack }: TelegramStepProps) {
return ( return (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
{/* Header */} {/* Telegram as current option */}
<div className="flex flex-col gap-2 text-center"> <div className="flex flex-col gap-4">
<h1 className="text-2xl font-semibold tracking-tight">Connect Telegram</h1> <p className="text-sm font-medium">Telegram Bot</p>
<p className="text-sm text-muted-foreground">
Get instant notifications and interact with your agents via Telegram. {/* BotFather instructions */}
</p> <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> </div>
{/* BotFather instructions */} {/* Future bridges note */}
<div className="flex flex-col gap-2"> <p className="text-sm text-muted-foreground">
<p className="text-sm font-medium">Set up your bot in 4 steps:</p> Discord and WhatsApp bridges coming in a future update.
<ol className="flex flex-col gap-2 list-none pl-0"> </p>
{[
<>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>
{/* Actions */} {/* Actions */}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@ -122,7 +124,7 @@ export function TelegramStep({ onNext, onBack }: TelegramStepProps) {
variant="outline" variant="outline"
className="w-full" className="w-full"
> >
{validating ? "Validating" : "Validate Token"} {validating ? "Validating..." : "Validate Token"}
</Button> </Button>
<Button <Button

View file

@ -1,5 +1,6 @@
import { useEffect, useState } from "react"; 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 { Button } from "@/components/ui/button";
import type { VoiceCapability } from "../../hooks/useHardwareInfo"; import type { VoiceCapability } from "../../hooks/useHardwareInfo";
@ -21,55 +22,73 @@ export function VoiceStep({ onEnable, onSkip, voiceCapability }: VoiceStepProps)
// Determine STT status label // Determine STT status label
function whisperStatusLabel(): string { function whisperStatusLabel(): string {
if (!voiceCapability) { if (!voiceCapability) {
// Fall back to mic-only check if (micAvailable === false) return "No microphone detected -- unavailable";
if (micAvailable === false) return "No microphone detected — unavailable"; if (micAvailable === true) return "Microphone detected -- speak to your assistant";
if (micAvailable === true) return "Microphone detected — speak to your assistant";
return "Checking microphone..."; return "Checking microphone...";
} }
if (voiceCapability.whisperAvailable) return "Whisper detected speech recognition ready"; if (voiceCapability.whisperAvailable) return "Whisper detected -- speech recognition ready";
return "Whisper not found install whisper-cpp for voice input"; return "Whisper not found -- install whisper-cpp for voice input";
} }
function whisperStatusIcon() { function whisperBadge() {
if (!voiceCapability) return null; if (!voiceCapability) return null;
if (voiceCapability.whisperAvailable) { 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 // Determine TTS status label
function piperStatusLabel(): string { function piperStatusLabel(): string {
if (!voiceCapability) { 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"; if (voiceCapability.piperAvailable) return "Piper detected -- text-to-speech ready";
return "Piper not found install piper for voice output"; return "Piper not found -- install piper for voice output";
} }
function piperStatusIcon() { function piperBadge() {
if (!voiceCapability) return null; if (!voiceCapability) return null;
if (voiceCapability.piperAvailable) { 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) { if (voiceCapability && !voiceCapability.voiceTierSufficient) {
return ( return (
<div className="flex flex-col gap-4"> <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"> <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-amber-500 shrink-0 mt-0.5" /> <Info className="h-5 w-5 text-[color:var(--chart-4)] shrink-0 mt-0.5" />
<div> <div>
<p className="text-sm font-medium text-amber-800 dark:text-amber-300">Hardware may not support voice</p> <p className="text-sm font-medium">Limited hardware for voice</p>
<p className="text-xs text-amber-700 dark:text-amber-400 mt-1"> <p className="text-sm text-muted-foreground mt-1">
Voice features require at least 4GB free RAM. Your system currently has insufficient free memory for local voice processing. Voice features require at least 4GB free RAM. Transcription may be slower on your system.
</p> </p>
</div> </div>
</div> </div>
<div className="flex flex-col gap-2"> <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"> <Button variant="ghost" onClick={onSkip} className="w-full">
Skip voice setup Skip voice setup
</Button> </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 = const neitherBinaryFound =
voiceCapability && voiceCapability &&
!voiceCapability.whisperAvailable && !voiceCapability.whisperAvailable &&
!voiceCapability.piperAvailable; !voiceCapability.piperAvailable;
// CPU-only note
const isCpuOnly = voiceCapability && !voiceCapability.whisperAvailable && !voiceCapability.piperAvailable;
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{/* Install note when tier is sufficient but binaries are missing */} {/* Install note when tier is sufficient but binaries are missing */}
{neitherBinaryFound && ( {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"> <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-blue-500 shrink-0 mt-0.5" /> <Info className="h-5 w-5 text-primary shrink-0 mt-0.5" />
<p className="text-xs text-blue-700 dark:text-blue-400"> <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. Install whisper-cpp and piper for local voice features. You can enable voice now and configure binaries later.
</p> </p>
</div> </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 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" /> <Mic className="h-5 w-5 text-primary shrink-0" />
<div className="flex-1"> <div className="flex-1">
<p className="text-sm font-medium">Speech-to-Text (Whisper)</p> <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()} {whisperStatusLabel()}
</p> </p>
</div> </div>
{whisperStatusIcon()} {whisperBadge()}
</div> </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" /> <Volume2 className="h-5 w-5 text-primary shrink-0" />
<div className="flex-1"> <div className="flex-1">
<p className="text-sm font-medium">Text-to-Speech (Piper)</p> <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()} {piperStatusLabel()}
</p> </p>
</div> </div>
{piperStatusIcon()} {piperBadge()}
</div> </div>
</div> </div>

View file

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

View file

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

View file

@ -401,14 +401,14 @@ function LiveRunIndicator({
return ( return (
<Link <Link
to={`/agents/${agentRef}/runs/${runId}`} 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()} onClick={(e) => e.stopPropagation()}
> >
<span className="relative flex h-2 w-2"> <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="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-blue-500" /> <span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
</span> </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})` : ""} Live{liveCount > 1 ? ` (${liveCount})` : ""}
</span> </span>
</Link> </Link>

View file

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

View file

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

View file

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

View file

@ -942,7 +942,7 @@ export function CompanyExport() {
{selectedCount} / {totalFiles} file{totalFiles === 1 ? "" : "s"} selected {selectedCount} / {totalFiles} file{totalFiles === 1 ? "" : "s"} selected
</span> </span>
{warnings.length > 0 && ( {warnings.length > 0 && (
<span className="text-amber-500"> <span className="text-warning">
{warnings.length} warning{warnings.length === 1 ? "" : "s"} {warnings.length} warning{warnings.length === 1 ? "" : "s"}
</span> </span>
)} )}
@ -962,9 +962,9 @@ export function CompanyExport() {
{/* Warnings */} {/* Warnings */}
{warnings.length > 0 && ( {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) => ( {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> </div>
)} )}

View file

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

View file

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

View file

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

View file

@ -695,10 +695,10 @@ export function Costs() {
className={cn( className={cn(
"h-full transition-[width,background-color] duration-150", "h-full transition-[width,background-color] duration-150",
spendData.summary.utilizationPercent > 90 spendData.summary.utilizationPercent > 90
? "bg-red-400" ? "bg-destructive"
: spendData.summary.utilizationPercent > 70 : spendData.summary.utilizationPercent > 70
? "bg-yellow-400" ? "bg-warning"
: "bg-emerald-400", : "bg-success",
)} )}
style={{ width: `${Math.min(100, spendData.summary.utilizationPercent)}%` }} 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>} {error && <p className="text-sm text-destructive">{error.message}</p>}
{hasNoAgents && ( {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"> <div className="flex items-center gap-2.5">
<Bot className="h-4 w-4 text-amber-600 dark:text-amber-400 shrink-0" /> <Bot className="h-4 w-4 text-warning shrink-0" />
<p className="text-sm text-amber-900 dark:text-amber-100"> <p className="text-sm text-warning">
You have no agents. You have no agents.
</p> </p>
</div> </div>
<button <button
onClick={() => openOnboarding({ initialStep: 2, companyId: selectedCompanyId! })} 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 Create one here
</button> </button>
@ -212,19 +212,19 @@ export function Dashboard() {
{data && ( {data && (
<> <>
{data.budgets.activeIncidents > 0 ? ( {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"> <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> <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"} {data.budgets.activeIncidents} active budget incident{data.budgets.activeIncidents === 1 ? "" : "s"}
</p> </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 {data.budgets.pausedAgents} agents paused · {data.budgets.pausedProjects} projects paused · {data.budgets.pendingApprovals} pending budget approvals
</p> </p>
</div> </div>
</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 Open budgets
</Link> </Link>
</div> </div>

View file

@ -461,9 +461,9 @@ export function DesignGuide() {
<SubSection title="Run invocation badges"> <SubSection title="Run invocation badges">
<div className="flex items-center gap-2 flex-wrap"> <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"], ["timer", "bg-primary/10 text-primary"],
["assignment", "bg-violet-100 text-violet-700 dark:bg-violet-900/50 dark:text-violet-300"], ["assignment", "bg-muted text-primary"],
["on_demand", "bg-cyan-100 text-cyan-700 dark:bg-cyan-900/50 dark:text-cyan-300"], ["on_demand", "bg-primary/10 text-primary"],
["automation", "bg-muted text-muted-foreground"], ["automation", "bg-muted text-muted-foreground"],
].map(([label, cls]) => ( ].map(([label, cls]) => (
<span key={label} className={`rounded-full px-1.5 py-0.5 text-[10px] font-medium ${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)"> <Section title="Progress Bars (Budget)">
<div className="space-y-3"> <div className="space-y-3">
{[ {[
{ label: "Under budget (40%)", pct: 40, color: "bg-green-400" }, { label: "Under budget (40%)", pct: 40, color: "bg-success" },
{ label: "Warning (75%)", pct: 75, color: "bg-yellow-400" }, { label: "Warning (75%)", pct: 75, color: "bg-warning" },
{ label: "Over budget (95%)", pct: 95, color: "bg-red-400" }, { label: "Over budget (95%)", pct: 95, color: "bg-destructive" },
].map(({ label, pct, color }) => ( ].map(({ label, pct, color }) => (
<div key={label} className="space-y-1"> <div key={label} className="space-y-1">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -1057,20 +1057,20 @@ export function DesignGuide() {
{/* LOG VIEWER */} {/* LOG VIEWER */}
{/* ============================================================ */} {/* ============================================================ */}
<Section title="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:01] INFO Agent started successfully</div>
<div className="text-foreground">[12:00:02] INFO Processing task PAP-001</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-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-destructive">[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-primary">[12:00:12] SYS Retrying connection in 5s...</div>
<div className="text-foreground">[12:00:17] INFO Reconnected successfully</div> <div className="text-foreground">[12:00:17] INFO Reconnected successfully</div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<span className="relative flex h-1.5 w-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="absolute inline-flex h-full w-full rounded-full bg-primary animate-pulse" />
<span className="inline-flex h-full w-full rounded-full bg-cyan-400" /> <span className="inline-flex h-full w-full rounded-full bg-primary" />
</span> </span>
<span className="text-cyan-400">Live</span> <span className="text-primary">Live</span>
</div> </div>
</div> </div>
</Section> </Section>

View file

@ -384,7 +384,7 @@ export function ExecutionWorkspaceDetail() {
</Button> </Button>
<StatusPill>{workspace.mode}</StatusPill> <StatusPill>{workspace.mode}</StatusPill>
<StatusPill>{workspace.providerType}</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} {workspace.status}
</StatusPill> </StatusPill>
</div> </div>

View file

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

View file

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

View file

@ -123,7 +123,7 @@ export function InstanceGeneralSettings() {
disabled={updateGeneralMutation.isPending} disabled={updateGeneralMutation.isPending}
className={cn( className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-60", "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={() => onClick={() =>
updateGeneralMutation.mutate({ updateGeneralMutation.mutate({
@ -157,7 +157,7 @@ export function InstanceGeneralSettings() {
disabled={updateGeneralMutation.isPending} disabled={updateGeneralMutation.isPending}
className={cn( className={cn(
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-60", "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 })} onClick={() => updateGeneralMutation.mutate({ keyboardShortcuts: !keyboardShortcuts })}
> >

View file

@ -218,7 +218,7 @@ export function InviteLandingPage() {
<p className="font-medium text-foreground">Connectivity diagnostics</p> <p className="font-medium text-foreground">Connectivity diagnostics</p>
{diagnostics.map((diag, idx) => ( {diagnostics.map((diag, idx) => (
<div key={`${diag.code}:${idx}`} className="space-y-0.5"> <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} [{diag.level}] {diag.message}
</p> </p>
{diag.hint && <p className="font-mono break-all">{diag.hint}</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> <span className="text-sm font-mono text-muted-foreground shrink-0">{issue.identifier ?? issue.id.slice(0, 8)}</span>
{hasLiveRuns && ( {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="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="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-cyan-400" /> <span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-primary" />
</span> </span>
Live Live
</span> </span>
@ -1189,7 +1189,7 @@ export function IssueDetail() {
{issue.originKind === "routine_execution" && issue.originId && ( {issue.originKind === "routine_execution" && issue.originId && (
<Link <Link
to={`/routines/${issue.originId}`} 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" /> <Repeat className="h-3 w-3" />
Routine Routine
@ -1239,7 +1239,7 @@ export function IssueDetail() {
onClick={copyIssueToClipboard} onClick={copyIssueToClipboard}
title="Copy issue as markdown" 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>
<Button <Button
variant="ghost" variant="ghost"
@ -1258,7 +1258,7 @@ export function IssueDetail() {
onClick={copyIssueToClipboard} onClick={copyIssueToClipboard}
title="Copy issue as markdown" 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>
<Button <Button
variant="ghost" variant="ghost"

View file

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

View file

@ -129,15 +129,17 @@ const adapterLabels: Record<string, string> = {
http: "HTTP", 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> = { const statusDotColor: Record<string, string> = {
running: "#22d3ee", running: "var(--primary)",
active: "#4ade80", active: "var(--success)",
paused: "#facc15", paused: "var(--warning)",
idle: "#facc15", idle: "var(--muted-foreground)",
error: "#f87171", error: "var(--destructive)",
terminated: "#a3a3a3", terminated: "var(--muted-foreground)",
}; };
const defaultDotColor = "#a3a3a3"; const defaultDotColor = "var(--muted-foreground)";
// ── Main component ────────────────────────────────────────────────────── // ── Main component ──────────────────────────────────────────────────────

View file

@ -205,9 +205,9 @@ export function PluginManager() {
</Dialog> </Dialog>
</div> </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"> <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"> <div className="space-y-1 text-sm">
<p className="font-medium text-foreground">Plugins are alpha.</p> <p className="font-medium text-foreground">Plugins are alpha.</p>
<p className="text-muted-foreground"> <p className="text-muted-foreground">
@ -251,7 +251,7 @@ export function PluginManager() {
{installedPlugin ? ( {installedPlugin ? (
<Badge <Badge
variant={installedPlugin.status === "ready" ? "default" : "secondary"} 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} {installedPlugin.status}
</Badge> </Badge>
@ -347,15 +347,15 @@ export function PluginManager() {
{plugin.manifestJson.description || "No description provided."} {plugin.manifestJson.description || "No description provided."}
</p> </p>
{plugin.status === "error" && ( {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="flex flex-wrap items-start gap-3">
<div className="min-w-0 flex-1"> <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" /> <AlertTriangle className="h-4 w-4 shrink-0" />
<span>Plugin error</span> <span>Plugin error</span>
</div> </div>
<p <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} title={plugin.lastError ?? undefined}
> >
{errorSummaryByPluginId.get(plugin.id)} {errorSummaryByPluginId.get(plugin.id)}
@ -364,7 +364,7 @@ export function PluginManager() {
<Button <Button
variant="outline" variant="outline"
size="sm" 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)} onClick={() => setErrorDetailsPlugin(plugin)}
> >
View full error View full error
@ -386,7 +386,7 @@ export function PluginManager() {
} }
className={cn( className={cn(
"shrink-0", "shrink-0",
plugin.status === "ready" ? "bg-green-600 hover:bg-green-700" : "" plugin.status === "ready" ? "bg-success hover:bg-success" : ""
)} )}
> >
{plugin.status} {plugin.status}
@ -405,7 +405,7 @@ export function PluginManager() {
}} }}
disabled={enableMutation.isPending || disableMutation.isPending} 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>
<Button <Button
variant="outline" variant="outline"
@ -478,14 +478,14 @@ export function PluginManager() {
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <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"> <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"> <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 What errored
</p> </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."} {errorDetailsPlugin ? getPluginErrorSummary(errorDetailsPlugin) : "No error summary available."}
</p> </p>
</div> </div>

View file

@ -290,7 +290,7 @@ export function PluginSettings() {
<> <>
<div className="flex justify-between col-span-2"> <div className="flex justify-between col-span-2">
<span className="text-muted-foreground flex items-center gap-1"> <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 Crashes
</span> </span>
<span className="text-xs"> <span className="text-xs">
@ -409,7 +409,7 @@ export function PluginSettings() {
entry.level === "error" entry.level === "error"
? "text-destructive" ? "text-destructive"
: entry.level === "warn" : entry.level === "warn"
? "text-yellow-600 dark:text-yellow-400" ? "text-warning"
: entry.level === "debug" : entry.level === "debug"
? "text-muted-foreground/60" ? "text-muted-foreground/60"
: "text-muted-foreground" : "text-muted-foreground"
@ -454,7 +454,7 @@ export function PluginSettings() {
{check.name} {check.name}
</span> </span>
{check.passed ? ( {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" /> <XCircle className="h-4 w-4 shrink-0 text-destructive" />
)} )}
@ -682,7 +682,7 @@ function PluginConfigForm({ pluginId, schema, initialValues, isLoading, pluginSt
<div <div
className={`text-sm p-2 rounded border ${ className={`text-sm p-2 rounded border ${
saveMessage.type === "success" 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" : "text-destructive bg-destructive/10 border-destructive/20"
}`} }`}
> >
@ -694,7 +694,7 @@ function PluginConfigForm({ pluginId, schema, initialValues, isLoading, pluginSt
<div <div
className={`text-sm p-2 rounded border ${ className={`text-sm p-2 rounded border ${
testResult.type === "success" 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" : "text-destructive bg-destructive/10 border-destructive/20"
}`} }`}
> >
@ -800,14 +800,14 @@ function formatTimestamp(epochMs: number): string {
function JobStatusDot({ status }: { status: string }) { function JobStatusDot({ status }: { status: string }) {
const colorClass = const colorClass =
status === "success" || status === "succeeded" status === "success" || status === "succeeded"
? "bg-green-500" ? "bg-success"
: status === "failed" : status === "failed"
? "bg-red-500" ? "bg-destructive"
: status === "running" : status === "running"
? "bg-blue-500 animate-pulse" ? "bg-primary animate-pulse"
: status === "cancelled" : status === "cancelled"
? "bg-gray-400" ? "bg-muted"
: "bg-amber-500"; // queued, pending : "bg-warning"; // queued, pending
return ( return (
<span <span
className={`inline-block h-2 w-2 rounded-full shrink-0 ${colorClass}`} 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 }) { function DeliveryStatusDot({ status }: { status: string }) {
const colorClass = const colorClass =
status === "processed" || status === "success" status === "processed" || status === "success"
? "bg-green-500" ? "bg-success"
: status === "failed" : status === "failed"
? "bg-red-500" ? "bg-destructive"
: status === "received" : status === "received"
? "bg-blue-500" ? "bg-primary"
: "bg-amber-500"; // pending : "bg-warning"; // pending
return ( return (
<span <span
className={`inline-block h-2 w-2 rounded-full shrink-0 ${colorClass}`} 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"> <div className="flex shrink-0 items-center gap-2 text-xs text-muted-foreground">
{summary.serviceCount > 0 ? ( {summary.serviceCount > 0 ? (
<span className={`inline-flex items-center gap-1 ${hasRunningServices ? "text-emerald-500" : ""}`}> <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-emerald-500" : "bg-muted-foreground/40"}`} /> <span className={`inline-block h-1.5 w-1.5 rounded-full ${hasRunningServices ? "bg-success" : "bg-muted-foreground/40"}`} />
{summary.runningServiceCount}/{summary.serviceCount} {summary.runningServiceCount}/{summary.serviceCount}
</span> </span>
) : null} ) : null}
@ -410,7 +410,7 @@ function ProjectWorkspacesContent({
<div className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground"> <div className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">
Cleanup attention needed Cleanup attention needed
</div> </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)} {cleanupFailedSummaries.map(renderSummaryRow)}
</div> </div>
</div> </div>
@ -796,7 +796,7 @@ export function ProjectDetail() {
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<div className="h-7 flex items-center"> <div className="h-7 flex items-center">
<ColorPicker <ColorPicker
currentColor={project.color ?? "#6366f1"} currentColor={project.color ?? "var(--primary)"}
onSelect={(color) => updateProject.mutate({ color })} onSelect={(color) => updateProject.mutate({ color })}
/> />
</div> </div>
@ -808,8 +808,8 @@ export function ProjectDetail() {
className="text-xl font-bold" className="text-xl font-bold"
/> />
{project.pauseReason === "budget" ? ( {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"> <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-red-400" /> <span className="h-2 w-2 rounded-full bg-destructive" />
Paused by budget hard stop Paused by budget hard stop
</div> </div>
) : null} ) : null}

View file

@ -389,7 +389,7 @@ export function ProjectWorkspaceDetail() {
Make primary Make primary
</Button> </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" /> <Sparkles className="h-4 w-4" />
This is the projects primary codebase workspace. This is the projects primary codebase workspace.
</div> </div>

View file

@ -663,7 +663,7 @@ export function RoutineDetail() {
const automationLabelClassName = routine.status === "archived" const automationLabelClassName = routine.status === "archived"
? "text-muted-foreground" ? "text-muted-foreground"
: automationEnabled : automationEnabled
? "text-emerald-400" ? "text-success"
: "text-muted-foreground"; : "text-muted-foreground";
return ( return (
@ -719,7 +719,7 @@ export function RoutineDetail() {
aria-label={automationEnabled ? "Pause automatic triggers" : "Enable automatic triggers"} aria-label={automationEnabled ? "Pause automatic triggers" : "Enable automatic triggers"}
disabled={automationToggleDisabled} disabled={automationToggleDisabled}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${ 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" : ""}`} } ${automationToggleDisabled ? "cursor-not-allowed opacity-50" : ""}`}
onClick={() => updateRoutineStatus.mutate(automationEnabled ? "paused" : "active")} onClick={() => updateRoutineStatus.mutate(automationEnabled ? "paused" : "active")}
> >
@ -737,7 +737,7 @@ export function RoutineDetail() {
{/* Secret message banner */} {/* Secret message banner */}
{secretMessage && ( {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> <div>
<p className="font-medium">{secretMessage.title}</p> <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> <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 <span
className="h-3.5 w-3.5 shrink-0 rounded-sm" 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> <span className="truncate">{option.label}</span>
</> </>
@ -840,7 +840,7 @@ export function RoutineDetail() {
<> <>
<span <span
className="h-3.5 w-3.5 shrink-0 rounded-sm" 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> <span className="truncate">{option.label}</span>
</> </>
@ -920,7 +920,7 @@ export function RoutineDetail() {
{/* Save bar */} {/* Save bar */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
{isEditDirty ? ( {isEditDirty ? (
<span className="text-xs text-amber-600">Unsaved changes</span> <span className="text-xs text-warning">Unsaved changes</span>
) : ( ) : (
<span /> <span />
)} )}
@ -945,7 +945,7 @@ export function RoutineDetail() {
<TabsTrigger value="runs" className="gap-1.5"> <TabsTrigger value="runs" className="gap-1.5">
<Play className="h-3.5 w-3.5" /> <Play className="h-3.5 w-3.5" />
Runs 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>
<TabsTrigger value="activity" className="gap-1.5"> <TabsTrigger value="activity" className="gap-1.5">
<ActivityIcon className="h-3.5 w-3.5" /> <ActivityIcon className="h-3.5 w-3.5" />

View file

@ -282,7 +282,7 @@ export function Routines() {
<div className="space-y-1"> <div className="space-y-1">
<h1 className="text-2xl font-semibold tracking-tight flex items-center gap-2"> <h1 className="text-2xl font-semibold tracking-tight flex items-center gap-2">
Routines 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> </h1>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Recurring work definitions that materialize into auditable execution issues. Recurring work definitions that materialize into auditable execution issues.
@ -425,7 +425,7 @@ export function Routines() {
<> <>
<span <span
className="h-3.5 w-3.5 shrink-0 rounded-sm" 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> <span className="truncate">{option.label}</span>
</> </>
@ -440,7 +440,7 @@ export function Routines() {
<> <>
<span <span
className="h-3.5 w-3.5 shrink-0 rounded-sm" 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> <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"> <div className="flex items-center gap-2 text-sm text-muted-foreground">
<span <span
className="shrink-0 h-3 w-3 rounded-sm" 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> <span className="truncate">{projectById.get(routine.projectId)?.name ?? "Unknown"}</span>
</div> </div>

View file

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

View file

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