import { useEffect, useMemo, useState, type ReactNode } from "react"; import { useHive } from "../store"; import { pruneTelemetry, saveOverride } from "../store/wiring"; import { confirmAction, deleteProject } from "../store/raw"; import { fetchStorage, type StorageBreakdown } from "../api"; import { fmtBytes, fmtNum } from "../lib/format"; // One labelled metric tile in the storage summary. `accent` gives the prune- // preview tile a subtly tinted card so it reads as the actionable figure. function StorageTile(props: { label: string; value: string; valueClass?: string; hint?: ReactNode; accent?: boolean }) { return (
{props.label}
{props.value}
{props.hint &&
{props.hint}
}
); } // Settings: rename projects (persisted in the telemetry DB, keyed by cwd). // Future settings can be added as additional sections below. export default function Settings() { const projectGroups = useHive((s) => s.projectGroups); const overrides = useHive((s) => s.projectOverrides); const scope = useHive((s) => s.scope); // Settings is scoped to the selected project. const scopedProject = scope.level !== "fleet" ? scope.project : undefined; // Local edit buffer per group key so typing doesn't fight the store. const [draft, setDraft] = useState>({}); const [busy, setBusy] = useState(""); const [pruneDays, setPruneDays] = useState("30"); const [pruning, setPruning] = useState(false); const [storage, setStorage] = useState(null); const [storageLoading, setStorageLoading] = useState(false); // The cwd of the scoped project (first cwd of its group), or undefined at fleet // scope → whole-DB storage. The server resolves it to the project's full cwd set. const scopedCwd = useMemo(() => { if (!scopedProject) return undefined; return projectGroups.find((g) => g.name === scopedProject)?.cwds[0]; }, [projectGroups, scopedProject]); // Fetch storage usage + prune preview whenever the scope or the days input // settles. Debounced so typing in the days field doesn't spam the endpoint. useEffect(() => { const days = Number(pruneDays.trim()); const validDays = Number.isFinite(days) && days >= 0 ? days : undefined; let cancelled = false; setStorageLoading(true); const t = setTimeout(async () => { const res = await fetchStorage(scopedCwd, validDays); if (!cancelled) { setStorage(res); setStorageLoading(false); } }, 300); return () => { cancelled = true; clearTimeout(t); }; }, [scopedCwd, pruneDays]); // Seed the draft from current labels when the group set changes. useEffect(() => { setDraft((prev) => { const next = { ...prev }; for (const g of projectGroups) if (next[g.name] === undefined) next[g.name] = g.label; return next; }); }, [projectGroups]); const rows = useMemo( () => projectGroups .filter((g) => !scopedProject || g.name === scopedProject) .map((g) => ({ key: g.name, derived: g.name, label: g.label, cwd: g.cwds[0] || "", cwds: g.cwds, sessions: g.sessions.length, overridden: g.cwds.some((c) => overrides.has(c)), })), [projectGroups, overrides, scopedProject], ); async function save(cwd: string, key: string, label: string) { if (!cwd) return; setBusy(key); await saveOverride(cwd, label); setBusy(""); } function requestDelete(project: string, label: string, sessions: number) { confirmAction({ title: "Delete project telemetry?", message: <>This permanently removes all telemetry for {label} — {sessions} session{sessions === 1 ? "" : "s"} plus their events, delegations, tool calls, and messages. Project files on disk are not touched., confirmLabel: "Delete telemetry", danger: true, onConfirm: () => deleteProject(project), }); } function requestPrune() { const days = Number(pruneDays.trim()); if (!Number.isFinite(days) || days < 0) return; confirmAction({ title: "Prune telemetry history?", message: <>This permanently deletes all telemetry older than {days} day{days === 1 ? "" : "s"} across every project — events, delegations, tool calls, and any session whose entire history predates the cutoff. Project logs on disk are not touched., confirmLabel: "Prune history", danger: true, onConfirm: async () => { setPruning(true); try { await pruneTelemetry(days); await refreshStorage(); } finally { setPruning(false); } }, }); } async function refreshStorage() { const days = Number(pruneDays.trim()); const validDays = Number.isFinite(days) && days >= 0 ? days : undefined; setStorage(await fetchStorage(scopedCwd, validDays)); } return (

Projects

Rename how a project appears across the dashboard. The change is saved locally and applies to every session from that working directory.

{!rows.length ?
No projects yet.
: (
{rows.map((r) => { const value = draft[r.key] ?? r.label; const dirty = value.trim() !== r.label && value.trim() !== ""; return (
{r.derived} {r.overridden && renamed}
{r.cwd || "—"}{r.cwds.length > 1 ? ` (+${r.cwds.length - 1} more)` : ""} · {r.sessions} session{r.sessions === 1 ? "" : "s"}
setDraft((d) => ({ ...d, [r.key]: e.target.value }))} onKeyDown={(e) => { if (e.key === "Enter" && dirty) save(r.cwd, r.key, value); }} />
{r.overridden && ( )}
); })}
)}

Storage & prune

Telemetry content size {scopedProject ? "for this project" : "across all projects"} (payloads + tool/message text; the physical database file is larger and shrinks after a prune). Pruning removes telemetry older than a chosen age and cannot be undone.

{scopedProject ? "This project" : "All telemetry"}
{storageLoading && !storage ? (
Measuring…
) : storage ? (
{storage.prune && ( storage.prune.removeEvents > 0 ? ( {storage.prune.removeSessions > 0 && `${storage.prune.removeSessions} session${storage.prune.removeSessions === 1 ? "" : "s"} · `}{fmtBytes(storage.prune.keepBytes)} would remain} /> ) : ( ) )}
) : (
Unavailable — is the dashboard daemon running?
)}
Retain the last
Older events are removed and sessions entirely older than this are dropped. Prune runs across all projects, not just this one.
setPruneDays(e.target.value)} aria-label="Days of telemetry to retain" /> day{pruneDays.trim() === "1" ? "" : "s"}
); }