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:
parent
4b8f8178ee
commit
3a41ec7b9c
84 changed files with 1049 additions and 913 deletions
|
|
@ -118,13 +118,13 @@ export function HermesLocalConfigFields({
|
|||
}
|
||||
>
|
||||
{showInstallCallout && (
|
||||
<div className="mb-2 rounded-md border border-amber-500/30 bg-amber-500/5 p-3 text-sm">
|
||||
<span className="text-amber-200">Ollama is not detected. </span>
|
||||
<div className="mb-2 rounded-md border border-warning/30 bg-warning/5 p-3 text-sm">
|
||||
<span className="text-warning">Ollama is not detected. </span>
|
||||
<a
|
||||
href={ollamaStatus.installUrl ?? "https://ollama.com/download"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-amber-400 underline hover:text-amber-300"
|
||||
className="text-warning underline hover:text-warning"
|
||||
>
|
||||
Install Ollama
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -8,21 +8,21 @@ const SURFACES = [
|
|||
description: "Request-scoped usage and billed runs from cost_events.",
|
||||
icon: Database,
|
||||
points: ["tokens + billed dollars", "provider, biller, model", "subscription and overage aware"],
|
||||
tone: "from-sky-500/12 via-sky-500/6 to-transparent",
|
||||
tone: "from-primary/12 via-primary/6 to-transparent",
|
||||
},
|
||||
{
|
||||
title: "Finance ledger",
|
||||
description: "Account-level charges that are not one prompt-response pair.",
|
||||
icon: ReceiptText,
|
||||
points: ["top-ups, refunds, fees", "Bedrock provisioned or training charges", "credit expiries and adjustments"],
|
||||
tone: "from-amber-500/14 via-amber-500/6 to-transparent",
|
||||
tone: "from-warning/14 via-warning/6 to-transparent",
|
||||
},
|
||||
{
|
||||
title: "Live quotas",
|
||||
description: "Provider or biller windows that can stop traffic in real time.",
|
||||
icon: Gauge,
|
||||
points: ["provider quota windows", "biller credit systems", "errors surfaced directly"],
|
||||
tone: "from-emerald-500/14 via-emerald-500/6 to-transparent",
|
||||
tone: "from-success/14 via-success/6 to-transparent",
|
||||
},
|
||||
] as const;
|
||||
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ function AgentRunCard({
|
|||
<div className={cn(
|
||||
"flex h-[320px] flex-col overflow-hidden rounded-xl border shadow-sm",
|
||||
isActive
|
||||
? "border-cyan-500/25 bg-cyan-500/[0.04] shadow-[0_16px_40px_rgba(6,182,212,0.08)]"
|
||||
? "border-primary/25 bg-primary/[0.04] shadow-[0_16px_40px_rgba(6,182,212,0.08)]"
|
||||
: "border-border bg-background/70",
|
||||
)}>
|
||||
<div className="border-b border-border/60 px-3 py-3">
|
||||
|
|
@ -102,8 +102,8 @@ function AgentRunCard({
|
|||
<div className="flex items-center gap-2">
|
||||
{isActive ? (
|
||||
<span className="relative flex h-2.5 w-2.5 shrink-0">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-cyan-400 opacity-70" />
|
||||
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-cyan-500" />
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-70" />
|
||||
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-primary" />
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex h-2.5 w-2.5 rounded-full bg-muted-foreground/35" />
|
||||
|
|
@ -129,7 +129,7 @@ function AgentRunCard({
|
|||
to={`/issues/${issue?.identifier ?? run.issueId}`}
|
||||
className={cn(
|
||||
"line-clamp-2 hover:underline",
|
||||
isActive ? "text-cyan-700 dark:text-cyan-300" : "text-muted-foreground hover:text-foreground",
|
||||
isActive ? "text-primary" : "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
title={issue?.title ? `${issue?.identifier ?? run.issueId.slice(0, 8)} - ${issue.title}` : issue?.identifier ?? run.issueId.slice(0, 8)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -88,9 +88,9 @@ export function RunActivityChart({ runs }: { runs: HeartbeatRun[] }) {
|
|||
<div key={day} className="flex-1 h-full flex flex-col justify-end" title={`${day}: ${total} runs`}>
|
||||
{total > 0 ? (
|
||||
<div className="flex flex-col-reverse gap-px overflow-hidden" style={{ height: `${heightPct}%`, minHeight: 2 }}>
|
||||
{entry.succeeded > 0 && <div className="bg-emerald-500" style={{ flex: entry.succeeded }} />}
|
||||
{entry.failed > 0 && <div className="bg-red-500" style={{ flex: entry.failed }} />}
|
||||
{entry.other > 0 && <div className="bg-neutral-500" style={{ flex: entry.other }} />}
|
||||
{entry.succeeded > 0 && <div className="bg-success" style={{ flex: entry.succeeded }} />}
|
||||
{entry.failed > 0 && <div className="bg-destructive" style={{ flex: entry.failed }} />}
|
||||
{entry.other > 0 && <div className="bg-muted" style={{ flex: entry.other }} />}
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-muted/30 rounded-sm" style={{ height: 2 }} />
|
||||
|
|
@ -105,10 +105,10 @@ export function RunActivityChart({ runs }: { runs: HeartbeatRun[] }) {
|
|||
}
|
||||
|
||||
const priorityColors: Record<string, string> = {
|
||||
critical: "#ef4444",
|
||||
high: "#f97316",
|
||||
medium: "#eab308",
|
||||
low: "#6b7280",
|
||||
critical: "var(--destructive)",
|
||||
high: "var(--chart-4)",
|
||||
medium: "var(--chart-1)",
|
||||
low: "var(--muted-foreground)",
|
||||
};
|
||||
|
||||
const priorityOrder = ["critical", "high", "medium", "low"] as const;
|
||||
|
|
@ -158,13 +158,13 @@ export function PriorityChart({ issues }: { issues: { priority: string; createdA
|
|||
}
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
todo: "#3b82f6",
|
||||
in_progress: "#8b5cf6",
|
||||
in_review: "#a855f7",
|
||||
done: "#10b981",
|
||||
blocked: "#ef4444",
|
||||
cancelled: "#6b7280",
|
||||
backlog: "#64748b",
|
||||
todo: "var(--chart-1)",
|
||||
in_progress: "var(--chart-3)",
|
||||
in_review: "var(--chart-4)",
|
||||
done: "var(--chart-2)",
|
||||
blocked: "var(--destructive)",
|
||||
cancelled: "var(--muted-foreground)",
|
||||
backlog: "var(--muted-foreground)",
|
||||
};
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
|
|
@ -208,7 +208,7 @@ export function IssueStatusChart({ issues }: { issues: { status: string; created
|
|||
{total > 0 ? (
|
||||
<div className="flex flex-col-reverse gap-px overflow-hidden" style={{ height: `${heightPct}%`, minHeight: 2 }}>
|
||||
{statusOrder.map(s => (entry[s] ?? 0) > 0 ? (
|
||||
<div key={s} style={{ flex: entry[s], backgroundColor: statusColors[s] ?? "#6b7280" }} />
|
||||
<div key={s} style={{ flex: entry[s], backgroundColor: statusColors[s] ?? "var(--muted-foreground)" }} />
|
||||
) : null)}
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -219,7 +219,7 @@ export function IssueStatusChart({ issues }: { issues: { status: string; created
|
|||
})}
|
||||
</div>
|
||||
<DateLabels days={days} />
|
||||
<ChartLegend items={statusOrder.map(s => ({ color: statusColors[s] ?? "#6b7280", label: statusLabels[s] ?? s }))} />
|
||||
<ChartLegend items={statusOrder.map(s => ({ color: statusColors[s] ?? "var(--muted-foreground)", label: statusLabels[s] ?? s }))} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -245,7 +245,7 @@ export function SuccessRateChart({ runs }: { runs: HeartbeatRun[] }) {
|
|||
{days.map(day => {
|
||||
const entry = grouped.get(day)!;
|
||||
const rate = entry.total > 0 ? entry.succeeded / entry.total : 0;
|
||||
const color = entry.total === 0 ? undefined : rate >= 0.8 ? "#10b981" : rate >= 0.5 ? "#eab308" : "#ef4444";
|
||||
const color = entry.total === 0 ? undefined : rate >= 0.8 ? "var(--chart-2)" : rate >= 0.5 ? "var(--chart-4)" : "var(--destructive)";
|
||||
return (
|
||||
<div key={day} className="flex-1 h-full flex flex-col justify-end" title={`${day}: ${entry.total > 0 ? Math.round(rate * 100) : 0}% (${entry.succeeded}/${entry.total})`}>
|
||||
{entry.total > 0 ? (
|
||||
|
|
|
|||
|
|
@ -550,7 +550,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||
}}
|
||||
/>
|
||||
</Field>
|
||||
<div className="rounded-md border border-amber-500/25 bg-amber-500/10 px-3 py-2 text-xs text-amber-100">
|
||||
<div className="rounded-md border border-warning/25 bg-warning/10 px-3 py-2 text-xs text-warning">
|
||||
Prompt template is replayed on every heartbeat. Keep it compact and dynamic to avoid recurring token cost and cache churn.
|
||||
</div>
|
||||
</>
|
||||
|
|
@ -687,7 +687,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||
}}
|
||||
/>
|
||||
</Field>
|
||||
<div className="rounded-md border border-amber-500/25 bg-amber-500/10 px-3 py-2 text-xs text-amber-100">
|
||||
<div className="rounded-md border border-warning/25 bg-warning/10 px-3 py-2 text-xs text-warning">
|
||||
Prompt template is replayed on every heartbeat. Prefer small task framing and variables like <code>{"{{ context.* }}"}</code> or <code>{"{{ run.* }}"}</code>; avoid repeating stable instructions here.
|
||||
</div>
|
||||
</>
|
||||
|
|
@ -786,7 +786,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||
{adapterType === "codex_local" &&
|
||||
codexSearchEnabled &&
|
||||
currentThinkingEffort === "minimal" && (
|
||||
<p className="text-xs text-amber-400">
|
||||
<p className="text-xs text-warning">
|
||||
Codex may reject `minimal` thinking when search is enabled.
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -813,7 +813,7 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
|||
}}
|
||||
/>
|
||||
</Field>
|
||||
<div className="rounded-md border border-amber-500/25 bg-amber-500/10 px-3 py-2 text-xs text-amber-200">
|
||||
<div className="rounded-md border border-warning/25 bg-warning/10 px-3 py-2 text-xs text-warning">
|
||||
Bootstrap prompt is legacy and will be removed in a future release. Consider moving this content into the agent's prompt template or instructions file instead.
|
||||
</div>
|
||||
</>
|
||||
|
|
@ -993,10 +993,10 @@ function AdapterEnvironmentResult({ result }: { result: AdapterEnvironmentTestRe
|
|||
result.status === "pass" ? "Passed" : result.status === "warn" ? "Warnings" : "Failed";
|
||||
const statusClass =
|
||||
result.status === "pass"
|
||||
? "text-green-700 dark:text-green-300 border-green-300 dark:border-green-500/40 bg-green-50 dark:bg-green-500/10"
|
||||
? "text-success border-success/30 bg-success/10"
|
||||
: result.status === "warn"
|
||||
? "text-amber-700 dark:text-amber-300 border-amber-300 dark:border-amber-500/40 bg-amber-50 dark:bg-amber-500/10"
|
||||
: "text-red-700 dark:text-red-300 border-red-300 dark:border-red-500/40 bg-red-50 dark:bg-red-500/10";
|
||||
? "text-warning border-warning/30 bg-warning/10"
|
||||
: "text-destructive border-destructive/30 bg-destructive/10";
|
||||
|
||||
return (
|
||||
<div className={`rounded-md border px-3 py-2 text-xs ${statusClass}`}>
|
||||
|
|
@ -1504,7 +1504,7 @@ function ModelDropdown({
|
|||
<span className="block w-full text-left truncate font-mono text-xs" title={value}>
|
||||
{value}
|
||||
</span>
|
||||
<span className="shrink-0 ml-auto text-[9px] font-medium px-1.5 py-0.5 rounded-full bg-green-500/15 text-green-400 border border-green-500/20">
|
||||
<span className="shrink-0 ml-auto text-[9px] font-medium px-1.5 py-0.5 rounded-full bg-success/15 text-success border border-success/20">
|
||||
current
|
||||
</span>
|
||||
</button>
|
||||
|
|
@ -1523,7 +1523,7 @@ function ModelDropdown({
|
|||
<span className="block w-full text-left truncate font-mono text-xs" title={detectedModel}>
|
||||
{detectedModel}
|
||||
</span>
|
||||
<span className="shrink-0 ml-auto text-[9px] font-medium px-1.5 py-0.5 rounded-full bg-blue-500/15 text-blue-400 border border-blue-500/20">
|
||||
<span className="shrink-0 ml-auto text-[9px] font-medium px-1.5 py-0.5 rounded-full bg-primary/15 text-primary border border-primary/20">
|
||||
detected
|
||||
</span>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ export function AgentProperties({ agent, runtimeState }: AgentPropertiesProps) {
|
|||
)}
|
||||
{runtimeState?.lastError && (
|
||||
<PropertyRow label="Last error">
|
||||
<span className="text-xs text-red-600 dark:text-red-400 truncate max-w-[160px]">{runtimeState.lastError}</span>
|
||||
<span className="text-xs text-destructive truncate max-w-[160px]">{runtimeState.lastError}</span>
|
||||
</PropertyRow>
|
||||
)}
|
||||
{agent.lastHeartbeatAt && (
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import { timeAgo } from "../lib/timeAgo";
|
|||
import type { Approval, Agent } from "@paperclipai/shared";
|
||||
|
||||
function statusIcon(status: string) {
|
||||
if (status === "approved") return <CheckCircle2 className="h-3.5 w-3.5 text-green-600 dark:text-green-400" />;
|
||||
if (status === "rejected") return <XCircle className="h-3.5 w-3.5 text-red-600 dark:text-red-400" />;
|
||||
if (status === "revision_requested") return <Clock className="h-3.5 w-3.5 text-amber-600 dark:text-amber-400" />;
|
||||
if (status === "pending") return <Clock className="h-3.5 w-3.5 text-yellow-600 dark:text-yellow-400" />;
|
||||
if (status === "approved") return <CheckCircle2 className="h-3.5 w-3.5 text-success" />;
|
||||
if (status === "rejected") return <XCircle className="h-3.5 w-3.5 text-destructive" />;
|
||||
if (status === "revision_requested") return <Clock className="h-3.5 w-3.5 text-warning" />;
|
||||
if (status === "pending") return <Clock className="h-3.5 w-3.5 text-warning" />;
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ export function ApprovalCard({
|
|||
<div className="flex gap-2 mt-4 pt-3 border-t border-border">
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-green-700 hover:bg-green-600 text-white"
|
||||
className="bg-success hover:bg-success text-white"
|
||||
onClick={onApprove}
|
||||
disabled={isPending}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -33,25 +33,25 @@ export function BudgetIncidentCard({
|
|||
const parsed = parseDollarInput(draftAmount);
|
||||
|
||||
return (
|
||||
<Card className="overflow-hidden border-red-500/20 bg-[linear-gradient(180deg,rgba(255,70,70,0.10),rgba(255,255,255,0.02))]">
|
||||
<Card className="overflow-hidden border-destructive/20 bg-[linear-gradient(180deg,rgba(255,70,70,0.10),rgba(255,255,255,0.02))]">
|
||||
<CardHeader className="px-5 pt-5 pb-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-[11px] uppercase tracking-[0.22em] text-red-200/80">
|
||||
<div className="text-[11px] uppercase tracking-[0.22em] text-destructive">
|
||||
{incident.scopeType} hard stop
|
||||
</div>
|
||||
<CardTitle className="mt-1 text-base text-red-50">{incident.scopeName}</CardTitle>
|
||||
<CardDescription className="mt-1 text-red-100/70">
|
||||
<CardTitle className="mt-1 text-base text-destructive">{incident.scopeName}</CardTitle>
|
||||
<CardDescription className="mt-1 text-destructive">
|
||||
Spending reached {formatCents(incident.amountObserved)} against a limit of {formatCents(incident.amountLimit)}.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="rounded-full border border-red-400/30 bg-red-500/10 p-2 text-red-200">
|
||||
<div className="rounded-full border border-destructive/30 bg-destructive/10 p-2 text-destructive">
|
||||
<AlertOctagon className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4 px-5 pb-5 pt-0">
|
||||
<div className="flex items-start gap-2 rounded-xl border border-red-400/20 bg-red-500/10 px-3 py-2 text-sm text-red-50/90">
|
||||
<div className="flex items-start gap-2 rounded-xl border border-destructive/20 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||
<PauseCircle className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<div>
|
||||
{incident.scopeType === "project"
|
||||
|
|
@ -83,7 +83,7 @@ export function BudgetIncidentCard({
|
|||
</Button>
|
||||
</div>
|
||||
{parsed !== null && parsed <= incident.amountObserved ? (
|
||||
<p className="mt-2 text-xs text-red-200/80">
|
||||
<p className="mt-2 text-xs text-destructive">
|
||||
The new budget must exceed current observed spend.
|
||||
</p>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ function windowLabel(windowKind: BudgetPolicySummary["windowKind"]) {
|
|||
}
|
||||
|
||||
function statusTone(status: BudgetPolicySummary["status"]) {
|
||||
if (status === "hard_stop") return "text-red-300 border-red-500/30 bg-red-500/10";
|
||||
if (status === "warning") return "text-amber-200 border-amber-500/30 bg-amber-500/10";
|
||||
return "text-emerald-200 border-emerald-500/30 bg-emerald-500/10";
|
||||
if (status === "hard_stop") return "text-destructive border-destructive/30 bg-destructive/10";
|
||||
if (status === "warning") return "text-warning border-warning/30 bg-warning/10";
|
||||
return "text-success border-success/30 bg-success/10";
|
||||
}
|
||||
|
||||
export function BudgetPolicyCard({
|
||||
|
|
@ -104,10 +104,10 @@ export function BudgetPolicyCard({
|
|||
className={cn(
|
||||
"h-full rounded-full transition-[width,background-color] duration-200",
|
||||
summary.status === "hard_stop"
|
||||
? "bg-red-400"
|
||||
? "bg-destructive"
|
||||
: summary.status === "warning"
|
||||
? "bg-amber-300"
|
||||
: "bg-emerald-300",
|
||||
? "bg-warning/15"
|
||||
: "bg-success/15",
|
||||
)}
|
||||
style={{ width: `${progress}%` }}
|
||||
/>
|
||||
|
|
@ -116,7 +116,7 @@ export function BudgetPolicyCard({
|
|||
);
|
||||
|
||||
const pausedPane = summary.paused ? (
|
||||
<div className="flex items-start gap-2 rounded-xl border border-red-500/30 bg-red-500/10 px-3 py-2 text-sm text-red-100">
|
||||
<div className="flex items-start gap-2 rounded-xl border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive">
|
||||
<PauseCircle className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<div>
|
||||
{summary.scopeType === "project"
|
||||
|
|
@ -166,9 +166,9 @@ export function BudgetPolicyCard({
|
|||
className={cn(
|
||||
"inline-flex items-center gap-2 text-[11px] uppercase tracking-[0.18em]",
|
||||
summary.status === "hard_stop"
|
||||
? "text-red-300"
|
||||
? "text-destructive"
|
||||
: summary.status === "warning"
|
||||
? "text-amber-200"
|
||||
? "text-warning"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export function BudgetSidebarMarker({ title = "Paused by budget" }: { title?: st
|
|||
<span
|
||||
title={title}
|
||||
aria-label={title}
|
||||
className="ml-auto inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-red-500/90 text-white shadow-[0_0_0_1px_rgba(255,255,255,0.08)]"
|
||||
className="ml-auto inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-destructive/90 text-white shadow-[0_0_0_1px_rgba(255,255,255,0.08)]"
|
||||
>
|
||||
<DollarSign className="h-3 w-3" />
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export function ChatMessageIdentityBar({
|
|||
<AgentIcon icon={agentIcon} className={`h-4 w-4 ${colorClass}`} />
|
||||
<span className={`text-[13px] font-semibold ${colorClass}`}>{agentName}</span>
|
||||
{isStreaming && (
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-cyan-400 animate-pulse" />
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-primary animate-pulse" />
|
||||
)}
|
||||
{timestamp && (
|
||||
<span className="text-[11px] text-muted-foreground">
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ function HighlightedText({ text, query }: { text: string; query: string }) {
|
|||
<>
|
||||
{segments.map((seg, i) =>
|
||||
seg.highlight ? (
|
||||
<mark key={i} className="bg-yellow-200 dark:bg-yellow-800 rounded-sm">
|
||||
<mark key={i} className="bg-warning/15 rounded-sm">
|
||||
{seg.text}
|
||||
</mark>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export function ChatStatusUpdateBadge({ agentName, taskId, taskTitle, taskUrl }:
|
|||
)}
|
||||
role="status"
|
||||
>
|
||||
<CheckCircle2 className="h-3.5 w-3.5 text-green-500 dark:text-green-400" />
|
||||
<CheckCircle2 className="h-3.5 w-3.5 text-success" />
|
||||
<span className="text-foreground">
|
||||
{agentName} completed {taskId}
|
||||
{displayTitle ? `: ${displayTitle}` : ""}
|
||||
|
|
|
|||
|
|
@ -45,9 +45,9 @@ function orderedWindows(windows: QuotaWindow[]): QuotaWindow[] {
|
|||
}
|
||||
|
||||
function fillClass(usedPercent: number | null): string {
|
||||
if (usedPercent == null) return "bg-zinc-700";
|
||||
if (usedPercent >= 90) return "bg-red-400";
|
||||
if (usedPercent >= 70) return "bg-amber-400";
|
||||
if (usedPercent == null) return "bg-muted";
|
||||
if (usedPercent >= 90) return "bg-destructive";
|
||||
if (usedPercent >= 70) return "bg-warning";
|
||||
return "bg-primary/70";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,9 +41,9 @@ function detailText(window: QuotaWindow): string | null {
|
|||
}
|
||||
|
||||
function fillClass(usedPercent: number | null): string {
|
||||
if (usedPercent == null) return "bg-zinc-700";
|
||||
if (usedPercent >= 90) return "bg-red-400";
|
||||
if (usedPercent >= 70) return "bg-amber-400";
|
||||
if (usedPercent == null) return "bg-muted";
|
||||
if (usedPercent >= 90) return "bg-destructive";
|
||||
if (usedPercent >= 70) return "bg-warning";
|
||||
return "bg-primary/70";
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -183,17 +183,17 @@ function runTimestamp(run: LinkedRunItem) {
|
|||
function runStatusClass(status: string) {
|
||||
switch (status) {
|
||||
case "succeeded":
|
||||
return "text-green-700 dark:text-green-300";
|
||||
return "text-success";
|
||||
case "failed":
|
||||
case "error":
|
||||
return "text-red-700 dark:text-red-300";
|
||||
return "text-destructive";
|
||||
case "timed_out":
|
||||
return "text-orange-700 dark:text-orange-300";
|
||||
return "text-warning";
|
||||
case "running":
|
||||
return "text-cyan-700 dark:text-cyan-300";
|
||||
return "text-primary";
|
||||
case "queued":
|
||||
case "pending":
|
||||
return "text-amber-700 dark:text-amber-300";
|
||||
return "text-warning";
|
||||
case "cancelled":
|
||||
return "text-muted-foreground";
|
||||
default:
|
||||
|
|
@ -258,7 +258,7 @@ function CommentCard({
|
|||
id={`comment-${comment.id}`}
|
||||
className={`border p-3 overflow-hidden min-w-0 rounded-sm transition-colors duration-1000 ${
|
||||
isQueued
|
||||
? "border-amber-300/70 bg-amber-50/70 dark:border-amber-500/40 dark:bg-amber-500/10"
|
||||
? "border-warning/70 bg-warning/70"
|
||||
: isHighlighted
|
||||
? "border-primary/50 bg-primary/5"
|
||||
: "border-border"
|
||||
|
|
@ -277,7 +277,7 @@ function CommentCard({
|
|||
)}
|
||||
<span className="flex items-center gap-1.5">
|
||||
{isQueued ? (
|
||||
<span className="inline-flex items-center rounded-full border border-amber-400/60 bg-amber-100/70 px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.14em] text-amber-800 dark:border-amber-400/40 dark:bg-amber-500/20 dark:text-amber-200">
|
||||
<span className="inline-flex items-center rounded-full border border-warning/60 bg-warning/70 px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.14em] text-warning">
|
||||
Queued
|
||||
</span>
|
||||
) : null}
|
||||
|
|
@ -765,14 +765,14 @@ export function CommentThread({
|
|||
{queuedComments.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-[0.14em] text-amber-700 dark:text-amber-300">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-[0.14em] text-warning">
|
||||
Queued Comments ({queuedComments.length})
|
||||
</h4>
|
||||
{onInterruptQueued && queuedComments[0]?.queueTargetRunId ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="border-red-300 text-red-700 hover:bg-red-50 hover:text-red-800 dark:border-red-500/40 dark:text-red-300 dark:hover:bg-red-500/10"
|
||||
className="border-destructive/30 text-destructive hover:bg-destructive/10 hover:text-destructive hover:bg-destructive/10"
|
||||
disabled={interruptingQueuedRunId === queuedComments[0].queueTargetRunId}
|
||||
onClick={() => void onInterruptQueued(queuedComments[0]!.queueTargetRunId!)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -134,13 +134,13 @@ function SortableCompanyItem({
|
|||
{hasLiveAgents && (
|
||||
<span className="pointer-events-none absolute -right-0.5 -top-0.5 z-10">
|
||||
<span className="relative flex h-2.5 w-2.5">
|
||||
<span className="absolute inline-flex h-full w-full animate-pulse rounded-full bg-blue-400 opacity-80" />
|
||||
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-blue-500 ring-2 ring-background" />
|
||||
<span className="absolute inline-flex h-full w-full animate-pulse rounded-full bg-primary opacity-80" />
|
||||
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-primary ring-2 ring-background" />
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
{hasUnreadInbox && (
|
||||
<span className="pointer-events-none absolute -bottom-0.5 -right-0.5 z-10 h-2.5 w-2.5 rounded-full bg-red-500 ring-2 ring-background" />
|
||||
<span className="pointer-events-none absolute -bottom-0.5 -right-0.5 z-10 h-2.5 w-2.5 rounded-full bg-destructive ring-2 ring-background" />
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -15,13 +15,13 @@ import { Button } from "@/components/ui/button";
|
|||
function statusDotColor(status?: string): string {
|
||||
switch (status) {
|
||||
case "active":
|
||||
return "bg-green-400";
|
||||
return "bg-success";
|
||||
case "paused":
|
||||
return "bg-yellow-400";
|
||||
return "bg-warning";
|
||||
case "archived":
|
||||
return "bg-neutral-400";
|
||||
return "bg-muted";
|
||||
default:
|
||||
return "bg-green-400";
|
||||
return "bg-success";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,14 +33,14 @@ export function DevRestartBanner({ devServer }: { devServer?: DevServerHealthSta
|
|||
const sample = devServer.changedPathsSample.slice(0, 3);
|
||||
|
||||
return (
|
||||
<div className="border-b border-amber-300/60 bg-amber-50 text-amber-950 dark:border-amber-500/25 dark:bg-amber-500/10 dark:text-amber-100">
|
||||
<div className="border-b border-warning/60 bg-warning/10 text-warning">
|
||||
<div className="flex flex-col gap-3 px-3 py-2.5 md:flex-row md:items-center md:justify-between">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2 text-[12px] font-semibold uppercase tracking-[0.18em]">
|
||||
<AlertTriangle className="h-3.5 w-3.5 shrink-0" />
|
||||
<span>Restart Required</span>
|
||||
{devServer.autoRestartEnabled ? (
|
||||
<span className="rounded-full bg-amber-900/10 px-2 py-0.5 text-[10px] tracking-[0.14em] dark:bg-amber-100/10">
|
||||
<span className="rounded-full bg-warning/10 px-2 py-0.5 text-[10px] tracking-[0.14em]">
|
||||
Auto-Restart On
|
||||
</span>
|
||||
) : null}
|
||||
|
|
@ -49,7 +49,7 @@ export function DevRestartBanner({ devServer }: { devServer?: DevServerHealthSta
|
|||
{describeReason(devServer)}
|
||||
{changedAt ? ` · updated ${changedAt}` : ""}
|
||||
</p>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-amber-900/80 dark:text-amber-100/75">
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-warning">
|
||||
{sample.length > 0 ? (
|
||||
<span>
|
||||
Changed: {sample.join(", ")}
|
||||
|
|
@ -67,17 +67,17 @@ export function DevRestartBanner({ devServer }: { devServer?: DevServerHealthSta
|
|||
|
||||
<div className="flex shrink-0 items-center gap-2 text-xs font-medium">
|
||||
{devServer.waitingForIdle ? (
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-amber-900/10 px-3 py-1.5 dark:bg-amber-100/10">
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-warning/10 px-3 py-1.5">
|
||||
<TimerReset className="h-3.5 w-3.5" />
|
||||
<span>Waiting for {devServer.activeRunCount} live run{devServer.activeRunCount === 1 ? "" : "s"} to finish</span>
|
||||
</div>
|
||||
) : devServer.autoRestartEnabled ? (
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-amber-900/10 px-3 py-1.5 dark:bg-amber-100/10">
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-warning/10 px-3 py-1.5">
|
||||
<RotateCcw className="h-3.5 w-3.5" />
|
||||
<span>Auto-restart will trigger when the instance is idle</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-amber-900/10 px-3 py-1.5 dark:bg-amber-100/10">
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-warning/10 px-3 py-1.5">
|
||||
<RotateCcw className="h-3.5 w-3.5" />
|
||||
<span>Restart <code>pnpm dev:once</code> after the active work is safe to interrupt</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@ function readinessTone(state: "ready" | "ready_with_warnings" | "blocked") {
|
|||
return "border-destructive/30 bg-destructive/5 text-destructive";
|
||||
}
|
||||
if (state === "ready_with_warnings") {
|
||||
return "border-amber-500/30 bg-amber-500/10 text-amber-800 dark:text-amber-300";
|
||||
return "border-warning/30 bg-warning/10 text-warning";
|
||||
}
|
||||
return "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300";
|
||||
return "border-success/30 bg-success/10 text-success";
|
||||
}
|
||||
|
||||
export function ExecutionWorkspaceCloseDialog({
|
||||
|
|
@ -163,7 +163,7 @@ export function ExecutionWorkspaceCloseDialog({
|
|||
<h3 className="text-sm font-medium">Warnings</h3>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
{readiness.warnings.map((warning, idx) => (
|
||||
<li key={`warning-${idx}`} className="break-words rounded-lg border border-amber-500/20 bg-amber-500/5 px-3 py-2">
|
||||
<li key={`warning-${idx}`} className="break-words rounded-lg border border-warning/20 bg-warning/5 px-3 py-2">
|
||||
{warning}
|
||||
</li>
|
||||
))}
|
||||
|
|
@ -262,7 +262,7 @@ export function ExecutionWorkspaceCloseDialog({
|
|||
</section>
|
||||
|
||||
{currentStatus === "cleanup_failed" ? (
|
||||
<div className="rounded-xl border border-amber-500/20 bg-amber-500/5 px-4 py-3 text-sm text-muted-foreground">
|
||||
<div className="rounded-xl border border-warning/20 bg-warning/5 px-4 py-3 text-sm text-muted-foreground">
|
||||
Cleanup previously failed on this workspace. Retrying close will rerun the cleanup flow and update the
|
||||
workspace status if it succeeds.
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export function FinanceTimelineCard({
|
|||
<div className="text-right tabular-nums">
|
||||
<div className="text-sm font-semibold">{formatCents(row.amountCents)}</div>
|
||||
<div className="text-xs text-muted-foreground">{row.currency}</div>
|
||||
{row.estimated ? <div className="text-[11px] uppercase tracking-[0.12em] text-amber-600">estimated</div> : null}
|
||||
{row.estimated ? <div className="text-[11px] uppercase tracking-[0.12em] text-warning">estimated</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -764,13 +764,13 @@ export function IssueDocumentsSection({
|
|||
<div
|
||||
id="document-plan"
|
||||
className={cn(
|
||||
"rounded-lg border border-amber-500/30 bg-amber-500/5 p-3 transition-colors duration-1000",
|
||||
"rounded-lg border border-warning/30 bg-warning/5 p-3 transition-colors duration-1000",
|
||||
highlightDocumentKey === "plan" && "border-primary/50 bg-primary/5",
|
||||
)}
|
||||
>
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<FileText className="h-4 w-4 text-amber-600" />
|
||||
<span className="rounded-full border border-amber-500/30 px-2 py-0.5 font-mono text-[10px] uppercase tracking-[0.16em] text-amber-700 dark:text-amber-300">
|
||||
<FileText className="h-4 w-4 text-warning" />
|
||||
<span className="rounded-full border border-warning/30 px-2 py-0.5 font-mono text-[10px] uppercase tracking-[0.16em] text-warning">
|
||||
PLAN
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -834,7 +834,7 @@ export function IssueDocumentsSection({
|
|||
size="sm"
|
||||
className={cn(
|
||||
"h-auto px-1.5 py-0 text-[11px] font-normal text-muted-foreground hover:text-foreground",
|
||||
isHistoricalPreview && "text-amber-300 hover:text-amber-200",
|
||||
isHistoricalPreview && "text-warning hover:text-warning",
|
||||
)}
|
||||
>
|
||||
rev {displayedRevisionNumber}
|
||||
|
|
@ -963,10 +963,10 @@ export function IssueDocumentsSection({
|
|||
: undefined}
|
||||
>
|
||||
{isHistoricalPreview && selectedHistoricalRevision && (
|
||||
<div className="rounded-md border border-amber-500/30 bg-amber-500/5 px-3 py-3">
|
||||
<div className="rounded-md border border-warning/30 bg-warning/5 px-3 py-3">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-amber-200">
|
||||
<p className="text-sm font-medium text-warning">
|
||||
Viewing revision {selectedHistoricalRevision.revisionNumber}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
|
|
@ -998,10 +998,10 @@ export function IssueDocumentsSection({
|
|||
</div>
|
||||
)}
|
||||
{activeConflict && !isHistoricalPreview && (
|
||||
<div className="rounded-md border border-amber-500/30 bg-amber-500/5 px-3 py-3">
|
||||
<div className="rounded-md border border-warning/30 bg-warning/5 px-3 py-3">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-amber-200">Out of date</p>
|
||||
<p className="text-sm font-medium text-warning">Out of date</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
This document changed while you were editing. Your local draft is preserved and autosave is paused.
|
||||
</p>
|
||||
|
|
@ -1074,7 +1074,7 @@ export function IssueDocumentsSection({
|
|||
}`}
|
||||
>
|
||||
{isHistoricalPreview ? (
|
||||
<div className="rounded-md border border-amber-500/20 bg-background/50 p-3">
|
||||
<div className="rounded-md border border-warning/20 bg-background/50 p-3">
|
||||
{renderBody(displayedBody, documentBodyContentClassName)}
|
||||
</div>
|
||||
) : activeDraft ? (
|
||||
|
|
@ -1107,9 +1107,9 @@ export function IssueDocumentsSection({
|
|||
<span
|
||||
className={`text-[11px] transition-opacity duration-150 ${
|
||||
isHistoricalPreview
|
||||
? "text-amber-300"
|
||||
? "text-warning"
|
||||
: activeConflict
|
||||
? "text-amber-300"
|
||||
? "text-warning"
|
||||
: autosaveState === "error"
|
||||
? "text-destructive"
|
||||
: "text-muted-foreground"
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
|
|||
const [labelsOpen, setLabelsOpen] = useState(false);
|
||||
const [labelSearch, setLabelSearch] = useState("");
|
||||
const [newLabelName, setNewLabelName] = useState("");
|
||||
const [newLabelColor, setNewLabelColor] = useState("#6366f1");
|
||||
const [newLabelColor, setNewLabelColor] = useState("var(--primary)");
|
||||
|
||||
const { data: session } = useQuery({
|
||||
queryKey: queryKeys.auth.session,
|
||||
|
|
@ -411,7 +411,7 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
|
|||
<>
|
||||
<span
|
||||
className="shrink-0 h-3 w-3 rounded-sm"
|
||||
style={{ backgroundColor: orderedProjects.find((p) => p.id === issue.projectId)?.color ?? "#6366f1" }}
|
||||
style={{ backgroundColor: orderedProjects.find((p) => p.id === issue.projectId)?.color ?? "var(--primary)" }}
|
||||
/>
|
||||
<span className="text-sm truncate">{projectName(issue.projectId)}</span>
|
||||
</>
|
||||
|
|
@ -479,7 +479,7 @@ export function IssueProperties({ issue, onUpdate, inline }: IssuePropertiesProp
|
|||
>
|
||||
<span
|
||||
className="shrink-0 h-3 w-3 rounded-sm"
|
||||
style={{ backgroundColor: p.color ?? "#6366f1" }}
|
||||
style={{ backgroundColor: p.color ?? "var(--primary)" }}
|
||||
/>
|
||||
{p.name}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -116,14 +116,14 @@ export function IssueRow({
|
|||
}}
|
||||
className={cn(
|
||||
"inline-flex h-4 w-4 items-center justify-center rounded-full transition-colors",
|
||||
selected ? "hover:bg-muted/80" : "hover:bg-blue-500/20",
|
||||
selected ? "hover:bg-muted/80" : "hover:bg-primary/20",
|
||||
)}
|
||||
aria-label="Mark as read"
|
||||
>
|
||||
<span
|
||||
className={cn(
|
||||
"block h-2 w-2 rounded-full transition-opacity duration-300",
|
||||
selected ? "bg-muted-foreground/70" : "bg-blue-600 dark:bg-blue-400",
|
||||
selected ? "bg-muted-foreground/70" : "bg-primary",
|
||||
unreadState === "fading" ? "opacity-0" : "opacity-100",
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ function CopyableInline({ value, label, mono }: { value: string; label?: string;
|
|||
onClick={handleCopy}
|
||||
title={copied ? "Copied!" : "Copy"}
|
||||
>
|
||||
{copied ? <Check className="h-3 w-3 text-green-500" /> : <Copy className="h-3 w-3" />}
|
||||
{copied ? <Check className="h-3 w-3 text-success" /> : <Copy className="h-3 w-3" />}
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
|
|
@ -144,9 +144,9 @@ function workspaceDetailLink(input: {
|
|||
|
||||
function statusBadge(status: string) {
|
||||
const colors: Record<string, string> = {
|
||||
active: "bg-green-500/15 text-green-700 dark:text-green-400",
|
||||
active: "bg-success/15 text-success",
|
||||
idle: "bg-muted text-muted-foreground",
|
||||
in_review: "bg-blue-500/15 text-blue-700 dark:text-blue-400",
|
||||
in_review: "bg-primary/15 text-primary",
|
||||
archived: "bg-muted text-muted-foreground",
|
||||
};
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -400,7 +400,7 @@ export function IssuesList({
|
|||
{/* Filter */}
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className={`text-xs ${activeFilterCount > 0 ? "text-blue-600 dark:text-blue-400" : ""}`}>
|
||||
<Button variant="ghost" size="sm" className={`text-xs ${activeFilterCount > 0 ? "text-primary" : ""}`}>
|
||||
<Filter className="h-3.5 w-3.5 sm:h-3 sm:w-3 sm:mr-1" />
|
||||
<span className="hidden sm:inline">{activeFilterCount > 0 ? `Filters: ${activeFilterCount}` : "Filter"}</span>
|
||||
{activeFilterCount > 0 && (
|
||||
|
|
@ -734,12 +734,12 @@ export function IssuesList({
|
|||
{issue.identifier ?? issue.id.slice(0, 8)}
|
||||
</span>
|
||||
{liveIssueIds?.has(issue.id) && (
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-blue-500/10 px-1.5 py-0.5 sm:gap-1.5 sm:px-2">
|
||||
<span className="inline-flex items-center gap-1 rounded-full bg-primary/10 px-1.5 py-0.5 sm:gap-1.5 sm:px-2">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="absolute inline-flex h-full w-full animate-pulse rounded-full bg-blue-400 opacity-75" />
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-blue-500" />
|
||||
<span className="absolute inline-flex h-full w-full animate-pulse rounded-full bg-primary opacity-75" />
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-primary" />
|
||||
</span>
|
||||
<span className="hidden text-[11px] font-medium text-blue-600 dark:text-blue-400 sm:inline">
|
||||
<span className="hidden text-[11px] font-medium text-primary sm:inline">
|
||||
Live
|
||||
</span>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -154,8 +154,8 @@ function KanbanCard({
|
|||
</span>
|
||||
{isLive && (
|
||||
<span className="relative flex h-2 w-2 shrink-0 mt-0.5">
|
||||
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500" />
|
||||
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -87,9 +87,9 @@ export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
|
|||
if (runs.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-xl border border-cyan-500/25 bg-background/80 shadow-[0_18px_50px_rgba(6,182,212,0.08)]">
|
||||
<div className="border-b border-border/60 bg-cyan-500/[0.04] px-4 py-3">
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-cyan-700 dark:text-cyan-300">
|
||||
<div className="overflow-hidden rounded-xl border border-primary/25 bg-background/80 shadow-[0_18px_50px_rgba(6,182,212,0.08)]">
|
||||
<div className="border-b border-border/60 bg-primary/[0.04] px-4 py-3">
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.18em] text-primary">
|
||||
Live Runs
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
|
|
@ -111,7 +111,7 @@ export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
|
|||
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
<Link
|
||||
to={`/agents/${run.agentId}/runs/${run.id}`}
|
||||
className="inline-flex items-center rounded-full border border-border/70 bg-background/70 px-2 py-1 font-mono hover:border-cyan-500/30 hover:text-foreground"
|
||||
className="inline-flex items-center rounded-full border border-border/70 bg-background/70 px-2 py-1 font-mono hover:border-primary/30 hover:text-foreground"
|
||||
>
|
||||
{run.id.slice(0, 8)}
|
||||
</Link>
|
||||
|
|
@ -125,7 +125,7 @@ export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
|
|||
<button
|
||||
onClick={() => handleCancelRun(run.id)}
|
||||
disabled={cancellingRunIds.has(run.id)}
|
||||
className="inline-flex items-center gap-1 rounded-full border border-red-500/20 bg-red-500/[0.06] px-2.5 py-1 text-[11px] font-medium text-red-700 transition-colors hover:bg-red-500/[0.12] dark:text-red-300 disabled:opacity-50"
|
||||
className="inline-flex items-center gap-1 rounded-full border border-destructive/20 bg-destructive/[0.06] px-2.5 py-1 text-[11px] font-medium text-destructive transition-colors hover:bg-destructive/[0.12] disabled:opacity-50"
|
||||
>
|
||||
<Square className="h-2.5 w-2.5" fill="currentColor" />
|
||||
{cancellingRunIds.has(run.id) ? "Stopping…" : "Stop"}
|
||||
|
|
@ -133,7 +133,7 @@ export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
|
|||
)}
|
||||
<Link
|
||||
to={`/agents/${run.agentId}/runs/${run.id}`}
|
||||
className="inline-flex items-center gap-1 rounded-full border border-border/70 bg-background/70 px-2.5 py-1 text-[11px] font-medium text-cyan-700 transition-colors hover:border-cyan-500/30 hover:text-cyan-600 dark:text-cyan-300"
|
||||
className="inline-flex items-center gap-1 rounded-full border border-border/70 bg-background/70 px-2.5 py-1 text-[11px] font-medium text-primary transition-colors hover:border-primary/30 hover:text-primary"
|
||||
>
|
||||
Open run
|
||||
<ExternalLink className="h-3 w-3" />
|
||||
|
|
|
|||
|
|
@ -663,7 +663,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
|||
{option.kind === "project" && option.projectId ? (
|
||||
<span
|
||||
className="inline-flex h-2 w-2 rounded-full border border-border/50"
|
||||
style={{ backgroundColor: option.projectColor ?? "#64748b" }}
|
||||
style={{ backgroundColor: option.projectColor ?? "var(--muted-foreground)" }}
|
||||
/>
|
||||
) : (
|
||||
<AgentIcon
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ export function NewAgentDialog() {
|
|||
onClick={() => handleAdvancedAdapterPick(opt.value)}
|
||||
>
|
||||
{opt.recommended && (
|
||||
<span className="absolute -top-1.5 right-1.5 bg-green-500 text-white text-[9px] font-semibold px-1.5 py-0.5 rounded-full leading-none">
|
||||
<span className="absolute -top-1.5 right-1.5 bg-success text-white text-[9px] font-semibold px-1.5 py-0.5 rounded-full leading-none">
|
||||
Recommended
|
||||
</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1092,7 +1092,7 @@ export function NewIssueDialog() {
|
|||
<>
|
||||
<span
|
||||
className="h-3.5 w-3.5 shrink-0 rounded-sm"
|
||||
style={{ backgroundColor: currentProject.color ?? "#6366f1" }}
|
||||
style={{ backgroundColor: currentProject.color ?? "var(--primary)" }}
|
||||
/>
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
|
|
@ -1107,7 +1107,7 @@ export function NewIssueDialog() {
|
|||
<>
|
||||
<span
|
||||
className="h-3.5 w-3.5 shrink-0 rounded-sm"
|
||||
style={{ backgroundColor: project?.color ?? "#6366f1" }}
|
||||
style={{ backgroundColor: project?.color ?? "var(--primary)" }}
|
||||
/>
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
|
|
@ -1212,7 +1212,7 @@ export function NewIssueDialog() {
|
|||
data-slot="toggle"
|
||||
className={cn(
|
||||
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors",
|
||||
assigneeChrome ? "bg-green-600" : "bg-muted"
|
||||
assigneeChrome ? "bg-success" : "bg-muted"
|
||||
)}
|
||||
onClick={() => setAssigneeChrome((value) => !value)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// [nexus] Replacement onboarding wizard — 5-step flow: hardware detection, mode selection, provider selection, root directory, summary
|
||||
// [nexus] Replacement onboarding wizard — 6-step full-page flow: hardware detection, mode selection, provider selection, voice, phone access, summary
|
||||
// Exports `OnboardingWizard` to match the named import in App.tsx.
|
||||
// Wired via Vite alias: all imports of ./components/OnboardingWizard are
|
||||
// redirected here at build time; the original file is preserved for upstream rebase.
|
||||
|
|
@ -14,7 +14,6 @@ import { agentsApi } from "../api/agents";
|
|||
import { puterProxyApi } from "../api/puter-proxy";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { resolveRouteOnboardingOptions } from "../lib/onboarding-route";
|
||||
import { Dialog, DialogPortal } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { cn } from "../lib/utils";
|
||||
|
|
@ -27,19 +26,54 @@ import { TelegramStep } from "./onboarding/TelegramStep";
|
|||
import { useHardwareInfo } from "../hooks/useHardwareInfo";
|
||||
import { updateNexusSettings, type NexusMode } from "../api/hardware";
|
||||
import { useChatPanel } from "../context/ChatPanelContext";
|
||||
import {
|
||||
Cpu,
|
||||
LayoutGrid,
|
||||
Cloud,
|
||||
Mic,
|
||||
Smartphone,
|
||||
CheckCircle,
|
||||
Check,
|
||||
} from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
const ADAPTER_LABELS: Record<string, string> = {
|
||||
claude_local: "Claude Code (detected)",
|
||||
hermes_local: "Hermes (detected)",
|
||||
openclaw_gateway: "OpenClaw Gateway (detected)",
|
||||
};
|
||||
|
||||
function deriveProviderLabel(
|
||||
puterToken: string | null,
|
||||
googleOAuthStateId: string | null,
|
||||
apiKeyData: { provider: string; apiKey: string } | null,
|
||||
selectedAdapterChoice: string | null,
|
||||
): string {
|
||||
if (selectedAdapterChoice) return ADAPTER_LABELS[selectedAdapterChoice] ?? selectedAdapterChoice;
|
||||
if (puterToken) return "Puter (free, zero-config)";
|
||||
if (googleOAuthStateId) return "Google Gemini (free tier)";
|
||||
if (apiKeyData) return `API key — ${apiKeyData.provider}`;
|
||||
return "None selected";
|
||||
}
|
||||
|
||||
// [nexus] 7-step onboarding wizard: hardware detection → mode selection → provider selection → voice → telegram → root directory → summary
|
||||
interface StepDef {
|
||||
id: number;
|
||||
name: string;
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const STEPS: StepDef[] = [
|
||||
{ id: 1, name: "Hardware", icon: Cpu, title: "Your hardware", description: "Detecting what your machine can do." },
|
||||
{ id: 2, name: "Mode", icon: LayoutGrid, title: "Choose your mode", description: "How do you want to use Nexus?" },
|
||||
{ id: 3, name: "Provider", icon: Cloud, title: "Choose a provider", description: "No API keys needed for the zero-config path." },
|
||||
{ id: 4, name: "Voice", icon: Mic, title: "Voice features", description: "Speak to your assistant and hear responses read aloud. Runs entirely on your device." },
|
||||
{ id: 5, name: "Phone Access", icon: Smartphone, title: "Phone Access", description: "Get notifications and send quick replies from your phone." },
|
||||
{ id: 6, name: "Summary", icon: CheckCircle, title: "Ready to go", description: "Review your setup before starting." },
|
||||
];
|
||||
|
||||
// [nexus] 6-step onboarding wizard: hardware detection -> mode selection -> provider selection -> voice -> phone access -> summary
|
||||
export function OnboardingWizard() {
|
||||
const { onboardingOpen, onboardingOptions, closeOnboarding } = useDialog();
|
||||
const { companies, setSelectedCompanyId, loading: companiesLoading } = useCompany();
|
||||
|
|
@ -67,7 +101,7 @@ export function OnboardingWizard() {
|
|||
setRouteDismissed(false);
|
||||
}, [location.pathname]);
|
||||
|
||||
// Step state: 1 = hardware detection, 2 = mode selection, 3 = provider selection, 4 = voice, 5 = telegram, 6 = root directory, 7 = summary
|
||||
// Step state: 1 = hardware, 2 = mode, 3 = provider, 4 = voice, 5 = phone access, 6 = summary
|
||||
const [step, setStep] = useState(1);
|
||||
|
||||
// Mode state: "both" pre-selected per UI-SPEC
|
||||
|
|
@ -80,6 +114,7 @@ export function OnboardingWizard() {
|
|||
const [puterToken, setPuterToken] = useState<string | null>(null);
|
||||
const [googleOAuthStateId, setGoogleOAuthStateId] = useState<string | null>(null);
|
||||
const [apiKeyData, setApiKeyData] = useState<{ provider: string; apiKey: string } | null>(null);
|
||||
const [selectedAdapterChoice, setSelectedAdapterChoice] = useState<string | null>(null);
|
||||
|
||||
// Form state
|
||||
const [rootDir, setRootDir] = useState("");
|
||||
|
|
@ -131,12 +166,7 @@ export function OnboardingWizard() {
|
|||
}).catch(() => {}).finally(() => setProbing(false));
|
||||
}, [effectiveOnboardingOpen]);
|
||||
|
||||
function handleClose() {
|
||||
setRouteDismissed(true);
|
||||
closeOnboarding();
|
||||
}
|
||||
|
||||
// [nexus] Shared workspace creation logic used by both handleSubmit (step 4 direct) and handleStartChat (step 6)
|
||||
// [nexus] Shared workspace creation logic used by both handleSubmit and handleStartChat
|
||||
async function createWorkspace() {
|
||||
// Step 1: Create workspace (company) named after VOCAB.appName
|
||||
const company = await companiesApi.create({ name: VOCAB.appName });
|
||||
|
|
@ -263,7 +293,7 @@ export function OnboardingWizard() {
|
|||
async function handleStartChat() {
|
||||
// Guard: claude_local requires rootDir
|
||||
if (defaultAdapter === "claude_local" && !rootDir.trim()) {
|
||||
setError("Root directory is required for Claude Code. Go back to step 6 to set it.");
|
||||
setError("Root directory is required for Claude Code. Go back to step 5 to set it.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -283,265 +313,182 @@ export function OnboardingWizard() {
|
|||
}
|
||||
}
|
||||
|
||||
// Current step definition
|
||||
const currentStep = STEPS.find(s => s.id === step) ?? STEPS[0];
|
||||
|
||||
// Navigation helpers
|
||||
function goBack() {
|
||||
if (step > 1) setStep(step - 1);
|
||||
}
|
||||
|
||||
function goNext() {
|
||||
if (step < 6) setStep(step + 1);
|
||||
}
|
||||
|
||||
// Render step content
|
||||
function renderStepContent() {
|
||||
switch (step) {
|
||||
case 1:
|
||||
return (
|
||||
<HardwareSummaryStep
|
||||
hardwareInfo={hardwareInfo}
|
||||
isLoading={hwLoading}
|
||||
isError={hwError}
|
||||
/>
|
||||
);
|
||||
case 2:
|
||||
return <ModeSelector value={selectedMode} onChange={setSelectedMode} />;
|
||||
case 3:
|
||||
return (
|
||||
<ProviderSelectionStep
|
||||
onPuterToken={(token) => { setPuterToken(token); setSelectedAdapterChoice(null); }}
|
||||
onGoogleOAuthState={(id) => { setGoogleOAuthStateId(id); setSelectedAdapterChoice(null); }}
|
||||
onApiKey={(provider, apiKey) => { setApiKeyData({ provider, apiKey }); setSelectedAdapterChoice(null); }}
|
||||
onAdapterSelected={(adapter) => { setSelectedAdapterChoice(adapter); setPuterToken(null); setGoogleOAuthStateId(null); setApiKeyData(null); }}
|
||||
onSkip={goNext}
|
||||
onContinue={goNext}
|
||||
detectedAdapters={detectedAdapters}
|
||||
probing={probing}
|
||||
/>
|
||||
);
|
||||
case 4:
|
||||
return (
|
||||
<VoiceStep
|
||||
onEnable={() => {
|
||||
setVoiceEnabled(true);
|
||||
goNext();
|
||||
}}
|
||||
onSkip={goNext}
|
||||
voiceCapability={hardwareInfo?.voiceCapability}
|
||||
/>
|
||||
);
|
||||
case 5:
|
||||
return (
|
||||
<TelegramStep
|
||||
onNext={goNext}
|
||||
onBack={goBack}
|
||||
/>
|
||||
);
|
||||
case 6:
|
||||
return (
|
||||
<OnboardingSummaryStep
|
||||
hardwareInfo={hardwareInfo}
|
||||
selectedMode={selectedMode}
|
||||
providerLabel={deriveProviderLabel(puterToken, googleOAuthStateId, apiKeyData, selectedAdapterChoice)}
|
||||
rootDir={rootDir}
|
||||
onRootDirChange={setRootDir}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onStartChat={handleStartChat}
|
||||
onBack={goBack}
|
||||
voiceEnabled={voiceEnabled}
|
||||
defaultAdapter={defaultAdapter}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Steps 3, 4, 5 have their own navigation — don't render the shared footer
|
||||
const stepHasOwnNav = step === 3 || step === 4 || step === 5 || step === 6;
|
||||
|
||||
if (!effectiveOnboardingOpen) return null;
|
||||
|
||||
return (
|
||||
<DialogPortal>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
|
||||
onClick={handleClose}
|
||||
/>
|
||||
<div className="fixed inset-0 z-50 flex bg-background">
|
||||
{/* Left sidebar - step navigation (hidden on mobile) */}
|
||||
<aside className="hidden md:flex w-[260px] flex-col border-r border-border bg-card p-6">
|
||||
{/* Logo/title at top */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-lg font-bold">Nexus Setup</h1>
|
||||
<p className="text-xs text-muted-foreground mt-1">Configure your workspace</p>
|
||||
</div>
|
||||
|
||||
{/* Card */}
|
||||
<div
|
||||
className={cn(
|
||||
"relative z-10 w-full max-w-md mx-4 rounded-xl border bg-card text-card-foreground shadow-2xl",
|
||||
"p-8 flex flex-col gap-6"
|
||||
)}
|
||||
>
|
||||
{/* Step indicator */}
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
{step === 7 ? "Summary" : `Step ${step} of 6`}
|
||||
</p>
|
||||
{/* Step list */}
|
||||
<nav className="flex flex-col gap-1">
|
||||
{STEPS.map((s) => {
|
||||
const isCompleted = s.id < step;
|
||||
const isCurrent = s.id === step;
|
||||
const isUpcoming = s.id > step;
|
||||
const Icon = s.icon;
|
||||
|
||||
{/* Step 1 — Hardware Detection */}
|
||||
{step === 1 && (
|
||||
<>
|
||||
<div className="flex flex-col gap-2 text-center">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
{hwLoading ? "Detecting your hardware..." : "Your hardware"}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<HardwareSummaryStep
|
||||
hardwareInfo={hardwareInfo}
|
||||
isLoading={hwLoading}
|
||||
isError={hwError}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setStep(2)}
|
||||
className="w-full"
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setStep(2)}
|
||||
className="w-full"
|
||||
>
|
||||
Skip
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step 2 — Mode Selection */}
|
||||
{step === 2 && (
|
||||
<>
|
||||
<div className="flex flex-col gap-2 text-center">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
Choose your mode
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<ModeSelector value={selectedMode} onChange={setSelectedMode} />
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setStep(3)}
|
||||
className="w-full"
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setStep(1)}
|
||||
className="w-full"
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setStep(3)}
|
||||
className="w-full"
|
||||
>
|
||||
Skip
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step 3 — Provider Selection (NEW) */}
|
||||
{step === 3 && (
|
||||
<>
|
||||
<div className="flex flex-col gap-2 text-center">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
Choose a provider
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No API keys needed for the zero-config path.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ProviderSelectionStep
|
||||
onPuterToken={setPuterToken}
|
||||
onGoogleOAuthState={setGoogleOAuthStateId}
|
||||
onApiKey={(provider, apiKey) => setApiKeyData({ provider, apiKey })}
|
||||
onSkip={() => setStep(4)}
|
||||
onContinue={() => setStep(4)}
|
||||
detectedAdapters={detectedAdapters}
|
||||
/>
|
||||
|
||||
<Button
|
||||
return (
|
||||
<button
|
||||
key={s.id}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setStep(2)}
|
||||
className="w-full"
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step 4 — Voice */}
|
||||
{step === 4 && (
|
||||
<>
|
||||
<div className="flex flex-col gap-2 text-center">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
Voice features
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Speak to your assistant and hear responses read aloud. Runs entirely on your device.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<VoiceStep
|
||||
onEnable={() => {
|
||||
setVoiceEnabled(true);
|
||||
setStep(5);
|
||||
onClick={() => {
|
||||
if (isCompleted) setStep(s.id);
|
||||
}}
|
||||
onSkip={() => setStep(5)}
|
||||
voiceCapability={hardwareInfo?.voiceCapability}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setStep(3)}
|
||||
className="w-full"
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step 5 — Telegram Bridge */}
|
||||
{step === 5 && (
|
||||
<TelegramStep
|
||||
onNext={() => setStep(6)}
|
||||
onBack={() => setStep(4)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Step 6 — Root Directory (was step 5, now step 6) */}
|
||||
{step === 6 && (
|
||||
<>
|
||||
{/* Header */}
|
||||
<div className="flex flex-col gap-2 text-center">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
Welcome to {VOCAB.appName}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{defaultAdapter === "hermes_local"
|
||||
? `${VOCAB.appName} will set up a local AI workspace with a ${VOCAB.ceo.toLowerCase()}, engineer, and generalist — no API key needed.`
|
||||
: `Choose a project root directory. ${VOCAB.appName} will set up a ${VOCAB.ceo.toLowerCase()} and engineer to start working.`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label
|
||||
htmlFor="nexus-root-dir"
|
||||
className="text-sm font-medium leading-none"
|
||||
>
|
||||
Project root directory{defaultAdapter === "hermes_local" ? " (optional)" : ""}
|
||||
</label>
|
||||
<Input
|
||||
id="nexus-root-dir"
|
||||
type="text"
|
||||
placeholder="~/projects/my-project"
|
||||
value={rootDir}
|
||||
onChange={(e) => setRootDir(e.target.value)}
|
||||
disabled={loading}
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<p className="text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2">
|
||||
{error}
|
||||
</p>
|
||||
disabled={isUpcoming}
|
||||
className={cn(
|
||||
"flex items-center gap-3 px-3 py-2.5 text-sm rounded-lg transition-colors text-left",
|
||||
isCurrent && "bg-primary/10 text-primary font-medium",
|
||||
isCompleted && "text-foreground hover:bg-muted/50 cursor-pointer",
|
||||
isUpcoming && "text-muted-foreground/50 cursor-default"
|
||||
)}
|
||||
>
|
||||
{isCompleted ? (
|
||||
<Check className="h-4 w-4 shrink-0 text-[color:var(--chart-2)]" />
|
||||
) : (
|
||||
<Icon className={cn(
|
||||
"h-4 w-4 shrink-0",
|
||||
isCurrent ? "text-primary" : "text-muted-foreground/50"
|
||||
)} />
|
||||
)}
|
||||
<span>{s.name}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setStep(7)}
|
||||
disabled={loading || probing}
|
||||
className="w-full"
|
||||
>
|
||||
Review & finish
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setStep(5)}
|
||||
className="w-full"
|
||||
disabled={loading}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setStep(7)}
|
||||
className="w-full"
|
||||
disabled={loading}
|
||||
>
|
||||
Skip to summary
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Step 7 — Summary (was step 6) */}
|
||||
{step === 7 && (
|
||||
<OnboardingSummaryStep
|
||||
hardwareInfo={hardwareInfo}
|
||||
selectedMode={selectedMode}
|
||||
providerLabel={deriveProviderLabel(puterToken, googleOAuthStateId, apiKeyData)}
|
||||
rootDir={rootDir}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onStartChat={handleStartChat}
|
||||
onBack={() => setStep(6)}
|
||||
{/* Mobile step indicator - horizontal bar at top */}
|
||||
<div className="md:hidden fixed top-0 left-0 right-0 z-50 bg-card border-b border-border px-4 py-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium">Nexus Setup</span>
|
||||
<span className="text-xs text-muted-foreground">Step {step} of 6</span>
|
||||
</div>
|
||||
<div className="flex gap-1.5">
|
||||
{STEPS.map((s) => (
|
||||
<div
|
||||
key={s.id}
|
||||
className={cn(
|
||||
"h-1 flex-1 rounded-full transition-colors",
|
||||
s.id < step && "bg-[color:var(--chart-2)]",
|
||||
s.id === step && "bg-primary",
|
||||
s.id > step && "bg-muted"
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</DialogPortal>
|
||||
|
||||
{/* Right content area */}
|
||||
<main className="flex-1 flex flex-col overflow-y-auto">
|
||||
<div className="flex-1 flex items-start justify-center p-8 md:p-12 pt-20 md:pt-12">
|
||||
<div className="w-full max-w-[640px]">
|
||||
{/* Step heading */}
|
||||
<h2 className="text-2xl md:text-3xl font-bold mb-2">{currentStep.title}</h2>
|
||||
<p className="text-base text-muted-foreground mb-8">{currentStep.description}</p>
|
||||
|
||||
{/* Step content */}
|
||||
{renderStepContent()}
|
||||
|
||||
{/* Shared navigation footer — only for steps 1 and 2 */}
|
||||
{!stepHasOwnNav && (
|
||||
<div className="flex items-center justify-between mt-8 pt-6 border-t border-border">
|
||||
{step > 1 ? (
|
||||
<Button variant="ghost" size="sm" onClick={goBack}>Back</Button>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<Button className="ml-auto" onClick={goNext}>Continue</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export function OfflineBanner({ queuedCount = 0 }: OfflineBannerProps) {
|
|||
|
||||
return (
|
||||
<div
|
||||
className="fixed top-0 left-0 right-0 z-50 px-4 py-2 text-sm flex items-center gap-2 bg-amber-50 text-amber-800 border-b border-amber-200 dark:bg-amber-900/40 dark:text-amber-200 dark:border-amber-800"
|
||||
className="fixed top-0 left-0 right-0 z-50 px-4 py-2 text-sm flex items-center gap-2 bg-warning/10 text-warning border-b border-warning/30"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -796,7 +796,7 @@ export function OnboardingWizard() {
|
|||
}}
|
||||
>
|
||||
{opt.recommended && (
|
||||
<span className="absolute -top-1.5 right-1.5 bg-green-500 text-white text-[9px] font-semibold px-1.5 py-0.5 rounded-full leading-none">
|
||||
<span className="absolute -top-1.5 right-1.5 bg-success text-white text-[9px] font-semibold px-1.5 py-0.5 rounded-full leading-none">
|
||||
Recommended
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -1050,7 +1050,7 @@ export function OnboardingWizard() {
|
|||
|
||||
{adapterEnvResult &&
|
||||
adapterEnvResult.status === "pass" ? (
|
||||
<div className="flex items-center gap-2 rounded-md border border-green-300 dark:border-green-500/40 bg-green-50 dark:bg-green-500/10 px-3 py-2 text-xs text-green-700 dark:text-green-300 animate-in fade-in slide-in-from-bottom-1 duration-300">
|
||||
<div className="flex items-center gap-2 rounded-md border border-success/30 bg-success/10 px-3 py-2 text-xs text-success animate-in fade-in slide-in-from-bottom-1 duration-300">
|
||||
<Check className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="font-medium">Passed</span>
|
||||
</div>
|
||||
|
|
@ -1059,8 +1059,8 @@ export function OnboardingWizard() {
|
|||
) : null}
|
||||
|
||||
{shouldSuggestUnsetAnthropicApiKey && (
|
||||
<div className="rounded-md border border-amber-300/60 bg-amber-50/40 px-2.5 py-2 space-y-2">
|
||||
<p className="text-[11px] text-amber-900/90 leading-relaxed">
|
||||
<div className="rounded-md border border-warning/60 bg-warning/40 px-2.5 py-2 space-y-2">
|
||||
<p className="text-[11px] text-warning leading-relaxed">
|
||||
Claude failed while{" "}
|
||||
<span className="font-mono">ANTHROPIC_API_KEY</span>{" "}
|
||||
is set. You can clear it in this {VOCAB.ceo} adapter config
|
||||
|
|
@ -1224,7 +1224,7 @@ export function OnboardingWizard() {
|
|||
</p>
|
||||
<p className="text-xs text-muted-foreground">{VOCAB.company}</p>
|
||||
</div>
|
||||
<Check className="h-4 w-4 text-green-500 shrink-0" />
|
||||
<Check className="h-4 w-4 text-success shrink-0" />
|
||||
</div>
|
||||
<div className="flex items-center gap-3 px-3 py-2.5">
|
||||
<Bot className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
|
|
@ -1236,7 +1236,7 @@ export function OnboardingWizard() {
|
|||
{getUIAdapter(adapterType).label}
|
||||
</p>
|
||||
</div>
|
||||
<Check className="h-4 w-4 text-green-500 shrink-0" />
|
||||
<Check className="h-4 w-4 text-success shrink-0" />
|
||||
</div>
|
||||
<div className="flex items-center gap-3 px-3 py-2.5">
|
||||
<ListTodo className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
|
|
@ -1246,7 +1246,7 @@ export function OnboardingWizard() {
|
|||
</p>
|
||||
<p className="text-xs text-muted-foreground">Task</p>
|
||||
</div>
|
||||
<Check className="h-4 w-4 text-green-500 shrink-0" />
|
||||
<Check className="h-4 w-4 text-success shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1337,7 +1337,7 @@ export function OnboardingWizard() {
|
|||
{/* Right half — ASCII art (hidden on mobile) */}
|
||||
<div
|
||||
className={cn(
|
||||
"hidden md:block overflow-hidden bg-[#1d1d1d] transition-[width,opacity] duration-500 ease-in-out",
|
||||
"hidden md:block overflow-hidden bg-card transition-[width,opacity] duration-500 ease-in-out",
|
||||
step === 1 ? "w-1/2 opacity-100" : "w-0 opacity-0"
|
||||
)}
|
||||
>
|
||||
|
|
@ -1362,10 +1362,10 @@ function AdapterEnvironmentResult({
|
|||
: "Failed";
|
||||
const statusClass =
|
||||
result.status === "pass"
|
||||
? "text-green-700 dark:text-green-300 border-green-300 dark:border-green-500/40 bg-green-50 dark:bg-green-500/10"
|
||||
? "text-success border-success/30 bg-success/10"
|
||||
: result.status === "warn"
|
||||
? "text-amber-700 dark:text-amber-300 border-amber-300 dark:border-amber-500/40 bg-amber-50 dark:bg-amber-500/10"
|
||||
: "text-red-700 dark:text-red-300 border-red-300 dark:border-red-500/40 bg-red-50 dark:bg-red-500/10";
|
||||
? "text-warning border-warning/30 bg-warning/10"
|
||||
: "text-destructive border-destructive/30 bg-destructive/10";
|
||||
|
||||
return (
|
||||
<div className={`rounded-md border px-2.5 py-2 text-[11px] ${statusClass}`}>
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ export function OutputFeedbackButtons({
|
|||
size="sm"
|
||||
variant="outline"
|
||||
disabled={disabled || isSaving}
|
||||
className={cn(visibleVote === "up" && "border-green-600/50 bg-green-500/10 text-green-700")}
|
||||
className={cn(visibleVote === "up" && "border-success/50 bg-success/10 text-success")}
|
||||
onClick={() => handleVote("up")}
|
||||
>
|
||||
<ThumbsUp className="mr-1.5 h-3.5 w-3.5" />
|
||||
|
|
@ -126,7 +126,7 @@ export function OutputFeedbackButtons({
|
|||
size="sm"
|
||||
variant="outline"
|
||||
disabled={disabled || isSaving}
|
||||
className={cn(visibleVote === "down" && "border-amber-600/50 bg-amber-500/10 text-amber-800")}
|
||||
className={cn(visibleVote === "down" && "border-warning/50 bg-warning/10 text-warning")}
|
||||
onClick={() => handleVote("down")}
|
||||
>
|
||||
<ThumbsDown className="mr-1.5 h-3.5 w-3.5" />
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ function SaveIndicator({ state }: { state: ProjectFieldSaveState }) {
|
|||
}
|
||||
if (state === "saved") {
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1 text-[11px] text-green-600 dark:text-green-400">
|
||||
<span className="inline-flex items-center gap-1 text-[11px] text-success">
|
||||
<Check className="h-3 w-3" />
|
||||
Saved
|
||||
</span>
|
||||
|
|
@ -738,9 +738,9 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
|
|||
className={cn(
|
||||
"rounded-full px-1.5 py-0.5 text-[10px] uppercase tracking-wide",
|
||||
service.status === "running"
|
||||
? "bg-green-500/15 text-green-700 dark:text-green-300"
|
||||
? "bg-success/15 text-success"
|
||||
: service.status === "failed"
|
||||
? "bg-red-500/15 text-red-700 dark:text-red-300"
|
||||
? "bg-destructive/15 text-destructive"
|
||||
: "bg-muted text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
|
|
@ -891,7 +891,7 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
|
|||
data-slot="toggle"
|
||||
className={cn(
|
||||
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors",
|
||||
executionWorkspacesEnabled ? "bg-green-600" : "bg-muted",
|
||||
executionWorkspacesEnabled ? "bg-success" : "bg-muted",
|
||||
)}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
|
|
@ -930,7 +930,7 @@ export function ProjectProperties({ project, onUpdate, onFieldUpdate, getFieldSa
|
|||
data-slot="toggle"
|
||||
className={cn(
|
||||
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors",
|
||||
executionWorkspaceDefaultMode === "isolated_workspace" ? "bg-green-600" : "bg-muted",
|
||||
executionWorkspaceDefaultMode === "isolated_workspace" ? "bg-success" : "bg-muted",
|
||||
)}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
|
|
|
|||
|
|
@ -338,10 +338,10 @@ export function ProviderQuotaCard({
|
|||
qw.usedPercent == null
|
||||
? null
|
||||
: qw.usedPercent >= 90
|
||||
? "bg-red-400"
|
||||
? "bg-destructive"
|
||||
: qw.usedPercent >= 70
|
||||
? "bg-yellow-400"
|
||||
: "bg-green-400";
|
||||
? "bg-warning"
|
||||
: "bg-success";
|
||||
return (
|
||||
<div key={qw.label} className="space-y-1">
|
||||
<div className="flex items-center justify-between gap-2 text-xs">
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ interface QuotaBarProps {
|
|||
}
|
||||
|
||||
function fillColor(pct: number): string {
|
||||
if (pct > 90) return "bg-red-400";
|
||||
if (pct > 70) return "bg-yellow-400";
|
||||
return "bg-green-400";
|
||||
if (pct > 90) return "bg-destructive";
|
||||
if (pct > 70) return "bg-warning";
|
||||
return "bg-success";
|
||||
}
|
||||
|
||||
export function QuotaBar({
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export function ReportsToPicker({
|
|||
type="button"
|
||||
className={cn(
|
||||
"inline-flex max-w-full min-w-0 items-center gap-1.5 overflow-hidden rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors",
|
||||
terminatedManager && "border-amber-600/45 bg-amber-500/5",
|
||||
terminatedManager && "border-warning/45 bg-warning/5",
|
||||
disabled && "opacity-60 cursor-not-allowed",
|
||||
)}
|
||||
disabled={disabled}
|
||||
|
|
@ -61,7 +61,7 @@ export function ReportsToPicker({
|
|||
<span
|
||||
className={cn(
|
||||
"min-w-0 truncate",
|
||||
terminatedManager && "text-amber-900 dark:text-amber-200",
|
||||
terminatedManager && "text-warning",
|
||||
)}
|
||||
>
|
||||
{`Reports to ${current.name}${terminatedManager ? " (terminated)" : ""}`}
|
||||
|
|
|
|||
|
|
@ -274,11 +274,11 @@ export function RoutineRunVariablesDialog({
|
|||
|
||||
<DialogFooter showCloseButton={false}>
|
||||
{missingRequired.length > 0 ? (
|
||||
<p className="mr-auto text-xs text-amber-600">
|
||||
<p className="mr-auto text-xs text-warning">
|
||||
Missing: {missingRequired.join(", ")}
|
||||
</p>
|
||||
) : workspaceSelectionEnabled && !workspaceConfigValid ? (
|
||||
<p className="mr-auto text-xs text-amber-600">
|
||||
<p className="mr-auto text-xs text-warning">
|
||||
Choose an existing workspace before running.
|
||||
</p>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -124,12 +124,12 @@ export function SidebarAgents() {
|
|||
) : null}
|
||||
{runCount > 0 ? (
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500" />
|
||||
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
|
||||
</span>
|
||||
) : null}
|
||||
{runCount > 0 ? (
|
||||
<span className="text-[11px] font-medium text-blue-600 dark:text-blue-400">
|
||||
<span className="text-[11px] font-medium text-primary">
|
||||
{runCount} live
|
||||
</span>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export function SidebarNavItem({
|
|||
<span className="relative shrink-0">
|
||||
<Icon className="h-4 w-4" />
|
||||
{alert && (
|
||||
<span className="absolute -right-0.5 -top-0.5 h-2 w-2 rounded-full bg-red-500 shadow-[0_0_0_2px_hsl(var(--background))]" />
|
||||
<span className="absolute -right-0.5 -top-0.5 h-2 w-2 rounded-full bg-destructive shadow-[0_0_0_2px_hsl(var(--background))]" />
|
||||
)}
|
||||
</span>
|
||||
<span className="flex-1 truncate">{label}</span>
|
||||
|
|
@ -59,7 +59,7 @@ export function SidebarNavItem({
|
|||
className={cn(
|
||||
"ml-auto rounded-full px-1.5 py-0.5 text-[10px] font-medium leading-none",
|
||||
textBadgeTone === "amber"
|
||||
? "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400"
|
||||
? "bg-warning/10 text-warning"
|
||||
: "bg-muted text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
|
|
@ -69,10 +69,10 @@ export function SidebarNavItem({
|
|||
{liveCount != null && liveCount > 0 && (
|
||||
<span className="ml-auto flex items-center gap-1.5">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500" />
|
||||
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
|
||||
</span>
|
||||
<span className="text-[11px] font-medium text-blue-600 dark:text-blue-400">{liveCount} live</span>
|
||||
<span className="text-[11px] font-medium text-primary">{liveCount} live</span>
|
||||
</span>
|
||||
)}
|
||||
{badge != null && badge > 0 && (
|
||||
|
|
@ -80,7 +80,7 @@ export function SidebarNavItem({
|
|||
className={cn(
|
||||
"ml-auto rounded-full px-1.5 py-0.5 text-xs leading-none",
|
||||
badgeTone === "danger"
|
||||
? "bg-red-600/90 text-red-50"
|
||||
? "bg-destructive/90 text-destructive"
|
||||
: "bg-primary text-primary-foreground",
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ function SortableProjectItem({
|
|||
>
|
||||
<span
|
||||
className="shrink-0 h-3.5 w-3.5 rounded-sm"
|
||||
style={{ backgroundColor: project.color ?? "#6366f1" }}
|
||||
style={{ backgroundColor: project.color ?? "var(--primary)" }}
|
||||
/>
|
||||
<span className="flex-1 truncate">{project.name}</span>
|
||||
{project.pauseReason === "budget" ? <BudgetSidebarMarker title="Project paused by budget" /> : null}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export function SkillCard({
|
|||
{hasUpdate && !isReadOnly && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs text-amber-600 border-amber-500"
|
||||
className="text-xs text-warning border-warning/30"
|
||||
aria-label="Update available"
|
||||
>
|
||||
Update
|
||||
|
|
@ -91,7 +91,7 @@ export function SkillCard({
|
|||
<Badge variant="secondary" className="text-xs">{skill.sourceId}</Badge>
|
||||
{skill.averageRating != null && (
|
||||
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Star className="h-3 w-3 fill-amber-400 text-amber-400" />
|
||||
<Star className="h-3 w-3 fill-amber-400 text-warning" />
|
||||
{skill.averageRating.toFixed(1)}
|
||||
</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export function StarRating({
|
|||
<Star
|
||||
className={cn(
|
||||
iconClass,
|
||||
filled ? "fill-amber-400 text-amber-400" : "text-muted-foreground",
|
||||
filled ? "fill-amber-400 text-warning" : "text-muted-foreground",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ export function SwipeToArchive({
|
|||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 flex items-center justify-end bg-emerald-600 px-4 text-white"
|
||||
className="pointer-events-none absolute inset-0 flex items-center justify-end bg-success px-4 text-white"
|
||||
style={{ opacity: Math.max(archiveReveal, 0.2) }}
|
||||
>
|
||||
<span className="inline-flex items-center gap-2 text-sm font-medium">
|
||||
|
|
@ -153,7 +153,7 @@ export function SwipeToArchive({
|
|||
data-inbox-row-surface
|
||||
className={cn(
|
||||
"relative will-change-transform",
|
||||
selected ? "bg-zinc-100 dark:bg-zinc-800" : "bg-card",
|
||||
selected ? "bg-muted" : "bg-card",
|
||||
)}
|
||||
style={{
|
||||
transform: `translate3d(${offsetX}px, 0, 0)`,
|
||||
|
|
|
|||
|
|
@ -5,17 +5,17 @@ import { useToast, type ToastItem, type ToastTone } from "../context/ToastContex
|
|||
import { cn } from "../lib/utils";
|
||||
|
||||
const toneClasses: Record<ToastTone, string> = {
|
||||
info: "border-sky-300 bg-sky-50 text-sky-900 dark:border-sky-500/25 dark:bg-sky-950/60 dark:text-sky-100",
|
||||
success: "border-emerald-300 bg-emerald-50 text-emerald-900 dark:border-emerald-500/25 dark:bg-emerald-950/60 dark:text-emerald-100",
|
||||
warn: "border-amber-300 bg-amber-50 text-amber-900 dark:border-amber-500/25 dark:bg-amber-950/60 dark:text-amber-100",
|
||||
error: "border-red-300 bg-red-50 text-red-900 dark:border-red-500/30 dark:bg-red-950/60 dark:text-red-100",
|
||||
info: "border-primary/30 bg-primary/10 text-primary",
|
||||
success: "border-success/30 bg-success/10 text-success",
|
||||
warn: "border-warning/30 bg-warning/10 text-warning",
|
||||
error: "border-destructive/30 bg-destructive/10 text-destructive",
|
||||
};
|
||||
|
||||
const toneDotClasses: Record<ToastTone, string> = {
|
||||
info: "bg-sky-500 dark:bg-sky-400",
|
||||
success: "bg-emerald-500 dark:bg-emerald-400",
|
||||
warn: "bg-amber-500 dark:bg-amber-400",
|
||||
error: "bg-red-500 dark:bg-red-400",
|
||||
info: "bg-primary",
|
||||
success: "bg-success",
|
||||
warn: "bg-warning",
|
||||
error: "bg-destructive",
|
||||
};
|
||||
|
||||
function AnimatedToast({
|
||||
|
|
|
|||
|
|
@ -40,10 +40,10 @@ export function VoiceWaveform({ stream, active }: VoiceWaveformProps) {
|
|||
const canvasHeight = canvas.height;
|
||||
const ctx2d = canvas.getContext("2d");
|
||||
|
||||
// Get primary color from CSS variable
|
||||
// Get primary color from CSS variable; fall back to volt (the dark-mode brand accent).
|
||||
const primaryColor =
|
||||
getComputedStyle(document.documentElement).getPropertyValue("--primary").trim() ||
|
||||
"#1e66f5";
|
||||
"#faff69";
|
||||
|
||||
const draw = () => {
|
||||
analyser.getByteFrequencyData(dataArray);
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ export function ToggleField({
|
|||
type="button"
|
||||
className={cn(
|
||||
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors",
|
||||
checked ? "bg-green-600" : "bg-muted"
|
||||
checked ? "bg-success" : "bg-muted"
|
||||
)}
|
||||
onClick={() => onChange(!checked)}
|
||||
>
|
||||
|
|
@ -175,7 +175,7 @@ export function ToggleWithNumber({
|
|||
data-slot="toggle"
|
||||
className={cn(
|
||||
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors shrink-0",
|
||||
checked ? "bg-green-600" : "bg-muted"
|
||||
checked ? "bg-success" : "bg-muted"
|
||||
)}
|
||||
onClick={() => onCheckedChange(!checked)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// [nexus] Hardware summary display for onboarding wizard step 1
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import type { HardwareInfo } from "@/api/hardware";
|
||||
|
||||
interface HardwareSummaryStepProps {
|
||||
|
|
@ -16,28 +17,39 @@ interface StatRowProps {
|
|||
function StatRow({ label, value }: StatRowProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-xs text-muted-foreground">{label}</span>
|
||||
<span className="text-sm font-medium">{value}</span>
|
||||
<span className="text-sm text-muted-foreground">{label}</span>
|
||||
<span className="text-base font-medium">{value}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const TIER_BADGE: Record<string, { label: string; color: string }> = {
|
||||
apple_silicon: { label: "Apple Silicon", color: "text-[color:var(--chart-2)] border-[color:var(--chart-2)]/30 bg-[color:var(--chart-2)]/10" },
|
||||
gpu: { label: "GPU Available", color: "text-primary border-primary/30 bg-primary/10" },
|
||||
cpu_only: { label: "CPU Only", color: "text-[color:var(--chart-4)] border-[color:var(--chart-4)]/30 bg-[color:var(--chart-4)]/10" },
|
||||
};
|
||||
|
||||
export function HardwareSummaryStep({ hardwareInfo, isLoading, isError }: HardwareSummaryStepProps) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-4 w-full rounded" />
|
||||
<Skeleton className="h-4 w-full rounded" />
|
||||
<Skeleton className="h-4 w-full rounded" />
|
||||
<div className="flex flex-col gap-3">
|
||||
<Skeleton className="h-5 w-full rounded" />
|
||||
<Skeleton className="h-5 w-full rounded" />
|
||||
<Skeleton className="h-5 w-full rounded" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Could not detect hardware. You can still continue.
|
||||
</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Could not detect hardware. You can still continue.
|
||||
</p>
|
||||
<p className="text-xs text-destructive/70">
|
||||
GET /api/system/providers failed -- check server logs
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -50,10 +62,24 @@ export function HardwareSummaryStep({ hardwareInfo, isLoading, isError }: Hardwa
|
|||
}
|
||||
|
||||
const { hardwareTier } = hardwareInfo;
|
||||
const tierBadge = TIER_BADGE[hardwareTier];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-5">
|
||||
{/* Detection status + tier badge */}
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
<Badge variant="outline" className="text-[color:var(--chart-2)] border-[color:var(--chart-2)]/30 bg-[color:var(--chart-2)]/10">
|
||||
Detected
|
||||
</Badge>
|
||||
{tierBadge && (
|
||||
<Badge variant="outline" className={tierBadge.color}>
|
||||
{tierBadge.label}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Hardware stats */}
|
||||
<div className="flex flex-col gap-3">
|
||||
{hardwareTier === "apple_silicon" && (
|
||||
<>
|
||||
<StatRow label="Unified memory" value={`${hardwareInfo.totalGb} GB`} />
|
||||
|
|
@ -74,7 +100,7 @@ export function HardwareSummaryStep({ hardwareInfo, isLoading, isError }: Hardwa
|
|||
<>
|
||||
<StatRow label="System RAM" value={`${hardwareInfo.totalGb} GB`} />
|
||||
<StatRow label="CPU" value={hardwareInfo.cpuModel} />
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Slower than GPU-accelerated models -- cloud AI recommended
|
||||
</p>
|
||||
</>
|
||||
|
|
@ -83,10 +109,9 @@ export function HardwareSummaryStep({ hardwareInfo, isLoading, isError }: Hardwa
|
|||
|
||||
{hardwareTier !== "cpu_only" && (
|
||||
<div className="flex flex-col gap-1 pt-2">
|
||||
<span className="text-sm font-medium">Local AI (recommended for privacy)</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Runs entirely on your machine.{"\n"}
|
||||
No accounts. No tracking. Works offline.
|
||||
<span className="text-base font-medium">Local AI (recommended for privacy)</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Runs entirely on your machine. No accounts. No tracking. Works offline.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,49 +1,74 @@
|
|||
// [nexus] Three-card mode selector for onboarding wizard step 2
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { MessageSquare, Hammer, Layers } from "lucide-react";
|
||||
import type { NexusMode } from "@/api/hardware";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
interface ModeSelectorProps {
|
||||
value: NexusMode;
|
||||
onChange: (mode: NexusMode) => void;
|
||||
}
|
||||
|
||||
const MODES: { id: NexusMode; label: string; description: string }[] = [
|
||||
const MODES: { id: NexusMode; label: string; description: string; icon: LucideIcon; accent: string; recommended?: boolean }[] = [
|
||||
{
|
||||
id: "personal_ai",
|
||||
label: "Personal AI Assistant",
|
||||
description: "Always available, persistent memory, private.",
|
||||
icon: MessageSquare,
|
||||
accent: "text-[color:var(--chart-3)]",
|
||||
},
|
||||
{
|
||||
id: "project_builder",
|
||||
label: "Project Builder",
|
||||
description: "Brainstorm -> PM -> Engineer -> shipped product.",
|
||||
icon: Hammer,
|
||||
accent: "text-[color:var(--chart-4)]",
|
||||
},
|
||||
{
|
||||
id: "both",
|
||||
label: "Both (recommended)",
|
||||
label: "Both",
|
||||
description: "A conversation becomes a project with one click.",
|
||||
icon: Layers,
|
||||
accent: "text-primary",
|
||||
recommended: true,
|
||||
},
|
||||
];
|
||||
|
||||
export function ModeSelector({ value, onChange }: ModeSelectorProps) {
|
||||
return (
|
||||
<div className="grid gap-3">
|
||||
{MODES.map((mode) => (
|
||||
<button
|
||||
key={mode.id}
|
||||
type="button"
|
||||
onClick={() => onChange(mode.id)}
|
||||
className={cn(
|
||||
"flex flex-col gap-1 rounded-lg border p-4 text-left transition-colors",
|
||||
value === mode.id
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border hover:border-muted-foreground/50"
|
||||
)}
|
||||
>
|
||||
<span className="font-medium text-sm">{mode.label}</span>
|
||||
<span className="text-xs text-muted-foreground">{mode.description}</span>
|
||||
</button>
|
||||
))}
|
||||
{MODES.map((mode) => {
|
||||
const Icon = mode.icon;
|
||||
const isSelected = value === mode.id;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={mode.id}
|
||||
type="button"
|
||||
onClick={() => onChange(mode.id)}
|
||||
className={cn(
|
||||
"flex items-start gap-4 rounded-lg border p-5 text-left transition-colors",
|
||||
isSelected
|
||||
? "border-l-2 border-l-primary border-t border-r border-b border-border bg-primary/5"
|
||||
: "border-border hover:border-muted-foreground/50"
|
||||
)}
|
||||
>
|
||||
<Icon className={cn("h-5 w-5 shrink-0 mt-0.5", mode.accent)} />
|
||||
<div className="flex flex-col gap-1 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-base font-medium">{mode.label}</span>
|
||||
{mode.recommended && (
|
||||
<Badge variant="outline" className="text-primary border-primary/30 bg-primary/10 text-xs">
|
||||
Recommended
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">{mode.description}</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
// [nexus] Summary screen for onboarding wizard step 5 — read-only review before starting
|
||||
// [nexus] Summary screen for onboarding wizard step 6 — read-only review before starting
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import type { HardwareInfo, HardwareTier, NexusMode } from "@/api/hardware";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
|
|
@ -7,23 +9,35 @@ interface OnboardingSummaryStepProps {
|
|||
selectedMode: NexusMode;
|
||||
providerLabel: string;
|
||||
rootDir: string;
|
||||
onRootDirChange: (value: string) => void;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
onStartChat: () => void;
|
||||
onBack: () => void;
|
||||
voiceEnabled?: boolean;
|
||||
defaultAdapter?: string;
|
||||
}
|
||||
|
||||
interface SummaryRowProps {
|
||||
label: string;
|
||||
value: string;
|
||||
mono?: boolean;
|
||||
warn?: boolean;
|
||||
}
|
||||
|
||||
function SummaryRow({ label, value, mono }: SummaryRowProps) {
|
||||
function SummaryRow({ label, value, mono, warn }: SummaryRowProps) {
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<span className="text-sm text-muted-foreground shrink-0">{label}</span>
|
||||
<span className={mono ? "font-mono text-sm text-right" : "text-sm text-right"}>{value}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"text-base font-medium text-right",
|
||||
mono && "font-mono text-sm",
|
||||
warn && "text-[color:var(--chart-4)]"
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -45,32 +59,54 @@ export function OnboardingSummaryStep({
|
|||
selectedMode,
|
||||
providerLabel,
|
||||
rootDir,
|
||||
onRootDirChange,
|
||||
loading,
|
||||
error,
|
||||
onStartChat,
|
||||
onBack,
|
||||
voiceEnabled,
|
||||
defaultAdapter,
|
||||
}: OnboardingSummaryStepProps) {
|
||||
const hardwareLabel = hardwareInfo
|
||||
? (HARDWARE_TIER_LABELS[hardwareInfo.hardwareTier] ?? "Unknown")
|
||||
: "Unknown";
|
||||
|
||||
const modeLabel = MODE_LABELS[selectedMode];
|
||||
const isProviderNone = providerLabel === "None selected";
|
||||
const isVoiceNone = voiceEnabled === false || voiceEnabled === undefined;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-2 text-center">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Ready to go</h1>
|
||||
<p className="text-sm text-muted-foreground">Review your setup before starting.</p>
|
||||
</div>
|
||||
|
||||
{/* Summary card */}
|
||||
<div className="rounded-lg border border-border p-4 flex flex-col gap-3">
|
||||
<div className="rounded-lg border border-border p-5 flex flex-col gap-4">
|
||||
<SummaryRow label="Hardware" value={hardwareLabel} />
|
||||
<SummaryRow label="Mode" value={modeLabel} />
|
||||
<SummaryRow label="Provider" value={providerLabel} />
|
||||
{rootDir && <SummaryRow label="Root directory" value={rootDir} mono />}
|
||||
<SummaryRow label="Provider" value={providerLabel} warn={isProviderNone} />
|
||||
<SummaryRow label="Voice" value={voiceEnabled ? "Enabled" : "None selected"} warn={isVoiceNone} />
|
||||
</div>
|
||||
|
||||
{/* Root directory — editable input for claude_local, read-only row for others */}
|
||||
{defaultAdapter === "claude_local" ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<label htmlFor="summary-root-dir" className="text-sm text-muted-foreground">
|
||||
Root directory (required for Claude Code)
|
||||
</label>
|
||||
<Input
|
||||
id="summary-root-dir"
|
||||
type="text"
|
||||
placeholder="~/projects/my-project"
|
||||
className="font-mono text-sm"
|
||||
autoComplete="off"
|
||||
value={rootDir}
|
||||
onChange={(e) => onRootDirChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
) : rootDir ? (
|
||||
<div className="rounded-lg border border-border p-5">
|
||||
<SummaryRow label="Root directory" value={rootDir} mono />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
<p className="text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2">
|
||||
|
|
@ -84,7 +120,7 @@ export function OnboardingSummaryStep({
|
|||
type="button"
|
||||
onClick={onStartChat}
|
||||
disabled={loading}
|
||||
className="w-full"
|
||||
className="w-full h-12 text-base font-medium"
|
||||
>
|
||||
{loading ? (
|
||||
<span className="flex items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// [nexus] Provider selection step — Step 3 of 4 in the onboarding wizard
|
||||
// Heading: "Choose a provider" / Subheading: "No API keys needed for the zero-config path."
|
||||
// Three provider cards (Puter, Google, API key) with adapter badges and skip button
|
||||
// [nexus] Provider selection step — Step 3 of 6 in the onboarding wizard
|
||||
// Separated detected adapters section from provider cards
|
||||
import { useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PuterAuthButton } from "./PuterAuthButton";
|
||||
import { GoogleOAuthButton } from "./GoogleOAuthButton";
|
||||
|
|
@ -12,68 +12,139 @@ interface ProviderSelectionStepProps {
|
|||
onPuterToken: (token: string) => void;
|
||||
onGoogleOAuthState: (stateId: string) => void;
|
||||
onApiKey: (provider: string, apiKey: string) => void;
|
||||
onAdapterSelected: (adapter: string | null) => void;
|
||||
onSkip: () => void;
|
||||
onContinue: () => void;
|
||||
detectedAdapters: Record<string, boolean>;
|
||||
probing?: boolean;
|
||||
}
|
||||
|
||||
type ProviderChoice = "puter" | "google" | "apikey" | null;
|
||||
type AdapterChoice = "claude_local" | "hermes_local" | "openclaw_gateway" | null;
|
||||
|
||||
const ADAPTER_INFO: Record<string, { name: string; description: string }> = {
|
||||
claude_local: { name: "Claude Code", description: "CLI agent — uses your Anthropic API key" },
|
||||
hermes_local: { name: "Hermes", description: "Free local agent — no API key needed" },
|
||||
openclaw_gateway: { name: "OpenClaw Gateway", description: "Multi-model gateway" },
|
||||
};
|
||||
|
||||
export function ProviderSelectionStep({
|
||||
onPuterToken,
|
||||
onGoogleOAuthState,
|
||||
onApiKey,
|
||||
onAdapterSelected,
|
||||
onSkip,
|
||||
onContinue,
|
||||
detectedAdapters,
|
||||
probing,
|
||||
}: ProviderSelectionStepProps) {
|
||||
const [selectedProvider, setSelectedProvider] = useState<ProviderChoice>(null);
|
||||
const [selectedAdapter, setSelectedAdapter] = useState<AdapterChoice>(null);
|
||||
const [providerReady, setProviderReady] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
function handleSelect(provider: ProviderChoice) {
|
||||
function handleSelectProvider(provider: ProviderChoice) {
|
||||
setSelectedProvider(provider);
|
||||
setSelectedAdapter(null);
|
||||
setProviderReady(false);
|
||||
setError(null);
|
||||
onAdapterSelected(null);
|
||||
}
|
||||
|
||||
function handleSelectAdapter(adapter: AdapterChoice) {
|
||||
setSelectedAdapter(adapter);
|
||||
setSelectedProvider(null);
|
||||
setProviderReady(false);
|
||||
setError(null);
|
||||
onAdapterSelected(adapter);
|
||||
}
|
||||
|
||||
const hermesDetected = detectedAdapters["hermes_local"] === true;
|
||||
const claudeDetected = detectedAdapters["claude_local"] === true;
|
||||
const openclawDetected = detectedAdapters["openclaw_gateway"] === true;
|
||||
const hasDetectedAdapters = hermesDetected || claudeDetected || openclawDetected;
|
||||
|
||||
// Determine if continue should be active
|
||||
const canContinue = selectedAdapter !== null || (selectedProvider !== null && providerReady);
|
||||
const hasSelection = selectedAdapter !== null || selectedProvider !== null;
|
||||
|
||||
function handleContinue() {
|
||||
if (selectedAdapter) {
|
||||
onContinue();
|
||||
} else if (selectedProvider && providerReady) {
|
||||
onContinue();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Three provider cards */}
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Detected local adapters section */}
|
||||
{(probing || hasDetectedAdapters) && (
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-sm font-medium text-muted-foreground">Detected on this machine</p>
|
||||
{probing ? (
|
||||
<div className="flex items-center gap-3 rounded-lg border border-border p-4">
|
||||
<svg className="h-4 w-4 animate-spin text-muted-foreground" viewBox="0 0 24 24" fill="none">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
</svg>
|
||||
<span className="text-sm text-muted-foreground">Checking for installed tools...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-2">
|
||||
{(["claude_local", "hermes_local", "openclaw_gateway"] as const).map((key) => {
|
||||
if (!detectedAdapters[key]) return null;
|
||||
const info = ADAPTER_INFO[key];
|
||||
const isSelected = selectedAdapter === key;
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
onClick={() => handleSelectAdapter(key)}
|
||||
className={cn(
|
||||
"flex items-center justify-between rounded-lg border p-4 text-left transition-colors w-full",
|
||||
isSelected
|
||||
? "border-l-2 border-l-primary bg-primary/5"
|
||||
: "border-border hover:border-muted-foreground/50"
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-sm font-medium">{info.name}</span>
|
||||
<span className="text-xs text-muted-foreground">{info.description}</span>
|
||||
</div>
|
||||
<Badge variant="outline" className="text-[color:var(--chart-2)] border-[color:var(--chart-2)]/30 bg-[color:var(--chart-2)]/10 shrink-0 ml-3">
|
||||
Detected
|
||||
</Badge>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Cloud provider cards */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-sm font-medium text-muted-foreground">Cloud providers</p>
|
||||
|
||||
{/* Puter card */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSelect("puter")}
|
||||
onClick={() => handleSelectProvider("puter")}
|
||||
className={cn(
|
||||
"flex flex-col gap-1 rounded-lg border p-4 text-left transition-colors",
|
||||
"flex flex-col gap-1 rounded-lg border p-4 text-left transition-colors w-full",
|
||||
selectedProvider === "puter"
|
||||
? "border-primary bg-primary/5"
|
||||
? "border-l-2 border-l-[var(--primary)] bg-[var(--primary)]/5"
|
||||
: "border-border hover:border-muted-foreground/50"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-medium text-sm">Puter -- free, zero-config</span>
|
||||
{hermesDetected && (
|
||||
<span className="text-xs text-primary">Hermes detected</span>
|
||||
)}
|
||||
{claudeDetected && (
|
||||
<span className="text-xs text-primary">Claude Code detected</span>
|
||||
)}
|
||||
{openclawDetected && (
|
||||
<span className="text-xs text-primary">OpenClaw detected</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<span className="text-base font-medium">Puter -- free, zero-config</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Free AI powered by your Puter.com account. No API key needed.
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Puter auth component — shown when Puter is selected */}
|
||||
{/* Puter auth component */}
|
||||
{selectedProvider === "puter" && (
|
||||
<PuterAuthButton
|
||||
onSuccess={(token) => {
|
||||
|
|
@ -87,21 +158,21 @@ export function ProviderSelectionStep({
|
|||
{/* Google card */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSelect("google")}
|
||||
onClick={() => handleSelectProvider("google")}
|
||||
className={cn(
|
||||
"flex flex-col gap-1 rounded-lg border p-4 text-left transition-colors",
|
||||
"flex flex-col gap-1 rounded-lg border p-4 text-left transition-colors w-full",
|
||||
selectedProvider === "google"
|
||||
? "border-primary bg-primary/5"
|
||||
? "border-l-2 border-l-[var(--primary)] bg-[var(--primary)]/5"
|
||||
: "border-border hover:border-muted-foreground/50"
|
||||
)}
|
||||
>
|
||||
<span className="font-medium text-sm">Google -- Gemini free tier</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<span className="text-base font-medium">Google Gemini -- free tier</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Sign in with Google to access Gemini via your Google account.
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Google OAuth component — shown when Google is selected */}
|
||||
{/* Google OAuth component */}
|
||||
{selectedProvider === "google" && (
|
||||
<GoogleOAuthButton
|
||||
onSuccess={(stateId) => {
|
||||
|
|
@ -115,21 +186,21 @@ export function ProviderSelectionStep({
|
|||
{/* API key card */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSelect("apikey")}
|
||||
onClick={() => handleSelectProvider("apikey")}
|
||||
className={cn(
|
||||
"flex flex-col gap-1 rounded-lg border p-4 text-left transition-colors",
|
||||
"flex flex-col gap-1 rounded-lg border p-4 text-left transition-colors w-full",
|
||||
selectedProvider === "apikey"
|
||||
? "border-primary bg-primary/5"
|
||||
? "border-l-2 border-l-[var(--primary)] bg-[var(--primary)]/5"
|
||||
: "border-border hover:border-muted-foreground/50"
|
||||
)}
|
||||
>
|
||||
<span className="font-medium text-sm">API key -- subscription provider</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<span className="text-base font-medium">API key -- subscription provider</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
Use your own OpenAI, Anthropic, or Groq API key.
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* API key form — shown when API key is selected */}
|
||||
{/* API key form */}
|
||||
{selectedProvider === "apikey" && (
|
||||
<ApiKeyEntryForm
|
||||
onSave={(prov, key) => {
|
||||
|
|
@ -148,23 +219,22 @@ export function ProviderSelectionStep({
|
|||
</p>
|
||||
)}
|
||||
|
||||
{/* Continue button — shown when provider auth is complete */}
|
||||
{providerReady && (
|
||||
<Button type="button" onClick={onContinue} className="w-full">
|
||||
Continue
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Skip button — always visible */}
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={onSkip}
|
||||
aria-label="Skip provider setup for now"
|
||||
className="w-full"
|
||||
>
|
||||
Skip for now
|
||||
</Button>
|
||||
{/* Single bottom action — contextual based on selection state */}
|
||||
<div className="flex items-center justify-center mt-2">
|
||||
{canContinue ? (
|
||||
<Button type="button" onClick={handleContinue} className="w-full">
|
||||
Continue
|
||||
</Button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
onClick={onSkip}
|
||||
>
|
||||
Continue without a provider
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// [nexus] Telegram bridge onboarding step — BotFather guided setup with token validation
|
||||
// [nexus] Phone access onboarding step — Telegram bridge with BotFather guided setup
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
|
@ -50,68 +50,70 @@ export function TelegramStep({ onNext, onBack }: TelegramStepProps) {
|
|||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col gap-2 text-center">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">Connect Telegram</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Get instant notifications and interact with your agents via Telegram.
|
||||
</p>
|
||||
{/* Telegram as current option */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="text-sm font-medium">Telegram Bot</p>
|
||||
|
||||
{/* BotFather instructions */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="text-sm text-muted-foreground">Set up your bot in 4 steps:</p>
|
||||
<ol className="flex flex-col gap-3 list-none pl-0">
|
||||
{[
|
||||
<>Open Telegram and search for <span className="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">@BotFather</span></>,
|
||||
<>Send <span className="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">/newbot</span> and follow the prompts to create a bot</>,
|
||||
<>Copy the bot token -- it looks like <span className="font-mono text-xs bg-muted px-1.5 py-0.5 rounded">123456:ABC-DEF...</span></>,
|
||||
"Paste the token below and click Validate",
|
||||
].map((instruction, i) => (
|
||||
<li key={i} className="flex items-start gap-3 text-sm text-muted-foreground">
|
||||
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-medium text-foreground">
|
||||
{i + 1}
|
||||
</span>
|
||||
<span className="mt-0.5">{instruction}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{/* Token input */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label htmlFor="telegram-token" className="text-sm font-medium leading-none">
|
||||
Bot token
|
||||
</label>
|
||||
<Input
|
||||
id="telegram-token"
|
||||
type="text"
|
||||
placeholder="Paste bot token here"
|
||||
value={token}
|
||||
onChange={(e) => {
|
||||
setToken(e.target.value);
|
||||
setBotUsername(null);
|
||||
setError(null);
|
||||
}}
|
||||
disabled={validating}
|
||||
autoComplete="off"
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
|
||||
{/* Success state */}
|
||||
{botUsername && (
|
||||
<p className={cn("text-sm", "text-[color:var(--chart-2)]")}>
|
||||
Connected to @{botUsername}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Error state */}
|
||||
{error && (
|
||||
<p className="text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* BotFather instructions */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm font-medium">Set up your bot in 4 steps:</p>
|
||||
<ol className="flex flex-col gap-2 list-none pl-0">
|
||||
{[
|
||||
<>Open Telegram and search for <span className="font-mono text-xs bg-muted px-1 py-0.5 rounded">@BotFather</span></>,
|
||||
<>Send <span className="font-mono text-xs bg-muted px-1 py-0.5 rounded">/newbot</span> and follow the prompts to create a bot</>,
|
||||
<>Copy the bot token — it looks like <span className="font-mono text-xs bg-muted px-1 py-0.5 rounded">123456:ABC-DEF...</span></>,
|
||||
"Paste the token below and click Validate",
|
||||
].map((instruction, i) => (
|
||||
<li key={i} className="flex items-start gap-3 text-sm text-muted-foreground">
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-muted text-xs font-medium text-foreground">
|
||||
{i + 1}
|
||||
</span>
|
||||
<span className="mt-0.5">{instruction}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
{/* Token input */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<label htmlFor="telegram-token" className="text-sm font-medium leading-none">
|
||||
Bot token
|
||||
</label>
|
||||
<Input
|
||||
id="telegram-token"
|
||||
type="text"
|
||||
placeholder="Paste bot token here"
|
||||
value={token}
|
||||
onChange={(e) => {
|
||||
setToken(e.target.value);
|
||||
setBotUsername(null);
|
||||
setError(null);
|
||||
}}
|
||||
disabled={validating}
|
||||
autoComplete="off"
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
|
||||
{/* Success state */}
|
||||
{botUsername && (
|
||||
<p className={cn("text-sm", "text-green-600 dark:text-green-400")}>
|
||||
Connected to @{botUsername}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Error state */}
|
||||
{error && (
|
||||
<p className="text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{/* Future bridges note */}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Discord and WhatsApp bridges coming in a future update.
|
||||
</p>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex flex-col gap-2">
|
||||
|
|
@ -122,7 +124,7 @@ export function TelegramStep({ onNext, onBack }: TelegramStepProps) {
|
|||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
{validating ? "Validating…" : "Validate Token"}
|
||||
{validating ? "Validating..." : "Validate Token"}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { Mic, Volume2, CheckCircle2, AlertTriangle, Info } from "lucide-react";
|
||||
import { Mic, Volume2, CheckCircle2, Info } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { VoiceCapability } from "../../hooks/useHardwareInfo";
|
||||
|
||||
|
|
@ -21,55 +22,73 @@ export function VoiceStep({ onEnable, onSkip, voiceCapability }: VoiceStepProps)
|
|||
// Determine STT status label
|
||||
function whisperStatusLabel(): string {
|
||||
if (!voiceCapability) {
|
||||
// Fall back to mic-only check
|
||||
if (micAvailable === false) return "No microphone detected — unavailable";
|
||||
if (micAvailable === true) return "Microphone detected — speak to your assistant";
|
||||
if (micAvailable === false) return "No microphone detected -- unavailable";
|
||||
if (micAvailable === true) return "Microphone detected -- speak to your assistant";
|
||||
return "Checking microphone...";
|
||||
}
|
||||
if (voiceCapability.whisperAvailable) return "Whisper detected — speech recognition ready";
|
||||
return "Whisper not found — install whisper-cpp for voice input";
|
||||
if (voiceCapability.whisperAvailable) return "Whisper detected -- speech recognition ready";
|
||||
return "Whisper not found -- install whisper-cpp for voice input";
|
||||
}
|
||||
|
||||
function whisperStatusIcon() {
|
||||
function whisperBadge() {
|
||||
if (!voiceCapability) return null;
|
||||
if (voiceCapability.whisperAvailable) {
|
||||
return <CheckCircle2 className="h-4 w-4 text-green-500 shrink-0" />;
|
||||
return (
|
||||
<Badge variant="outline" className="text-[color:var(--chart-2)] border-[color:var(--chart-2)]/30 bg-[color:var(--chart-2)]/10 text-xs">
|
||||
Available
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return <AlertTriangle className="h-4 w-4 text-amber-500 shrink-0" />;
|
||||
return (
|
||||
<Badge variant="outline" className="text-[color:var(--chart-4)] border-[color:var(--chart-4)]/30 bg-[color:var(--chart-4)]/10 text-xs">
|
||||
Install needed
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
// Determine TTS status label
|
||||
function piperStatusLabel(): string {
|
||||
if (!voiceCapability) {
|
||||
return "Hear responses read aloud. Runs entirely on your device — no server needed.";
|
||||
return "Hear responses read aloud. Runs entirely on your device -- no server needed.";
|
||||
}
|
||||
if (voiceCapability.piperAvailable) return "Piper detected — text-to-speech ready";
|
||||
return "Piper not found — install piper for voice output";
|
||||
if (voiceCapability.piperAvailable) return "Piper detected -- text-to-speech ready";
|
||||
return "Piper not found -- install piper for voice output";
|
||||
}
|
||||
|
||||
function piperStatusIcon() {
|
||||
function piperBadge() {
|
||||
if (!voiceCapability) return null;
|
||||
if (voiceCapability.piperAvailable) {
|
||||
return <CheckCircle2 className="h-4 w-4 text-green-500 shrink-0" />;
|
||||
return (
|
||||
<Badge variant="outline" className="text-[color:var(--chart-2)] border-[color:var(--chart-2)]/30 bg-[color:var(--chart-2)]/10 text-xs">
|
||||
Available
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return <AlertTriangle className="h-4 w-4 text-amber-500 shrink-0" />;
|
||||
return (
|
||||
<Badge variant="outline" className="text-[color:var(--chart-4)] border-[color:var(--chart-4)]/30 bg-[color:var(--chart-4)]/10 text-xs">
|
||||
Install needed
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
// Insufficient hardware: show note + skip only
|
||||
// Insufficient hardware: show note but still allow enabling
|
||||
if (voiceCapability && !voiceCapability.voiceTierSufficient) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-start gap-3 rounded-lg border border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950/20 p-3">
|
||||
<Info className="h-5 w-5 text-amber-500 shrink-0 mt-0.5" />
|
||||
<div className="flex items-start gap-3 rounded-lg border border-[color:var(--chart-4)]/30 bg-[color:var(--chart-4)]/5 p-4">
|
||||
<Info className="h-5 w-5 text-[color:var(--chart-4)] shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-amber-800 dark:text-amber-300">Hardware may not support voice</p>
|
||||
<p className="text-xs text-amber-700 dark:text-amber-400 mt-1">
|
||||
Voice features require at least 4GB free RAM. Your system currently has insufficient free memory for local voice processing.
|
||||
<p className="text-sm font-medium">Limited hardware for voice</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Voice features require at least 4GB free RAM. Transcription may be slower on your system.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button onClick={onEnable} variant="outline" className="w-full">
|
||||
Enable voice anyway
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={onSkip} className="w-full">
|
||||
Skip voice setup
|
||||
</Button>
|
||||
|
|
@ -78,45 +97,55 @@ export function VoiceStep({ onEnable, onSkip, voiceCapability }: VoiceStepProps)
|
|||
);
|
||||
}
|
||||
|
||||
// Sufficient hardware or unknown — show full UI
|
||||
// Sufficient hardware or unknown -- show full UI
|
||||
const neitherBinaryFound =
|
||||
voiceCapability &&
|
||||
!voiceCapability.whisperAvailable &&
|
||||
!voiceCapability.piperAvailable;
|
||||
|
||||
// CPU-only note
|
||||
const isCpuOnly = voiceCapability && !voiceCapability.whisperAvailable && !voiceCapability.piperAvailable;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Install note when tier is sufficient but binaries are missing */}
|
||||
{neitherBinaryFound && (
|
||||
<div className="flex items-start gap-3 rounded-lg border border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950/20 p-3">
|
||||
<Info className="h-5 w-5 text-blue-500 shrink-0 mt-0.5" />
|
||||
<p className="text-xs text-blue-700 dark:text-blue-400">
|
||||
<div className="flex items-start gap-3 rounded-lg border border-primary/20 bg-primary/5 p-4">
|
||||
<Info className="h-5 w-5 text-primary shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Install whisper-cpp and piper for local voice features. You can enable voice now and configure binaries later.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CPU-only note */}
|
||||
{voiceCapability?.voiceTierSufficient && !voiceCapability.whisperAvailable && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Works on CPU -- transcription may take a few extra seconds.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-3 rounded-lg border p-3">
|
||||
<div className="flex items-center gap-3 rounded-lg border border-border p-4">
|
||||
<Mic className="h-5 w-5 text-primary shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">Speech-to-Text (Whisper)</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{whisperStatusLabel()}
|
||||
</p>
|
||||
</div>
|
||||
{whisperStatusIcon()}
|
||||
{whisperBadge()}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 rounded-lg border p-3">
|
||||
<div className="flex items-center gap-3 rounded-lg border border-border p-4">
|
||||
<Volume2 className="h-5 w-5 text-primary shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm font-medium">Text-to-Speech (Piper)</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{piperStatusLabel()}
|
||||
</p>
|
||||
</div>
|
||||
{piperStatusIcon()}
|
||||
{piperBadge()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -658,21 +658,21 @@ function TranscriptToolCard({
|
|||
: "Completed";
|
||||
const statusTone =
|
||||
block.status === "running"
|
||||
? "text-cyan-700 dark:text-cyan-300"
|
||||
? "text-primary"
|
||||
: block.status === "error"
|
||||
? "text-red-700 dark:text-red-300"
|
||||
: "text-emerald-700 dark:text-emerald-300";
|
||||
? "text-destructive"
|
||||
: "text-success";
|
||||
const detailsClass = cn(
|
||||
"space-y-3",
|
||||
block.status === "error" && "rounded-xl border border-red-500/20 bg-red-500/[0.06] p-3",
|
||||
block.status === "error" && "rounded-xl border border-destructive/20 bg-destructive/[0.06] p-3",
|
||||
);
|
||||
const iconClass = cn(
|
||||
"mt-0.5 h-3.5 w-3.5 shrink-0",
|
||||
block.status === "error"
|
||||
? "text-red-600 dark:text-red-300"
|
||||
? "text-destructive"
|
||||
: block.status === "completed"
|
||||
? "text-emerald-600 dark:text-emerald-300"
|
||||
: "text-cyan-600 dark:text-cyan-300",
|
||||
? "text-success"
|
||||
: "text-primary",
|
||||
);
|
||||
const summary = block.status === "running"
|
||||
? summarizeToolInput(block.name, block.input, density)
|
||||
|
|
@ -681,7 +681,7 @@ function TranscriptToolCard({
|
|||
: summarizeToolResult(block.result, block.isError, density);
|
||||
|
||||
return (
|
||||
<div className={cn(block.status === "error" && "rounded-xl border border-red-500/20 bg-red-500/[0.04] p-3")}>
|
||||
<div className={cn(block.status === "error" && "rounded-xl border border-destructive/20 bg-destructive/[0.04] p-3")}>
|
||||
<div className="flex items-start gap-2">
|
||||
{block.status === "error" ? (
|
||||
<CircleAlert className={iconClass} />
|
||||
|
|
@ -730,7 +730,7 @@ function TranscriptToolCard({
|
|||
</div>
|
||||
<pre className={cn(
|
||||
"overflow-x-auto whitespace-pre-wrap break-words font-mono text-[11px]",
|
||||
block.status === "error" ? "text-red-700 dark:text-red-300" : "text-foreground/80",
|
||||
block.status === "error" ? "text-destructive" : "text-foreground/80",
|
||||
)}>
|
||||
{block.result ? formatToolPayload(block.result) : "Waiting for result..."}
|
||||
</pre>
|
||||
|
|
@ -771,11 +771,11 @@ function TranscriptCommandGroup({
|
|||
? summarizeToolInput("command_execution", runningItem.input, density)
|
||||
: null;
|
||||
const statusTone = isRunning
|
||||
? "text-cyan-700 dark:text-cyan-300"
|
||||
? "text-primary"
|
||||
: "text-foreground/70";
|
||||
|
||||
return (
|
||||
<div className={cn(showExpandedErrorState && "rounded-xl border border-red-500/20 bg-red-500/[0.04] p-3")}>
|
||||
<div className={cn(showExpandedErrorState && "rounded-xl border border-destructive/20 bg-destructive/[0.04] p-3")}>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
|
|
@ -799,7 +799,7 @@ function TranscriptCommandGroup({
|
|||
"inline-flex h-6 w-6 items-center justify-center rounded-full border shadow-sm",
|
||||
index > 0 && "-ml-1.5",
|
||||
isRunning
|
||||
? "border-cyan-500/25 bg-cyan-500/[0.08] text-cyan-600 dark:text-cyan-300"
|
||||
? "border-primary/25 bg-primary/[0.08] text-primary"
|
||||
: "border-border/70 bg-background text-foreground/55",
|
||||
isRunning && "animate-pulse",
|
||||
)}
|
||||
|
|
@ -839,16 +839,16 @@ function TranscriptCommandGroup({
|
|||
</button>
|
||||
</div>
|
||||
{open && (
|
||||
<div className={cn("mt-3 space-y-3", hasError && "rounded-xl border border-red-500/20 bg-red-500/[0.06] p-3")}>
|
||||
<div className={cn("mt-3 space-y-3", hasError && "rounded-xl border border-destructive/20 bg-destructive/[0.06] p-3")}>
|
||||
{block.items.map((item, index) => (
|
||||
<div key={`${item.ts}-${index}`} className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn(
|
||||
"inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full border",
|
||||
item.status === "error"
|
||||
? "border-red-500/25 bg-red-500/[0.08] text-red-600 dark:text-red-300"
|
||||
? "border-destructive/25 bg-destructive/[0.08] text-destructive"
|
||||
: item.status === "running"
|
||||
? "border-cyan-500/25 bg-cyan-500/[0.08] text-cyan-600 dark:text-cyan-300"
|
||||
? "border-primary/25 bg-primary/[0.08] text-primary"
|
||||
: "border-border/70 bg-background text-foreground/55",
|
||||
)}>
|
||||
<TerminalSquare className="h-3 w-3" />
|
||||
|
|
@ -860,7 +860,7 @@ function TranscriptCommandGroup({
|
|||
{item.result && (
|
||||
<pre className={cn(
|
||||
"overflow-x-auto whitespace-pre-wrap break-words font-mono text-[11px]",
|
||||
item.status === "error" ? "text-red-700 dark:text-red-300" : "text-foreground/80",
|
||||
item.status === "error" ? "text-destructive" : "text-foreground/80",
|
||||
)}>
|
||||
{formatToolPayload(item.result)}
|
||||
</pre>
|
||||
|
|
@ -899,7 +899,7 @@ function TranscriptToolGroup({
|
|||
? summarizeToolInput(runningItem.name, runningItem.input, density)
|
||||
: null;
|
||||
const statusTone = isRunning
|
||||
? "text-cyan-700 dark:text-cyan-300"
|
||||
? "text-primary"
|
||||
: "text-foreground/70";
|
||||
|
||||
return (
|
||||
|
|
@ -922,9 +922,9 @@ function TranscriptToolGroup({
|
|||
"inline-flex h-6 w-6 items-center justify-center rounded-full border shadow-sm",
|
||||
index > 0 && "-ml-1.5",
|
||||
isItemRunning
|
||||
? "border-cyan-500/25 bg-cyan-500/[0.08] text-cyan-600 dark:text-cyan-300"
|
||||
? "border-primary/25 bg-primary/[0.08] text-primary"
|
||||
: isItemError
|
||||
? "border-red-500/25 bg-red-500/[0.08] text-red-600 dark:text-red-300"
|
||||
? "border-destructive/25 bg-destructive/[0.08] text-destructive"
|
||||
: "border-border/70 bg-background text-foreground/55",
|
||||
isItemRunning && "animate-pulse",
|
||||
)}
|
||||
|
|
@ -961,9 +961,9 @@ function TranscriptToolGroup({
|
|||
<span className={cn(
|
||||
"inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full border",
|
||||
item.status === "error"
|
||||
? "border-red-500/25 bg-red-500/[0.08] text-red-600 dark:text-red-300"
|
||||
? "border-destructive/25 bg-destructive/[0.08] text-destructive"
|
||||
: item.status === "running"
|
||||
? "border-cyan-500/25 bg-cyan-500/[0.08] text-cyan-600 dark:text-cyan-300"
|
||||
? "border-primary/25 bg-primary/[0.08] text-primary"
|
||||
: "border-border/70 bg-background text-foreground/55",
|
||||
)}>
|
||||
<Wrench className="h-3 w-3" />
|
||||
|
|
@ -972,9 +972,9 @@ function TranscriptToolGroup({
|
|||
{humanizeLabel(item.name)}
|
||||
</span>
|
||||
<span className={cn("text-[10px] font-semibold uppercase tracking-[0.14em]",
|
||||
item.status === "running" ? "text-cyan-700 dark:text-cyan-300"
|
||||
: item.status === "error" ? "text-red-700 dark:text-red-300"
|
||||
: "text-emerald-700 dark:text-emerald-300"
|
||||
item.status === "running" ? "text-primary"
|
||||
: item.status === "error" ? "text-destructive"
|
||||
: "text-success"
|
||||
)}>
|
||||
{item.status === "running" ? "Running" : item.status === "error" ? "Errored" : "Completed"}
|
||||
</span>
|
||||
|
|
@ -991,7 +991,7 @@ function TranscriptToolGroup({
|
|||
<div className="mb-0.5 text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">Result</div>
|
||||
<pre className={cn(
|
||||
"overflow-x-auto whitespace-pre-wrap break-words font-mono text-[11px]",
|
||||
item.status === "error" ? "text-red-700 dark:text-red-300" : "text-foreground/80",
|
||||
item.status === "error" ? "text-destructive" : "text-foreground/80",
|
||||
)}>
|
||||
{formatToolPayload(item.result)}
|
||||
</pre>
|
||||
|
|
@ -1016,11 +1016,11 @@ function TranscriptActivityRow({
|
|||
return (
|
||||
<div className="flex items-start gap-2">
|
||||
{block.status === "completed" ? (
|
||||
<Check className="mt-0.5 h-3.5 w-3.5 shrink-0 text-emerald-600 dark:text-emerald-300" />
|
||||
<Check className="mt-0.5 h-3.5 w-3.5 shrink-0 text-success" />
|
||||
) : (
|
||||
<span className="relative mt-1 flex h-2.5 w-2.5 shrink-0">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-cyan-400 opacity-70" />
|
||||
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-cyan-500" />
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-70" />
|
||||
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-primary" />
|
||||
</span>
|
||||
)}
|
||||
<div className={cn(
|
||||
|
|
@ -1043,11 +1043,11 @@ function TranscriptEventRow({
|
|||
const compact = density === "compact";
|
||||
const toneClasses =
|
||||
block.tone === "error"
|
||||
? "rounded-xl border border-red-500/20 bg-red-500/[0.06] p-3 text-red-700 dark:text-red-300"
|
||||
? "rounded-xl border border-destructive/20 bg-destructive/[0.06] p-3 text-destructive"
|
||||
: block.tone === "warn"
|
||||
? "text-amber-700 dark:text-amber-300"
|
||||
? "text-warning"
|
||||
: block.tone === "info"
|
||||
? "text-sky-700 dark:text-sky-300"
|
||||
? "text-primary"
|
||||
: "text-foreground/75";
|
||||
|
||||
return (
|
||||
|
|
@ -1062,7 +1062,7 @@ function TranscriptEventRow({
|
|||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
{block.label === "result" && block.tone !== "error" ? (
|
||||
<div className={cn("whitespace-pre-wrap break-words text-sky-700 dark:text-sky-300", compact ? "text-[11px]" : "text-xs")}>
|
||||
<div className={cn("whitespace-pre-wrap break-words text-primary", compact ? "text-[11px]" : "text-xs")}>
|
||||
{block.text}
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -1094,7 +1094,7 @@ function TranscriptStderrGroup({
|
|||
const [open, setOpen] = useState(false);
|
||||
const compact = density === "compact";
|
||||
return (
|
||||
<div className="rounded-xl border border-amber-500/20 bg-amber-500/[0.06] p-2 text-amber-700 dark:text-amber-300">
|
||||
<div className="rounded-xl border border-warning/20 bg-warning/[0.06] p-2 text-warning">
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
|
|
@ -1108,10 +1108,10 @@ function TranscriptStderrGroup({
|
|||
{open ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
|
||||
</div>
|
||||
{open && (
|
||||
<pre className="mt-2 overflow-x-auto whitespace-pre-wrap break-words font-mono text-[11px] text-amber-700/80 dark:text-amber-300/80 pl-5">
|
||||
<pre className="mt-2 overflow-x-auto whitespace-pre-wrap break-words font-mono text-[11px] text-warning pl-5">
|
||||
{block.lines.map((line, i) => (
|
||||
<span key={`${line.ts}-${i}`}>
|
||||
<span className="select-none text-amber-500/50 dark:text-amber-400/40">{i > 0 ? "\n" : ""}</span>
|
||||
<span className="select-none text-warning">{i > 0 ? "\n" : ""}</span>
|
||||
{line.text}
|
||||
</span>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -99,12 +99,12 @@ import {
|
|||
} from "../lib/agent-skills-state";
|
||||
|
||||
const runStatusIcons: Record<string, { icon: typeof CheckCircle2; color: string }> = {
|
||||
succeeded: { icon: CheckCircle2, color: "text-green-600 dark:text-green-400" },
|
||||
failed: { icon: XCircle, color: "text-red-600 dark:text-red-400" },
|
||||
running: { icon: Loader2, color: "text-cyan-600 dark:text-cyan-400" },
|
||||
queued: { icon: Clock, color: "text-yellow-600 dark:text-yellow-400" },
|
||||
timed_out: { icon: Timer, color: "text-orange-600 dark:text-orange-400" },
|
||||
cancelled: { icon: Slash, color: "text-neutral-500 dark:text-neutral-400" },
|
||||
succeeded: { icon: CheckCircle2, color: "text-success" },
|
||||
failed: { icon: XCircle, color: "text-destructive" },
|
||||
running: { icon: Loader2, color: "text-primary" },
|
||||
queued: { icon: Clock, color: "text-warning" },
|
||||
timed_out: { icon: Timer, color: "text-warning" },
|
||||
cancelled: { icon: Slash, color: "text-muted-foreground" },
|
||||
};
|
||||
|
||||
const REDACTED_ENV_VALUE = "***REDACTED***";
|
||||
|
|
@ -324,13 +324,13 @@ function workspaceOperationPhaseLabel(phase: WorkspaceOperation["phase"]) {
|
|||
function workspaceOperationStatusTone(status: WorkspaceOperation["status"]) {
|
||||
switch (status) {
|
||||
case "succeeded":
|
||||
return "border-green-500/20 bg-green-500/10 text-green-700 dark:text-green-300";
|
||||
return "border-success/20 bg-success/10 text-success";
|
||||
case "failed":
|
||||
return "border-red-500/20 bg-red-500/10 text-red-700 dark:text-red-300";
|
||||
return "border-destructive/20 bg-destructive/10 text-destructive";
|
||||
case "running":
|
||||
return "border-cyan-500/20 bg-cyan-500/10 text-cyan-700 dark:text-cyan-300";
|
||||
return "border-primary/20 bg-primary/10 text-primary";
|
||||
case "skipped":
|
||||
return "border-yellow-500/20 bg-yellow-500/10 text-yellow-700 dark:text-yellow-300";
|
||||
return "border-warning/20 bg-warning/10 text-warning";
|
||||
default:
|
||||
return "border-border bg-muted/40 text-muted-foreground";
|
||||
}
|
||||
|
|
@ -390,19 +390,19 @@ function WorkspaceOperationLogViewer({
|
|||
<div className="text-xs text-muted-foreground">No persisted log lines.</div>
|
||||
)}
|
||||
{chunks.length > 0 && (
|
||||
<div className="max-h-64 overflow-y-auto rounded bg-neutral-100 p-2 font-mono text-xs dark:bg-neutral-950">
|
||||
<div className="max-h-64 overflow-y-auto rounded bg-muted p-2 font-mono text-xs">
|
||||
{chunks.map((chunk, index) => (
|
||||
<div key={`${chunk.ts}-${index}`} className="flex gap-2">
|
||||
<span className="shrink-0 text-neutral-500">
|
||||
<span className="shrink-0 text-muted-foreground">
|
||||
{new Date(chunk.ts).toLocaleTimeString("en-US", { hour12: false })}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"shrink-0 w-14",
|
||||
chunk.stream === "stderr"
|
||||
? "text-red-600 dark:text-red-300"
|
||||
? "text-destructive"
|
||||
: chunk.stream === "system"
|
||||
? "text-blue-600 dark:text-blue-300"
|
||||
? "text-primary"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
|
|
@ -488,8 +488,8 @@ function WorkspaceOperationsSection({
|
|||
)}
|
||||
{operation.stderrExcerpt && operation.stderrExcerpt.trim() && (
|
||||
<div>
|
||||
<div className="mb-1 text-xs text-red-700 dark:text-red-300">stderr excerpt</div>
|
||||
<pre className="rounded-md bg-red-50 p-2 text-xs whitespace-pre-wrap break-all text-red-800 dark:bg-neutral-950 dark:text-red-100">
|
||||
<div className="mb-1 text-xs text-destructive">stderr excerpt</div>
|
||||
<pre className="rounded-md bg-destructive/10 p-2 text-xs whitespace-pre-wrap break-all text-destructive">
|
||||
{redactPathText(operation.stderrExcerpt, censorUsernameInLogs)}
|
||||
</pre>
|
||||
</div>
|
||||
|
|
@ -497,7 +497,7 @@ function WorkspaceOperationsSection({
|
|||
{operation.stdoutExcerpt && operation.stdoutExcerpt.trim() && (
|
||||
<div>
|
||||
<div className="mb-1 text-xs text-muted-foreground">stdout excerpt</div>
|
||||
<pre className="rounded-md bg-neutral-100 p-2 text-xs whitespace-pre-wrap break-all dark:bg-neutral-950">
|
||||
<pre className="rounded-md bg-muted p-2 text-xs whitespace-pre-wrap break-all">
|
||||
{redactPathText(operation.stdoutExcerpt, censorUsernameInLogs)}
|
||||
</pre>
|
||||
</div>
|
||||
|
|
@ -849,13 +849,13 @@ export function AgentDetail() {
|
|||
{mobileLiveRun && (
|
||||
<Link
|
||||
to={`/agents/${canonicalAgentRef}/runs/${mobileLiveRun.id}`}
|
||||
className="sm:hidden flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-blue-500/10 hover:bg-blue-500/20 transition-colors no-underline"
|
||||
className="sm:hidden flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-primary/10 hover:bg-primary/20 transition-colors no-underline"
|
||||
>
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500" />
|
||||
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
|
||||
</span>
|
||||
<span className="text-[11px] font-medium text-blue-600 dark:text-blue-400">Live</span>
|
||||
<span className="text-[11px] font-medium text-primary">Live</span>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
|
|
@ -924,7 +924,7 @@ export function AgentDetail() {
|
|||
|
||||
{actionError && <p className="text-sm text-destructive">{actionError}</p>}
|
||||
{isPendingApproval && (
|
||||
<p className="text-sm text-amber-500">
|
||||
<p className="text-sm text-warning">
|
||||
This agent is pending board approval and cannot be invoked yet.
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -1074,7 +1074,7 @@ function LatestRunCard({ runs, agentId }: { runs: HeartbeatRun[]; agentId: strin
|
|||
const liveRun = sorted.find((r) => r.status === "running" || r.status === "queued");
|
||||
const run = liveRun ?? sorted[0];
|
||||
const isLive = run.status === "running" || run.status === "queued";
|
||||
const statusInfo = runStatusIcons[run.status] ?? { icon: Clock, color: "text-neutral-400" };
|
||||
const statusInfo = runStatusIcons[run.status] ?? { icon: Clock, color: "text-muted-foreground" };
|
||||
const StatusIcon = statusInfo.icon;
|
||||
const summary = run.resultJson
|
||||
? String((run.resultJson as Record<string, unknown>).summary ?? (run.resultJson as Record<string, unknown>).result ?? "")
|
||||
|
|
@ -1086,8 +1086,8 @@ function LatestRunCard({ runs, agentId }: { runs: HeartbeatRun[]; agentId: strin
|
|||
<h3 className="flex items-center gap-2 text-sm font-medium">
|
||||
{isLive && (
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-cyan-400 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-cyan-400" />
|
||||
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
|
||||
</span>
|
||||
)}
|
||||
{isLive ? "Live Run" : "Latest Run"}
|
||||
|
|
@ -1104,7 +1104,7 @@ function LatestRunCard({ runs, agentId }: { runs: HeartbeatRun[]; agentId: strin
|
|||
to={`/agents/${agentId}/runs/${run.id}`}
|
||||
className={cn(
|
||||
"block border rounded-lg p-4 space-y-2 w-full no-underline transition-colors hover:bg-muted/50 cursor-pointer",
|
||||
isLive ? "border-cyan-500/30 shadow-[0_0_12px_rgba(6,182,212,0.08)]" : "border-border"
|
||||
isLive ? "border-primary/30 shadow-[0_0_12px_rgba(6,182,212,0.08)]" : "border-border"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -1113,9 +1113,9 @@ function LatestRunCard({ runs, agentId }: { runs: HeartbeatRun[]; agentId: strin
|
|||
<span className="font-mono text-xs text-muted-foreground">{run.id.slice(0, 8)}</span>
|
||||
<span className={cn(
|
||||
"inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-medium",
|
||||
run.invocationSource === "timer" ? "bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300"
|
||||
: run.invocationSource === "assignment" ? "bg-violet-100 text-violet-700 dark:bg-violet-900/50 dark:text-violet-300"
|
||||
: run.invocationSource === "on_demand" ? "bg-cyan-100 text-cyan-700 dark:bg-cyan-900/50 dark:text-cyan-300"
|
||||
run.invocationSource === "timer" ? "bg-primary/10 text-primary"
|
||||
: run.invocationSource === "assignment" ? "bg-muted text-primary"
|
||||
: run.invocationSource === "on_demand" ? "bg-primary/10 text-primary"
|
||||
: "bg-muted text-muted-foreground"
|
||||
)}>
|
||||
{sourceLabels[run.invocationSource] ?? run.invocationSource}
|
||||
|
|
@ -1571,7 +1571,7 @@ function ConfigurationTab({
|
|||
aria-checked={canCreateAgents}
|
||||
className={cn(
|
||||
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors shrink-0 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
canCreateAgents ? "bg-green-600" : "bg-muted",
|
||||
canCreateAgents ? "bg-success" : "bg-muted",
|
||||
)}
|
||||
onClick={() =>
|
||||
updatePermissions.mutate({
|
||||
|
|
@ -1603,7 +1603,7 @@ function ConfigurationTab({
|
|||
aria-checked={canAssignTasks}
|
||||
className={cn(
|
||||
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors shrink-0 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
canAssignTasks ? "bg-green-600" : "bg-muted",
|
||||
canAssignTasks ? "bg-success" : "bg-muted",
|
||||
)}
|
||||
onClick={() =>
|
||||
updatePermissions.mutate({
|
||||
|
|
@ -1955,7 +1955,7 @@ function PromptsTab({
|
|||
{(bundle?.warnings ?? []).length > 0 && (
|
||||
<div className="space-y-2">
|
||||
{(bundle?.warnings ?? []).map((warning) => (
|
||||
<div key={warning} className="rounded-md border border-sky-500/25 bg-sky-500/10 px-3 py-2 text-xs text-sky-100">
|
||||
<div key={warning} className="rounded-md border border-primary/25 bg-primary/10 px-3 py-2 text-xs text-primary">
|
||||
{warning}
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -2222,7 +2222,7 @@ function PromptsTab({
|
|||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="ml-3 shrink-0 rounded border border-amber-500/40 bg-amber-500/10 text-amber-200 px-1.5 py-0.5 text-[10px] uppercase tracking-wide cursor-help">
|
||||
<span className="ml-3 shrink-0 rounded border border-warning/40 bg-warning/10 text-warning px-1.5 py-0.5 text-[10px] uppercase tracking-wide cursor-help">
|
||||
virtual file
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
|
|
@ -2592,7 +2592,7 @@ function AgentSkillsTab({
|
|||
</div>
|
||||
|
||||
{skillSnapshot?.warnings.length ? (
|
||||
<div className="space-y-1 rounded-xl border border-amber-300/60 bg-amber-50/60 px-4 py-3 text-sm text-amber-800 dark:border-amber-500/30 dark:bg-amber-950/20 dark:text-amber-200">
|
||||
<div className="space-y-1 rounded-xl border border-warning/60 bg-warning/60 px-4 py-3 text-sm text-warning">
|
||||
{skillSnapshot.warnings.map((warning) => (
|
||||
<div key={warning}>{warning}</div>
|
||||
))}
|
||||
|
|
@ -2745,7 +2745,7 @@ function AgentSkillsTab({
|
|||
})()}
|
||||
|
||||
{desiredOnlyMissingSkills.length > 0 && (
|
||||
<div className="rounded-xl border border-amber-300/60 bg-amber-50/60 px-4 py-3 text-sm text-amber-800 dark:border-amber-500/30 dark:bg-amber-950/20 dark:text-amber-200">
|
||||
<div className="rounded-xl border border-warning/60 bg-warning/60 px-4 py-3 text-sm text-warning">
|
||||
<div className="font-medium">Requested skills missing from the company library</div>
|
||||
<div className="mt-1 text-xs">
|
||||
{desiredOnlyMissingSkills.join(", ")}
|
||||
|
|
@ -2784,7 +2784,7 @@ function AgentSkillsTab({
|
|||
/* ---- Runs Tab ---- */
|
||||
|
||||
function RunListItem({ run, isSelected, agentId }: { run: HeartbeatRun; isSelected: boolean; agentId: string }) {
|
||||
const statusInfo = runStatusIcons[run.status] ?? { icon: Clock, color: "text-neutral-400" };
|
||||
const statusInfo = runStatusIcons[run.status] ?? { icon: Clock, color: "text-muted-foreground" };
|
||||
const StatusIcon = statusInfo.icon;
|
||||
const metrics = runMetrics(run);
|
||||
const summary = run.resultJson
|
||||
|
|
@ -2806,9 +2806,9 @@ function RunListItem({ run, isSelected, agentId }: { run: HeartbeatRun; isSelect
|
|||
</span>
|
||||
<span className={cn(
|
||||
"inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-medium shrink-0",
|
||||
run.invocationSource === "timer" ? "bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300"
|
||||
: run.invocationSource === "assignment" ? "bg-violet-100 text-violet-700 dark:bg-violet-900/50 dark:text-violet-300"
|
||||
: run.invocationSource === "on_demand" ? "bg-cyan-100 text-cyan-700 dark:bg-cyan-900/50 dark:text-cyan-300"
|
||||
run.invocationSource === "timer" ? "bg-primary/10 text-primary"
|
||||
: run.invocationSource === "assignment" ? "bg-muted text-primary"
|
||||
: run.invocationSource === "on_demand" ? "bg-primary/10 text-primary"
|
||||
: "bg-muted text-muted-foreground"
|
||||
)}>
|
||||
{sourceLabels[run.invocationSource] ?? run.invocationSource}
|
||||
|
|
@ -3138,7 +3138,7 @@ function RunDetail({ run: initialRun, agentRouteId, adapterType }: { run: Heartb
|
|||
)}
|
||||
{run.error && (
|
||||
<div className="text-xs">
|
||||
<span className="text-red-600 dark:text-red-400">{run.error}</span>
|
||||
<span className="text-destructive">{run.error}</span>
|
||||
{run.errorCode && <span className="text-muted-foreground ml-1">({run.errorCode})</span>}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -3165,7 +3165,7 @@ function RunDetail({ run: initialRun, agentRouteId, adapterType }: { run: Heartb
|
|||
Login URL:
|
||||
<a
|
||||
href={claudeLoginResult.loginUrl}
|
||||
className="text-blue-600 underline underline-offset-2 ml-1 break-all dark:text-blue-400"
|
||||
className="text-primary underline underline-offset-2 ml-1 break-all"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
|
|
@ -3176,12 +3176,12 @@ function RunDetail({ run: initialRun, agentRouteId, adapterType }: { run: Heartb
|
|||
{claudeLoginResult && (
|
||||
<>
|
||||
{!!claudeLoginResult.stdout && (
|
||||
<pre className="bg-neutral-100 dark:bg-neutral-950 rounded-md p-3 text-xs font-mono text-foreground overflow-x-auto whitespace-pre-wrap">
|
||||
<pre className="bg-muted rounded-md p-3 text-xs font-mono text-foreground overflow-x-auto whitespace-pre-wrap">
|
||||
{claudeLoginResult.stdout}
|
||||
</pre>
|
||||
)}
|
||||
{!!claudeLoginResult.stderr && (
|
||||
<pre className="bg-neutral-100 dark:bg-neutral-950 rounded-md p-3 text-xs font-mono text-red-700 dark:text-red-300 overflow-x-auto whitespace-pre-wrap">
|
||||
<pre className="bg-muted rounded-md p-3 text-xs font-mono text-destructive overflow-x-auto whitespace-pre-wrap">
|
||||
{claudeLoginResult.stderr}
|
||||
</pre>
|
||||
)}
|
||||
|
|
@ -3190,7 +3190,7 @@ function RunDetail({ run: initialRun, agentRouteId, adapterType }: { run: Heartb
|
|||
</div>
|
||||
)}
|
||||
{hasNonZeroExit && (
|
||||
<div className="text-xs text-red-600 dark:text-red-400">
|
||||
<div className="text-xs text-destructive">
|
||||
Exit code {run.exitCode}
|
||||
{run.signal && <span className="text-muted-foreground ml-1">(signal: {run.signal})</span>}
|
||||
</div>
|
||||
|
|
@ -3229,7 +3229,7 @@ function RunDetail({ run: initialRun, agentRouteId, adapterType }: { run: Heartb
|
|||
>
|
||||
<ChevronRight className={cn("h-3 w-3 transition-transform", sessionOpen && "rotate-90")} />
|
||||
Session
|
||||
{sessionChanged && <span className="text-yellow-400 ml-1">(changed)</span>}
|
||||
{sessionChanged && <span className="text-warning ml-1">(changed)</span>}
|
||||
</button>
|
||||
{sessionOpen && (
|
||||
<div className="px-4 pb-3 space-y-1 text-xs">
|
||||
|
|
@ -3304,8 +3304,8 @@ function RunDetail({ run: initialRun, agentRouteId, adapterType }: { run: Heartb
|
|||
{/* stderr excerpt for failed runs */}
|
||||
{run.stderrExcerpt && (
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs font-medium text-red-600 dark:text-red-400">stderr</span>
|
||||
<pre className="bg-neutral-100 dark:bg-neutral-950 rounded-md p-3 text-xs font-mono text-red-700 dark:text-red-300 overflow-x-auto whitespace-pre-wrap">{run.stderrExcerpt}</pre>
|
||||
<span className="text-xs font-medium text-destructive">stderr</span>
|
||||
<pre className="bg-muted rounded-md p-3 text-xs font-mono text-destructive overflow-x-auto whitespace-pre-wrap">{run.stderrExcerpt}</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -3313,7 +3313,7 @@ function RunDetail({ run: initialRun, agentRouteId, adapterType }: { run: Heartb
|
|||
{run.stdoutExcerpt && !run.logRef && (
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs font-medium text-muted-foreground">stdout</span>
|
||||
<pre className="bg-neutral-100 dark:bg-neutral-950 rounded-md p-3 text-xs font-mono text-foreground overflow-x-auto whitespace-pre-wrap">{run.stdoutExcerpt}</pre>
|
||||
<pre className="bg-muted rounded-md p-3 text-xs font-mono text-foreground overflow-x-auto whitespace-pre-wrap">{run.stdoutExcerpt}</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -3716,14 +3716,14 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
|||
|
||||
const levelColors: Record<string, string> = {
|
||||
info: "text-foreground",
|
||||
warn: "text-yellow-600 dark:text-yellow-400",
|
||||
error: "text-red-600 dark:text-red-400",
|
||||
warn: "text-warning",
|
||||
error: "text-destructive",
|
||||
};
|
||||
|
||||
const streamColors: Record<string, string> = {
|
||||
stdout: "text-foreground",
|
||||
stderr: "text-red-600 dark:text-red-300",
|
||||
system: "text-blue-600 dark:text-blue-300",
|
||||
stderr: "text-destructive",
|
||||
system: "text-primary",
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -3771,7 +3771,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
|||
{adapterInvokePayload.prompt !== undefined && (
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-1">Prompt</div>
|
||||
<pre className="bg-neutral-100 dark:bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap">
|
||||
<pre className="bg-muted rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap">
|
||||
{typeof adapterInvokePayload.prompt === "string"
|
||||
? redactPathText(adapterInvokePayload.prompt, censorUsernameInLogs)
|
||||
: JSON.stringify(redactPathValue(adapterInvokePayload.prompt, censorUsernameInLogs), null, 2)}
|
||||
|
|
@ -3781,7 +3781,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
|||
{adapterInvokePayload.context !== undefined && (
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-1">Context</div>
|
||||
<pre className="bg-neutral-100 dark:bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap">
|
||||
<pre className="bg-muted rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap">
|
||||
{JSON.stringify(redactPathValue(adapterInvokePayload.context, censorUsernameInLogs), null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
|
|
@ -3789,7 +3789,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
|||
{adapterInvokePayload.env !== undefined && (
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-1">Environment</div>
|
||||
<pre className="bg-neutral-100 dark:bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap font-mono">
|
||||
<pre className="bg-muted rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap font-mono">
|
||||
{formatEnvForDisplay(adapterInvokePayload.env, censorUsernameInLogs)}
|
||||
</pre>
|
||||
</div>
|
||||
|
|
@ -3835,10 +3835,10 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
|||
</Button>
|
||||
)}
|
||||
{isLive && (
|
||||
<span className="flex items-center gap-1 text-xs text-cyan-400">
|
||||
<span className="flex items-center gap-1 text-xs text-primary">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-cyan-400 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-cyan-400" />
|
||||
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
|
||||
</span>
|
||||
Live
|
||||
</span>
|
||||
|
|
@ -3853,7 +3853,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
|||
emptyMessage={run.logRef ? "Waiting for transcript..." : "No persisted transcript for this run."}
|
||||
/>
|
||||
{logError && (
|
||||
<div className="mt-3 rounded-xl border border-red-500/20 bg-red-500/[0.06] px-3 py-2 text-xs text-red-700 dark:text-red-300">
|
||||
<div className="mt-3 rounded-xl border border-destructive/20 bg-destructive/[0.06] px-3 py-2 text-xs text-destructive">
|
||||
{logError}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -3861,34 +3861,34 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
|||
</div>
|
||||
|
||||
{(run.status === "failed" || run.status === "timed_out") && (
|
||||
<div className="rounded-lg border border-red-300 dark:border-red-500/30 bg-red-50 dark:bg-red-950/20 p-3 space-y-2">
|
||||
<div className="text-xs font-medium text-red-700 dark:text-red-300">Failure details</div>
|
||||
<div className="rounded-lg border border-destructive/30 bg-destructive/10 p-3 space-y-2">
|
||||
<div className="text-xs font-medium text-destructive">Failure details</div>
|
||||
{run.error && (
|
||||
<div className="text-xs text-red-600 dark:text-red-200">
|
||||
<span className="text-red-700 dark:text-red-300">Error: </span>
|
||||
<div className="text-xs text-destructive">
|
||||
<span className="text-destructive">Error: </span>
|
||||
{redactPathText(run.error, censorUsernameInLogs)}
|
||||
</div>
|
||||
)}
|
||||
{run.stderrExcerpt && run.stderrExcerpt.trim() && (
|
||||
<div>
|
||||
<div className="text-xs text-red-700 dark:text-red-300 mb-1">stderr excerpt</div>
|
||||
<pre className="bg-red-50 dark:bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap text-red-800 dark:text-red-100">
|
||||
<div className="text-xs text-destructive mb-1">stderr excerpt</div>
|
||||
<pre className="bg-destructive/10 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap text-destructive">
|
||||
{redactPathText(run.stderrExcerpt, censorUsernameInLogs)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{run.resultJson && (
|
||||
<div>
|
||||
<div className="text-xs text-red-700 dark:text-red-300 mb-1">adapter result JSON</div>
|
||||
<pre className="bg-red-50 dark:bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap text-red-800 dark:text-red-100">
|
||||
<div className="text-xs text-destructive mb-1">adapter result JSON</div>
|
||||
<pre className="bg-destructive/10 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap text-destructive">
|
||||
{JSON.stringify(redactPathValue(run.resultJson, censorUsernameInLogs), null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{run.stdoutExcerpt && run.stdoutExcerpt.trim() && !run.resultJson && (
|
||||
<div>
|
||||
<div className="text-xs text-red-700 dark:text-red-300 mb-1">stdout excerpt</div>
|
||||
<pre className="bg-red-50 dark:bg-neutral-950 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap text-red-800 dark:text-red-100">
|
||||
<div className="text-xs text-destructive mb-1">stdout excerpt</div>
|
||||
<pre className="bg-destructive/10 rounded-md p-2 text-xs overflow-x-auto whitespace-pre-wrap text-destructive">
|
||||
{redactPathText(run.stdoutExcerpt, censorUsernameInLogs)}
|
||||
</pre>
|
||||
</div>
|
||||
|
|
@ -3899,7 +3899,7 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
|||
{events.length > 0 && (
|
||||
<div>
|
||||
<div className="mb-2 text-xs font-medium text-muted-foreground">Events ({events.length})</div>
|
||||
<div className="bg-neutral-100 dark:bg-neutral-950 rounded-lg p-3 font-mono text-xs space-y-0.5">
|
||||
<div className="bg-muted rounded-lg p-3 font-mono text-xs space-y-0.5">
|
||||
{events.map((evt) => {
|
||||
const color = evt.color
|
||||
?? (evt.level ? levelColors[evt.level] : null)
|
||||
|
|
@ -3908,10 +3908,10 @@ function LogViewer({ run, adapterType }: { run: HeartbeatRun; adapterType: strin
|
|||
|
||||
return (
|
||||
<div key={evt.id} className="flex gap-2">
|
||||
<span className="text-neutral-400 dark:text-neutral-600 shrink-0 select-none w-16">
|
||||
<span className="text-muted-foreground shrink-0 select-none w-16">
|
||||
{new Date(evt.createdAt).toLocaleTimeString("en-US", { hour12: false })}
|
||||
</span>
|
||||
<span className={cn("shrink-0 w-14", evt.stream ? (streamColors[evt.stream] ?? "text-neutral-500") : "text-neutral-500")}>
|
||||
<span className={cn("shrink-0 w-14", evt.stream ? (streamColors[evt.stream] ?? "text-muted-foreground") : "text-muted-foreground")}>
|
||||
{evt.stream ? `[${evt.stream}]` : ""}
|
||||
</span>
|
||||
<span className={cn("break-all", color)}>
|
||||
|
|
@ -3976,12 +3976,12 @@ function KeysTab({ agentId, companyId }: { agentId: string; companyId?: string }
|
|||
<div className="space-y-6">
|
||||
{/* New token banner */}
|
||||
{newToken && (
|
||||
<div className="border border-yellow-300 dark:border-yellow-600/40 bg-yellow-50 dark:bg-yellow-500/5 rounded-lg p-4 space-y-2">
|
||||
<p className="text-sm font-medium text-yellow-700 dark:text-yellow-400">
|
||||
<div className="border border-warning/30 bg-warning/10 rounded-lg p-4 space-y-2">
|
||||
<p className="text-sm font-medium text-warning">
|
||||
API key created — copy it now, it will not be shown again.
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="flex-1 bg-neutral-100 dark:bg-neutral-950 rounded px-3 py-1.5 text-xs font-mono text-green-700 dark:text-green-300 truncate">
|
||||
<code className="flex-1 bg-muted rounded px-3 py-1.5 text-xs font-mono text-success truncate">
|
||||
{tokenVisible ? newToken : newToken.replace(/./g, "•")}
|
||||
</code>
|
||||
<Button
|
||||
|
|
@ -4000,7 +4000,7 @@ function KeysTab({ agentId, companyId }: { agentId: string; companyId?: string }
|
|||
>
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
{copied && <span className="text-xs text-green-400">Copied!</span>}
|
||||
{copied && <span className="text-xs text-success">Copied!</span>}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
|
|||
|
|
@ -401,14 +401,14 @@ function LiveRunIndicator({
|
|||
return (
|
||||
<Link
|
||||
to={`/agents/${agentRef}/runs/${runId}`}
|
||||
className="flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-blue-500/10 hover:bg-blue-500/20 transition-colors no-underline"
|
||||
className="flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-primary/10 hover:bg-primary/20 transition-colors no-underline"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-blue-500" />
|
||||
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-primary" />
|
||||
</span>
|
||||
<span className="text-[11px] font-medium text-blue-600 dark:text-blue-400">
|
||||
<span className="text-[11px] font-medium text-primary">
|
||||
Live{liveCount > 1 ? ` (${liveCount})` : ""}
|
||||
</span>
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -174,16 +174,16 @@ export function ApprovalDetail() {
|
|||
return (
|
||||
<div className="space-y-6 max-w-3xl">
|
||||
{showApprovedBanner && (
|
||||
<div className="border border-green-300 dark:border-green-700/40 bg-green-50 dark:bg-green-900/20 rounded-lg px-4 py-3 animate-in fade-in zoom-in-95 duration-300">
|
||||
<div className="border border-success/30 bg-success/10 rounded-lg px-4 py-3 animate-in fade-in zoom-in-95 duration-300">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="relative mt-0.5">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600 dark:text-green-300" />
|
||||
<Sparkles className="h-3 w-3 text-green-500 dark:text-green-200 absolute -right-2 -top-1 animate-pulse" />
|
||||
<CheckCircle2 className="h-4 w-4 text-success" />
|
||||
<Sparkles className="h-3 w-3 text-success absolute -right-2 -top-1 animate-pulse" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-green-800 dark:text-green-100 font-medium">Approval confirmed</p>
|
||||
<p className="text-xs text-green-700 dark:text-green-200/90">
|
||||
<p className="text-sm text-success font-medium">Approval confirmed</p>
|
||||
<p className="text-xs text-success">
|
||||
Requesting agent was notified to review this approval and linked issues.
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -191,7 +191,7 @@ export function ApprovalDetail() {
|
|||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="border-green-400 dark:border-green-600/50 text-green-800 dark:text-green-100 hover:bg-green-100 dark:hover:bg-green-900/30"
|
||||
className="border-success/30 text-success hover:bg-success/10 hover:bg-success/30"
|
||||
onClick={() => navigate(resolvedCta.to)}
|
||||
>
|
||||
{resolvedCta.label}
|
||||
|
|
@ -266,7 +266,7 @@ export function ApprovalDetail() {
|
|||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-green-700 hover:bg-green-600 text-white"
|
||||
className="bg-success hover:bg-success text-white"
|
||||
onClick={() => approveMutation.mutate()}
|
||||
disabled={approveMutation.isPending}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ export function Approvals() {
|
|||
{ value: "pending", label: <>Pending{pendingCount > 0 && (
|
||||
<span className={cn(
|
||||
"ml-1.5 rounded-full px-1.5 py-0.5 text-[10px] font-medium",
|
||||
"bg-yellow-500/20 text-yellow-500"
|
||||
"bg-warning/20 text-warning"
|
||||
)}>
|
||||
{pendingCount}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ export function Companies() {
|
|||
onClick={saveEdit}
|
||||
disabled={editMutation.isPending}
|
||||
>
|
||||
<Check className="h-3.5 w-3.5 text-green-500" />
|
||||
<Check className="h-3.5 w-3.5 text-success" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon-xs" onClick={cancelEdit}>
|
||||
<X className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
|
|
@ -171,9 +171,9 @@ export function Companies() {
|
|||
<span
|
||||
className={`inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium ${
|
||||
company.status === "active"
|
||||
? "bg-green-500/10 text-green-600 dark:text-green-400"
|
||||
? "bg-success/10 text-success"
|
||||
: company.status === "paused"
|
||||
? "bg-yellow-500/10 text-yellow-600 dark:text-yellow-400"
|
||||
? "bg-warning/10 text-warning"
|
||||
: "bg-muted text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -942,7 +942,7 @@ export function CompanyExport() {
|
|||
{selectedCount} / {totalFiles} file{totalFiles === 1 ? "" : "s"} selected
|
||||
</span>
|
||||
{warnings.length > 0 && (
|
||||
<span className="text-amber-500">
|
||||
<span className="text-warning">
|
||||
{warnings.length} warning{warnings.length === 1 ? "" : "s"}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -962,9 +962,9 @@ export function CompanyExport() {
|
|||
|
||||
{/* Warnings */}
|
||||
{warnings.length > 0 && (
|
||||
<div className="mx-5 mt-3 rounded-md border border-amber-500/30 bg-amber-500/5 px-4 py-3">
|
||||
<div className="mx-5 mt-3 rounded-md border border-warning/30 bg-warning/5 px-4 py-3">
|
||||
{warnings.map((w) => (
|
||||
<div key={w} className="text-xs text-amber-500">{w}</div>
|
||||
<div key={w} className="text-xs text-warning">{w}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -104,10 +104,10 @@ function ensureMarkdownPath(p: string): string {
|
|||
}
|
||||
|
||||
const ACTION_COLORS: Record<string, string> = {
|
||||
create: "text-emerald-500 border-emerald-500/30",
|
||||
update: "text-amber-500 border-amber-500/30",
|
||||
overwrite: "text-red-500 border-red-500/30",
|
||||
replace: "text-red-500 border-red-500/30",
|
||||
create: "text-success border-success/30",
|
||||
update: "text-warning border-warning/30",
|
||||
overwrite: "text-destructive border-destructive/30",
|
||||
replace: "text-destructive border-destructive/30",
|
||||
skip: "text-muted-foreground border-border",
|
||||
none: "text-muted-foreground border-border",
|
||||
};
|
||||
|
|
@ -163,7 +163,7 @@ function renderImportFileExtra(node: FileTreeNode, checked: boolean, renameMap:
|
|||
return (
|
||||
<span className="inline-flex items-center gap-1.5 shrink-0">
|
||||
{renamedTo && checked && (
|
||||
<span className="text-[10px] text-cyan-500 font-mono truncate max-w-[7rem]" title={renamedTo}>
|
||||
<span className="text-[10px] text-primary font-mono truncate max-w-[7rem]" title={renamedTo}>
|
||||
→ {renamedTo}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -222,7 +222,7 @@ function ImportPreviewPane({
|
|||
<div className="min-w-0 flex items-center gap-2">
|
||||
<span className="truncate font-mono text-sm">{selectedFile}</span>
|
||||
{renamedTo && (
|
||||
<span className="shrink-0 font-mono text-sm text-cyan-500">
|
||||
<span className="shrink-0 font-mono text-sm text-primary">
|
||||
→ {renamedTo}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -428,7 +428,7 @@ function ConflictResolutionList({
|
|||
className={cn(
|
||||
"flex items-center gap-3 px-4 py-2.5 text-sm",
|
||||
isSkipped && "opacity-40",
|
||||
isConfirmed && !isSkipped && "bg-emerald-500/5",
|
||||
isConfirmed && !isSkipped && "bg-success/5",
|
||||
)}
|
||||
>
|
||||
{/* Skip button on the left */}
|
||||
|
|
@ -450,8 +450,8 @@ function ConflictResolutionList({
|
|||
isSkipped
|
||||
? "text-muted-foreground border-border"
|
||||
: isConfirmed
|
||||
? "text-emerald-500 border-emerald-500/30"
|
||||
: "text-amber-500 border-amber-500/30",
|
||||
? "text-success border-success/30"
|
||||
: "text-warning border-warning/30",
|
||||
)}>
|
||||
{item.kind}
|
||||
</span>
|
||||
|
|
@ -467,7 +467,7 @@ function ConflictResolutionList({
|
|||
<>
|
||||
<ArrowRight className="h-3 w-3 shrink-0 text-muted-foreground" />
|
||||
{isConfirmed ? (
|
||||
<span className="min-w-0 flex-1 font-mono text-xs text-emerald-500">
|
||||
<span className="min-w-0 flex-1 font-mono text-xs text-success">
|
||||
{currentName}
|
||||
</span>
|
||||
) : (
|
||||
|
|
@ -487,7 +487,7 @@ function ConflictResolutionList({
|
|||
className={cn(
|
||||
"ml-auto shrink-0 rounded-md border px-2.5 py-1 text-xs transition-colors inline-flex items-center gap-1.5",
|
||||
isConfirmed
|
||||
? "border-emerald-500/30 bg-emerald-500/10 text-emerald-500"
|
||||
? "border-success/30 bg-success/10 text-success"
|
||||
: "border-border text-muted-foreground hover:bg-accent/50",
|
||||
)}
|
||||
onClick={() => onToggleConfirm(item.slug)}
|
||||
|
|
@ -565,7 +565,7 @@ function AdapterPickerList({
|
|||
<div className="flex items-center gap-3 px-4 py-2.5 text-sm">
|
||||
<span className={cn(
|
||||
"shrink-0 rounded-full border px-2 py-0.5 text-[10px] uppercase tracking-wide",
|
||||
"text-blue-500 border-blue-500/30",
|
||||
"text-primary border-primary/30",
|
||||
)}>
|
||||
agent
|
||||
</span>
|
||||
|
|
@ -1252,7 +1252,7 @@ export function CompanyImport() {
|
|||
{selectedCount} / {totalFiles} file{totalFiles === 1 ? "" : "s"} selected
|
||||
</span>
|
||||
{conflicts.length > 0 && (
|
||||
<span className="text-amber-500">
|
||||
<span className="text-warning">
|
||||
{conflicts.length} conflict{conflicts.length === 1 ? "" : "s"}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -1302,9 +1302,9 @@ export function CompanyImport() {
|
|||
|
||||
{/* Warnings */}
|
||||
{importPreview.warnings.length > 0 && (
|
||||
<div className="mx-5 mt-3 rounded-md border border-amber-500/30 bg-amber-500/5 px-4 py-3">
|
||||
<div className="mx-5 mt-3 rounded-md border border-warning/30 bg-warning/5 px-4 py-3">
|
||||
{importPreview.warnings.map((w) => (
|
||||
<div key={w} className="text-xs text-amber-500">{w}</div>
|
||||
<div key={w} className="text-xs text-warning">{w}</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -345,7 +345,7 @@ export function CompanySettings() {
|
|||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="color"
|
||||
value={brandColor || "#6366f1"}
|
||||
value={brandColor || "var(--primary)"}
|
||||
onChange={(e) => setBrandColor(e.target.value)}
|
||||
className="h-8 w-8 cursor-pointer rounded border border-border bg-transparent p-0"
|
||||
/>
|
||||
|
|
@ -498,7 +498,7 @@ export function CompanySettings() {
|
|||
{snippetCopied && (
|
||||
<span
|
||||
key={snippetCopyDelightId}
|
||||
className="flex items-center gap-1 text-xs text-green-600 animate-pulse"
|
||||
className="flex items-center gap-1 text-xs text-success animate-pulse"
|
||||
>
|
||||
<Check className="h-3 w-3" />
|
||||
Copied
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export function ContentStudio() {
|
|||
const companyId = selectedCompanyId ?? "";
|
||||
const themeJob = useContentJob(companyId);
|
||||
const [showApplyDialog, setShowApplyDialog] = useState(false);
|
||||
const [seedColor, setSeedColor] = useState("#4f46e5");
|
||||
const [seedColor, setSeedColor] = useState("var(--primary)");
|
||||
const [themeBundle, setThemeBundle] = useState<{
|
||||
palette: PaletteRole[];
|
||||
exports: { css: string; tailwind: string; vscode: string; json: string };
|
||||
|
|
|
|||
|
|
@ -695,10 +695,10 @@ export function Costs() {
|
|||
className={cn(
|
||||
"h-full transition-[width,background-color] duration-150",
|
||||
spendData.summary.utilizationPercent > 90
|
||||
? "bg-red-400"
|
||||
? "bg-destructive"
|
||||
: spendData.summary.utilizationPercent > 70
|
||||
? "bg-yellow-400"
|
||||
: "bg-emerald-400",
|
||||
? "bg-warning"
|
||||
: "bg-success",
|
||||
)}
|
||||
style={{ width: `${Math.min(100, spendData.summary.utilizationPercent)}%` }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -191,16 +191,16 @@ export function Dashboard() {
|
|||
{error && <p className="text-sm text-destructive">{error.message}</p>}
|
||||
|
||||
{hasNoAgents && (
|
||||
<div className="flex items-center justify-between gap-3 rounded-md border border-amber-300 bg-amber-50 px-4 py-3 dark:border-amber-500/25 dark:bg-amber-950/60">
|
||||
<div className="flex items-center justify-between gap-3 rounded-md border border-warning/30 bg-warning/10 px-4 py-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Bot className="h-4 w-4 text-amber-600 dark:text-amber-400 shrink-0" />
|
||||
<p className="text-sm text-amber-900 dark:text-amber-100">
|
||||
<Bot className="h-4 w-4 text-warning shrink-0" />
|
||||
<p className="text-sm text-warning">
|
||||
You have no agents.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => openOnboarding({ initialStep: 2, companyId: selectedCompanyId! })}
|
||||
className="text-sm font-medium text-amber-700 hover:text-amber-900 dark:text-amber-300 dark:hover:text-amber-100 underline underline-offset-2 shrink-0"
|
||||
className="text-sm font-medium text-warning hover:text-warning hover:text-warning underline underline-offset-2 shrink-0"
|
||||
>
|
||||
Create one here
|
||||
</button>
|
||||
|
|
@ -212,19 +212,19 @@ export function Dashboard() {
|
|||
{data && (
|
||||
<>
|
||||
{data.budgets.activeIncidents > 0 ? (
|
||||
<div className="flex items-start justify-between gap-3 rounded-xl border border-red-500/20 bg-[linear-gradient(180deg,rgba(255,80,80,0.12),rgba(255,255,255,0.02))] px-4 py-3">
|
||||
<div className="flex items-start justify-between gap-3 rounded-xl border border-destructive/20 bg-[linear-gradient(180deg,rgba(255,80,80,0.12),rgba(255,255,255,0.02))] px-4 py-3">
|
||||
<div className="flex items-start gap-2.5">
|
||||
<PauseCircle className="mt-0.5 h-4 w-4 shrink-0 text-red-300" />
|
||||
<PauseCircle className="mt-0.5 h-4 w-4 shrink-0 text-destructive" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-red-50">
|
||||
<p className="text-sm font-medium text-destructive">
|
||||
{data.budgets.activeIncidents} active budget incident{data.budgets.activeIncidents === 1 ? "" : "s"}
|
||||
</p>
|
||||
<p className="text-xs text-red-100/70">
|
||||
<p className="text-xs text-destructive">
|
||||
{data.budgets.pausedAgents} agents paused · {data.budgets.pausedProjects} projects paused · {data.budgets.pendingApprovals} pending budget approvals
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Link to="/costs" className="text-sm underline underline-offset-2 text-red-100">
|
||||
<Link to="/costs" className="text-sm underline underline-offset-2 text-destructive">
|
||||
Open budgets
|
||||
</Link>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -461,9 +461,9 @@ export function DesignGuide() {
|
|||
<SubSection title="Run invocation badges">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
{[
|
||||
["timer", "bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300"],
|
||||
["assignment", "bg-violet-100 text-violet-700 dark:bg-violet-900/50 dark:text-violet-300"],
|
||||
["on_demand", "bg-cyan-100 text-cyan-700 dark:bg-cyan-900/50 dark:text-cyan-300"],
|
||||
["timer", "bg-primary/10 text-primary"],
|
||||
["assignment", "bg-muted text-primary"],
|
||||
["on_demand", "bg-primary/10 text-primary"],
|
||||
["automation", "bg-muted text-muted-foreground"],
|
||||
].map(([label, cls]) => (
|
||||
<span key={label} className={`rounded-full px-1.5 py-0.5 text-[10px] font-medium ${cls}`}>
|
||||
|
|
@ -1033,9 +1033,9 @@ export function DesignGuide() {
|
|||
<Section title="Progress Bars (Budget)">
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ label: "Under budget (40%)", pct: 40, color: "bg-green-400" },
|
||||
{ label: "Warning (75%)", pct: 75, color: "bg-yellow-400" },
|
||||
{ label: "Over budget (95%)", pct: 95, color: "bg-red-400" },
|
||||
{ label: "Under budget (40%)", pct: 40, color: "bg-success" },
|
||||
{ label: "Warning (75%)", pct: 75, color: "bg-warning" },
|
||||
{ label: "Over budget (95%)", pct: 95, color: "bg-destructive" },
|
||||
].map(({ label, pct, color }) => (
|
||||
<div key={label} className="space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
|
|
@ -1057,20 +1057,20 @@ export function DesignGuide() {
|
|||
{/* LOG VIEWER */}
|
||||
{/* ============================================================ */}
|
||||
<Section title="Log Viewer">
|
||||
<div className="bg-neutral-950 rounded-lg p-3 font-mono text-xs max-h-80 overflow-y-auto">
|
||||
<div className="bg-muted rounded-lg p-3 font-mono text-xs max-h-80 overflow-y-auto">
|
||||
<div className="text-foreground">[12:00:01] INFO Agent started successfully</div>
|
||||
<div className="text-foreground">[12:00:02] INFO Processing task PAP-001</div>
|
||||
<div className="text-yellow-400">[12:00:05] WARN Rate limit approaching (80%)</div>
|
||||
<div className="text-warning">[12:00:05] WARN Rate limit approaching (80%)</div>
|
||||
<div className="text-foreground">[12:00:08] INFO Task PAP-001 completed</div>
|
||||
<div className="text-red-400">[12:00:12] ERROR Connection timeout to upstream service</div>
|
||||
<div className="text-blue-300">[12:00:12] SYS Retrying connection in 5s...</div>
|
||||
<div className="text-destructive">[12:00:12] ERROR Connection timeout to upstream service</div>
|
||||
<div className="text-primary">[12:00:12] SYS Retrying connection in 5s...</div>
|
||||
<div className="text-foreground">[12:00:17] INFO Reconnected successfully</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="relative flex h-1.5 w-1.5">
|
||||
<span className="absolute inline-flex h-full w-full rounded-full bg-cyan-400 animate-pulse" />
|
||||
<span className="inline-flex h-full w-full rounded-full bg-cyan-400" />
|
||||
<span className="absolute inline-flex h-full w-full rounded-full bg-primary animate-pulse" />
|
||||
<span className="inline-flex h-full w-full rounded-full bg-primary" />
|
||||
</span>
|
||||
<span className="text-cyan-400">Live</span>
|
||||
<span className="text-primary">Live</span>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
|
|
|
|||
|
|
@ -384,7 +384,7 @@ export function ExecutionWorkspaceDetail() {
|
|||
</Button>
|
||||
<StatusPill>{workspace.mode}</StatusPill>
|
||||
<StatusPill>{workspace.providerType}</StatusPill>
|
||||
<StatusPill className={workspace.status === "active" ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300" : undefined}>
|
||||
<StatusPill className={workspace.status === "active" ? "border-success/30 bg-success/10 text-success" : undefined}>
|
||||
{workspace.status}
|
||||
</StatusPill>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -188,22 +188,22 @@ export function InboxIssueMetaLeading({
|
|||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 sm:gap-1.5 sm:px-2",
|
||||
"bg-blue-500/10",
|
||||
"bg-primary/10",
|
||||
)}
|
||||
>
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="absolute inline-flex h-full w-full animate-pulse rounded-full bg-blue-400 opacity-75" />
|
||||
<span className="absolute inline-flex h-full w-full animate-pulse rounded-full bg-primary opacity-75" />
|
||||
<span
|
||||
className={cn(
|
||||
"relative inline-flex h-2 w-2 rounded-full",
|
||||
"bg-blue-500",
|
||||
"bg-primary",
|
||||
)}
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"hidden text-[11px] font-medium sm:inline",
|
||||
"text-blue-600 dark:text-blue-400",
|
||||
"text-primary",
|
||||
)}
|
||||
>
|
||||
Live
|
||||
|
|
@ -286,7 +286,7 @@ export function InboxIssueTrailingColumns({
|
|||
|
||||
if (column === "project") {
|
||||
if (projectName) {
|
||||
const accentColor = projectColor ?? "#64748b";
|
||||
const accentColor = projectColor ?? "var(--muted-foreground)";
|
||||
return (
|
||||
<span
|
||||
key={column}
|
||||
|
|
@ -407,13 +407,13 @@ export function FailedRunInboxRow({
|
|||
onClick={onMarkRead}
|
||||
className={cn(
|
||||
"inline-flex h-4 w-4 items-center justify-center rounded-full transition-colors",
|
||||
"hover:bg-blue-500/20",
|
||||
"hover:bg-primary/20",
|
||||
)}
|
||||
aria-label="Mark as read"
|
||||
>
|
||||
<span className={cn(
|
||||
"block h-2 w-2 rounded-full transition-opacity duration-300",
|
||||
"bg-blue-600 dark:bg-blue-400",
|
||||
"bg-primary",
|
||||
unreadState === "fading" ? "opacity-0" : "opacity-100",
|
||||
)} />
|
||||
</button>
|
||||
|
|
@ -441,8 +441,8 @@ export function FailedRunInboxRow({
|
|||
>
|
||||
{!showUnreadSlot && <span className="hidden h-2 w-2 shrink-0 sm:inline-flex" aria-hidden="true" />}
|
||||
<span className="hidden h-3.5 w-3.5 shrink-0 sm:inline-flex" aria-hidden="true" />
|
||||
<span className="mt-0.5 shrink-0 rounded-md bg-red-500/20 p-1.5 sm:mt-0">
|
||||
<XCircle className="h-4 w-4 text-red-600 dark:text-red-400" />
|
||||
<span className="mt-0.5 shrink-0 rounded-md bg-destructive/20 p-1.5 sm:mt-0">
|
||||
<XCircle className="h-4 w-4 text-destructive" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="line-clamp-2 text-sm font-medium sm:truncate sm:line-clamp-none">
|
||||
|
|
@ -563,13 +563,13 @@ function ApprovalInboxRow({
|
|||
onClick={onMarkRead}
|
||||
className={cn(
|
||||
"inline-flex h-4 w-4 items-center justify-center rounded-full transition-colors",
|
||||
"hover:bg-blue-500/20",
|
||||
"hover:bg-primary/20",
|
||||
)}
|
||||
aria-label="Mark as read"
|
||||
>
|
||||
<span className={cn(
|
||||
"block h-2 w-2 rounded-full transition-opacity duration-300",
|
||||
"bg-blue-600 dark:bg-blue-400",
|
||||
"bg-primary",
|
||||
unreadState === "fading" ? "opacity-0" : "opacity-100",
|
||||
)} />
|
||||
</button>
|
||||
|
|
@ -615,7 +615,7 @@ function ApprovalInboxRow({
|
|||
<div className="hidden shrink-0 items-center gap-2 sm:flex">
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-8 bg-green-700 px-3 text-white hover:bg-green-600"
|
||||
className="h-8 bg-success px-3 text-white hover:bg-success"
|
||||
onClick={onApprove}
|
||||
disabled={isPending}
|
||||
>
|
||||
|
|
@ -637,7 +637,7 @@ function ApprovalInboxRow({
|
|||
<div className="mt-3 flex gap-2 sm:hidden">
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-8 bg-green-700 px-3 text-white hover:bg-green-600"
|
||||
className="h-8 bg-success px-3 text-white hover:bg-success"
|
||||
onClick={onApprove}
|
||||
disabled={isPending}
|
||||
>
|
||||
|
|
@ -702,13 +702,13 @@ function JoinRequestInboxRow({
|
|||
onClick={onMarkRead}
|
||||
className={cn(
|
||||
"inline-flex h-4 w-4 items-center justify-center rounded-full transition-colors",
|
||||
"hover:bg-blue-500/20",
|
||||
"hover:bg-primary/20",
|
||||
)}
|
||||
aria-label="Mark as read"
|
||||
>
|
||||
<span className={cn(
|
||||
"block h-2 w-2 rounded-full transition-opacity duration-300",
|
||||
"bg-blue-600 dark:bg-blue-400",
|
||||
"bg-primary",
|
||||
unreadState === "fading" ? "opacity-0" : "opacity-100",
|
||||
)} />
|
||||
</button>
|
||||
|
|
@ -746,7 +746,7 @@ function JoinRequestInboxRow({
|
|||
<div className="hidden shrink-0 items-center gap-2 sm:flex">
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-8 bg-green-700 px-3 text-white hover:bg-green-600"
|
||||
className="h-8 bg-success px-3 text-white hover:bg-success"
|
||||
onClick={onApprove}
|
||||
disabled={isPending}
|
||||
>
|
||||
|
|
@ -766,7 +766,7 @@ function JoinRequestInboxRow({
|
|||
<div className="mt-3 flex gap-2 sm:hidden">
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-8 bg-green-700 px-3 text-white hover:bg-green-600"
|
||||
className="h-8 bg-success px-3 text-white hover:bg-success"
|
||||
onClick={onApprove}
|
||||
disabled={isPending}
|
||||
>
|
||||
|
|
@ -1775,8 +1775,8 @@ export function Inbox() {
|
|||
if (showTodayDivider) {
|
||||
elements.push(
|
||||
<div key="today-divider" className="flex items-center gap-3 px-4 my-2">
|
||||
<div className="flex-1 border-t border-zinc-600" />
|
||||
<span className="shrink-0 text-[11px] font-medium uppercase tracking-wider text-zinc-500">
|
||||
<div className="flex-1 border-t border-border" />
|
||||
<span className="shrink-0 text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
|
||||
Earlier
|
||||
</span>
|
||||
</div>,
|
||||
|
|
@ -1979,7 +1979,7 @@ export function Inbox() {
|
|||
to="/agents"
|
||||
className="flex flex-1 cursor-pointer items-center gap-3 no-underline text-inherit"
|
||||
>
|
||||
<AlertTriangle className="h-4 w-4 shrink-0 text-red-600 dark:text-red-400" />
|
||||
<AlertTriangle className="h-4 w-4 shrink-0 text-destructive" />
|
||||
<span className="text-sm">
|
||||
<span className="font-medium">{dashboard!.agents.error}</span>{" "}
|
||||
{dashboard!.agents.error === 1 ? "agent has" : "agents have"} errors
|
||||
|
|
@ -2001,7 +2001,7 @@ export function Inbox() {
|
|||
to="/costs"
|
||||
className="flex flex-1 cursor-pointer items-center gap-3 no-underline text-inherit"
|
||||
>
|
||||
<AlertTriangle className="h-4 w-4 shrink-0 text-yellow-400" />
|
||||
<AlertTriangle className="h-4 w-4 shrink-0 text-warning" />
|
||||
<span className="text-sm">
|
||||
Budget at{" "}
|
||||
<span className="font-medium">{dashboard!.costs.monthUtilizationPercent}%</span>{" "}
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ export function InstanceExperimentalSettings() {
|
|||
disabled={toggleMutation.isPending}
|
||||
className={cn(
|
||||
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-60",
|
||||
enableIsolatedWorkspaces ? "bg-green-600" : "bg-muted",
|
||||
enableIsolatedWorkspaces ? "bg-success" : "bg-muted",
|
||||
)}
|
||||
onClick={() => toggleMutation.mutate({ enableIsolatedWorkspaces: !enableIsolatedWorkspaces })}
|
||||
>
|
||||
|
|
@ -119,7 +119,7 @@ export function InstanceExperimentalSettings() {
|
|||
disabled={toggleMutation.isPending}
|
||||
className={cn(
|
||||
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-60",
|
||||
autoRestartDevServerWhenIdle ? "bg-green-600" : "bg-muted",
|
||||
autoRestartDevServerWhenIdle ? "bg-success" : "bg-muted",
|
||||
)}
|
||||
onClick={() =>
|
||||
toggleMutation.mutate({ autoRestartDevServerWhenIdle: !autoRestartDevServerWhenIdle })
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ export function InstanceGeneralSettings() {
|
|||
disabled={updateGeneralMutation.isPending}
|
||||
className={cn(
|
||||
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-60",
|
||||
censorUsernameInLogs ? "bg-green-600" : "bg-muted",
|
||||
censorUsernameInLogs ? "bg-success" : "bg-muted",
|
||||
)}
|
||||
onClick={() =>
|
||||
updateGeneralMutation.mutate({
|
||||
|
|
@ -157,7 +157,7 @@ export function InstanceGeneralSettings() {
|
|||
disabled={updateGeneralMutation.isPending}
|
||||
className={cn(
|
||||
"relative inline-flex h-5 w-9 items-center rounded-full transition-colors disabled:cursor-not-allowed disabled:opacity-60",
|
||||
keyboardShortcuts ? "bg-green-600" : "bg-muted",
|
||||
keyboardShortcuts ? "bg-success" : "bg-muted",
|
||||
)}
|
||||
onClick={() => updateGeneralMutation.mutate({ keyboardShortcuts: !keyboardShortcuts })}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ export function InviteLandingPage() {
|
|||
<p className="font-medium text-foreground">Connectivity diagnostics</p>
|
||||
{diagnostics.map((diag, idx) => (
|
||||
<div key={`${diag.code}:${idx}`} className="space-y-0.5">
|
||||
<p className={diag.level === "warn" ? "text-amber-600 dark:text-amber-400" : undefined}>
|
||||
<p className={diag.level === "warn" ? "text-warning" : undefined}>
|
||||
[{diag.level}] {diag.message}
|
||||
</p>
|
||||
{diag.hint && <p className="font-mono break-all">{diag.hint}</p>}
|
||||
|
|
|
|||
|
|
@ -1177,10 +1177,10 @@ export function IssueDetail() {
|
|||
<span className="text-sm font-mono text-muted-foreground shrink-0">{issue.identifier ?? issue.id.slice(0, 8)}</span>
|
||||
|
||||
{hasLiveRuns && (
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-cyan-500/10 border border-cyan-500/30 px-2 py-0.5 text-[10px] font-medium text-cyan-600 dark:text-cyan-400 shrink-0">
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary/10 border border-primary/30 px-2 py-0.5 text-[10px] font-medium text-primary shrink-0">
|
||||
<span className="relative flex h-1.5 w-1.5">
|
||||
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-cyan-400 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-cyan-400" />
|
||||
<span className="animate-pulse absolute inline-flex h-full w-full rounded-full bg-primary opacity-75" />
|
||||
<span className="relative inline-flex rounded-full h-1.5 w-1.5 bg-primary" />
|
||||
</span>
|
||||
Live
|
||||
</span>
|
||||
|
|
@ -1189,7 +1189,7 @@ export function IssueDetail() {
|
|||
{issue.originKind === "routine_execution" && issue.originId && (
|
||||
<Link
|
||||
to={`/routines/${issue.originId}`}
|
||||
className="inline-flex items-center gap-1 rounded-full bg-violet-500/10 border border-violet-500/30 px-2 py-0.5 text-[10px] font-medium text-violet-600 dark:text-violet-400 shrink-0 hover:bg-violet-500/20 transition-colors"
|
||||
className="inline-flex items-center gap-1 rounded-full bg-primary/10 border border-primary/30 px-2 py-0.5 text-[10px] font-medium text-primary shrink-0 hover:bg-primary/20 transition-colors"
|
||||
>
|
||||
<Repeat className="h-3 w-3" />
|
||||
Routine
|
||||
|
|
@ -1239,7 +1239,7 @@ export function IssueDetail() {
|
|||
onClick={copyIssueToClipboard}
|
||||
title="Copy issue as markdown"
|
||||
>
|
||||
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||||
{copied ? <Check className="h-4 w-4 text-success" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
@ -1258,7 +1258,7 @@ export function IssueDetail() {
|
|||
onClick={copyIssueToClipboard}
|
||||
title="Copy issue as markdown"
|
||||
>
|
||||
{copied ? <Check className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
|
||||
{copied ? <Check className="h-4 w-4 text-success" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
|
|||
|
|
@ -69,14 +69,14 @@ function OrgTreeNode({
|
|||
className={cn(
|
||||
"h-2 w-2 rounded-full shrink-0",
|
||||
node.status === "active"
|
||||
? "bg-green-400"
|
||||
? "bg-success"
|
||||
: node.status === "paused"
|
||||
? "bg-yellow-400"
|
||||
? "bg-warning"
|
||||
: node.status === "pending_approval"
|
||||
? "bg-amber-400"
|
||||
? "bg-warning"
|
||||
: node.status === "error"
|
||||
? "bg-red-400"
|
||||
: "bg-neutral-400"
|
||||
? "bg-destructive"
|
||||
: "bg-muted"
|
||||
)}
|
||||
/>
|
||||
<span className="font-medium flex-1">{node.name}</span>
|
||||
|
|
|
|||
|
|
@ -129,15 +129,17 @@ const adapterLabels: Record<string, string> = {
|
|||
http: "HTTP",
|
||||
};
|
||||
|
||||
// [nexus] Design system migration Phase 3 — status colors now reference
|
||||
// semantic CSS variables so they auto-switch with light/dark themes.
|
||||
const statusDotColor: Record<string, string> = {
|
||||
running: "#22d3ee",
|
||||
active: "#4ade80",
|
||||
paused: "#facc15",
|
||||
idle: "#facc15",
|
||||
error: "#f87171",
|
||||
terminated: "#a3a3a3",
|
||||
running: "var(--primary)",
|
||||
active: "var(--success)",
|
||||
paused: "var(--warning)",
|
||||
idle: "var(--muted-foreground)",
|
||||
error: "var(--destructive)",
|
||||
terminated: "var(--muted-foreground)",
|
||||
};
|
||||
const defaultDotColor = "#a3a3a3";
|
||||
const defaultDotColor = "var(--muted-foreground)";
|
||||
|
||||
// ── Main component ──────────────────────────────────────────────────────
|
||||
|
||||
|
|
|
|||
|
|
@ -205,9 +205,9 @@ export function PluginManager() {
|
|||
</Dialog>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-amber-500/30 bg-amber-500/5 px-4 py-3">
|
||||
<div className="rounded-lg border border-warning/30 bg-warning/5 px-4 py-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-amber-700" />
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-warning" />
|
||||
<div className="space-y-1 text-sm">
|
||||
<p className="font-medium text-foreground">Plugins are alpha.</p>
|
||||
<p className="text-muted-foreground">
|
||||
|
|
@ -251,7 +251,7 @@ export function PluginManager() {
|
|||
{installedPlugin ? (
|
||||
<Badge
|
||||
variant={installedPlugin.status === "ready" ? "default" : "secondary"}
|
||||
className={installedPlugin.status === "ready" ? "bg-green-600 hover:bg-green-700" : ""}
|
||||
className={installedPlugin.status === "ready" ? "bg-success hover:bg-success" : ""}
|
||||
>
|
||||
{installedPlugin.status}
|
||||
</Badge>
|
||||
|
|
@ -347,15 +347,15 @@ export function PluginManager() {
|
|||
{plugin.manifestJson.description || "No description provided."}
|
||||
</p>
|
||||
{plugin.status === "error" && (
|
||||
<div className="mt-3 rounded-md border border-red-500/25 bg-red-500/[0.06] px-3 py-2">
|
||||
<div className="mt-3 rounded-md border border-destructive/25 bg-destructive/[0.06] px-3 py-2">
|
||||
<div className="flex flex-wrap items-start gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-red-700 dark:text-red-300">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-destructive">
|
||||
<AlertTriangle className="h-4 w-4 shrink-0" />
|
||||
<span>Plugin error</span>
|
||||
</div>
|
||||
<p
|
||||
className="mt-1 text-sm text-red-700/90 dark:text-red-200/90 break-words"
|
||||
className="mt-1 text-sm text-destructive break-words"
|
||||
title={plugin.lastError ?? undefined}
|
||||
>
|
||||
{errorSummaryByPluginId.get(plugin.id)}
|
||||
|
|
@ -364,7 +364,7 @@ export function PluginManager() {
|
|||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-red-500/30 bg-background/60 text-red-700 hover:bg-red-500/10 hover:text-red-800 dark:text-red-200 dark:hover:text-red-100"
|
||||
className="border-destructive/30 bg-background/60 text-destructive hover:bg-destructive/10 hover:text-destructive hover:text-destructive"
|
||||
onClick={() => setErrorDetailsPlugin(plugin)}
|
||||
>
|
||||
View full error
|
||||
|
|
@ -386,7 +386,7 @@ export function PluginManager() {
|
|||
}
|
||||
className={cn(
|
||||
"shrink-0",
|
||||
plugin.status === "ready" ? "bg-green-600 hover:bg-green-700" : ""
|
||||
plugin.status === "ready" ? "bg-success hover:bg-success" : ""
|
||||
)}
|
||||
>
|
||||
{plugin.status}
|
||||
|
|
@ -405,7 +405,7 @@ export function PluginManager() {
|
|||
}}
|
||||
disabled={enableMutation.isPending || disableMutation.isPending}
|
||||
>
|
||||
<Power className={cn("h-4 w-4", plugin.status === "ready" ? "text-green-600" : "")} />
|
||||
<Power className={cn("h-4 w-4", plugin.status === "ready" ? "text-success" : "")} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
|
@ -478,14 +478,14 @@ export function PluginManager() {
|
|||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-md border border-red-500/25 bg-red-500/[0.06] px-4 py-3">
|
||||
<div className="rounded-md border border-destructive/25 bg-destructive/[0.06] px-4 py-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-red-700 dark:text-red-300" />
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0 text-destructive" />
|
||||
<div className="space-y-1 text-sm">
|
||||
<p className="font-medium text-red-700 dark:text-red-300">
|
||||
<p className="font-medium text-destructive">
|
||||
What errored
|
||||
</p>
|
||||
<p className="text-red-700/90 dark:text-red-200/90 break-words">
|
||||
<p className="text-destructive break-words">
|
||||
{errorDetailsPlugin ? getPluginErrorSummary(errorDetailsPlugin) : "No error summary available."}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -290,7 +290,7 @@ export function PluginSettings() {
|
|||
<>
|
||||
<div className="flex justify-between col-span-2">
|
||||
<span className="text-muted-foreground flex items-center gap-1">
|
||||
<AlertTriangle className="h-3 w-3 text-amber-500" />
|
||||
<AlertTriangle className="h-3 w-3 text-warning" />
|
||||
Crashes
|
||||
</span>
|
||||
<span className="text-xs">
|
||||
|
|
@ -409,7 +409,7 @@ export function PluginSettings() {
|
|||
entry.level === "error"
|
||||
? "text-destructive"
|
||||
: entry.level === "warn"
|
||||
? "text-yellow-600 dark:text-yellow-400"
|
||||
? "text-warning"
|
||||
: entry.level === "debug"
|
||||
? "text-muted-foreground/60"
|
||||
: "text-muted-foreground"
|
||||
|
|
@ -454,7 +454,7 @@ export function PluginSettings() {
|
|||
{check.name}
|
||||
</span>
|
||||
{check.passed ? (
|
||||
<CheckCircle className="h-4 w-4 shrink-0 text-green-500" />
|
||||
<CheckCircle className="h-4 w-4 shrink-0 text-success" />
|
||||
) : (
|
||||
<XCircle className="h-4 w-4 shrink-0 text-destructive" />
|
||||
)}
|
||||
|
|
@ -682,7 +682,7 @@ function PluginConfigForm({ pluginId, schema, initialValues, isLoading, pluginSt
|
|||
<div
|
||||
className={`text-sm p-2 rounded border ${
|
||||
saveMessage.type === "success"
|
||||
? "text-green-700 bg-green-50 border-green-200 dark:text-green-400 dark:bg-green-950/30 dark:border-green-900"
|
||||
? "text-success bg-success/10 border-success/30"
|
||||
: "text-destructive bg-destructive/10 border-destructive/20"
|
||||
}`}
|
||||
>
|
||||
|
|
@ -694,7 +694,7 @@ function PluginConfigForm({ pluginId, schema, initialValues, isLoading, pluginSt
|
|||
<div
|
||||
className={`text-sm p-2 rounded border ${
|
||||
testResult.type === "success"
|
||||
? "text-green-700 bg-green-50 border-green-200 dark:text-green-400 dark:bg-green-950/30 dark:border-green-900"
|
||||
? "text-success bg-success/10 border-success/30"
|
||||
: "text-destructive bg-destructive/10 border-destructive/20"
|
||||
}`}
|
||||
>
|
||||
|
|
@ -800,14 +800,14 @@ function formatTimestamp(epochMs: number): string {
|
|||
function JobStatusDot({ status }: { status: string }) {
|
||||
const colorClass =
|
||||
status === "success" || status === "succeeded"
|
||||
? "bg-green-500"
|
||||
? "bg-success"
|
||||
: status === "failed"
|
||||
? "bg-red-500"
|
||||
? "bg-destructive"
|
||||
: status === "running"
|
||||
? "bg-blue-500 animate-pulse"
|
||||
? "bg-primary animate-pulse"
|
||||
: status === "cancelled"
|
||||
? "bg-gray-400"
|
||||
: "bg-amber-500"; // queued, pending
|
||||
? "bg-muted"
|
||||
: "bg-warning"; // queued, pending
|
||||
return (
|
||||
<span
|
||||
className={`inline-block h-2 w-2 rounded-full shrink-0 ${colorClass}`}
|
||||
|
|
@ -822,12 +822,12 @@ function JobStatusDot({ status }: { status: string }) {
|
|||
function DeliveryStatusDot({ status }: { status: string }) {
|
||||
const colorClass =
|
||||
status === "processed" || status === "success"
|
||||
? "bg-green-500"
|
||||
? "bg-success"
|
||||
: status === "failed"
|
||||
? "bg-red-500"
|
||||
? "bg-destructive"
|
||||
: status === "received"
|
||||
? "bg-blue-500"
|
||||
: "bg-amber-500"; // pending
|
||||
? "bg-primary"
|
||||
: "bg-warning"; // pending
|
||||
return (
|
||||
<span
|
||||
className={`inline-block h-2 w-2 rounded-full shrink-0 ${colorClass}`}
|
||||
|
|
|
|||
|
|
@ -287,8 +287,8 @@ function ProjectWorkspacesContent({
|
|||
|
||||
<div className="flex shrink-0 items-center gap-2 text-xs text-muted-foreground">
|
||||
{summary.serviceCount > 0 ? (
|
||||
<span className={`inline-flex items-center gap-1 ${hasRunningServices ? "text-emerald-500" : ""}`}>
|
||||
<span className={`inline-block h-1.5 w-1.5 rounded-full ${hasRunningServices ? "bg-emerald-500" : "bg-muted-foreground/40"}`} />
|
||||
<span className={`inline-flex items-center gap-1 ${hasRunningServices ? "text-success" : ""}`}>
|
||||
<span className={`inline-block h-1.5 w-1.5 rounded-full ${hasRunningServices ? "bg-success" : "bg-muted-foreground/40"}`} />
|
||||
{summary.runningServiceCount}/{summary.serviceCount}
|
||||
</span>
|
||||
) : null}
|
||||
|
|
@ -410,7 +410,7 @@ function ProjectWorkspacesContent({
|
|||
<div className="text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">
|
||||
Cleanup attention needed
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-xl border border-amber-500/20 bg-amber-500/5">
|
||||
<div className="overflow-hidden rounded-xl border border-warning/20 bg-warning/5">
|
||||
{cleanupFailedSummaries.map(renderSummaryRow)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -796,7 +796,7 @@ export function ProjectDetail() {
|
|||
<div className="flex items-start gap-3">
|
||||
<div className="h-7 flex items-center">
|
||||
<ColorPicker
|
||||
currentColor={project.color ?? "#6366f1"}
|
||||
currentColor={project.color ?? "var(--primary)"}
|
||||
onSelect={(color) => updateProject.mutate({ color })}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -808,8 +808,8 @@ export function ProjectDetail() {
|
|||
className="text-xl font-bold"
|
||||
/>
|
||||
{project.pauseReason === "budget" ? (
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-red-500/30 bg-red-500/10 px-3 py-1 text-[11px] font-medium uppercase tracking-[0.18em] text-red-200">
|
||||
<span className="h-2 w-2 rounded-full bg-red-400" />
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-destructive/30 bg-destructive/10 px-3 py-1 text-[11px] font-medium uppercase tracking-[0.18em] text-destructive">
|
||||
<span className="h-2 w-2 rounded-full bg-destructive" />
|
||||
Paused by budget hard stop
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -389,7 +389,7 @@ export function ProjectWorkspaceDetail() {
|
|||
Make primary
|
||||
</Button>
|
||||
) : (
|
||||
<div className="inline-flex items-center gap-2 rounded-xl border border-emerald-500/25 bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700 dark:text-emerald-300 sm:max-w-sm">
|
||||
<div className="inline-flex items-center gap-2 rounded-xl border border-success/25 bg-success/10 px-3 py-2 text-sm text-success sm:max-w-sm">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
This is the project’s primary codebase workspace.
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -663,7 +663,7 @@ export function RoutineDetail() {
|
|||
const automationLabelClassName = routine.status === "archived"
|
||||
? "text-muted-foreground"
|
||||
: automationEnabled
|
||||
? "text-emerald-400"
|
||||
? "text-success"
|
||||
: "text-muted-foreground";
|
||||
|
||||
return (
|
||||
|
|
@ -719,7 +719,7 @@ export function RoutineDetail() {
|
|||
aria-label={automationEnabled ? "Pause automatic triggers" : "Enable automatic triggers"}
|
||||
disabled={automationToggleDisabled}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
automationEnabled ? "bg-emerald-500" : "bg-muted"
|
||||
automationEnabled ? "bg-success" : "bg-muted"
|
||||
} ${automationToggleDisabled ? "cursor-not-allowed opacity-50" : ""}`}
|
||||
onClick={() => updateRoutineStatus.mutate(automationEnabled ? "paused" : "active")}
|
||||
>
|
||||
|
|
@ -737,7 +737,7 @@ export function RoutineDetail() {
|
|||
|
||||
{/* Secret message banner */}
|
||||
{secretMessage && (
|
||||
<div className="rounded-lg border border-blue-500/30 bg-blue-500/5 p-4 space-y-3 text-sm">
|
||||
<div className="rounded-lg border border-primary/30 bg-primary/5 p-4 space-y-3 text-sm">
|
||||
<div>
|
||||
<p className="font-medium">{secretMessage.title}</p>
|
||||
<p className="text-xs text-muted-foreground">{`Save this now. ${VOCAB.appName} will not show the secret value again.`}</p>
|
||||
|
|
@ -825,7 +825,7 @@ export function RoutineDetail() {
|
|||
<>
|
||||
<span
|
||||
className="h-3.5 w-3.5 shrink-0 rounded-sm"
|
||||
style={{ backgroundColor: currentProject.color ?? "#64748b" }}
|
||||
style={{ backgroundColor: currentProject.color ?? "var(--muted-foreground)" }}
|
||||
/>
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
|
|
@ -840,7 +840,7 @@ export function RoutineDetail() {
|
|||
<>
|
||||
<span
|
||||
className="h-3.5 w-3.5 shrink-0 rounded-sm"
|
||||
style={{ backgroundColor: project?.color ?? "#64748b" }}
|
||||
style={{ backgroundColor: project?.color ?? "var(--muted-foreground)" }}
|
||||
/>
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
|
|
@ -920,7 +920,7 @@ export function RoutineDetail() {
|
|||
{/* Save bar */}
|
||||
<div className="flex items-center justify-between">
|
||||
{isEditDirty ? (
|
||||
<span className="text-xs text-amber-600">Unsaved changes</span>
|
||||
<span className="text-xs text-warning">Unsaved changes</span>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
|
|
@ -945,7 +945,7 @@ export function RoutineDetail() {
|
|||
<TabsTrigger value="runs" className="gap-1.5">
|
||||
<Play className="h-3.5 w-3.5" />
|
||||
Runs
|
||||
{hasLiveRun && <span className="h-2 w-2 rounded-full bg-blue-500 animate-pulse" />}
|
||||
{hasLiveRun && <span className="h-2 w-2 rounded-full bg-primary animate-pulse" />}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="activity" className="gap-1.5">
|
||||
<ActivityIcon className="h-3.5 w-3.5" />
|
||||
|
|
|
|||
|
|
@ -282,7 +282,7 @@ export function Routines() {
|
|||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-semibold tracking-tight flex items-center gap-2">
|
||||
Routines
|
||||
<span className="rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-800 dark:bg-amber-900/30 dark:text-amber-400">Beta</span>
|
||||
<span className="rounded-full bg-warning/10 px-2 py-0.5 text-xs font-medium text-warning">Beta</span>
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Recurring work definitions that materialize into auditable execution issues.
|
||||
|
|
@ -425,7 +425,7 @@ export function Routines() {
|
|||
<>
|
||||
<span
|
||||
className="h-3.5 w-3.5 shrink-0 rounded-sm"
|
||||
style={{ backgroundColor: currentProject.color ?? "#64748b" }}
|
||||
style={{ backgroundColor: currentProject.color ?? "var(--muted-foreground)" }}
|
||||
/>
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
|
|
@ -440,7 +440,7 @@ export function Routines() {
|
|||
<>
|
||||
<span
|
||||
className="h-3.5 w-3.5 shrink-0 rounded-sm"
|
||||
style={{ backgroundColor: project?.color ?? "#64748b" }}
|
||||
style={{ backgroundColor: project?.color ?? "var(--muted-foreground)" }}
|
||||
/>
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
|
|
@ -610,7 +610,7 @@ export function Routines() {
|
|||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<span
|
||||
className="shrink-0 h-3 w-3 rounded-sm"
|
||||
style={{ backgroundColor: projectById.get(routine.projectId)?.color ?? "#6366f1" }}
|
||||
style={{ backgroundColor: projectById.get(routine.projectId)?.color ?? "var(--primary)" }}
|
||||
/>
|
||||
<span className="truncate">{projectById.get(routine.projectId)?.name ?? "Unknown"}</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -97,9 +97,9 @@ function LiveWidgetPreview({
|
|||
density: TranscriptDensity;
|
||||
}) {
|
||||
return (
|
||||
<div className="overflow-hidden rounded-xl border border-cyan-500/25 bg-background/85 shadow-[0_20px_50px_rgba(6,182,212,0.10)]">
|
||||
<div className="border-b border-border/60 bg-cyan-500/[0.05] px-5 py-4">
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.2em] text-cyan-700 dark:text-cyan-300">
|
||||
<div className="overflow-hidden rounded-xl border border-primary/25 bg-background/85 shadow-[0_20px_50px_rgba(6,182,212,0.10)]">
|
||||
<div className="border-b border-border/60 bg-primary/[0.05] px-5 py-4">
|
||||
<div className="text-xs font-semibold uppercase tracking-[0.2em] text-primary">
|
||||
Live Runs
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
|
|
@ -151,7 +151,7 @@ function DashboardPreview({
|
|||
<div className={cn(
|
||||
"flex h-[320px] flex-col overflow-hidden rounded-xl border shadow-[0_20px_40px_rgba(15,23,42,0.10)]",
|
||||
streaming
|
||||
? "border-cyan-500/25 bg-cyan-500/[0.04]"
|
||||
? "border-primary/25 bg-primary/[0.04]"
|
||||
: "border-border bg-background/75",
|
||||
)}>
|
||||
<div className="border-b border-border/60 px-4 py-4">
|
||||
|
|
@ -160,7 +160,7 @@ function DashboardPreview({
|
|||
<div className="flex items-center gap-2">
|
||||
<span className={cn(
|
||||
"inline-flex h-2.5 w-2.5 rounded-full",
|
||||
streaming ? "bg-cyan-500 shadow-[0_0_0_6px_rgba(34,211,238,0.12)]" : "bg-muted-foreground/35",
|
||||
streaming ? "bg-primary shadow-[0_0_0_6px_rgba(34,211,238,0.12)]" : "bg-muted-foreground/35",
|
||||
)} />
|
||||
<Identity name={runTranscriptFixtureMeta.agentName} size="sm" />
|
||||
</div>
|
||||
|
|
@ -172,7 +172,7 @@ function DashboardPreview({
|
|||
<ExternalLink className="h-2.5 w-2.5" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 rounded-lg border border-border/60 bg-background/60 px-3 py-2 text-xs text-cyan-700 dark:text-cyan-300">
|
||||
<div className="mt-3 rounded-lg border border-border/60 bg-background/60 px-3 py-2 text-xs text-primary">
|
||||
{runTranscriptFixtureMeta.issueIdentifier} - {runTranscriptFixtureMeta.issueTitle}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -204,7 +204,7 @@ export function RunTranscriptUxLab() {
|
|||
<div className="grid gap-6 lg:grid-cols-[260px_minmax(0,1fr)]">
|
||||
<aside className="border-b border-border/60 bg-background/75 p-5 lg:border-b-0 lg:border-r">
|
||||
<div className="mb-5">
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-cyan-500/25 bg-cyan-500/[0.08] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.24em] text-cyan-700 dark:text-cyan-300">
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-primary/25 bg-primary/[0.08] px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.24em] text-primary">
|
||||
<FlaskConical className="h-3.5 w-3.5" />
|
||||
UX Lab
|
||||
</div>
|
||||
|
|
@ -225,12 +225,12 @@ export function RunTranscriptUxLab() {
|
|||
className={cn(
|
||||
"w-full rounded-xl border px-4 py-3 text-left transition-all",
|
||||
selectedSurface === option.id
|
||||
? "border-cyan-500/35 bg-cyan-500/[0.10] shadow-[0_12px_24px_rgba(6,182,212,0.12)]"
|
||||
: "border-border/70 bg-background/70 hover:border-cyan-500/20 hover:bg-cyan-500/[0.04]",
|
||||
? "border-primary/35 bg-primary/[0.10] shadow-[0_12px_24px_rgba(6,182,212,0.12)]"
|
||||
: "border-border/70 bg-background/70 hover:border-primary/20 hover:bg-primary/[0.04]",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="rounded-lg border border-current/15 p-2 text-cyan-700 dark:text-cyan-300">
|
||||
<span className="rounded-lg border border-current/15 p-2 text-primary">
|
||||
<Icon className="h-4 w-4" />
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
|
|
|
|||
|
|
@ -58,8 +58,8 @@ function VersionDiff({ oldContent, newContent }: { oldContent: string; newConten
|
|||
<span
|
||||
key={i}
|
||||
className={cn(
|
||||
part.added && "bg-green-500/20 text-green-700 dark:text-green-400",
|
||||
part.removed && "bg-red-500/20 text-red-700 dark:text-red-400",
|
||||
part.added && "bg-success/20 text-success",
|
||||
part.removed && "bg-destructive/20 text-destructive",
|
||||
!part.added && !part.removed && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
|
|
@ -343,7 +343,7 @@ export function SkillDetail() {
|
|||
<span className="flex items-center gap-1 text-sm font-semibold">
|
||||
{skill.averageRating != null ? (
|
||||
<>
|
||||
<Star className="h-3.5 w-3.5 fill-amber-400 text-amber-400" />
|
||||
<Star className="h-3.5 w-3.5 fill-amber-400 text-warning" />
|
||||
{skill.averageRating.toFixed(1)}
|
||||
</>
|
||||
) : (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue