diff --git a/console/index.html b/console/index.html index 5d29f1e..7b71010 100644 --- a/console/index.html +++ b/console/index.html @@ -181,18 +181,18 @@
-
- -
- -
+ diff --git a/console/src/config.ts b/console/src/config.ts index f1c7177..4654294 100644 --- a/console/src/config.ts +++ b/console/src/config.ts @@ -38,7 +38,7 @@ export function initConfigTab(hooks: ConfigHooks = {}): void { const profile = document.getElementById("cfg-profile") as HTMLInputElement | null; const region = document.getElementById("cfg-region") as HTMLInputElement | null; const cluster = document.getElementById("cfg-cluster") as HTMLInputElement | null; - const save = document.getElementById("cfg-save") as HTMLButtonElement | null; + const save = document.getElementById("debug-cfg-save") as HTMLButtonElement | null; const status = document.getElementById("cfg-status"); if (!form || !profile || !region || !cluster) return; diff --git a/console/src/main.ts b/console/src/main.ts index 1f7fb7b..18fe630 100644 --- a/console/src/main.ts +++ b/console/src/main.ts @@ -70,17 +70,22 @@ const chatStopEl = document.getElementById("chat-stop") as HTMLButtonElement | n const chatConnEl = document.getElementById("chat-conn"); const source = defaultSource(); -// Two tabs, one pane each: Activity (lifecycle + failures) and MCP (the raw -// oab-mcp JSON-RPC interaction). `data-target` links a tab to its pane id. -const tabs = Array.from( - document.querySelectorAll("#tabs .tab"), +// The Debug drawer (ADR #83 Part D / slice 6, 7.6): Activity/MCP/Config +// collapsed off the top-level nav into one secondary panel, opened by any +// `[⚙]`. Internally it keeps the same three-tab switch the top-level strip +// used to have — `data-target` still links a tab to its pane id. +const debugDrawer = document.getElementById("debug-drawer"); +const debugTitleEl = document.getElementById("debug-title"); +const debugTabs = Array.from( + document.querySelectorAll("#debug-tabs .tab"), ); -let activeTarget = tabs.find((t) => t.classList.contains("is-active"))?.dataset - .target; +let debugActiveTarget = debugTabs.find((t) => + t.classList.contains("is-active"), +)?.dataset.target; -function show(target: string): void { - activeTarget = target; - for (const tab of tabs) { +function showDebugTab(target: string): void { + debugActiveTarget = target; + for (const tab of debugTabs) { const on = tab.dataset.target === target; tab.classList.toggle("is-active", on); if (on) tab.classList.remove("has-new"); @@ -88,16 +93,32 @@ function show(target: string): void { if (pane) pane.hidden = !on; } } -for (const tab of tabs) { - tab.addEventListener("click", () => show(tab.dataset.target ?? "")); +for (const tab of debugTabs) { + tab.addEventListener("click", () => showDebugTab(tab.dataset.target ?? "")); } -// Flag a tab when its (hidden) pane gets new lines, so nothing is missed. +// Flag a debug tab when its (hidden) pane gets new lines, so nothing is missed +// even while the drawer itself is closed. function flag(target: string): void { - if (target === activeTarget) return; - tabs.find((t) => t.dataset.target === target)?.classList.add("has-new"); + if (target === debugActiveTarget && debugDrawer && !debugDrawer.hidden) return; + debugTabs.find((t) => t.dataset.target === target)?.classList.add("has-new"); } +function openDebugDrawer(scopeLabel: string): void { + if (!debugDrawer) return; + if (debugTitleEl) debugTitleEl.textContent = `Debug: ${scopeLabel}`; + debugDrawer.hidden = false; +} + +function closeDebugDrawer(): void { + if (!debugDrawer) return; + debugDrawer.hidden = true; +} + +document + .getElementById("debug-close") + ?.addEventListener("click", closeDebugDrawer); + const activity = logEl ? createPane(logEl, () => flag("log")) : null; const mcp = mcpEl ? createPane(mcpEl, () => flag("mcpio")) : null; @@ -479,14 +500,19 @@ if (configEl) { deployPanel?.open({ kind: "new-fleet" }); return; } + const debugBtn = target.closest('[data-action="fleet-debug"]'); + if (debugBtn) { + openDebugDrawer(debugBtn.dataset.fleet ?? ""); + return; + } const btn = target.closest("[data-fleet]"); if (btn?.dataset.fleet) selectFleet(btn.dataset.fleet); }); } // The Fleet detail header: "← Fleets" backs out; "+ Add instance" opens the -// deploy panel scoped to the active fleet (7.5.2). `⚙` still renders disabled -// (slice 6 wires the Debug drawer). +// deploy panel scoped to the active fleet (7.5.2); `⚙` opens the Debug drawer +// scoped to the active fleet (slice 6, 7.6). if (fleetDetailEl) { fleetDetailEl.addEventListener("click", (ev) => { const target = ev.target as HTMLElement; @@ -496,6 +522,10 @@ if (fleetDetailEl) { } if (target.closest('[data-action="add-instance"]') && activeFleet) { deployPanel?.open({ kind: "add-instance", fleetName: activeFleet }); + return; + } + if (target.closest('[data-action="fleet-debug"]') && activeFleet) { + openDebugDrawer(activeFleet); } }); } diff --git a/console/src/render.test.ts b/console/src/render.test.ts index 681f33c..96b5a10 100644 --- a/console/src/render.test.ts +++ b/console/src/render.test.ts @@ -294,6 +294,14 @@ describe("fleetConfigHtml", () => { ).toContain('data-action="new-fleet"'); }); + it("gives each fleet row its own Debug-drawer gear, scoped by fleet name (ADR #83 slice 6, 7.2)", () => { + const html = fleetConfigHtml(FIXTURE_FLEET_CONFIG, "orca"); + const gears = html.match(/data-action="fleet-debug"/g) ?? []; + expect(gears.length).toBe(FIXTURE_FLEET_CONFIG.fleets.length); + expect(html).toContain('data-action="fleet-debug" data-fleet="orca"'); + expect(html).toContain('data-action="fleet-debug" data-fleet="mira"'); + }); + it("renders an unavailable state for null", () => { expect(fleetConfigHtml(null, null)).toContain("fleet config unavailable"); }); @@ -314,11 +322,12 @@ describe("fleetDetailHeaderHtml", () => { expect(html).toContain("oab-prod-orca"); }); - it("wires + Add instance (slice 5) but leaves the Debug drawer disabled (slice 6 scope)", () => { + it("wires + Add instance (slice 5) and the Debug drawer gear (slice 6)", () => { const html = fleetDetailHeaderHtml("oab-prod-orca"); expect(html).toContain('data-action="add-instance"'); expect(html).not.toContain('data-action="add-instance" disabled'); - expect(html).toContain('data-action="fleet-debug" disabled'); + expect(html).toContain('data-action="fleet-debug"'); + expect(html).not.toContain('data-action="fleet-debug" disabled'); }); it("escapes the fleet name", () => { diff --git a/console/src/render.ts b/console/src/render.ts index afbb248..ba569cb 100644 --- a/console/src/render.ts +++ b/console/src/render.ts @@ -211,18 +211,27 @@ function membersLine(f: FleetConfig["fleets"][number]): string { return `${chips}`; } +// The `[⚙]` sits beside, not inside, the switch button — a fleet row is two +// independent click targets (select vs. debug), not one giant button, so +// they're siblings under a `.fleets-row` wrapper rather than nested +// ``; + const name = escapeHtml(f.name); + return `
+ + +
`; } // Pure: the fleet-binding config -> the config panel HTML. A fleet is a @@ -268,8 +277,8 @@ export function renderFleetConfig( // 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]` is the slice 5 entry point (7.5.2: deploy into this fleet, no new -// fleet-identity step). `[⚙]` is the slice 6 Debug-drawer entry point — -// stubbed disabled here, still out of scope. +// fleet-identity step). `[⚙]` is the slice 6 entry point — opens the Debug +// drawer (Activity/MCP/Config) scoped to this fleet. export function fleetDetailHeaderHtml(fleetName: string): string { return `
@@ -277,7 +286,7 @@ export function fleetDetailHeaderHtml(fleetName: string): string { ${escapeHtml(fleetName)} - +
`; } diff --git a/console/src/styles.css b/console/src/styles.css index 2e54d37..ec341d7 100644 --- a/console/src/styles.css +++ b/console/src/styles.css @@ -202,6 +202,57 @@ body { } } +/* Compose (agent-deployment ADR): always-visible now that it's the only thing + left in this position — Activity/MCP/Config moved into the Debug drawer + below (ADR #83 Part D / slice 6), so there's no longer a tab strip to + switch away from it. */ +.compose-standalone { + border-bottom: 1px solid var(--border); + background: var(--panel); + padding: 10px 12px 0; +} +.compose-standalone-head { + display: flex; + align-items: baseline; + gap: 8px; + margin-bottom: 4px; +} + +/* ---- Debug drawer (ADR #83 Part D / slice 6, 7.6) — Activity/MCP/Config, + collapsed off the top-level nav, opened by any `[⚙]`. Slides over the right + edge; container shape left as an overlay (ADR Open question 10.2 — not a + final answer, the simplest thing that satisfies "one affordance, from + anywhere"). ---- */ +.debug-drawer { + position: fixed; + top: 0; + right: 0; + bottom: 0; + width: min(520px, 92vw); + background: var(--panel); + border-left: 1px solid var(--border); + box-shadow: -8px 0 24px rgba(0, 0, 0, 0.25); + z-index: 50; + display: flex; + flex-direction: column; + padding: 12px 0 0; + overflow: auto; +} +.debug-head { + display: flex; + align-items: center; + gap: 10px; + padding: 0 12px 10px; +} +.debug-title { + font-size: 13px; + font-weight: 600; + color: var(--text); +} +.debug-spacer { + flex: 1; +} + /* Two tabbed panes — the first display, so launch + core lifecycle and the raw MCP interaction are one click apart and visible immediately. */ .logs { @@ -573,8 +624,17 @@ button.act:disabled { grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 8px; } +.fleets-row { + display: flex; + align-items: stretch; + gap: 6px; +} +.fleets-row .fd-gear { + flex: 0 0 auto; +} .cfg-fleet { display: flex; + flex: 1; flex-direction: column; gap: 2px; text-align: left;