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
1 change: 1 addition & 0 deletions console/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@
<button type="button" id="deploy-name-shuffle" class="cfg-btn cfg-btn-ghost" title="Shuffle name">🔀</button>
</span>
</label>
<p class="config-hint" id="deploy-name-preview"></p>
<div class="compose-actions">
<button type="submit" id="deploy-deploy-btn">Deploy</button>
<span class="compose-status" id="deploy-deploy-status"></span>
Expand Down
24 changes: 23 additions & 1 deletion console/src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ export function initDeployPanel(deps: DeployPanelDeps): DeployPanelHandle | null
const acpTokenGenerateBtn = document.getElementById("deploy-acp-token-generate") as HTMLButtonElement | null;
const agentNameInput = document.getElementById("deploy-name") as HTMLInputElement | null;
const agentNameShuffleBtn = document.getElementById("deploy-name-shuffle") as HTMLButtonElement | null;
const agentNamePreviewEl = document.getElementById("deploy-name-preview");
const deployBtn = document.getElementById("deploy-deploy-btn") as HTMLButtonElement | null;
const deployStatusEl = document.getElementById("deploy-deploy-status");

Expand Down Expand Up @@ -211,6 +212,7 @@ export function initDeployPanel(deps: DeployPanelDeps): DeployPanelHandle | null
!acpTokenGenerateBtn ||
!agentNameInput ||
!agentNameShuffleBtn ||
!agentNamePreviewEl ||
!deployBtn
) {
return null;
Expand Down Expand Up @@ -328,6 +330,7 @@ export function initDeployPanel(deps: DeployPanelDeps): DeployPanelHandle | null
applyChatPlatformMode();
applyVendorMode();
agentNameInput.value = randomGreekName();
updateNamePreview();
void loadVendorImage();
};

Expand All @@ -346,6 +349,19 @@ export function initDeployPanel(deps: DeployPanelDeps): DeployPanelHandle | null
? k8sNamespaceNewInput.value.trim()
: k8sNamespaceSel.value;

// The `oab-${namespace}-${name}` service-name convention (mirrored from the
// deploy submit handler below) was previously discoverable only by reading
// source — nothing in the wizard showed what actually lands in fleets.toml's
// `members` array (Brett, 2026-09-07). "add-instance" into an existing k8s
// fleet isn't wired through this wizard yet (see the isK8s check below), so
// it's always the ecs/"default" namespace outside "new-fleet" + k8s.
const updateNamePreview = (): void => {
const isK8s = mode?.kind === "new-fleet" && providerSel.value === "k8s";
const namespace = isK8s ? currentNamespace() || "default" : "default";
const name = agentNameInput.value.trim() || "<name>";
agentNamePreviewEl.textContent = `→ recorded in fleets.toml as oab-${namespace}-${name}`;
};

// Toggle the AWS/k8s field groups per studio#104's design: switching
// providers resets which group is visible; field *values* aren't cleared
// here (identityForm.reset() already did that on open/close) since the
Expand Down Expand Up @@ -441,6 +457,7 @@ export function initDeployPanel(deps: DeployPanelDeps): DeployPanelHandle | null
providerSel.addEventListener("change", () => {
showProviderFields(providerSel.value);
if (providerSel.value === "k8s") void loadK8sContexts();
updateNamePreview();
});
k8sContextSel.addEventListener("change", () => {
void loadK8sNamespaces();
Expand All @@ -449,10 +466,13 @@ export function initDeployPanel(deps: DeployPanelDeps): DeployPanelHandle | null
k8sNamespaceSel.addEventListener("change", () => {
applyNamespaceMode();
void loadK8sServiceAccounts();
updateNamePreview();
});
// "change" (fires on commit/blur), not "input" (every keystroke) — avoids a
// tool call per character typed into the new-namespace field.
// tool call per character typed into the new-namespace field. The name
// preview updates live regardless (no tool call involved).
k8sNamespaceNewInput.addEventListener("change", () => void loadK8sServiceAccounts());
k8sNamespaceNewInput.addEventListener("input", updateNamePreview);

vendorSel.addEventListener("change", () => {
applyVendorMode();
Expand All @@ -470,7 +490,9 @@ export function initDeployPanel(deps: DeployPanelDeps): DeployPanelHandle | null
});
agentNameShuffleBtn.addEventListener("click", () => {
agentNameInput.value = randomGreekName();
updateNamePreview();
});
agentNameInput.addEventListener("input", updateNamePreview);

const open = (m: DeployMode): void => {
mode = m;
Expand Down
46 changes: 45 additions & 1 deletion console/src/fleetToml.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
import { appendMember, appendFleetBlock, fleetBlockExists } from "./fleetToml";
import { appendMember, appendFleetBlock, fleetBlockExists, removeFleetBlock } from "./fleetToml";

describe("appendMember", () => {
const text = `default_cluster = "oab"
Expand Down Expand Up @@ -136,3 +136,47 @@ describe("appendFleetBlock", () => {
expect(out).toBe('default_cluster = "oab"\n\n[fleet.x]\nruntime = "ecs"\nmembers = ["m"]\n');
});
});

describe("removeFleetBlock", () => {
const text = `default_cluster = "oab"

[fleet.a]
members = ["oab-default-a1"]

[fleet.b]
members = ["oab-default-b1"]

[fleet.c]
members = ["oab-default-c1"]
`;

it("removes a middle block, leaving one blank line between its neighbors", () => {
const out = removeFleetBlock(text, "b");
expect(out).toBe(
'default_cluster = "oab"\n\n[fleet.a]\nmembers = ["oab-default-a1"]\n\n[fleet.c]\nmembers = ["oab-default-c1"]\n',
);
});

it("removes the first block with no leading blank line left behind", () => {
const out = removeFleetBlock(text, "a");
expect(out).toBe(
'default_cluster = "oab"\n\n[fleet.b]\nmembers = ["oab-default-b1"]\n\n[fleet.c]\nmembers = ["oab-default-c1"]\n',
);
});

it("removes the last block with no trailing blank line left behind", () => {
const out = removeFleetBlock(text, "c");
expect(out).toBe(
'default_cluster = "oab"\n\n[fleet.a]\nmembers = ["oab-default-a1"]\n\n[fleet.b]\nmembers = ["oab-default-b1"]\n',
);
});

it("removes the only fleet block, leaving the rest of the file intact", () => {
const onlyOne = 'default_cluster = "oab"\n\n[fleet.a]\nmembers = ["oab-default-a1"]\n';
expect(removeFleetBlock(onlyOne, "a")).toBe('default_cluster = "oab"\n');
});

it("is a no-op when the fleet isn't found", () => {
expect(removeFleetBlock(text, "no-such-fleet")).toBe(text);
});
});
17 changes: 17 additions & 0 deletions console/src/fleetToml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,23 @@ export function appendMember(text: string, fleetName: string, member: string): s
return text.slice(0, block.headerEnd) + newBody + text.slice(block.end);
}

// Remove a `[fleet.<name>]` block entirely — the inverse of `appendFleetBlock`
// (Fleet detail's "Delete fleet" action: the console had no way to remove a
// fleet short of hand-editing raw TOML via "Edit config"). `findFleetBlock`'s
// `end` already lands exactly at the next block's `[` (or EOF), so the blank
// line `appendFleetBlock` writes *before* each block travels with `before`,
// not `after` — a plain concatenation needs no separator patching. The
// `\n{3,}` collapse and single trailing newline are defensive tidy-ups, not
// load-bearing (see fleetToml.test.ts for why each is safe). A no-op if the
// fleet isn't found.
export function removeFleetBlock(text: string, name: string): string {
const block = findFleetBlock(text, name);
if (!block) return text;
const result = text.slice(0, block.start) + text.slice(block.end);
const collapsed = result.replace(/\n{3,}/g, "\n\n");
return collapsed.trim() ? collapsed.replace(/\n+$/, "\n") : "";
}

export interface NewFleetEntry {
name: string;
member: string;
Expand Down
35 changes: 30 additions & 5 deletions console/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defaultSource } from "./source";
import { initConfigTab } from "./config";
import { removeFleetBlock } from "./fleetToml";
import {
renderRoster,
renderFleetConfig,
Expand Down Expand Up @@ -487,6 +488,31 @@ function deselectFleet(): void {
void tick();
}

// Fleet detail's "Delete fleet" button: the only way to remove a fleet used
// to be hand-editing raw TOML via "Edit config" (Brett, 2026-09-07). This is
// the same text edit (`removeFleetBlock`), just behind a confirm dialog —
// removes the `[fleet.<name>]` declaration only, it does not touch the
// underlying deployments/instances themselves.
async function deleteFleet(name: string): Promise<void> {
if (
!window.confirm(
`Delete fleet "${name}"? This removes it from fleets.toml — the instances themselves keep running.`,
)
) {
return;
}
try {
const current = fleetConfig ?? (await source.fleetConfig());
fleetConfig = await source.writeFleetConfig(removeFleetBlock(current.text, name));
note("info", `config: deleted fleet "${name}"`);
} catch (e) {
note("error", `config: delete fleet failed — ${errText(e)}`);
return;
}
if (activeFleet === name) deselectFleet();
else if (configEl) renderFleetConfig(configEl, fleetConfig, activeFleet);
}

// After a deploy panel run succeeds (deploy_provision + fleets.toml write both
// landed — `deploy.ts` guarantees that ordering): re-read fleets.toml, then
// either land on the new fleet's detail screen (7.5.1 step 4) or, if we're
Expand Down Expand Up @@ -681,11 +707,6 @@ 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);
});
Expand All @@ -707,6 +728,10 @@ if (fleetDetailEl) {
}
if (target.closest('[data-action="fleet-debug"]') && activeFleet) {
openDebugDrawer(activeFleet);
return;
}
if (target.closest('[data-action="delete-fleet"]') && activeFleet) {
void deleteFleet(activeFleet);
}
});
}
Expand Down
12 changes: 7 additions & 5 deletions console/src/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,12 +234,9 @@ 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)", () => {
it("has no per-card Debug gear on the Fleets list (removed 2026-09-07 — Fleet detail's own [⚙] covers it)", () => {
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"');
expect(html).not.toContain('data-action="fleet-debug"');
});

it("renders an unavailable state for null", () => {
Expand Down Expand Up @@ -270,6 +267,11 @@ describe("fleetDetailHeaderHtml", () => {
expect(html).not.toContain('data-action="fleet-debug" disabled');
});

it("wires the Delete fleet action", () => {
const html = fleetDetailHeaderHtml("oab-prod-orca");
expect(html).toContain('data-action="delete-fleet"');
});

it("escapes the fleet name", () => {
const html = fleetDetailHeaderHtml("<x>");
expect(html).toContain("&lt;x&gt;");
Expand Down
23 changes: 12 additions & 11 deletions console/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,27 +170,24 @@ 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).
// The Fleets-list card for one fleet — clicking it selects the fleet
// (drills into Fleet detail). The list used to carry its own `[⚙]` Debug
// shortcut beside each card (slice 6), removed (Brett, 2026-09-07) since
// Fleet detail's own `[⚙]` (`fleetDetailHeaderHtml`) already covers it once
// drilled in, and having it on both screens read as clutter.
function fleetButton(
f: FleetConfig["fleets"][number],
activeFleet: string | null,
): string {
const active = f.name === activeFleet;
const cls = active ? "cfg-fleet is-active" : "cfg-fleet";
const name = escapeHtml(f.name);
return `<div class="fleets-row">
<button class="${cls}" type="button" data-fleet="${name}" aria-pressed="${active}">
return `<button class="${cls}" type="button" data-fleet="${name}" aria-pressed="${active}">
<span class="cfg-name">${escapeHtml(f.name || locationLine(f))}</span>
<span class="cfg-cluster">${escapeHtml(locationLine(f))}</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>`;
</button>`;
}

// Pure: the fleet-binding config -> the config panel HTML. A fleet is a
Expand Down Expand Up @@ -237,7 +234,10 @@ export function renderFleetConfig(
// "← 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 entry point — opens the Debug
// drawer (Activity/MCP/Config) scoped to this fleet.
// drawer (Activity/MCP/Config) scoped to this fleet. `[Delete fleet]` is a
// follow-up (Brett, 2026-09-07): the only way to remove a fleet before this
// was hand-editing raw TOML via "Edit config" — this just does that same
// text edit (`removeFleetBlock`) behind a button + confirm, in `main.ts`.
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>
Expand All @@ -246,6 +246,7 @@ export function fleetDetailHeaderHtml(fleetName: string): string {
<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" title="Debug: ${escapeHtml(fleetName)}">⚙</button>
<button class="fd-btn fd-danger" type="button" data-action="delete-fleet" title="Delete fleet: ${escapeHtml(fleetName)}">Delete fleet</button>
</div>`;
}

Expand Down
12 changes: 4 additions & 8 deletions console/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -712,14 +712,6 @@ 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;
Expand Down Expand Up @@ -830,6 +822,10 @@ button.act:disabled {
.fd-gear {
padding: 3px 8px;
}
.fd-danger:hover {
color: var(--s-unhealthy);
border-color: var(--s-unhealthy);
}
.fd-sep {
color: var(--muted);
}
Expand Down
Loading