Skip to content

feat(coordinator): UI components, control banners, and coordinator entry points (PR 4/4)#125

Open
brooksc wants to merge 3 commits into
johannesjo:mainfrom
brooksc:coordinator-4-ui
Open

feat(coordinator): UI components, control banners, and coordinator entry points (PR 4/4)#125
brooksc wants to merge 3 commits into
johannesjo:mainfrom
brooksc:coordinator-4-ui

Conversation

@brooksc
Copy link
Copy Markdown
Contributor

@brooksc brooksc commented May 17, 2026

Overview

This is PR 4 of 4 in the coordinator series splitting #100 as requested in the round-4 review. It is stacked on PR 3 (#124) (coordinator-3-store-ipc) and must be merged after that one. The diff shown here includes PRs 1–3's content; the meaningful delta for this PR is the UI components and entry points described below.

PR sequence:

PR Branch Status Contents
1 (#118) coordinator-1-security Open Atomic writes, input validators, static analysis configs
2 (#120) coordinator-2-mcp-backend Open MCP coordinator engine + REST API hardening
3 (#124) coordinator-3-store-ipc Open Frontend store wiring + IPC handlers
4 (this PR) coordinator-4-ui Open UI components + coordinator entry points

This PR makes coordinator mode user-visible for the first time. After it merges, users can enable coordinator mode in Settings and create coordinator tasks from NewTaskDialog.


What's in this PR (delta over PR 3)

New component: SubTaskStrip

Nested status strip rendered inside a coordinator task's panel. Shows each sub-task's name, status dot, branch, and action buttons (send prompt, merge, close). Collapses to a compact summary when the coordinator panel is not focused.

Sidebar — coordinator grouping

Coordinator tasks render with their children nested beneath them in the sidebar. Children are hidden from the flat task list (already excluded by PR 3's isCoordinatedChild guard). A control banner appears below the coordinator's terminal when controlledBy === 'coordinator' to show the user they can take control.

TaskPanel — coordinator detail view

Full coordinator task panel with sub-task list, per-sub-task diff summary, merge/close actions, and MCP startup status display (pending / ready / error with retry button).

PromptInput — staged notification and autofire

  • Staged coordinator notification is displayed as a pending message above the input, with Send / Edit / Dismiss controls
  • autofire-tick.ts: fires the staged notification after the configured delay; 2-minute grace period for coordinator mode when no prompt marker is visible, so notifications fire even during long tool-call loops
  • prompt-control.ts: coordinator-aware send gate — blocks input when controlledBy === 'coordinator'

SettingsDialog — coordinator settings

New "Coordinator Mode" section with:

  • Enable/disable toggle (sets coordinatorModeEnabled)
  • Notification delay slider (5 s – 5 min, default 60 s)

NewTaskDialog — coordinator entry point

New "Coordinator mode" checkbox in the advanced section. When checked, exposes a "Propagate skip permissions" option. Creates the task with coordinatorMode: true.

Other component updates

  • TaskTitleBar: shows coordinator badge; sub-tasks show "controlled by coordinator" indicator
  • TaskAITerminal: renders SubTaskStrip for coordinator tasks
  • TerminalView: disables stdin when controlledBy === 'coordinator'; re-enables on hand-off
  • SidebarFooter: shows MCP server status indicator
  • StatusDot: renders 'review' state (orange dot) for tasks with needsReview
  • CloseTaskDialog: shows warning when closing a coordinator with active children
  • PlanViewerDialog: minor coordinator-aware rendering fix

Bug fixes (live e2e testing)

  • show_notification IPC: ipcMain.onipcMain.handle to satisfy ipcRenderer.invoke
  • merge_task git lock: retry once after 2 s when index.lock is held by a crashed process
  • getBranchCommits: guard against missing worktree (existsSync before git spawn)
  • getWorktreeStatus: wrap exec in try/catch to handle ENOENT race after worktree deletion
  • wait_for_signal_done timeout: resolve with { timedOut: true } instead of reject, so coordinator Claude receives a structured result rather than isError: true

CI / tooling

  • Pin GitHub Actions to exact SHAs (supply chain hardening on pre-existing workflows)
  • Husky pre-push: runs full test suite before push
  • Husky commit-msg: enforces conventional commit format
  • pre-commit: lockfile integrity check (fails if package.json changes without package-lock.json)

Tests

File New / updated cases
src/components/PromptInput.test.ts 284 new — autofire tick, staged notification rendering, coordinator control state, send gate, 2-min grace period
src/components/prompt-control.test.ts 28 new — send gate behaviour for coordinator vs human control
src/lib/terminalDisableStdin.test.ts 16 new — stdin disable/re-enable logic
electron/ipc/register-mcp.test.ts TOCTOU fix — all findFreePort() calls replaced with port: 0
electron/mcp/coordinator.test.ts 5 updated — timeout assertions changed from rejects.toThrow to timedOut === true

Assumptions and important notes

  1. PR 3 prerequisite — depends on the store functions, IPC listeners, and type definitions introduced in PR 3. Without PR 3, nothing here compiles.
  2. Feature now livecoordinatorModeEnabled can be set to true via the Settings toggle. First coordinator task can be created from NewTaskDialog.
  3. autofire grace period — 2-minute grace is intentional. Coordinators running long tool-call loops (no visible) need a time-based fallback to fire staged notifications; without it, the notification would never send until the agent returned to prompt.
  4. CI/hooks commits — The GitHub Actions SHA pinning modifies pre-existing workflow files only (no new workflows). Husky hooks and lockfile integrity check are also included here; they can be cherry-picked to an earlier PR if preferred.

🤖 Generated with Claude Code

brooksc and others added 3 commits May 16, 2026 23:09
…task model

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…restore

Wires the coordinator MCP engine (PR 2) into the frontend store and
Electron IPC layer. Feature is dark by default — coordinatorModeEnabled
defaults to false; nothing is user-visible until PR 4 adds the
Settings checkbox and NewTaskDialog entry point.

Key additions:
- Task model: coordinatorMode, coordinatedBy, controlledBy, stagedNotification,
  mcpStartupStatus, signalDone*, needsReview, and 3 global coordinator fields
- initMCPListeners(): subscribes to 7 MCP push events (TaskCreated, TaskClosed,
  CleanupFailed, NotificationStaged/Cleared/Orphaned, TaskStateSync, TaskHydrated)
- MCP startup state machine: markTaskMcpPending/Ready/Error, retryTaskMcpStartup
- setTaskControl(): coordinator ↔ human hand-off; collapseTask guard for children
- closeTask(): calls MCP_CoordinatedTaskClosed / MCP_CoordinatorDeregistered on close
- Session restore: App.tsx calls StartMCPServer for each persisted coordinator task
  on startup, rewriting .mcp.json with the new session port/token; children start
  pending until MCP_HydrateCoordinatedTask returns
- Persistence: all coordinator fields round-tripped through saveState/loadState;
  tasks with coordinatorMode/coordinatedBy load with mcpStartupStatus pending
- coordinator-preamble.ts: system preamble injected into coordinator agent prompts
- mcpStatus.ts: polls GetMCPStatus every 3 s; inert when no coordinator is active
- Deregister fix: suppresses spurious needsReview notification for children that
  never received their prompt (assignedPromptDelivered false)
- Tests: ~40 new cases across tasks, notifications, taskStatus, persistence

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…points, and e2e fixes

Adds all user-visible coordinator UI on top of the store/IPC layer (PR 3).
Feature becomes live: Settings checkbox sets coordinatorModeEnabled, and
NewTaskDialog exposes the coordinator mode option.

UI components:
- SubTaskStrip: nested sub-task status strip rendered inside coordinator task panel
- Sidebar: groups coordinator + children, hides children from flat list, control banner
- TaskPanel: coordinator detail view, sub-task list, merge/close actions
- PromptInput: staged notification display, send/edit/dismiss controls, autofire tick
- SettingsDialog: coordinator mode toggle + notification delay slider
- NewTaskDialog: coordinator mode checkbox + propagate-skip-permissions option
- TaskTitleBar, TaskAITerminal, TerminalView, SidebarFooter, StatusDot: coordinator-aware rendering
- CloseTaskDialog: warns when closing coordinator with active children
- terminalDisableStdin: utility for blocking input on coordinator-controlled terminals

Autofire notification:
- autofire-tick.ts: fires staged notification after configurable delay
- 2-minute grace period for coordinator mode without ❯ prompt marker, so
  notifications fire even during long tool-call loops (no prompt visible)
- prompt-control.ts: coordinator-aware send gate

Bug fixes (found during live e2e testing):
- show_notification IPC: changed ipcMain.on → ipcMain.handle to satisfy ipcRenderer.invoke
- merge_task git lock: retry once after 2 s when index.lock is held by a crashed process
- getBranchCommits: guard against missing worktree (existsSync before git spawn)
- getWorktreeStatus: wrap exec in try/catch to handle ENOENT race after worktree deletion
- wait_for_signal_done timeout: resolve with { timedOut: true } instead of reject,
  so coordinator Claude receives a structured result rather than isError: true

Test fixes:
- register-mcp.test.ts: eliminate TOCTOU port race by passing port: 0 directly
- coordinator.test.ts: update 5 timeout assertions from rejects.toThrow to timedOut===true
- docker.integration.test.ts: replace 'coord-1' fixture with valid UUID constant
- MCPStatus type mismatch, PTY preservation, multi-agent rendering regressions

CI / tooling:
- Pin GitHub Actions to exact SHAs (supply chain hardening)
- Husky pre-push: runs full test suite before push
- Husky commit-msg: enforces conventional commit format
- pre-commit: lockfile integrity check (fails if package.json changes without package-lock.json)

Tests: 284 new cases in PromptInput.test.ts covering autofire tick, staged notification
rendering, coordinator control state, send gate, and 2-min grace period behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@johannesjo
Copy link
Copy Markdown
Owner

Thanks for the coordinator UI work. I did another pass with multiple reviewers focused on autofire/control flow, backend lifecycle sync, and UI entry points. A few blockers stood out:

  1. Autofire can drop coordinator notifications before the configured delay/grace elapses. processAutoFireTick skips autoFireAt for controlledBy === coordinator, so promptless ticks become misses immediately and PromptInput drop-acks after 10 misses, well before the default 60s delay and 2-minute promptless grace. Refs: src/components/autofire-tick.ts:32, src/components/PromptInput.tsx:469, src/components/PromptInput.tsx:481.

  2. Manual/global send still bypasses coordinator control. The visible input handlers block coordinator-controlled input, but the global send action still calls handleSend(), and handleSend() does not guard props.controlledBy === coordinator. Cmd/Ctrl+Enter can therefore send hidden staged text immediately in auto mode. Refs: src/components/PromptInput.tsx:583, src/components/PromptInput.tsx:622.

  3. Release Control immediate autofire bypasses the question guard. On human -> coordinator, the immediate path only checks for ❯/› in the terminal tail, then sends. It does not check questionActive(), unlike the normal autofire helper, so active TUI dialogs can receive the staged notification. Ref: src/components/PromptInput.tsx:525.

  4. Backend prompt sends do not clear frontend review/done flags. Coordinator.sendPrompt() sets backend status back to running, but no MCP_TaskStateSync clears needsReview, signalDoneReceived, etc. A child can keep showing stale done/review UI after receiving new work. Refs: electron/mcp/coordinator.ts:769, src/store/tasks.ts:1193.

  5. Coordinator close/detach has conflicting child state updates. Deregistration always emits needsReview: true, while the renderer close path clears coordinator fields and review flags. Depending on IPC order, this can either lose the orphan/review marker or re-mark detached standalone children unexpectedly. It also leaves mcpStartupStatus/mcpStartupError, which can keep a detached child gated by MCP startup. Refs: electron/mcp/coordinator.ts:1420, src/store/tasks.ts:443, src/components/TerminalView.tsx:675.

  6. The "one coordinator per project" UI guard can be bypassed by changing projects. coordinatorMode() is not cleared when the selected project changes, and submit does not re-check hasActiveCoordinator(). A user can enable coordinator mode on one project, switch to a project that already has a coordinator, and submit another coordinator. Refs: src/components/NewTaskDialog.tsx:461, src/components/NewTaskDialog.tsx:544, src/components/NewTaskDialog.tsx:1086.

Smaller follow-ups: SubTaskStrip can select collapsed children without uncollapsing them, regular TaskRow appears to have lost the offscreen attention state/badge path, and visible nested subtasks are excluded from computeSidebarTaskOrder(), so keyboard navigation cannot reach them.

I would add regression coverage around the PromptInput call site, not only the pure processAutoFireTick helper, plus coordinator close/detach state synchronization.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants