From 7d40150c6a3ec13ac5edb20dff2cf9cec20110a8 Mon Sep 17 00:00:00 2001 From: Brett Chien Date: Mon, 7 Sep 2026 18:24:09 +0800 Subject: [PATCH] feat(console): drill into a k8s fleet's roster instead of declining MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit studio#146 slice 4 of 4 (final slice) — the console-side fix for the originally-reported bug: clicking a k8s-runtime fleet in the Fleets panel now actually opens Fleet detail and loads its roster, instead of `selectFleet` showing an info toast and refusing to switch. - `source.ts`: `listDeployments`/`runtimeContext` gain an optional `fleet` param, forwarded to the Tauri bridge (#150) alongside `cluster`. - `main.ts`: `selectFleet` branches on `fleet.runtime` instead of declining for k8s — `activeCluster` becomes an unused "" sentinel for a k8s fleet (reads now go by `fleet` name, which oab-mcp resolves to the bound context/namespace, #149), and the header label shows `context/namespace` instead of a cluster. `tick()` now always passes `activeFleet` as `fleet` to `listDeployments` (a no-op for the no-fleet-selected case, and for an ecs fleet this only makes oab-mcp's existing member-side filtering redundant with the console's own `filterByMembers` — same result, not a behavior change). - `types.ts`: `RuntimeContext.cluster` is `string | null` now (`null` for a k8s-runtime fleet, with `context`/`namespace` set instead); `FleetBinding` gained optional `context`/`namespace` fields alongside the existing `profile`/`region`. `runtimeContext()` itself has no console call site yet (true before this change too) — this is type-contract parity with oab-mcp's tool, not new UI. **Not in scope** (matches the issue body): start/stop (scale) for a k8s roster row isn't wired — `scale()` still calls `scaleDeployment` without a `fleet` arg, so it'll hit the ecs-only `deploy_scale` path and fail with a clear error toast (not silently) if attempted against a k8s deployment. That's a separate action-surface follow-up, not blocking this read-only roster view. ## Test plan - `npm test`: 107/107 passing (no new tests — this is control-flow wiring over already-tested pure functions; `filterByMembers`/ `serviceName` already have k8s-shaped-data coverage from the delete-fleet/ member-name-preview work). - `npm run typecheck`: clean. - `npm run build`: clean. - Manual: no live k8s cluster in this sandbox to exercise end-to-end — same caveat #149 already carries. 🤖 Generated with Claude Code --- console/src/main.ts | 31 +++++++++++++++---------------- console/src/source.ts | 17 +++++++++++------ console/src/types.ts | 23 ++++++++++++++++------- 3 files changed, 42 insertions(+), 29 deletions(-) diff --git a/console/src/main.ts b/console/src/main.ts index 3188c34..c00780f 100644 --- a/console/src/main.ts +++ b/console/src/main.ts @@ -352,7 +352,7 @@ function repaintRoster(): void { async function tick(): Promise { if (!roster) return; try { - const all = await source.listDeployments(activeCluster); + const all = await source.listDeployments(activeCluster, activeFleet ?? undefined); // Filter to the active fleet's members (empty ⇒ whole cluster). const deployments = filterByMembers(all, activeMembers); lastDeployments = deployments; @@ -450,24 +450,23 @@ function selectFleet(name: string): void { if (!name || name === activeFleet) return; const fleet = fleetConfig?.fleets.find((f) => f.name === name); if (!fleet) return; - // k8s fleets have no ECS cluster to point reads at — listDeployments/ - // runtimeContext/tick are all cluster-keyed and ecs-only today (a separate, - // larger gap: k8s fleets have no roster/observe UI yet, tracked apart from - // this schema unification). Don't pretend a switch worked when reads would - // silently break against an empty cluster string. - if (fleet.runtime === "k8s" || fleet.cluster === null) { - note( - "info", - `fleet "${name}" is a k8s fleet — switching the roster view to it isn't wired up yet`, - ); - return; - } closeOpenAgentConsole(); activeFleet = name; - activeCluster = fleet.cluster; activeMembers = fleet.members; - if (clusterLabel) clusterLabel.textContent = `${activeFleet} · ${activeCluster}`; - note("info", `config: switched to fleet "${activeFleet}" (cluster "${activeCluster}")`); + if (fleet.runtime === "k8s") { + // No ECS cluster for a k8s fleet — reads now go by the `fleet` name + // instead (oab-mcp resolves the bound context/namespace itself, + // studio#146 slices 2-3); `activeCluster` stays a plain "" sentinel, + // never read on this path. + activeCluster = ""; + const where = `${fleet.context ?? "current-context"}/${fleet.namespace ?? "default"}`; + if (clusterLabel) clusterLabel.textContent = `${activeFleet} · ${where}`; + note("info", `config: switched to fleet "${activeFleet}" (k8s ${where})`); + } else { + activeCluster = fleet.cluster ?? DEFAULT_CLUSTER; + if (clusterLabel) clusterLabel.textContent = `${activeFleet} · ${activeCluster}`; + note("info", `config: switched to fleet "${activeFleet}" (cluster "${activeCluster}")`); + } if (configEl) renderFleetConfig(configEl, fleetConfig, activeFleet); updateScreen(); void tick(); diff --git a/console/src/source.ts b/console/src/source.ts index 19a81e8..1a40f47 100644 --- a/console/src/source.ts +++ b/console/src/source.ts @@ -24,8 +24,13 @@ import { // A read source for the console. Desktop (Tauri → studio-cp) and the standalone // browser build implement this identically, so the UI never knows which it is. export interface Source { - listDeployments(cluster?: string): Promise; - runtimeContext(cluster?: string): Promise; + // `fleet` (studio#146 slices 2-4) is required to reach a k8s-runtime + // fleet's roster/identity — `cluster` has no k8s equivalent to resolve + // against, since a k8s fleet targets a (context, namespace) instead. Safe + // to pass alongside `cluster` for an ecs-runtime fleet too: oab-mcp + // resolves the fleet's own cluster from the binding either way. + listDeployments(cluster?: string, fleet?: string): Promise; + runtimeContext(cluster?: string, fleet?: string): Promise; fleetConfig(): Promise; // Persist the raw TOML `text` of the config file, returning the reloaded // config. Rejects (without writing) when the text doesn't parse. @@ -158,11 +163,11 @@ export class TauriSource implements Source { if (!invoke) throw new Error("Tauri bridge unavailable"); return invoke; } - async listDeployments(cluster?: string): Promise { - return this.invoke()("deploy_list", { cluster }); + async listDeployments(cluster?: string, fleet?: string): Promise { + return this.invoke()("deploy_list", { cluster, fleet }); } - async runtimeContext(cluster?: string): Promise { - return this.invoke()("runtime_context", { cluster }); + async runtimeContext(cluster?: string, fleet?: string): Promise { + return this.invoke()("runtime_context", { cluster, fleet }); } async fleetConfig(): Promise { return this.invoke()("fleet_config"); diff --git a/console/src/types.ts b/console/src/types.ts index e246da9..d02b678 100644 --- a/console/src/types.ts +++ b/console/src/types.ts @@ -34,12 +34,16 @@ export interface Deployment { instances: InstancePhase[]; } -// The fleet binding in effect for a cluster (ADR #19). Mirrors oab-mcp's -// `runtime_context.binding`. +// The fleet binding in effect for a cluster or k8s context (ADR #19). +// Mirrors oab-mcp's `runtime_context.binding` — `profile`/`region` for an +// ecs-runtime fleet, `context`/`namespace` for a k8s-runtime one (studio#146 +// slice 2); the other set is absent for a given entry. export interface FleetBinding { name: string; - profile: string | null; - region: string | null; + profile?: string | null; + region?: string | null; + context?: string | null; + namespace?: string | null; expected_principal: string | null; } @@ -160,10 +164,15 @@ export interface FsCapability { } // The effective runtime identity/context the control plane resolved for a -// cluster — mirrors oab-mcp's `runtime_context` tool (ADR #19). `identity_matches` -// is `null` when the binding declares no `expected_principal`. +// cluster or k8s-runtime fleet — mirrors oab-mcp's `runtime_context` tool +// (ADR #19). `cluster` is `null` for a k8s-runtime fleet (`context`/ +// `namespace` are set instead, studio#146 slice 2) — always check `cluster` +// before assuming an ecs shape. `identity_matches` is `null` when the +// binding declares no `expected_principal`. export interface RuntimeContext { - cluster: string; + cluster: string | null; + context?: string | null; + namespace?: string | null; principal: string; principal_kind: string; // "role" | "user" | "unknown" scope: string; // AWS account id