@pellux/goodvibes-tui 1.0.0 → 1.7.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 (209) hide show
  1. package/CHANGELOG.md +130 -127
  2. package/README.md +32 -14
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/cli/ensure-goodvibes-gitignore.ts +32 -0
  6. package/src/cli/entrypoint.ts +2 -0
  7. package/src/core/alert-gating.ts +67 -0
  8. package/src/core/approval-alert.ts +72 -0
  9. package/src/core/budget-breach-notifier.ts +106 -0
  10. package/src/core/conversation-rendering.ts +19 -0
  11. package/src/core/conversation.ts +58 -0
  12. package/src/core/focus-tracker.ts +41 -0
  13. package/src/core/long-task-notifier.ts +33 -6
  14. package/src/core/stream-event-wiring.ts +104 -2
  15. package/src/core/stream-stall-watchdog.ts +41 -17
  16. package/src/core/system-message-router.ts +37 -51
  17. package/src/core/turn-cancellation.ts +20 -0
  18. package/src/core/turn-event-wiring.ts +64 -3
  19. package/src/export/cost-utils.ts +126 -8
  20. package/src/input/command-registry.ts +54 -1
  21. package/src/input/commands/checkpoint-runtime.ts +280 -0
  22. package/src/input/commands/codebase-runtime.ts +192 -0
  23. package/src/input/commands/diff-runtime.ts +61 -30
  24. package/src/input/commands/eval.ts +1 -1
  25. package/src/input/commands/health-runtime.ts +1 -1
  26. package/src/input/commands/image-runtime.ts +112 -0
  27. package/src/input/commands/intelligence-runtime.ts +11 -1
  28. package/src/input/commands/local-auth-runtime.ts +4 -1
  29. package/src/input/commands/local-runtime.ts +9 -3
  30. package/src/input/commands/marketplace-runtime.ts +2 -2
  31. package/src/input/commands/memory.ts +6 -1
  32. package/src/input/commands/operator-panel-runtime.ts +40 -24
  33. package/src/input/commands/planning-runtime.ts +26 -3
  34. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  35. package/src/input/commands/plugin-runtime.ts +2 -2
  36. package/src/input/commands/policy-dispatch.ts +21 -0
  37. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  38. package/src/input/commands/qrcode-runtime.ts +3 -3
  39. package/src/input/commands/recall-review.ts +35 -0
  40. package/src/input/commands/remote-runtime.ts +3 -3
  41. package/src/input/commands/runtime-services.ts +54 -13
  42. package/src/input/commands/services-runtime.ts +1 -1
  43. package/src/input/commands/session-content.ts +12 -0
  44. package/src/input/commands/session-workflow.ts +19 -1
  45. package/src/input/commands/settings-sync-runtime.ts +1 -1
  46. package/src/input/commands/share-runtime.ts +9 -2
  47. package/src/input/commands/shell-core.ts +15 -7
  48. package/src/input/commands/skills-runtime.ts +2 -8
  49. package/src/input/commands/subscription-runtime.ts +2 -2
  50. package/src/input/commands/test-runtime.ts +277 -0
  51. package/src/input/commands/websearch-runtime.ts +101 -0
  52. package/src/input/commands/work-plan-runtime.ts +36 -10
  53. package/src/input/commands/workstream-runtime.ts +338 -0
  54. package/src/input/commands.ts +12 -0
  55. package/src/input/config-modal-types.ts +161 -0
  56. package/src/input/config-modal.ts +377 -0
  57. package/src/input/feed-context-factory.ts +7 -8
  58. package/src/input/handler-command-route.ts +27 -17
  59. package/src/input/handler-content-actions.ts +22 -8
  60. package/src/input/handler-feed-routes.ts +107 -7
  61. package/src/input/handler-feed.ts +49 -13
  62. package/src/input/handler-interactions.ts +3 -3
  63. package/src/input/handler-modal-routes.ts +65 -0
  64. package/src/input/handler-modal-stack.ts +3 -5
  65. package/src/input/handler-modal-token-routes.ts +16 -49
  66. package/src/input/handler-picker-routes.ts +11 -94
  67. package/src/input/handler-shortcuts.ts +24 -14
  68. package/src/input/handler-types.ts +3 -6
  69. package/src/input/handler-ui-state.ts +10 -22
  70. package/src/input/handler.ts +10 -28
  71. package/src/input/keybindings.ts +22 -8
  72. package/src/input/model-picker-types.ts +24 -6
  73. package/src/input/model-picker.ts +58 -0
  74. package/src/input/panel-integration-actions.ts +9 -170
  75. package/src/input/settings-modal-data.ts +95 -0
  76. package/src/input/settings-modal-mutations.ts +6 -4
  77. package/src/main.ts +41 -44
  78. package/src/panels/agent-inspector-shared.ts +35 -11
  79. package/src/panels/base-panel.ts +22 -1
  80. package/src/panels/builtin/agent.ts +21 -107
  81. package/src/panels/builtin/development.ts +16 -62
  82. package/src/panels/builtin/knowledge.ts +8 -32
  83. package/src/panels/builtin/operations.ts +84 -428
  84. package/src/panels/builtin/session.ts +21 -112
  85. package/src/panels/builtin/shared.ts +9 -43
  86. package/src/panels/builtin-modals.ts +218 -0
  87. package/src/panels/builtin-panels.ts +5 -0
  88. package/src/panels/cost-tracker-panel.ts +63 -14
  89. package/src/panels/diff-panel.ts +123 -60
  90. package/src/panels/eval-registry.ts +60 -0
  91. package/src/panels/fleet-panel.ts +800 -0
  92. package/src/panels/fleet-read-model.ts +477 -0
  93. package/src/panels/fleet-steer.ts +92 -0
  94. package/src/panels/fleet-stop.ts +125 -0
  95. package/src/panels/fleet-tabs.ts +230 -0
  96. package/src/panels/fleet-transcript.ts +356 -0
  97. package/src/panels/git-panel.ts +9 -9
  98. package/src/panels/index.ts +7 -31
  99. package/src/panels/local-auth-panel.ts +10 -0
  100. package/src/panels/modals/hooks-modal.ts +187 -0
  101. package/src/panels/modals/keybindings-modal.ts +151 -0
  102. package/src/panels/modals/knowledge-modal.ts +158 -0
  103. package/src/panels/modals/local-auth-modal.ts +132 -0
  104. package/src/panels/modals/marketplace-modal.ts +218 -0
  105. package/src/panels/modals/memory-modal.ts +180 -0
  106. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  107. package/src/panels/modals/modal-theme.ts +36 -0
  108. package/src/panels/modals/pairing-modal.ts +144 -0
  109. package/src/panels/modals/planning-modal.ts +282 -0
  110. package/src/panels/modals/plugins-modal.ts +174 -0
  111. package/src/panels/modals/policy-modal.ts +256 -0
  112. package/src/panels/modals/provider-health-modal.ts +136 -0
  113. package/src/panels/modals/remote-modal.ts +115 -0
  114. package/src/panels/modals/sandbox-modal.ts +150 -0
  115. package/src/panels/modals/security-modal.ts +217 -0
  116. package/src/panels/modals/services-modal.ts +179 -0
  117. package/src/panels/modals/settings-sync-modal.ts +142 -0
  118. package/src/panels/modals/skills-modal.ts +189 -0
  119. package/src/panels/modals/subscription-modal.ts +172 -0
  120. package/src/panels/modals/work-plan-modal.ts +149 -0
  121. package/src/panels/panel-confirm-overlay.ts +72 -0
  122. package/src/panels/panel-manager.ts +108 -5
  123. package/src/panels/project-planning-answer-actions.ts +4 -2
  124. package/src/panels/scrollable-list-panel.ts +9 -0
  125. package/src/panels/skills-panel.ts +7 -51
  126. package/src/panels/token-budget-panel.ts +5 -3
  127. package/src/panels/types.ts +26 -0
  128. package/src/permissions/hunk-selection.ts +179 -0
  129. package/src/permissions/prompt.ts +60 -4
  130. package/src/renderer/compaction-history-modal.ts +19 -3
  131. package/src/renderer/compaction-preview.ts +13 -2
  132. package/src/renderer/compaction-quality.ts +100 -0
  133. package/src/renderer/config-modal.ts +81 -0
  134. package/src/renderer/conversation-overlays.ts +10 -17
  135. package/src/renderer/fleet-tab-strip.ts +56 -0
  136. package/src/renderer/footer-tips.ts +10 -2
  137. package/src/renderer/git-status.ts +40 -0
  138. package/src/renderer/help-overlay.ts +2 -2
  139. package/src/renderer/model-workspace.ts +44 -1
  140. package/src/renderer/panel-workspace-bar.ts +8 -2
  141. package/src/renderer/shell-surface.ts +8 -1
  142. package/src/renderer/turn-injection.ts +114 -0
  143. package/src/renderer/ui-factory.ts +91 -7
  144. package/src/runtime/bootstrap-command-context.ts +24 -1
  145. package/src/runtime/bootstrap-command-parts.ts +36 -7
  146. package/src/runtime/bootstrap-core.ts +19 -4
  147. package/src/runtime/bootstrap-hook-bridge.ts +5 -0
  148. package/src/runtime/bootstrap-shell.ts +41 -17
  149. package/src/runtime/bootstrap.ts +11 -17
  150. package/src/runtime/code-index-services.ts +112 -0
  151. package/src/runtime/orchestrator-core-services.ts +49 -0
  152. package/src/runtime/process-lifecycle.ts +3 -1
  153. package/src/runtime/services.ts +91 -43
  154. package/src/runtime/ui-services.ts +8 -0
  155. package/src/runtime/workstream-services.ts +195 -0
  156. package/src/shell/blocking-input.ts +22 -1
  157. package/src/shell/ui-openers.ts +129 -17
  158. package/src/utils/format-duration.ts +8 -18
  159. package/src/utils/splash-lines.ts +1 -1
  160. package/src/version.ts +1 -1
  161. package/src/panels/agent-inspector-panel.ts +0 -786
  162. package/src/panels/approval-panel.ts +0 -252
  163. package/src/panels/automation-control-panel.ts +0 -479
  164. package/src/panels/cockpit-panel.ts +0 -481
  165. package/src/panels/cockpit-read-model.ts +0 -233
  166. package/src/panels/communication-panel.ts +0 -256
  167. package/src/panels/control-plane-panel.ts +0 -470
  168. package/src/panels/debug-panel.ts +0 -609
  169. package/src/panels/docs-panel.ts +0 -375
  170. package/src/panels/eval-panel.ts +0 -627
  171. package/src/panels/file-explorer-panel.ts +0 -664
  172. package/src/panels/file-preview-panel.ts +0 -517
  173. package/src/panels/hooks-panel.ts +0 -401
  174. package/src/panels/incident-review-panel.ts +0 -406
  175. package/src/panels/intelligence-panel.ts +0 -383
  176. package/src/panels/knowledge-graph-panel.ts +0 -506
  177. package/src/panels/marketplace-panel.ts +0 -399
  178. package/src/panels/memory-panel.ts +0 -558
  179. package/src/panels/ops-control-panel.ts +0 -329
  180. package/src/panels/ops-strategy-panel.ts +0 -321
  181. package/src/panels/orchestration-panel.ts +0 -465
  182. package/src/panels/panel-list-panel.ts +0 -557
  183. package/src/panels/plan-dashboard-panel.ts +0 -707
  184. package/src/panels/policy-panel.ts +0 -517
  185. package/src/panels/project-planning-panel.ts +0 -721
  186. package/src/panels/provider-health-panel.ts +0 -678
  187. package/src/panels/provider-health-tracker.ts +0 -306
  188. package/src/panels/provider-health-views.ts +0 -560
  189. package/src/panels/qr-panel.ts +0 -280
  190. package/src/panels/remote-panel.ts +0 -534
  191. package/src/panels/routes-panel.ts +0 -241
  192. package/src/panels/sandbox-panel.ts +0 -456
  193. package/src/panels/security-panel.ts +0 -447
  194. package/src/panels/services-panel.ts +0 -329
  195. package/src/panels/session-browser-panel.ts +0 -487
  196. package/src/panels/settings-sync-panel.ts +0 -398
  197. package/src/panels/subscription-panel.ts +0 -342
  198. package/src/panels/symbol-outline-panel.ts +0 -619
  199. package/src/panels/system-messages-panel.ts +0 -364
  200. package/src/panels/tasks-panel.ts +0 -608
  201. package/src/panels/thinking-panel.ts +0 -333
  202. package/src/panels/tool-inspector-panel.ts +0 -537
  203. package/src/panels/work-plan-panel.ts +0 -530
  204. package/src/panels/worktree-panel.ts +0 -360
  205. package/src/panels/wrfc-panel.ts +0 -790
  206. package/src/renderer/agent-detail-modal.ts +0 -465
  207. package/src/renderer/live-tail-modal.ts +0 -156
  208. package/src/renderer/process-modal.ts +0 -656
  209. 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
+ }
@@ -178,11 +178,9 @@ export class GitPanel extends BasePanel {
178
178
  const changed = this.getChangedFiles ? new Set(this.getChangedFiles()) : null;
179
179
  const markChanged = (path: string): boolean | undefined => (changed ? changed.has(path) : undefined);
180
180
 
181
- // Classify from the raw per-file porcelain columns (index/working_dir)
182
- // rather than simple-git's `modified`/`staged` convenience arrays: those
183
- // arrays both include a file that is staged with no further edits (index
184
- // 'M', working_dir ' '), which would otherwise show it as both staged
185
- // AND unstaged and make 's'/'u' land on the wrong (phantom) row.
181
+ // Classify from the raw per-file porcelain columns (index/working_dir), not simple-git's
182
+ // modified/staged arrays: those double-count a staged-with-no-further-edits file (index 'M',
183
+ // working_dir ' ') as both staged AND unstaged, landing 's'/'u' on a phantom row.
186
184
  const stagedFiles: GitFileEntry[] = [];
187
185
  const unstagedFiles: GitFileEntry[] = [];
188
186
  for (const f of statusResult.files) {
@@ -278,6 +276,11 @@ export class GitPanel extends BasePanel {
278
276
 
279
277
  // Input handling
280
278
 
279
+ /** Commit-message entry wants every char of a burst delivered one at a time — see Panel.isCapturingTextBurst. */
280
+ isCapturingTextBurst(): boolean {
281
+ return this.commitMessage !== null;
282
+ }
283
+
281
284
  handleInput(key: string): boolean {
282
285
  // Confirm (init repo / commit) takes priority over everything else.
283
286
  const confirmResult = handleConfirmInput(this.confirm, key);
@@ -728,10 +731,7 @@ export class GitPanel extends BasePanel {
728
731
  });
729
732
  }
730
733
 
731
- /**
732
- * Map an item index in `this.items` to the row index in the rendered row list.
733
- * Header items expand to 2 rows (branch + spacer).
734
- */
734
+ /** Map an item index in `this.items` to its rendered row index (header items expand to 2 rows: branch + spacer). */
735
735
  private getRowIndexForItem(itemIndex: number): number {
736
736
  let row = 0;
737
737
  for (let i = 0; i < itemIndex && i < this.items.length; i++) {
@@ -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';
@@ -96,6 +96,16 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
96
96
  return this.maskedState !== null;
97
97
  }
98
98
 
99
+ /**
100
+ * Masked password/username entry wants every character of a burst (paste,
101
+ * or fast typing landing in one input.feed() call) delivered one at a
102
+ * time, same as it always has — see the interface doc on
103
+ * `Panel.isCapturingTextBurst`.
104
+ */
105
+ public override isCapturingTextBurst(): boolean {
106
+ return this.isMaskedEntryActive || this.usernameEntry !== null || super.isCapturingTextBurst();
107
+ }
108
+
99
109
  public override handleInput(key: KeyName): boolean {
100
110
  // Masked entry takes priority when active — it must capture every
101
111
  // keystroke (including letters that would otherwise be p/a/d/b actions)
@@ -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
+ }