ba2c4e1914
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.
92 lines
3.2 KiB
TypeScript
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>
|
|
);
|
|
}
|