@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
@@ -0,0 +1,128 @@
1
+ // ---------------------------------------------------------------------------
2
+ // fleet-observed-render.ts — the fleet-pane rows for observed FOREIGN coding
3
+ // agents (kind 'observed-external').
4
+ //
5
+ // goodvibes did NOT spawn these — they are externally-launched Claude Code /
6
+ // Codex / opencode sessions, detected read-only from OS signals. The row's
7
+ // FIRST job is visibility ("it's in claude or codex"): goodvibes never presents
8
+ // itself as the foreign session's cockpit. So an observed row:
9
+ // - shows its honest external kind + a liveness-derived state (active/quiet),
10
+ // - is NEVER counted in any of the panel's own-fleet counts,
11
+ // - carries NO stop affordance, ever,
12
+ // - offers steer as a DRILL-IN only (in the detail view), and only where a
13
+ // real channel exists; where steer.kind is 'none' the detail states why.
14
+ // ---------------------------------------------------------------------------
15
+
16
+ import type { ProcessNode, ProcessObserved, ObservedAgentKind } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
17
+ import type { Line } from '../types/grid.ts';
18
+ import { buildPanelLine, buildSearchInputLine, DEFAULT_PANEL_PALETTE, type PanelPalette } from './polish.ts';
19
+ import { truncateDisplay, wrapText } from '../utils/terminal-width.ts';
20
+
21
+ /** An observed-external node carries its foreign-agent facts on `observed`. */
22
+ export type ObservedNode = ProcessNode & { readonly observed: ProcessObserved };
23
+
24
+ /** True for a foreign observed-agent node (never one of our own fleet rows). */
25
+ export function isObservedExternalNode(node: ProcessNode): node is ObservedNode {
26
+ return node.kind === 'observed-external' && node.observed !== undefined;
27
+ }
28
+
29
+ /** The honest plain-language label for an observed foreign agent's kind. */
30
+ export function observedKindLabel(kind: ObservedAgentKind): string {
31
+ switch (kind) {
32
+ case 'claude-code': return 'Claude Code';
33
+ case 'codex': return 'Codex';
34
+ case 'opencode': return 'opencode';
35
+ default: return 'unknown agent';
36
+ }
37
+ }
38
+
39
+ /** Liveness glyph: a filled ring while CPU is advancing, a hollow one while quiet. */
40
+ function livenessGlyph(state: ProcessObserved['liveness']['state']): string {
41
+ return state === 'active' ? '◉' : '○';
42
+ }
43
+
44
+ /**
45
+ * One observed-external row. Deliberately NOT the standard fleet row: no cost/
46
+ * token/steer-badge columns (a foreign session has no LLM turn we account for),
47
+ * no stop marker — just the glyph, an "observed" tag, the session label, its
48
+ * external kind, and its liveness state.
49
+ */
50
+ export function renderObservedRowLine(node: ObservedNode, width: number, palette: PanelPalette = DEFAULT_PANEL_PALETTE): Line {
51
+ const C = palette;
52
+ const observed = node.observed;
53
+ const glyph = livenessGlyph(observed.liveness.state);
54
+ const kindLabel = observedKindLabel(observed.externalKind);
55
+ // The label is width-budgeted so the trailing "kind · state" tell never
56
+ // overflows the row (the tell is the honesty signal — it must survive).
57
+ const tell = `${kindLabel} · ${observed.liveness.state}`;
58
+ const labelBudget = Math.max(4, width - tell.length - 14);
59
+ return buildPanelLine(width, [
60
+ [' ', C.dim],
61
+ [glyph, observed.liveness.state === 'active' ? (C.info ?? DEFAULT_PANEL_PALETTE.info) : C.dim],
62
+ [' observed ', C.dim],
63
+ [truncateDisplay(node.label, labelBudget), C.value],
64
+ [' ', C.dim],
65
+ [tell, C.dim],
66
+ ]);
67
+ }
68
+
69
+ /**
70
+ * The detail block for a selected observed-external row. States the foreign
71
+ * facts (external kind, pid, cwd, liveness meaning), then the steer affordance —
72
+ * a DRILL-IN: where a real channel exists it names the action; where steer.kind
73
+ * is 'none' it states the honest reason. It NEVER shows a stop affordance —
74
+ * observing a foreign session is not owning its lifecycle.
75
+ */
76
+ /**
77
+ * Render the observed-row detail. When `steerDraft` is a string the drill-in
78
+ * steer composer is open on THIS row: an active input line replaces the passive
79
+ * "s: steer" hint. A channel-less row never shows an input (owner ruling) — its
80
+ * detail states the honest reason and stops there.
81
+ */
82
+ export function renderObservedDetailLines(node: ObservedNode, width: number, palette: PanelPalette = DEFAULT_PANEL_PALETTE, steerDraft: string | null = null): Line[] {
83
+ const C = palette;
84
+ const observed = node.observed;
85
+ const lines: Line[] = [];
86
+ lines.push(buildPanelLine(width, [
87
+ [' ', C.dim],
88
+ [livenessGlyph(observed.liveness.state), observed.liveness.state === 'active' ? (C.info ?? DEFAULT_PANEL_PALETTE.info) : C.dim],
89
+ [' observed ', C.dim],
90
+ [observedKindLabel(observed.externalKind), C.value],
91
+ [' pid ', C.label],
92
+ [String(observed.pid), C.value],
93
+ [' state ', C.label],
94
+ [observed.liveness.state, C.value],
95
+ ]));
96
+ if (observed.cwd) {
97
+ lines.push(buildPanelLine(width, [[' cwd ', C.label], [truncateDisplay(observed.cwd, Math.max(0, width - 6)), C.dim]]));
98
+ }
99
+ // Liveness meaning, verbatim — honest about what 'quiet' can and cannot tell.
100
+ for (const segment of wrapText(observed.liveness.detail, Math.max(1, width - 2))) {
101
+ lines.push(buildPanelLine(width, [[' ', C.dim], [segment, C.dim]]));
102
+ }
103
+ // Steer is a drill-in: a real channel is named here (a message sent from this
104
+ // detail travels over it — fleet.observed.steer), and where none exists the
105
+ // honest reason is stated instead of a dead action. Wrapped so a long reason
106
+ // is never clipped at a narrow width.
107
+ const steerText = observed.steer.kind === 'tmux'
108
+ ? `available via tmux pane ${observed.steer.paneId} (send-keys)`
109
+ : `unavailable — ${observed.steer.reason}`;
110
+ const steerFg = observed.steer.kind === 'tmux' ? (C.info ?? DEFAULT_PANEL_PALETTE.info) : C.dim;
111
+ const steerSegments = wrapText(steerText, Math.max(1, width - 8));
112
+ steerSegments.forEach((segment, i) => {
113
+ lines.push(buildPanelLine(width, [[i === 0 ? ' steer ' : ' ', C.label], [segment, steerFg]]));
114
+ });
115
+ // Drill-in compose input: only where a real channel exists. An active draft
116
+ // renders the input line + send/cancel hint; otherwise a discoverability hint.
117
+ if (observed.steer.kind === 'tmux') {
118
+ if (steerDraft !== null) {
119
+ lines.push(buildSearchInputLine(width, ' send ', `${steerDraft}_`, C, { active: true, bg: C.inputBg, valueColor: C.info }));
120
+ lines.push(buildPanelLine(width, [[' ', C.label], ['Enter: send Esc: cancel', C.dim]]));
121
+ } else {
122
+ lines.push(buildPanelLine(width, [[' ', C.label], ['s: steer this session', C.dim]]));
123
+ }
124
+ }
125
+ // Visibility, not a cockpit: there is no stop here, by design.
126
+ lines.push(buildPanelLine(width, [[' stop ', C.label], ['not offered — goodvibes only observes this foreign session', C.dim]]));
127
+ return lines;
128
+ }
@@ -7,20 +7,23 @@
7
7
  // renderItem/renderDetail call, with no `this` dependency.
8
8
  // ---------------------------------------------------------------------------
9
9
 
10
- import type { ProcessCostState, ProcessNode, ProcessUsage } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
10
+ import type { ProcessCostState, ProcessNode, ProcessReviewSummary, ProcessUsage } from '@pellux/goodvibes-sdk/platform/runtime/fleet';
11
11
  import type { Line } from '../types/grid.ts';
12
12
  import { formatAgentCost } from './agent-inspector-shared.ts';
13
13
  import { buildPanelLine, DEFAULT_PANEL_PALETTE, type ColumnSpec, type PanelPalette } from './polish.ts';
14
- import { fleetUsageTokens, hasFleetCost, type FleetStateTone } from './fleet-read-model.ts';
14
+ import { fleetAttentionText, fleetNodeAttention, fleetStallMarker, fleetUsageTokens, hasFleetCost, type FleetStateTone } from './fleet-read-model.ts';
15
15
  import { fleetStateDisplay } from './fleet-stop.ts';
16
16
  import { formatElapsed } from '../utils/format-elapsed.ts';
17
17
  import { truncateDisplay } from '../utils/terminal-width.ts';
18
- import { formatWorkItemIsolationDetailFromRaw } from './fleet-panel-worktree-detail.ts';
18
+ import { conflictFilesFromRaw, formatWorkItemIsolationDetailFromRaw } from './fleet-panel-worktree-detail.ts';
19
+ import { wrapText } from '../utils/terminal-width.ts';
19
20
  import { buildAlignedRow } from './polish.ts';
20
21
  import { fleetKindTag } from './fleet-read-model.ts';
21
22
  import { steerBadgeGlyph, steerBadgeTone } from './fleet-steer.ts';
22
23
  import type { SteerBadge } from './fleet-tabs.ts';
23
24
  import type { FleetTreeRow } from './fleet-read-model.ts';
25
+ import { isObservedExternalNode, renderObservedDetailLines, renderObservedRowLine } from './fleet-observed-render.ts';
26
+ import { renderPoolSummary, type WorkstreamGraphSnapshot } from './workstream-graph-render.ts';
24
27
 
25
28
  // Column widths for the tree row layout. `label` absorbs whatever width is
26
29
  // left over after the fixed columns + gaps; on hostile (narrow) widths the
@@ -88,6 +91,9 @@ export function renderFleetRowLine(
88
91
  ): Line {
89
92
  const C = palette;
90
93
  const node = row.node;
94
+ // Observed foreign agents render as their own visibility row (no cost/steer
95
+ // columns, no stop marker) — see fleet-observed-render.ts.
96
+ if (isObservedExternalNode(node)) return renderObservedRowLine(node, width, palette);
91
97
  const disp = fleetStateDisplay(node.state, stopping, blocked);
92
98
  const color = toneColor(disp.tone, C);
93
99
  // Best-of-N grouping: badge a sibling attempt so the N candidates read as one
@@ -96,16 +102,39 @@ export function renderFleetRowLine(
96
102
  ? ` [attempt ${node.attemptGroup.index + 1}/${node.attemptGroup.total}${node.attemptGroup.held ? ', held' : ''}]`
97
103
  : '';
98
104
  const label = `${row.treePrefix}${node.label}${attemptBadge}`;
99
- // Activity column doubles as the badge slot: 'blocked on you' shouts the
100
- // wait louder than the raw currentActivity text ('awaiting approval …').
105
+ // Activity column doubles as the badge slot, in priority order:
106
+ // stopping > waiting-on-human text ('blocked on you' / 'needs your input')
107
+ // > the read-model HEADLINE (derived from task/phase identity only —
108
+ // replaced in place, never a scrolling feed) with the stall marker
109
+ // ('quiet Nm') appended when the node has gone observably silent.
110
+ // Nodes without a headline fall back to the live currentActivity text.
111
+ const attention = fleetNodeAttention(node);
112
+ const stallMarker = fleetStallMarker(node);
113
+ const steadyText = node.headline?.text ?? node.currentActivity?.text ?? '';
114
+ // The stall marker must survive the 20-char activity cell: budget the
115
+ // headline around it (the marker is the honesty signal; a stalled node must
116
+ // never render as if it were healthily mid-headline).
117
+ const steadyWithStall = stallMarker
118
+ ? (steadyText
119
+ ? `${truncateDisplay(steadyText, Math.max(1, ACTIVITY_W - stallMarker.length - 3))} · ${stallMarker}`
120
+ : stallMarker)
121
+ : steadyText;
101
122
  const activity = stopping
102
123
  ? disp.label
103
124
  : blocked
104
- ? 'blocked on you'
125
+ ? fleetAttentionText(attention ?? { reason: 'approval' })
105
126
  : badge
106
- ? `${steerBadgeGlyph(badge.status)} ${node.currentActivity?.text ?? ''}`.trimEnd()
107
- : (node.currentActivity?.text ?? '');
108
- const activityColor = stopping ? color : blocked ? color : badge ? steerBadgeTone(badge.status, C) : C.dim;
127
+ ? `${steerBadgeGlyph(badge.status)} ${steadyWithStall}`.trimEnd()
128
+ : steadyWithStall;
129
+ const activityColor = stopping
130
+ ? color
131
+ : blocked
132
+ ? color
133
+ : badge
134
+ ? steerBadgeTone(badge.status, C)
135
+ : stallMarker
136
+ ? (C.warn ?? DEFAULT_PANEL_PALETTE.warn)
137
+ : C.dim;
109
138
 
110
139
  return buildAlignedRow(
111
140
  width,
@@ -130,16 +159,103 @@ export function renderFleetRowLine(
130
159
  * 'state' text never claims a past-tense outcome mid-write, and a
131
160
  * blocked-on-user node reads 'blocked on you' here too.
132
161
  */
162
+ /**
163
+ * The reviewer's acceptance checklist + verdict, from the fleet node's served
164
+ * `review` field (ProcessReviewSummary, rides fleet.snapshot/list). Rendered
165
+ * only when a review has completed — never an empty shell. Each item shows
166
+ * whether it was verified, the evidence, and (when present) how it was
167
+ * exercised, matching what the webui's review detail surfaces.
168
+ */
169
+ export function renderReviewLines(review: ProcessReviewSummary, width: number, palette: PanelPalette = DEFAULT_PANEL_PALETTE): Line[] {
170
+ const C = palette;
171
+ const passTone = C.good ?? C.info;
172
+ const failTone = C.bad ?? C.warn ?? DEFAULT_PANEL_PALETTE.warn;
173
+ const cyclesLabel = review.cycles === 1 ? '1 cycle' : `${review.cycles} cycles`;
174
+ const lines: Line[] = [
175
+ buildPanelLine(width, [
176
+ [' review ', C.label],
177
+ [review.passed ? '✓ passed' : '✗ not passed', review.passed ? passTone : failTone],
178
+ [' score ', C.label], [String(review.score), C.value],
179
+ [' ', C.dim], [cyclesLabel, C.dim],
180
+ ]),
181
+ ];
182
+ if (review.checklist.length === 0) {
183
+ // Empty checklist is itself a gate failure — say so honestly, don't hide it.
184
+ lines.push(buildPanelLine(width, [[' ', C.dim], ['(the reviewer emitted no acceptance checklist — a gate failure)', failTone]]));
185
+ return lines;
186
+ }
187
+ for (const it of review.checklist) {
188
+ const mark = it.verified ? '[verified] ' : '[unverified]';
189
+ const markTone = it.verified ? passTone : failTone;
190
+ wrapText(it.item, Math.max(1, width - 16)).forEach((seg, i) => {
191
+ lines.push(buildPanelLine(width, i === 0
192
+ ? [[' ', C.dim], [`${mark} `, markTone], [seg, C.value]]
193
+ : [[' ', C.dim], [seg, C.value]]));
194
+ });
195
+ if (it.evidence) {
196
+ for (const seg of wrapText(`evidence: ${it.evidence}`, Math.max(1, width - 6))) {
197
+ lines.push(buildPanelLine(width, [[' ', C.dim], [seg, C.dim]]));
198
+ }
199
+ }
200
+ if (it.howExercised) {
201
+ for (const seg of wrapText(`exercised: ${it.howExercised}`, Math.max(1, width - 6))) {
202
+ lines.push(buildPanelLine(width, [[' ', C.dim], [seg, C.dim]]));
203
+ }
204
+ }
205
+ }
206
+ return lines;
207
+ }
208
+
209
+ /**
210
+ * The task graph's edges + elastic-pool posture for a workstream row, rendered
211
+ * IN the fleet detail (the tree shows nodes; edges/pool used to be reachable
212
+ * only via /graph — /graph still works). Fetched lazily and cached by fleet-acts;
213
+ * rendered only once a snapshot is in hand. An empty graph states so honestly.
214
+ */
215
+ export function renderGraphPostureLines(graph: WorkstreamGraphSnapshot, width: number, palette: PanelPalette = DEFAULT_PANEL_PALETTE): Line[] {
216
+ const C = palette;
217
+ const lines: Line[] = [
218
+ buildPanelLine(width, [[' graph ', C.label], [renderPoolSummary(graph.pool), C.value]]),
219
+ ];
220
+ if (graph.edges.length === 0) {
221
+ lines.push(buildPanelLine(width, [[' ', C.dim], ['no dependency edges', C.dim]]));
222
+ return lines;
223
+ }
224
+ const titleById = new Map(graph.nodes.map((n) => [n.id, n.title]));
225
+ lines.push(buildPanelLine(width, [[' edges ', C.label], [`${graph.edges.length} dependency link(s)`, C.value]]));
226
+ for (const edge of graph.edges) {
227
+ const from = titleById.get(edge.from) ?? edge.from;
228
+ const to = titleById.get(edge.to) ?? edge.to;
229
+ for (const seg of wrapText(`${from} → ${to}`, Math.max(1, width - 5))) {
230
+ lines.push(buildPanelLine(width, [[' ', C.dim], [seg, C.dim]]));
231
+ }
232
+ }
233
+ return lines;
234
+ }
235
+
133
236
  export function renderFleetDetailLines(
134
237
  node: ProcessNode,
135
238
  width: number,
136
239
  stopping: boolean,
137
240
  blocked: boolean,
138
241
  palette: PanelPalette = DEFAULT_PANEL_PALETTE,
242
+ observedSteerDraft: string | null = null,
243
+ graphSnapshot: WorkstreamGraphSnapshot | null = null,
139
244
  ): Line[] {
140
245
  const C = palette;
246
+ // Observed foreign agents drill into their own detail (facts + steer-or-reason,
247
+ // never a stop) — see fleet-observed-render.ts. The drill-in steer draft (when
248
+ // the composer is open on this row) renders an input line in that detail.
249
+ if (isObservedExternalNode(node)) return renderObservedDetailLines(node, width, palette, observedSteerDraft);
141
250
  const disp = fleetStateDisplay(node.state, stopping, blocked);
142
251
  const color = toneColor(disp.tone, C);
252
+ // A blocked node's state text names WHY in the reason's own words
253
+ // ('needs your pick' / 'merge conflict waiting on you' / …) so the detail
254
+ // block never contradicts the row's reason-specific badge; the shared ⚑
255
+ // glyph/tone (disp) is unchanged. 'stopping' still wins over the reason.
256
+ const stateLabel = blocked && !stopping
257
+ ? fleetAttentionText(fleetNodeAttention(node) ?? { reason: 'approval' })
258
+ : disp.label;
143
259
 
144
260
  const line1 = buildPanelLine(width, [
145
261
  [' ', C.dim],
@@ -148,7 +264,7 @@ export function renderFleetDetailLines(
148
264
  [' id ', C.label],
149
265
  [node.id, C.value],
150
266
  [' state ', C.label],
151
- [disp.label, color],
267
+ [stateLabel, color],
152
268
  [' elapsed ', C.label],
153
269
  [formatElapsed(node.elapsedMs), C.value],
154
270
  ]);
@@ -160,6 +276,19 @@ export function renderFleetDetailLines(
160
276
  [' cost ', C.label],
161
277
  [formatFleetCost(node.costUsd, node.costState), C.value],
162
278
  ]);
279
+ // The headline (task/phase identity, replaced in place) gets its own row
280
+ // when present, with the stall marker appended — the detail block mirrors
281
+ // exactly what the tree row's steady slot shows.
282
+ const stallMarker = fleetStallMarker(node);
283
+ const headlineText = node.headline
284
+ ? `${node.headline.text}${stallMarker ? ` · ${stallMarker}` : ''}`
285
+ : stallMarker;
286
+ const headlineLine = headlineText
287
+ ? [buildPanelLine(width, [
288
+ [' headline ', C.label],
289
+ [truncateDisplay(headlineText, Math.max(0, width - 11)), stallMarker ? (C.warn ?? DEFAULT_PANEL_PALETTE.warn) : C.value],
290
+ ])]
291
+ : [];
163
292
  const activityText = node.currentActivity
164
293
  ? `${node.currentActivity.kind}: ${node.currentActivity.text}`
165
294
  : '(no recent activity)';
@@ -170,5 +299,28 @@ export function renderFleetDetailLines(
170
299
  // Approval history attaches here once session tabs land.
171
300
  const line4 = buildPanelLine(width, [[' approvals ', C.label], ['—', C.dim]]);
172
301
  const isolationDetail = node.kind === 'work-item' ? formatWorkItemIsolationDetailFromRaw(node.raw) : null;
173
- return [line1, line2, line3, line4, ...(isolationDetail ? [buildPanelLine(width, [[' isolation ', C.label], [isolationDetail, C.dim]])] : [])];
302
+ // A merge-conflict row shows its STRUCTURED conflicting-path list, wrapped so a
303
+ // long path is fully readable — never clipped (STEP 4: the conflict row acts,
304
+ // and the operator sees exactly which files need resolving before pressing Enter).
305
+ const conflictFiles = node.kind === 'work-item' ? conflictFilesFromRaw(node.raw) : null;
306
+ const conflictLines: Line[] = conflictFiles
307
+ ? [
308
+ buildPanelLine(width, [[' conflicts ', C.label], [`${conflictFiles.length} file(s) — press Enter to resolve`, C.warn ?? DEFAULT_PANEL_PALETTE.warn]]),
309
+ // Prefix is 3 spaces of indent + a 2-col bullet/continuation marker, so
310
+ // wrap the path at width-5 to keep the composed line within `width`
311
+ // (a hard-wrapped long path is fully readable, never clipped).
312
+ ...conflictFiles.flatMap((file) =>
313
+ wrapText(file, Math.max(1, width - 5)).map((segment, i) =>
314
+ buildPanelLine(width, [[' ', C.dim], [i === 0 ? `• ${segment}` : ` ${segment}`, C.value]]),
315
+ ),
316
+ ),
317
+ ]
318
+ : [];
319
+ // The reviewer's acceptance checklist + verdict, when a review has completed
320
+ // (served on node.review; absent before any review — never an empty shell).
321
+ const reviewLines = node.review ? renderReviewLines(node.review, width, C) : [];
322
+ // The task graph's edges/pool posture for a workstream row (fetched + cached
323
+ // by fleet-acts); rendered in-panel under the chain, /graph still available.
324
+ const graphLines = graphSnapshot ? renderGraphPostureLines(graphSnapshot, width, C) : [];
325
+ return [line1, line2, ...headlineLine, line3, line4, ...(isolationDetail ? [buildPanelLine(width, [[' isolation ', C.label], [isolationDetail, C.dim]])] : []), ...conflictLines, ...reviewLines, ...graphLines];
174
326
  }
@@ -46,3 +46,17 @@ export function formatWorkItemIsolationDetailFromRaw(raw: unknown): string | nul
46
46
  const item = (raw as { item?: WorkItem } | undefined)?.item;
47
47
  return item ? formatWorkItemIsolationDetail(item) : null;
48
48
  }
49
+
50
+ /**
51
+ * The STRUCTURED conflicting-path list a merge-conflict work item recorded
52
+ * (WorkItem.conflictFiles), for the Fleet detail block to show a human exactly
53
+ * which files need resolving. Null when the node is not a conflicted work item
54
+ * or recorded no files. The caller wraps these (never clips) so a long path is
55
+ * always fully readable.
56
+ */
57
+ export function conflictFilesFromRaw(raw: unknown): readonly string[] | null {
58
+ const item = (raw as { item?: WorkItem } | undefined)?.item;
59
+ if (!item || item.mergeState !== 'conflict') return null;
60
+ const files = item.conflictFiles;
61
+ return files && files.length > 0 ? files : null;
62
+ }