singularity-forge/web/components/sf/status-bar.tsx
ace-pm 83feadb4e1 wip: rename gsd-parser dir + exports, fix native package.json
- packages/native/src/gsd-parser → packages/native/src/forge-parser
- Update packages/native/package.json exports: ./gsd-parser → ./forge-parser
- Update packages/native/src/index.ts imports: ./gsd-parser → ./forge-parser

Build in progress: native tsc output missing submodule dists (fd, text, image, etc).
This is a pre-existing issue with the build system, not caused by rebrand.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:22:21 +02:00

163 lines
6.9 KiB
TypeScript

"use client"
import { useEffect, useState, useCallback } from "react"
import { GitBranch, Cpu, DollarSign, Clock, Zap, AlertTriangle, Wifi, Info, LifeBuoy } from "lucide-react"
import { cn } from "@/lib/utils"
import { Skeleton } from "@/components/ui/skeleton"
import {
buildProjectUrl,
getCurrentBranch,
getCurrentScopeLabel,
getLiveAutoDashboard,
getLiveWorkspaceIndex,
getModelLabel,
getStatusPresentation,
getVisibleWorkspaceError,
useSFWorkspaceState,
} from "@/lib/sf-workspace-store"
import {
formatCost as formatProjectCost,
formatDuration as formatProjectDuration,
formatTokenCount,
type ProjectTotals,
} from "@/lib/visualizer-types"
import { ScopeBadgeInline } from "@/components/sf/scope-badge"
import { authFetch } from "@/lib/auth"
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>
)
}