feat(theme): dark/light mode with a Radix Colors-based palette
Adds a working Dark/Light toggle (next to the language switcher, same row
as EN/VI) and re-themes the whole dashboard, not just the handful of
components that already used semantic tokens.
- Tailwind darkMode:"class" + CSS-variable color tokens (client/src/index.css,
tailwind.config.js): surface.0-5, border/border-light, accent/accent-hover,
fg.primary/secondary/muted, status.success/danger/warning. One class flip
on <html> re-themes everything — no per-element dark: variant pairs.
- useTheme() hook: localStorage-persisted, defaults to dark, no
prefers-color-scheme fallback (client/src/hooks/useTheme.ts).
- Mechanical, table-driven migration (scripts/migrate-color-tokens.mjs,
scripts/tokenize-status-colors.mjs, scripts/darken-status-colors.mjs) of
every raw neutral/gray/slate + emerald/red/amber Tailwind utility across
client/src onto the new tokens, so every badge/button/component pulls the
same shade per status/role instead of each picking its own.
- Palette values are the literal Radix Colors (radix-ui.com/colors) scale
constants — slate/blue/green/red/amber steps 1-12 — adopted after three
rounds of hand-picked values that kept overshooting (flat, then too dark,
then glaring); see docs/superpowers/specs/2026-07-31-color-redesign-
dark-light-mode-design.md for the full history and role mapping.
- PipelineMap: done/current/failed/passed-no-evidence/detected share one
visual language (border + text + translucent wash of the same status
color); `current` alone stays a solid accent fill, the one state that
gets to look bolder ("you are here").
- LaneCard: removed the stage/kind/auto-stage chips that duplicated the
Workspace lane-detail header already showing them.
Categorical/decorative hues (violet, indigo, cyan, teal, sky, rose, pink,
orange, yellow, and blue where it plays a role-coloring part e.g. message
bubbles) are deliberately out of scope — collapsing those onto shared
tokens would erase the distinction between different kinds of thing, not
a status.
This commit is contained in:
@@ -331,20 +331,20 @@ export function ActivityFeed() {
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-lg font-semibold text-gray-100">{t("title")}</h1>
|
||||
<h1 className="text-lg font-semibold text-fg-primary">{t("title")}</h1>
|
||||
{wsConnected ? (
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse-dot" />
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse-dot" />
|
||||
{t("common:live")}
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-gray-400 bg-gray-500/10 border border-gray-500/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-gray-400" />
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-fg-secondary bg-surface-4/10 border border-border-light/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-surface-4" />
|
||||
{t("common:offline")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
<p className="text-xs text-fg-muted">
|
||||
{t("subtitle")}
|
||||
{paused && (
|
||||
<span className="ml-2 text-yellow-400">{t("paused", { count: bufferCount })}</span>
|
||||
@@ -430,14 +430,14 @@ export function ActivityFeed() {
|
||||
className="flex items-center px-5 py-3.5 gap-4 hover:bg-surface-4 transition-colors cursor-pointer select-none"
|
||||
>
|
||||
<ChevronRight
|
||||
className={`w-3.5 h-3.5 text-gray-500 transition-transform flex-shrink-0 -mr-1.5 ${isOpen ? "rotate-90" : ""}`}
|
||||
className={`w-3.5 h-3.5 text-fg-muted transition-transform flex-shrink-0 -mr-1.5 ${isOpen ? "rotate-90" : ""}`}
|
||||
/>
|
||||
|
||||
<div className="w-16 flex-shrink-0 text-right font-mono leading-tight">
|
||||
<div className="text-[11px] text-gray-500">
|
||||
<div className="text-[11px] text-fg-muted">
|
||||
{formatTime(event.created_at)}
|
||||
</div>
|
||||
<div className="text-[9px] text-gray-600">
|
||||
<div className="text-[9px] text-fg-muted">
|
||||
{formatDateShort(event.created_at)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -457,10 +457,10 @@ export function ActivityFeed() {
|
||||
);
|
||||
return (
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-gray-300 truncate">
|
||||
<p className="text-sm text-fg-secondary truncate">
|
||||
{origin && (
|
||||
<span
|
||||
className="text-gray-500 mr-1"
|
||||
className="text-fg-muted mr-1"
|
||||
title={`${event.session_id} · ${event.agent_id ?? ""}`}
|
||||
>
|
||||
{origin} ·
|
||||
@@ -473,12 +473,12 @@ export function ActivityFeed() {
|
||||
})()}
|
||||
|
||||
{event.tool_name && (
|
||||
<span className="text-[11px] px-2 py-0.5 bg-surface-2 rounded text-gray-500 font-mono flex-shrink-0">
|
||||
<span className="text-[11px] px-2 py-0.5 bg-surface-2 rounded text-fg-muted font-mono flex-shrink-0">
|
||||
{event.tool_name}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<span className="text-[11px] text-gray-600 flex-shrink-0 w-16 text-right">
|
||||
<span className="text-[11px] text-fg-muted flex-shrink-0 w-16 text-right">
|
||||
{timeAgo(event.created_at)}
|
||||
</span>
|
||||
|
||||
@@ -486,7 +486,7 @@ export function ActivityFeed() {
|
||||
to={`/sessions/${event.session_id}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
title={t("viewSession")}
|
||||
className="flex items-center gap-1 text-[11px] px-2.5 py-1 rounded-md bg-surface-2 text-gray-400 hover:text-accent hover:bg-accent/10 border border-border hover:border-accent/30 transition-colors flex-shrink-0 font-medium"
|
||||
className="flex items-center gap-1 text-[11px] px-2.5 py-1 rounded-md bg-surface-2 text-fg-secondary hover:text-accent hover:bg-accent/10 border border-border hover:border-accent/30 transition-colors flex-shrink-0 font-medium"
|
||||
>
|
||||
{t("viewSession")}
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
@@ -500,7 +500,7 @@ export function ActivityFeed() {
|
||||
</div>
|
||||
{total > 0 && (
|
||||
<div className="flex items-center justify-between mt-4 px-1">
|
||||
<span className="text-xs text-gray-500">
|
||||
<span className="text-xs text-fg-muted">
|
||||
{t("common:pagination.showing", {
|
||||
from: page * PAGE_SIZE + 1,
|
||||
to: Math.min((page + 1) * PAGE_SIZE, total),
|
||||
@@ -511,7 +511,7 @@ export function ActivityFeed() {
|
||||
<button
|
||||
onClick={() => setPage(0)}
|
||||
disabled={page === 0}
|
||||
className="px-2 py-1.5 text-xs font-medium rounded-md bg-surface-2 text-gray-400 hover:text-gray-200 disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
|
||||
className="px-2 py-1.5 text-xs font-medium rounded-md bg-surface-2 text-fg-secondary hover:text-fg-primary disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
|
||||
aria-label="First page"
|
||||
>
|
||||
«
|
||||
@@ -519,7 +519,7 @@ export function ActivityFeed() {
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
||||
disabled={page === 0}
|
||||
className="px-3 py-1.5 text-xs font-medium rounded-md bg-surface-2 text-gray-400 hover:text-gray-200 disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
|
||||
className="px-3 py-1.5 text-xs font-medium rounded-md bg-surface-2 text-fg-secondary hover:text-fg-primary disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
|
||||
>
|
||||
{t("common:pagination.previous")}
|
||||
</button>
|
||||
@@ -544,7 +544,7 @@ export function ActivityFeed() {
|
||||
p === "..." ? (
|
||||
<span
|
||||
key={`ellipsis-${idx}`}
|
||||
className="px-2 py-1.5 text-xs text-gray-600 select-none"
|
||||
className="px-2 py-1.5 text-xs text-fg-muted select-none"
|
||||
>
|
||||
...
|
||||
</span>
|
||||
@@ -556,7 +556,7 @@ export function ActivityFeed() {
|
||||
className={`min-w-[32px] px-2.5 py-1.5 text-xs font-medium rounded-md cursor-pointer transition-colors ${
|
||||
p === page
|
||||
? "bg-accent/20 text-accent border border-accent/30"
|
||||
: "bg-surface-2 text-gray-400 hover:text-gray-200"
|
||||
: "bg-surface-2 text-fg-secondary hover:text-fg-primary"
|
||||
}`}
|
||||
>
|
||||
{p + 1}
|
||||
@@ -567,14 +567,14 @@ export function ActivityFeed() {
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
|
||||
disabled={page >= totalPages - 1}
|
||||
className="px-3 py-1.5 text-xs font-medium rounded-md bg-surface-2 text-gray-400 hover:text-gray-200 disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
|
||||
className="px-3 py-1.5 text-xs font-medium rounded-md bg-surface-2 text-fg-secondary hover:text-fg-primary disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
|
||||
>
|
||||
{t("common:pagination.next")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPage(totalPages - 1)}
|
||||
disabled={page >= totalPages - 1}
|
||||
className="px-2 py-1.5 text-xs font-medium rounded-md bg-surface-2 text-gray-400 hover:text-gray-200 disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
|
||||
className="px-2 py-1.5 text-xs font-medium rounded-md bg-surface-2 text-fg-secondary hover:text-fg-primary disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
|
||||
aria-label="Last page"
|
||||
>
|
||||
»
|
||||
|
||||
+107
-97
@@ -85,7 +85,7 @@ function ChartTooltip({ x, y, children }: { x: number; y: number; children: Reac
|
||||
const nearRight = x > window.innerWidth - 200;
|
||||
return (
|
||||
<div
|
||||
className="fixed z-50 px-2 py-1.5 text-xs bg-[#12121f] border border-[#2a2a4a] rounded shadow-xl text-gray-200 pointer-events-none whitespace-nowrap"
|
||||
className="fixed z-50 px-2 py-1.5 text-xs bg-[#12121f] border border-[#2a2a4a] rounded shadow-xl text-fg-secondary pointer-events-none whitespace-nowrap"
|
||||
style={{
|
||||
left: nearRight ? x - 14 : x + 14,
|
||||
top: y - 10,
|
||||
@@ -211,7 +211,7 @@ function Heatmap({ weeks }: { weeks: Array<Array<{ date: string; count: number }
|
||||
{monthPositions.map((mp, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute text-[10px] text-gray-600 font-medium whitespace-nowrap"
|
||||
className="absolute text-[10px] text-fg-muted font-medium whitespace-nowrap"
|
||||
style={{ left: mp.col * 16 }}
|
||||
>
|
||||
{mp.label}
|
||||
@@ -224,7 +224,7 @@ function Heatmap({ weeks }: { weeks: Array<Array<{ date: string; count: number }
|
||||
{dayLabels.map((d, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-[9px] text-gray-700 flex items-center justify-end pr-1.5"
|
||||
className="text-[9px] text-fg-muted flex items-center justify-end pr-1.5"
|
||||
style={{ height: 13 }}
|
||||
>
|
||||
{d}
|
||||
@@ -247,7 +247,7 @@ function Heatmap({ weeks }: { weeks: Array<Array<{ date: string; count: number }
|
||||
show(
|
||||
e,
|
||||
<>
|
||||
<span className="text-gray-400">
|
||||
<span className="text-fg-secondary">
|
||||
{dayNames[dow] ?? ""}, {cell.date}
|
||||
</span>
|
||||
<span className="ml-2 font-medium">
|
||||
@@ -273,7 +273,7 @@ function Heatmap({ weeks }: { weeks: Array<Array<{ date: string; count: number }
|
||||
))}
|
||||
</div>
|
||||
{/* Legend */}
|
||||
<div className="flex items-center gap-2 mt-3 text-[11px] text-gray-600">
|
||||
<div className="flex items-center gap-2 mt-3 text-[11px] text-fg-muted">
|
||||
<span>{t("less")}</span>
|
||||
{[0, 0.25, 0.5, 0.75, 1].map((f) => {
|
||||
const v = Math.round(f * maxCount);
|
||||
@@ -324,7 +324,7 @@ function Sparkline({
|
||||
show(
|
||||
e,
|
||||
<>
|
||||
<span className="text-gray-400">{date}</span>
|
||||
<span className="text-fg-secondary">{date}</span>
|
||||
<span className="ml-2 font-medium">{t("eventCountLabel", { count })}</span>
|
||||
</>
|
||||
)
|
||||
@@ -404,7 +404,7 @@ function CostTrendLine({
|
||||
show(
|
||||
e,
|
||||
<>
|
||||
<span className="text-gray-400">{point.date}</span>
|
||||
<span className="text-fg-secondary">{point.date}</span>
|
||||
<span className="ml-2 font-medium">{fmtCostFull(point.cost)}</span>
|
||||
</>
|
||||
)
|
||||
@@ -437,7 +437,7 @@ function BarRow({
|
||||
const width = pct !== undefined ? pct : max > 0 ? Math.round((count / max) * 100) : 0;
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-gray-400 w-28 truncate flex-shrink-0" title={label}>
|
||||
<span className="text-xs text-fg-secondary w-28 truncate flex-shrink-0" title={label}>
|
||||
{label}
|
||||
</span>
|
||||
<div className="flex-1 bg-surface-3 rounded-full h-2">
|
||||
@@ -447,7 +447,7 @@ function BarRow({
|
||||
/>
|
||||
</div>
|
||||
<Tip raw={count.toLocaleString()}>
|
||||
<span className="text-xs text-gray-500 w-10 text-right flex-shrink-0">{fmt(count)}</span>
|
||||
<span className="text-xs text-fg-muted w-10 text-right flex-shrink-0">{fmt(count)}</span>
|
||||
</Tip>
|
||||
</div>
|
||||
);
|
||||
@@ -457,7 +457,7 @@ function CostBarRow({
|
||||
label,
|
||||
cost,
|
||||
max,
|
||||
color = "bg-emerald-400",
|
||||
color = "bg-status-success",
|
||||
}: {
|
||||
label: string;
|
||||
cost: number;
|
||||
@@ -467,7 +467,7 @@ function CostBarRow({
|
||||
const width = max > 0 ? Math.max(2, Math.round((cost / max) * 100)) : 0;
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-gray-400 w-24 truncate flex-shrink-0" title={label}>
|
||||
<span className="text-xs text-fg-secondary w-24 truncate flex-shrink-0" title={label}>
|
||||
{label}
|
||||
</span>
|
||||
<div className="flex-1 bg-surface-3 rounded-full h-2">
|
||||
@@ -476,7 +476,7 @@ function CostBarRow({
|
||||
style={{ width: `${width}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-emerald-400 font-mono w-16 text-right flex-shrink-0">
|
||||
<span className="text-xs text-status-success font-mono w-16 text-right flex-shrink-0">
|
||||
<Tip raw={fmtCostFull(cost)}>{fmtCost(cost)}</Tip>
|
||||
</span>
|
||||
</div>
|
||||
@@ -495,7 +495,7 @@ function DonutChart({
|
||||
const { t } = useTranslation(["analytics", "common"]);
|
||||
const { show, move, hide, node } = useTooltip();
|
||||
const total = segments.reduce((s, g) => s + g.value, 0);
|
||||
if (total === 0) return <div className="text-xs text-gray-500">{t("common:noData")}</div>;
|
||||
if (total === 0) return <div className="text-xs text-fg-muted">{t("common:noData")}</div>;
|
||||
|
||||
const r = 52;
|
||||
const cx = 64;
|
||||
@@ -543,10 +543,10 @@ function DonutChart({
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<text x={cx} y={cy - 6} textAnchor="middle" className="fill-gray-300" fontSize={11}>
|
||||
<text x={cx} y={cy - 6} textAnchor="middle" className="fill-fg-secondary" fontSize={11}>
|
||||
{(formatTotal ?? fmt)(total)}
|
||||
</text>
|
||||
<text x={cx} y={cy + 10} textAnchor="middle" className="fill-gray-600" fontSize={9}>
|
||||
<text x={cx} y={cy + 10} textAnchor="middle" className="fill-fg-muted" fontSize={9}>
|
||||
{t("common:total_lower")}
|
||||
</text>
|
||||
</svg>
|
||||
@@ -557,8 +557,8 @@ function DonutChart({
|
||||
className="w-2.5 h-2.5 rounded-sm flex-shrink-0"
|
||||
style={{ backgroundColor: color }}
|
||||
/>
|
||||
<span className="text-gray-400">{label}</span>
|
||||
<span className="text-gray-500 ml-auto pl-4">{Math.round((value / total) * 100)}%</span>
|
||||
<span className="text-fg-secondary">{label}</span>
|
||||
<span className="text-fg-muted ml-auto pl-4">{Math.round((value / total) * 100)}%</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -588,7 +588,7 @@ function StatPill({
|
||||
return (
|
||||
<div className="card p-5 flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-gray-500 uppercase tracking-wider">{label}</span>
|
||||
<span className="text-xs text-fg-muted uppercase tracking-wider">{label}</span>
|
||||
<Icon className={`w-4 h-4 ${color}`} />
|
||||
</div>
|
||||
{loading ? (
|
||||
@@ -601,7 +601,7 @@ function StatPill({
|
||||
{loading ? (
|
||||
<TextSkeleton width="w-20" />
|
||||
) : (
|
||||
sub && <p className="text-[11px] text-gray-500">{sub}</p>
|
||||
sub && <p className="text-[11px] text-fg-muted">{sub}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -881,8 +881,8 @@ export function Analytics() {
|
||||
].filter((s) => s.value > 0);
|
||||
|
||||
const EVENT_TYPE_COLORS: Record<string, string> = {
|
||||
PreToolUse: "bg-emerald-400",
|
||||
PostToolUse: "bg-blue-400",
|
||||
PreToolUse: "bg-status-success",
|
||||
PostToolUse: "bg-blue-500",
|
||||
Stop: "bg-violet-400",
|
||||
SubagentStop: "bg-yellow-400",
|
||||
Notification: "bg-orange-400",
|
||||
@@ -899,22 +899,22 @@ export function Analytics() {
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-lg font-semibold text-gray-100">{t("title")}</h1>
|
||||
<h1 className="text-lg font-semibold text-fg-primary">{t("title")}</h1>
|
||||
{wsConnected ? (
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse-dot" />
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse-dot" />
|
||||
{t("common:live")}
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-gray-400 bg-gray-500/10 border border-gray-500/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-gray-400" />
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-fg-secondary bg-surface-4/10 border border-border-light/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-surface-4" />
|
||||
{t("common:offline")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 flex items-center gap-2">
|
||||
<p className="text-xs text-fg-muted flex items-center gap-2">
|
||||
{t("subtitle")}
|
||||
<span className="inline-flex items-center gap-1.5 text-[11px] text-gray-500 bg-surface-2 border border-border px-2 py-0.5 rounded-md font-mono ml-2">
|
||||
<span className="inline-flex items-center gap-1.5 text-[11px] text-fg-muted bg-surface-2 border border-border px-2 py-0.5 rounded-md font-mono ml-2">
|
||||
<Clock className="w-3 h-3" />
|
||||
{lastUpdate.toLocaleTimeString()}
|
||||
</span>
|
||||
@@ -942,7 +942,7 @@ export function Analytics() {
|
||||
raw={data ? data.overview.total_sessions.toLocaleString() : undefined}
|
||||
sub={data ? `${data.overview.active_sessions} ${t("common:active")}` : undefined}
|
||||
icon={FolderOpen}
|
||||
color="text-blue-400"
|
||||
color="text-blue-500"
|
||||
loading={!data}
|
||||
/>
|
||||
<StatPill
|
||||
@@ -951,7 +951,7 @@ export function Analytics() {
|
||||
raw={data ? data.overview.total_agents.toLocaleString() : undefined}
|
||||
sub={data ? `${data.overview.active_agents} ${t("common:active")}` : undefined}
|
||||
icon={Bot}
|
||||
color="text-emerald-400"
|
||||
color="text-status-success"
|
||||
loading={!data}
|
||||
/>
|
||||
<StatPill
|
||||
@@ -973,7 +973,7 @@ export function Analytics() {
|
||||
: undefined
|
||||
}
|
||||
icon={DollarSign}
|
||||
color="text-emerald-400"
|
||||
color="text-status-success"
|
||||
loading={!costData}
|
||||
/>
|
||||
<StatPill
|
||||
@@ -994,7 +994,7 @@ export function Analytics() {
|
||||
{/* Activity heatmap + 30-day sparkline */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="card p-5 lg:col-span-2">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-4">{t("eventActivity")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-4">{t("eventActivity")}</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<div className="w-fit min-w-max mx-auto">
|
||||
<Heatmap weeks={weeks} />
|
||||
@@ -1002,17 +1002,17 @@ export function Analytics() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-1">{t("last30Days")}</h3>
|
||||
<p className="text-[11px] text-gray-600 mb-4">{t("dailyEventCount")}</p>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-1">{t("last30Days")}</h3>
|
||||
<p className="text-[11px] text-fg-muted mb-4">{t("dailyEventCount")}</p>
|
||||
<Sparkline data={last30} />
|
||||
<div className="flex justify-between text-[11px] text-gray-600 mt-2">
|
||||
<div className="flex justify-between text-[11px] text-fg-muted mt-2">
|
||||
<span>{last30[0]?.date?.slice(5)}</span>
|
||||
<span>{last30[last30.length - 1]?.date?.slice(5)}</span>
|
||||
</div>
|
||||
<div className="mt-4 pt-4 border-t border-border space-y-1">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-gray-500">{t("peakDay")}</span>
|
||||
<span className="text-gray-300 font-mono">
|
||||
<span className="text-fg-muted">{t("peakDay")}</span>
|
||||
<span className="text-fg-secondary font-mono">
|
||||
<Tip raw={Math.max(...last30.map((d) => d.count)).toLocaleString()}>
|
||||
{fmt(Math.max(...last30.map((d) => d.count)))}
|
||||
</Tip>{" "}
|
||||
@@ -1020,8 +1020,8 @@ export function Analytics() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-gray-500">{t("total30d")}</span>
|
||||
<span className="text-gray-300 font-mono">
|
||||
<span className="text-fg-muted">{t("total30d")}</span>
|
||||
<span className="text-fg-secondary font-mono">
|
||||
<Tip raw={last30.reduce((s, d) => s + d.count, 0).toLocaleString()}>
|
||||
{fmt(last30.reduce((s, d) => s + d.count, 0))}
|
||||
</Tip>{" "}
|
||||
@@ -1048,8 +1048,8 @@ export function Analytics() {
|
||||
onClick={() => setActiveTab(key)}
|
||||
className={`px-4 py-1.5 text-xs font-medium rounded-md transition-colors ${
|
||||
activeTab === key
|
||||
? "bg-surface-4 text-gray-200"
|
||||
: "text-gray-500 hover:text-gray-300"
|
||||
? "bg-surface-4 text-fg-secondary"
|
||||
: "text-fg-muted hover:text-fg-secondary"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
@@ -1061,7 +1061,7 @@ export function Analytics() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{/* Token bars */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-5">
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">
|
||||
{t("tokenDistribution")}
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
@@ -1069,12 +1069,12 @@ export function Analytics() {
|
||||
{
|
||||
label: t("common:token.input"),
|
||||
value: data?.tokens.total_input ?? 0,
|
||||
color: "bg-blue-400",
|
||||
color: "bg-blue-500",
|
||||
},
|
||||
{
|
||||
label: t("common:token.output"),
|
||||
value: data?.tokens.total_output ?? 0,
|
||||
color: "bg-emerald-400",
|
||||
color: "bg-status-success",
|
||||
},
|
||||
{
|
||||
label: t("common:token.cacheRead"),
|
||||
@@ -1097,13 +1097,13 @@ export function Analytics() {
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 pt-4 border-t border-border space-y-1.5">
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
<div className="flex justify-between text-xs text-fg-muted">
|
||||
<span>{t("common:token.totalTokens")}</span>
|
||||
<Tip raw={totalTokens.toLocaleString()}>
|
||||
<span className="text-gray-300 font-mono">{fmt(totalTokens)}</span>
|
||||
<span className="text-fg-secondary font-mono">{fmt(totalTokens)}</span>
|
||||
</Tip>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
<div className="flex justify-between text-xs text-fg-muted">
|
||||
<span>{t("cacheEfficiency")}</span>
|
||||
<span className="text-violet-400 font-mono">{cacheHitPct}%</span>
|
||||
</div>
|
||||
@@ -1112,18 +1112,20 @@ export function Analytics() {
|
||||
|
||||
{/* Token summary */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("tokenBreakdown")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">
|
||||
{t("tokenBreakdown")}
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{
|
||||
label: t("common:token.input"),
|
||||
value: data?.tokens.total_input ?? 0,
|
||||
color: "text-blue-400",
|
||||
color: "text-blue-500",
|
||||
},
|
||||
{
|
||||
label: t("common:token.output"),
|
||||
value: data?.tokens.total_output ?? 0,
|
||||
color: "text-emerald-400",
|
||||
color: "text-status-success",
|
||||
},
|
||||
{
|
||||
label: t("common:token.cacheRead"),
|
||||
@@ -1135,13 +1137,13 @@ export function Analytics() {
|
||||
value: data?.tokens.total_cache_write ?? 0,
|
||||
color: "text-yellow-400",
|
||||
},
|
||||
{ label: t("common:total"), value: totalTokens, color: "text-gray-100" },
|
||||
{ label: t("common:total"), value: totalTokens, color: "text-fg-primary" },
|
||||
].map(({ label, value, color }) => (
|
||||
<div
|
||||
key={label}
|
||||
className="flex justify-between items-center py-2 border-b border-border last:border-0"
|
||||
>
|
||||
<span className="text-xs text-gray-400">{label}</span>
|
||||
<span className="text-xs text-fg-secondary">{label}</span>
|
||||
<span className={`text-sm font-mono font-medium ${color}`}>
|
||||
{value.toLocaleString()}
|
||||
</span>
|
||||
@@ -1149,23 +1151,23 @@ export function Analytics() {
|
||||
))}
|
||||
</div>
|
||||
{totalTokens === 0 && (
|
||||
<p className="text-[11px] text-gray-600 mt-4">{t("tokenInfo")}</p>
|
||||
<p className="text-[11px] text-fg-muted mt-4">{t("tokenInfo")}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Token mix donut */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("tokenMix")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("tokenMix")}</h3>
|
||||
{tokenMixSegments.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">{t("common:noData")}</p>
|
||||
<p className="text-sm text-fg-muted">{t("common:noData")}</p>
|
||||
) : (
|
||||
<>
|
||||
<DonutChart segments={tokenMixSegments} formatTotal={(total) => fmt(total)} />
|
||||
<div className="mt-4 pt-4 border-t border-border space-y-2">
|
||||
{tokenMixSegments.map((segment) => (
|
||||
<div key={segment.label} className="flex justify-between text-xs">
|
||||
<span className="text-gray-400">{segment.label}</span>
|
||||
<span className="text-gray-300 font-mono">
|
||||
<span className="text-fg-secondary">{segment.label}</span>
|
||||
<span className="text-fg-secondary font-mono">
|
||||
<Tip raw={segment.value.toLocaleString()}>{fmt(segment.value)}</Tip>
|
||||
</span>
|
||||
</div>
|
||||
@@ -1181,29 +1183,31 @@ export function Analytics() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{/* Daily cost trends */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-1">{t("dailyCostTrends")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-1">
|
||||
{t("dailyCostTrends")}
|
||||
</h3>
|
||||
{dailyCostsLocal.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">{t("noDailyCostData")}</p>
|
||||
<p className="text-sm text-fg-muted">{t("noDailyCostData")}</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-[11px] text-gray-600 mb-4">{t("costPerDay")}</p>
|
||||
<p className="text-[11px] text-fg-muted mb-4">{t("costPerDay")}</p>
|
||||
<CostTrendLine data={dailyCostLast30} />
|
||||
<div className="flex justify-between text-[11px] text-gray-600 mt-2">
|
||||
<div className="flex justify-between text-[11px] text-fg-muted mt-2">
|
||||
<span>{dailyCostLast30[0]?.date?.slice(5)}</span>
|
||||
<span>{dailyCostLast30[dailyCostLast30.length - 1]?.date?.slice(5)}</span>
|
||||
</div>
|
||||
<div className="mt-4 pt-4 border-t border-border space-y-1">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-gray-500">{t("peakCostDay")}</span>
|
||||
<span className="text-emerald-400 font-mono">
|
||||
<span className="text-fg-muted">{t("peakCostDay")}</span>
|
||||
<span className="text-status-success font-mono">
|
||||
<Tip raw={`${peakCostDay.date} • ${fmtCostFull(peakCostDay.cost)}`}>
|
||||
{fmtCost(peakCostDay.cost)}
|
||||
</Tip>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-gray-500">{t("totalCost30d")}</span>
|
||||
<span className="text-emerald-400 font-mono">
|
||||
<span className="text-fg-muted">{t("totalCost30d")}</span>
|
||||
<span className="text-status-success font-mono">
|
||||
<Tip raw={fmtCostFull(totalCost30d)}>{fmtCost(totalCost30d)}</Tip>
|
||||
</span>
|
||||
</div>
|
||||
@@ -1214,7 +1218,7 @@ export function Analytics() {
|
||||
|
||||
{/* Cost by model */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("costByModel")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("costByModel")}</h3>
|
||||
{costBreakdown.length > 0 ? (
|
||||
<>
|
||||
<DonutChart
|
||||
@@ -1231,17 +1235,17 @@ export function Analytics() {
|
||||
<div className="mt-4 pt-4 border-t border-border space-y-2">
|
||||
{costBreakdown.map((b) => (
|
||||
<div key={b.model} className="flex justify-between text-xs">
|
||||
<span className="text-gray-400 font-mono truncate">
|
||||
<span className="text-fg-secondary font-mono truncate">
|
||||
{formatModelName(b.model)}
|
||||
</span>
|
||||
<span className="text-emerald-400 font-mono font-medium ml-2">
|
||||
<span className="text-status-success font-mono font-medium ml-2">
|
||||
<Tip raw={fmtCostFull(b.cost)}>{fmtCost(b.cost)}</Tip>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex justify-between text-xs pt-2 border-t border-border">
|
||||
<span className="text-gray-300 font-medium">{t("common:total")}</span>
|
||||
<span className="text-emerald-400 font-mono font-semibold">
|
||||
<span className="text-fg-secondary font-medium">{t("common:total")}</span>
|
||||
<span className="text-status-success font-mono font-semibold">
|
||||
<Tip raw={fmtCostFull(costData?.total_cost ?? 0)}>
|
||||
{fmtCost(costData?.total_cost ?? 0)}
|
||||
</Tip>
|
||||
@@ -1250,18 +1254,20 @@ export function Analytics() {
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm text-gray-500">{t("noCostData")}</p>
|
||||
<p className="text-sm text-fg-muted">{t("noCostData")}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Cost by weekday */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-1">{t("costByWeekday")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-1">
|
||||
{t("costByWeekday")}
|
||||
</h3>
|
||||
{dailyCostsLocal.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">{t("noDailyCostData")}</p>
|
||||
<p className="text-sm text-fg-muted">{t("noDailyCostData")}</p>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-[11px] text-gray-600 mb-4">{t("last30Days")}</p>
|
||||
<p className="text-[11px] text-fg-muted mb-4">{t("last30Days")}</p>
|
||||
<div className="space-y-3">
|
||||
{weekdayCosts.map(({ label, cost }) => (
|
||||
<CostBarRow
|
||||
@@ -1274,7 +1280,7 @@ export function Analytics() {
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 pt-4 border-t border-border text-xs flex justify-between">
|
||||
<span className="text-gray-500">{t("common:total")}</span>
|
||||
<span className="text-fg-muted">{t("common:total")}</span>
|
||||
<span className="text-cyan-400 font-mono">
|
||||
<Tip raw={fmtCostFull(totalCost30d)}>{fmtCost(totalCost30d)}</Tip>
|
||||
</span>
|
||||
@@ -1289,9 +1295,11 @@ export function Analytics() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{/* Agent type distribution */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("subagentTypes")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">
|
||||
{t("subagentTypes")}
|
||||
</h3>
|
||||
{(data?.agent_types ?? []).length === 0 ? (
|
||||
<p className="text-sm text-gray-500">{t("noSubagentData")}</p>
|
||||
<p className="text-sm text-fg-muted">{t("noSubagentData")}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{(data?.agent_types ?? []).slice(0, 10).map(({ subagent_type, count }) => (
|
||||
@@ -1309,13 +1317,13 @@ export function Analytics() {
|
||||
|
||||
{/* Agent status donut */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("agentStatus")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("agentStatus")}</h3>
|
||||
<DonutChart segments={agentStatusSegments} />
|
||||
<div className="mt-4 pt-4 border-t border-border space-y-1.5">
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
<div className="flex justify-between text-xs text-fg-muted">
|
||||
<span>{t("totalAgentsLabel")}</span>
|
||||
<Tip raw={(data?.overview.total_agents ?? 0).toLocaleString()}>
|
||||
<span className="text-gray-300 font-mono">
|
||||
<span className="text-fg-secondary font-mono">
|
||||
{fmt(data?.overview.total_agents ?? 0)}
|
||||
</span>
|
||||
</Tip>
|
||||
@@ -1323,7 +1331,7 @@ export function Analytics() {
|
||||
{agentStatusSegments.map((s) => (
|
||||
<div
|
||||
key={s.label}
|
||||
className="flex items-center justify-between text-xs text-gray-500"
|
||||
className="flex items-center justify-between text-xs text-fg-muted"
|
||||
>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span
|
||||
@@ -1333,7 +1341,7 @@ export function Analytics() {
|
||||
{s.label}
|
||||
</span>
|
||||
<Tip raw={s.value.toLocaleString()}>
|
||||
<span className="text-gray-400 font-mono">{fmt(s.value)}</span>
|
||||
<span className="text-fg-secondary font-mono">{fmt(s.value)}</span>
|
||||
</Tip>
|
||||
</div>
|
||||
))}
|
||||
@@ -1342,9 +1350,9 @@ export function Analytics() {
|
||||
|
||||
{/* Event type breakdown */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("eventTypes")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("eventTypes")}</h3>
|
||||
{(data?.event_types ?? []).length === 0 ? (
|
||||
<p className="text-sm text-gray-500">{t("noEventData")}</p>
|
||||
<p className="text-sm text-fg-muted">{t("noEventData")}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{(data?.event_types ?? []).map(({ event_type, count }) => (
|
||||
@@ -1353,7 +1361,7 @@ export function Analytics() {
|
||||
label={event_type}
|
||||
count={count}
|
||||
max={maxEventTypeCount}
|
||||
color={EVENT_TYPE_COLORS[event_type] ?? "bg-gray-400"}
|
||||
color={EVENT_TYPE_COLORS[event_type] ?? "bg-surface-4"}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -1366,9 +1374,9 @@ export function Analytics() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{/* Top tools */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("toolUsage")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">{t("toolUsage")}</h3>
|
||||
{(data?.tool_usage ?? []).length === 0 ? (
|
||||
<p className="text-sm text-gray-500">{t("noToolData")}</p>
|
||||
<p className="text-sm text-fg-muted">{t("noToolData")}</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{(data?.tool_usage ?? []).slice(0, 12).map(({ tool_name, count }) => (
|
||||
@@ -1386,13 +1394,15 @@ export function Analytics() {
|
||||
|
||||
{/* Session outcomes donut */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-5">{t("sessionOutcomes")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">
|
||||
{t("sessionOutcomes")}
|
||||
</h3>
|
||||
<DonutChart segments={sessionOutcomeSegments} />
|
||||
<div className="mt-4 pt-4 border-t border-border space-y-1.5">
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
<div className="flex justify-between text-xs text-fg-muted">
|
||||
<span>{t("totalSessionsLabel")}</span>
|
||||
<Tip raw={(data?.overview.total_sessions ?? 0).toLocaleString()}>
|
||||
<span className="text-gray-300 font-mono">
|
||||
<span className="text-fg-secondary font-mono">
|
||||
{fmt(data?.overview.total_sessions ?? 0)}
|
||||
</span>
|
||||
</Tip>
|
||||
@@ -1400,7 +1410,7 @@ export function Analytics() {
|
||||
{sessionOutcomeSegments.map((s) => (
|
||||
<div
|
||||
key={s.label}
|
||||
className="flex items-center justify-between text-xs text-gray-500"
|
||||
className="flex items-center justify-between text-xs text-fg-muted"
|
||||
>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span
|
||||
@@ -1410,7 +1420,7 @@ export function Analytics() {
|
||||
{s.label}
|
||||
</span>
|
||||
<Tip raw={s.value.toLocaleString()}>
|
||||
<span className="text-gray-400 font-mono">{fmt(s.value)}</span>
|
||||
<span className="text-fg-secondary font-mono">{fmt(s.value)}</span>
|
||||
</Tip>
|
||||
</div>
|
||||
))}
|
||||
@@ -1419,11 +1429,11 @@ export function Analytics() {
|
||||
|
||||
{/* Daily session trends */}
|
||||
<div className="card p-5">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-5">
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-5">
|
||||
{t("dailySessionTrends")}
|
||||
</h3>
|
||||
{dailySessionsLocal.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">{t("noSessionTrendData")}</p>
|
||||
<p className="text-sm text-fg-muted">{t("noSessionTrendData")}</p>
|
||||
) : (
|
||||
<>
|
||||
<Sparkline data={dailySessionsLocal.slice(-30)} color="#6366f1" />
|
||||
@@ -1440,7 +1450,7 @@ export function Analytics() {
|
||||
);
|
||||
return (
|
||||
<div key={date} className="flex items-center gap-3">
|
||||
<span className="text-[11px] text-gray-500 font-mono w-20 flex-shrink-0">
|
||||
<span className="text-[11px] text-fg-muted font-mono w-20 flex-shrink-0">
|
||||
{date.slice(5)}
|
||||
</span>
|
||||
<div className="flex-1 bg-surface-3 rounded-full h-1.5">
|
||||
@@ -1451,14 +1461,14 @@ export function Analytics() {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[11px] text-gray-500 w-4 text-right">
|
||||
<span className="text-[11px] text-fg-muted w-4 text-right">
|
||||
{count}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="text-[11px] text-gray-600 mt-3">{t("last7Days")}</p>
|
||||
<p className="text-[11px] text-fg-muted mt-3">{t("last7Days")}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+246
-242
File diff suppressed because it is too large
Load Diff
+112
-108
@@ -278,28 +278,28 @@ function SystemHealthTab() {
|
||||
<div className="card p-5 flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Server className="w-4 h-4 text-emerald-400" />
|
||||
<span className="text-xs text-gray-500 uppercase tracking-wider">Runtime</span>
|
||||
<Server className="w-4 h-4 text-status-success" />
|
||||
<span className="text-xs text-fg-muted uppercase tracking-wider">Runtime</span>
|
||||
</div>
|
||||
<span className="text-[10px] font-mono text-gray-500">
|
||||
<span className="text-[10px] font-mono text-fg-muted">
|
||||
{info.server.cpus} cores · {info.server.arch}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2.5">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-gray-400 w-28 flex-shrink-0">Uptime</span>
|
||||
<span className="text-xs text-gray-200 font-mono ml-auto">
|
||||
<span className="text-xs text-fg-secondary w-28 flex-shrink-0">Uptime</span>
|
||||
<span className="text-xs text-fg-secondary font-mono ml-auto">
|
||||
{formatUptime(info.server.uptime)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-gray-400 w-28 flex-shrink-0">CPU (1/5/15m)</span>
|
||||
<span className="text-xs text-fg-secondary w-28 flex-shrink-0">CPU (1/5/15m)</span>
|
||||
<div className="flex gap-1 ml-auto">
|
||||
{(info.server.cpu_load || []).slice(0, 3).map((load, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={`text-[10px] font-mono px-1.5 py-0.5 rounded ${i === 0 && load > info.server.cpus ? "bg-red-500/20 text-red-400" : "bg-surface-3 text-gray-300"}`}
|
||||
className={`text-[10px] font-mono px-1.5 py-0.5 rounded ${i === 0 && load > info.server.cpus ? "bg-status-danger/20 text-status-danger" : "bg-surface-3 text-fg-secondary"}`}
|
||||
>
|
||||
{load.toFixed(2)}
|
||||
</span>
|
||||
@@ -307,8 +307,8 @@ function SystemHealthTab() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-gray-400 w-28 flex-shrink-0">Node RSS</span>
|
||||
<span className="text-xs text-gray-200 font-mono ml-auto">
|
||||
<span className="text-xs text-fg-secondary w-28 flex-shrink-0">Node RSS</span>
|
||||
<span className="text-xs text-fg-secondary font-mono ml-auto">
|
||||
{formatBytes(info.server.memory.rss)}
|
||||
</span>
|
||||
</div>
|
||||
@@ -321,12 +321,12 @@ function SystemHealthTab() {
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-[10px]">
|
||||
<span className="text-gray-500">Host Memory</span>
|
||||
<span className="text-gray-400 font-mono">{memUsedPct.toFixed(0)}%</span>
|
||||
<span className="text-fg-muted">Host Memory</span>
|
||||
<span className="text-fg-secondary font-mono">{memUsedPct.toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-surface-3 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all duration-700 ${memUsedPct > 90 ? "bg-red-500" : memUsedPct > 70 ? "bg-amber-500" : "bg-emerald-500"}`}
|
||||
className={`h-2 rounded-full transition-all duration-700 ${memUsedPct > 90 ? "bg-status-danger" : memUsedPct > 70 ? "bg-status-warning" : "bg-status-success"}`}
|
||||
style={{ width: `${memUsedPct}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -338,12 +338,12 @@ function SystemHealthTab() {
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-[10px]">
|
||||
<span className="text-gray-500">V8 Heap</span>
|
||||
<span className="text-gray-400 font-mono">{heapUsedPct.toFixed(0)}%</span>
|
||||
<span className="text-fg-muted">V8 Heap</span>
|
||||
<span className="text-fg-secondary font-mono">{heapUsedPct.toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-surface-3 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all duration-700 ${heapUsedPct > 85 ? "bg-red-500" : heapUsedPct > 60 ? "bg-amber-500" : "bg-blue-500"}`}
|
||||
className={`h-2 rounded-full transition-all duration-700 ${heapUsedPct > 85 ? "bg-status-danger" : heapUsedPct > 60 ? "bg-status-warning" : "bg-blue-600"}`}
|
||||
style={{ width: `${heapUsedPct}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -356,13 +356,13 @@ function SystemHealthTab() {
|
||||
<div className="card p-5 flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Database className="w-4 h-4 text-blue-400" />
|
||||
<span className="text-xs text-gray-500 uppercase tracking-wider">Storage</span>
|
||||
<Database className="w-4 h-4 text-blue-500" />
|
||||
<span className="text-xs text-fg-muted uppercase tracking-wider">Storage</span>
|
||||
</div>
|
||||
<Tip
|
||||
raw={`Write velocity (events):\n5 min: ${info.db.load_stats?.m5 ?? 0}\n15 min: ${info.db.load_stats?.m15 ?? 0}\n1 hr: ${info.db.load_stats?.h1 ?? 0}`}
|
||||
>
|
||||
<span className="text-[10px] font-mono text-emerald-400 bg-emerald-500/5 px-2 py-0.5 rounded border border-emerald-500/10 cursor-default">
|
||||
<span className="text-[10px] font-mono text-status-success bg-status-success/5 px-2 py-0.5 rounded border border-status-success/10 cursor-default">
|
||||
⚡ {info.db.load_stats?.m5 ?? 0}/{info.db.load_stats?.m15 ?? 0}/
|
||||
{info.db.load_stats?.h1 ?? 0}
|
||||
</span>
|
||||
@@ -370,8 +370,8 @@ function SystemHealthTab() {
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-gray-400 w-28 flex-shrink-0">Database</span>
|
||||
<span className="text-xs text-gray-200 font-mono ml-auto">
|
||||
<span className="text-xs text-fg-secondary w-28 flex-shrink-0">Database</span>
|
||||
<span className="text-xs text-fg-secondary font-mono ml-auto">
|
||||
{formatBytes(info.db.size)} · {info.db.pragmas?.journal_mode?.toUpperCase() || "WAL"}
|
||||
</span>
|
||||
</div>
|
||||
@@ -425,7 +425,7 @@ function SystemHealthTab() {
|
||||
y="46"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
className="fill-gray-300"
|
||||
className="fill-fg-secondary"
|
||||
fontSize="12"
|
||||
fontWeight="700"
|
||||
fontFamily="monospace"
|
||||
@@ -437,7 +437,7 @@ function SystemHealthTab() {
|
||||
y="60"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
className="fill-gray-600"
|
||||
className="fill-fg-muted"
|
||||
fontSize="8"
|
||||
>
|
||||
total
|
||||
@@ -475,8 +475,8 @@ function SystemHealthTab() {
|
||||
className="w-2.5 h-2.5 rounded-sm flex-shrink-0"
|
||||
style={{ backgroundColor: item.color }}
|
||||
/>
|
||||
<span className="text-gray-400">{item.label}</span>
|
||||
<span className="text-gray-500 ml-auto pl-3 font-mono">
|
||||
<span className="text-fg-secondary">{item.label}</span>
|
||||
<span className="text-fg-muted ml-auto pl-3 font-mono">
|
||||
{Math.round(item.pct)}%
|
||||
</span>
|
||||
</div>
|
||||
@@ -490,13 +490,13 @@ function SystemHealthTab() {
|
||||
<div className="card p-5 flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<ShieldCheck className="w-4 h-4 text-emerald-400" />
|
||||
<span className="text-xs text-gray-500 uppercase tracking-wider">Health Score</span>
|
||||
<ShieldCheck className="w-4 h-4 text-status-success" />
|
||||
<span className="text-xs text-fg-muted uppercase tracking-wider">Health Score</span>
|
||||
</div>
|
||||
<Tip
|
||||
raw={`Composite Index formula:\n0.4 × Success Rate (${successRate.toFixed(1)}%)\n+ 0.25 × Cache Hit (${cacheHitRate.toFixed(1)}%)\n+ 0.25 × (100 − Error Rate) (${(100 - errorRate).toFixed(1)}%)\n+ 0.10 × (100 − Heap%) (${(100 - heapUsedPct).toFixed(1)}%)\n= ${healthScore.toFixed(1)}`}
|
||||
>
|
||||
<span className="text-[10px] text-gray-500 cursor-help border-b border-dashed border-gray-700">
|
||||
<span className="text-[10px] text-fg-muted cursor-help border-b border-dashed border-border-light">
|
||||
ⓘ Formula
|
||||
</span>
|
||||
</Tip>
|
||||
@@ -526,7 +526,7 @@ function SystemHealthTab() {
|
||||
y="57"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
className="fill-gray-100"
|
||||
className="fill-fg-primary"
|
||||
fontSize="24"
|
||||
fontWeight="800"
|
||||
fontFamily="monospace"
|
||||
@@ -538,7 +538,7 @@ function SystemHealthTab() {
|
||||
y="76"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
className="fill-gray-600"
|
||||
className="fill-fg-muted"
|
||||
fontSize="9"
|
||||
fontWeight="500"
|
||||
>
|
||||
@@ -555,8 +555,8 @@ function SystemHealthTab() {
|
||||
raw={`Cache Hit Rate: ${cacheHitRate.toFixed(1)}%\nHits: ${info.transcript_cache?.hits ?? 0}\nMisses: ${info.transcript_cache?.misses ?? 0}`}
|
||||
>
|
||||
<div className="text-center cursor-default">
|
||||
<p className="text-[9px] text-gray-600 uppercase">Cache</p>
|
||||
<p className="text-xs font-mono font-bold text-blue-400">
|
||||
<p className="text-[9px] text-fg-muted uppercase">Cache</p>
|
||||
<p className="text-xs font-mono font-bold text-blue-500">
|
||||
{cacheHitRate.toFixed(0)}%
|
||||
</p>
|
||||
</div>
|
||||
@@ -566,9 +566,9 @@ function SystemHealthTab() {
|
||||
raw={`Error Rate: ${errorRate.toFixed(2)}%\n<5% = healthy, 5-15% = warning, >15% = critical`}
|
||||
>
|
||||
<div className="text-center cursor-default">
|
||||
<p className="text-[9px] text-gray-600 uppercase">Errors</p>
|
||||
<p className="text-[9px] text-fg-muted uppercase">Errors</p>
|
||||
<p
|
||||
className={`text-xs font-mono font-bold ${errorRate < 5 ? "text-emerald-400" : errorRate < 15 ? "text-amber-400" : "text-red-400"}`}
|
||||
className={`text-xs font-mono font-bold ${errorRate < 5 ? "text-status-success" : errorRate < 15 ? "text-status-warning" : "text-status-danger"}`}
|
||||
>
|
||||
{errorRate.toFixed(1)}%
|
||||
</p>
|
||||
@@ -579,7 +579,7 @@ function SystemHealthTab() {
|
||||
raw={`Transcript compactions: ${workflow.compaction?.totalCompactions ?? 0}\nReduces context window by summarizing turns.`}
|
||||
>
|
||||
<div className="text-center cursor-default">
|
||||
<p className="text-[9px] text-gray-600 uppercase">Compact</p>
|
||||
<p className="text-[9px] text-fg-muted uppercase">Compact</p>
|
||||
<p className="text-xs font-mono font-bold text-violet-400">
|
||||
{workflow.compaction?.totalCompactions ?? 0}
|
||||
</p>
|
||||
@@ -590,8 +590,8 @@ function SystemHealthTab() {
|
||||
raw={`Tokens recovered: ${(workflow.compaction?.tokensRecovered ?? 0).toLocaleString()}\nFreed by compaction.`}
|
||||
>
|
||||
<div className="text-center cursor-default">
|
||||
<p className="text-[9px] text-gray-600 uppercase">Saved</p>
|
||||
<p className="text-xs font-mono font-bold text-emerald-400">
|
||||
<p className="text-[9px] text-fg-muted uppercase">Saved</p>
|
||||
<p className="text-xs font-mono font-bold text-status-success">
|
||||
{((workflow.compaction?.tokensRecovered ?? 0) / 1000).toFixed(1)}K
|
||||
</p>
|
||||
</div>
|
||||
@@ -606,10 +606,10 @@ function SystemHealthTab() {
|
||||
<div className="card p-5 flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Bot className="w-4 h-4 text-blue-400" />
|
||||
<span className="text-xs text-gray-500 uppercase tracking-wider">Token Usage</span>
|
||||
<Bot className="w-4 h-4 text-blue-500" />
|
||||
<span className="text-xs text-fg-muted uppercase tracking-wider">Token Usage</span>
|
||||
</div>
|
||||
<span className="text-[10px] font-mono text-gray-500">
|
||||
<span className="text-[10px] font-mono text-fg-muted">
|
||||
{(totalTokens / 1000).toFixed(1)}K total
|
||||
</span>
|
||||
</div>
|
||||
@@ -619,10 +619,10 @@ function SystemHealthTab() {
|
||||
const pct =
|
||||
totalTokens > 0 ? ((m.input_tokens + m.output_tokens) / totalTokens) * 100 : 0;
|
||||
const colors = [
|
||||
"bg-blue-400",
|
||||
"bg-blue-500",
|
||||
"bg-violet-400",
|
||||
"bg-emerald-400",
|
||||
"bg-amber-400",
|
||||
"bg-status-success",
|
||||
"bg-status-warning",
|
||||
"bg-pink-400",
|
||||
"bg-cyan-400",
|
||||
];
|
||||
@@ -634,7 +634,7 @@ function SystemHealthTab() {
|
||||
>
|
||||
<div className="flex items-center gap-3 cursor-default">
|
||||
<span
|
||||
className="text-xs text-gray-400 w-28 truncate flex-shrink-0"
|
||||
className="text-xs text-fg-secondary w-28 truncate flex-shrink-0"
|
||||
title={formatModelName(m.model) ?? m.model}
|
||||
>
|
||||
{formatModelName(m.model) ?? m.model}
|
||||
@@ -645,7 +645,7 @@ function SystemHealthTab() {
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 w-12 text-right flex-shrink-0 font-mono">
|
||||
<span className="text-xs text-fg-muted w-12 text-right flex-shrink-0 font-mono">
|
||||
{pct.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
@@ -653,7 +653,7 @@ function SystemHealthTab() {
|
||||
);
|
||||
})}
|
||||
{modelStats.length === 0 && (
|
||||
<p className="text-xs text-gray-600 text-center py-4">No model data</p>
|
||||
<p className="text-xs text-fg-muted text-center py-4">No model data</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -663,9 +663,9 @@ function SystemHealthTab() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<BarChart3 className="w-4 h-4 text-violet-400" />
|
||||
<span className="text-xs text-gray-500 uppercase tracking-wider">Concurrency</span>
|
||||
<span className="text-xs text-fg-muted uppercase tracking-wider">Concurrency</span>
|
||||
</div>
|
||||
<span className="text-[10px] font-mono text-gray-500">{lanes.length} intervals</span>
|
||||
<span className="text-[10px] font-mono text-fg-muted">{lanes.length} intervals</span>
|
||||
</div>
|
||||
|
||||
{/* Sparkline-style bar chart matching Analytics sparkline */}
|
||||
@@ -698,7 +698,7 @@ function SystemHealthTab() {
|
||||
);
|
||||
})}
|
||||
{lanes.length === 0 && (
|
||||
<p className="text-xs text-gray-600 text-center w-full self-center">
|
||||
<p className="text-xs text-fg-muted text-center w-full self-center">
|
||||
No concurrency data
|
||||
</p>
|
||||
)}
|
||||
@@ -708,8 +708,8 @@ function SystemHealthTab() {
|
||||
<div className="grid grid-cols-3 gap-3 border-t border-border/40 pt-3">
|
||||
<Tip block raw={`Peak: ${maxLaneCount} sessions running simultaneously.`}>
|
||||
<div className="text-center cursor-default">
|
||||
<p className="text-[9px] text-gray-600 uppercase">Peak</p>
|
||||
<p className="text-sm font-mono font-bold text-gray-200">{maxLaneCount}</p>
|
||||
<p className="text-[9px] text-fg-muted uppercase">Peak</p>
|
||||
<p className="text-sm font-mono font-bold text-fg-secondary">{maxLaneCount}</p>
|
||||
</div>
|
||||
</Tip>
|
||||
<Tip
|
||||
@@ -717,16 +717,16 @@ function SystemHealthTab() {
|
||||
raw={`${lanes.filter((l) => l.count > 0).length} of ${lanes.length} intervals have active sessions.`}
|
||||
>
|
||||
<div className="text-center cursor-default">
|
||||
<p className="text-[9px] text-gray-600 uppercase">Active</p>
|
||||
<p className="text-sm font-mono font-bold text-emerald-400">
|
||||
<p className="text-[9px] text-fg-muted uppercase">Active</p>
|
||||
<p className="text-sm font-mono font-bold text-status-success">
|
||||
{lanes.filter((l) => l.count > 0).length}
|
||||
</p>
|
||||
</div>
|
||||
</Tip>
|
||||
<Tip block raw={`Average concurrency across all intervals.`}>
|
||||
<div className="text-center cursor-default">
|
||||
<p className="text-[9px] text-gray-600 uppercase">Avg</p>
|
||||
<p className="text-sm font-mono font-bold text-blue-400">
|
||||
<p className="text-[9px] text-fg-muted uppercase">Avg</p>
|
||||
<p className="text-sm font-mono font-bold text-blue-500">
|
||||
{lanes.length > 0
|
||||
? (lanes.reduce((s, l) => s + l.count, 0) / lanes.length).toFixed(1)
|
||||
: "0"}
|
||||
@@ -743,23 +743,23 @@ function SystemHealthTab() {
|
||||
<div className="card p-5 flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Zap className="w-4 h-4 text-amber-400" />
|
||||
<span className="text-xs text-gray-500 uppercase tracking-wider">Tool Usage</span>
|
||||
<Zap className="w-4 h-4 text-status-warning" />
|
||||
<span className="text-xs text-fg-muted uppercase tracking-wider">Tool Usage</span>
|
||||
</div>
|
||||
<span className="text-[10px] font-mono text-gray-500">top {topTools.length}</span>
|
||||
<span className="text-[10px] font-mono text-fg-muted">top {topTools.length}</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{topTools.map((tool, i) => {
|
||||
const pct = maxToolCount > 0 ? Math.round((tool.count / maxToolCount) * 100) : 0;
|
||||
const colors = [
|
||||
"bg-amber-400",
|
||||
"bg-blue-400",
|
||||
"bg-emerald-400",
|
||||
"bg-status-warning",
|
||||
"bg-blue-500",
|
||||
"bg-status-success",
|
||||
"bg-violet-400",
|
||||
"bg-pink-400",
|
||||
"bg-cyan-400",
|
||||
"bg-red-400",
|
||||
"bg-status-danger",
|
||||
"bg-indigo-400",
|
||||
];
|
||||
return (
|
||||
@@ -770,7 +770,7 @@ function SystemHealthTab() {
|
||||
>
|
||||
<div className="flex items-center gap-3 cursor-default">
|
||||
<span
|
||||
className="text-xs text-gray-400 w-28 truncate flex-shrink-0"
|
||||
className="text-xs text-fg-secondary w-28 truncate flex-shrink-0"
|
||||
title={tool.tool_name}
|
||||
>
|
||||
{tool.tool_name}
|
||||
@@ -781,7 +781,7 @@ function SystemHealthTab() {
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-gray-500 w-10 text-right flex-shrink-0 font-mono">
|
||||
<span className="text-xs text-fg-muted w-10 text-right flex-shrink-0 font-mono">
|
||||
{tool.count > 999 ? `${(tool.count / 1000).toFixed(1)}K` : tool.count}
|
||||
</span>
|
||||
</div>
|
||||
@@ -789,7 +789,7 @@ function SystemHealthTab() {
|
||||
);
|
||||
})}
|
||||
{topTools.length === 0 && (
|
||||
<p className="text-xs text-gray-600 text-center py-6">No tool data yet</p>
|
||||
<p className="text-xs text-fg-muted text-center py-6">No tool data yet</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -799,7 +799,7 @@ function SystemHealthTab() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<GitBranch className="w-4 h-4 text-violet-400" />
|
||||
<span className="text-xs text-gray-500 uppercase tracking-wider">
|
||||
<span className="text-xs text-fg-muted uppercase tracking-wider">
|
||||
Subagent Effectiveness
|
||||
</span>
|
||||
</div>
|
||||
@@ -809,10 +809,10 @@ function SystemHealthTab() {
|
||||
{effectiveness.map((item, i) => {
|
||||
const color =
|
||||
item.successRate >= 90
|
||||
? "bg-emerald-400"
|
||||
? "bg-status-success"
|
||||
: item.successRate >= 70
|
||||
? "bg-amber-400"
|
||||
: "bg-red-400";
|
||||
? "bg-status-warning"
|
||||
: "bg-status-danger";
|
||||
return (
|
||||
<Tip
|
||||
block
|
||||
@@ -820,7 +820,7 @@ function SystemHealthTab() {
|
||||
raw={`${item.subagent_type || "default"}\nSuccess: ${item.successRate.toFixed(1)}%\nTotal: ${item.total} · OK: ${item.completed} · Errors: ${item.errors}`}
|
||||
>
|
||||
<div className="flex items-center gap-3 cursor-default">
|
||||
<span className="text-xs text-gray-400 w-28 truncate flex-shrink-0">
|
||||
<span className="text-xs text-fg-secondary w-28 truncate flex-shrink-0">
|
||||
{item.subagent_type || "default"}
|
||||
</span>
|
||||
<div className="flex-1 bg-surface-3 rounded-full h-2">
|
||||
@@ -830,7 +830,7 @@ function SystemHealthTab() {
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
className={`text-xs w-12 text-right flex-shrink-0 font-mono ${item.successRate >= 90 ? "text-emerald-400" : item.successRate >= 70 ? "text-amber-400" : "text-red-400"}`}
|
||||
className={`text-xs w-12 text-right flex-shrink-0 font-mono ${item.successRate >= 90 ? "text-status-success" : item.successRate >= 70 ? "text-status-warning" : "text-status-danger"}`}
|
||||
>
|
||||
{item.successRate.toFixed(0)}%
|
||||
</span>
|
||||
@@ -839,7 +839,7 @@ function SystemHealthTab() {
|
||||
);
|
||||
})}
|
||||
{effectiveness.length === 0 && (
|
||||
<p className="text-xs text-gray-600 text-center py-6">No subagent data yet</p>
|
||||
<p className="text-xs text-fg-muted text-center py-6">No subagent data yet</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -851,11 +851,11 @@ function SystemHealthTab() {
|
||||
<div className="card p-5 flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Plug className="w-4 h-4 text-amber-400" />
|
||||
<span className="text-xs text-gray-500 uppercase tracking-wider">Integration</span>
|
||||
<Plug className="w-4 h-4 text-status-warning" />
|
||||
<span className="text-xs text-fg-muted uppercase tracking-wider">Integration</span>
|
||||
</div>
|
||||
<span
|
||||
className={`text-[10px] font-mono px-2 py-0.5 rounded ${info.hooks.installed ? "bg-emerald-500/10 text-emerald-400 border border-emerald-500/20" : "bg-surface-3 text-gray-500 border border-border"}`}
|
||||
className={`text-[10px] font-mono px-2 py-0.5 rounded ${info.hooks.installed ? "bg-status-success/10 text-status-success border border-status-success/20" : "bg-surface-3 text-fg-muted border border-border"}`}
|
||||
>
|
||||
{info.hooks.installed ? "Active" : "Offline"}
|
||||
</span>
|
||||
@@ -871,9 +871,9 @@ function SystemHealthTab() {
|
||||
>
|
||||
<div className="flex items-center gap-3 bg-surface-2/50 px-3 py-2 rounded-lg border border-border/30 cursor-default">
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full flex-shrink-0 ${active ? "bg-emerald-400" : "bg-gray-600"}`}
|
||||
className={`w-2 h-2 rounded-full flex-shrink-0 ${active ? "bg-status-success" : "bg-surface-4"}`}
|
||||
/>
|
||||
<span className="text-xs text-gray-300 truncate font-mono">
|
||||
<span className="text-xs text-fg-secondary truncate font-mono">
|
||||
{cwd.split("/").pop() || cwd}
|
||||
</span>
|
||||
</div>
|
||||
@@ -882,8 +882,8 @@ function SystemHealthTab() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-6 border border-dashed border-border/40 rounded-lg">
|
||||
<Search className="w-4 h-4 text-gray-600 mb-2" />
|
||||
<p className="text-xs text-gray-500">No project hooks registered</p>
|
||||
<Search className="w-4 h-4 text-fg-muted mb-2" />
|
||||
<p className="text-xs text-fg-muted">No project hooks registered</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -891,11 +891,11 @@ function SystemHealthTab() {
|
||||
block
|
||||
raw={`WebSocket connections: ${info.server.ws_connections}\nProtocol: RFC 6455`}
|
||||
>
|
||||
<div className="flex items-center gap-3 bg-emerald-500/5 px-3 py-2.5 rounded-lg border border-emerald-500/10 cursor-default">
|
||||
<Activity className="w-3.5 h-3.5 text-emerald-400 animate-pulse" />
|
||||
<div className="flex items-center gap-3 bg-status-success/5 px-3 py-2.5 rounded-lg border border-status-success/10 cursor-default">
|
||||
<Activity className="w-3.5 h-3.5 text-status-success animate-pulse" />
|
||||
<div>
|
||||
<p className="text-[10px] text-emerald-400 font-medium">WebSocket Active</p>
|
||||
<p className="text-[10px] text-gray-500">
|
||||
<p className="text-[10px] text-status-success font-medium">WebSocket Active</p>
|
||||
<p className="text-[10px] text-fg-muted">
|
||||
{info.server.ws_connections} connection
|
||||
{info.server.ws_connections !== 1 ? "s" : ""}
|
||||
</p>
|
||||
@@ -909,9 +909,9 @@ function SystemHealthTab() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Cpu className="w-4 h-4 text-cyan-400" />
|
||||
<span className="text-xs text-gray-500 uppercase tracking-wider">Platform</span>
|
||||
<span className="text-xs text-fg-muted uppercase tracking-wider">Platform</span>
|
||||
</div>
|
||||
<span className="text-[10px] font-mono text-gray-500">{info.server.node_version}</span>
|
||||
<span className="text-[10px] font-mono text-fg-muted">{info.server.node_version}</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
@@ -935,16 +935,18 @@ function SystemHealthTab() {
|
||||
{ label: "Platform", value: `${info.server.platform} / ${info.server.arch}` },
|
||||
].map((row) => (
|
||||
<div key={row.label} className="flex items-center gap-3">
|
||||
<span className="text-xs text-gray-400 w-28 flex-shrink-0">{row.label}</span>
|
||||
<span className="text-xs text-gray-200 font-mono ml-auto">{row.value}</span>
|
||||
<span className="text-xs text-fg-secondary w-28 flex-shrink-0">{row.label}</span>
|
||||
<span className="text-xs text-fg-secondary font-mono ml-auto">{row.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Tip block raw={info.db.path}>
|
||||
<div className="flex items-center gap-2 bg-surface-2/50 px-3 py-2 rounded-lg border border-border/30 cursor-default">
|
||||
<HardDrive className="w-3 h-3 text-gray-500 flex-shrink-0" />
|
||||
<span className="text-[10px] text-gray-400 font-mono truncate">{info.db.path}</span>
|
||||
<HardDrive className="w-3 h-3 text-fg-muted flex-shrink-0" />
|
||||
<span className="text-[10px] text-fg-secondary font-mono truncate">
|
||||
{info.db.path}
|
||||
</span>
|
||||
</div>
|
||||
</Tip>
|
||||
</div>
|
||||
@@ -1145,8 +1147,8 @@ export function Dashboard() {
|
||||
if (error) {
|
||||
return (
|
||||
<div className="text-center py-20">
|
||||
<p className="text-red-400 mb-2">{t("failedConnect")}</p>
|
||||
<p className="text-sm text-gray-500">{error}</p>
|
||||
<p className="text-status-danger mb-2">{t("failedConnect")}</p>
|
||||
<p className="text-sm text-fg-muted">{error}</p>
|
||||
<button onClick={load} className="btn-primary mt-4">
|
||||
{t("common:retry")}
|
||||
</button>
|
||||
@@ -1163,20 +1165,20 @@ export function Dashboard() {
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-lg font-semibold text-gray-100">{t("title")}</h1>
|
||||
<h1 className="text-lg font-semibold text-fg-primary">{t("title")}</h1>
|
||||
{wsConnected ? (
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse-dot" />
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse-dot" />
|
||||
{t("common:live")}
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-gray-400 bg-gray-500/10 border border-gray-500/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-gray-400" />
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-fg-secondary bg-surface-4/10 border border-border-light/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-surface-4" />
|
||||
{t("common:offline")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">{t("subtitle")}</p>
|
||||
<p className="text-xs text-fg-muted">{t("subtitle")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -1187,7 +1189,7 @@ export function Dashboard() {
|
||||
className={`px-2.5 py-1.5 rounded-md text-xs font-medium transition-all flex items-center gap-2 ${
|
||||
activeTab === "monitor"
|
||||
? "bg-accent/15 text-accent shadow-sm"
|
||||
: "text-gray-500 hover:text-gray-300"
|
||||
: "text-fg-muted hover:text-fg-secondary"
|
||||
}`}
|
||||
>
|
||||
<Activity className="w-3.5 h-3.5" /> Monitor
|
||||
@@ -1197,7 +1199,7 @@ export function Dashboard() {
|
||||
className={`px-2.5 py-1.5 rounded-md text-xs font-medium transition-all flex items-center gap-2 ${
|
||||
activeTab === "health"
|
||||
? "bg-accent/15 text-accent shadow-sm"
|
||||
: "text-gray-500 hover:text-gray-300"
|
||||
: "text-fg-muted hover:text-fg-secondary"
|
||||
}`}
|
||||
>
|
||||
<Server className="w-3.5 h-3.5" /> Health
|
||||
@@ -1225,7 +1227,7 @@ export function Dashboard() {
|
||||
label={t("activeAgents")}
|
||||
value={stats?.active_agents ?? ""}
|
||||
icon={Bot}
|
||||
accentColor="text-emerald-400"
|
||||
accentColor="text-status-success"
|
||||
loading={!stats}
|
||||
/>
|
||||
<StatCard
|
||||
@@ -1261,7 +1263,7 @@ export function Dashboard() {
|
||||
: undefined
|
||||
}
|
||||
icon={DollarSign}
|
||||
accentColor="text-emerald-400"
|
||||
accentColor="text-status-success"
|
||||
loading={totalCost === null}
|
||||
/>
|
||||
</div>
|
||||
@@ -1270,7 +1272,9 @@ export function Dashboard() {
|
||||
{/* Active agents */}
|
||||
<div ref={agentsContainerRef} className="min-w-0 overflow-y-auto pr-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-sm font-medium text-gray-300">{t("activeAgentsSection")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary">
|
||||
{t("activeAgentsSection")}
|
||||
</h3>
|
||||
<button onClick={() => navigate("/kanban")} className="btn-ghost text-xs">
|
||||
{t("viewBoard")} <ArrowRight className="w-3 h-3" />
|
||||
</button>
|
||||
@@ -1312,7 +1316,7 @@ export function Dashboard() {
|
||||
{hasChildren && (
|
||||
<button
|
||||
onClick={toggleExpanded}
|
||||
className="p-1 text-gray-500 hover:text-gray-300 transition-colors flex-shrink-0"
|
||||
className="p-1 text-fg-muted hover:text-fg-secondary transition-colors flex-shrink-0"
|
||||
aria-label={isExpanded ? "Collapse subagents" : "Expand subagents"}
|
||||
aria-expanded={isExpanded}
|
||||
>
|
||||
@@ -1375,7 +1379,7 @@ export function Dashboard() {
|
||||
>
|
||||
{t("common:subagent_label", { count: totalDesc })}
|
||||
{activeDesc > 0 && (
|
||||
<span className="text-emerald-400 ml-1">
|
||||
<span className="text-status-success ml-1">
|
||||
({activeDesc} {t("common:active")})
|
||||
</span>
|
||||
)}
|
||||
@@ -1436,7 +1440,7 @@ export function Dashboard() {
|
||||
{/* Recent activity */}
|
||||
<div ref={activityContainerRef} className="min-w-0 overflow-y-auto pl-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-sm font-medium text-gray-300">{t("recentActivity")}</h3>
|
||||
<h3 className="text-sm font-medium text-fg-secondary">{t("recentActivity")}</h3>
|
||||
<button onClick={() => navigate("/activity")} className="btn-ghost text-xs">
|
||||
{t("viewAll")} <ArrowRight className="w-3 h-3" />
|
||||
</button>
|
||||
@@ -1469,7 +1473,7 @@ export function Dashboard() {
|
||||
: "waiting"
|
||||
}
|
||||
/>
|
||||
<span className="text-sm text-gray-300 truncate flex-1">
|
||||
<span className="text-sm text-fg-secondary truncate flex-1">
|
||||
{event.summary || event.event_type}
|
||||
</span>
|
||||
{(() => {
|
||||
@@ -1479,7 +1483,7 @@ export function Dashboard() {
|
||||
const isAuto = /^Session [0-9a-f]{8}$/i.test(sname);
|
||||
return (
|
||||
<span
|
||||
className="text-[11px] text-gray-500 truncate max-w-[9rem] flex-shrink-0"
|
||||
className="text-[11px] text-fg-muted truncate max-w-[9rem] flex-shrink-0"
|
||||
title={event.session_id}
|
||||
>
|
||||
{sname && !isAuto ? (
|
||||
@@ -1491,11 +1495,11 @@ export function Dashboard() {
|
||||
);
|
||||
})()}
|
||||
{event.tool_name && (
|
||||
<span className="text-[11px] text-gray-500 font-mono">
|
||||
<span className="text-[11px] text-fg-muted font-mono">
|
||||
{event.tool_name}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[11px] text-gray-600 flex-shrink-0">
|
||||
<span className="text-[11px] text-fg-muted flex-shrink-0">
|
||||
{timeAgo(event.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -256,20 +256,20 @@ export function KanbanBoard() {
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-lg font-semibold text-gray-100 truncate">{t("title")}</h1>
|
||||
<h1 className="text-lg font-semibold text-fg-primary truncate">{t("title")}</h1>
|
||||
{wsConnected ? (
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse-dot" />
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse-dot" />
|
||||
{t("common:live")}
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-gray-400 bg-gray-500/10 border border-gray-500/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-gray-400" />
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-fg-secondary bg-surface-4/10 border border-border-light/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-surface-4" />
|
||||
{t("common:offline")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 truncate">{subtitle}</p>
|
||||
<p className="text-xs text-fg-muted truncate">{subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
@@ -392,7 +392,7 @@ function ViewToggle({ view, onChange }: ViewToggleProps) {
|
||||
const baseClass =
|
||||
"px-3 py-1.5 text-xs font-medium transition-colors first:rounded-l-lg last:rounded-r-lg";
|
||||
const activeClass = "bg-accent/15 text-accent";
|
||||
const inactiveClass = "text-gray-400 hover:text-gray-200 hover:bg-surface-3";
|
||||
const inactiveClass = "text-fg-secondary hover:text-fg-primary hover:bg-surface-3";
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -463,7 +463,7 @@ function Column({
|
||||
{t(labelKey)}
|
||||
</span>
|
||||
{tooltip && <ColumnHelp text={tooltip} />}
|
||||
<span className="ml-auto text-[11px] text-gray-600 bg-surface-3 px-2 py-0.5 rounded-full">
|
||||
<span className="ml-auto text-[11px] text-fg-muted bg-surface-3 px-2 py-0.5 rounded-full">
|
||||
{count}
|
||||
</span>
|
||||
</div>
|
||||
@@ -475,7 +475,7 @@ function Column({
|
||||
{remaining > 0 && (
|
||||
<button
|
||||
onClick={onShowMore}
|
||||
className="w-full py-2 text-[11px] text-gray-500 hover:text-gray-300 flex items-center justify-center gap-1 transition-colors"
|
||||
className="w-full py-2 text-[11px] text-fg-muted hover:text-fg-secondary flex items-center justify-center gap-1 transition-colors"
|
||||
>
|
||||
<ChevronDown className="w-3 h-3" />
|
||||
{t("common:showMore", { count: remaining })}
|
||||
@@ -483,7 +483,7 @@ function Column({
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-24 text-xs text-gray-600">
|
||||
<div className="flex items-center justify-center h-24 text-xs text-fg-muted">
|
||||
{emptyLabel}
|
||||
</div>
|
||||
)}
|
||||
@@ -516,11 +516,11 @@ function ColumnHelp({ text }: { text: string }) {
|
||||
onFocus={() => setShow(true)}
|
||||
onBlur={() => setShow(false)}
|
||||
>
|
||||
<HelpCircle className="w-3 h-3 text-gray-500 hover:text-gray-300 transition-colors" />
|
||||
<HelpCircle className="w-3 h-3 text-fg-muted hover:text-fg-secondary transition-colors" />
|
||||
{show && (
|
||||
<span
|
||||
role="tooltip"
|
||||
className="absolute left-0 top-full mt-1.5 w-64 px-3 py-2 text-[11px] leading-relaxed text-gray-200 bg-surface-3 border border-border rounded-md shadow-xl z-50 pointer-events-none whitespace-pre-line"
|
||||
className="absolute left-0 top-full mt-1.5 w-64 px-3 py-2 text-[11px] leading-relaxed text-fg-secondary bg-surface-3 border border-border rounded-md shadow-xl z-50 pointer-events-none whitespace-pre-line"
|
||||
>
|
||||
{text}
|
||||
</span>
|
||||
|
||||
@@ -73,11 +73,11 @@ export function NotFound() {
|
||||
<AlertTriangle className="w-7 h-7 text-accent" />
|
||||
</div>
|
||||
|
||||
<p className="text-xs uppercase tracking-[0.18em] text-gray-500 mb-2">
|
||||
<p className="text-xs uppercase tracking-[0.18em] text-fg-muted mb-2">
|
||||
{t("notFound.code")}
|
||||
</p>
|
||||
<h2 className="text-2xl font-semibold text-gray-100 mb-2">{t("notFound.title")}</h2>
|
||||
<p className="text-sm text-gray-400 mb-8">{t("notFound.description")}</p>
|
||||
<h2 className="text-2xl font-semibold text-fg-primary mb-2">{t("notFound.title")}</h2>
|
||||
<p className="text-sm text-fg-secondary mb-8">{t("notFound.description")}</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||
<button className="btn-primary" onClick={() => navigate("/")}>
|
||||
|
||||
@@ -577,7 +577,7 @@ export function SessionDetail() {
|
||||
if (error || !session) {
|
||||
return (
|
||||
<div className="text-center py-20">
|
||||
<p className="text-red-400 mb-2">{error || t("detail.notFound")}</p>
|
||||
<p className="text-status-danger mb-2">{error || t("detail.notFound")}</p>
|
||||
<button onClick={goBack} className="btn-ghost mt-4">
|
||||
<ArrowLeft className="w-4 h-4" /> {t("detail.backToSessions")}
|
||||
</button>
|
||||
@@ -594,7 +594,7 @@ export function SessionDetail() {
|
||||
</button>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h2 className="text-xl font-semibold text-gray-100">
|
||||
<h2 className="text-xl font-semibold text-fg-primary">
|
||||
{session.name || `${t("defaultName")}${session.id.slice(0, 8)}`}
|
||||
</h2>
|
||||
<SessionStatusBadge
|
||||
@@ -603,34 +603,34 @@ export function SessionDetail() {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 mt-1">
|
||||
<span className="inline-flex items-center gap-1 text-xs text-gray-500 font-mono bg-surface-2 px-2 py-1 rounded">
|
||||
<span className="inline-flex items-center gap-1 text-xs text-fg-muted font-mono bg-surface-2 px-2 py-1 rounded">
|
||||
<span title={session.id}>{session.id.slice(0, 16)}</span>
|
||||
<CopyButton text={session.id} />
|
||||
</span>
|
||||
{session.model && (
|
||||
<span className="inline-flex items-center gap-1.5 text-xs text-gray-400 bg-surface-2 px-2 py-1 rounded">
|
||||
<Cpu className="w-3 h-3 text-gray-500" />
|
||||
<span className="inline-flex items-center gap-1.5 text-xs text-fg-secondary bg-surface-2 px-2 py-1 rounded">
|
||||
<Cpu className="w-3 h-3 text-fg-muted" />
|
||||
{formatModelName(session.model)}
|
||||
</span>
|
||||
)}
|
||||
<span className="inline-flex items-center gap-1.5 text-xs text-gray-400 bg-surface-2 px-2 py-1 rounded">
|
||||
<Clock className="w-3 h-3 text-gray-500" />
|
||||
<span className="inline-flex items-center gap-1.5 text-xs text-fg-secondary bg-surface-2 px-2 py-1 rounded">
|
||||
<Clock className="w-3 h-3 text-fg-muted" />
|
||||
{formatDateTime(session.started_at)}
|
||||
{session.ended_at && (
|
||||
<span className="text-gray-500 ml-1">
|
||||
<span className="text-fg-muted ml-1">
|
||||
({formatDuration(session.started_at, session.ended_at)})
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
{cost && cost.total_cost > 0 && (
|
||||
<span className="inline-flex items-center gap-1.5 text-xs font-medium text-emerald-400 bg-emerald-500/10 px-2 py-1 rounded">
|
||||
<span className="inline-flex items-center gap-1.5 text-xs font-medium text-status-success bg-status-success/10 px-2 py-1 rounded">
|
||||
<DollarSign className="w-3 h-3" />
|
||||
{fmtCostFull(cost.total_cost).slice(1)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{session.cwd && (
|
||||
<div className="flex items-center gap-1.5 text-xs text-gray-500 mt-2 min-w-0">
|
||||
<div className="flex items-center gap-1.5 text-xs text-fg-muted mt-2 min-w-0">
|
||||
<FolderOpen className="w-3 h-3 flex-shrink-0" />
|
||||
<span className="font-mono truncate" title={session.cwd}>
|
||||
{session.cwd}
|
||||
@@ -660,35 +660,35 @@ export function SessionDetail() {
|
||||
<div
|
||||
className={`flex items-center gap-3 rounded-lg border px-4 py-2.5 ${
|
||||
urgent
|
||||
? "border-amber-500/40 bg-amber-500/[0.08]"
|
||||
? "border-status-warning/40 bg-status-warning/[0.08]"
|
||||
: "border-yellow-500/25 bg-yellow-500/[0.05]"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`w-7 h-7 rounded-md inline-flex items-center justify-center flex-shrink-0 border ${
|
||||
urgent
|
||||
? "bg-amber-500/15 border-amber-500/30"
|
||||
? "bg-status-warning/15 border-status-warning/30"
|
||||
: "bg-yellow-500/10 border-yellow-500/25"
|
||||
}`}
|
||||
>
|
||||
<ReasonIcon
|
||||
className={`w-3.5 h-3.5 ${urgent ? "text-amber-300" : "text-yellow-300"}`}
|
||||
className={`w-3.5 h-3.5 ${urgent ? "text-status-warning" : "text-yellow-300"}`}
|
||||
/>
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div
|
||||
className={`text-sm font-medium ${urgent ? "text-amber-200" : "text-yellow-200"}`}
|
||||
className={`text-sm font-medium ${urgent ? "text-status-warning" : "text-yellow-200"}`}
|
||||
>
|
||||
{t("detail.waitingBanner.title")}
|
||||
{cfg && (
|
||||
<span className={urgent ? "text-amber-300/90" : "text-yellow-300/80"}>
|
||||
<span className={urgent ? "text-status-warning/90" : "text-yellow-300/80"}>
|
||||
{" · "}
|
||||
{t(cfg.labelKey)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={`text-[11px] ${urgent ? "text-amber-400/70" : "text-yellow-400/60"}`}
|
||||
className={`text-[11px] ${urgent ? "text-status-warning/70" : "text-yellow-400/60"}`}
|
||||
>
|
||||
{cfg ? t(cfg.descKey) : t("detail.waitingBanner.generic")}
|
||||
</div>
|
||||
@@ -696,12 +696,12 @@ export function SessionDetail() {
|
||||
{session.awaiting_input_since && (
|
||||
<span
|
||||
className={`text-[11px] flex-shrink-0 flex items-center gap-1.5 ${
|
||||
urgent ? "text-amber-300/80" : "text-yellow-400/70"
|
||||
urgent ? "text-status-warning/80" : "text-yellow-400/70"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`w-1.5 h-1.5 rounded-full animate-pulse-dot ${
|
||||
urgent ? "bg-amber-400" : "bg-yellow-400"
|
||||
urgent ? "bg-status-warning" : "bg-yellow-400"
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
@@ -715,23 +715,23 @@ export function SessionDetail() {
|
||||
{isDashboardRun && (
|
||||
<Link
|
||||
to={`/run?session=${encodeURIComponent(id || "")}`}
|
||||
className="flex items-center gap-3 rounded-lg border border-emerald-500/30 bg-emerald-500/[0.06] hover:bg-emerald-500/[0.12] hover:border-emerald-500/50 px-4 py-2.5 transition-colors group"
|
||||
className="flex items-center gap-3 rounded-lg border border-status-success/30 bg-status-success/[0.06] hover:bg-status-success/[0.12] hover:border-status-success/50 px-4 py-2.5 transition-colors group"
|
||||
>
|
||||
<span className="w-7 h-7 rounded-md bg-emerald-500/15 border border-emerald-500/30 inline-flex items-center justify-center flex-shrink-0">
|
||||
<Play className="w-3.5 h-3.5 text-emerald-300" />
|
||||
<span className="w-7 h-7 rounded-md bg-status-success/15 border border-status-success/30 inline-flex items-center justify-center flex-shrink-0">
|
||||
<Play className="w-3.5 h-3.5 text-status-success" />
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm font-medium text-emerald-200">
|
||||
<div className="text-sm font-medium text-status-success">
|
||||
{t("detail.dashboardRun.title", "This session is being driven from the Run page")}
|
||||
</div>
|
||||
<div className="text-[11px] text-emerald-400/70">
|
||||
<div className="text-[11px] text-status-success/70">
|
||||
{t(
|
||||
"detail.dashboardRun.body",
|
||||
"Send follow-ups, watch streaming output, or stop the run from there."
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ExternalLink className="w-4 h-4 text-emerald-300/70 group-hover:text-emerald-200 flex-shrink-0" />
|
||||
<ExternalLink className="w-4 h-4 text-status-success/70 group-hover:text-status-success flex-shrink-0" />
|
||||
</Link>
|
||||
)}
|
||||
|
||||
@@ -745,7 +745,7 @@ export function SessionDetail() {
|
||||
className={`flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === "agents"
|
||||
? "border-violet-500 text-violet-400"
|
||||
: "border-transparent text-gray-500 hover:text-gray-300"
|
||||
: "border-transparent text-fg-muted hover:text-fg-secondary"
|
||||
}`}
|
||||
>
|
||||
<Bot className="w-4 h-4" />
|
||||
@@ -759,7 +759,7 @@ export function SessionDetail() {
|
||||
className={`flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === "conversation"
|
||||
? "border-violet-500 text-violet-400"
|
||||
: "border-transparent text-gray-500 hover:text-gray-300"
|
||||
: "border-transparent text-fg-muted hover:text-fg-secondary"
|
||||
}`}
|
||||
>
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
@@ -773,7 +773,7 @@ export function SessionDetail() {
|
||||
className={`flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === "timeline"
|
||||
? "border-violet-500 text-violet-400"
|
||||
: "border-transparent text-gray-500 hover:text-gray-300"
|
||||
: "border-transparent text-fg-muted hover:text-fg-secondary"
|
||||
}`}
|
||||
>
|
||||
<List className="w-4 h-4" />
|
||||
@@ -783,12 +783,12 @@ export function SessionDetail() {
|
||||
|
||||
{/* Tab Content */}
|
||||
{transcriptNotFound && (
|
||||
<div className="flex items-center gap-2 px-4 py-2.5 mb-3 text-sm text-amber-400 bg-amber-500/10 border border-amber-500/20 rounded-lg">
|
||||
<div className="flex items-center gap-2 px-4 py-2.5 mb-3 text-sm text-status-warning bg-status-warning/10 border border-status-warning/20 rounded-lg">
|
||||
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
||||
<span>{t("detail.transcriptNotFound")}</span>
|
||||
<button
|
||||
onClick={() => setTranscriptNotFound(false)}
|
||||
className="ml-auto text-amber-400/60 hover:text-amber-400 transition-colors"
|
||||
className="ml-auto text-status-warning/60 hover:text-status-warning transition-colors"
|
||||
>
|
||||
<List className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@@ -801,23 +801,23 @@ export function SessionDetail() {
|
||||
|
||||
{workflows.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h3 className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-2 flex items-center gap-1.5">
|
||||
<h3 className="text-xs font-semibold text-fg-secondary uppercase tracking-wider mb-2 flex items-center gap-1.5">
|
||||
<Workflow className="w-3.5 h-3.5 text-violet-400" />
|
||||
{wfT("runs.sessionTitle")}
|
||||
<span className="text-gray-600 font-mono">· {workflows.length}</span>
|
||||
<span className="text-fg-muted font-mono">· {workflows.length}</span>
|
||||
</h3>
|
||||
<WorkflowRunsPanel runs={workflows} hideSessionLink />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{agents.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">{t("detail.noAgents")}</p>
|
||||
<p className="text-sm text-fg-muted">{t("detail.noAgents")}</p>
|
||||
) : (
|
||||
<>
|
||||
<h3 className="text-xs font-semibold text-gray-400 uppercase tracking-wider mb-3 flex items-center gap-1.5">
|
||||
<h3 className="text-xs font-semibold text-fg-secondary uppercase tracking-wider mb-3 flex items-center gap-1.5">
|
||||
<Bot className="w-3.5 h-3.5 text-violet-400" />
|
||||
{t("detail.agents")}
|
||||
<span className="text-gray-600 font-mono">· {agents.length}</span>
|
||||
<span className="text-fg-muted font-mono">· {agents.length}</span>
|
||||
</h3>
|
||||
<div className="space-y-2" data-testid="agent-tree">
|
||||
{(() => {
|
||||
@@ -880,7 +880,7 @@ export function SessionDetail() {
|
||||
{hasChildren && (
|
||||
<button
|
||||
onClick={toggleExpanded}
|
||||
className="p-1 text-gray-500 hover:text-gray-300 transition-colors flex-shrink-0"
|
||||
className="p-1 text-fg-muted hover:text-fg-secondary transition-colors flex-shrink-0"
|
||||
aria-label={isExpanded ? "Collapse subagents" : "Expand subagents"}
|
||||
aria-expanded={isExpanded}
|
||||
>
|
||||
@@ -966,7 +966,7 @@ export function SessionDetail() {
|
||||
{/* Orphaned subagents */}
|
||||
{orphans.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<p className="text-[11px] text-gray-500 mb-2 uppercase tracking-wider">
|
||||
<p className="text-[11px] text-fg-muted mb-2 uppercase tracking-wider">
|
||||
{t("detail.unparented")}
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
@@ -984,7 +984,7 @@ export function SessionDetail() {
|
||||
{/* Cost Breakdown - shown under Agents tab */}
|
||||
{cost && cost.breakdown.length > 0 && cost.total_cost > 0 && (
|
||||
<div className="mt-8">
|
||||
<h3 className="text-sm font-medium text-gray-300 mb-4 flex items-center gap-2">
|
||||
<h3 className="text-sm font-medium text-fg-secondary mb-4 flex items-center gap-2">
|
||||
<DollarSign className="w-4 h-4" />
|
||||
{t("detail.costBreakdown")}
|
||||
</h3>
|
||||
@@ -992,22 +992,22 @@ export function SessionDetail() {
|
||||
<table className="w-full min-w-[600px]">
|
||||
<thead>
|
||||
<tr className="border-b border-border text-left">
|
||||
<th className="px-5 py-2.5 text-[11px] font-semibold text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-5 py-2.5 text-[11px] font-semibold text-fg-muted uppercase tracking-wider">
|
||||
{t("common:cost.model")}
|
||||
</th>
|
||||
<th className="px-5 py-2.5 text-[11px] font-semibold text-gray-500 uppercase tracking-wider text-right">
|
||||
<th className="px-5 py-2.5 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
|
||||
{t("common:token.input")}
|
||||
</th>
|
||||
<th className="px-5 py-2.5 text-[11px] font-semibold text-gray-500 uppercase tracking-wider text-right">
|
||||
<th className="px-5 py-2.5 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
|
||||
{t("common:token.output")}
|
||||
</th>
|
||||
<th className="px-5 py-2.5 text-[11px] font-semibold text-gray-500 uppercase tracking-wider text-right">
|
||||
<th className="px-5 py-2.5 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
|
||||
{t("common:token.cacheRead")}
|
||||
</th>
|
||||
<th className="px-5 py-2.5 text-[11px] font-semibold text-gray-500 uppercase tracking-wider text-right">
|
||||
<th className="px-5 py-2.5 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
|
||||
{t("common:token.cacheWrite")}
|
||||
</th>
|
||||
<th className="px-5 py-2.5 text-[11px] font-semibold text-gray-500 uppercase tracking-wider text-right">
|
||||
<th className="px-5 py-2.5 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
|
||||
{t("common:cost.cost")}
|
||||
</th>
|
||||
</tr>
|
||||
@@ -1015,31 +1015,31 @@ export function SessionDetail() {
|
||||
<tbody className="divide-y divide-border">
|
||||
{cost.breakdown.map((row) => (
|
||||
<tr key={row.model} className="hover:bg-surface-4 transition-colors">
|
||||
<td className="px-5 py-2.5 text-sm font-mono text-gray-300">
|
||||
<td className="px-5 py-2.5 text-sm font-mono text-fg-secondary">
|
||||
{formatModelName(row.model)}
|
||||
</td>
|
||||
<td className="px-5 py-2.5 text-sm text-gray-400 text-right font-mono">
|
||||
<td className="px-5 py-2.5 text-sm text-fg-secondary text-right font-mono">
|
||||
{row.input_tokens.toLocaleString()}
|
||||
</td>
|
||||
<td className="px-5 py-2.5 text-sm text-gray-400 text-right font-mono">
|
||||
<td className="px-5 py-2.5 text-sm text-fg-secondary text-right font-mono">
|
||||
{row.output_tokens.toLocaleString()}
|
||||
</td>
|
||||
<td className="px-5 py-2.5 text-sm text-gray-400 text-right font-mono">
|
||||
<td className="px-5 py-2.5 text-sm text-fg-secondary text-right font-mono">
|
||||
{row.cache_read_tokens.toLocaleString()}
|
||||
</td>
|
||||
<td className="px-5 py-2.5 text-sm text-gray-400 text-right font-mono">
|
||||
<td className="px-5 py-2.5 text-sm text-fg-secondary text-right font-mono">
|
||||
{row.cache_write_tokens.toLocaleString()}
|
||||
</td>
|
||||
<td className="px-5 py-2.5 text-sm text-emerald-400 text-right font-mono font-medium">
|
||||
<td className="px-5 py-2.5 text-sm text-status-success text-right font-mono font-medium">
|
||||
{fmtCostFull(row.cost, 4)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
<tr className="bg-surface-2">
|
||||
<td className="px-5 py-2.5 text-sm font-medium text-gray-200" colSpan={5}>
|
||||
<td className="px-5 py-2.5 text-sm font-medium text-fg-secondary" colSpan={5}>
|
||||
{t("common:total")}
|
||||
</td>
|
||||
<td className="px-5 py-2.5 text-sm text-emerald-400 text-right font-mono font-semibold">
|
||||
<td className="px-5 py-2.5 text-sm text-status-success text-right font-mono font-semibold">
|
||||
{fmtCostFull(cost.total_cost, 4)}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -1071,7 +1071,7 @@ export function SessionDetail() {
|
||||
/>
|
||||
</div>
|
||||
{events.length === 0 ? (
|
||||
<p className="text-sm text-gray-500">
|
||||
<p className="text-sm text-fg-muted">
|
||||
{isEmptyFilters(filters) ? t("detail.noEvents") : t("common:eventFilters.noResults")}
|
||||
</p>
|
||||
) : (
|
||||
@@ -1092,12 +1092,12 @@ export function SessionDetail() {
|
||||
className="w-full text-left px-5 py-3 flex items-center gap-4 hover:bg-surface-4 transition-colors min-w-0 cursor-pointer"
|
||||
>
|
||||
<span
|
||||
className={`text-gray-500 text-[10px] w-3 flex-shrink-0 transition-transform ${isOpen ? "rotate-90" : ""}`}
|
||||
className={`text-fg-muted text-[10px] w-3 flex-shrink-0 transition-transform ${isOpen ? "rotate-90" : ""}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
▶
|
||||
</span>
|
||||
<div className="w-16 text-[11px] text-gray-600 font-mono flex-shrink-0">
|
||||
<div className="w-16 text-[11px] text-fg-muted font-mono flex-shrink-0">
|
||||
{timeAgo(event.created_at)}
|
||||
</div>
|
||||
<AgentStatusBadge status={statusFromEventType(event.event_type)} />
|
||||
@@ -1115,10 +1115,10 @@ export function SessionDetail() {
|
||||
agentOriginLabel(event.agent_id, agentInfoById)
|
||||
);
|
||||
return (
|
||||
<span className="text-sm text-gray-300 flex-1 truncate">
|
||||
<span className="text-sm text-fg-secondary flex-1 truncate">
|
||||
{origin && (
|
||||
<span
|
||||
className="text-gray-500 mr-1"
|
||||
className="text-fg-muted mr-1"
|
||||
title={event.agent_id ?? undefined}
|
||||
>
|
||||
{origin} ·
|
||||
@@ -1129,7 +1129,7 @@ export function SessionDetail() {
|
||||
);
|
||||
})()}
|
||||
{event.tool_name && (
|
||||
<span className="text-[11px] px-2 py-0.5 bg-surface-2 rounded text-gray-500 font-mono">
|
||||
<span className="text-[11px] px-2 py-0.5 bg-surface-2 rounded text-fg-muted font-mono">
|
||||
{event.tool_name}
|
||||
</span>
|
||||
)}
|
||||
@@ -1149,7 +1149,7 @@ export function SessionDetail() {
|
||||
)}
|
||||
{events.length < eventsTotal && (
|
||||
<div className="flex items-center justify-between mt-3 px-1">
|
||||
<span className="text-xs text-gray-500">
|
||||
<span className="text-xs text-fg-muted">
|
||||
{t("common:eventFilters.showing", { shown: events.length, total: eventsTotal })}
|
||||
</span>
|
||||
<button
|
||||
|
||||
@@ -283,20 +283,20 @@ export function Sessions() {
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-lg font-semibold text-gray-100">{t("title")}</h1>
|
||||
<h1 className="text-lg font-semibold text-fg-primary">{t("title")}</h1>
|
||||
{wsConnected ? (
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse-dot" />
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse-dot" />
|
||||
{t("common:live")}
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-gray-400 bg-gray-500/10 border border-gray-500/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-gray-400" />
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-fg-secondary bg-surface-4/10 border border-border-light/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-surface-4" />
|
||||
{t("common:offline")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
<p className="text-xs text-fg-muted">
|
||||
{t("sessionCount", { count: total })}
|
||||
{filter ? ` ${filter}` : ""}
|
||||
</p>
|
||||
@@ -311,7 +311,7 @@ export function Sessions() {
|
||||
<div className="flex flex-wrap lg:flex-nowrap items-center gap-3 mb-6 bg-surface-2/40 p-2 rounded-xl border border-border w-full">
|
||||
{/* Search */}
|
||||
<div className="relative flex-1 min-w-[180px] max-w-[340px]">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-fg-muted" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t("searchPlaceholder")}
|
||||
@@ -335,7 +335,7 @@ export function Sessions() {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-gray-500 pointer-events-none" />
|
||||
<ChevronDown className="absolute right-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-fg-muted pointer-events-none" />
|
||||
</div>
|
||||
|
||||
{/* Sort Controls */}
|
||||
@@ -344,7 +344,7 @@ export function Sessions() {
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value)}
|
||||
className="bg-transparent w-full text-sm text-gray-200 outline-none pl-3 pr-8 appearance-none cursor-pointer whitespace-nowrap"
|
||||
className="bg-transparent w-full text-sm text-fg-secondary outline-none pl-3 pr-8 appearance-none cursor-pointer whitespace-nowrap"
|
||||
>
|
||||
<option value="time">Sort by Time ({sortDesc ? "Newest" : "Oldest"})</option>
|
||||
<option value="duration">
|
||||
@@ -352,12 +352,12 @@ export function Sessions() {
|
||||
</option>
|
||||
<option value="price">Sort by Price ({sortDesc ? "Highest" : "Lowest"})</option>
|
||||
</select>
|
||||
<ChevronDown className="absolute right-2 top-1/2 -translate-y-1/2 w-3 h-3 text-gray-500 pointer-events-none" />
|
||||
<ChevronDown className="absolute right-2 top-1/2 -translate-y-1/2 w-3 h-3 text-fg-muted pointer-events-none" />
|
||||
</div>
|
||||
<div className="w-px h-4 bg-border mx-1" />
|
||||
<button
|
||||
onClick={() => setSortDesc(!sortDesc)}
|
||||
className="p-1.5 rounded hover:bg-surface-3 text-gray-400 hover:text-gray-200 transition-colors shrink-0"
|
||||
className="p-1.5 rounded hover:bg-surface-3 text-fg-secondary hover:text-fg-primary transition-colors shrink-0"
|
||||
title={sortDesc ? "Descending" : "Ascending"}
|
||||
>
|
||||
{sortDesc ? <SortDesc className="w-4 h-4" /> : <SortAsc className="w-4 h-4" />}
|
||||
@@ -372,8 +372,8 @@ export function Sessions() {
|
||||
onClick={() => setFilter(opt.value)}
|
||||
className={`px-3 py-1.5 text-xs font-medium rounded-md transition-colors whitespace-nowrap ${
|
||||
filter === opt.value
|
||||
? "bg-surface-4 text-gray-200"
|
||||
: "text-gray-500 hover:text-gray-300"
|
||||
? "bg-surface-4 text-fg-secondary"
|
||||
: "text-fg-muted hover:text-fg-secondary"
|
||||
}`}
|
||||
>
|
||||
{opt.label}
|
||||
@@ -394,25 +394,25 @@ export function Sessions() {
|
||||
<table className="w-full min-w-[800px]">
|
||||
<thead>
|
||||
<tr className="border-b border-border text-left">
|
||||
<th className="px-5 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-5 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider">
|
||||
{t("tableSession")}
|
||||
</th>
|
||||
<th className="px-5 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-5 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider">
|
||||
{t("tableStatus")}
|
||||
</th>
|
||||
<th className="px-5 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-5 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider">
|
||||
{t("tableLastActive")}
|
||||
</th>
|
||||
<th className="px-5 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-5 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider">
|
||||
{t("tableDuration")}
|
||||
</th>
|
||||
<th className="px-5 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-5 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider">
|
||||
{t("tableAgents")}
|
||||
</th>
|
||||
<th className="px-5 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-5 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider">
|
||||
{t("tableCost")}
|
||||
</th>
|
||||
<th className="px-5 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-5 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider">
|
||||
{t("tableDirectory")}
|
||||
</th>
|
||||
<th className="w-10"></th>
|
||||
@@ -437,7 +437,7 @@ export function Sessions() {
|
||||
<td className="px-5 py-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm font-medium text-gray-200">
|
||||
<p className="text-sm font-medium text-fg-secondary">
|
||||
{session.name || `${t("defaultName")}${session.id.slice(0, 8)}`}
|
||||
</p>
|
||||
{session.source && session.source !== "local" && (
|
||||
@@ -453,7 +453,7 @@ export function Sessions() {
|
||||
<Link
|
||||
to={`/run?session=${encodeURIComponent(session.id)}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="inline-flex items-center gap-1 text-[10px] font-semibold text-emerald-300 bg-emerald-500/10 border border-emerald-500/25 hover:bg-emerald-500/20 hover:text-emerald-200 px-1.5 py-0.5 rounded-full transition-colors"
|
||||
className="inline-flex items-center gap-1 text-[10px] font-semibold text-status-success bg-status-success/10 border border-status-success/25 hover:bg-status-success/20 hover:text-status-success px-1.5 py-0.5 rounded-full transition-colors"
|
||||
title={t("dashboardRunBadge", "Driven by Run page · click to open")}
|
||||
>
|
||||
<Play className="w-2.5 h-2.5" />
|
||||
@@ -461,7 +461,7 @@ export function Sessions() {
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[11px] text-gray-600 font-mono">
|
||||
<p className="text-[11px] text-fg-muted font-mono">
|
||||
{session.id.slice(0, 12)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -472,28 +472,28 @@ export function Sessions() {
|
||||
reason={sessionAwaitingReason(session)}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-5 py-4 text-sm text-gray-400">
|
||||
<td className="px-5 py-4 text-sm text-fg-secondary">
|
||||
{formatDateTime(session.last_activity || session.started_at)}
|
||||
</td>
|
||||
<td className="px-5 py-4 text-sm text-gray-400 font-mono">
|
||||
<td className="px-5 py-4 text-sm text-fg-secondary font-mono">
|
||||
{session.ended_at
|
||||
? formatDuration(session.started_at, session.ended_at)
|
||||
: t("common:running")}
|
||||
</td>
|
||||
<td className="px-5 py-4 text-sm text-gray-400">
|
||||
<td className="px-5 py-4 text-sm text-fg-secondary">
|
||||
{session.agent_count ?? "-"}
|
||||
</td>
|
||||
<td className="px-5 py-4 text-sm text-gray-400 font-mono">
|
||||
<td className="px-5 py-4 text-sm text-fg-secondary font-mono">
|
||||
{session.cost != null && session.cost > 0 ? fmtCost(session.cost) : "-"}
|
||||
</td>
|
||||
<td
|
||||
className="px-5 py-4 text-[11px] text-gray-500 font-mono"
|
||||
className="px-5 py-4 text-[11px] text-fg-muted font-mono"
|
||||
title={session.cwd || undefined}
|
||||
>
|
||||
{session.cwd ? truncate(session.cwd, 30) : "-"}
|
||||
</td>
|
||||
<td className="px-3 py-4">
|
||||
<ChevronRight className="w-4 h-4 text-gray-600 group-hover:text-gray-400 transition-colors" />
|
||||
<ChevronRight className="w-4 h-4 text-fg-muted group-hover:text-fg-secondary transition-colors" />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@@ -502,7 +502,7 @@ export function Sessions() {
|
||||
</div>
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between mt-4 px-1">
|
||||
<span className="text-xs text-gray-500">
|
||||
<span className="text-xs text-fg-muted">
|
||||
{t("common:pagination.showing", {
|
||||
from: page * PAGE_SIZE + 1,
|
||||
to: Math.min((page + 1) * PAGE_SIZE, total),
|
||||
@@ -513,17 +513,17 @@ export function Sessions() {
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.max(0, p - 1))}
|
||||
disabled={page === 0}
|
||||
className="px-3 py-1.5 text-xs font-medium rounded-md bg-surface-2 text-gray-400 hover:text-gray-200 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
className="px-3 py-1.5 text-xs font-medium rounded-md bg-surface-2 text-fg-secondary hover:text-fg-primary disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
{t("common:pagination.previous")}
|
||||
</button>
|
||||
<span className="px-3 py-1.5 text-xs text-gray-500">
|
||||
<span className="px-3 py-1.5 text-xs text-fg-muted">
|
||||
{page + 1} / {totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
|
||||
disabled={page >= totalPages - 1}
|
||||
className="px-3 py-1.5 text-xs font-medium rounded-md bg-surface-2 text-gray-400 hover:text-gray-200 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
className="px-3 py-1.5 text-xs font-medium rounded-md bg-surface-2 text-fg-secondary hover:text-fg-primary disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
{t("common:pagination.next")}
|
||||
</button>
|
||||
|
||||
+167
-157
@@ -311,8 +311,10 @@ function Toggle({
|
||||
return (
|
||||
<label className="flex items-center justify-between gap-3 cursor-pointer group">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm text-gray-300 group-hover:text-gray-200 transition-colors">{label}</p>
|
||||
{description && <p className="text-xs text-gray-500 mt-0.5">{description}</p>}
|
||||
<p className="text-sm text-fg-secondary group-hover:text-fg-secondary transition-colors">
|
||||
{label}
|
||||
</p>
|
||||
{description && <p className="text-xs text-fg-muted mt-0.5">{description}</p>}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -320,7 +322,7 @@ function Toggle({
|
||||
aria-checked={checked}
|
||||
onClick={() => onChange(!checked)}
|
||||
className={`relative inline-flex h-5 w-9 flex-shrink-0 rounded-full border-2 border-transparent transition-colors duration-200 ${
|
||||
checked ? "bg-blue-500" : "bg-surface-4"
|
||||
checked ? "bg-blue-600" : "bg-surface-4"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
@@ -394,7 +396,7 @@ function PricingInfoTooltip() {
|
||||
onMouseLeave={() => setOpen(false)}
|
||||
onFocus={() => setOpen(true)}
|
||||
onBlur={() => setOpen(false)}
|
||||
className="inline-flex items-center justify-center rounded-full p-0.5 text-gray-500 hover:text-gray-300 focus:outline-none focus:ring-1 focus:ring-accent/40"
|
||||
className="inline-flex items-center justify-center rounded-full p-0.5 text-fg-muted hover:text-fg-secondary focus:outline-none focus:ring-1 focus:ring-accent/40"
|
||||
>
|
||||
<Info className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@@ -402,32 +404,36 @@ function PricingInfoTooltip() {
|
||||
<div
|
||||
ref={popoverRef}
|
||||
role="tooltip"
|
||||
className="fixed z-50 p-3 bg-[#12121f] border border-[#2a2a4a] rounded-lg shadow-2xl text-[11px] text-gray-300 pointer-events-none"
|
||||
className="fixed z-50 p-3 bg-[#12121f] border border-[#2a2a4a] rounded-lg shadow-2xl text-[11px] text-fg-secondary pointer-events-none"
|
||||
style={{ left: pos.left, top: pos.top, width: 320 }}
|
||||
>
|
||||
<p className="text-xs font-semibold text-gray-100 mb-2">{t("pricing.tooltip.title")}</p>
|
||||
<p className="text-xs font-semibold text-fg-primary mb-2">{t("pricing.tooltip.title")}</p>
|
||||
|
||||
<p className="font-semibold text-gray-200 uppercase tracking-wider text-[9px] mb-1">
|
||||
<p className="font-semibold text-fg-secondary uppercase tracking-wider text-[9px] mb-1">
|
||||
{t("pricing.tooltip.howItWorks")}
|
||||
</p>
|
||||
<p className="text-gray-400 leading-snug mb-2.5">{t("pricing.tooltip.howItWorksBody")}</p>
|
||||
<p className="text-fg-secondary leading-snug mb-2.5">
|
||||
{t("pricing.tooltip.howItWorksBody")}
|
||||
</p>
|
||||
|
||||
<p className="font-semibold text-gray-200 uppercase tracking-wider text-[9px] mb-1">
|
||||
<p className="font-semibold text-fg-secondary uppercase tracking-wider text-[9px] mb-1">
|
||||
{t("pricing.tooltip.patternsTitle")}
|
||||
</p>
|
||||
<p className="text-gray-400 leading-snug mb-2.5">{t("pricing.tooltip.patternsBody")}</p>
|
||||
<p className="text-fg-secondary leading-snug mb-2.5">
|
||||
{t("pricing.tooltip.patternsBody")}
|
||||
</p>
|
||||
|
||||
<p className="font-semibold text-amber-300 uppercase tracking-wider text-[9px] mb-1">
|
||||
<p className="font-semibold text-status-warning uppercase tracking-wider text-[9px] mb-1">
|
||||
{t("pricing.tooltip.manualUpdates")}
|
||||
</p>
|
||||
<p className="text-gray-400 leading-snug mb-2.5">
|
||||
<p className="text-fg-secondary leading-snug mb-2.5">
|
||||
{t("pricing.tooltip.manualUpdatesBody")}
|
||||
</p>
|
||||
|
||||
<p className="font-semibold text-amber-300 uppercase tracking-wider text-[9px] mb-1">
|
||||
<p className="font-semibold text-status-warning uppercase tracking-wider text-[9px] mb-1">
|
||||
{t("pricing.tooltip.apiPricing")}
|
||||
</p>
|
||||
<p className="text-gray-400 leading-snug">{t("pricing.tooltip.apiPricingBody")}</p>
|
||||
<p className="text-fg-secondary leading-snug">{t("pricing.tooltip.apiPricingBody")}</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -866,14 +872,14 @@ export function Settings() {
|
||||
<button
|
||||
onClick={saveEdit}
|
||||
disabled={saving}
|
||||
className="p-1.5 rounded-md text-emerald-400 hover:bg-emerald-500/10 transition-colors disabled:opacity-50"
|
||||
className="p-1.5 rounded-md text-status-success hover:bg-status-success/10 transition-colors disabled:opacity-50"
|
||||
title={t("common:save")}
|
||||
>
|
||||
<Check className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={cancelEdit}
|
||||
className="p-1.5 rounded-md text-gray-400 hover:bg-surface-4 transition-colors"
|
||||
className="p-1.5 rounded-md text-fg-secondary hover:bg-surface-4 transition-colors"
|
||||
title={t("common:cancel")}
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
@@ -909,7 +915,7 @@ export function Settings() {
|
||||
<span className="text-[11px] font-semibold text-violet-300 uppercase tracking-wider">
|
||||
{t("pricing.introRatesTitle")}
|
||||
</span>
|
||||
<span className="text-[11px] text-gray-500">{t("pricing.introRatesHint")}</span>
|
||||
<span className="text-[11px] text-fg-muted">{t("pricing.introRatesHint")}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-end gap-3">
|
||||
{introField("intro_until", "pricing.introUntil", { date: true })}
|
||||
@@ -931,8 +937,8 @@ export function Settings() {
|
||||
<div
|
||||
className={`px-3 py-2 rounded-lg text-xs ${
|
||||
match.isError
|
||||
? "bg-red-500/10 border border-red-500/20 text-red-400"
|
||||
: "bg-emerald-500/10 border border-emerald-500/20 text-emerald-400"
|
||||
? "bg-status-danger/10 border border-status-danger/20 text-status-danger"
|
||||
: "bg-status-success/10 border border-status-success/20 text-status-success"
|
||||
}`}
|
||||
>
|
||||
{match.message}
|
||||
@@ -981,27 +987,27 @@ export function Settings() {
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-lg font-semibold text-gray-100">{t("title")}</h1>
|
||||
<h1 className="text-lg font-semibold text-fg-primary">{t("title")}</h1>
|
||||
{wsConnected ? (
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse-dot" />
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse-dot" />
|
||||
{t("common:live")}
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-gray-400 bg-gray-500/10 border border-gray-500/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-gray-400" />
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-fg-secondary bg-surface-4/10 border border-border-light/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-surface-4" />
|
||||
{t("common:offline")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">{t("subtitle")}</p>
|
||||
<p className="text-xs text-fg-muted">{t("subtitle")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<a
|
||||
href={api.settings.exportData()}
|
||||
download
|
||||
className="inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-md border border-border text-gray-400 hover:text-gray-200 hover:border-gray-500 transition-colors"
|
||||
className="inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-md border border-border text-fg-secondary hover:text-fg-primary hover:border-border-light transition-colors"
|
||||
>
|
||||
<FileDown className="w-3.5 h-3.5" />
|
||||
{t("exportData")}
|
||||
@@ -1015,7 +1021,7 @@ export function Settings() {
|
||||
{/* In-page section navigation - Settings is dense, so this TOC jumps to
|
||||
and scroll-spies each section. */}
|
||||
<nav className="sticky top-0 z-20 -mx-1 !mt-2 px-1 py-2 bg-surface-0/85 backdrop-blur border-b border-border/60 flex items-center gap-1.5">
|
||||
<span className="flex-shrink-0 pl-1 text-[10px] font-semibold uppercase tracking-wider text-gray-500">
|
||||
<span className="flex-shrink-0 pl-1 text-[10px] font-semibold uppercase tracking-wider text-fg-muted">
|
||||
{t("jumpTo", "Jump to")}
|
||||
</span>
|
||||
{tocOverflow.left && (
|
||||
@@ -1023,7 +1029,7 @@ export function Settings() {
|
||||
type="button"
|
||||
onClick={() => scrollTocBy(-180)}
|
||||
aria-label="Scroll left"
|
||||
className="flex-shrink-0 flex items-center justify-center w-6 h-7 rounded-md border border-border text-gray-400 hover:text-gray-200 hover:bg-surface-3 transition-colors"
|
||||
className="flex-shrink-0 flex items-center justify-center w-6 h-7 rounded-md border border-border text-fg-secondary hover:text-fg-primary hover:bg-surface-3 transition-colors"
|
||||
>
|
||||
<ChevronLeft className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@@ -1043,7 +1049,7 @@ export function Settings() {
|
||||
className={`inline-flex items-center gap-1.5 text-xs whitespace-nowrap px-2.5 py-1.5 rounded-lg border transition-colors flex-shrink-0 ${
|
||||
active
|
||||
? "bg-accent/15 border-accent/30 text-accent"
|
||||
: "border-border text-gray-400 hover:text-gray-200 hover:bg-surface-3"
|
||||
: "border-border text-fg-secondary hover:text-fg-primary hover:bg-surface-3"
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-3.5 h-3.5" />
|
||||
@@ -1057,7 +1063,7 @@ export function Settings() {
|
||||
type="button"
|
||||
onClick={() => scrollTocBy(180)}
|
||||
aria-label="Scroll right"
|
||||
className="flex-shrink-0 flex items-center justify-center w-6 h-7 rounded-md border border-border text-gray-400 hover:text-gray-200 hover:bg-surface-3 transition-colors"
|
||||
className="flex-shrink-0 flex items-center justify-center w-6 h-7 rounded-md border border-border text-fg-secondary hover:text-fg-primary hover:bg-surface-3 transition-colors"
|
||||
>
|
||||
<ChevronRight className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@@ -1068,12 +1074,12 @@ export function Settings() {
|
||||
<div className="card p-6">
|
||||
<div className="flex items-center justify-between flex-wrap gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center">
|
||||
<DollarSign className="w-6 h-6 text-emerald-400" />
|
||||
<div className="w-12 h-12 rounded-xl bg-status-success/10 border border-status-success/20 flex items-center justify-center">
|
||||
<DollarSign className="w-6 h-6 text-status-success" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">{t("common:cost.totalEstimatedCost")}</p>
|
||||
<p className="text-2xl font-semibold text-gray-100">
|
||||
<p className="text-sm text-fg-muted">{t("common:cost.totalEstimatedCost")}</p>
|
||||
<p className="text-2xl font-semibold text-fg-primary">
|
||||
<Tip
|
||||
raw={
|
||||
totalCost !== null
|
||||
@@ -1086,7 +1092,7 @@ export function Settings() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right text-xs text-gray-500">
|
||||
<div className="text-right text-xs text-fg-muted">
|
||||
<p>{t("acrossSessions")}</p>
|
||||
<p>{t("basedOnUsage")}</p>
|
||||
</div>
|
||||
@@ -1097,12 +1103,12 @@ export function Settings() {
|
||||
<section id="pricing" className="scroll-mt-24">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 mb-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2">
|
||||
<DollarSign className="w-4 h-4 text-gray-500" />
|
||||
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2">
|
||||
<DollarSign className="w-4 h-4 text-fg-muted" />
|
||||
{t("pricing.title")}
|
||||
<PricingInfoTooltip />
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 mt-0.5">{t("pricing.description")}</p>
|
||||
<p className="text-xs text-fg-muted mt-0.5">{t("pricing.description")}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
@@ -1114,8 +1120,8 @@ export function Settings() {
|
||||
disabled={isEditing || actionLoading !== null}
|
||||
className={`text-xs px-3 py-1.5 rounded-md transition-colors disabled:opacity-50 inline-flex items-center gap-1.5 ${
|
||||
confirmAction === "reset-pricing"
|
||||
? "bg-amber-500/20 text-amber-400 border border-amber-500/30"
|
||||
: "text-gray-400 hover:text-gray-300 hover:bg-surface-4"
|
||||
? "bg-status-warning/20 text-status-warning border border-status-warning/30"
|
||||
: "text-fg-secondary hover:text-fg-primary hover:bg-surface-4"
|
||||
}`}
|
||||
>
|
||||
<RotateCcw className="w-3 h-3" />
|
||||
@@ -1134,7 +1140,7 @@ export function Settings() {
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 px-4 py-2.5 bg-red-500/10 border border-red-500/20 rounded-lg text-sm text-red-400">
|
||||
<div className="mb-4 px-4 py-2.5 bg-status-danger/10 border border-status-danger/20 rounded-lg text-sm text-status-danger">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
@@ -1145,34 +1151,34 @@ export function Settings() {
|
||||
<table className="w-full min-w-[1000px]">
|
||||
<thead>
|
||||
<tr className="border-b border-border text-left">
|
||||
<th className="px-4 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider">
|
||||
{t("pricing.pattern")}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider">
|
||||
<th className="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider">
|
||||
{t("common:cost.model")}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider text-right">
|
||||
<th className="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
|
||||
{t("common:token.input")}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider text-right">
|
||||
<th className="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
|
||||
{t("common:token.output")}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider text-right">
|
||||
<th className="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
|
||||
{t("common:token.cacheRead")}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider text-right">
|
||||
<th className="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
|
||||
{t("pricing.cacheWrite5m")}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider text-right">
|
||||
<th className="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
|
||||
{t("pricing.cacheWrite1h")}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider text-right">
|
||||
<th className="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
|
||||
{t("pricing.fastInput")}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider text-right">
|
||||
<th className="px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider text-right">
|
||||
{t("pricing.fastOutput")}
|
||||
</th>
|
||||
<th className="w-24 px-4 py-3 text-[11px] font-semibold text-gray-500 uppercase tracking-wider">
|
||||
<th className="w-24 px-4 py-3 text-[11px] font-semibold text-fg-muted uppercase tracking-wider">
|
||||
{t("common:actions")}
|
||||
</th>
|
||||
</tr>
|
||||
@@ -1189,10 +1195,10 @@ export function Settings() {
|
||||
key={rule.model_pattern}
|
||||
className="hover:bg-surface-4 transition-colors group"
|
||||
>
|
||||
<td className="px-4 py-3 text-sm font-mono text-gray-300">
|
||||
<td className="px-4 py-3 text-sm font-mono text-fg-secondary">
|
||||
{rule.model_pattern}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-300">
|
||||
<td className="px-4 py-3 text-sm text-fg-secondary">
|
||||
{rule.display_name}
|
||||
{rule.intro_until && (
|
||||
<div className="text-[11px] font-normal text-violet-400/80 mt-0.5">
|
||||
@@ -1204,7 +1210,7 @@ export function Settings() {
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-400 text-right font-mono">
|
||||
<td className="px-4 py-3 text-sm text-fg-secondary text-right font-mono">
|
||||
${rule.input_per_mtok}
|
||||
{rule.intro_until && (
|
||||
<span className="block text-[11px] text-violet-400/80">
|
||||
@@ -1212,7 +1218,7 @@ export function Settings() {
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-400 text-right font-mono">
|
||||
<td className="px-4 py-3 text-sm text-fg-secondary text-right font-mono">
|
||||
${rule.output_per_mtok}
|
||||
{rule.intro_until && (
|
||||
<span className="block text-[11px] text-violet-400/80">
|
||||
@@ -1220,7 +1226,7 @@ export function Settings() {
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-400 text-right font-mono">
|
||||
<td className="px-4 py-3 text-sm text-fg-secondary text-right font-mono">
|
||||
${rule.cache_read_per_mtok}
|
||||
{rule.intro_until && (
|
||||
<span className="block text-[11px] text-violet-400/80">
|
||||
@@ -1228,7 +1234,7 @@ export function Settings() {
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-400 text-right font-mono">
|
||||
<td className="px-4 py-3 text-sm text-fg-secondary text-right font-mono">
|
||||
${rule.cache_write_per_mtok}
|
||||
{rule.intro_until && (
|
||||
<span className="block text-[11px] text-violet-400/80">
|
||||
@@ -1236,7 +1242,7 @@ export function Settings() {
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-400 text-right font-mono">
|
||||
<td className="px-4 py-3 text-sm text-fg-secondary text-right font-mono">
|
||||
${rule.cache_write_1h_per_mtok}
|
||||
{rule.intro_until && (
|
||||
<span className="block text-[11px] text-violet-400/80">
|
||||
@@ -1244,10 +1250,10 @@ export function Settings() {
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-400 text-right font-mono">
|
||||
<td className="px-4 py-3 text-sm text-fg-secondary text-right font-mono">
|
||||
{rule.fast_input_per_mtok ? `$${rule.fast_input_per_mtok}` : "-"}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-gray-400 text-right font-mono">
|
||||
<td className="px-4 py-3 text-sm text-fg-secondary text-right font-mono">
|
||||
{rule.fast_output_per_mtok ? `$${rule.fast_output_per_mtok}` : "-"}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
@@ -1255,7 +1261,7 @@ export function Settings() {
|
||||
<button
|
||||
onClick={() => startEdit(rule)}
|
||||
disabled={isEditing}
|
||||
className="p-1.5 rounded-md text-gray-400 hover:text-blue-400 hover:bg-blue-500/10 transition-colors disabled:opacity-30"
|
||||
className="p-1.5 rounded-md text-fg-secondary hover:text-blue-500 hover:bg-blue-600/10 transition-colors disabled:opacity-30"
|
||||
title={t("common:edit")}
|
||||
>
|
||||
<Pencil className="w-3.5 h-3.5" />
|
||||
@@ -1263,7 +1269,7 @@ export function Settings() {
|
||||
<button
|
||||
onClick={() => deleteRule(rule.model_pattern)}
|
||||
disabled={isEditing}
|
||||
className="p-1.5 rounded-md text-gray-400 hover:text-red-400 hover:bg-red-500/10 transition-colors disabled:opacity-30"
|
||||
className="p-1.5 rounded-md text-fg-secondary hover:text-status-danger hover:bg-status-danger/10 transition-colors disabled:opacity-30"
|
||||
title={t("common:delete")}
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
@@ -1284,7 +1290,7 @@ export function Settings() {
|
||||
</div>
|
||||
|
||||
{lastUpdated && (
|
||||
<p className="text-xs text-gray-600 mt-3">
|
||||
<p className="text-xs text-fg-muted mt-3">
|
||||
{t("pricing.lastUpdated")}
|
||||
{formatTimestamp(lastUpdated)}
|
||||
</p>
|
||||
@@ -1293,21 +1299,21 @@ export function Settings() {
|
||||
|
||||
{/* ─── HOOK CONFIGURATION ─── */}
|
||||
<section id="hooks" className="scroll-mt-24">
|
||||
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2 mb-1">
|
||||
<Plug className="w-4 h-4 text-gray-500" />
|
||||
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
|
||||
<Plug className="w-4 h-4 text-fg-muted" />
|
||||
{t("hooks.title")}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 mb-4">{t("hooks.description")}</p>
|
||||
<p className="text-xs text-fg-muted mb-4">{t("hooks.description")}</p>
|
||||
|
||||
<div className="card p-5 space-y-4">
|
||||
<div className="flex items-center justify-between flex-wrap gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
{sysInfo?.hooks.installed ? (
|
||||
<span className="inline-flex items-center gap-1.5 text-xs font-medium text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 px-2.5 py-1 rounded-full">
|
||||
<span className="inline-flex items-center gap-1.5 text-xs font-medium text-status-success bg-status-success/10 border border-status-success/20 px-2.5 py-1 rounded-full">
|
||||
<CheckCircle className="w-3.5 h-3.5" /> {t("hooks.allInstalled")}
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1.5 text-xs font-medium text-amber-400 bg-amber-500/10 border border-amber-500/20 px-2.5 py-1 rounded-full">
|
||||
<span className="inline-flex items-center gap-1.5 text-xs font-medium text-status-warning bg-status-warning/10 border border-status-warning/20 px-2.5 py-1 rounded-full">
|
||||
<AlertTriangle className="w-3.5 h-3.5" /> {t("hooks.incomplete")}
|
||||
</span>
|
||||
)}
|
||||
@@ -1337,15 +1343,15 @@ export function Settings() {
|
||||
className="flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-md bg-surface-2"
|
||||
>
|
||||
{active ? (
|
||||
<CheckCircle className="w-3 h-3 text-emerald-400 flex-shrink-0" />
|
||||
<CheckCircle className="w-3 h-3 text-status-success flex-shrink-0" />
|
||||
) : (
|
||||
<XCircle className="w-3 h-3 text-red-400 flex-shrink-0" />
|
||||
<XCircle className="w-3 h-3 text-status-danger flex-shrink-0" />
|
||||
)}
|
||||
<span className="text-gray-400 truncate">{hook}</span>
|
||||
<span className="text-fg-secondary truncate">{hook}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-[11px] text-gray-600 font-mono truncate">{sysInfo.hooks.path}</p>
|
||||
<p className="text-[11px] text-fg-muted font-mono truncate">{sysInfo.hooks.path}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -1353,12 +1359,12 @@ export function Settings() {
|
||||
|
||||
{/* ─── CLAUDE HOME ─── */}
|
||||
<section id="claude-home" className="scroll-mt-24">
|
||||
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2 mb-1">
|
||||
<FolderOpen className="w-4 h-4 text-gray-500" />
|
||||
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
|
||||
<FolderOpen className="w-4 h-4 text-fg-muted" />
|
||||
{t("claudeHome.title")}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 mb-1">{t("claudeHome.description")}</p>
|
||||
<p className="text-[11px] text-gray-600 italic mb-4 leading-snug">{t("cursorPathsNote")}</p>
|
||||
<p className="text-xs text-fg-muted mb-1">{t("claudeHome.description")}</p>
|
||||
<p className="text-[11px] text-fg-muted italic mb-4 leading-snug">{t("cursorPathsNote")}</p>
|
||||
|
||||
<div className="card p-5 space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -1369,7 +1375,7 @@ export function Settings() {
|
||||
setClaudeHomeInput(e.target.value);
|
||||
setClaudeHomeError(null);
|
||||
}}
|
||||
className="flex-1 bg-surface-4 border border-surface-3 rounded-lg px-3 py-2 text-sm text-gray-200 font-mono focus:outline-none focus:border-violet-500/50"
|
||||
className="flex-1 bg-surface-4 border border-surface-3 rounded-lg px-3 py-2 text-sm text-fg-secondary font-mono focus:outline-none focus:border-violet-500/50"
|
||||
placeholder={t("claudeHome.placeholder")}
|
||||
/>
|
||||
<button
|
||||
@@ -1380,10 +1386,10 @@ export function Settings() {
|
||||
{claudeHomeSaving ? t("claudeHome.saving") : t("claudeHome.save")}
|
||||
</button>
|
||||
</div>
|
||||
{claudeHomeError && <p className="text-xs text-red-400">{claudeHomeError}</p>}
|
||||
{claudeHomeError && <p className="text-xs text-status-danger">{claudeHomeError}</p>}
|
||||
{claudeHome && (
|
||||
<p className="text-xs text-gray-500">
|
||||
{t("claudeHome.current")} <code className="text-gray-400">{claudeHome}</code>
|
||||
<p className="text-xs text-fg-muted">
|
||||
{t("claudeHome.current")} <code className="text-fg-secondary">{claudeHome}</code>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -1401,13 +1407,13 @@ export function Settings() {
|
||||
|
||||
{/* ─── TABBY COMPANION ─── */}
|
||||
<section id="tabby" className="scroll-mt-24">
|
||||
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2 mb-1">
|
||||
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
|
||||
<span className="text-base leading-none" aria-hidden>
|
||||
🐾
|
||||
</span>
|
||||
{t("tabby.title", "Tabby companion")}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 mb-4">
|
||||
<p className="text-xs text-fg-muted mb-4">
|
||||
{t("tabby.description", "A floating cat that reacts to your live sessions.")}
|
||||
</p>
|
||||
|
||||
@@ -1416,7 +1422,7 @@ export function Settings() {
|
||||
<div
|
||||
className={`w-10 h-10 rounded-xl flex items-center justify-center transition-colors ${
|
||||
tabbyEnabled
|
||||
? "bg-blue-500/10 border border-blue-500/20"
|
||||
? "bg-blue-600/10 border border-blue-600/20"
|
||||
: "bg-surface-2 border border-border"
|
||||
}`}
|
||||
>
|
||||
@@ -1439,11 +1445,11 @@ export function Settings() {
|
||||
|
||||
{/* ─── NOTIFICATIONS ─── */}
|
||||
<section id="notifications" className="scroll-mt-24">
|
||||
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2 mb-1">
|
||||
<Bell className="w-4 h-4 text-gray-500" />
|
||||
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
|
||||
<Bell className="w-4 h-4 text-fg-muted" />
|
||||
{t("notifications.title")}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 mb-4">{t("notifications.description")}</p>
|
||||
<p className="text-xs text-fg-muted mb-4">{t("notifications.description")}</p>
|
||||
|
||||
<div className="card p-5 space-y-5">
|
||||
<div className="flex items-center justify-between flex-wrap gap-3">
|
||||
@@ -1451,14 +1457,14 @@ export function Settings() {
|
||||
<div
|
||||
className={`w-10 h-10 rounded-xl flex items-center justify-center transition-colors ${
|
||||
notifPrefs.enabled
|
||||
? "bg-blue-500/10 border border-blue-500/20"
|
||||
? "bg-blue-600/10 border border-blue-600/20"
|
||||
: "bg-surface-2 border border-border"
|
||||
}`}
|
||||
>
|
||||
{notifPrefs.enabled ? (
|
||||
<BellRing className="w-5 h-5 text-blue-400" />
|
||||
<BellRing className="w-5 h-5 text-blue-500" />
|
||||
) : (
|
||||
<BellOff className="w-5 h-5 text-gray-500" />
|
||||
<BellOff className="w-5 h-5 text-fg-muted" />
|
||||
)}
|
||||
</div>
|
||||
<Toggle
|
||||
@@ -1483,10 +1489,10 @@ export function Settings() {
|
||||
<span
|
||||
className={`inline-flex items-center gap-1.5 text-xs font-medium px-2.5 py-1 rounded-full ${
|
||||
Notification.permission === "granted"
|
||||
? "text-emerald-400 bg-emerald-500/10 border border-emerald-500/20"
|
||||
? "text-status-success bg-status-success/10 border border-status-success/20"
|
||||
: Notification.permission === "denied"
|
||||
? "text-red-400 bg-red-500/10 border border-red-500/20"
|
||||
: "text-amber-400 bg-amber-500/10 border border-amber-500/20"
|
||||
? "text-status-danger bg-status-danger/10 border border-status-danger/20"
|
||||
: "text-status-warning bg-status-warning/10 border border-status-warning/20"
|
||||
}`}
|
||||
>
|
||||
{Notification.permission === "granted" ? (
|
||||
@@ -1507,12 +1513,12 @@ export function Settings() {
|
||||
|
||||
{notifPrefs.enabled && (
|
||||
<div className="space-y-3 pt-4 border-t border-border">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wider font-semibold">
|
||||
<p className="text-xs text-fg-muted uppercase tracking-wider font-semibold">
|
||||
{t("notifications.notifyWhen")}
|
||||
</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
<div className="flex items-center gap-3 bg-surface-2 rounded-lg px-3.5 py-3">
|
||||
<Play className="w-4 h-4 text-emerald-400 flex-shrink-0" />
|
||||
<Play className="w-4 h-4 text-status-success flex-shrink-0" />
|
||||
<Toggle
|
||||
checked={notifPrefs.onNewSession}
|
||||
onChange={(v) => updateNotifPrefs({ onNewSession: v })}
|
||||
@@ -1528,7 +1534,7 @@ export function Settings() {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 bg-surface-2 rounded-lg px-3.5 py-3">
|
||||
<AlertCircle className="w-4 h-4 text-red-400 flex-shrink-0" />
|
||||
<AlertCircle className="w-4 h-4 text-status-danger flex-shrink-0" />
|
||||
<Toggle
|
||||
checked={notifPrefs.onSessionError}
|
||||
onChange={(v) => updateNotifPrefs({ onSessionError: v })}
|
||||
@@ -1536,7 +1542,7 @@ export function Settings() {
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 bg-surface-2 rounded-lg px-3.5 py-3">
|
||||
<GitBranch className="w-4 h-4 text-blue-400 flex-shrink-0" />
|
||||
<GitBranch className="w-4 h-4 text-blue-500 flex-shrink-0" />
|
||||
<Toggle
|
||||
checked={notifPrefs.onSubagentSpawn}
|
||||
onChange={(v) => updateNotifPrefs({ onSubagentSpawn: v })}
|
||||
@@ -1559,7 +1565,7 @@ export function Settings() {
|
||||
}),
|
||||
});
|
||||
}}
|
||||
className="inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-md text-gray-400 hover:text-gray-200 hover:bg-surface-4 border border-border transition-colors"
|
||||
className="inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-md text-fg-secondary hover:text-fg-primary hover:bg-surface-4 border border-border transition-colors"
|
||||
>
|
||||
<Zap className="w-3 h-3" />
|
||||
{t("notifications.sendTest")}
|
||||
@@ -1569,7 +1575,7 @@ export function Settings() {
|
||||
)}
|
||||
|
||||
{!notifPrefs.enabled && (
|
||||
<div className="flex items-center gap-2 text-xs text-gray-500">
|
||||
<div className="flex items-center gap-2 text-xs text-fg-muted">
|
||||
<BellOff className="w-3.5 h-3.5" />
|
||||
{t("notifications.disabledInfo")}
|
||||
</div>
|
||||
@@ -1579,30 +1585,30 @@ export function Settings() {
|
||||
|
||||
{/* ─── ALERTS ─── */}
|
||||
<section id="alerts" className="scroll-mt-24">
|
||||
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2 mb-1">
|
||||
<BellRing className="w-4 h-4 text-gray-500" />
|
||||
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
|
||||
<BellRing className="w-4 h-4 text-fg-muted" />
|
||||
{t("alertsHub.title")}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 mb-4">{t("alertsHub.description")}</p>
|
||||
<p className="text-xs text-fg-muted mb-4">{t("alertsHub.description")}</p>
|
||||
<AlertsNotifications />
|
||||
</section>
|
||||
|
||||
{/* ─── DATA MANAGEMENT ─── */}
|
||||
<section id="data" className="scroll-mt-24">
|
||||
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2 mb-1">
|
||||
<Database className="w-4 h-4 text-gray-500" />
|
||||
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
|
||||
<Database className="w-4 h-4 text-fg-muted" />
|
||||
{t("data.title")}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 mb-4">{t("data.description")}</p>
|
||||
<p className="text-xs text-fg-muted mb-4">{t("data.description")}</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="card p-5 space-y-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<p className="text-xs text-gray-500 uppercase tracking-wider font-semibold flex-shrink-0">
|
||||
<p className="text-xs text-fg-muted uppercase tracking-wider font-semibold flex-shrink-0">
|
||||
{t("data.dbOverview")}
|
||||
</p>
|
||||
{sysInfo && (
|
||||
<div className="flex items-center gap-1.5 text-[11px] text-gray-600 font-mono bg-surface-2 px-2.5 py-1 rounded-md min-w-0">
|
||||
<div className="flex items-center gap-1.5 text-[11px] text-fg-muted font-mono bg-surface-2 px-2.5 py-1 rounded-md min-w-0">
|
||||
<HardDrive className="w-3 h-3 flex-shrink-0" />
|
||||
<span className="truncate">{sysInfo.db.path}</span>
|
||||
</div>
|
||||
@@ -1613,10 +1619,10 @@ export function Settings() {
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-2">
|
||||
{(() => {
|
||||
const tableIcons: Record<string, React.ReactNode> = {
|
||||
sessions: <Layers className="w-4 h-4 text-blue-400" />,
|
||||
agents: <Users className="w-4 h-4 text-emerald-400" />,
|
||||
sessions: <Layers className="w-4 h-4 text-blue-500" />,
|
||||
agents: <Users className="w-4 h-4 text-status-success" />,
|
||||
events: <Activity className="w-4 h-4 text-violet-400" />,
|
||||
token_usage: <Coins className="w-4 h-4 text-amber-400" />,
|
||||
token_usage: <Coins className="w-4 h-4 text-status-warning" />,
|
||||
model_pricing: <BarChart3 className="w-4 h-4 text-cyan-400" />,
|
||||
};
|
||||
const tableLabels: Record<string, string> = {
|
||||
@@ -1627,24 +1633,24 @@ export function Settings() {
|
||||
model_pricing: t("tables.pricingRules"),
|
||||
};
|
||||
const tableColors: Record<string, string> = {
|
||||
sessions: "border-blue-500/20",
|
||||
agents: "border-emerald-500/20",
|
||||
sessions: "border-blue-600/20",
|
||||
agents: "border-status-success/20",
|
||||
events: "border-violet-500/20",
|
||||
token_usage: "border-amber-500/20",
|
||||
token_usage: "border-status-warning/20",
|
||||
model_pricing: "border-cyan-500/20",
|
||||
};
|
||||
return Object.entries(sysInfo.db.counts).map(([table, count]) => (
|
||||
<div
|
||||
key={table}
|
||||
className={`bg-surface-2 rounded-lg px-3 py-3 border-l-2 ${tableColors[table] || "border-gray-500/20"}`}
|
||||
className={`bg-surface-2 rounded-lg px-3 py-3 border-l-2 ${tableColors[table] || "border-border-light/20"}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
{tableIcons[table] || <Database className="w-4 h-4 text-gray-500" />}
|
||||
<p className="text-[11px] text-gray-500 uppercase tracking-wider">
|
||||
{tableIcons[table] || <Database className="w-4 h-4 text-fg-muted" />}
|
||||
<p className="text-[11px] text-fg-muted uppercase tracking-wider">
|
||||
{tableLabels[table] || table.replace(/_/g, " ")}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xl font-semibold text-gray-200">
|
||||
<p className="text-xl font-semibold text-fg-secondary">
|
||||
<Tip raw={count.toLocaleString()}>{fmt(count)}</Tip>
|
||||
</p>
|
||||
</div>
|
||||
@@ -1653,35 +1659,37 @@ export function Settings() {
|
||||
<div className="bg-surface-2 rounded-lg px-3 py-3 border-l-2 border-indigo-500/20">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<HardDrive className="w-4 h-4 text-indigo-400" />
|
||||
<p className="text-[11px] text-gray-500 uppercase tracking-wider">
|
||||
<p className="text-[11px] text-fg-muted uppercase tracking-wider">
|
||||
{t("data.dbSize")}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xl font-semibold text-gray-200">
|
||||
<p className="text-xl font-semibold text-fg-secondary">
|
||||
{formatBytes(sysInfo.db.size)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-gray-500">{t("data.loadingDb")}</p>
|
||||
<p className="text-xs text-fg-muted">{t("data.loadingDb")}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Session Cleanup */}
|
||||
<div className="card p-5 space-y-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-lg bg-amber-500/10 border border-amber-500/20 flex items-center justify-center">
|
||||
<Eraser className="w-4 h-4 text-amber-400" />
|
||||
<div className="w-9 h-9 rounded-lg bg-status-warning/10 border border-status-warning/20 flex items-center justify-center">
|
||||
<Eraser className="w-4 h-4 text-status-warning" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-300">{t("data.sessionCleanup")}</p>
|
||||
<p className="text-xs text-gray-500">{t("data.cleanupDesc")}</p>
|
||||
<p className="text-sm font-medium text-fg-secondary">{t("data.sessionCleanup")}</p>
|
||||
<p className="text-xs text-fg-muted">{t("data.cleanupDesc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div className="bg-surface-2 rounded-lg px-4 py-3">
|
||||
<label className="text-xs text-gray-400 block mb-2">{t("data.abandonAfter")}</label>
|
||||
<label className="text-xs text-fg-secondary block mb-2">
|
||||
{t("data.abandonAfter")}
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
@@ -1690,11 +1698,13 @@ export function Settings() {
|
||||
onChange={(e) => setAbandonHours(e.target.value)}
|
||||
className="input w-20 text-sm text-right font-mono"
|
||||
/>
|
||||
<span className="text-xs text-gray-500">{t("common:hours")}</span>
|
||||
<span className="text-xs text-fg-muted">{t("common:hours")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-surface-2 rounded-lg px-4 py-3">
|
||||
<label className="text-xs text-gray-400 block mb-2">{t("data.purgeAfter")}</label>
|
||||
<label className="text-xs text-fg-secondary block mb-2">
|
||||
{t("data.purgeAfter")}
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
@@ -1703,7 +1713,7 @@ export function Settings() {
|
||||
onChange={(e) => setPurgeDays(e.target.value)}
|
||||
className="input w-20 text-sm text-right font-mono"
|
||||
/>
|
||||
<span className="text-xs text-gray-500">{t("common:days")}</span>
|
||||
<span className="text-xs text-fg-muted">{t("common:days")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1715,8 +1725,8 @@ export function Settings() {
|
||||
disabled={actionLoading !== null}
|
||||
className={`text-xs px-3 py-1.5 rounded-md transition-colors disabled:opacity-50 ${
|
||||
confirmAction === "cleanup"
|
||||
? "bg-amber-500/20 text-amber-400 border border-amber-500/30"
|
||||
: "text-gray-400 hover:text-gray-300 hover:bg-surface-4 border border-border"
|
||||
? "bg-status-warning/20 text-status-warning border border-status-warning/30"
|
||||
: "text-fg-secondary hover:text-fg-primary hover:bg-surface-4 border border-border"
|
||||
}`}
|
||||
>
|
||||
{actionLoading === "cleanup" ? (
|
||||
@@ -1731,25 +1741,25 @@ export function Settings() {
|
||||
</div>
|
||||
|
||||
{/* Danger zone */}
|
||||
<div className="card p-5 space-y-4 border-red-500/10">
|
||||
<div className="card p-5 space-y-4 border-status-danger/10">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-lg bg-red-500/10 border border-red-500/20 flex items-center justify-center">
|
||||
<AlertTriangle className="w-4 h-4 text-red-400" />
|
||||
<div className="w-9 h-9 rounded-lg bg-status-danger/10 border border-status-danger/20 flex items-center justify-center">
|
||||
<AlertTriangle className="w-4 h-4 text-status-danger" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-red-400">{t("danger.title")}</p>
|
||||
<p className="text-xs text-gray-500">{t("danger.description")}</p>
|
||||
<p className="text-sm font-medium text-status-danger">{t("danger.title")}</p>
|
||||
<p className="text-xs text-fg-muted">{t("danger.description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{confirmAction === "clear" ? (
|
||||
<div className="bg-red-500/5 border border-red-500/20 rounded-lg px-4 py-3 flex items-center justify-between flex-wrap gap-3">
|
||||
<span className="text-xs text-amber-400">{t("danger.warning")}</span>
|
||||
<div className="bg-status-danger/5 border border-status-danger/20 rounded-lg px-4 py-3 flex items-center justify-between flex-wrap gap-3">
|
||||
<span className="text-xs text-status-warning">{t("danger.warning")}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleClearData}
|
||||
disabled={actionLoading !== null}
|
||||
className="text-xs px-3 py-1.5 rounded-md bg-red-500/20 text-red-400 border border-red-500/30 hover:bg-red-500/30 transition-colors disabled:opacity-50"
|
||||
className="text-xs px-3 py-1.5 rounded-md bg-status-danger/20 text-status-danger border border-status-danger/30 hover:bg-status-danger/30 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{actionLoading === "clear" ? (
|
||||
<RefreshCw className="w-3.5 h-3.5 animate-spin inline mr-1" />
|
||||
@@ -1758,7 +1768,7 @@ export function Settings() {
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConfirmAction(null)}
|
||||
className="text-xs px-3 py-1.5 rounded-md text-gray-400 hover:bg-surface-4 transition-colors"
|
||||
className="text-xs px-3 py-1.5 rounded-md text-fg-secondary hover:bg-surface-4 transition-colors"
|
||||
>
|
||||
{t("common:cancel")}
|
||||
</button>
|
||||
@@ -1768,7 +1778,7 @@ export function Settings() {
|
||||
<button
|
||||
onClick={() => setConfirmAction("clear")}
|
||||
disabled={actionLoading !== null}
|
||||
className="text-xs px-3 py-1.5 rounded-md text-red-400 hover:bg-red-500/10 border border-red-500/20 transition-colors disabled:opacity-50"
|
||||
className="text-xs px-3 py-1.5 rounded-md text-status-danger hover:bg-status-danger/10 border border-status-danger/20 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<AlertTriangle className="w-3.5 h-3.5 inline mr-1" />
|
||||
{t("danger.clearAllData")}
|
||||
@@ -1782,11 +1792,11 @@ export function Settings() {
|
||||
|
||||
{/* ─── ABOUT ─── */}
|
||||
<section id="about" className="scroll-mt-24">
|
||||
<h3 className="text-sm font-medium text-gray-300 flex items-center gap-2 mb-1">
|
||||
<Server className="w-4 h-4 text-gray-500" />
|
||||
<h3 className="text-sm font-medium text-fg-secondary flex items-center gap-2 mb-1">
|
||||
<Server className="w-4 h-4 text-fg-muted" />
|
||||
{t("about.title")}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 mb-4">{t("about.description")}</p>
|
||||
<p className="text-xs text-fg-muted mb-4">{t("about.description")}</p>
|
||||
|
||||
{sysInfo ? (
|
||||
<div className="card p-5">
|
||||
@@ -1794,65 +1804,65 @@ export function Settings() {
|
||||
<div className="bg-surface-2 rounded-lg px-4 py-3">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<Server className="w-4 h-4 text-indigo-400" />
|
||||
<p className="text-[11px] text-gray-500 uppercase tracking-wider">
|
||||
<p className="text-[11px] text-fg-muted uppercase tracking-wider">
|
||||
{t("about.release")}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-gray-200 font-mono">
|
||||
<p className="text-sm font-semibold text-fg-secondary font-mono">
|
||||
v{sysInfo.server.version}
|
||||
</p>
|
||||
{sysInfo.server.version !== __APP_VERSION__ && (
|
||||
<p className="text-[10px] text-amber-400/90 mt-1">
|
||||
<p className="text-[10px] text-status-warning/90 mt-1">
|
||||
{t("about.uiBuild", { version: __APP_VERSION__ })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="bg-surface-2 rounded-lg px-4 py-3">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<Clock className="w-4 h-4 text-blue-400" />
|
||||
<p className="text-[11px] text-gray-500 uppercase tracking-wider">
|
||||
<Clock className="w-4 h-4 text-blue-500" />
|
||||
<p className="text-[11px] text-fg-muted uppercase tracking-wider">
|
||||
{t("about.uptime")}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-gray-200">
|
||||
<p className="text-sm font-semibold text-fg-secondary">
|
||||
{formatUptime(sysInfo.server.uptime)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-surface-2 rounded-lg px-4 py-3">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<Cpu className="w-4 h-4 text-emerald-400" />
|
||||
<p className="text-[11px] text-gray-500 uppercase tracking-wider">
|
||||
<Cpu className="w-4 h-4 text-status-success" />
|
||||
<p className="text-[11px] text-fg-muted uppercase tracking-wider">
|
||||
{t("about.nodejs")}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-gray-200 font-mono">
|
||||
<p className="text-sm font-semibold text-fg-secondary font-mono">
|
||||
{sysInfo.server.node_version}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-surface-2 rounded-lg px-4 py-3">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<Globe className="w-4 h-4 text-violet-400" />
|
||||
<p className="text-[11px] text-gray-500 uppercase tracking-wider">
|
||||
<p className="text-[11px] text-fg-muted uppercase tracking-wider">
|
||||
{t("about.platform")}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-gray-200">{sysInfo.server.platform}</p>
|
||||
<p className="text-sm font-semibold text-fg-secondary">{sysInfo.server.platform}</p>
|
||||
</div>
|
||||
<div className="bg-surface-2 rounded-lg px-4 py-3">
|
||||
<div className="flex items-center gap-2 mb-1.5">
|
||||
<Wifi className="w-4 h-4 text-amber-400" />
|
||||
<p className="text-[11px] text-gray-500 uppercase tracking-wider">
|
||||
<Wifi className="w-4 h-4 text-status-warning" />
|
||||
<p className="text-[11px] text-fg-muted uppercase tracking-wider">
|
||||
{t("about.wsClients")}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-gray-200">
|
||||
<p className="text-sm font-semibold text-fg-secondary">
|
||||
{sysInfo.server.ws_connections}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-gray-500">{t("about.loadingInfo")}</p>
|
||||
<p className="text-xs text-fg-muted">{t("about.loadingInfo")}</p>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -182,8 +182,8 @@ export function Workflows() {
|
||||
lastUpdated={null}
|
||||
/>
|
||||
<div className="card flex flex-col items-center justify-center py-16 gap-4">
|
||||
<AlertCircle className="w-10 h-10 text-red-400" />
|
||||
<p className="text-red-400 text-sm">{error}</p>
|
||||
<AlertCircle className="w-10 h-10 text-status-danger" />
|
||||
<p className="text-status-danger text-sm">{error}</p>
|
||||
<button onClick={handleRefresh} className="btn-primary text-sm">
|
||||
{t("common:retry")}
|
||||
</button>
|
||||
@@ -211,11 +211,11 @@ export function Workflows() {
|
||||
{/* Workflow-tool runs (issue #167) - fleets ingested from on-disk journals */}
|
||||
<div className="card p-4 space-y-3">
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-gray-200 flex items-center gap-2">
|
||||
<h2 className="text-sm font-semibold text-fg-secondary flex items-center gap-2">
|
||||
<Workflow className="w-4 h-4 text-violet-400" />
|
||||
{t("runs.title")}
|
||||
</h2>
|
||||
<p className="text-xs text-gray-500 mt-0.5">{t("runs.subtitle")}</p>
|
||||
<p className="text-xs text-fg-muted mt-0.5">{t("runs.subtitle")}</p>
|
||||
</div>
|
||||
<WorkflowRunsPanel statusFilter={statusFilter} />
|
||||
</div>
|
||||
@@ -234,13 +234,13 @@ export function Workflows() {
|
||||
/>
|
||||
{selectedNode && (
|
||||
<div className="mt-3 flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500">{t("filteredBy")}</span>
|
||||
<span className="text-xs text-fg-muted">{t("filteredBy")}</span>
|
||||
<span className="badge bg-accent/15 text-accent border border-accent/20 text-xs">
|
||||
{selectedNode}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setSelectedNode(null)}
|
||||
className="text-xs text-gray-500 hover:text-gray-300 underline"
|
||||
className="text-xs text-fg-muted hover:text-fg-secondary underline"
|
||||
>
|
||||
{t("clearFilter")}
|
||||
</button>
|
||||
@@ -384,14 +384,14 @@ function Section({
|
||||
<span className="w-5 h-5 rounded-md bg-accent/15 text-accent text-[11px] font-bold flex items-center justify-center flex-shrink-0">
|
||||
{number}
|
||||
</span>
|
||||
<h2 className="text-sm font-semibold text-gray-100">{title}</h2>
|
||||
<h2 className="text-sm font-semibold text-fg-primary">{title}</h2>
|
||||
<ChartInfoPopover infoKey={infoKey} title={title} />
|
||||
</div>
|
||||
{/* Quick descriptor; the full explanation lives in the ⓘ popover, so we
|
||||
keep this to a single clamped line (ellipsis + hover title) so a long
|
||||
translation never wraps and unbalances the header row. */}
|
||||
<span
|
||||
className="hidden lg:block flex-shrink-0 max-w-[20rem] xl:max-w-sm truncate text-right text-[11px] text-gray-600"
|
||||
className="hidden lg:block flex-shrink-0 max-w-[20rem] xl:max-w-sm truncate text-right text-[11px] text-fg-muted"
|
||||
title={subtitle}
|
||||
>
|
||||
{subtitle}
|
||||
@@ -477,7 +477,7 @@ function ChartInfoPopover({ infoKey, title }: { infoKey: string; title: string }
|
||||
onMouseLeave={() => setOpen(false)}
|
||||
onFocus={() => setOpen(true)}
|
||||
onBlur={() => setOpen(false)}
|
||||
className="flex items-center justify-center rounded-full p-0.5 -m-0.5 text-gray-600 hover:text-gray-400 transition-colors focus:outline-none focus:ring-1 focus:ring-accent/40"
|
||||
className="flex items-center justify-center rounded-full p-0.5 -m-0.5 text-fg-muted hover:text-fg-secondary transition-colors focus:outline-none focus:ring-1 focus:ring-accent/40"
|
||||
>
|
||||
<Info className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
@@ -485,27 +485,29 @@ function ChartInfoPopover({ infoKey, title }: { infoKey: string; title: string }
|
||||
<div
|
||||
ref={popoverRef}
|
||||
role="tooltip"
|
||||
className="fixed z-50 p-3.5 bg-[#12121f] border border-[#2a2a4a] rounded-lg shadow-2xl text-[11px] text-gray-300 pointer-events-none"
|
||||
className="fixed z-50 p-3.5 bg-[#12121f] border border-[#2a2a4a] rounded-lg shadow-2xl text-[11px] text-fg-secondary pointer-events-none"
|
||||
style={{ left: coords.left, top: coords.top, width: POPOVER_W }}
|
||||
>
|
||||
<p className="text-xs font-semibold text-gray-100 mb-2.5 pb-2 border-b border-[#2a2a4a]">
|
||||
<p className="text-xs font-semibold text-fg-primary mb-2.5 pb-2 border-b border-[#2a2a4a]">
|
||||
{title}
|
||||
</p>
|
||||
|
||||
<p className="font-semibold text-gray-200 uppercase tracking-wider text-[9px] mb-1">
|
||||
<p className="font-semibold text-fg-secondary uppercase tracking-wider text-[9px] mb-1">
|
||||
{t("chartInfo.labels.what")}
|
||||
</p>
|
||||
<p className="text-gray-400 leading-snug mb-2.5">{t(`chartInfo.${infoKey}.what`)}</p>
|
||||
<p className="text-fg-secondary leading-snug mb-2.5">{t(`chartInfo.${infoKey}.what`)}</p>
|
||||
|
||||
<p className="font-semibold text-gray-200 uppercase tracking-wider text-[9px] mb-1">
|
||||
<p className="font-semibold text-fg-secondary uppercase tracking-wider text-[9px] mb-1">
|
||||
{t("chartInfo.labels.howToRead")}
|
||||
</p>
|
||||
<p className="text-gray-400 leading-snug mb-2.5">{t(`chartInfo.${infoKey}.howToRead`)}</p>
|
||||
<p className="text-fg-secondary leading-snug mb-2.5">
|
||||
{t(`chartInfo.${infoKey}.howToRead`)}
|
||||
</p>
|
||||
|
||||
<p className="font-semibold text-gray-200 uppercase tracking-wider text-[9px] mb-1">
|
||||
<p className="font-semibold text-fg-secondary uppercase tracking-wider text-[9px] mb-1">
|
||||
{t("chartInfo.labels.why")}
|
||||
</p>
|
||||
<p className="text-gray-400 leading-snug">{t(`chartInfo.${infoKey}.why`)}</p>
|
||||
<p className="text-fg-secondary leading-snug">{t(`chartInfo.${infoKey}.why`)}</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -542,20 +544,20 @@ function PageHeader({
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-lg font-semibold text-gray-100">{t("title")}</h1>
|
||||
<h1 className="text-lg font-semibold text-fg-primary">{t("title")}</h1>
|
||||
{wsConnected ? (
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse-dot" />
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse-dot" />
|
||||
{t("common:live")}
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-gray-400 bg-gray-500/10 border border-gray-500/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-gray-400" />
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-fg-secondary bg-surface-4/10 border border-border-light/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-surface-4" />
|
||||
{t("common:offline")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">{t("subtitle")}</p>
|
||||
<p className="text-xs text-fg-muted">{t("subtitle")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -569,7 +571,7 @@ function PageHeader({
|
||||
className={`px-3 py-1.5 rounded-md text-xs font-medium transition-colors ${
|
||||
statusFilter === f.value
|
||||
? "bg-accent/15 text-accent"
|
||||
: "text-gray-500 hover:text-gray-300"
|
||||
: "text-fg-muted hover:text-fg-secondary"
|
||||
}`}
|
||||
>
|
||||
{f.label}
|
||||
@@ -580,21 +582,21 @@ function PageHeader({
|
||||
{/* Actions */}
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
className="p-2 rounded-lg text-gray-500 hover:text-gray-300 hover:bg-surface-3 transition-colors"
|
||||
className="p-2 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-3 transition-colors"
|
||||
title={t("refreshData")}
|
||||
>
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={onExport}
|
||||
className="p-2 rounded-lg text-gray-500 hover:text-gray-300 hover:bg-surface-3 transition-colors"
|
||||
className="p-2 rounded-lg text-fg-muted hover:text-fg-secondary hover:bg-surface-3 transition-colors"
|
||||
title={t("exportJson")}
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
{lastUpdated && (
|
||||
<span className="text-[10px] text-gray-600 ml-1">
|
||||
<span className="text-[10px] text-fg-muted ml-1">
|
||||
{t("common:updated")}
|
||||
{lastUpdated.toLocaleTimeString()}
|
||||
</span>
|
||||
|
||||
+130
-116
@@ -267,6 +267,19 @@ export function Workspace() {
|
||||
});
|
||||
}, [refreshLanes]);
|
||||
|
||||
// A reconnect (e.g. the dashboard server restarting) resumes the WS but does
|
||||
// not replay missed lane_update diffs, so a lane whose fields changed while
|
||||
// disconnected — status, needs_action — would keep showing its pre-restart
|
||||
// badges forever with no further server-side change to broadcast. Refetch
|
||||
// the full list whenever the socket comes back up.
|
||||
useEffect(
|
||||
() =>
|
||||
eventBus.onConnection((isConnected) => {
|
||||
if (isConnected) void refreshLanes();
|
||||
}),
|
||||
[refreshLanes]
|
||||
);
|
||||
|
||||
const refreshList = useCallback(() => {
|
||||
api.run
|
||||
.list()
|
||||
@@ -782,96 +795,95 @@ export function Workspace() {
|
||||
|
||||
const consoleSection = (
|
||||
<>
|
||||
{/* Always attached under the pipeline - no header, no collapse. The
|
||||
{/* Always attached under the pipeline - no header, no collapse. The
|
||||
pipeline panel above already names the lane; unmounting RunConsole
|
||||
would throw away a live run's rendered history and scroll
|
||||
position, so this stays mounted for the page's whole life. */}
|
||||
<div data-testid="console-body" className="flex min-h-0 flex-1 flex-col gap-5">
|
||||
<Header
|
||||
activeRuns={activeRuns}
|
||||
currentHandleId={handle?.id || null}
|
||||
onAttach={attachToRun}
|
||||
wsConnected={wsConnected}
|
||||
runHistory={runHistory}
|
||||
onResumeFromHistory={onResumeFromHistory}
|
||||
onViewFromHistory={onViewFromHistory}
|
||||
onRefresh={refreshList}
|
||||
/>
|
||||
<div data-testid="console-body" className="flex min-h-0 flex-1 flex-col gap-5">
|
||||
<Header
|
||||
activeRuns={activeRuns}
|
||||
currentHandleId={handle?.id || null}
|
||||
onAttach={attachToRun}
|
||||
wsConnected={wsConnected}
|
||||
runHistory={runHistory}
|
||||
onResumeFromHistory={onResumeFromHistory}
|
||||
onViewFromHistory={onViewFromHistory}
|
||||
onRefresh={refreshList}
|
||||
/>
|
||||
|
||||
{binaryStatus && !binaryStatus.found && (
|
||||
<div className="rounded-lg border border-red-500/40 bg-red-500/10 px-4 py-3 text-sm text-red-200 flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
||||
<span>{t("binary.missing")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg border border-red-500/40 bg-red-500/10 px-4 py-3 text-sm text-red-200 flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
||||
<span className="flex-1 break-all">{error}</span>
|
||||
<button
|
||||
onClick={() => setError(null)}
|
||||
className="text-red-200/70 hover:text-red-100 p-0.5"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{!handle ? (
|
||||
// Config card uses normal page flow - page scrolls if needed.
|
||||
<RunSetup
|
||||
mode={mode}
|
||||
onModeChange={(m) => {
|
||||
setMode(m);
|
||||
// Headless can't resume - clearing keeps the UI honest if the
|
||||
// user had a session pinned and then switched mode.
|
||||
if (m === "headless") setResumeSession(null);
|
||||
}}
|
||||
prompt={prompt}
|
||||
onPromptChange={setPrompt}
|
||||
cwd={cwd}
|
||||
onCwdChange={setCwd}
|
||||
cwdSuggestions={cwdSuggestions}
|
||||
model={model}
|
||||
onModelChange={setModel}
|
||||
permissionMode={permissionMode}
|
||||
onPermissionModeChange={setPermissionMode}
|
||||
effort={effort}
|
||||
onEffortChange={setEffort}
|
||||
binaryFound={binaryStatus?.found ?? true}
|
||||
busy={busy === "start"}
|
||||
onStart={start}
|
||||
activeRuns={activeRuns}
|
||||
laneCwd={currentLane?.cwd}
|
||||
resumeSession={resumeSession}
|
||||
onResumeSessionChange={setResumeSession}
|
||||
slashCommands={slashCommands}
|
||||
runHistory={runHistory}
|
||||
onResumeFromHistory={onResumeFromHistory}
|
||||
/>
|
||||
) : (
|
||||
// Run session is wrapped in a flex container so its inner chat panel
|
||||
// can take all remaining viewport height; long chats scroll inside.
|
||||
<div className="flex-1 min-h-0 flex flex-col">
|
||||
<RunConsole
|
||||
handle={handle}
|
||||
envelopes={displayEnvelopes}
|
||||
mode={handle.mode}
|
||||
isLive={isLive}
|
||||
hasFinished={hasFinished}
|
||||
followUp={followUp}
|
||||
onFollowUpChange={setFollowUp}
|
||||
busy={busy}
|
||||
onSend={send}
|
||||
onStop={stop}
|
||||
onNewRun={newRun}
|
||||
slashCommands={slashCommands}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{binaryStatus && !binaryStatus.found && (
|
||||
<div className="rounded-lg border border-status-danger/40 bg-status-danger/10 px-4 py-3 text-sm text-status-danger flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
||||
<span>{t("binary.missing")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg border border-status-danger/40 bg-status-danger/10 px-4 py-3 text-sm text-status-danger flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
||||
<span className="flex-1 break-all">{error}</span>
|
||||
<button
|
||||
onClick={() => setError(null)}
|
||||
className="text-status-danger/70 hover:text-status-danger p-0.5"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!handle ? (
|
||||
// Config card uses normal page flow - page scrolls if needed.
|
||||
<RunSetup
|
||||
mode={mode}
|
||||
onModeChange={(m) => {
|
||||
setMode(m);
|
||||
// Headless can't resume - clearing keeps the UI honest if the
|
||||
// user had a session pinned and then switched mode.
|
||||
if (m === "headless") setResumeSession(null);
|
||||
}}
|
||||
prompt={prompt}
|
||||
onPromptChange={setPrompt}
|
||||
cwd={cwd}
|
||||
onCwdChange={setCwd}
|
||||
cwdSuggestions={cwdSuggestions}
|
||||
model={model}
|
||||
onModelChange={setModel}
|
||||
permissionMode={permissionMode}
|
||||
onPermissionModeChange={setPermissionMode}
|
||||
effort={effort}
|
||||
onEffortChange={setEffort}
|
||||
binaryFound={binaryStatus?.found ?? true}
|
||||
busy={busy === "start"}
|
||||
onStart={start}
|
||||
activeRuns={activeRuns}
|
||||
laneCwd={currentLane?.cwd}
|
||||
resumeSession={resumeSession}
|
||||
onResumeSessionChange={setResumeSession}
|
||||
slashCommands={slashCommands}
|
||||
runHistory={runHistory}
|
||||
onResumeFromHistory={onResumeFromHistory}
|
||||
/>
|
||||
) : (
|
||||
// Run session is wrapped in a flex container so its inner chat panel
|
||||
// can take all remaining viewport height; long chats scroll inside.
|
||||
<div className="flex-1 min-h-0 flex flex-col">
|
||||
<RunConsole
|
||||
handle={handle}
|
||||
envelopes={displayEnvelopes}
|
||||
mode={handle.mode}
|
||||
isLive={isLive}
|
||||
hasFinished={hasFinished}
|
||||
followUp={followUp}
|
||||
onFollowUpChange={setFollowUp}
|
||||
busy={busy}
|
||||
onSend={send}
|
||||
onStop={stop}
|
||||
onNewRun={newRun}
|
||||
slashCommands={slashCommands}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -885,27 +897,27 @@ export function Workspace() {
|
||||
>
|
||||
{/* Page header: what this screen is, and the four numbers that say
|
||||
whether anything needs a human right now. */}
|
||||
<div className="flex flex-wrap items-center gap-3 border-b border-neutral-800 pb-3">
|
||||
<h2 className="text-base font-semibold tracking-tight text-neutral-100">
|
||||
<div className="flex flex-wrap items-center gap-3 border-b border-border pb-3">
|
||||
<h2 className="text-base font-semibold tracking-tight text-fg-primary">
|
||||
{tLanes("title")}
|
||||
</h2>
|
||||
<div className="flex flex-wrap items-center gap-1.5 text-xs">
|
||||
<span
|
||||
data-testid="count-total"
|
||||
className="rounded-full bg-neutral-800 px-2.5 py-0.5 text-neutral-300"
|
||||
className="rounded-full bg-surface-2 px-2.5 py-0.5 text-fg-secondary"
|
||||
>
|
||||
{counts.total} {tLanes("countTotal")}
|
||||
</span>
|
||||
<span
|
||||
data-testid="count-running"
|
||||
className="rounded-full bg-blue-500/20 px-2.5 py-0.5 text-blue-300"
|
||||
className="rounded-full bg-blue-600/20 px-2.5 py-0.5 text-blue-400"
|
||||
>
|
||||
{counts.running} {tLanes("countRunning")}
|
||||
</span>
|
||||
{counts.needs_you > 0 && (
|
||||
<span
|
||||
data-testid="count-needs-you"
|
||||
className="rounded-full bg-amber-500/20 px-2.5 py-0.5 text-amber-300"
|
||||
className="rounded-full bg-status-warning/20 px-2.5 py-0.5 text-status-warning"
|
||||
>
|
||||
{counts.needs_you} {tLanes("countNeedsYou")}
|
||||
</span>
|
||||
@@ -913,7 +925,7 @@ export function Workspace() {
|
||||
{counts.dead > 0 && (
|
||||
<span
|
||||
data-testid="count-dead"
|
||||
className="rounded-full bg-red-500/20 px-2.5 py-0.5 text-red-300"
|
||||
className="rounded-full bg-status-danger/20 px-2.5 py-0.5 text-status-danger"
|
||||
>
|
||||
{counts.dead} {tLanes("countDead")}
|
||||
</span>
|
||||
@@ -921,7 +933,7 @@ export function Workspace() {
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setAddLaneOpen(true)}
|
||||
className="ml-auto flex items-center gap-1.5 rounded border border-neutral-700 px-3 py-1 text-xs text-neutral-300 transition-colors hover:border-neutral-500 hover:text-neutral-100"
|
||||
className="ml-auto flex items-center gap-1.5 rounded border border-border-light px-3 py-1 text-xs text-fg-secondary transition-colors hover:border-border-light hover:text-fg-primary"
|
||||
title={tLanes("addLane")}
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
@@ -944,7 +956,7 @@ export function Workspace() {
|
||||
/>
|
||||
))}
|
||||
{!lanes.length && (
|
||||
<p className="text-sm text-neutral-500">
|
||||
<p className="text-sm text-fg-muted">
|
||||
{tLanes("emptyState")} <code>ccam lanes add --cwd $(pwd)</code>
|
||||
</p>
|
||||
)}
|
||||
@@ -953,26 +965,32 @@ export function Workspace() {
|
||||
{/* The selected lane's pipeline, full width — the thing you actually
|
||||
come to this page to read. */}
|
||||
{currentLane && (
|
||||
<section
|
||||
data-testid="lane-detail"
|
||||
className="rounded-xl border border-neutral-800 bg-neutral-900/40 p-4"
|
||||
>
|
||||
<section data-testid="lane-detail" className="card p-4">
|
||||
<div className="mb-3 flex flex-wrap items-baseline gap-2">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-widest text-neutral-500">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-widest text-fg-muted">
|
||||
{tLanes("cardId", { id: currentLane.id })}
|
||||
</span>
|
||||
<span className="truncate text-sm font-semibold text-neutral-100">
|
||||
<span className="truncate text-sm font-semibold text-fg-primary">
|
||||
{currentLane.title || currentLane.cwd}
|
||||
</span>
|
||||
<span className="text-[11px] text-neutral-600">{currentLane.pipeline_name}</span>
|
||||
<span className="rounded bg-neutral-800 px-2 py-0.5 text-xs text-neutral-200">
|
||||
{currentLane.stage}
|
||||
<span className="text-[11px] text-fg-muted">{currentLane.pipeline_name}</span>
|
||||
{/* `stage` defaults to the DB sentinel "idle" until the driving
|
||||
session ever calls `ccam stage` — that string collides with
|
||||
`status`'s own "idle"/"running" vocabulary, so a lane that is
|
||||
actively running but has never declared a stage read as if it
|
||||
were sitting idle. Show a distinct label instead of the raw
|
||||
sentinel whenever it doesn't match any node this pipeline
|
||||
actually has. */}
|
||||
<span className="rounded bg-surface-2 px-2 py-0.5 text-xs text-fg-secondary">
|
||||
{currentLane.pipeline_nodes.some((n) => n.id === currentLane.stage)
|
||||
? currentLane.stage
|
||||
: tLanes("stageUndeclared")}
|
||||
</span>
|
||||
{currentLane.detected_stage && (
|
||||
<span
|
||||
data-testid="detail-auto-stage"
|
||||
title={currentLane.detected_signal || undefined}
|
||||
className="rounded border border-dashed border-amber-500 px-2 py-0.5 text-xs text-amber-300"
|
||||
className="rounded border border-dashed border-status-warning px-2 py-0.5 text-xs text-status-warning"
|
||||
>
|
||||
{tLanes("autoStage", { stage: currentLane.detected_stage })}
|
||||
</span>
|
||||
@@ -990,7 +1008,7 @@ export function Workspace() {
|
||||
detectedSignal={currentLane.detected_signal}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex min-h-0 flex-col gap-2 border-t border-neutral-800 pt-3">
|
||||
<div className="flex min-h-0 flex-col gap-2 border-t border-border pt-3">
|
||||
{consoleSection}
|
||||
</div>
|
||||
</section>
|
||||
@@ -1010,20 +1028,16 @@ export function Workspace() {
|
||||
{laneActionError && (
|
||||
<p
|
||||
role="alert"
|
||||
className="rounded border border-red-800 bg-red-950/40 px-3 py-2 text-sm text-red-300"
|
||||
className="rounded border border-status-danger bg-status-danger/40 px-3 py-2 text-sm text-status-danger"
|
||||
>
|
||||
{tLanes("actionError", { message: laneActionError })}
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
{/* No lane selected (none exist, or nothing picked yet): the console has
|
||||
nowhere to attach, so it falls back to page level. Without this the
|
||||
start form would be unreachable on a fresh install. */}
|
||||
{!currentLane && (
|
||||
<div className="flex min-h-0 flex-col gap-2">{consoleSection}</div>
|
||||
)}
|
||||
|
||||
{!currentLane && <div className="flex min-h-0 flex-col gap-2">{consoleSection}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1087,20 +1101,20 @@ function Header({
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-lg font-semibold text-gray-100">{t("title")}</h1>
|
||||
<h1 className="text-lg font-semibold text-fg-primary">{t("title")}</h1>
|
||||
{wsConnected ? (
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse-dot" />
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse-dot" />
|
||||
{tCommon("live")}
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-gray-400 bg-gray-500/10 border border-gray-500/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-gray-400" />
|
||||
<span className="flex items-center gap-1.5 text-[11px] text-fg-secondary bg-surface-4/10 border border-border-light/20 px-2 py-0.5 rounded-full">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-surface-4" />
|
||||
{tCommon("offline")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 max-w-3xl">{t("subtitle")}</p>
|
||||
<p className="text-xs text-fg-muted max-w-3xl">{t("subtitle")}</p>
|
||||
</div>
|
||||
<ActiveRunsSwitcher
|
||||
activeRuns={activeRuns}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user