@pellux/goodvibes-tui 1.16.1 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (257) hide show
  1. package/CHANGELOG.md +138 -9
  2. package/README.md +1 -1
  3. package/docs/foundation-artifacts/operator-contract.json +5490 -1806
  4. package/package.json +7 -7
  5. package/src/cli/completions/generate.ts +5 -4
  6. package/src/cli/config-overrides.ts +51 -11
  7. package/src/cli/doctor.ts +4 -5
  8. package/src/cli/entrypoint.ts +13 -8
  9. package/src/cli/help.ts +7 -6
  10. package/src/cli/launch-auto-update.ts +214 -0
  11. package/src/cli/management-commands.ts +82 -15
  12. package/src/cli/management.ts +0 -2
  13. package/src/cli/parser.ts +5 -0
  14. package/src/cli/status.ts +13 -0
  15. package/src/cli/surface-command.ts +5 -5
  16. package/src/cli/tui-startup.ts +60 -97
  17. package/src/cli/types.ts +2 -0
  18. package/src/config/index.ts +1 -1
  19. package/src/config/tui-extension-settings.ts +33 -0
  20. package/src/core/composer-state.ts +1 -1
  21. package/src/core/consolidation-receipt.ts +41 -0
  22. package/src/core/conversation-line-cache.ts +3 -3
  23. package/src/core/conversation-rendering.ts +18 -9
  24. package/src/core/conversation.ts +2 -2
  25. package/src/core/long-task-notifier.ts +7 -7
  26. package/src/core/memory-provenance.ts +67 -0
  27. package/src/core/pairing-devices.ts +49 -0
  28. package/src/core/pairing-handoff.ts +101 -0
  29. package/src/core/pairing-offers.ts +83 -0
  30. package/src/core/pairing-origin.ts +78 -0
  31. package/src/core/pairing-tailscale-gateway.ts +56 -0
  32. package/src/core/power-chip-source.ts +91 -0
  33. package/src/core/power-status.ts +95 -0
  34. package/src/core/stable-host.ts +171 -0
  35. package/src/core/stream-event-wiring.ts +29 -6
  36. package/src/core/system-message-noise.ts +1 -1
  37. package/src/core/system-message-router.ts +6 -6
  38. package/src/core/turn-budget-outcome.ts +60 -0
  39. package/src/core/turn-cancellation.ts +40 -1
  40. package/src/core/turn-event-wiring.ts +14 -8
  41. package/src/daemon/cli.ts +64 -58
  42. package/src/daemon/lifecycle.ts +41 -0
  43. package/src/daemon/service-commands.ts +5 -5
  44. package/src/export/cost-utils.ts +128 -6
  45. package/src/input/autocomplete.ts +2 -2
  46. package/src/input/command-registry.ts +55 -9
  47. package/src/input/commands/acp-runtime.ts +72 -0
  48. package/src/input/commands/ci-runtime.ts +5 -1
  49. package/src/input/commands/codebase-runtime.ts +6 -6
  50. package/src/input/commands/cost-runtime.ts +1 -1
  51. package/src/input/commands/devices-runtime.ts +109 -0
  52. package/src/input/commands/git-runtime.ts +28 -4
  53. package/src/input/commands/graph-runtime.ts +45 -0
  54. package/src/input/commands/health-runtime.ts +2 -2
  55. package/src/input/commands/intelligence-runtime.ts +1 -1
  56. package/src/input/commands/local-auth-runtime.ts +1 -1
  57. package/src/input/commands/local-provider-runtime.ts +49 -10
  58. package/src/input/commands/local-runtime.ts +53 -5
  59. package/src/input/commands/memory.ts +2 -2
  60. package/src/input/commands/operator-panel-runtime.ts +5 -5
  61. package/src/input/commands/operator-rpc.ts +32 -11
  62. package/src/input/commands/permissions-provenance.ts +1 -1
  63. package/src/input/commands/permissions-runtime.ts +99 -8
  64. package/src/input/commands/planning-runtime.ts +3 -3
  65. package/src/input/commands/platform-sandbox-runtime.ts +1 -1
  66. package/src/input/commands/power-runtime.ts +56 -0
  67. package/src/input/commands/provider-accounts-runtime.ts +71 -6
  68. package/src/input/commands/provider.ts +7 -5
  69. package/src/input/commands/qrcode-runtime.ts +14 -19
  70. package/src/input/commands/queue-runtime.ts +70 -0
  71. package/src/input/commands/recall-files-git.ts +13 -51
  72. package/src/input/commands/recall-files.ts +8 -11
  73. package/src/input/commands/recall-review.ts +2 -2
  74. package/src/input/commands/relay-runtime.ts +1 -1
  75. package/src/input/commands/remote-runtime.ts +1 -1
  76. package/src/input/commands/runtime-services.ts +4 -4
  77. package/src/input/commands/schedule-runtime.ts +14 -7
  78. package/src/input/commands/services-runtime.ts +1 -1
  79. package/src/input/commands/session-workflow.ts +2 -2
  80. package/src/input/commands/settings-sync-runtime.ts +2 -2
  81. package/src/input/commands/share-runtime.ts +1 -1
  82. package/src/input/commands/shell-core.ts +46 -68
  83. package/src/input/commands/subscription-runtime.ts +1 -1
  84. package/src/input/commands/update-runtime.ts +153 -96
  85. package/src/input/commands/work-plan-runtime.ts +1 -1
  86. package/src/input/commands/workstream-attempts.ts +6 -1
  87. package/src/input/commands/workstream-runtime.ts +46 -27
  88. package/src/input/commands/worktree-runtime.ts +35 -8
  89. package/src/input/commands.ts +10 -2
  90. package/src/input/config-modal-types.ts +14 -3
  91. package/src/input/config-modal.ts +75 -13
  92. package/src/input/feature-unit-layout.ts +93 -82
  93. package/src/input/feed-context-factory.ts +2 -2
  94. package/src/input/handler-command-route.ts +2 -2
  95. package/src/input/handler-content-actions.ts +1 -1
  96. package/src/input/handler-feed-routes.ts +7 -7
  97. package/src/input/handler-feed.ts +4 -4
  98. package/src/input/handler-interactions.ts +3 -2
  99. package/src/input/handler-modal-routes.ts +12 -4
  100. package/src/input/handler-modal-token-routes.ts +1 -1
  101. package/src/input/handler-onboarding-daemon-adopt.ts +2 -2
  102. package/src/input/handler-onboarding.ts +3 -2
  103. package/src/input/handler-picker-routes.ts +26 -13
  104. package/src/input/handler-shortcuts.ts +29 -6
  105. package/src/input/handler.ts +2 -2
  106. package/src/input/keybindings.ts +21 -3
  107. package/src/input/onboarding/onboarding-feature-units.ts +34 -27
  108. package/src/input/onboarding/onboarding-provider-key-fields.ts +112 -0
  109. package/src/input/onboarding/onboarding-wizard-apply.ts +22 -21
  110. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +17 -0
  111. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +2 -2
  112. package/src/input/onboarding/onboarding-wizard-steps.ts +30 -42
  113. package/src/input/panel-integration-actions.ts +2 -2
  114. package/src/input/panel-mouse-geometry.ts +1 -1
  115. package/src/input/panel-paste-flood-guard.ts +1 -1
  116. package/src/input/provider-key-intake.ts +167 -0
  117. package/src/input/sandbox-exec-config.ts +2 -2
  118. package/src/input/selection-modal.ts +1 -1
  119. package/src/input/settings-modal-activation.ts +5 -0
  120. package/src/input/settings-modal-adjustment.ts +4 -31
  121. package/src/input/settings-modal-data.ts +52 -57
  122. package/src/input/settings-modal-mutations.ts +12 -68
  123. package/src/input/settings-modal-types.ts +27 -17
  124. package/src/input/settings-modal.ts +74 -55
  125. package/src/main.ts +90 -90
  126. package/src/panels/agent-inspector-shared.ts +4 -4
  127. package/src/panels/base-panel.ts +2 -2
  128. package/src/panels/builtin/agent.ts +4 -4
  129. package/src/panels/builtin/development.ts +4 -4
  130. package/src/panels/builtin/knowledge.ts +1 -1
  131. package/src/panels/builtin/operations.ts +83 -11
  132. package/src/panels/builtin/session.ts +5 -5
  133. package/src/panels/builtin/shared.ts +16 -4
  134. package/src/panels/builtin-modals.ts +59 -42
  135. package/src/panels/builtin-panels.ts +1 -1
  136. package/src/panels/cost-tracker-panel.ts +124 -7
  137. package/src/panels/diff-panel.ts +39 -6
  138. package/src/panels/eval-registry.ts +2 -2
  139. package/src/panels/fleet-acts.ts +446 -0
  140. package/src/panels/fleet-deep-link.ts +1 -1
  141. package/src/panels/fleet-gateway.ts +117 -0
  142. package/src/panels/fleet-observed-render.ts +128 -0
  143. package/src/panels/fleet-panel-format.ts +163 -11
  144. package/src/panels/fleet-panel-worktree-detail.ts +14 -0
  145. package/src/panels/fleet-panel.ts +73 -57
  146. package/src/panels/fleet-read-model.ts +84 -14
  147. package/src/panels/fleet-spawn.ts +266 -0
  148. package/src/panels/fleet-steer.ts +2 -2
  149. package/src/panels/fleet-stop.ts +41 -5
  150. package/src/panels/fleet-tabs.ts +15 -16
  151. package/src/panels/fleet-transcript.ts +5 -5
  152. package/src/panels/git-panel.ts +2 -2
  153. package/src/panels/index.ts +4 -1
  154. package/src/panels/local-auth-panel.ts +1 -1
  155. package/src/panels/memory-consolidation-gateway.ts +94 -0
  156. package/src/panels/modals/devices-modal.ts +161 -0
  157. package/src/panels/modals/hooks-modal.ts +1 -1
  158. package/src/panels/modals/keybindings-modal.ts +2 -2
  159. package/src/panels/modals/knowledge-modal.ts +1 -1
  160. package/src/panels/modals/marketplace-modal.ts +4 -4
  161. package/src/panels/modals/memory-modal.ts +153 -9
  162. package/src/panels/modals/modal-surface-helpers.ts +1 -1
  163. package/src/panels/modals/modal-theme.ts +1 -1
  164. package/src/panels/modals/pairing-modal.ts +198 -68
  165. package/src/panels/modals/planning-modal.ts +2 -2
  166. package/src/panels/modals/plugins-modal.ts +1 -1
  167. package/src/panels/modals/policy-modal.ts +1 -1
  168. package/src/panels/modals/security-modal.ts +7 -1
  169. package/src/panels/modals/skills-modal.ts +1 -1
  170. package/src/panels/modals/work-plan-modal.ts +3 -3
  171. package/src/panels/notifications-feed.ts +127 -0
  172. package/src/panels/notifications-panel.ts +160 -0
  173. package/src/panels/panel-manager.ts +9 -9
  174. package/src/panels/plugins-panel.ts +4 -4
  175. package/src/panels/project-planning-answer-actions.ts +1 -1
  176. package/src/panels/provider-health-routes.ts +2 -2
  177. package/src/panels/skills-panel.ts +4 -5
  178. package/src/panels/token-budget-panel.ts +11 -3
  179. package/src/panels/types.ts +5 -5
  180. package/src/panels/workstream-graph-render.ts +123 -0
  181. package/src/permissions/broker-approval-card.ts +201 -0
  182. package/src/permissions/hunk-selection.ts +28 -4
  183. package/src/permissions/prompt.ts +244 -29
  184. package/src/renderer/autocomplete-overlay.ts +107 -54
  185. package/src/renderer/code-block.ts +2 -2
  186. package/src/renderer/compaction-history-modal.ts +1 -1
  187. package/src/renderer/compaction-preview.ts +1 -1
  188. package/src/renderer/compaction-quality.ts +1 -1
  189. package/src/renderer/composer-fragments.ts +69 -0
  190. package/src/renderer/compositor.ts +2 -2
  191. package/src/renderer/config-modal.ts +1 -1
  192. package/src/renderer/conversation-overlays.ts +1 -1
  193. package/src/renderer/footer-tips.ts +3 -3
  194. package/src/renderer/hint-grammar.ts +1 -1
  195. package/src/renderer/modal-factory.ts +1 -1
  196. package/src/renderer/model-picker-overlay.ts +1 -1
  197. package/src/renderer/model-workspace.ts +7 -2
  198. package/src/renderer/panel-composite.ts +2 -2
  199. package/src/renderer/panel-workspace-bar.ts +1 -1
  200. package/src/renderer/process-indicator.ts +1 -1
  201. package/src/renderer/selection-modal-overlay.ts +1 -1
  202. package/src/renderer/session-picker-modal.ts +2 -2
  203. package/src/renderer/settings-modal-helpers.ts +23 -5
  204. package/src/renderer/settings-modal.ts +129 -39
  205. package/src/renderer/shell-surface.ts +6 -0
  206. package/src/renderer/terminal-bg-probe.ts +1 -1
  207. package/src/renderer/theme.ts +3 -3
  208. package/src/renderer/tool-call.ts +13 -7
  209. package/src/renderer/tool-result-summary.ts +1 -1
  210. package/src/renderer/turn-injection.ts +7 -6
  211. package/src/renderer/ui-factory.ts +38 -1
  212. package/src/runtime/bootstrap-command-context.ts +7 -2
  213. package/src/runtime/bootstrap-command-parts.ts +9 -5
  214. package/src/runtime/bootstrap-core.ts +41 -41
  215. package/src/runtime/bootstrap-hook-bridge.ts +1 -1
  216. package/src/runtime/bootstrap-shell.ts +13 -5
  217. package/src/runtime/bootstrap.ts +34 -32
  218. package/src/runtime/channel-composition.ts +46 -0
  219. package/src/runtime/code-index-services.ts +3 -4
  220. package/src/runtime/daemon-attach-notices.ts +120 -0
  221. package/src/runtime/durability-services.ts +90 -0
  222. package/src/runtime/embedded-service-factories.ts +40 -0
  223. package/src/runtime/feature-settings.ts +147 -0
  224. package/src/runtime/fleet-services.ts +41 -0
  225. package/src/runtime/idle-power-services.ts +85 -0
  226. package/src/runtime/index.ts +6 -21
  227. package/src/runtime/interaction-seams.ts +109 -0
  228. package/src/runtime/legacy-daemon-migration.ts +29 -19
  229. package/src/runtime/memory-fold.ts +1 -1
  230. package/src/runtime/notification-dispatch.ts +110 -0
  231. package/src/runtime/onboarding/apply.ts +3 -29
  232. package/src/runtime/onboarding/fast-path.ts +39 -6
  233. package/src/runtime/onboarding/provider-key-capture.ts +126 -0
  234. package/src/runtime/onboarding/snapshot.ts +0 -1
  235. package/src/runtime/onboarding/types.ts +9 -3
  236. package/src/runtime/power-keepawake-remote.ts +75 -0
  237. package/src/runtime/process-lifecycle.ts +3 -1
  238. package/src/runtime/provider-fallback.ts +55 -0
  239. package/src/runtime/provider-optimizer-wiring.ts +55 -0
  240. package/src/runtime/release-artifacts.ts +19 -103
  241. package/src/runtime/resume-notice.ts +1 -1
  242. package/src/runtime/services.ts +113 -113
  243. package/src/runtime/session-continuity-hints.ts +38 -0
  244. package/src/runtime/surface-feature-flags.ts +40 -9
  245. package/src/runtime/terminal-output-guard.ts +2 -2
  246. package/src/runtime/trust/workspace-registration.ts +10 -2
  247. package/src/runtime/trust/workspace-trust.ts +58 -31
  248. package/src/runtime/ui-services.ts +6 -1
  249. package/src/runtime/update-check.ts +15 -69
  250. package/src/runtime/workstream-services.ts +2 -2
  251. package/src/shell/blocking-input.ts +108 -132
  252. package/src/shell/recovery-input-helpers.ts +53 -166
  253. package/src/shell/ui-openers.ts +38 -6
  254. package/src/utils/format-duration.ts +1 -1
  255. package/src/verification/verification-ledger.ts +21 -4
  256. package/src/version.ts +1 -1
  257. package/src/input/commands/flags-runtime.ts +0 -328
@@ -1,14 +1,14 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // fleet-panel.ts
3
3
  //
4
- // W2.2 — FleetPanel: the live unified observability tree. Renders the
4
+ // — FleetPanel: the live unified observability tree. Renders the
5
5
  // registered process fleet (agents incl. WRFC roles, WRFC chains/subtasks,
6
6
  // workflow-tool FSMs, watchers, background processes) as a depth-first tree
7
- // with a selected-row detail region. Session tabs (W3.1 Part C): Enter on an
7
+ // with a selected-row detail region. Session tabs (Part C): Enter on an
8
8
  // attachable node (agent/wrfc-chain) opens a tab with that process's
9
9
  // transcript/member summary — see fleet-tabs.ts + fleet-transcript.ts. This
10
10
  // file owns input dispatch and layout; fleet-deep-link.ts owns the
11
- // cross-panel jump target (DEBT-5 item 4).
11
+ // cross-panel jump target (item 4).
12
12
  // ---------------------------------------------------------------------------
13
13
 
14
14
  import { readFile } from 'node:fs/promises';
@@ -21,12 +21,14 @@ import { PanelConfirmOverlay } from './panel-confirm-overlay.ts';
21
21
  import { isPanelSearchBackspace, isPanelSearchCancel, isPanelSearchCommit, isPanelSearchPrintable } from './search-focus.ts';
22
22
  import { buildKeyboardHints, buildPanelWorkspace, buildSearchInputLine, DEFAULT_PANEL_PALETTE, getPanelWorkspaceContentBudget, type PanelPalette } from './polish.ts';
23
23
  import {
24
- isBlockedOnUserState,
24
+ isBlockedOnUserNode,
25
25
  isRunningProcessState,
26
26
  isTerminalProcessState,
27
27
  type FleetReadModel,
28
28
  type FleetTreeRow,
29
29
  } from './fleet-read-model.ts';
30
+ import type { FleetActs } from './fleet-acts.ts';
31
+ import type { FleetSpawn } from './fleet-spawn.ts';
30
32
  import {
31
33
  activeFleetTab,
32
34
  appendSteerText,
@@ -40,7 +42,7 @@ import {
40
42
  type SteerBadge,
41
43
  } from './fleet-tabs.ts';
42
44
  import { liveSteerableLabels, reconcileSteerBadges as reconcileSteerBadgesPure, renderSteerBadgeLine, steerRefusalMessage } from './fleet-steer.ts';
43
- import { FleetStopTracker, toggleFleetPause, buildFleetTreeHints, countDescendantStats } from './fleet-stop.ts';
45
+ import { FleetStopTracker, toggleFleetPause, buildFleetTreeHints, fleetKillConfirmArgs } from './fleet-stop.ts';
44
46
  import { resolveFleetDeepLinkIndex } from './fleet-deep-link.ts';
45
47
  import { parseAgentLedger, renderFleetAgentTranscript, renderFleetChainSummary, renderFleetLedgerFallback, renderFleetTranscriptLoading } from './fleet-transcript.ts';
46
48
  import { renderFleetTabStrip } from '../renderer/fleet-tab-strip.ts';
@@ -95,26 +97,28 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
95
97
  private tickTimer: ReturnType<typeof setInterval> | null = null;
96
98
  /**
97
99
  * The node id the cursor is anchored to, independent of its row index.
98
- * `selectedIndex` is a plain offset into `getItems()`'s current order —
99
- * every snapshot rebuilds that array from scratch (new nodes, completed
100
- * nodes pruned, tree re-sorted), so a stale index silently points at
101
- * whatever row now happens to occupy that slot. `reanchorSelection()`
102
- * re-locates this id in every fresh snapshot and repoints selectedIndex
103
- * there; null means "no anchor yet" (follows the base class's index-0
104
- * default until the first navigation/tick establishes one).
100
+ * `selectedIndex` is a plain offset into `getItems()`'s current (rebuilt-every-
101
+ * snapshot) order, so a stale index silently drifts; `reanchorSelection()`
102
+ * re-locates this id each snapshot and repoints selectedIndex. null means "no
103
+ * anchor yet" (base class index-0 default until first navigation/tick).
105
104
  */
106
105
  private selectedNodeId: string | null = null;
107
106
 
108
- /** d1 (W6.2): the "stopping…" write-window overlay tracker (see fleet-stop.ts). */
107
+ /** d1: the "stopping…" write-window overlay tracker (see fleet-stop.ts). */
109
108
  private readonly stopTracker = new FleetStopTracker();
110
109
 
111
110
  public constructor(
112
111
  private readonly readModel: FleetReadModel,
113
112
  actions: Partial<FleetActionCallbacks> = {},
114
113
  private readonly configManager: ConfigManager | null = null,
114
+ // The waiting-on-human acts (pick / conflict / discard). Null in a runtime
115
+ // with no daemon gateway wired — those keys then fall through honestly.
116
+ private readonly acts: FleetActs | null = null,
117
+ // The ACP spawn affordance ('n' hosts a discovered third-party agent); null with no gateway wired.
118
+ private readonly spawn: FleetSpawn | null = null,
115
119
  ) {
116
120
  super('fleet', 'Fleet', '⊟', 'runtime-ops');
117
- this.showSelectionGutter = true; // W0.8: visible ▸ focus indicator
121
+ this.showSelectionGutter = true; // visible ▸ focus indicator
118
122
  this.actions = { ...NOOP_ACTIONS, ...actions };
119
123
  this.confirmOverlay = new PanelConfirmOverlay(() => this.markDirty());
120
124
  this.unsub = readModel.subscribe(() => {
@@ -232,11 +236,10 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
232
236
  }
233
237
 
234
238
  /**
235
- * W3.3 (cross-restart honesty): a restart always starts from an empty
236
- * `AgentManager` Map (no bridge from a prior TUI/daemon registry), so a
237
- * completed process from a previous session can never reappear here — a
238
- * real, permanent limitation (W3.3 brief point 5), documented rather than
239
- * silently implying "nothing has ever run" or that a restart brings it back.
239
+ * (cross-restart honesty): a restart starts from an empty `AgentManager` Map
240
+ * (no bridge from a prior registry), so a completed process from a previous
241
+ * session can never reappear here — a real, permanent limitation, documented
242
+ * rather than implying "nothing has ever run" or that a restart brings it back.
240
243
  */
241
244
  protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
242
245
  return [
@@ -275,15 +278,11 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
275
278
  }
276
279
 
277
280
  /**
278
- * Re-locate `selectedNodeId` in the current (possibly just-changed)
279
- * snapshot and repoint `selectedIndex` at its new position — this is what
280
- * makes the selection follow a specific process across adds/removes/
281
- * reorders instead of drifting onto whatever row now occupies the old
282
- * index. When the anchored node is no longer present (completed and
283
- * pruned, killed and pruned, etc.) — or no anchor has been established
284
- * yet — falls back to the base class's clampSelection() (nearest valid
285
- * index, never an out-of-bounds/vanished selection) and re-anchors to
286
- * whatever that lands on.
281
+ * Re-locate `selectedNodeId` in the current snapshot and repoint
282
+ * `selectedIndex` at it, so the selection follows a specific process across
283
+ * adds/removes/reorders instead of drifting onto whatever row occupies the old
284
+ * index. When the anchored node is gone (pruned) — or no anchor exists yet —
285
+ * falls back to clampSelection() (nearest valid index) and re-anchors there.
287
286
  */
288
287
  private reanchorSelection(): void {
289
288
  const rows = this.getItems();
@@ -303,7 +302,7 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
303
302
  return this.getItems().find((row) => row.node.id === nodeId)?.node ?? null;
304
303
  }
305
304
 
306
- /** DEBT-5 item 4 (see fleet-deep-link.ts). */
305
+ /** item 4 (see fleet-deep-link.ts). */
307
306
  public receiveDeepLink(target: { readonly id: string; readonly kind?: string }): void {
308
307
  const idx = resolveFleetDeepLinkIndex(this.getItems(), target);
309
308
  if (idx < 0) { this.setError('node no longer present.'); return; }
@@ -317,12 +316,10 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
317
316
 
318
317
  /**
319
318
  * Focus the next node blocked on the operator (awaiting approval/input),
320
- * cycling top-to-bottom in display order and wrapping. Always resolves
321
- * against the LIVE fleet (blocked nodes never live in the archive), and
322
- * returns to the tree view from any attached tab so the reveal is visible —
323
- * mirrors receiveDeepLink's "make it visible" contract. Honest no-op with a
324
- * message when nothing is currently waiting on the operator. Returns true
325
- * (the 'b' key is always consumed by the panel).
319
+ * cycling in display order and wrapping. Resolves against the LIVE fleet
320
+ * (blocked nodes never live in the archive) and returns to the tree view from
321
+ * any attached tab so the reveal is visible (receiveDeepLink's contract).
322
+ * Honest no-op message when nothing waits; always consumes 'b' (returns true).
326
323
  */
327
324
  private jumpToNextBlocked(): boolean {
328
325
  const snapshot = this.readModel.getSnapshot();
@@ -384,6 +381,13 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
384
381
  public override handleInput(key: string): boolean {
385
382
  if (this.lastError !== null) this.clearError();
386
383
 
384
+ // The candidate picker (a flagged pick decision) owns the view + input while active.
385
+ if (this.acts?.pickModeActive()) return this.acts.handlePickInput(key);
386
+ // The observed-agent steer composer (a drill-in) likewise owns EVERY key until Enter/Esc.
387
+ if (this.acts?.observedSteerActive()) return this.acts.handleObservedSteerInput(key);
388
+ // The ACP spawn picker (host a third-party agent) likewise owns view + input.
389
+ if (this.spawn?.spawnModeActive()) return this.spawn.handleSpawnInput(key);
390
+
387
391
  // Confirm overlay owns input first (K-armed kill confirm).
388
392
  if (this.confirmOverlay.handleInput(key)) return true;
389
393
 
@@ -391,7 +395,7 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
391
395
  // owns EVERY key (same full-priority-while-composing contract as
392
396
  // git-panel.ts's commitMessage entry) until Enter (submit) or Esc
393
397
  // (cancel) — a burst/paste and single hotkeys like 'j'/'s'/'[' all land
394
- // in the draft, never as tree/tab navigation (W0.8 focus rule).
398
+ // in the draft, never as tree/tab navigation (focus rule).
395
399
  const composingTab = activeFleetTab(this.tabsState);
396
400
  if (composingTab && composingTab.steerDraft !== null) {
397
401
  return this.handleSteerInput(composingTab, key);
@@ -425,6 +429,18 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
425
429
 
426
430
  const selected = this.getSelectedItem();
427
431
 
432
+ // Waiting-on-human acts on the selected row: Enter on a flagged pick/conflict
433
+ // row, D on a worktree-owning row. Returns false (falls through to attach /
434
+ // ordinary handling) for any other row, so Enter still opens an agent tab.
435
+ if (this.acts && selected && this.acts.handleTreeKey(key, selected.node)) {
436
+ this.captureSelectionAnchor();
437
+ return true;
438
+ }
439
+
440
+ // Host a third-party ACP agent: lists discovered agents, then a known dir,
441
+ // then spawns it as an acp-agent row. Quiet when none are discovered.
442
+ if (key === 'n' && this.spawn) { void this.spawn.begin(); return true; }
443
+
428
444
  // Archive controls: v toggles the live fleet <-> session archive view;
429
445
  // a archives the selected finished subtree (or restores it from the
430
446
  // archive view); A archives every fully-finished root subtree.
@@ -479,29 +495,20 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
479
495
 
480
496
  if (key === 'K') {
481
497
  if (!selected || isTerminalProcessState(selected.node.state)) return false;
498
+ // Observed foreign agents have killable:false — the refusal below reads
499
+ // honestly; goodvibes only observes them, so stop is never offered.
482
500
  if (!selected.node.capabilities.killable) {
483
501
  this.setError(`${selected.node.kind} does not support kill.`);
484
502
  return true;
485
503
  }
486
- const node = selected.node;
487
- const shortId = node.id.length > 8 ? node.id.slice(-8) : node.id;
488
- const stats = countDescendantStats(this.getItems(), node.id);
489
- const suffix = stats.total > 0 ? ` (+${stats.total} descendant${stats.total === 1 ? '' : 's'}, ${stats.active} active)` : '';
490
- this.confirmOverlay.arm({
491
- id: node.id,
492
- label: `${node.kind} ${shortId}${suffix}`,
493
- verb: 'Kill',
494
- // d1 (W6.2): mark 'stopping…' only once the kill is confirmed, not while
495
- // the confirm overlay is merely armed.
496
- onConfirm: () => {
497
- this.actions.kill(node.id, { cascade: true });
498
- this.stopTracker.mark(node.id);
499
- },
500
- });
504
+ this.confirmOverlay.arm(fleetKillConfirmArgs(selected.node, this.getItems(), {
505
+ kill: (id) => this.actions.kill(id, { cascade: true }),
506
+ markStopping: (id) => this.stopTracker.mark(id),
507
+ }));
501
508
  return true;
502
509
  }
503
510
 
504
- // d2 (W6.2): 'p' toggles pause<->resume by the selected node's state (see
511
+ // d2: 'p' toggles pause<->resume by the selected node's state (see
505
512
  // toggleFleetPause in fleet-stop.ts).
506
513
  if (key === 'p') {
507
514
  if (!selected) return false;
@@ -539,6 +546,9 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
539
546
  // one press for a steerable node; honest refusal otherwise.
540
547
  if (!selected) return false;
541
548
  const node = selected.node;
549
+ // Observed foreign agents steer as a DRILL-IN only; that 's' is handled by
550
+ // acts.handleTreeKey above (opens the composer in the row's detail), so it
551
+ // never reaches this attach path.
542
552
  // Closure replay note: a node that FINISHED in the hint→keypress race
543
553
  // window must say so — "does not support steer" is misleading for an
544
554
  // agent whose only problem is being done.
@@ -621,7 +631,7 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
621
631
 
622
632
  /**
623
633
  * Submit a composed steer message. Returns whether it was queued so the caller
624
- * clears the draft only on a confirmed send (WO UX-A item 4): a refusal (target
634
+ * clears the draft only on a confirmed send (WO item 4): a refusal (target
625
635
  * went idle while composing) PRESERVES the typed text and suggests which agents
626
636
  * ARE steerable, instead of silently discarding the draft. On a successful queue
627
637
  * the ⧗ badge line is the immediate honest acknowledgment.
@@ -644,7 +654,7 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
644
654
  // Display-only overrides (stopping wins, then blocked-on-user) are derived
645
655
  // here from panel state and passed into the pure row renderer.
646
656
  const stopping = this.stopTracker.isStopping(row.node.id);
647
- const blocked = !stopping && isBlockedOnUserState(row.node.state);
657
+ const blocked = !stopping && isBlockedOnUserNode(row.node);
648
658
  return renderFleetRowLine(row, width, stopping, blocked, this.steerBadgeForNode(row.node.id), C);
649
659
  }
650
660
 
@@ -658,8 +668,12 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
658
668
  // Mirror the tree row's display-only overrides (stopping wins, then blocked)
659
669
  // so the detail's literal state text never contradicts the row glyph.
660
670
  const stopping = this.stopTracker.isStopping(node.id);
661
- const blocked = !stopping && isBlockedOnUserState(node.state);
662
- return renderFleetDetailLines(node, width, stopping, blocked, C);
671
+ const blocked = !stopping && isBlockedOnUserNode(node);
672
+ // The drill-in observed-steer draft (when the composer is open on this row)
673
+ // renders an input line in the observed detail. For a workstream row, lazily
674
+ // fetch its task graph so the edges/pool posture renders in-panel.
675
+ this.acts?.ensureGraphFor(node);
676
+ return renderFleetDetailLines(node, width, stopping, blocked, C, this.acts?.observedSteerDraftFor(node.id) ?? null, this.acts?.graphFor(node.id) ?? null);
663
677
  }
664
678
 
665
679
  /** Renders the active session tab's content (transcript / chain summary / ledger fallback) in place of the tree. */
@@ -718,7 +732,7 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
718
732
 
719
733
  if (tab.kind === 'wrfc-chain') {
720
734
  const memberRows = snapshotRows.filter((row) => row.node.parentId === tab.nodeId);
721
- // d3 (W6.2): a completed chain prunes its wrapper node, so an empty member
735
+ // d3: a completed chain prunes its wrapper node, so an empty member
722
736
  // list can mean "finished", not "not started yet". Absent (pruned) OR
723
737
  // terminal => the honest "completed" wording; present + non-terminal with
724
738
  // no members yet => the "not started" wording.
@@ -777,6 +791,8 @@ export class FleetPanel extends ScrollableListPanel<FleetTreeRow> {
777
791
  }
778
792
 
779
793
  public override render(width: number, height: number): Line[] {
794
+ if (this.acts?.pickModeActive()) return this.acts.renderPickMode(width, height, C);
795
+ if (this.spawn?.spawnModeActive()) return this.spawn.renderSpawnMode(width, height, C);
780
796
  if (this.tabsState.activeTabIndex > 0) return this.renderTabView(width, height);
781
797
  return this.renderTreeView(width, height);
782
798
  }
@@ -1,14 +1,14 @@
1
1
  // ---------------------------------------------------------------------------
2
2
  // fleet-read-model.ts
3
3
  //
4
- // W2.2 (Fleet tree panel) — pure, testable read-model over the SDK's live
4
+ // (Fleet tree panel) — pure, testable read-model over the SDK's live
5
5
  // process registry (@pellux/goodvibes-sdk/platform/runtime/fleet, landed on
6
- // SDK main as W2.1). No BasePanel/rendering dependency: this module owns the
6
+ // SDK main as). No BasePanel/rendering dependency: this module owns the
7
7
  // flat ProcessNode[] -> tree transformation, the honest cost/token
8
8
  // aggregates, and the state->glyph/tone mapping that FleetPanel renders.
9
9
  //
10
10
  // Design notes:
11
- // - The W2.2 brief predates the real SDK API and sketched a bespoke
11
+ // - The brief predates the real SDK API and sketched a bespoke
12
12
  // FleetRegistryNode/FleetRegistry adapter pair. The real
13
13
  // createProcessRegistry() ProcessNode already carries elapsedMs,
14
14
  // usage, costUsd, costState, and currentActivity directly, so no
@@ -30,6 +30,7 @@
30
30
 
31
31
  import type {
32
32
  ArchivableProcessRegistry,
33
+ ProcessAttention,
33
34
  ProcessCostState,
34
35
  ProcessKind,
35
36
  ProcessNode,
@@ -136,13 +137,11 @@ const STATE_TONES: Record<ProcessState, FleetStateTone> = {
136
137
  const TERMINAL_STATES = new Set<ProcessState>(['done', 'failed', 'killed', 'interrupted']);
137
138
 
138
139
  /**
139
- * States that mean "this node is waiting on ME right now" — a user approval or
140
- * user input. Today the registry derives exactly one such state,
141
- * `awaiting-approval` (an agent parked at a permission prompt). This is the
142
- * single place that mapping lives, so if the SDK adds a distinct
143
- * awaiting-user-input state later it joins here and every "blocked on me"
144
- * surface (badge, sort, jump) picks it up for free. Kept as a set for that
145
- * forward-compatibility, not because more than one state qualifies today.
140
+ * States that mean "this node is waiting on ME right now" via the state field
141
+ * alone. The registry now also publishes the canonical `needsAttention`
142
+ * projection (reason 'approval' | 'input' — see fleetNodeAttention below),
143
+ * which every surface prefers; this set remains only as the fallback mapping
144
+ * for nodes without the projection, so nothing regresses on older registries.
146
145
  */
147
146
  const BLOCKED_ON_USER_STATES = new Set<ProcessState>(['awaiting-approval']);
148
147
 
@@ -168,6 +167,17 @@ const KIND_TAGS: Record<ProcessKind, string> = {
168
167
  // A single leaf node — never a rollup of other flat-list nodes (see
169
168
  // ROLLUP_KINDS below) — so no other list in this module needs updating.
170
169
  'code-index': 'index',
170
+ // A hosted third-party coding agent (Claude Code / Codex / opencode) spawned
171
+ // over ACP (adapters/acp-host.ts). A plain leaf row — steer/stop/attention
172
+ // ride on its node capabilities and the SDK's needsAttention projection, so
173
+ // this glyph/label is the only read-model special-casing it needs.
174
+ 'acp-agent': 'acp',
175
+ // An externally-launched foreign coding agent (Claude Code / Codex / opencode)
176
+ // that goodvibes did NOT spawn — observed read-only from OS signals. The row's
177
+ // first job is visibility ("it's in claude or codex"); it is never counted in
178
+ // any our-fleet count, never carries a stop affordance, and steer is drill-in
179
+ // only. See ObservedProcess handling in fleet-panel-format.ts.
180
+ 'observed-external': 'observed',
171
181
  };
172
182
 
173
183
  export function fleetStateGlyph(state: ProcessState): string {
@@ -195,8 +205,66 @@ export function isBlockedOnUserState(state: ProcessState): boolean {
195
205
  return BLOCKED_ON_USER_STATES.has(state);
196
206
  }
197
207
 
208
+ /**
209
+ * The node's waiting-on-human classification, preferring the SDK's canonical
210
+ * `needsAttention` projection (reason 'approval' | 'input' — broader than the
211
+ * awaiting-approval state alone: it also covers a session waiting on user
212
+ * input). Falls back to the state-derived mapping for registries that predate
213
+ * the projection, so nothing regresses. This is THE membership every
214
+ * "blocked on me" surface (badge, sort, jump) derives from.
215
+ */
216
+ export function fleetNodeAttention(node: ProcessNode): ProcessAttention | null {
217
+ if (node.needsAttention) return node.needsAttention;
218
+ if (isBlockedOnUserState(node.state)) return { reason: 'approval' };
219
+ return null;
220
+ }
221
+
222
+ /** True when the node is waiting on the operator right now (approval or input). */
223
+ export function isBlockedOnUserNode(node: ProcessNode): boolean {
224
+ return fleetNodeAttention(node) !== null;
225
+ }
226
+
227
+ /**
228
+ * The literal badge text for a waiting-on-human node, by reason. Every reason
229
+ * is a first-class member of the ONE waiting-on-human state class (same ⚑
230
+ * glyph, same jump key, same count) but names WHY in its own words:
231
+ * - 'approval' — a tool call is held for an approve/deny decision.
232
+ * - 'input' — the node is otherwise blocked on operator input.
233
+ * - 'pick' — a best-of-N attempt group is ready and only a human's
234
+ * winner pick advances it.
235
+ * - 'conflict' — a merge conflict needs a human resolution before the work
236
+ * can land.
237
+ * The pick/conflict wording is deliberately DISTINCT from the bare
238
+ * 'blocked on you' so an operator reads the required act, not just the state.
239
+ */
240
+ export function fleetAttentionText(attention: ProcessAttention): string {
241
+ switch (attention.reason) {
242
+ case 'input': return 'needs your input';
243
+ case 'pick': return 'needs your pick';
244
+ case 'conflict': return 'merge conflict waiting on you';
245
+ case 'approval': return 'blocked on you';
246
+ default: return 'blocked on you';
247
+ }
248
+ }
249
+
250
+ /**
251
+ * The stall marker for a row — 'quiet Nm' (or 'quiet NhMm' past an hour),
252
+ * derived from the read-model's stall tell (pure timestamp comparison on the
253
+ * registry side; a live node whose last observable output is older than the
254
+ * threshold). Null when the node is not stalled-quiet.
255
+ */
256
+ export function fleetStallMarker(node: ProcessNode): string | null {
257
+ const stall = node.stall;
258
+ if (!stall) return null;
259
+ const minutes = Math.max(1, Math.floor(stall.quietForMs / 60_000));
260
+ if (minutes < 60) return `quiet ${minutes}m`;
261
+ const hours = Math.floor(minutes / 60);
262
+ const rest = minutes % 60;
263
+ return rest > 0 ? `quiet ${hours}h${rest}m` : `quiet ${hours}h`;
264
+ }
265
+
198
266
  // ---------------------------------------------------------------------------
199
- // Honest usage/cost helpers (W0.9 convention, ported from agent-inspector-shared.ts)
267
+ // Honest usage/cost helpers (convention, ported from agent-inspector-shared.ts)
200
268
  // ---------------------------------------------------------------------------
201
269
 
202
270
  /**
@@ -261,7 +329,7 @@ function collectBlockedSubtrees(
261
329
  if (cached !== undefined) return cached;
262
330
  if (visiting.has(node.id)) return false; // cycle guard
263
331
  visiting.add(node.id);
264
- let has = isBlockedOnUserState(node.state);
332
+ let has = isBlockedOnUserNode(node);
265
333
  for (const child of childrenByParent.get(node.id) ?? []) {
266
334
  if (dfs(child)) has = true;
267
335
  }
@@ -431,7 +499,9 @@ function isFleetAggregateRollupNode(node: ProcessNode): boolean {
431
499
  * count in the way summing its rolled-up usage/cost would be.
432
500
  */
433
501
  function isFleetRunningLeaf(node: ProcessNode): boolean {
434
- return !ROLLUP_KINDS.has(node.kind) && isRunningProcessState(node.state);
502
+ // Observed foreign agents are NEVER counted in our own fleet counts — they
503
+ // are externally-launched sessions goodvibes only watches, not work it runs.
504
+ return node.kind !== 'observed-external' && !ROLLUP_KINDS.has(node.kind) && isRunningProcessState(node.state);
435
505
  }
436
506
 
437
507
  /** Build the full FleetSnapshot (rows + honest aggregates) from a flat node list. */
@@ -458,7 +528,7 @@ export function buildFleetSnapshot(nodes: readonly ProcessNode[], capturedAt: nu
458
528
  // Blocked ids in display (row) order so the panel's jump key steps through
459
529
  // them top-to-bottom exactly as they appear on screen.
460
530
  const blockedNodeIds = rows
461
- .filter((row) => isBlockedOnUserState(row.node.state))
531
+ .filter((row) => isBlockedOnUserNode(row.node))
462
532
  .map((row) => row.node.id);
463
533
 
464
534
  return { rows, totalCost, totalTokens, runningCount, blockedNodeIds, capturedAt };