@pellux/goodvibes-tui 1.1.0 → 1.9.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 (267) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +28 -18
  3. package/docs/foundation-artifacts/operator-contract.json +4045 -1763
  4. package/package.json +2 -2
  5. package/src/audio/spoken-turn-controller.ts +12 -2
  6. package/src/audio/spoken-turn-wiring.ts +2 -1
  7. package/src/cli/help.ts +8 -1
  8. package/src/cli/service-posture.ts +2 -1
  9. package/src/cli/status.ts +2 -1
  10. package/src/cli/surface-command.ts +2 -2
  11. package/src/core/alert-gating.ts +67 -0
  12. package/src/core/approval-alert.ts +72 -0
  13. package/src/core/budget-breach-notifier.ts +106 -0
  14. package/src/core/composer-state.ts +11 -3
  15. package/src/core/conversation-line-cache.ts +27 -3
  16. package/src/core/conversation-rendering.ts +90 -14
  17. package/src/core/conversation.ts +18 -0
  18. package/src/core/focus-tracker.ts +41 -0
  19. package/src/core/long-task-notifier.ts +33 -6
  20. package/src/core/stream-event-wiring.ts +20 -1
  21. package/src/core/system-message-noise.ts +87 -0
  22. package/src/core/system-message-router.ts +104 -51
  23. package/src/core/turn-cancellation.ts +25 -0
  24. package/src/core/turn-event-wiring.ts +72 -3
  25. package/src/daemon/cli.ts +29 -2
  26. package/src/daemon/handlers/register.ts +8 -1
  27. package/src/daemon/service-commands.ts +329 -0
  28. package/src/export/cost-utils.ts +36 -0
  29. package/src/input/autocomplete.ts +27 -1
  30. package/src/input/command-registry.ts +84 -5
  31. package/src/input/commands/checkpoint-runtime.ts +280 -0
  32. package/src/input/commands/codebase-runtime.ts +232 -0
  33. package/src/input/commands/config.ts +43 -3
  34. package/src/input/commands/eval.ts +1 -1
  35. package/src/input/commands/health-runtime.ts +10 -2
  36. package/src/input/commands/image-runtime.ts +112 -0
  37. package/src/input/commands/intelligence-runtime.ts +11 -1
  38. package/src/input/commands/local-auth-runtime.ts +4 -1
  39. package/src/input/commands/local-runtime.ts +9 -3
  40. package/src/input/commands/marketplace-runtime.ts +2 -2
  41. package/src/input/commands/memory.ts +73 -35
  42. package/src/input/commands/operator-panel-runtime.ts +71 -31
  43. package/src/input/commands/planning-runtime.ts +116 -4
  44. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  45. package/src/input/commands/plugin-runtime.ts +2 -2
  46. package/src/input/commands/policy-dispatch.ts +21 -0
  47. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  48. package/src/input/commands/qrcode-runtime.ts +26 -6
  49. package/src/input/commands/recall-review.ts +35 -0
  50. package/src/input/commands/remote-runtime-setup.ts +5 -3
  51. package/src/input/commands/remote-runtime.ts +3 -3
  52. package/src/input/commands/runtime-services.ts +54 -13
  53. package/src/input/commands/services-runtime.ts +1 -1
  54. package/src/input/commands/session-content.ts +20 -9
  55. package/src/input/commands/session-workflow.ts +16 -1
  56. package/src/input/commands/settings-sync-runtime.ts +16 -4
  57. package/src/input/commands/shell-core.ts +25 -8
  58. package/src/input/commands/skills-runtime.ts +2 -8
  59. package/src/input/commands/subscription-runtime.ts +2 -2
  60. package/src/input/commands/test-runtime.ts +277 -0
  61. package/src/input/commands/websearch-runtime.ts +101 -0
  62. package/src/input/commands/work-plan-runtime.ts +36 -10
  63. package/src/input/commands/workstream-runtime.ts +488 -0
  64. package/src/input/commands.ts +12 -0
  65. package/src/input/config-modal-types.ts +175 -0
  66. package/src/input/config-modal.ts +592 -0
  67. package/src/input/feed-context-factory.ts +10 -8
  68. package/src/input/handler-command-route.ts +37 -20
  69. package/src/input/handler-content-actions.ts +17 -2
  70. package/src/input/handler-feed-routes.ts +74 -114
  71. package/src/input/handler-feed.ts +79 -27
  72. package/src/input/handler-interactions.ts +1 -3
  73. package/src/input/handler-modal-routes.ts +136 -6
  74. package/src/input/handler-modal-stack.ts +14 -7
  75. package/src/input/handler-modal-token-routes.ts +16 -49
  76. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  77. package/src/input/handler-onboarding.ts +71 -59
  78. package/src/input/handler-picker-routes.ts +26 -102
  79. package/src/input/handler-shortcuts.ts +74 -14
  80. package/src/input/handler-types.ts +2 -6
  81. package/src/input/handler-ui-state.ts +10 -22
  82. package/src/input/handler.ts +12 -29
  83. package/src/input/keybindings.ts +23 -8
  84. package/src/input/model-picker-types.ts +24 -6
  85. package/src/input/model-picker.ts +77 -2
  86. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  87. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  88. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  89. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  90. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  91. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  92. package/src/input/panel-integration-actions.ts +9 -170
  93. package/src/input/panel-mouse-geometry.ts +97 -0
  94. package/src/input/panel-paste-flood-guard.ts +86 -0
  95. package/src/input/selection-modal.ts +11 -0
  96. package/src/input/session-picker-modal.ts +44 -4
  97. package/src/input/settings-modal-data.ts +146 -2
  98. package/src/input/settings-modal-mutations.ts +6 -4
  99. package/src/input/settings-modal-types.ts +4 -2
  100. package/src/main.ts +49 -51
  101. package/src/panels/agent-inspector-shared.ts +2 -1
  102. package/src/panels/base-panel.ts +22 -1
  103. package/src/panels/builtin/agent.ts +21 -107
  104. package/src/panels/builtin/development.ts +15 -61
  105. package/src/panels/builtin/knowledge.ts +8 -32
  106. package/src/panels/builtin/operations.ts +84 -428
  107. package/src/panels/builtin/session.ts +21 -112
  108. package/src/panels/builtin/shared.ts +18 -46
  109. package/src/panels/builtin-modals.ts +218 -0
  110. package/src/panels/builtin-panels.ts +5 -0
  111. package/src/panels/cost-tracker-panel.ts +36 -10
  112. package/src/panels/diff-panel.ts +12 -43
  113. package/src/panels/eval-registry.ts +60 -0
  114. package/src/panels/fleet-deep-link.ts +31 -0
  115. package/src/panels/fleet-panel-format.ts +62 -0
  116. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  117. package/src/panels/fleet-panel.ts +758 -0
  118. package/src/panels/fleet-read-model.ts +520 -0
  119. package/src/panels/fleet-steer.ts +125 -0
  120. package/src/panels/fleet-stop.ts +153 -0
  121. package/src/panels/fleet-tabs.ts +230 -0
  122. package/src/panels/fleet-transcript.ts +356 -0
  123. package/src/panels/index.ts +7 -31
  124. package/src/panels/modals/hooks-modal.ts +187 -0
  125. package/src/panels/modals/keybindings-modal.ts +166 -0
  126. package/src/panels/modals/knowledge-modal.ts +158 -0
  127. package/src/panels/modals/local-auth-modal.ts +132 -0
  128. package/src/panels/modals/marketplace-modal.ts +218 -0
  129. package/src/panels/modals/memory-modal.ts +180 -0
  130. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  131. package/src/panels/modals/modal-theme.ts +42 -0
  132. package/src/panels/modals/pairing-modal.ts +163 -0
  133. package/src/panels/modals/planning-modal.ts +304 -0
  134. package/src/panels/modals/plugins-modal.ts +174 -0
  135. package/src/panels/modals/policy-modal.ts +256 -0
  136. package/src/panels/modals/provider-health-modal.ts +136 -0
  137. package/src/panels/modals/remote-modal.ts +115 -0
  138. package/src/panels/modals/sandbox-modal.ts +150 -0
  139. package/src/panels/modals/security-modal.ts +217 -0
  140. package/src/panels/modals/services-modal.ts +179 -0
  141. package/src/panels/modals/settings-sync-modal.ts +142 -0
  142. package/src/panels/modals/skills-modal.ts +189 -0
  143. package/src/panels/modals/subscription-modal.ts +172 -0
  144. package/src/panels/modals/work-plan-modal.ts +177 -0
  145. package/src/panels/panel-confirm-overlay.ts +72 -0
  146. package/src/panels/panel-manager.ts +123 -9
  147. package/src/panels/polish-core.ts +38 -25
  148. package/src/panels/project-planning-answer-actions.ts +8 -11
  149. package/src/panels/skills-panel.ts +7 -51
  150. package/src/panels/types.ts +37 -0
  151. package/src/permissions/hunk-selection.ts +179 -0
  152. package/src/permissions/prompt.ts +216 -13
  153. package/src/renderer/autocomplete-overlay.ts +28 -2
  154. package/src/renderer/compaction-history-modal.ts +19 -3
  155. package/src/renderer/compaction-preview.ts +13 -2
  156. package/src/renderer/compaction-quality.ts +100 -0
  157. package/src/renderer/compositor.ts +2 -3
  158. package/src/renderer/config-modal.ts +95 -0
  159. package/src/renderer/conversation-overlays.ts +10 -17
  160. package/src/renderer/fleet-tab-strip.ts +56 -0
  161. package/src/renderer/footer-tips.ts +15 -3
  162. package/src/renderer/fullscreen-primitives.ts +32 -22
  163. package/src/renderer/git-status.ts +43 -1
  164. package/src/renderer/help-overlay.ts +2 -2
  165. package/src/renderer/layout.ts +0 -4
  166. package/src/renderer/markdown.ts +7 -3
  167. package/src/renderer/modal-factory.ts +25 -20
  168. package/src/renderer/model-workspace.ts +62 -4
  169. package/src/renderer/overlay-box.ts +21 -17
  170. package/src/renderer/panel-workspace-bar.ts +8 -2
  171. package/src/renderer/process-indicator.ts +14 -3
  172. package/src/renderer/selection-modal-overlay.ts +6 -1
  173. package/src/renderer/session-picker-modal.ts +196 -3
  174. package/src/renderer/settings-modal-helpers.ts +2 -0
  175. package/src/renderer/settings-modal.ts +7 -0
  176. package/src/renderer/shell-surface.ts +8 -1
  177. package/src/renderer/status-glyphs.ts +14 -15
  178. package/src/renderer/system-message.ts +15 -3
  179. package/src/renderer/terminal-bg-probe.ts +339 -0
  180. package/src/renderer/terminal-escapes.ts +20 -0
  181. package/src/renderer/theme-mode-config.ts +67 -0
  182. package/src/renderer/theme.ts +91 -1
  183. package/src/renderer/thinking.ts +11 -3
  184. package/src/renderer/tool-call.ts +15 -9
  185. package/src/renderer/tool-result-summary.ts +148 -0
  186. package/src/renderer/turn-injection.ts +133 -0
  187. package/src/renderer/ui-factory.ts +154 -85
  188. package/src/renderer/ui-primitives.ts +30 -129
  189. package/src/runtime/bootstrap-command-context.ts +27 -1
  190. package/src/runtime/bootstrap-command-parts.ts +41 -10
  191. package/src/runtime/bootstrap-core.ts +47 -15
  192. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  193. package/src/runtime/bootstrap-shell.ts +48 -17
  194. package/src/runtime/bootstrap.ts +129 -22
  195. package/src/runtime/code-index-services.ts +135 -0
  196. package/src/runtime/legacy-daemon-migration.ts +516 -0
  197. package/src/runtime/memory-fold.ts +26 -0
  198. package/src/runtime/onboarding/derivation.ts +7 -2
  199. package/src/runtime/onboarding/snapshot.ts +27 -1
  200. package/src/runtime/onboarding/types.ts +31 -1
  201. package/src/runtime/operator-token-cleanup.ts +82 -1
  202. package/src/runtime/orchestrator-core-services.ts +59 -0
  203. package/src/runtime/process-lifecycle.ts +3 -1
  204. package/src/runtime/resume-notice.ts +209 -0
  205. package/src/runtime/services.ts +98 -46
  206. package/src/runtime/session-inbound-inputs.ts +252 -0
  207. package/src/runtime/session-spine-transport.ts +64 -0
  208. package/src/runtime/terminal-output-guard.ts +15 -8
  209. package/src/runtime/ui-services.ts +27 -3
  210. package/src/runtime/workstream-services.ts +327 -0
  211. package/src/runtime/wrfc-persistence.ts +124 -17
  212. package/src/shell/blocking-input.ts +68 -4
  213. package/src/shell/recovery-input-helpers.ts +170 -1
  214. package/src/shell/ui-openers.ts +171 -26
  215. package/src/utils/format-duration.ts +8 -18
  216. package/src/utils/splash-lines.ts +1 -1
  217. package/src/utils/terminal-width.ts +52 -0
  218. package/src/version.ts +1 -1
  219. package/src/panels/agent-inspector-panel.ts +0 -786
  220. package/src/panels/approval-panel.ts +0 -252
  221. package/src/panels/automation-control-panel.ts +0 -479
  222. package/src/panels/cockpit-panel.ts +0 -481
  223. package/src/panels/cockpit-read-model.ts +0 -238
  224. package/src/panels/communication-panel.ts +0 -256
  225. package/src/panels/control-plane-panel.ts +0 -470
  226. package/src/panels/debug-panel.ts +0 -615
  227. package/src/panels/docs-panel.ts +0 -384
  228. package/src/panels/eval-panel.ts +0 -627
  229. package/src/panels/file-explorer-panel.ts +0 -673
  230. package/src/panels/file-preview-panel.ts +0 -517
  231. package/src/panels/hooks-panel.ts +0 -401
  232. package/src/panels/incident-review-panel.ts +0 -406
  233. package/src/panels/intelligence-panel.ts +0 -383
  234. package/src/panels/knowledge-graph-panel.ts +0 -515
  235. package/src/panels/marketplace-panel.ts +0 -399
  236. package/src/panels/memory-panel.ts +0 -558
  237. package/src/panels/ops-control-panel.ts +0 -329
  238. package/src/panels/ops-strategy-panel.ts +0 -321
  239. package/src/panels/orchestration-panel.ts +0 -465
  240. package/src/panels/panel-list-panel.ts +0 -566
  241. package/src/panels/plan-dashboard-panel.ts +0 -707
  242. package/src/panels/policy-panel.ts +0 -517
  243. package/src/panels/project-planning-panel.ts +0 -731
  244. package/src/panels/provider-health-panel.ts +0 -678
  245. package/src/panels/provider-health-tracker.ts +0 -310
  246. package/src/panels/provider-health-views.ts +0 -567
  247. package/src/panels/qr-panel.ts +0 -280
  248. package/src/panels/remote-panel.ts +0 -534
  249. package/src/panels/routes-panel.ts +0 -241
  250. package/src/panels/sandbox-panel.ts +0 -456
  251. package/src/panels/security-panel.ts +0 -447
  252. package/src/panels/services-panel.ts +0 -329
  253. package/src/panels/session-browser-panel.ts +0 -496
  254. package/src/panels/settings-sync-panel.ts +0 -398
  255. package/src/panels/subscription-panel.ts +0 -342
  256. package/src/panels/symbol-outline-panel.ts +0 -619
  257. package/src/panels/system-messages-panel.ts +0 -364
  258. package/src/panels/tasks-panel.ts +0 -608
  259. package/src/panels/thinking-panel.ts +0 -333
  260. package/src/panels/tool-inspector-panel.ts +0 -537
  261. package/src/panels/work-plan-panel.ts +0 -540
  262. package/src/panels/worktree-panel.ts +0 -360
  263. package/src/panels/wrfc-panel.ts +0 -790
  264. package/src/renderer/agent-detail-modal.ts +0 -466
  265. package/src/renderer/live-tail-modal.ts +0 -156
  266. package/src/renderer/process-modal.ts +0 -671
  267. package/src/renderer/qr-renderer.ts +0 -120
@@ -0,0 +1,356 @@
1
+ // ---------------------------------------------------------------------------
2
+ // fleet-transcript.ts
3
+ //
4
+ // Wave-3 (W3.1 Part C6) — renders the content of an attached FleetPanel
5
+ // session tab. Three sources, chosen by what the SDK can actually provide
6
+ // (see the W3.1 brief's "central reality check" — a full-fidelity live
7
+ // transcript needs a ConversationMessageSnapshot[] history source, which
8
+ // only exists for a RUNNING agent or a just-completed one still inside the
9
+ // SDK's bounded retention ring):
10
+ //
11
+ // - 'live'/'frozen' — a non-empty ConversationMessageSnapshot[] from
12
+ // AgentManager.getConversationSnapshot(agentId), rendered through the
13
+ // EXACT SAME conversation-rendering machinery the main session surface
14
+ // uses (MessageLineCache -> conversation-rendering.ts render functions),
15
+ // so a fleet tab's transcript is byte-for-byte the same rendering a
16
+ // user would see in their own session. 'live' while the agent is still
17
+ // running, 'frozen' once it has completed but the snapshot has not yet
18
+ // been evicted from the retention ring.
19
+ // - 'unavailable' — a terminal agent whose snapshot came back empty
20
+ // (evicted past the retention bound, or an agent kind — e.g. the WRFC
21
+ // owner — that never registered a live conversation source at all).
22
+ // FleetPanel degrades to the on-disk event ledger (renderFleetLedgerFallback)
23
+ // for this case; this module never fabricates transcript content.
24
+ //
25
+ // A 'wrfc-chain' tab has no single conversation of its own (it coordinates
26
+ // member agents, each of which has its own), so it gets a live member
27
+ // summary (renderFleetChainSummary) instead of a transcript.
28
+ // ---------------------------------------------------------------------------
29
+
30
+ import type { ConversationMessageSnapshot } from '@pellux/goodvibes-sdk/platform/core';
31
+ import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
32
+ import { createEmptyLine, type Line } from '../types/grid.ts';
33
+ import type { MessageLineCache } from '../core/conversation-line-cache.ts';
34
+ import type { ConversationRenderContext } from '../core/conversation-rendering.ts';
35
+ import { renderConversationEventLine, renderConversationNotice } from '../renderer/conversation-surface.ts';
36
+ import { buildPanelLine, DEFAULT_PANEL_PALETTE, type PanelPalette } from './polish.ts';
37
+ import { formatElapsed } from '../utils/format-elapsed.ts';
38
+ import { truncateDisplay } from '../utils/terminal-width.ts';
39
+ import {
40
+ fleetKindTag,
41
+ fleetStateGlyph,
42
+ fleetStateTone,
43
+ type FleetStateTone,
44
+ type FleetTreeRow,
45
+ } from './fleet-read-model.ts';
46
+
47
+ /** Duplicated from fleet-panel.ts's private helper (same tiny mapping) rather than
48
+ * cross-importing — fleet-panel.ts imports THIS module to render tab content, so
49
+ * importing back would form a cycle. See fleet-panel.ts's own toneColor for the
50
+ * canonical copy; keep both in sync if the tone set ever changes. */
51
+ function toneColor(tone: FleetStateTone, palette: PanelPalette): string {
52
+ switch (tone) {
53
+ case 'active': return palette.info ?? DEFAULT_PANEL_PALETTE.info;
54
+ case 'success': return palette.good ?? DEFAULT_PANEL_PALETTE.good;
55
+ case 'failure': return palette.bad ?? DEFAULT_PANEL_PALETTE.bad;
56
+ case 'warn': return palette.warn ?? DEFAULT_PANEL_PALETTE.warn;
57
+ case 'muted': return palette.dim;
58
+ }
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Agent transcript (C6 live/frozen split)
63
+ // ---------------------------------------------------------------------------
64
+
65
+ export type FleetTranscriptMode = 'live' | 'frozen' | 'unavailable';
66
+
67
+ export interface FleetTranscriptRender {
68
+ readonly mode: FleetTranscriptMode;
69
+ readonly lines: Line[];
70
+ }
71
+
72
+ /**
73
+ * W3.3 (design point 4) — a 'frozen' transcript is a static capture of a
74
+ * completed process, not something that will change on the next render. The
75
+ * distinction matters because this wave also introduced kill/interrupt
76
+ * display bugs (elapsed/usage briefly climbing after a terminal state
77
+ * shows) — a done-section tab that looked indistinguishable from a live one
78
+ * would misleadingly suggest the underlying agent is still doing something.
79
+ * Shown once at the top of a 'frozen' tab's content; 'live' tabs never show
80
+ * it, and 'unavailable' tabs get the ledger fallback's own notice instead
81
+ * (renderFleetLedgerFallback below) rather than this one.
82
+ */
83
+ const FROZEN_TRANSCRIPT_NOTICE = 'Read-only — this agent finished; not a live view.';
84
+
85
+ function renderFrozenTranscriptNotice(width: number): Line[] {
86
+ const palette = DEFAULT_PANEL_PALETTE;
87
+ return renderConversationNotice(FROZEN_TRANSCRIPT_NOTICE, width, {
88
+ accent: palette.warn ?? DEFAULT_PANEL_PALETTE.warn,
89
+ text: palette.dim,
90
+ dim: true,
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Render an agent tab's transcript from a `ConversationMessageSnapshot[]`
96
+ * already fetched by the caller (this module does no I/O of its own — it is
97
+ * a pure renderer, same convention as fleet-read-model.ts).
98
+ *
99
+ * `isTerminal` distinguishes 'live' (the agent is still running — the
100
+ * snapshot came from the SDK's live source, so it will change on the next
101
+ * render) from 'frozen' (the agent finished — the snapshot is a static final
102
+ * capture; caller is showing a completed conversation, not a live one).
103
+ * An empty snapshot on a terminal agent means the source was never
104
+ * registered or has been evicted — the caller falls back to
105
+ * `renderFleetLedgerFallback`; an empty snapshot on a non-terminal agent
106
+ * (e.g. the SDK hasn't sent a turn yet) legitimately means "no messages yet".
107
+ */
108
+ export function renderFleetAgentTranscript(
109
+ snapshot: readonly ConversationMessageSnapshot[],
110
+ isTerminal: boolean,
111
+ lineCache: MessageLineCache,
112
+ width: number,
113
+ height: number,
114
+ configManager: ConfigManager | null,
115
+ ): FleetTranscriptRender {
116
+ if (snapshot.length === 0) {
117
+ if (isTerminal) return { mode: 'unavailable', lines: [] };
118
+ return { mode: 'live', lines: [buildPanelLine(width, [[' (no messages yet)', DEFAULT_PANEL_PALETTE.dim]])] };
119
+ }
120
+
121
+ const historyLines: Line[] = [];
122
+ const history = {
123
+ addLine: (line: Line): void => { historyLines.push(line); },
124
+ addLines: (lines: Line[]): void => { historyLines.push(...lines); },
125
+ getLineCount: (): number => historyLines.length,
126
+ };
127
+ // Scratch block/collapse/error registries — collapse-toggle and block-copy
128
+ // interactions are out of scope for this tab (a later item, per the brief's
129
+ // "transcript browse" follow-on); this render is a read-only tail view.
130
+ const context: ConversationRenderContext = {
131
+ history,
132
+ blockRegistry: [],
133
+ collapseState: new Map(),
134
+ errorLineRegistry: [],
135
+ messageKindRegistry: new Map(),
136
+ configManager,
137
+ splashOptions: {},
138
+ };
139
+ const messageLineRegistry: number[] = [];
140
+ lineCache.renderInto(context, snapshot as ConversationMessageSnapshot[], width, messageLineRegistry, 0, -1);
141
+
142
+ // Terminal agents reserve room for the frozen-transcript notice ABOVE the
143
+ // tail window so the "read-only" framing is always visible, never scrolled
144
+ // off by a long conversation's tail-window slice.
145
+ const notice = isTerminal ? renderFrozenTranscriptNotice(width) : [];
146
+ const budgetHeight = Math.max(0, height - notice.length);
147
+ // No `&& budgetHeight > 0` guard: when the notice alone already consumes
148
+ // the whole height budget, `historyLines.slice(historyLines.length - 0)`
149
+ // correctly yields an empty tail (nothing fits), rather than falling
150
+ // through to the full, un-sliced history — which the caller would then
151
+ // head-clip, silently swapping in the OLDEST lines instead of the
152
+ // most-recent tail this view promises.
153
+ const visible = historyLines.length > budgetHeight
154
+ ? historyLines.slice(historyLines.length - budgetHeight)
155
+ : historyLines;
156
+ return { mode: isTerminal ? 'frozen' : 'live', lines: [...notice, ...visible] };
157
+ }
158
+
159
+ // ---------------------------------------------------------------------------
160
+ // Chain summary — 'wrfc-chain' tabs have no single conversation (C2/C6)
161
+ // ---------------------------------------------------------------------------
162
+
163
+ /**
164
+ * Render a live one-line-per-member summary for an attached wrfc-chain tab.
165
+ *
166
+ * `chainDoneOrAbsent` disambiguates an empty member list (d3, W6.2): a completed
167
+ * chain prunes its wrapper node (Wave-3 reap), so zero members can mean the
168
+ * chain FINISHED, not that it has not started. When the chain node is absent
169
+ * (pruned) or terminal, say so honestly instead of the "(no member agents yet)"
170
+ * not-started wording.
171
+ */
172
+ export function renderFleetChainSummary(
173
+ memberRows: readonly FleetTreeRow[],
174
+ width: number,
175
+ chainDoneOrAbsent: boolean,
176
+ ): Line[] {
177
+ const C = DEFAULT_PANEL_PALETTE;
178
+ if (memberRows.length === 0) {
179
+ const message = chainDoneOrAbsent
180
+ ? ' chain completed — members no longer tracked'
181
+ : ' (no member agents yet)';
182
+ return [buildPanelLine(width, [[message, C.dim]])];
183
+ }
184
+ return memberRows.map((row) => {
185
+ const node = row.node;
186
+ const color = toneColor(fleetStateTone(node.state), C);
187
+ return buildPanelLine(width, [
188
+ [' ', C.dim],
189
+ [fleetStateGlyph(node.state), color],
190
+ [` ${fleetKindTag(node.kind)} `, C.dim],
191
+ [node.label, C.value],
192
+ [` ${node.state}`, color],
193
+ [` ${formatElapsed(node.elapsedMs)}`, C.dim],
194
+ ]);
195
+ });
196
+ }
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // Ledger fallback — degraded activity view for a terminal agent whose
200
+ // full-fidelity snapshot is unavailable (C6 "honest fallback")
201
+ // ---------------------------------------------------------------------------
202
+
203
+ /**
204
+ * Parse an agent's `<agentId>.jsonl` event ledger. Tolerant of malformed
205
+ * lines (skipped, not thrown) — matches agent-detail-modal.ts's existing
206
+ * JSONL-reading convention for the same file shape.
207
+ */
208
+ export function parseAgentLedger(raw: string): Record<string, unknown>[] {
209
+ const entries: Record<string, unknown>[] = [];
210
+ for (const line of raw.split('\n')) {
211
+ const trimmed = line.trim();
212
+ if (!trimmed) continue;
213
+ try {
214
+ const value: unknown = JSON.parse(trimmed);
215
+ if (value !== null && typeof value === 'object') entries.push(value as Record<string, unknown>);
216
+ } catch {
217
+ // Skip malformed lines rather than failing the whole fallback view.
218
+ }
219
+ }
220
+ return entries;
221
+ }
222
+
223
+ function formatLedgerDuration(ms: unknown): string {
224
+ return typeof ms === 'number' ? ` (${formatElapsed(ms)})` : '';
225
+ }
226
+
227
+ function renderLedgerEntry(width: number, entry: Record<string, unknown>, palette: PanelPalette): Line {
228
+ const type = typeof entry['type'] === 'string' ? entry['type'] : 'unknown';
229
+ switch (type) {
230
+ case 'meta': {
231
+ const model = typeof entry['model'] === 'string' ? entry['model'] : 'unknown model';
232
+ const provider = typeof entry['provider'] === 'string' ? entry['provider'] : 'unknown';
233
+ return renderConversationEventLine(width, { marker: '●', markerFg: palette.dim, label: 'session', labelFg: palette.dim }, [
234
+ { text: `started — ${model} (${provider})`, fg: palette.value },
235
+ ]);
236
+ }
237
+ case 'session_config': {
238
+ const task = typeof entry['task'] === 'string' ? entry['task'] : '';
239
+ return renderConversationEventLine(width, { marker: '●', markerFg: palette.dim, label: 'task', labelFg: palette.dim }, [
240
+ { text: truncateDisplay(task, Math.max(0, width - 20)), fg: palette.value },
241
+ ]);
242
+ }
243
+ case 'llm_request': {
244
+ const turn = typeof entry['turn'] === 'number' ? entry['turn'] : '?';
245
+ return renderConversationEventLine(width, { marker: '◔', markerFg: palette.info, label: 'request', labelFg: palette.info }, [
246
+ { text: `turn ${turn}`, fg: palette.dim },
247
+ ]);
248
+ }
249
+ case 'llm_response': {
250
+ const turn = typeof entry['turn'] === 'number' ? entry['turn'] : '?';
251
+ const toolCallCount = typeof entry['toolCallCount'] === 'number' ? entry['toolCallCount'] : 0;
252
+ return renderConversationEventLine(width, { marker: '◕', markerFg: palette.info, label: 'response', labelFg: palette.info }, [
253
+ { text: `turn ${turn} · ${toolCallCount} tool call${toolCallCount === 1 ? '' : 's'}`, fg: palette.dim },
254
+ ]);
255
+ }
256
+ case 'tool_execution': {
257
+ const toolName = typeof entry['toolName'] === 'string' ? entry['toolName'] : 'tool';
258
+ const success = entry['success'] !== false;
259
+ const bad = palette.bad ?? DEFAULT_PANEL_PALETTE.bad;
260
+ // W3.3: the writer already truncates this to 500 chars (session.ts's
261
+ // resultPreview field) — passed through verbatim (collapsed to one
262
+ // line), never re-summarized or fabricated, then tail-truncated again
263
+ // to fit the row.
264
+ const rawPreview = typeof entry['resultPreview'] === 'string' ? entry['resultPreview'] : '';
265
+ const preview = rawPreview.replace(/\s+/g, ' ').trim();
266
+ const label = `${toolName}${success ? '' : ' (failed)'}`;
267
+ const text = preview ? `${label} — ${preview}` : label;
268
+ return renderConversationEventLine(width, {
269
+ marker: success ? '●' : '✗',
270
+ markerFg: success ? palette.info : bad,
271
+ label: 'tool',
272
+ labelFg: success ? palette.info : bad,
273
+ }, [
274
+ { text: truncateDisplay(text, Math.max(0, width - 12)), fg: success ? palette.value : bad },
275
+ ]);
276
+ }
277
+ case 'session_end': {
278
+ const status = typeof entry['status'] === 'string' ? entry['status'] : 'ended';
279
+ const good = palette.good ?? DEFAULT_PANEL_PALETTE.good;
280
+ const bad = palette.bad ?? DEFAULT_PANEL_PALETTE.bad;
281
+ const warn = palette.warn ?? DEFAULT_PANEL_PALETTE.warn;
282
+ const tone = status === 'completed' ? good : status === 'failed' ? bad : warn;
283
+ return renderConversationEventLine(width, { marker: '■', markerFg: tone, label: 'session', labelFg: tone }, [
284
+ { text: `${status}${formatLedgerDuration(entry['durationMs'])}`, fg: tone },
285
+ ]);
286
+ }
287
+ // W5.2 (wo803) — Wave-5 wo801 per-turn passive knowledge injection
288
+ // records, appended to this same JSONL ledger as
289
+ // `{type:'knowledge_injection', turn, ...record}` (orchestrator-runner.ts).
290
+ // Without this case these fell through to the generic 'event' default
291
+ // below (just the bare type name) — this renders the honest outcome
292
+ // (what was injected, or why nothing was) instead.
293
+ case 'knowledge_injection': {
294
+ const turn = typeof entry['turn'] === 'number' ? entry['turn'] : '?';
295
+ const injectedIds = Array.isArray(entry['injectedIds']) ? entry['injectedIds'] : [];
296
+ const tokenCost = typeof entry['tokenCost'] === 'number' ? entry['tokenCost'] : 0;
297
+ const backendTag = entry['embeddingBackend'] === 'fallback-lexical' ? ' [lexical fallback]' : '';
298
+ if (injectedIds.length === 0) {
299
+ const reason = typeof entry['reason'] === 'string'
300
+ ? entry['reason']
301
+ : 'nothing cleared the relevance floor';
302
+ return renderConversationEventLine(width, { marker: '◇', markerFg: palette.dim, label: 'knowledge', labelFg: palette.dim }, [
303
+ { text: `turn ${turn} · nothing injected — ${reason}${backendTag}`, fg: palette.dim },
304
+ ]);
305
+ }
306
+ return renderConversationEventLine(width, { marker: '◇', markerFg: palette.info, label: 'knowledge', labelFg: palette.info }, [
307
+ { text: `turn ${turn} · injected ${injectedIds.length} (~${tokenCost} tok)${backendTag}`, fg: palette.value },
308
+ ]);
309
+ }
310
+ default:
311
+ return renderConversationEventLine(width, { marker: '·', markerFg: palette.dim, label: 'event', labelFg: palette.dim }, [
312
+ { text: type, fg: palette.dim },
313
+ ]);
314
+ }
315
+ }
316
+
317
+ /**
318
+ * Degraded activity view for a terminal agent whose full-fidelity
319
+ * conversation snapshot is unavailable (evicted, or never registered).
320
+ * Explicitly framed as an activity log, never presented as a transcript
321
+ * replay (the ledger is a truncated event record — see session.ts /
322
+ * orchestrator-runner.ts: tool args/results are sliced to 500 chars and
323
+ * assistant response TEXT is never written at all, only its length).
324
+ *
325
+ * Mirrors renderFleetAgentTranscript's notice-reservation above: the notice
326
+ * rows are reserved ABOVE the tail window and only the body is tail-sliced,
327
+ * so a long ledger's degraded-view cue is never the thing that scrolls off
328
+ * (previously this tail-sliced `[...notice, ...body]` as one array, which
329
+ * for any ledger longer than the tab height silently dropped the notice
330
+ * itself, leaving what looked like a raw, unexplained activity feed).
331
+ */
332
+ export function renderFleetLedgerFallback(
333
+ entries: readonly Record<string, unknown>[],
334
+ width: number,
335
+ height: number,
336
+ ): Line[] {
337
+ const palette = DEFAULT_PANEL_PALETTE;
338
+ const notice = renderConversationNotice(
339
+ 'Read-only. Full transcript unavailable for this agent — showing its activity log instead.',
340
+ width,
341
+ { accent: palette.warn, text: palette.dim, dim: true },
342
+ );
343
+ const body = entries.length === 0
344
+ ? [buildPanelLine(width, [[' (no activity recorded)', palette.dim]])]
345
+ : entries.map((entry) => renderLedgerEntry(width, entry, palette));
346
+ const budgetHeight = Math.max(0, height - notice.length);
347
+ const visible = body.length > budgetHeight
348
+ ? body.slice(body.length - budgetHeight)
349
+ : body;
350
+ return [...notice, ...visible];
351
+ }
352
+
353
+ /** A blank tab-content placeholder (e.g. while a ledger load is in flight). */
354
+ export function renderFleetTranscriptLoading(width: number): Line[] {
355
+ return [buildPanelLine(width, [[' Loading…', DEFAULT_PANEL_PALETTE.dim]]), createEmptyLine(width)];
356
+ }
@@ -4,39 +4,15 @@ export { BasePanel } from './base-panel.ts';
4
4
  export { PanelManager } from './panel-manager.ts';
5
5
  export { TokenBudgetPanel } from './token-budget-panel.ts';
6
6
  export { CostTrackerPanel } from './cost-tracker-panel.ts';
7
- export { AgentInspectorPanel } from './agent-inspector-panel.ts';
8
- export { WrfcPanel } from './wrfc-panel.ts';
9
- export { ProviderHealthPanel } from './provider-health-panel.ts';
10
- export { ProviderHealthTracker } from './provider-health-tracker.ts';
11
- export type { ProviderHealth, ProviderStatus } from './provider-health-tracker.ts';
12
7
  export { GitPanel } from './git-panel.ts';
13
- export { SymbolOutlinePanel } from './symbol-outline-panel.ts';
14
- export type { SymbolEntry, SymbolKind } from './symbol-outline-panel.ts';
15
- export { SessionBrowserPanel } from './session-browser-panel.ts';
16
- export { DocsPanel } from './docs-panel.ts';
17
- export { ThinkingPanel } from './thinking-panel.ts';
18
- export { ToolInspectorPanel } from './tool-inspector-panel.ts';
19
- export { DebugPanel } from './debug-panel.ts';
20
- export type { ApiCallEntry, ApiCallStatus } from './debug-panel.ts';
21
8
  export { registerBuiltinPanels } from './builtin-panels.ts';
22
9
  export type { BuiltinPanelDeps } from './builtin-panels.ts';
23
- export { IncidentReviewPanel } from './incident-review-panel.ts';
24
- export { PolicyPanel } from './policy-panel.ts';
10
+ // W6.1 (the purge) group B: plugins/skills panels are register-retired but
11
+ // their modules are retained for shared non-class exports (PluginManagerControls;
12
+ // discoverSkills + SkillRecord/SkillOrigin, consumed by command runtimes and the
13
+ // skills modal). The other group-A and group-B panel classes are deleted outright
14
+ // (their views migrated to config-modal surfaces in src/panels/modals/).
25
15
  export { PluginsPanel } from './plugins-panel.ts';
26
16
  export { SkillsPanel } from './skills-panel.ts';
27
- export { TasksPanel } from './tasks-panel.ts';
28
- export { OrchestrationPanel } from './orchestration-panel.ts';
29
- export { CommunicationPanel } from './communication-panel.ts';
30
- export { CockpitPanel } from './cockpit-panel.ts';
31
- export { RemotePanel } from './remote-panel.ts';
32
- export { ServicesPanel } from './services-panel.ts';
33
- export { SubscriptionPanel } from './subscription-panel.ts';
34
- export { HooksPanel } from './hooks-panel.ts';
35
- export { SecurityPanel } from './security-panel.ts';
36
- export { MarketplacePanel } from './marketplace-panel.ts';
37
- export { SandboxPanel } from './sandbox-panel.ts';
38
- export { ApprovalPanel } from './approval-panel.ts';
39
- export { KnowledgeGraphPanel } from './knowledge-graph-panel.ts';
40
- export { SystemMessagesPanel } from './system-messages-panel.ts';
41
- export { PanelListPanel } from './panel-list-panel.ts';
42
- export type { SystemMessageEntry, SystemMessagePriority } from './system-messages-panel.ts';
17
+ export { FleetPanel } from './fleet-panel.ts';
18
+ export type { FleetActionCallbacks } from './fleet-panel.ts';
@@ -0,0 +1,187 @@
1
+ import { MODAL_TONES } from './modal-theme.ts';
2
+ import { infoRow } from './modal-surface-helpers.ts';
3
+ import { listHookPointContracts } from '@pellux/goodvibes-sdk/platform/hooks';
4
+ import type {
5
+ HookActivityTracker,
6
+ HookDefinition,
7
+ HookDispatcher,
8
+ HookWorkbench,
9
+ } from '@pellux/goodvibes-sdk/platform/hooks';
10
+ import type {
11
+ ConfigModalActionContext,
12
+ ConfigModalRow,
13
+ ConfigModalSurface,
14
+ ConfigModalTab,
15
+ ConfigModalView,
16
+ } from '../../input/config-modal-types.ts';
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Hooks → config-modal surface (W6.1 group-B port). Two tabs: 'Hooks' (active
20
+ // registrations + contract posture) and 'Activity' (recent runtime activity +
21
+ // managed authoring + last simulation — the panel's expand-activity view, now a
22
+ // dedicated tab). All sources are in-memory (dispatcher/workbench/tracker hold
23
+ // live state), so buildView reads live and refresh() is a no-op. Toggle/remove/
24
+ // simulate route to their `/hooks` command path. Selection-blind port: the
25
+ // panel's selected-hook contract detail is folded into each hook row label.
26
+ // ---------------------------------------------------------------------------
27
+
28
+ export type HooksModalDispatcher = Pick<HookDispatcher, 'listHooks' | 'getChains'>;
29
+ export type HooksModalWorkbench = Pick<HookWorkbench, 'getHooksFilePath' | 'listManagedHooks' | 'listManagedChains' | 'listRecentActions' | 'getLastSimulation'>;
30
+ export type HooksModalActivityTracker = Pick<HookActivityTracker, 'listRecent'>;
31
+
32
+ export interface HooksModalDeps {
33
+ readonly hookDispatcher: HooksModalDispatcher;
34
+ readonly hookWorkbench: HooksModalWorkbench;
35
+ readonly hookActivityTracker: HooksModalActivityTracker;
36
+ }
37
+
38
+ type HookEntry = { pattern: string; hook: HookDefinition };
39
+
40
+ function rowId(entry: HookEntry, index: number): string {
41
+ return `${entry.pattern}#${entry.hook.name ?? index}`;
42
+ }
43
+
44
+ class HooksModalSurface implements ConfigModalSurface {
45
+ readonly name = 'hooks-modal';
46
+ readonly title = 'Hooks';
47
+
48
+ constructor(private readonly deps: HooksModalDeps) {}
49
+
50
+ readonly actions = [
51
+ { key: 't', id: 'toggle', label: 'toggle', enabledFor: (_row: ConfigModalRow | null, tabId: string) => tabId === 'hooks' },
52
+ { key: 'x', id: 'remove', label: 'remove', enabledFor: (_row: ConfigModalRow | null, tabId: string) => tabId === 'hooks' },
53
+ { key: 's', id: 'simulate', label: 'simulate', enabledFor: (_row: ConfigModalRow | null, tabId: string) => tabId === 'hooks' },
54
+ { key: 'r', id: 'refresh', label: 'refresh' },
55
+ ];
56
+
57
+ private entryFrom(id: string): HookEntry | undefined {
58
+ return this.deps.hookDispatcher.listHooks().find((entry, index) => rowId(entry, index) === id);
59
+ }
60
+
61
+ private hooksTab(): ConfigModalTab {
62
+ const hooks = this.deps.hookDispatcher.listHooks();
63
+ const chains = this.deps.hookDispatcher.getChains();
64
+ const contracts = listHookPointContracts();
65
+ const managedHooks = this.deps.hookWorkbench.listManagedHooks();
66
+ const hooksFilePath = this.deps.hookWorkbench.getHooksFilePath();
67
+
68
+ if (hooks.length === 0) {
69
+ const rows: ConfigModalRow[] = [
70
+ infoRow('empty:0', 'No hooks are currently registered.'),
71
+ infoRow('empty:1', `contracts ${contracts.length} chains ${chains.length} managed ${managedHooks.length}`, { dim: true }),
72
+ infoRow('empty:2', `hooks file: ${hooksFilePath}`, { dim: true }),
73
+ infoRow('empty:title', 'Next steps'),
74
+ infoRow('empty:hooks', '/hooks — review hook contracts and managed authoring actions', { dim: true }),
75
+ infoRow('empty:settings', '/settings — review hook/runtime behavior in the settings surface', { dim: true }),
76
+ ];
77
+ return { id: 'hooks', label: 'Hooks', rows, emptyText: '' };
78
+ }
79
+
80
+ const recentForStats = this.deps.hookActivityTracker.listRecent(3);
81
+ const denials = recentForStats.filter((r) => r.ok && r.decision === 'deny').length;
82
+ const errors = recentForStats.filter((r) => !r.ok).length;
83
+ const header = [`hooks ${hooks.length} chains ${chains.length} contracts ${contracts.length} denials ${denials} errors ${errors}`];
84
+
85
+ const rows: ConfigModalRow[] = hooks.map((entry, index) => {
86
+ const contract = contracts.find((c) => c.pattern === entry.pattern);
87
+ const contractPart = contract
88
+ ? ` · ${contract.authority}/${contract.executionMode} deny=${contract.canDeny ? 'y' : 'n'} mut=${contract.canMutateInput ? 'y' : 'n'} inj=${contract.canInjectContext ? 'y' : 'n'}`
89
+ : ' · no exact contract';
90
+ return {
91
+ id: rowId(entry, index),
92
+ label: `${(entry.hook.name ?? '(unnamed)').padEnd(20)} ${entry.pattern.padEnd(28)} ${(entry.hook.enabled === false ? 'DISABLED' : 'ENABLED').padEnd(8)} ${entry.hook.type}${contractPart}`,
93
+ ...(contract ? {} : { style: { fg: MODAL_TONES.warn } }),
94
+ };
95
+ });
96
+
97
+ return { id: 'hooks', label: 'Hooks', header, rows, hints: ['t toggle', 'x remove', 's simulate'] };
98
+ }
99
+
100
+ private activityTab(): ConfigModalTab {
101
+ const rows: ConfigModalRow[] = [];
102
+ const recentActivity = this.deps.hookActivityTracker.listRecent(20);
103
+ if (recentActivity.length === 0) {
104
+ rows.push(infoRow('act:none', 'No hook activity recorded yet.', { dim: true }));
105
+ } else {
106
+ recentActivity.forEach((record, i) => {
107
+ const decisionText = record.ok ? (record.decision ?? 'ok') : 'error';
108
+ const color = !record.ok ? MODAL_TONES.bad : record.decision === 'deny' ? MODAL_TONES.warn : MODAL_TONES.good;
109
+ rows.push(infoRow(`act:${i}`, `${record.hookName.padEnd(18)} ${record.path.padEnd(26)} ${decisionText}`, { fg: color }));
110
+ });
111
+ }
112
+
113
+ rows.push(infoRow('auth:title', 'Authoring'));
114
+ const recentAuthoring = this.deps.hookWorkbench.listRecentActions(3);
115
+ if (recentAuthoring.length === 0) {
116
+ rows.push(infoRow('auth:none', 'No managed hook authoring actions recorded yet.', { dim: true }));
117
+ } else {
118
+ recentAuthoring.forEach((action, i) => rows.push(infoRow(`auth:${i}`, `${action.kind.padEnd(14)} ${action.target}`, { dim: true })));
119
+ }
120
+
121
+ const lastSimulation = this.deps.hookWorkbench.getLastSimulation();
122
+ if (lastSimulation) {
123
+ rows.push(infoRow('sim:path', `last simulation: ${lastSimulation.eventPath}`));
124
+ rows.push(infoRow('sim:matches', `matches: hooks=${lastSimulation.matchedHooks.length} chains=${lastSimulation.matchedChains.length}`, { dim: true }));
125
+ }
126
+ return { id: 'activity', label: 'Activity', rows, emptyText: 'No hook activity recorded yet.' };
127
+ }
128
+
129
+ buildView(): ConfigModalView {
130
+ return { title: 'Hooks', tabs: [this.hooksTab(), this.activityTab()] };
131
+ }
132
+
133
+ onAction(id: string, ctx: ConfigModalActionContext): void {
134
+ if (id === 'refresh') { ctx.setStatus('Hooks are read live.'); ctx.requestRender(); return; }
135
+ const entry = ctx.row ? this.entryFrom(ctx.row.id) : undefined;
136
+ if (!entry) return;
137
+ const name = entry.hook.name;
138
+ switch (id) {
139
+ case 'toggle':
140
+ if (!name) { ctx.print('This hook has no managed name to toggle.'); return; }
141
+ void ctx.executeCommand?.('hooks', [entry.hook.enabled === false ? 'enable' : 'disable', name]);
142
+ ctx.setStatus(`Dispatched /hooks ${entry.hook.enabled === false ? 'enable' : 'disable'} ${name}.`);
143
+ break;
144
+ case 'remove':
145
+ if (!name) { ctx.print('This hook has no managed name to remove.'); return; }
146
+ void ctx.executeCommand?.('hooks', ['remove', name]);
147
+ ctx.setStatus(`Dispatched /hooks remove ${name}.`);
148
+ break;
149
+ case 'simulate':
150
+ void ctx.executeCommand?.('hooks', ['simulate', entry.pattern]);
151
+ ctx.setStatus(`Dispatched /hooks simulate ${entry.pattern}.`);
152
+ break;
153
+ }
154
+ }
155
+ }
156
+
157
+ export function createHooksModalSurface(deps: HooksModalDeps): ConfigModalSurface {
158
+ return new HooksModalSurface(deps);
159
+ }
160
+
161
+ /**
162
+ * Deterministic golden fixture: fixed dispatcher/workbench/activity-tracker
163
+ * stand-ins with two managed hooks (one enabled, one disabled) and one recorded
164
+ * activity entry. listHookPointContracts() reads the SDK's static contract table
165
+ * (no disk/wall-clock). All timestamps are fixed epoch numbers.
166
+ */
167
+ export function hooksModalGoldenSurface(): ConfigModalSurface {
168
+ const hooks: HookEntry[] = [
169
+ { pattern: 'Pre:tool:*', hook: { match: 'Pre:tool:*', type: 'command', command: 'echo pre', name: 'pre-tool-guard', enabled: true } },
170
+ { pattern: 'Post:file:write', hook: { match: 'Post:file:write', type: 'command', command: 'echo post', name: 'post-write-log', enabled: false } },
171
+ ];
172
+ const dispatcher: HooksModalDispatcher = { listHooks: () => hooks, getChains: () => [] };
173
+ const workbench: HooksModalWorkbench = {
174
+ getHooksFilePath: () => '/golden/hooks.json',
175
+ listManagedHooks: () => hooks,
176
+ listManagedChains: () => [],
177
+ listRecentActions: () => [{ kind: 'toggle', target: 'pre-tool-guard', timestamp: 0 }],
178
+ getLastSimulation: () => null,
179
+ };
180
+ const activityTracker: HooksModalActivityTracker = {
181
+ listRecent: () => [{
182
+ timestamp: 0, path: 'Pre:tool:*', specific: '*', pattern: 'Pre:tool:*',
183
+ hookName: 'pre-tool-guard', hookType: 'command', ok: true, decision: 'allow', durationMs: 1, async: false,
184
+ }],
185
+ };
186
+ return createHooksModalSurface({ hookDispatcher: dispatcher, hookWorkbench: workbench, hookActivityTracker: activityTracker });
187
+ }