Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 35 additions & 15 deletions console/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -181,18 +181,18 @@
</section>
<section id="identity" class="identity-wrap"></section>
<section id="remote" class="remote-wrap"></section>
<section class="logs">
<nav class="tabs" id="tabs">
<button class="tab is-active" data-target="log">Activity</button>
<button class="tab" data-target="mcpio">MCP · oab-mcp</button>
<button class="tab" data-target="compose">Compose</button>
<button class="tab" data-target="config">Config</button>
<span class="tabs-spacer"></span>
<button class="tab-filter" id="log-level" type="button" title="Activity verbosity — INFO+ hides DEBUG (e.g. keepalives); click for DEBUG+">INFO+</button>
</nav>
<div id="log" class="log pane"></div>
<div id="mcpio" class="log pane mcpio" hidden></div>
<div id="compose" class="pane compose" hidden>
<!-- Compose (agent-deployment-templates.md: author library → preview →
deploy_provision) is the one surface Part C didn't fully relocate —
`[+ New fleet]`/`[+ Add instance]` (slice 5) reach the same engine as
an action, but where template/overlay *authoring* moves to once this
standing section goes away isn't decided in the ADR. Left in place;
flagged for the slice 7 self-review. Not part of the Debug drawer. -->
<section class="compose-standalone">
<div class="compose-standalone-head">
<span class="compose-label">Compose</span>
<span class="compose-lib-path">template ⊕ overlay authoring — golden bundle library</span>
</div>
<div id="compose" class="pane compose">
<div class="compose-grid">
<div class="compose-lib">
<div class="compose-lib-head">
Expand Down Expand Up @@ -263,7 +263,27 @@
provider drivers that land a bundle on a runtime are a later slice.
</p>
</div>
<div id="config" class="pane config" hidden>
</section>
<!-- ADR #83 Part D / slice 6 (7.6): Activity/MCP/Config collapse into a
single secondary Debug drawer, reachable via any `[⚙]` — no longer
peer-level tabs. Their internals are unchanged, only their standing
in the top-level navigation. Slides over the right edge. -->
<aside id="debug-drawer" class="debug-drawer" hidden>
<div class="debug-head">
<span class="debug-title" id="debug-title">Debug</span>
<span class="debug-spacer"></span>
<button class="cfg-btn cfg-btn-ghost" id="debug-close" type="button">Close</button>
</div>
<nav class="tabs" id="debug-tabs">
<button class="tab is-active" data-target="log">Activity</button>
<button class="tab" data-target="mcpio">MCP · oab-mcp</button>
<button class="tab" data-target="debug-config">Config</button>
<span class="tabs-spacer"></span>
<button class="tab-filter" id="log-level" type="button" title="Activity verbosity — INFO+ hides DEBUG (e.g. keepalives); click for DEBUG+">INFO+</button>
</nav>
<div id="log" class="log pane"></div>
<div id="mcpio" class="log pane mcpio" hidden></div>
<div id="debug-config" class="pane config" hidden>
<form id="config-form" class="config-form" autocomplete="off">
<label
>Cluster
Expand Down Expand Up @@ -296,7 +316,7 @@
/>
</label>
<div class="config-actions">
<button type="submit" id="cfg-save">Save &amp; reload core</button>
<button type="submit" id="debug-cfg-save">Save &amp; reload core</button>
<span class="config-status" id="cfg-status"></span>
</div>
<p class="config-hint">
Expand All @@ -307,7 +327,7 @@
</p>
</form>
</div>
</section>
</aside>
</main>
<script type="module" src="/src/main.ts"></script>
</body>
Expand Down
2 changes: 1 addition & 1 deletion console/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
62 changes: 46 additions & 16 deletions console/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,34 +70,55 @@ 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<HTMLButtonElement>("#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<HTMLButtonElement>("#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");
const pane = document.getElementById(tab.dataset.target ?? "");
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;

Expand Down Expand Up @@ -479,14 +500,19 @@ if (configEl) {
deployPanel?.open({ kind: "new-fleet" });
return;
}
const debugBtn = target.closest<HTMLElement>('[data-action="fleet-debug"]');
if (debugBtn) {
openDebugDrawer(debugBtn.dataset.fleet ?? "");
return;
}
const btn = target.closest<HTMLElement>("[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;
Expand All @@ -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);
}
});
}
Expand Down
13 changes: 11 additions & 2 deletions console/src/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
Expand All @@ -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", () => {
Expand Down
27 changes: 18 additions & 9 deletions console/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,18 +211,27 @@ function membersLine(f: FleetConfig["fleets"][number]): string {
return `<span class="cfg-members">${chips}</span>`;
}

// 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
// `<button>`s (7.2: "a `[⚙]` that opens the Debug drawer scoped to that
// fleet's Activity/MCP/Config", slice 6).
function fleetButton(
f: FleetConfig["fleets"][number],
activeFleet: string | null,
): string {
const active = f.name === activeFleet;
const cls = active ? "cfg-fleet is-active" : "cfg-fleet";
return `<button class="${cls}" type="button" data-fleet="${escapeHtml(f.name)}" aria-pressed="${active}">
<span class="cfg-name">${escapeHtml(f.name || f.cluster)}</span>
<span class="cfg-cluster">${escapeHtml(f.cluster)}</span>
${membersLine(f)}
<span class="cfg-cred">${credLine(f)}</span>
</button>`;
const name = escapeHtml(f.name);
return `<div class="fleets-row">
<button class="${cls}" type="button" data-fleet="${name}" aria-pressed="${active}">
<span class="cfg-name">${escapeHtml(f.name || f.cluster)}</span>
<span class="cfg-cluster">${escapeHtml(f.cluster)}</span>
${membersLine(f)}
<span class="cfg-cred">${credLine(f)}</span>
</button>
<button class="fd-btn fd-gear" type="button" data-action="fleet-debug" data-fleet="${name}" title="Debug: ${name}">⚙</button>
</div>`;
}

// Pure: the fleet-binding config -> the config panel HTML. A fleet is a
Expand Down Expand Up @@ -268,16 +277,16 @@ 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 `<div class="fd-head">
<button class="fd-back" type="button" data-action="back-to-fleets">&larr; Fleets</button>
<span class="fd-sep">/</span>
<span class="fd-name">${escapeHtml(fleetName)}</span>
<span class="fd-spacer"></span>
<button class="fd-btn" type="button" data-action="add-instance">+ Add instance</button>
<button class="fd-btn fd-gear" type="button" data-action="fleet-debug" disabled title="coming soon">⚙</button>
<button class="fd-btn fd-gear" type="button" data-action="fleet-debug" title="Debug: ${escapeHtml(fleetName)}">⚙</button>
</div>`;
}

Expand Down
60 changes: 60 additions & 0 deletions console/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
Loading