feat(tui,cli): slice-17 navigator leg — file-tree re-skin + bare-rig front door

Navigator (mini-req 1): computeExplorerRows stays the ONE row model; the
renderer applies a display-only transform — ┊/├─/└─ branch guides, ⊕/▚ icons,
right-aligned meta (agent ctx% honest-—, pod counts), collapse glyphs only
where collapse exists (hosts/rigs lose the decorative ▾). Rail glyph is ┊
because │ is pane structure to the shipped floors. Selection-focus highlight
stays the existing render-only bar (arch ruling 1); drill/auto-expand
unchanged (ruling 2).

Front door (mini-req 7): bare rig with BOTH streams TTY probes the daemon and
launches the TUI (monorepo-first/bundled-fallback resolution, the daemon
resolver pattern); either stream piped → the unchanged commander usage path
(fast, no hang); daemon-down/init-fail → helpful usage naming 'rig up', never
a stack trace; args always fall through unchanged (byte-diff pinned).

TUI 131/131 (123 floors untouched + 8 re-skin pins), CLI front-door 10/10,
full CLI suite green vs the 31 pre-existing base failures, root lint, builds.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
v-openrig-buildandClaude Fable 5 committed 2026-08-03 21:20:48 +00:00
1 parent 0b04ba1c5a
commit 6e8f2e8c20
7 files changed
+516 -3

No files matched your search

+112
View File
@@ -0,0 +1,112 @@
// Slice-17 mini-req 7 — BARE-RIG FRONT DOOR (founder, arch-reinforced).
//
// bare `rig` (no args) opens the TUI, k9s-style, TTY-AWARE:
// - the guard checks BOTH stdin AND stdout isTTY — a pipe/redirect on
// EITHER stream means a script is involved, so the front door does NOT
// own the invocation and the normal commander usage path runs (prints
// usage, exits fast, never hangs — `echo x | rig` must not block);
// - first-impression degrade: daemon-down and TUI-init-failure print
// HELPFUL usage ("daemon not running — try: rig up"), never a stack
// trace — daemon-down is the likely first-run state;
// - `--help`, `--version`, and every subcommand are ARGS, so they are
// naturally excluded from bare invocation and behave unchanged.
import path from "node:path";
import { existsSync } from "node:fs";
import { spawn } from "node:child_process";
import { DaemonClient } from "./client.js";
/** Monorepo-first, bundled-fallback TUI entry resolution — the exact
* resolveDaemonPath pattern (see daemon-lifecycle.ts): a dev checkout's
* `packages/tui/dist` is the source of truth; an npm-install layout ships
* the bundled copy next to the cli. Null = not found (caller degrades). */
export function resolveTuiPath(baseDir: string, exists: (p: string) => boolean = existsSync): string | null {
const cliBaseDir = path.basename(baseDir) === "commands" ? path.resolve(baseDir, "..") : baseDir;
const monorepo = path.join(path.resolve(cliBaseDir, "../../tui"), "dist/main.js");
if (exists(monorepo)) return monorepo;
const bundled = path.join(path.resolve(cliBaseDir, "../tui"), "dist/main.js");
if (exists(bundled)) return bundled;
return null;
}
const USAGE_LINES = [
"rig — the OpenRig control plane",
"",
" rig open mission control (interactive terminal only)",
" rig --help full command list",
" rig up <rig> bring a rig up",
" rig ps list live seats",
];
export interface FrontDoorIo {
stdinIsTTY?: boolean;
stdoutIsTTY?: boolean;
out?: (line: string) => void;
err?: (line: string) => void;
exit?: (code: number) => void;
/** fast daemon reachability probe (default: GET /healthz, 1.5s cap) */
probeDaemon?: () => Promise<boolean>;
/** launch the TUI and resolve with its exit code */
launchTui?: () => Promise<number>;
}
async function defaultProbeDaemon(): Promise<boolean> {
try {
const client = new DaemonClient();
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 1500);
try {
const res = await fetch(`${client.baseUrl}/healthz`, { signal: controller.signal });
return res.ok;
} finally {
clearTimeout(timer);
}
} catch {
return false;
}
}
async function defaultLaunchTui(): Promise<number> {
const entry = resolveTuiPath(import.meta.dirname);
if (!entry) throw new Error("mission-control TUI is not installed (no tui/dist/main.js next to this CLI)");
return await new Promise<number>((resolve, reject) => {
const child = spawn(process.execPath, [entry], { stdio: "inherit" });
child.on("error", reject);
child.on("exit", (code) => resolve(code ?? 0));
});
}
/**
* Returns true when the front door OWNED the invocation (TUI launched or a
* degrade message printed + exit requested); false to fall through to the
* normal commander program (args present, or a non-TTY stream).
*/
export async function runFrontDoor(argv: readonly string[], io: FrontDoorIo = {}): Promise<boolean> {
if (argv.length > 2) return false; // any arg → the normal CLI, unchanged
const stdinIsTTY = io.stdinIsTTY ?? process.stdin.isTTY === true;
const stdoutIsTTY = io.stdoutIsTTY ?? process.stdout.isTTY === true;
if (!stdinIsTTY || !stdoutIsTTY) return false; // script involved → usage path, fast exit
const err = io.err ?? ((l: string) => process.stderr.write(l + "\n"));
const exit = io.exit ?? ((c: number) => process.exit(c));
const probe = io.probeDaemon ?? defaultProbeDaemon;
const launch = io.launchTui ?? defaultLaunchTui;
if (!(await probe())) {
for (const line of USAGE_LINES) err(line);
err("");
err("daemon not running — try: rig up (then bare `rig` opens mission control)");
exit(1);
return true;
}
try {
const code = await launch();
exit(code);
} catch (e) {
// first-impression degrade: the message, never the stack
for (const line of USAGE_LINES) err(line);
err("");
err(`mission control could not start: ${e instanceof Error ? e.message : String(e)}`);
exit(1);
}
return true;
}
+8 -2
View File
@@ -251,6 +251,12 @@ export { runProgram, wantsJsonOutput } from "./cli-error.js";
// Only parse when executed directly (not imported for testing)
if (isDirectRun()) {
const { runProgram: runProgramDirect } = await import("./cli-error.js");
await runProgramDirect(createProgram(), process.argv);
// Slice-17 mini-req 7 — bare `rig` in a real terminal opens the TUI; any
// arg or a non-TTY stream falls through to the normal program unchanged.
const { runFrontDoor } = await import("./front-door.js");
const owned = await runFrontDoor(process.argv);
if (!owned) {
const { runProgram: runProgramDirect } = await import("./cli-error.js");
await runProgramDirect(createProgram(), process.argv);
}
}
+145
View File
@@ -0,0 +1,145 @@
// Slice-17 mini-req 7 — BARE-RIG FRONT DOOR (founder + arch reinforcements).
// bare `rig` (no args): BOTH stdin+stdout TTY → launch the TUI; a pipe or
// redirect on EITHER stream → fall through to the normal usage path with a
// fast exit (never hang a script); daemon-down / TUI-init-fail → helpful
// usage, NEVER a stack trace; --help/--version/subcommands are args and
// behave unchanged. New file; shipped CLI floors untouched.
import { describe, it, expect, vi } from "vitest";
import { spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
import { join } from "node:path";
import { runFrontDoor, resolveTuiPath, type FrontDoorIo } from "../src/front-door.js";
function io(overrides: Partial<FrontDoorIo> = {}): FrontDoorIo & {
outLines: string[];
errLines: string[];
exits: number[];
launches: number;
} {
const outLines: string[] = [];
const errLines: string[] = [];
const exits: number[] = [];
const holder = { launches: 0 };
return {
outLines,
errLines,
exits,
get launches() {
return holder.launches;
},
stdinIsTTY: true,
stdoutIsTTY: true,
out: (l: string) => void outLines.push(l),
err: (l: string) => void errLines.push(l),
exit: (c: number) => void exits.push(c),
probeDaemon: async () => true,
launchTui: async () => {
holder.launches += 1;
return 0;
},
...overrides,
};
}
describe("bare-rig front door — ownership rules", () => {
it("bare `rig` with BOTH streams TTY and the daemon up launches the TUI", async () => {
const deps = io();
const handled = await runFrontDoor(["node", "rig"], deps);
expect(handled).toBe(true);
expect(deps.launches).toBe(1);
expect(deps.errLines.join("\n")).toBe("");
});
it("stdin piped (echo x | rig) → NOT owned: falls through to the normal usage path", async () => {
const deps = io({ stdinIsTTY: false });
expect(await runFrontDoor(["node", "rig"], deps)).toBe(false);
expect(deps.launches).toBe(0);
});
it("stdout redirected (rig > file) → NOT owned: falls through to the normal usage path", async () => {
const deps = io({ stdoutIsTTY: false });
expect(await runFrontDoor(["node", "rig"], deps)).toBe(false);
expect(deps.launches).toBe(0);
});
it("ANY argument (--help / --version / a subcommand) → NOT owned, regardless of TTY", async () => {
for (const argv of [["node", "rig", "--help"], ["node", "rig", "--version"], ["node", "rig", "ps"], ["node", "rig", "context", "list"]]) {
const deps = io();
expect(await runFrontDoor(argv, deps), argv.join(" ")).toBe(false);
expect(deps.launches).toBe(0);
}
});
});
describe("bare-rig front door — first-impression degrade (never a stack trace)", () => {
it("daemon down → helpful usage naming `rig up`, exit 1, launch NOT attempted", async () => {
const deps = io({ probeDaemon: async () => false });
const handled = await runFrontDoor(["node", "rig"], deps);
expect(handled).toBe(true);
expect(deps.launches).toBe(0);
const text = deps.errLines.join("\n");
expect(text).toMatch(/daemon not running/i);
expect(text).toMatch(/rig up/);
expect(text).toMatch(/rig --help/);
expect(text).not.toMatch(/\n\s+at /); // no stack frames
expect(deps.exits).toEqual([1]);
});
it("TUI init failure → helpful usage with the failure line, exit 1, no stack trace", async () => {
const deps = io({
launchTui: async () => {
throw new Error("tui entry not found at /nope/main.js");
},
});
const handled = await runFrontDoor(["node", "rig"], deps);
expect(handled).toBe(true);
const text = deps.errLines.join("\n");
expect(text).toMatch(/tui entry not found/);
expect(text).toMatch(/rig --help/);
expect(text).not.toMatch(/\n\s+at /);
expect(deps.exits).toEqual([1]);
});
it("the TUI's own exit code propagates", async () => {
const deps = io({ launchTui: async () => 3 });
await runFrontDoor(["node", "rig"], deps);
expect(deps.exits).toEqual([3]);
});
});
describe("resolveTuiPath — monorepo-first, bundled fallback (the daemon resolver pattern)", () => {
it("prefers the monorepo sibling dist, falls back to the bundled copy, null when neither", () => {
const base = "/repo/packages/cli/dist";
const mono = join("/repo/packages/cli/dist", "../../tui/dist/main.js");
const bundled = join("/repo/packages/cli/dist", "../tui/dist/main.js");
expect(resolveTuiPath(base, (p) => p === mono)).toBe(mono);
expect(resolveTuiPath(base, (p) => p === bundled)).toBe(bundled);
expect(resolveTuiPath(base, () => false)).toBeNull();
});
});
describe("script-safety integration (the compiled front door)", () => {
const cliEntry = join(__dirname, "..", "dist", "index.js");
it.skipIf(!existsSync(cliEntry))("`echo x | rig` returns usage fast — no hang (exit code preserves today's clean-help baseline)", () => {
const started = Date.now();
const result = spawnSync(process.execPath, [cliEntry], {
input: "x\n",
timeout: 5000,
encoding: "utf-8",
env: { ...process.env, OPENRIG_URL: "", OPENRIG_PORT: "" },
});
expect(Date.now() - started).toBeLessThan(5000);
// pre-slice-17 baseline: commander's clean-help path prints usage and
// exits 0 — the front door PRESERVES the piped path byte-for-byte
expect(result.status).toBe(0);
expect(`${result.stderr}${result.stdout}`).toMatch(/Usage: rig/);
expect(`${result.stderr}${result.stdout}`).not.toMatch(/\n\s+at /);
});
it.skipIf(!existsSync(cliEntry))("`rig --help` still exits 0 with the full usage (front-door regression)", () => {
const result = spawnSync(process.execPath, [cliEntry, "--help"], { timeout: 5000, encoding: "utf-8" });
expect(result.status).toBe(0);
expect(result.stdout).toMatch(/Usage: rig/);
expect(result.stdout).toMatch(/daemon/);
});
});
+124
View File
@@ -0,0 +1,124 @@
// Slice-17 mini-req 1 — the Direction-B file-tree navigator RE-SKIN.
//
// A PURE presentation transform over the ONE row model: computeExplorerRows
// stays the single source of rows/keys/actions (PIN-1 — the reducer's
// 'activate' and the renderer's hit-map keep resolving against it); this
// module only derives each row's DISPLAY label:
// · continuous branch guides │ ├─ └─ from the row's key depth,
// · icons: host ⊕ · rig ▚ · pod (dim name + genuine ▾/▸) · agent status ●,
// · meta right-aligned (agent ctx% — honest `—` when null; pod agent count),
// · collapse glyphs ONLY where collapse genuinely exists today (pods, spec
// folders, section headers) — hosts/rigs carried a decorative ▾ that
// afforded nothing; it is dropped, not re-skinned (no false affordances).
// "Hover" is the existing selection-focus highlight — RENDER-ONLY, no motion
// protocol, no second write-path to selection (arch ruling 1).
import type { ExplorerRow, FleetSnapshot } from "./types.js";
interface KeyParts {
kind: string;
parts: string[];
}
function parseKey(key: string | undefined): KeyParts | null {
if (!key) return null;
const at = key.indexOf(":");
if (at < 0) return null;
return { kind: key.slice(0, at), parts: key.slice(at + 1).split("/") };
}
function keyDepth(key: string | undefined): number {
const parsed = parseKey(key);
if (!parsed) return -1; // keyless rows (filter, needs items) keep their label
switch (parsed.kind) {
case "section":
return 0;
case "host":
case "specs-kind":
return 1;
case "rig":
case "folder":
return 2;
case "pod":
return 3;
case "spec":
return parsed.parts[0]?.includes("/") ? 3 : 2;
case "agent":
return 4;
default:
return -1;
}
}
/** the label with its legacy indent + list glyph stripped; pod/folder rows
* keep their GENUINE ▾/▸ (they really collapse), hosts/rigs lose theirs */
function contentOf(row: ExplorerRow, kind: string): string {
const stripped = row.label.replace(/^\s+/, "");
if (kind === "host") return `⊕ ${stripped.replace(/^▾ /, "")}`;
if (kind === "rig") return `▚ ${stripped.replace(/^▾ /, "")}`;
if (kind === "pod") return stripped.replace(/ \(\d+\)$/, ""); // count moves to meta
return stripped;
}
function metaOf(row: ExplorerRow, snap: FleetSnapshot): string {
const parsed = parseKey(row.key);
if (!parsed) return "";
if (parsed.kind === "agent") {
const [host, rig, pod, ...name] = parsed.parts;
const agent = snap.hosts
.find((h) => h.name === host)?.rigs.find((r) => r.name === rig)
?.pods.find((p) => p.name === pod)?.agents.find((a) => a.name === name.join("/"));
if (!agent) return "";
// honest-unknown: no served ctx → an explicit —, never a fabricated value
return agent.context == null ? "—" : `${agent.context}%`;
}
if (parsed.kind === "pod") {
const [host, rig, pod] = parsed.parts;
const found = snap.hosts.find((h) => h.name === host)?.rigs.find((r) => r.name === rig)?.pods.find((p) => p.name === pod);
return found ? String(found.agents.length) : "";
}
return "";
}
/**
* Display labels for the explorer pane — same order, same length as `rows`.
* Guides derive from key depth with last-sibling awareness so the vertical
* rails read continuously (the `tree` command look).
*/
export function navigatorLabels(rows: ExplorerRow[], snap: FleetSnapshot, width: number): string[] {
const depths = rows.map((row) => keyDepth(row.key));
const isLast = rows.map((_, i) => {
const depth = depths[i]!;
if (depth <= 0) return true;
for (let j = i + 1; j < rows.length; j++) {
const other = depths[j]!;
if (other < 0) continue;
if (other < depth) return true;
if (other === depth) return false;
}
return true;
});
// NB: the continuation rail is ┊ (not │) — the pane BORDER is │ and the
// shipped floor treats the first │ on a line as pane structure; the rail
// must never masquerade as the border (same glyph-collision class as the
// emoji-width lesson). Branch glyphs ├─ └─ are the mockup's exact chars.
const railOpen: boolean[] = []; // per depth level: does a later sibling exist?
return rows.map((row, i) => {
const depth = depths[i]!;
if (depth < 0) return row.label; // keyless rows untouched
if (depth === 0) return row.label; // section headers keep their ▾/▸ identity
railOpen[depth] = !isLast[i]!;
const guides = Array.from({ length: depth - 1 }, (_, level) => (railOpen[level + 1] ? "┊ " : " ")).join("");
const branch = isLast[i] ? "└─ " : "├─ ";
const parsed = parseKey(row.key)!;
let content = contentOf(row, parsed.kind);
const meta = metaOf(row, snap);
const prefix = ` ${guides}${branch}`;
if (!meta) return `${prefix}${content}`;
// meta is glance data — guaranteed; over-long content truncates honestly
const contentMax = width - prefix.length - meta.length - 1;
if (content.length > contentMax) content = `${content.slice(0, Math.max(contentMax - 1, 0))}…`;
const gap = Math.max(width - prefix.length - content.length - meta.length, 1);
return `${prefix}${content}${" ".repeat(gap)}${meta}`;
});
}
+5 -1
View File
@@ -5,6 +5,7 @@
// SAME semantic actions commands produce (PIN 1). Isolated seam: a substrate
// swap touches only this module (spike verdict revisit trigger).
import { computeExplorerRows, findAgent, findSpec, findAgentBySession, agentsRunningSpec, agentsRunningSpecTargets } from "./state.js";
import { navigatorLabels } from "./navigator.js";
import { detailPage, fieldLine, sectionRule, listItem, alignedRow } from "./detail.js";
import type { Action, FleetSnapshot, NeedsItem, Screen, ViewState } from "./types.js";
@@ -537,6 +538,9 @@ export function renderScreen(state: ViewState, snap: FleetSnapshot, options: Ren
lines.push(paneRule(cols, "┬", explorerTitle, contentTitle));
const explorer = computeExplorerRows(state, snap);
// Slice-17: the file-tree re-skin is a DISPLAY transform only — rows, keys,
// actions, and the hit-map all keep resolving against the row model above.
const explorerDisplay = navigatorLabels(explorer, snap, EXPL_W - 1);
const content = contentLines(state, snap, Math.max(cols - EXPL_W - 2, 0));
const footer = state.footerOn ? snap.stream.at(-1) : undefined;
const chromeRows = footer ? 4 : 3; // bottom rule + hint bar + status line (+ footer)
@@ -568,7 +572,7 @@ export function renderScreen(state: ViewState, snap: FleetSnapshot, options: Ren
const explorerIndex = explorerStart + i;
const row = explorer[explorerIndex];
const marker = explorerIndex === state.selection && row ? "›" : " ";
const left = pad(row ? `${marker}${row.label}` : "", EXPL_W);
const left = pad(row ? `${marker}${explorerDisplay[explorerIndex] ?? row.label}` : "", EXPL_W);
const item = visibleContent[i];
const targetIndex = contentTargets.length;
const zones = item?.zones ?? [];
+15
View File
@@ -29,6 +29,18 @@ function paintExplorer(text: string, s: Style, focused: boolean): string {
// pm-approved: the unfocused pane's selection bar dims (k9s/editor standard)
if (text.startsWith("›")) return s.paint(focused ? "accent" : "dim", text, { inverse: true, bold: focused });
if (/[▾▸] (TOPOLOGY|SPECS|NEEDS-YOU)/.test(text)) return s.paint("bright", text, { bold: true });
// Slice-17 re-skin: branch guides paint faint (chrome), the row body keeps
// its own rules — the tree rails read as structure, never as content.
const tree = text.match(/^( *(?:[┊ ] )*(?:├─|└─) )(.*)$/);
if (tree) return s.paint("chrome", tree[1]!) + paintExplorerBody(tree[2]!, s);
return paintExplorerBody(text, s);
}
/** row-body treatment (VISUAL-TARGETS: rig teal · pod dim · meta faint ·
* names default ink); the right-aligned meta column always dims */
function paintExplorerBody(text: string, s: Style): string {
const meta = text.match(/^(.*?\S)( {2,})([0-9]+%|—|[0-9]+)$/);
if (meta) return paintExplorerBody(meta[1]!, s) + meta[2]! + s.paint("dim", meta[3]!);
if (text.includes("⚑")) return s.paint("warn", text);
if (/\(unreachable\)/.test(text)) {
const at = text.indexOf("(unreachable)");
@@ -37,6 +49,9 @@ function paintExplorer(text: string, s: Style, focused: boolean): string {
if (/\((recoverable|degraded|stopped|attention_required)\)/.test(text)) {
return text.replace(/\((recoverable|degraded|stopped|attention_required)\)/, (m) => s.paint("warn", m));
}
if (text.startsWith("▚ ")) return s.paint("accent", text);
if (text.startsWith("⊕ ")) return s.paint("dim", "⊕ ") + text.slice(2);
if (/^[▾▸] /.test(text)) return s.paint("chrome", text.slice(0, 2)) + s.paint("dim", text.slice(2));
return text;
}
+107
View File
@@ -0,0 +1,107 @@
// Slice-17 mini-req 1 — explorer navigator RE-SKIN to the file-tree
// aesthetic. PURE re-skin: computeExplorerRows (the ONE row model) is
// untouched; the renderer displays branch guides + right-aligned meta while
// every action, key, and hit target stays identical (PIN-1). Collapse glyphs
// render ONLY where collapse genuinely exists today (pods; spec folders;
// section headers) — hosts/rigs lose their false ▾. New file; shipped floors
// untouched.
import { describe, it, expect } from "vitest";
import { createViewState, computeExplorerRows } from "../src/state.js";
import { renderScreen } from "../src/render.js";
import { demoSnapshot } from "../src/demo-data.js";
import { createStyle, stripAnsi } from "../src/theme.js";
import { stylizeLines } from "../src/stylize.js";
const snap = demoSnapshot();
function makeStore() {
return createViewState({ instanceId: "nav-test", getSnapshot: () => snap });
}
function explorerPane(lines: string[]): string[] {
// rows 3.. of the screen, left of the │ pane border (EXPL_W = 30)
return lines.slice(2).map((l) => l.slice(0, 30));
}
describe("file-tree re-skin (Direction B navigator)", () => {
it("renders continuous branch guides in the explorer pane", () => {
const s = makeStore();
const screen = renderScreen(s.get(), snap, { cols: 120, rows: 32 });
const pane = explorerPane(screen.lines).join("\n");
expect(pane).toMatch(/├─/);
expect(pane).toMatch(/└─/);
});
it("hosts and rigs carry NO collapse glyph (no false affordances — no collapse exists there today)", () => {
const s = makeStore();
const screen = renderScreen(s.get(), snap, { cols: 120, rows: 32 });
const hostRow = explorerPane(screen.lines).find((l) => l.includes("vm-host"))!;
const rigRow = explorerPane(screen.lines).find((l) => l.includes("openrig-build"))!;
expect(hostRow).not.toMatch(/[▾▸]/);
expect(rigRow).not.toMatch(/[▾▸]/);
});
it("pods KEEP their genuine collapse glyph (▸ collapsed, ▾ expanded via drill/auto-expand)", () => {
const s = makeStore();
let screen = renderScreen(s.get(), snap, { cols: 120, rows: 32 });
const collapsed = explorerPane(screen.lines).find((l) => l.includes("dev50"))!;
expect(collapsed).toContain("▸");
// drilling the pod auto-expands it — unchanged function, re-skinned look
s.dispatch({ type: "drill", resource: "pod", name: "dev50", target: { host: "vm-host", rig: "openrig-build" } });
screen = renderScreen(s.get(), snap, { cols: 120, rows: 32 });
const pane = explorerPane(screen.lines);
expect(pane.find((l) => l.includes("dev50") && !l.includes("driver") && !l.includes("guard") && !l.includes("qa"))).toContain("▾");
expect(pane.some((l) => l.includes("dev50.driver"))).toBe(true);
});
it("agent rows carry right-aligned ctx% meta; a null ctx renders — (honest-unknown)", () => {
const s = makeStore();
s.dispatch({ type: "drill", resource: "pod", name: "dev50", target: { host: "vm-host", rig: "openrig-build" } });
const screen = renderScreen(s.get(), snap, { cols: 120, rows: 32 });
const pane = explorerPane(screen.lines);
const driver = pane.find((l) => l.includes("dev50.driver"))!;
expect(driver.trimEnd()).toMatch(/62%$/); // demo fixture: driver ctx 62
const qa = pane.find((l) => l.includes("dev50.qa"))!;
expect(qa.trimEnd()).toMatch(/—$/); // demo fixture: qa ctx null → honest
});
it("pod rows carry their agent count right-aligned (moved out of the inline label)", () => {
const s = makeStore();
const screen = renderScreen(s.get(), snap, { cols: 120, rows: 32 });
const pod = explorerPane(screen.lines).find((l) => l.includes("dev50"))!;
expect(pod.trimEnd()).toMatch(/3$/); // dev50 pod has 3 agents in the demo fixture
});
it("PURE re-skin: the hit-map's explorer actions are EXACTLY the row model's actions (PIN-1 untouched)", () => {
const s = makeStore();
const screen = renderScreen(s.get(), snap, { cols: 120, rows: 32 });
const rows = computeExplorerRows(s.get(), snap);
screen.explorerRows.forEach((rendered, i) => {
expect(rendered.action).toEqual(rows[i]!.action);
expect(rendered.key).toBe(rows[i]!.key);
});
// clicking the rig row still drills the rig — same action, same reducer
const rigTarget = screen.hitMap.find((h) => h.action.type === "drill" && h.action.resource === "rig");
expect(rigTarget).toBeDefined();
const after = s.dispatch(rigTarget!.action);
expect(after.drill.map((d) => d.name)).toEqual(["vm-host", "openrig-build"]);
});
it("selection sync still lands ON the drilled agent (auto-expand preserved through the re-skin)", () => {
const s = makeStore();
s.dispatch({ type: "drill", resource: "agent", name: "dev50.guard", target: { host: "vm-host", rig: "openrig-build", pod: "dev50" } });
const rows = computeExplorerRows(s.get(), snap);
expect(rows[s.get().selection]?.key).toBe("agent:vm-host/openrig-build/dev50/dev50.guard");
const screen = renderScreen(s.get(), snap, { cols: 120, rows: 32 });
const selectedLine = screen.lines.find((l) => l.startsWith("›"))!;
expect(selectedLine).toContain("dev50.guard");
});
it("stylize keeps the strip-invariant over the re-skinned labels", () => {
const s = makeStore();
s.dispatch({ type: "drill", resource: "pod", name: "dev50", target: { host: "vm-host", rig: "openrig-build" } });
const screen = renderScreen(s.get(), snap, { cols: 120, rows: 32 });
const styled = stylizeLines(screen, createStyle("truecolor"));
styled.forEach((line, i) => expect(stripAnsi(line), `line ${i}`).toBe(screen.lines[i]));
});
});