fix(dashboard): UI polish — modals, layout, consistency, test fixes

Dashboard UX polish pass — consolidates create forms into modals
triggered from the page header, fixes layout inconsistencies, adds
scroll-to navigation for the Keys page, and aligns the TokenBar with
the design system.

Changes:
- App.tsx: add padding to sidebar header
- resolve-page-title.ts: add missing routes, better fallback title
- en.ts: fix nav labels (Profiles was 'profiles : multi agents')
- ModelsPage: two-col layout, auxiliary tasks modal, TokenBar redesign
- ProfilesPage: create button in header, form in modal, Checkbox component
- CronPage: create button in header, form in modal
- EnvPage: scroll-to sub-nav in header, fix text overflow

Modal and dialog standardization:
- Replace all native confirm()/window.confirm() with ConfirmDialog
  (OAuthProvidersCard, PluginsPage, ModelsPage, ConfigPage)
- Add useModalBehavior hook (Escape-to-close, scroll lock, focus restore)
- Apply hook to ProfilesPage, CronPage, AuxiliaryTasksModal

Component fixes (from PR review):
- Checkbox: fix controlled/uncontrolled mismatch, add focus-visible ring
- TokenBar: add rounded-full to legend dots, remove dead code

CI/test fixes:
- Fix TS unused imports (noUnusedLocals), type-narrow PickerTarget union
- Add windows-footgun suppression on platform-guarded os.killpg
- Fix 19 stale unit tests + 9 e2e tests broken by recent main changes
- Restore minimal example-dashboard plugin for plugin auth test
This commit is contained in:
Austin Pickett
2026-05-12 13:59:22 -04:00
parent dd0923bb89
commit fc3fd6bb6b
27 changed files with 788 additions and 295 deletions
+240 -130
View File
@@ -9,6 +9,7 @@ import {
Settings2,
Star,
Wrench,
X,
Zap,
} from "lucide-react";
import { api } from "@/lib/api";
@@ -25,6 +26,8 @@ import { Spinner } from "@nous-research/ui/ui/components/spinner";
import { Stats } from "@nous-research/ui/ui/components/stats";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@nous-research/ui/ui/components/badge";
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
import { useModalBehavior } from "@/hooks/useModalBehavior";
import { usePageHeader } from "@/contexts/usePageHeader";
import { useI18n } from "@/i18n";
import { PluginSlot } from "@/plugins";
@@ -91,27 +94,39 @@ function TokenBar({
if (total === 0) return null;
const segments = [
{ value: cacheRead, color: "bg-blue-400/60", label: "Cache Read" },
{ value: reasoning, color: "bg-purple-400/60", label: "Reasoning" },
{ value: input, color: "bg-[#ffe6cb]/70", label: "Input" },
{ value: output, color: "bg-emerald-500/70", label: "Output" },
{ value: cacheRead, color: "bg-blue-400/60", dotColor: "bg-blue-400", label: "Cache Read" },
{ value: reasoning, color: "bg-purple-400/60", dotColor: "bg-purple-400", label: "Reasoning" },
{ value: input, color: "bg-[#ffe6cb]/70", dotColor: "bg-[#ffe6cb]", label: "Input" },
{ value: output, color: "bg-emerald-500/70", dotColor: "bg-emerald-500", label: "Output" },
].filter((s) => s.value > 0);
return (
<div className="space-y-1">
<div className="flex h-2 w-full overflow-hidden rounded-sm bg-muted/30">
<div className="space-y-1.5">
{/* Stacked bar — segments fill proportionally to their share of total */}
<div className="relative flex min-h-[1.5rem] w-full items-stretch overflow-hidden">
{segments.map((s, i) => (
<div
key={i}
className={`${s.color} transition-all duration-300`}
className={`${s.color} relative flex items-center transition-all duration-300`}
style={{ width: `${(s.value / total) * 100}%` }}
/>
>
{/* Stepped fill pattern overlay */}
<div
className="absolute inset-0 opacity-30"
style={{
backgroundImage:
"repeating-linear-gradient(to right, transparent 0 0.4rem, currentColor 0.4rem calc(0.4rem + 1px))",
}}
/>
</div>
))}
</div>
{/* Legend */}
<div className="flex flex-wrap gap-x-3 gap-y-0.5 text-[10px] text-muted-foreground">
{segments.map((s, i) => (
<span key={i} className="flex items-center gap-1">
<span className={`inline-block h-1.5 w-1.5 rounded-full ${s.color}`} />
<span className={`inline-block h-1.5 w-1.5 rounded-full ${s.dotColor}`} />
{s.label} {formatTokens(s.value)}
</span>
))}
@@ -378,7 +393,7 @@ function ModelCard({
</div>
</div>
</CardHeader>
<CardContent className="space-y-3 pt-0">
<CardContent className="space-y-3 pt-3">
<TokenBar
input={entry.input_tokens}
output={entry.output_tokens}
@@ -445,6 +460,157 @@ type PickerTarget =
| { kind: "main" }
| { kind: "aux"; task: string };
function AuxiliaryTasksModal({
aux,
refreshKey,
onSaved,
onClose,
}: {
aux: AuxiliaryModelsResponse | null;
refreshKey: number;
onSaved(): void;
onClose(): void;
}) {
const [picker, setPicker] = useState<PickerTarget | null>(null);
const [resetBusy, setResetBusy] = useState(false);
const [confirmReset, setConfirmReset] = useState(false);
const modalRef = useModalBehavior({ open: true, onClose });
const resetAllAux = async () => {
setConfirmReset(false);
setResetBusy(true);
try {
await api.setModelAssignment({
scope: "auxiliary",
task: "__reset__",
provider: "",
model: "",
});
onSaved();
} finally {
setResetBusy(false);
}
};
return (
<div
ref={modalRef}
className="fixed inset-0 z-[100] flex items-center justify-center bg-background/85 backdrop-blur-sm p-4"
onClick={(e) => e.target === e.currentTarget && onClose()}
role="dialog"
aria-modal="true"
aria-labelledby="aux-modal-title"
>
<div className="relative w-full max-w-2xl max-h-[80vh] border border-border bg-card shadow-2xl flex flex-col">
<Button
ghost
size="icon"
onClick={onClose}
className="absolute right-2 top-2 text-muted-foreground hover:text-foreground"
aria-label="Close"
>
<X />
</Button>
<header className="p-5 pb-3 border-b border-border">
<div className="flex items-center justify-between gap-3 pr-8">
<h2
id="aux-modal-title"
className="font-display text-base tracking-wider uppercase"
>
Auxiliary Tasks
</h2>
<Button
size="sm"
outlined
onClick={() => setConfirmReset(true)}
disabled={resetBusy}
className="text-[10px] h-6"
prefix={resetBusy ? <Spinner /> : null}
>
Reset all to auto
</Button>
</div>
<p className="text-[10px] text-muted-foreground/80 mt-2">
Auxiliary tasks handle side-jobs like vision, session search, and
compression. <span className="font-mono">auto</span> means
&quot;use the main model&quot;. Override per-task when you want a
cheap/fast model for a specific job.
</p>
</header>
<div className="flex-1 overflow-y-auto p-5 space-y-1">
{AUX_TASKS.map((t) => {
const cur = aux?.tasks.find((a) => a.task === t.key);
const isAuto =
!cur || cur.provider === "auto" || !cur.provider;
return (
<div
key={t.key}
className="flex items-center justify-between gap-3 px-3 py-2 border border-border/30 bg-card/50 hover:bg-muted/20 transition-colors"
>
<div className="min-w-0 flex-1">
<div className="flex items-baseline gap-2">
<span className="text-xs font-medium">{t.label}</span>
<span className="text-[10px] text-muted-foreground/60">
{t.hint}
</span>
</div>
<div className="text-[10px] font-mono text-muted-foreground truncate">
{isAuto
? "auto (use main model)"
: `${cur?.provider} · ${cur?.model || "(provider default)"}`}
</div>
</div>
<Button
size="sm"
outlined
onClick={() => setPicker({ kind: "aux", task: t.key })}
className="text-[10px] h-6"
>
Change
</Button>
</div>
);
})}
</div>
{picker && picker.kind === "aux" && (
<ModelPickerDialog
key={`picker-${refreshKey}`}
loader={api.getModelOptions}
alwaysGlobal
title={`Set Auxiliary: ${
AUX_TASKS.find((t) => t.key === picker.task)?.label ??
picker.task
}`}
onApply={async ({ provider, model }) => {
await api.setModelAssignment({
scope: "auxiliary",
task: picker.task,
provider,
model,
});
onSaved();
}}
onClose={() => setPicker(null)}
/>
)}
<ConfirmDialog
open={confirmReset}
onCancel={() => setConfirmReset(false)}
onConfirm={() => void resetAllAux()}
title="Reset auxiliary models"
description="Reset every auxiliary task to 'auto'? This overrides any per-task overrides you've set."
destructive
confirmLabel="Reset all"
loading={resetBusy}
/>
</div>
</div>
);
}
function ModelSettingsPanel({
aux,
refreshKey,
@@ -454,9 +620,8 @@ function ModelSettingsPanel({
refreshKey: number;
onSaved(): void;
}) {
const [expanded, setExpanded] = useState(false);
const [auxModalOpen, setAuxModalOpen] = useState(false);
const [picker, setPicker] = useState<PickerTarget | null>(null);
const [resetBusy, setResetBusy] = useState(false);
const mainProv = aux?.main.provider ?? "";
const mainModel = aux?.main.model ?? "";
@@ -476,23 +641,10 @@ function ModelSettingsPanel({
onSaved();
};
const resetAllAux = async () => {
if (!window.confirm("Reset every auxiliary task to 'auto'? This overrides any per-task overrides you've set.")) {
return;
}
setResetBusy(true);
try {
await api.setModelAssignment({
scope: "auxiliary",
task: "__reset__",
provider: "",
model: "",
});
onSaved();
} finally {
setResetBusy(false);
}
};
// Count how many aux tasks have overrides
const auxOverrideCount = aux?.tasks.filter(
(a) => a.provider && a.provider !== "auto",
).length ?? 0;
return (
<Card>
@@ -505,21 +657,10 @@ function ModelSettingsPanel({
applies to new sessions
</span>
</div>
<Button
size="sm"
outlined
onClick={() => setExpanded((v) => !v)}
className="text-xs"
>
{expanded ? "Hide auxiliary" : "Show auxiliary"}
<ChevronDown
className={`h-3 w-3 transition-transform ${expanded ? "rotate-180" : ""}`}
/>
</Button>
</div>
</CardHeader>
<CardContent className="space-y-3 pt-0">
<CardContent className="space-y-3 pt-3">
{/* Main row */}
<div className="flex items-center justify-between gap-3 bg-muted/20 border border-border/50 px-3 py-2">
<div className="min-w-0 flex-1">
@@ -544,85 +685,41 @@ function ModelSettingsPanel({
</Button>
</div>
{/* Auxiliary rows */}
{expanded && (
<div className="space-y-1 border-t border-border/50 pt-3">
<div className="flex items-center justify-between pb-1">
<div className="text-[10px] uppercase tracking-wider text-muted-foreground">
{/* Auxiliary tasks summary + open modal */}
<div className="flex items-center justify-between gap-3 bg-muted/20 border border-border/50 px-3 py-2">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 mb-0.5">
<Cpu className="h-3 w-3 text-muted-foreground" />
<span className="text-xs font-medium uppercase tracking-wider">
Auxiliary tasks
</div>
<Button
size="sm"
outlined
onClick={resetAllAux}
disabled={resetBusy}
className="text-[10px] h-6"
prefix={resetBusy ? <Spinner /> : null}
>
Reset all to auto
</Button>
</span>
</div>
<div className="text-xs font-mono text-muted-foreground truncate">
{auxOverrideCount > 0
? `${auxOverrideCount} override${auxOverrideCount > 1 ? "s" : ""} · ${AUX_TASKS.length - auxOverrideCount} auto`
: `${AUX_TASKS.length} tasks · all auto`}
</div>
<p className="text-[10px] text-muted-foreground/80 pb-2">
Auxiliary tasks handle side-jobs like vision, session search, and
compression. <span className="font-mono">auto</span> means
&quot;use the main model&quot;. Override per-task when you want a
cheap/fast model for a specific job.
</p>
{AUX_TASKS.map((t) => {
const cur = aux?.tasks.find((a) => a.task === t.key);
const isAuto =
!cur || cur.provider === "auto" || !cur.provider;
return (
<div
key={t.key}
className="flex items-center justify-between gap-3 px-3 py-1.5 border border-border/30 bg-card/50 hover:bg-muted/20 transition-colors"
>
<div className="min-w-0 flex-1">
<div className="flex items-baseline gap-2">
<span className="text-xs font-medium">{t.label}</span>
<span className="text-[10px] text-muted-foreground/60">
{t.hint}
</span>
</div>
<div className="text-[10px] font-mono text-muted-foreground truncate">
{isAuto
? "auto (use main model)"
: `${cur?.provider} · ${cur?.model || "(provider default)"}`}
</div>
</div>
<Button
size="sm"
outlined
onClick={() => setPicker({ kind: "aux", task: t.key })}
className="text-[10px] h-6"
>
Change
</Button>
</div>
);
})}
</div>
)}
<Button
size="sm"
outlined
onClick={() => setAuxModalOpen(true)}
className="text-xs"
>
Configure
</Button>
</div>
{picker && (
<ModelPickerDialog
key={`picker-${refreshKey}`}
loader={api.getModelOptions}
alwaysGlobal
title={
picker.kind === "main"
? "Set Main Model"
: `Set Auxiliary: ${
AUX_TASKS.find((t) => t.key === picker.task)?.label ??
picker.task
}`
}
title="Set Main Model"
onApply={async ({ provider, model }) => {
await applyAssignment({
scope: picker.kind === "main" ? "main" : "auxiliary",
task: picker.kind === "main" ? "" : picker.task,
scope: "main",
task: "",
provider,
model,
});
@@ -630,6 +727,15 @@ function ModelSettingsPanel({
onClose={() => setPicker(null)}
/>
)}
{auxModalOpen && (
<AuxiliaryTasksModal
aux={aux}
refreshKey={refreshKey}
onSaved={onSaved}
onClose={() => setAuxModalOpen(false)}
/>
)}
</CardContent>
</Card>
);
@@ -725,28 +831,14 @@ export default function ModelsPage() {
<div className="flex flex-col gap-6">
<PluginSlot name="models:top" />
<ModelSettingsPanel
aux={aux}
refreshKey={saveKey}
onSaved={onAssigned}
/>
<div className="grid gap-6 lg:grid-cols-2">
<ModelSettingsPanel
aux={aux}
refreshKey={saveKey}
onSaved={onAssigned}
/>
{loading && !data && (
<div className="flex items-center justify-center py-24">
<Spinner className="text-2xl text-primary" />
</div>
)}
{error && (
<Card>
<CardContent className="py-6">
<p className="text-sm text-destructive text-center">{error}</p>
</CardContent>
</Card>
)}
{data && (
<>
{data && (
<Card>
<CardContent className="py-6">
<Stats
@@ -781,7 +873,25 @@ export default function ModelsPage() {
/>
</CardContent>
</Card>
)}
</div>
{loading && !data && (
<div className="flex items-center justify-center py-24">
<Spinner className="text-2xl text-primary" />
</div>
)}
{error && (
<Card>
<CardContent className="py-6">
<p className="text-sm text-destructive text-center">{error}</p>
</CardContent>
</Card>
)}
{data && (
<>
{data.models.length > 0 ? (
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
{data.models.map((m, i) => (