From 7bf776331d238ce013f12f64ede8b6c07f931ac7 Mon Sep 17 00:00:00 2001 From: Brett Chien Date: Wed, 19 Aug 2026 03:04:56 +0800 Subject: [PATCH] feat(console): Fleet detail screen reached by drill-down (slice 2/6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slice 2 of the console-navigation-ia ADR (studio#83): splits the flat, activeMembers-filtered roster out of the always-visible Fleets screen into its own "Fleet detail" screen (mockup 7.3), reached by selecting a fleet from slice 1's Fleets screen. Adds a `#fleet-detail` container (breadcrumb header + the existing roster, unchanged) that swaps places with the `#config` Fleets list โ€” `activeFleet === null` shows Fleets, a selection shows Fleet detail. `โ† Fleets` (wired) returns to the list; `+ Add instance` and `โš™` render disabled per the ADR's slice-2 scope (wired in slices 5/6). No change to roster rendering, fleet-switch logic, or any other screen โ€” purely relocation + the new breadcrumb header (`fleetDetailHeaderHtml`, unit tested). ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 5 --- console/index.html | 5 +++- console/src/main.ts | 40 +++++++++++++++++++++++++++++ console/src/render.test.ts | 21 +++++++++++++++ console/src/render.ts | 20 +++++++++++++++ console/src/styles.css | 52 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 137 insertions(+), 1 deletion(-) diff --git a/console/index.html b/console/index.html index eb4370a..5f82f25 100644 --- a/console/index.html +++ b/console/index.html @@ -33,6 +33,10 @@
+ -
diff --git a/console/src/main.ts b/console/src/main.ts index 4ba751b..eac09e6 100644 --- a/console/src/main.ts +++ b/console/src/main.ts @@ -5,6 +5,7 @@ import { renderRoster, renderIdentity, renderFleetConfig, + renderFleetDetailHeader, renderRemote, filterByMembers, deploymentKey, @@ -46,6 +47,8 @@ let agentConsole: AgentConsole | null = null; const roster = document.getElementById("roster"); const identityEl = document.getElementById("identity"); const configEl = document.getElementById("config"); +const fleetDetailEl = document.getElementById("fleet-detail"); +const fdHeaderEl = document.getElementById("fd-header"); const remoteEl = document.getElementById("remote"); const editorSection = document.getElementById("config-editor"); const editorMount = document.getElementById("cfg-editor-mount"); @@ -267,6 +270,17 @@ async function refreshRemote(): Promise { } } +// The console's top-level drill-down (ADR #83 Part A): Fleets โ†” Fleet detail. +// `activeFleet === null` shows the Fleets screen (the `#config` list); a +// selected fleet shows Fleet detail (breadcrumb header + the members roster) +// instead. This is screen 7.2 vs 7.3 โ€” slice 2's scope; the further drill into +// an Agent console (7.4) is slice 3. +function updateScreen(): void { + if (configEl) configEl.hidden = activeFleet !== null; + if (fleetDetailEl) fleetDetailEl.hidden = activeFleet === null; + if (activeFleet && fdHeaderEl) renderFleetDetailHeader(fdHeaderEl, activeFleet); +} + // Switch the active fleet by identity (name): re-point every read at its cluster // (and thus its bound credential), filter the roster to its members, and refresh // immediately โ€” so "switch fleet" == "switch managing account + roster" the ADR @@ -282,6 +296,22 @@ function selectFleet(name: string): void { if (clusterLabel) clusterLabel.textContent = `${activeFleet} ยท ${activeCluster}`; note("info", `config: switched to fleet "${activeFleet}" (cluster "${activeCluster}")`); if (configEl) renderFleetConfig(configEl, fleetConfig, activeFleet); + updateScreen(); + void refreshIdentity(); + void tick(); +} + +// The Fleet detail screen's "โ† Fleets" breadcrumb: back to the Fleets list, +// unfiltered roster (whole default cluster) until another fleet is picked. +function deselectFleet(): void { + if (!activeFleet) return; + activeFleet = null; + activeCluster = DEFAULT_CLUSTER; + activeMembers = []; + if (clusterLabel) clusterLabel.textContent = activeCluster; + note("info", "config: back to Fleets"); + if (configEl) renderFleetConfig(configEl, fleetConfig, activeFleet); + updateScreen(); void refreshIdentity(); void tick(); } @@ -395,6 +425,15 @@ if (configEl) { }); } +// The Fleet detail header: only "โ† Fleets" is wired this slice โ€” `+ Add +// instance` / `โš™` render disabled (slices 5/6 wire them). +if (fleetDetailEl) { + fleetDetailEl.addEventListener("click", (ev) => { + const target = ev.target as HTMLElement; + if (target.closest('[data-action="back-to-fleets"]')) deselectFleet(); + }); +} + // The remote panel: "Edit config" opens remote.toml in the editor; "Activate" // dials the /acp endpoint; "Disconnect" tears it down. Status then updates via // the `remote-status` event, with a refresh as a fallback. @@ -754,6 +793,7 @@ async function boot(): Promise { // Compose tab: author the template/overlay/skills library + preview the // composed bundle (agent-deployment ADR, slice 1). Self-contained; no polling. initComposeTab(); + updateScreen(); void refreshConfig(); void refreshIdentity(); void refreshRemote(); diff --git a/console/src/render.test.ts b/console/src/render.test.ts index 214832b..9dbb61e 100644 --- a/console/src/render.test.ts +++ b/console/src/render.test.ts @@ -3,6 +3,7 @@ import { rosterHtml, identityHtml, fleetConfigHtml, + fleetDetailHeaderHtml, remoteHtml, agentListHtml, agentConsoleHeaderHtml, @@ -284,6 +285,26 @@ describe("fleetConfigHtml", () => { }); }); +describe("fleetDetailHeaderHtml", () => { + it("renders the breadcrumb back to Fleets and the fleet name", () => { + const html = fleetDetailHeaderHtml("oab-prod-orca"); + expect(html).toContain('data-action="back-to-fleets"'); + expect(html).toContain("oab-prod-orca"); + }); + + it("renders the deploy and debug-drawer entry points disabled (slice 2 scope)", () => { + const html = fleetDetailHeaderHtml("oab-prod-orca"); + expect(html).toContain('data-action="add-instance" disabled'); + expect(html).toContain('data-action="fleet-debug" disabled'); + }); + + it("escapes the fleet name", () => { + const html = fleetDetailHeaderHtml(""); + expect(html).toContain("<x>"); + expect(html).not.toContain(""); + }); +}); + describe("remoteHtml", () => { it("shows the endpoint and an Activate button when configured + disconnected", () => { const html = remoteHtml(FIXTURE_REMOTE_CONFIG); diff --git a/console/src/render.ts b/console/src/render.ts index 85b07f0..42a324e 100644 --- a/console/src/render.ts +++ b/console/src/render.ts @@ -255,6 +255,26 @@ export function renderFleetConfig( el.innerHTML = fleetConfigHtml(cfg, activeFleet); } +// ---- Fleet detail screen header (ADR #83 slice 2) ----------------------------- +// The breadcrumb + action row shown above the roster once a fleet is selected โ€” +// "โ† Fleets" returns to the Fleets screen (Part A's drill-down). `[+ Add +// instance]` and `[โš™]` are the slice 5/6 entry points (deploy, Debug drawer); +// stubbed disabled here per the ADR's slice-2 scope ("No wiring yet"). +export function fleetDetailHeaderHtml(fleetName: string): string { + return `
+ + / + ${escapeHtml(fleetName)} + + + +
`; +} + +export function renderFleetDetailHeader(el: HTMLElement, fleetName: string): void { + el.innerHTML = fleetDetailHeaderHtml(fleetName); +} + // ---- Remote reverse-MCP connection panel (Part B) --------------------------- // The "activate the remote connection" surface: the /acp endpoint, live status, // an explicit Activate/Disconnect button, and Edit config. Since #69 the editor diff --git a/console/src/styles.css b/console/src/styles.css index 2187799..cc63d11 100644 --- a/console/src/styles.css +++ b/console/src/styles.css @@ -598,6 +598,58 @@ button.act:disabled { border-color: var(--s-starting); } +/* ---- Fleet detail screen (ADR #83 slice 2: 7.3) ---- */ +.fleet-detail-wrap { + margin: 0 0 16px; +} +.fd-head { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + border: 1px solid var(--border); + border-bottom: none; + border-radius: 6px 6px 0 0; + background: var(--panel); +} +.fd-back, +.fd-btn { + cursor: pointer; + border: 1px solid var(--border); + border-radius: 5px; + background: var(--bg); + color: var(--muted); + font: inherit; + font-size: 12px; + padding: 3px 10px; +} +.fd-back:hover { + color: var(--text); + border-color: var(--s-starting); +} +.fd-btn:disabled { + opacity: 0.5; + cursor: default; +} +.fd-gear { + padding: 3px 8px; +} +.fd-sep { + color: var(--muted); +} +.fd-name { + font-weight: 600; + font-size: 14px; +} +.fd-spacer { + flex: 1 1 auto; +} +.fleet-detail-wrap .roster-wrap { + border: 1px solid var(--border); + border-radius: 0 0 6px 6px; + background: var(--panel); +} + /* ---- fleets.toml editor (slice C) ---- */ .cfg-editor-wrap { margin: 0 0 12px;