feat: Claude Code Monitor — lanes, pipelines and a merged workspace

Internal SmartGift build of a Claude Code monitoring dashboard.

Lanes: a durable unit of parallel agent work, one per working directory,
tracked across session restarts. Managed lanes are git worktrees the
dashboard provisions and can reset or remove behind a three-check destroy
guard and a counted preflight; adopted lanes are directories you already
own and are never destroyable.

Pipelines: a lane moves through pipeline stages. A stage the agent declares
with evidence renders green; a stage inferred from the tool-event stream
renders dashed amber and never counts as done. Detection is forward-only
within a 30-minute window, and never writes the declared stage.

Workspace: one page at /run with a lane grid, the selected lane's pipeline,
and a full Claude console behind a disclosure.
This commit is contained in:
2026-07-29 17:07:45 +07:00
commit 794b240156
781 changed files with 220598 additions and 0 deletions
@@ -0,0 +1,229 @@
/**
* @file DestructiveLaneModal.tsx
* @description Fetches and displays the exact lane lifecycle preflight facts
* before delegating confirmation controls and accessibility to ConfirmModal.
*
* The server is the authority on what is permitted; this modal must never be
* stricter than it. A blocker that does not actually stop the chosen action is
* rendered as context (see HARD_BLOCKERS and noticesFor), not as a refusal —
* treating every blocker as fatal is what left "Forget" dead for adopted lanes.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { ConfirmModal } from "../ConfirmModal";
import { api } from "../../lib/api";
import type { Lane, LanePreflight } from "../../lib/types";
type DestructiveAction = "reset" | "remove" | "purge";
export interface DestructiveLaneModalProps {
lane: Lane;
action: DestructiveAction;
open: boolean;
onClose: () => void;
onConfirm: (body: { expect: Record<string, string | number | null>; force?: true }) => void;
}
function expectFor(preflight: LanePreflight): Record<string, string | number | null> {
if (preflight.action === "purge") {
return {
sessions: preflight.sessions,
events: preflight.events,
tokenRows: preflight.tokenRows,
};
}
return {
head: preflight.head,
dirty: preflight.dirty,
untracked: preflight.untracked,
unpushed: preflight.unpushed,
};
}
/**
* Which preflight blockers genuinely prevent each action in the UI.
*
* The server is the authority, and it permits EVERY shape of `remove`: an
* adopted lane's record is forgotten with its directory untouched, a
* hand-deleted worktree takes the prune path, an unreadable one is force
* -removed. So `remove` is blocked here by nothing — treating `adopted` or
* `missing` as blockers left the Forget button permanently dead with no
* fallback. `reset` really is impossible in all three shapes.
*
* `unpushed-commits` is force-overridable and is gated by the Force checkbox
* instead of by this list.
*/
const HARD_BLOCKERS: Record<"reset" | "remove", readonly string[]> = {
reset: ["adopted", "missing", "unreadable"],
remove: [],
};
function blockingReason(preflight: LanePreflight | null): string | null {
if (!preflight || preflight.action === "purge") return null;
const hard = HARD_BLOCKERS[preflight.action];
return preflight.blocked.find((blocker) => hard.includes(blocker)) || null;
}
/**
* Blockers that do not prevent THIS action but still change what it does, shown
* as context rather than as an obstacle — e.g. forgetting an adopted lane leaves
* its directory alone. `unpushed-commits` is excluded: the Force copy covers it.
*/
function noticesFor(preflight: LanePreflight | null): string[] {
if (!preflight || preflight.action === "purge") return [];
const hard = HARD_BLOCKERS[preflight.action];
return preflight.blocked.filter(
(blocker) => blocker !== "unpushed-commits" && !hard.includes(blocker)
);
}
/** Rough byte estimate as a short human string; the server's own number is a guess. */
function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
function warningsFor(preflight: LanePreflight | null): string[] {
if (!preflight || preflight.action === "purge") return [];
return preflight.warnings;
}
export function DestructiveLaneModal({
lane,
action,
open,
onClose,
onConfirm,
}: DestructiveLaneModalProps) {
const { t } = useTranslation(["lanes"]);
const [preflight, setPreflight] = useState<LanePreflight | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [force, setForce] = useState(false);
useEffect(() => {
if (!open) return;
let current = true;
setLoading(true);
setError(null);
setPreflight(null);
setForce(false);
void api.lanes
.preflight(lane.id, action)
.then((result) => current && setPreflight(result))
.catch((err) => current && setError(err instanceof Error ? err.message : t("preflightError")))
.finally(() => current && setLoading(false));
return () => {
current = false;
};
}, [action, lane.id, open, t]);
const blocked = blockingReason(preflight);
// Mirrors the server's gate exactly (server/routes/lanes.js): force is required
// for `reset`, and for `remove` only when a real worktree is at risk. Forgetting
// an adopted lane risks nothing on disk, so it needs no force even with unpushed
// commits. Never offered alongside a hard blocker, where confirming is
// impossible anyway.
const requiresForce =
!blocked &&
preflight !== null &&
preflight.action !== "purge" &&
preflight.blocked.includes("unpushed-commits") &&
(preflight.action === "reset" || preflight.kind === "managed");
const disabled =
loading || !preflight || Boolean(error) || Boolean(blocked) || (requiresForce && !force);
const facts = preflight ? Object.entries(expectFor(preflight)) : [];
const purge = preflight?.action === "purge" ? preflight : null;
return (
<ConfirmModal
open={open}
title={t(`destructive.${action}.title`)}
message={t(`destructive.${action}.message`)}
confirmLabel={t(`destructive.${action}.confirm`)}
cancelLabel={t("destructive.cancel")}
busy={loading}
disabled={disabled}
onCancel={onClose}
onConfirm={() => {
if (!preflight || disabled) return;
onConfirm({ expect: expectFor(preflight), ...(force ? { force: true as const } : {}) });
}}
>
{loading && <p className="mt-3 text-xs text-neutral-400">{t("destructive.loading")}</p>}
{error && (
<p className="mt-3 text-xs text-red-300">
{t("preflightErrorWithMessage", { message: error })}
</p>
)}
{preflight && (
<table className="mt-3 w-full text-left text-xs text-neutral-300">
<tbody>
{facts.map(([name, value]) => (
<tr key={name} className="border-t border-neutral-800">
<th scope="row" className="py-1.5 font-medium text-neutral-400">
{t(`destructive.count.${name}`)}
</th>
<td className="py-1.5 text-right tabular-nums">{value ?? "—"}</td>
</tr>
))}
{/* Not part of `expect`: an estimate the server never verifies. */}
{purge && (
<tr className="border-t border-neutral-800">
<th scope="row" className="py-1.5 font-medium text-neutral-400">
{t("destructive.count.bytesEstimate")}
</th>
<td className="py-1.5 text-right tabular-nums">
{formatBytes(purge.bytesEstimate)}
</td>
</tr>
)}
</tbody>
</table>
)}
{blocked && (
<p className="mt-3 rounded border border-amber-700/60 bg-amber-950/30 px-2 py-1.5 text-xs text-amber-200">
{t(`destructive.blocked.${blocked}`)}
</p>
)}
{purge?.activeSessionSkipped && (
<p className="mt-3 rounded border border-neutral-700 bg-neutral-800/50 px-2 py-1.5 text-xs text-neutral-300">
{t("destructive.notice.activeSessionSkipped")}
</p>
)}
{noticesFor(preflight).map((notice) => (
<p
key={notice}
className="mt-3 rounded border border-neutral-700 bg-neutral-800/50 px-2 py-1.5 text-xs text-neutral-300"
>
{t(`destructive.notice.${notice}`)}
</p>
))}
{warningsFor(preflight).map((warning) => (
<p
key={warning}
className="mt-3 rounded border border-neutral-700 bg-neutral-800/50 px-2 py-1.5 text-xs text-neutral-300"
>
{t(`destructive.warning.${warning}`)}
</p>
))}
{requiresForce && (
<label className="mt-3 flex items-start gap-2 text-xs text-amber-200">
<input
type="checkbox"
checked={force}
onChange={(event) => setForce(event.target.checked)}
className="mt-0.5"
/>
{t("destructive.force")}
</label>
)}
{action === "reset" && (
<p className="mt-3 text-xs text-neutral-400">{t("destructive.reset.survives")}</p>
)}
</ConfirmModal>
);
}