feat(web): Conductor ⇄ Sessions toggle - #329
Conversation
conductor-design §3 specifies that the fleet READ verbs — fleet_list, fleet_find, fleet_summary, fleet_recall, fleet_tasks, machine_map — "run silently". They did not. `isSafeTool` knew the memory and blackboard mounts but not the fleet, so in guarded mode every one of them raised an approval prompt. Found by driving a real conductor rather than reading the code: asking it to resolve a session reference produced an approval request for `fleet_find`, which is a read. An assistant that asks permission to look something up is not an assistant, and this is the first friction anyone meets in the new conductor pane (#325). The daemon already put these verbs in the provider's `allowedTools`, so the SDK warned it would auto-approve them — but codeoid's own gate is consulted independently and did not recognise them, which is why the two disagreed. Follows the established shape exactly: match the namespace prefix, then require the suffix to be a known read verb. Never a bare prefix match — an over-broad match here is a prompt bypass, which is why a look-alike segment (`mcp__evil_codeoid_fleet__…`) is asserted to gain nothing. The read list is `FLEET_READ_TOOLS` from the shared protocol package, so the send half cannot leak in by someone editing one of two copies. Send verbs remain hard-gated BEFORE this function is consulted (`isFleetSendTool` in Session#shouldAutoApprove, checked ahead of any mode logic); this is defence in depth, not the fence. Verified live, both directions. With the fix, fleet_list / fleet_find / machine_map go straight to `executing` with zero prompts; fleet_spawn still lands in `waiting_confirmation` and waits for the owner. The R3 invariant is intact. Also lists the bare (non-`mcp__`) namespacing so a mounted fleet (#245) does not silently regress to prompting on every read. 10 tool-safety tests; 2452 daemon tests; typecheck and lint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Completes P5.1 (docs/conductor-frontends-design.md §3.A). Two co-equal top-level homes with one control in the status bar. §3 is the constraint the whole feature rests on: the conductor is a LENS over the same sessions, never a wall, and there must be no state a user can get stuck in. So this is a navigation preference, not a mode — switching home changes which session you are looking at and nothing else, and every session stays reachable from the list in both homes. **Sessions stays the default, even once a conductor exists.** Silently relocating someone's home the first time they spawn a conductor is exactly the "trapped in an orchestrated mode" feeling §3 exists to prevent, and a user who wants the conductor is one click away — then remembered. This settles the "default home" question left open in §13. The resolution rules live in `lib/home.ts` as pure functions, since which session a home lands on is the decision worth testing: - Conductor home focuses the conductor, or does NOTHING when there is none — a normal state, not an error. `null` means "leave focus alone", deliberately distinct from "focus nothing", so re-selecting a home you are already on does not reset scroll for nothing. - Sessions home acts only when you are actually sitting on the conductor. Otherwise you are already somewhere in Sessions and moving you would be the surprise this design avoids. - It returns you to the session you came from, falling back to the first ordinary session when that one was destroyed. Workers are never a landing target, on either path. They are disposable and die with their task, so landing on one is landing somewhere about to disappear — and a worker can legitimately be the last thing you looked at, having drilled into it from the fleet rail. A test caught that: the remembered-session path originally excluded only conductors. Acting on the choice is an effect rather than click handling, so the two stay consistent when the population changes underneath — the conductor being created while Conductor home is already selected, for instance. The preference persists via the existing layout store, validated on read rather than cast: a stored value from a future build must fall back, not select a home that does not exist. The remembered session is deliberately NOT persisted — it is a within-visit convenience, and an id from days ago is likelier to name a destroyed session than to be useful. 11 home tests; 210 web tests; typecheck, lint and build clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🔮 Oracle Review
🎯 Start Here
src/daemon/providers/tool-safety.ts (~15 min) — Logic changes in tool-safety.ts
📋 PR Summary
What this PR does: Implements a toggle to switch between Conductor and Sessions homes as co-equal top-level views, treating the switch as a navigation preference rather than a mode change.
Key changes:
- Added HomeToggle component in StatusBar for navigation between Conductor and Sessions views
- Implemented pure resolution rules in lib/home.ts for determining which session to focus when switching homes
- Extended tool-safety.ts to support fleet read tools using established prefix/suffix validation
- Added state persistence for home preference while keeping remembered session as a within-visit convenience
Areas affected: Navigation/UI (StatusBar, HomeToggle), Home resolution logic, Tool safety validation, Layout state management
Testing notes: 11 home tests covering resolution rules including edge cases for workers and destroyed sessions; 210 web tests total with clean typecheck/lint/build
🔍 Code Review
This is a well-architected feature that thoughtfully addresses the design constraints around the conductor being a 'lens' rather than a mode. The pure function approach in home.ts and careful handling of edge cases (workers, destroyed sessions, null semantics) demonstrates solid engineering judgment.
What's good:
- ✨ Pure function approach for resolution rules makes behavior predictable and easily testable
- ✨ Correct use of null semantics ('leave focus alone') to prevent unnecessary scroll resets
- ✨ Proper handling of edge cases like workers as non-landing targets to avoid routing to disposable sessions
- ✨ Clear separation of concerns: preference persists across visits while remembered session is per-visit convenience
Generated by Oracle - Highflame's AI Code Reviewer
Review follow-up on #329 (Oracle, both comments). The feedback asked for an explicit role taxonomy instead of `s.role === undefined`, suggesting `role !== "conductor" && role !== "worker"`. Taking the first half and declining the second. **Taken: name it.** The check was an inline lambda in `home.ts` and a duplicated inline test in `HomeToggle.tsx`. Both now call an exported `isOrdinarySession`, so the taxonomy has a name and one definition. **Declined: the negative form**, because it inverts the safety property. `SessionInfo.role` is documented as "Absent = normal session", and the protocol deliberately anticipates roles this client has not heard of — `session.create` types its role as an open string precisely "so a future role from a newer client still type-checks on the wire". The two forms therefore differ exactly when a new role appears: role === undefined → an unknown role is NOT ordinary (excluded) role !== "conductor" && ... → an unknown role IS ordinary (included) The negative form reads as more explicit and is the more dangerous of the two: it silently opts every future session kind into being a focus target. Workers are excluded here because they vanish with their task; inheriting that risk for kinds we know nothing about is the wrong default. An unknown role now stays excluded until somebody adds it here deliberately. Pinned by two tests — one on the predicate, one through `homeTarget` — that a session with an unrecognised role is never a landing target. 14 home tests; 213 web tests; typecheck, lint and build clean. 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/HomeToggle.tsx (~23 min) — Logic changes in HomeToggle.tsx
📋 PR Summary
What this PR does: Implements a Conductor/Sessions navigation toggle allowing users to switch between two co-equal top-level homes, with Sessions remaining the persistent default.
Key changes:
- Added HomeToggle component to status bar with proper state management
- Implemented pure navigation resolution logic in lib/home.ts
- Excluded workers from landing targets to prevent navigating to disposable sessions
- Separated preference persistence from session memory
Areas affected: Web UI navigation, Home session resolution logic, Status bar controls
Testing notes: 11 home tests added, 210 web tests total passing, typecheck/lint/build clean
🔍 Code Review
This is a thoughtfully designed implementation that respects the core constraint of navigation preference over mode. The pure logic separation and comprehensive edge-case handling demonstrate strong architectural maturity.
What's good:
- ✨ Pure resolution logic separated from UI concerns in lib/home.ts
- ✨ Thorough edge-case handling around session roles and state transitions
- ✨ Sessions preserved as default to avoid 'trapped in mode' UX
Generated by Oracle - Highflame's AI Code Reviewer
| @@ -0,0 +1,108 @@ | |||
| /** | |||
There was a problem hiding this comment.
Cross-file issue: Ensure centralized resolution logic is used by Fleet Rail
The PR introduces lib/home.ts as a robust, pure module to resolve navigation targets, specifically handling the edge case where a landing target (like a worker) disappears. The PR description notes that users can drill into workers via the 'fleet rail'. If the Fleet Rail component does not delegate to lib/home.ts for handling session destruction or 'return home' logic, there is a risk of inconsistent behavior (e.g., the Fleet Rail falling back to a different session type compared to the home.ts logic). This creates a 'dual source of truth' for navigation rules.
Affected files: web/src/lib/home.ts, web/src/components/HomeToggle.tsx
Recommendation: Audit the Fleet Rail component (and other entry points) to ensure they delegate navigation resolution to lib/home.ts. This guarantees that the 'workers are never a landing target' and 'fallback to ordinary session' rules are applied consistently across the entire application.
| @@ -0,0 +1,109 @@ | |||
| /** | |||
There was a problem hiding this comment.
Cross-file issue: Verify framework compliance (React vs SolidJS patterns)
The provided Organization Standards explicitly mandate 'Framework: React 18+ / Next.js 14+'. However, the File Review Summary for HomeToggle.tsx states that the component follows 'SolidJS patterns'. If the web application is a standard React codebase, this suggests potential architectural drift or the introduction of patterns that violate the established stack standards.
Affected files: web/src/components/HomeToggle.tsx
Recommendation: Confirm whether 'SolidJS patterns' refers to the SolidJS framework or 'solid' design principles. If SolidJS framework dependencies or patterns (e.g., signals, fine-grained reactivity) are being introduced into a React app, evaluate if this is a deliberate architectural pivot or a mistake.
Completes P5.1 (conductor-frontends-design.md §3.A). Two co-equal top-level homes with one control in the status bar.
The constraint this is built around
§3 is what the whole feature rests on: the conductor is a lens over the same sessions, never a wall, and there must be no state a user can get stuck in.
So this is a navigation preference, not a mode — switching home changes which session you're looking at and nothing else, and every session stays reachable from the list in both homes.
Settles the open question in §13
Sessions stays the default, even once a conductor exists. Silently relocating someone's home the first time they spawn a conductor is exactly the "trapped in an orchestrated mode" feeling §3 exists to prevent. A user who wants the conductor is one click away — and it's then remembered.
Resolution rules (pure, in
lib/home.ts)Which session a home lands on is the decision worth testing:
nullmeans "leave focus alone", deliberately distinct from "focus nothing", so re-selecting the home you're already on doesn't reset scroll for nothing.A bug my own test caught
Workers are never a landing target, on either path. They're disposable and die with their task, so landing on one is landing somewhere about to disappear — and a worker can legitimately be the last thing you looked at, having drilled into it from the fleet rail (#325).
The remembered-session path originally excluded only conductors, so it would happily return a worker. The test failed, which is what surfaced it.
Two smaller choices
Acting on the choice is an effect, not click handling — so the two stay consistent when the session population changes underneath, e.g. the conductor being created while Conductor home is already selected.
The preference persists; the remembered session doesn't. The former is validated on read rather than cast (a stored value from a future build must fall back, not select a home that doesn't exist). The latter is a within-visit convenience — an id from days ago is likelier to name a destroyed session than to be useful.
Verification
11 home tests, 210 web tests total, typecheck / lint / build clean.
Component rendering isn't locally testable (the Node <20.19 jsdom gap), but CI runs those suites.
P5 status
P5.1 and P5.2 are now complete pending review: legibility (#320), action cards (#323), board state (#324), docked surface (#325), this toggle, plus the read-surface gate fix (#328). Next is P5.3 — the cross-session "Needs you" inbox, which #325's
needsYouCountalready feeds.🤖 Generated with Claude Code