singularity-forge/web/components/sf/status-bar.tsx
2026-05-05 14:46:18 +02:00

237 lines
6.9 KiB
TypeScript

"use client";
import {
AlertTriangle,
Clock,
Cpu,
DollarSign,
GitBranch,
Info,
LifeBuoy,
Wifi,
Zap,
} from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { ScopeBadgeInline } from "@/components/sf/scope-badge";
import { Skeleton } from "@/components/ui/skeleton";
import { authFetch } from "@/lib/auth";
import {
buildProjectUrl,
getCurrentBranch,
getCurrentScopeLabel,
getLiveAutoDashboard,
getLiveWorkspaceIndex,
getModelLabel,
getStatusPresentation,
getVisibleWorkspaceError,
useSFWorkspaceState,
} from "@/lib/sf-workspace-store";
import { cn } from "@/lib/utils";
import {
formatCost as formatProjectCost,
formatDuration as formatProjectDuration,
formatTokenCount,
type ProjectTotals,
} from "@/lib/visualizer-types";
function toneClass(
tone: ReturnType<typeof getStatusPresentation>["tone"],
): string {
switch (tone) {
case "success":
return "text-success";
case "warning":
return "text-warning";
case "danger":
return "text-destructive";
default:
return "text-muted-foreground";
}
}
export function StatusBar() {
const workspace = useSFWorkspaceState();
const status = getStatusPresentation(workspace);
const liveWorkspace = getLiveWorkspaceIndex(workspace);
const auto = getLiveAutoDashboard(workspace);
const branch = getCurrentBranch(liveWorkspace) ?? "project scope";
const model = getModelLabel(workspace.boot?.bridge);
const unitLabel =
auto?.currentUnit?.id ?? getCurrentScopeLabel(liveWorkspace);
const visibleError = getVisibleWorkspaceError(workspace);
const titleOverride = workspace.titleOverride?.trim() || null;
const statusTexts = workspace.statusTexts;
const recoverySummary = workspace.live.recoverySummary;
const validationCount =
getLiveWorkspaceIndex(workspace)?.validationIssues.length ?? 0;
const statusTextEntries = Object.entries(statusTexts);
const latestStatusText =
statusTextEntries.length > 0
? statusTextEntries[statusTextEntries.length - 1][1]
: null;
const isConnecting =
workspace.bootStatus === "idle" || workspace.bootStatus === "loading";
const projectCwd = workspace.boot?.project.cwd;
// ── Project-level totals from visualizer API ──
const [projectTotals, setProjectTotals] = useState<ProjectTotals | null>(
null,
);
const fetchProjectTotals = useCallback(async () => {
try {
const resp = await authFetch(
buildProjectUrl("/api/visualizer", projectCwd),
);
if (!resp.ok) return;
const json = await resp.json();
if (json.totals) setProjectTotals(json.totals);
} catch {
// Silently ignore — status bar is non-critical
}
}, [projectCwd]);
useEffect(() => {
const timeout = window.setTimeout(() => {
void fetchProjectTotals();
}, 0);
const interval = window.setInterval(() => {
void fetchProjectTotals();
}, 30_000);
return () => {
window.clearTimeout(timeout);
window.clearInterval(interval);
};
}, [fetchProjectTotals]);
return (
<div className="flex h-7 items-center justify-between border-t border-border bg-card px-2 md:px-3 text-[10px] md:text-xs">
<div className="flex min-w-0 items-center gap-2 md:gap-4">
<div className={`flex items-center gap-1.5 ${toneClass(status.tone)}`}>
<Wifi className="h-3 w-3" />
<span>{status.label}</span>
</div>
<div className="hidden sm:flex items-center gap-1.5 text-muted-foreground">
<GitBranch className="h-3 w-3" />
{isConnecting ? (
<Skeleton className="h-3 w-20" />
) : (
<span className="font-mono">{branch}</span>
)}
</div>
<div className="hidden lg:flex items-center gap-1.5 text-muted-foreground">
<Cpu className="h-3 w-3" />
{isConnecting ? (
<Skeleton className="h-3 w-24" />
) : (
<span className="font-mono">{model}</span>
)}
</div>
{!isConnecting && (
<div
className="hidden max-w-xs items-center gap-1.5 truncate text-muted-foreground xl:flex"
data-testid="status-bar-retry-compaction"
>
<LifeBuoy className="h-3 w-3 shrink-0" />
<span className="truncate">
{recoverySummary.retryInProgress
? `Retry ${Math.max(1, recoverySummary.retryAttempt)}`
: recoverySummary.isCompacting
? "Compacting"
: recoverySummary.freshness}
</span>
</div>
)}
{!isConnecting && (
<div
className={cn(
"hidden items-center gap-1.5 xl:flex",
validationCount > 0 ? "text-warning" : "text-muted-foreground",
)}
data-testid="status-bar-validation-count"
>
<AlertTriangle className="h-3 w-3 shrink-0" />
<span>
{validationCount} issue{validationCount === 1 ? "" : "s"}
</span>
</div>
)}
{!isConnecting && visibleError && (
<div
className="hidden max-w-sm items-center gap-1.5 truncate text-destructive lg:flex"
data-testid="status-bar-error"
>
<AlertTriangle className="h-3 w-3 shrink-0" />
<span className="truncate">{visibleError}</span>
</div>
)}
{!isConnecting && titleOverride && (
<div
className="hidden max-w-xs items-center gap-1.5 truncate text-foreground/80 xl:flex"
data-testid="status-bar-title-override"
>
<Info className="h-3 w-3 shrink-0" />
<span className="truncate" title={titleOverride}>
{titleOverride}
</span>
</div>
)}
{!isConnecting && latestStatusText && !visibleError && (
<div
className="hidden max-w-xs items-center gap-1.5 truncate text-muted-foreground lg:flex"
data-testid="status-bar-extension-status"
>
<Info className="h-3 w-3 shrink-0" />
<span className="truncate">{latestStatusText}</span>
</div>
)}
</div>
<div className="flex min-w-0 items-center gap-2 md:gap-4">
<div className="hidden sm:flex items-center gap-1.5 text-muted-foreground">
<Clock className="h-3 w-3" />
{isConnecting ? (
<Skeleton className="h-3 w-8" />
) : (
<span>
{formatProjectDuration(
projectTotals?.duration ?? auto?.elapsed ?? 0,
)}
</span>
)}
</div>
<div className="hidden sm:flex items-center gap-1.5 text-muted-foreground">
<Zap className="h-3 w-3" />
{isConnecting ? (
<Skeleton className="h-3 w-6" />
) : (
<span>
{formatTokenCount(
projectTotals?.tokens.total ?? auto?.totalTokens ?? 0,
)}
</span>
)}
</div>
<div className="flex items-center gap-1.5 text-muted-foreground">
<DollarSign className="h-3 w-3" />
{isConnecting ? (
<Skeleton className="h-3 w-10" />
) : (
<span>
{formatProjectCost(projectTotals?.cost ?? auto?.totalCost ?? 0)}
</span>
)}
</div>
<span
className="hidden sm:inline max-w-[20rem] truncate text-muted-foreground"
data-testid="status-bar-unit"
>
{isConnecting ? (
<Skeleton className="inline-block h-3 w-28 align-middle" />
) : (
<ScopeBadgeInline label={unitLabel} />
)}
</span>
</div>
</div>
);
}