Files
AgentHub/client/src/components/StatusBar.tsx
T
nntrivi2001 ba2c4e1914 feat(ui): sidebar project tree, status bar and a tabbed workbench for workspaces
The Workspace page's lane strip moves into the sidebar as a project tree:
workspaces grouped by git repository (new GET /api/projects, cached in
memory, POST /api/projects/refresh), untracked worktrees with Adopt, inline
rename, drag/Alt+arrow ordering and a right-click row menu. The selected
workspace is the URL's ?lane=, one shared lane list feeds the tree, the new
bottom status bar and the page. The Changes panel gains an activity bar
(Files / Changes / Sessions, with resume), and split panes get a header.
2026-10-01 16:33:25 +07:00

92 lines
3.2 KiB
TypeScript

/**
* @file StatusBar — the one-line bar along the bottom of the app shell, on
* every page: the live-connection state (click: the connection details modal,
* which the sidebar owns), the version, how many workspaces are running and how
* many need the user (from the shared lane list, no request of its own), and
* today's spend — the `daily_costs` bucket for the local date from
* `GET /api/pricing/cost`, the Dashboard's own cost call, re-read every 30 s
* while the tab is visible. No provider quota: reading Claude's quota rotates
* the stored OAuth token (`cc-usage.js`), and the CLI shows it in the pane.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Wifi, WifiOff } from "lucide-react";
import { api } from "../lib/api";
import { useLanes } from "../hooks/useLanes";
const COST_POLL_MS = 30_000;
function localDate(d = new Date()): string {
const pad = (n: number) => String(n).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
export function StatusBar({
wsConnected,
left,
onConnectionClick,
}: {
wsConnected: boolean;
/** Sidebar width, so the bar starts where the main column does. */
left: string;
onConnectionClick: () => void;
}) {
const { t } = useTranslation("nav");
const { lanes } = useLanes();
const running = lanes.filter((l) => l.status === "running").length;
const needYou = lanes.filter((l) => l.needs_action).length;
const [today, setToday] = useState<number | null>(null);
useEffect(() => {
const tick = () => {
if (document.visibilityState !== "visible") return;
api.pricing
.totalCost()
.then((r) => setToday(r.daily_costs.find((d) => d.date === localDate())?.cost ?? 0))
.catch(() => undefined);
};
tick();
const id = setInterval(tick, COST_POLL_MS);
document.addEventListener("visibilitychange", tick);
return () => {
clearInterval(id);
document.removeEventListener("visibilitychange", tick);
};
}, []);
return (
<footer
data-testid="status-bar"
style={{ left }}
className="fixed bottom-0 right-0 z-20 flex h-6 items-center gap-4 border-t border-border bg-surface-1 px-3 text-xs text-fg-muted transition-[left] duration-200"
>
<button
type="button"
onClick={onConnectionClick}
title={t("connectionDetails")}
className={`flex items-center gap-1.5 hover:text-fg-primary ${
wsConnected ? "text-status-success" : ""
}`}
>
{wsConnected ? <Wifi className="h-3 w-3" /> : <WifiOff className="h-3 w-3" />}
{wsConnected ? t("live") : t("disconnected")}
<span className="text-fg-muted">{`v${__APP_VERSION__}`}</span>
</button>
<span data-testid="status-bar-counts">
{t("statusBar.running", { count: running })}
{" · "}
<span className={needYou ? "text-status-warning" : ""}>
{t("statusBar.needYou", { count: needYou })}
</span>
</span>
{today !== null && (
<span data-testid="status-bar-cost" className="tabular-nums">
{t("statusBar.today", { cost: `$${today.toFixed(2)}` })}
</span>
)}
</footer>
);
}