feat(web): make the conductor and its workers legible in the session list - #320
Conversation
…list First slice of P5.1 (docs/conductor-frontends-design.md §12). The conductor has been drivable since P3 — `codeoid attach conductor` works — but in the web UI it renders as an ordinary row called "conductor", and its dispatch workers appear as unrelated top-level sessions with opaque names like `worker-scout-c0234348`. Nothing on screen says the fleet is a fleet. Groups dispatch workers under the conductor. They carry no parent pointer — the conductor→worker link lives in the dispatch queue, not on SessionInfo — but the daemon enforces one conductor per tenant, so it is an unambiguous parent. Reuses the existing collaboration grouping rather than adding a second mechanism, keeping `groupFleet`'s three properties: pure and testable without a reactive root, uniform shape so the renderer never branches on "is this a fleet", and orphans promoted rather than dropped (a worker whose conductor is not in the list still renders, at top level, instead of vanishing). `isConductor` is kept separate from `isFleet` rather than folded into one "has children" flag, because the two group kinds differ in what their children ARE: a collaboration's role-children are long-lived sessions you drive, while dispatch workers are disposable and die with their task. Adds two badges. `cond` marks the conductor, in the product accent rather than a semantic colour: the status vocabulary owns warn/danger/success, and "this row is the conductor" is an identity, not a state — a conductor in trouble must still read as `error` from its StatusDot. `ship`/`scout` marks a worker's contract, with scout quiet and ship warn-coloured for the same reason WriteBadge flags a writing role. Worker shape is read from the daemon's `worker-<shape>-<task>` naming because SessionInfo carries no shape field — the ship/scout contract lives on the dispatch task, which the session list does not join against. Guarded on `role === "worker"` so a user-named session starting with `worker-` is never mislabelled; only the daemon sets that role. Verified against real daemon output (`worker-scout-c0234348`, role `worker`). Workers sort oldest-first with a name tiebreak, not by createdAt alone: several can spawn in the same millisecond, and a list that reorders while you read it is worse than one that looks stale. Filtering now matches on role and shape, so "scout" or "conductor" finds these rows — the name alone only carries the shape by convention. Verification: 22 fleet unit tests (7 new), 35 across both lib suites, tsc + eslint + production build clean. Known gap, pre-existing: the jsdom test environment is broken locally (html-encoding-sniffer requires @exodus/bytes as CJS, but it resolves ESM-only), so no component test in web/ can run — 6 files including SessionListPane.test.tsx. The badge rendering is therefore covered only by typecheck and build here. Untouched lockfile; needs its own fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🔮 Oracle Review
🎯 Start Here
web/src/components/SessionListPane.tsx (~20 min) — Security changes in SessionListPane.tsx
📋 PR Summary
What this PR does: Implements visual grouping and differentiation for conductors and their dispatch workers in the web session list UI, allowing workers to appear as children under their conductor row with role-specific badges.
Key changes:
- Workers now group under conductor rows using existing collaboration grouping logic
- Added 'cond' badge for conductors and 'ship'/'scout' badges for workers based on parsed session names
- Implemented stable sorting (oldest-first with name tiebreak) and role-based filtering
Areas affected: SessionListPane.tsx (UI rendering and badges), fleet.ts (grouping and sorting logic), Session filtering behavior
Testing notes: 22 fleet unit tests (7 new), typecheck and build verified; component tests blocked by pre-existing jsdom/ESM compatibility issue unrelated to this PR
🔍 Code Review
This is a well-architected feature that thoughtfully reuses existing patterns rather than introducing parallel mechanisms. The implementation demonstrates strong attention to edge cases with proper orphan handling, stable sorting to prevent UI jitter, and appropriate role-based guards against spoofing.
What's good:
- ✨ Reuses existing
groupFleetmechanism instead of adding a second grouping path, maintaining architectural simplicity - ✨ Orphan promotion ensures workers render at top level if their conductor is missing, rather than vanishing
- ✨ Role-guards prevent user-named sessions starting with 'worker-' from being mislabeled as workers
Generated by Oracle - Highflame's AI Code Reviewer
Review follow-up on #320 (Oracle suggestion 2). `worker-<shape>-<task>` knowledge was split across two functions: `workerShape` matched the prefix, and `roleLabel` re-derived the same prefix to slice the task tail. A change to the daemon's naming had two call sites to find, and only one of them would have failed loudly. Folds both into `parseWorkerName`, returning `{ shape, task }`. The prefix and the shape list are named constants, and the two public readers are thin accessors over it. Puts the `role === "worker"` guard where it cannot be skipped. It was already correct and tested (suggestion 1 asked only that it be verified), but it lived in `workerShape` while `roleLabel` reached the same name format independently. With one guarded parser, a caller cannot opt out of the check by choosing the other entry point — asserted by a test that spoofs `worker-ship-nope` through BOTH readers. Two behaviours the consolidation made worth pinning: a daemon name with no task tail renders as the bare shape rather than a trailing separator, and an unrecognised shape (`worker-courier-…`) is not labelled a worker at all rather than guessed at. 24 fleet tests (2 new), typecheck, lint and build clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
First slice of P5.1 (conductor-frontends-design.md §12).
Why
The conductor has been drivable since P3 —
codeoid attach conductorworks — but in the web UI it renders as an ordinary row called "conductor", and its dispatch workers appear as unrelated top-level sessions namedworker-scout-c0234348. Nothing on screen says the fleet is a fleet.What
Workers group under the conductor. They carry no parent pointer — the conductor→worker link lives in the dispatch queue, not on
SessionInfo— but the daemon enforces one conductor per tenant, so it's an unambiguous parent.This reuses the existing collaboration grouping rather than adding a second mechanism, keeping
groupFleet's three properties: pure and testable without a reactive root; uniform shape so the renderer never branches on "is this a fleet"; and orphans promoted, not dropped — a worker whose conductor isn't in the list still renders at top level rather than vanishing.isConductoris kept separate fromisFleetrather than folded into one "has children" flag, because the two group kinds differ in what their children are: a collaboration's role-children are long-lived sessions you drive; dispatch workers are disposable and die with their task.Two badges.
condmarks the conductor in the product accent rather than a semantic colour — the status vocabulary (§6) owns warn/danger/success, and "this row is the conductor" is an identity, not a state, so a conductor in trouble must still read aserrorfrom its StatusDot.ship/scoutmarks a worker's contract, scout quiet and ship warn-coloured for the same reasonWriteBadgeflags a writing role.Worker shape is parsed from the name (
worker-<shape>-<task>) becauseSessionInfocarries no shape field — the ship/scout contract lives on the dispatch task, which the session list doesn't join against. Guarded onrole === "worker"so a user-named session starting withworker-is never mislabelled; only the daemon sets that role.Two smaller judgment calls:
scoutorconductorfinds these rows.Verification
tsc -b,eslint, and the productionvite buildall cleanworker-scout-c0234348withrole: "worker", observed driving an actual dispatchNo component test in
web/can run locally. The jsdom environment is broken:html-encoding-snifferrequires@exodus/bytesas CJS, but it resolves ESM-only —That silently excludes 6 test files, including
SessionListPane.test.tsx— which is exactly the file that would cover the badge rendering here.npm run teststill reports green ("12 passed") with the failure demoted to an unhandled error, so it's easy to miss.jsdomwas also simply not installed despite being inpackage.json; installing it moved the failure from "package missing" to the ESM/CJS conflict above.bun.lockandpackage.jsonare untouched by this PR.The badge rendering is therefore covered only by typecheck and build. Worth its own fix before P5.2 adds real components.
Next
P5.2 — the docked conductor surface:
CenterPanebranches onrole === "conductor"with the state-grouped fleet rail, backed by a newstate/fleet.tsover thefleet.subscribecontract (verified working end-to-end in #319's testing).🤖 Generated with Claude Code