@pellux/goodvibes-tui 1.1.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 (194) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +20 -11
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/core/alert-gating.ts +67 -0
  6. package/src/core/approval-alert.ts +72 -0
  7. package/src/core/budget-breach-notifier.ts +106 -0
  8. package/src/core/conversation-rendering.ts +19 -0
  9. package/src/core/conversation.ts +18 -0
  10. package/src/core/focus-tracker.ts +41 -0
  11. package/src/core/long-task-notifier.ts +33 -6
  12. package/src/core/system-message-router.ts +37 -51
  13. package/src/core/turn-cancellation.ts +20 -0
  14. package/src/core/turn-event-wiring.ts +64 -3
  15. package/src/export/cost-utils.ts +36 -0
  16. package/src/input/command-registry.ts +38 -1
  17. package/src/input/commands/checkpoint-runtime.ts +280 -0
  18. package/src/input/commands/codebase-runtime.ts +192 -0
  19. package/src/input/commands/eval.ts +1 -1
  20. package/src/input/commands/health-runtime.ts +1 -1
  21. package/src/input/commands/image-runtime.ts +112 -0
  22. package/src/input/commands/intelligence-runtime.ts +11 -1
  23. package/src/input/commands/local-auth-runtime.ts +4 -1
  24. package/src/input/commands/local-runtime.ts +9 -3
  25. package/src/input/commands/marketplace-runtime.ts +2 -2
  26. package/src/input/commands/memory.ts +6 -1
  27. package/src/input/commands/operator-panel-runtime.ts +40 -24
  28. package/src/input/commands/planning-runtime.ts +26 -3
  29. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  30. package/src/input/commands/plugin-runtime.ts +2 -2
  31. package/src/input/commands/policy-dispatch.ts +21 -0
  32. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  33. package/src/input/commands/qrcode-runtime.ts +3 -3
  34. package/src/input/commands/recall-review.ts +35 -0
  35. package/src/input/commands/remote-runtime.ts +3 -3
  36. package/src/input/commands/runtime-services.ts +54 -13
  37. package/src/input/commands/services-runtime.ts +1 -1
  38. package/src/input/commands/session-workflow.ts +16 -1
  39. package/src/input/commands/settings-sync-runtime.ts +1 -1
  40. package/src/input/commands/shell-core.ts +15 -7
  41. package/src/input/commands/skills-runtime.ts +2 -8
  42. package/src/input/commands/subscription-runtime.ts +2 -2
  43. package/src/input/commands/test-runtime.ts +277 -0
  44. package/src/input/commands/websearch-runtime.ts +101 -0
  45. package/src/input/commands/work-plan-runtime.ts +36 -10
  46. package/src/input/commands/workstream-runtime.ts +338 -0
  47. package/src/input/commands.ts +12 -0
  48. package/src/input/config-modal-types.ts +161 -0
  49. package/src/input/config-modal.ts +377 -0
  50. package/src/input/feed-context-factory.ts +7 -8
  51. package/src/input/handler-command-route.ts +27 -17
  52. package/src/input/handler-feed-routes.ts +61 -23
  53. package/src/input/handler-feed.ts +47 -23
  54. package/src/input/handler-interactions.ts +1 -3
  55. package/src/input/handler-modal-routes.ts +65 -0
  56. package/src/input/handler-modal-stack.ts +3 -5
  57. package/src/input/handler-modal-token-routes.ts +16 -49
  58. package/src/input/handler-picker-routes.ts +11 -94
  59. package/src/input/handler-shortcuts.ts +24 -14
  60. package/src/input/handler-types.ts +2 -6
  61. package/src/input/handler-ui-state.ts +10 -22
  62. package/src/input/handler.ts +6 -28
  63. package/src/input/keybindings.ts +22 -8
  64. package/src/input/model-picker-types.ts +24 -6
  65. package/src/input/model-picker.ts +58 -0
  66. package/src/input/panel-integration-actions.ts +9 -170
  67. package/src/input/settings-modal-data.ts +95 -0
  68. package/src/input/settings-modal-mutations.ts +6 -4
  69. package/src/main.ts +24 -27
  70. package/src/panels/agent-inspector-shared.ts +2 -1
  71. package/src/panels/base-panel.ts +22 -1
  72. package/src/panels/builtin/agent.ts +21 -107
  73. package/src/panels/builtin/development.ts +15 -61
  74. package/src/panels/builtin/knowledge.ts +8 -32
  75. package/src/panels/builtin/operations.ts +84 -428
  76. package/src/panels/builtin/session.ts +21 -112
  77. package/src/panels/builtin/shared.ts +9 -43
  78. package/src/panels/builtin-modals.ts +218 -0
  79. package/src/panels/builtin-panels.ts +5 -0
  80. package/src/panels/cost-tracker-panel.ts +36 -10
  81. package/src/panels/diff-panel.ts +12 -43
  82. package/src/panels/eval-registry.ts +60 -0
  83. package/src/panels/fleet-panel.ts +800 -0
  84. package/src/panels/fleet-read-model.ts +477 -0
  85. package/src/panels/fleet-steer.ts +92 -0
  86. package/src/panels/fleet-stop.ts +125 -0
  87. package/src/panels/fleet-tabs.ts +230 -0
  88. package/src/panels/fleet-transcript.ts +356 -0
  89. package/src/panels/index.ts +7 -31
  90. package/src/panels/modals/hooks-modal.ts +187 -0
  91. package/src/panels/modals/keybindings-modal.ts +151 -0
  92. package/src/panels/modals/knowledge-modal.ts +158 -0
  93. package/src/panels/modals/local-auth-modal.ts +132 -0
  94. package/src/panels/modals/marketplace-modal.ts +218 -0
  95. package/src/panels/modals/memory-modal.ts +180 -0
  96. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  97. package/src/panels/modals/modal-theme.ts +36 -0
  98. package/src/panels/modals/pairing-modal.ts +144 -0
  99. package/src/panels/modals/planning-modal.ts +282 -0
  100. package/src/panels/modals/plugins-modal.ts +174 -0
  101. package/src/panels/modals/policy-modal.ts +256 -0
  102. package/src/panels/modals/provider-health-modal.ts +136 -0
  103. package/src/panels/modals/remote-modal.ts +115 -0
  104. package/src/panels/modals/sandbox-modal.ts +150 -0
  105. package/src/panels/modals/security-modal.ts +217 -0
  106. package/src/panels/modals/services-modal.ts +179 -0
  107. package/src/panels/modals/settings-sync-modal.ts +142 -0
  108. package/src/panels/modals/skills-modal.ts +189 -0
  109. package/src/panels/modals/subscription-modal.ts +172 -0
  110. package/src/panels/modals/work-plan-modal.ts +149 -0
  111. package/src/panels/panel-confirm-overlay.ts +72 -0
  112. package/src/panels/panel-manager.ts +108 -5
  113. package/src/panels/project-planning-answer-actions.ts +4 -2
  114. package/src/panels/skills-panel.ts +7 -51
  115. package/src/panels/types.ts +13 -0
  116. package/src/permissions/hunk-selection.ts +179 -0
  117. package/src/permissions/prompt.ts +60 -4
  118. package/src/renderer/compaction-history-modal.ts +19 -3
  119. package/src/renderer/compaction-preview.ts +13 -2
  120. package/src/renderer/compaction-quality.ts +100 -0
  121. package/src/renderer/config-modal.ts +81 -0
  122. package/src/renderer/conversation-overlays.ts +10 -17
  123. package/src/renderer/fleet-tab-strip.ts +56 -0
  124. package/src/renderer/footer-tips.ts +10 -2
  125. package/src/renderer/git-status.ts +40 -0
  126. package/src/renderer/help-overlay.ts +2 -2
  127. package/src/renderer/model-workspace.ts +44 -1
  128. package/src/renderer/panel-workspace-bar.ts +8 -2
  129. package/src/renderer/turn-injection.ts +114 -0
  130. package/src/runtime/bootstrap-command-context.ts +21 -1
  131. package/src/runtime/bootstrap-command-parts.ts +34 -7
  132. package/src/runtime/bootstrap-core.ts +14 -4
  133. package/src/runtime/bootstrap-shell.ts +29 -16
  134. package/src/runtime/bootstrap.ts +11 -17
  135. package/src/runtime/code-index-services.ts +112 -0
  136. package/src/runtime/orchestrator-core-services.ts +49 -0
  137. package/src/runtime/process-lifecycle.ts +3 -1
  138. package/src/runtime/services.ts +91 -43
  139. package/src/runtime/ui-services.ts +8 -0
  140. package/src/runtime/workstream-services.ts +195 -0
  141. package/src/shell/blocking-input.ts +22 -1
  142. package/src/shell/ui-openers.ts +129 -17
  143. package/src/utils/format-duration.ts +8 -18
  144. package/src/utils/splash-lines.ts +1 -1
  145. package/src/version.ts +1 -1
  146. package/src/panels/agent-inspector-panel.ts +0 -786
  147. package/src/panels/approval-panel.ts +0 -252
  148. package/src/panels/automation-control-panel.ts +0 -479
  149. package/src/panels/cockpit-panel.ts +0 -481
  150. package/src/panels/cockpit-read-model.ts +0 -238
  151. package/src/panels/communication-panel.ts +0 -256
  152. package/src/panels/control-plane-panel.ts +0 -470
  153. package/src/panels/debug-panel.ts +0 -615
  154. package/src/panels/docs-panel.ts +0 -384
  155. package/src/panels/eval-panel.ts +0 -627
  156. package/src/panels/file-explorer-panel.ts +0 -673
  157. package/src/panels/file-preview-panel.ts +0 -517
  158. package/src/panels/hooks-panel.ts +0 -401
  159. package/src/panels/incident-review-panel.ts +0 -406
  160. package/src/panels/intelligence-panel.ts +0 -383
  161. package/src/panels/knowledge-graph-panel.ts +0 -515
  162. package/src/panels/marketplace-panel.ts +0 -399
  163. package/src/panels/memory-panel.ts +0 -558
  164. package/src/panels/ops-control-panel.ts +0 -329
  165. package/src/panels/ops-strategy-panel.ts +0 -321
  166. package/src/panels/orchestration-panel.ts +0 -465
  167. package/src/panels/panel-list-panel.ts +0 -566
  168. package/src/panels/plan-dashboard-panel.ts +0 -707
  169. package/src/panels/policy-panel.ts +0 -517
  170. package/src/panels/project-planning-panel.ts +0 -731
  171. package/src/panels/provider-health-panel.ts +0 -678
  172. package/src/panels/provider-health-tracker.ts +0 -310
  173. package/src/panels/provider-health-views.ts +0 -567
  174. package/src/panels/qr-panel.ts +0 -280
  175. package/src/panels/remote-panel.ts +0 -534
  176. package/src/panels/routes-panel.ts +0 -241
  177. package/src/panels/sandbox-panel.ts +0 -456
  178. package/src/panels/security-panel.ts +0 -447
  179. package/src/panels/services-panel.ts +0 -329
  180. package/src/panels/session-browser-panel.ts +0 -496
  181. package/src/panels/settings-sync-panel.ts +0 -398
  182. package/src/panels/subscription-panel.ts +0 -342
  183. package/src/panels/symbol-outline-panel.ts +0 -619
  184. package/src/panels/system-messages-panel.ts +0 -364
  185. package/src/panels/tasks-panel.ts +0 -608
  186. package/src/panels/thinking-panel.ts +0 -333
  187. package/src/panels/tool-inspector-panel.ts +0 -537
  188. package/src/panels/work-plan-panel.ts +0 -540
  189. package/src/panels/worktree-panel.ts +0 -360
  190. package/src/panels/wrfc-panel.ts +0 -790
  191. package/src/renderer/agent-detail-modal.ts +0 -466
  192. package/src/renderer/live-tail-modal.ts +0 -156
  193. package/src/renderer/process-modal.ts +0 -671
  194. package/src/renderer/qr-renderer.ts +0 -120
@@ -1,558 +0,0 @@
1
- /**
2
- * MemoryPanel — merged project memory substrate panel.
3
- *
4
- * TASK-040: Merged from the former separate MemoryPanel + KnowledgePanel.
5
- * One panel, two filter modes toggled with Tab:
6
- * - 'all' : full record list with search (former MemoryPanel behaviour)
7
- * - 'review' : review-queue view with r/s/c/f actions (former KnowledgePanel behaviour)
8
- *
9
- * Panel id stays 'memory'; the 'knowledge' id is repointed to the graph view.
10
- * No capability removed from either previous surface.
11
- */
12
-
13
- import type { Line } from '../types/grid.ts';
14
- import type { MemoryRegistry } from '@pellux/goodvibes-sdk/platform/state';
15
- import type { MemoryClass, MemoryRecord, MemoryReviewState } from '@pellux/goodvibes-sdk/platform/state';
16
- import { ScrollableListPanel } from './scrollable-list-panel.ts';
17
- import { truncateDisplay } from '../utils/terminal-width.ts';
18
- import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
19
- import {
20
- buildBodyText,
21
- buildKeyValueLine,
22
- buildPanelLine,
23
- buildPanelWorkspace,
24
- extendPalette,
25
- DEFAULT_PANEL_PALETTE,
26
- } from './polish.ts';
27
- import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
28
-
29
- // ---------------------------------------------------------------------------
30
- // Colour palette
31
- // ---------------------------------------------------------------------------
32
-
33
- // Domain accents only — one hue per memory class (base chrome: header/
34
- // headerBg/selectBg come from DEFAULT_PANEL_PALETTE).
35
- const C = extendPalette(DEFAULT_PANEL_PALETTE, {
36
- decision: '#38bdf8',
37
- constraint: '#f97316',
38
- incident: '#ef4444',
39
- pattern: '#a78bfa',
40
- fact: '#22c55e',
41
- risk: '#f43f5e',
42
- runbook: '#eab308',
43
- architecture: '#60a5fa',
44
- ownership: '#14b8a6',
45
- });
46
-
47
- // ---------------------------------------------------------------------------
48
- // Filter modes
49
- // ---------------------------------------------------------------------------
50
-
51
- type FilterMode = 'all' | 'review';
52
-
53
- const FILTER_LABELS: Record<FilterMode, string> = {
54
- all: 'All Records',
55
- review: 'Review Queue',
56
- };
57
-
58
- // ---------------------------------------------------------------------------
59
- // Helpers
60
- // ---------------------------------------------------------------------------
61
-
62
- function fmtTime(ts: number): string {
63
- const d = new Date(ts);
64
- return d.toISOString().slice(0, 16).replace('T', ' ');
65
- }
66
-
67
- function classColor(cls: MemoryClass): string {
68
- switch (cls) {
69
- case 'decision': return C.decision;
70
- case 'constraint': return C.constraint;
71
- case 'incident': return C.incident;
72
- case 'pattern': return C.pattern;
73
- case 'fact': return C.fact;
74
- case 'risk': return C.risk;
75
- case 'runbook': return C.runbook;
76
- case 'architecture': return C.architecture;
77
- case 'ownership': return C.ownership;
78
- }
79
- }
80
-
81
- function reviewStateColor(state: MemoryReviewState): string {
82
- switch (state) {
83
- case 'reviewed': return C.good;
84
- case 'stale': return C.warn;
85
- case 'contradicted': return C.bad;
86
- case 'fresh':
87
- default: return C.info;
88
- }
89
- }
90
-
91
- function formatConfidence(confidence: number): string {
92
- return `${confidence.toString().padStart(3, ' ')}%`;
93
- }
94
-
95
- // ---------------------------------------------------------------------------
96
- // MemoryPanel
97
- // ---------------------------------------------------------------------------
98
-
99
- export class MemoryPanel extends ScrollableListPanel<MemoryRecord> {
100
- private readonly registry: MemoryRegistry;
101
- private filterMode: FilterMode = 'all';
102
- private unsubscribe?: () => void;
103
-
104
- // Confirm state (for destructive stale/contradict/delete actions)
105
- private confirm: ConfirmState<{ id: string; action: 'stale' | 'contradicted' | 'delete' }> | null = null;
106
-
107
- // Cached records for review-mode (reviewQueue-first, same as former KnowledgePanel)
108
- private reviewRecords: MemoryRecord[] = [];
109
-
110
- constructor(registry: MemoryRegistry) {
111
- super('memory', 'Memory', 'M', 'agent');
112
- this.registry = registry;
113
- // WO-153: converged modal '/' filter — only active in 'all' mode; review
114
- // mode uses r/s/c/f/d single-letter actions instead of text filtering.
115
- this.filterEnabled = true;
116
- this.filterLabel = 'Search';
117
- }
118
-
119
- // ---------------------------------------------------------------------------
120
- // Lifecycle
121
- // ---------------------------------------------------------------------------
122
-
123
- override onActivate(): void {
124
- super.onActivate();
125
- this.filterQuery = '';
126
- this.filterActive = false;
127
- this.confirm = null;
128
- this.refreshReviewRecords();
129
- this.unsubscribe = this.registry.subscribe(() => {
130
- this.refreshReviewRecords();
131
- this.markDirty();
132
- });
133
- }
134
-
135
- override onDeactivate(): void {
136
- super.onDeactivate();
137
- }
138
-
139
- override onDestroy(): void {
140
- this.unsubscribe?.();
141
- this.unsubscribe = undefined;
142
- }
143
-
144
- // ---------------------------------------------------------------------------
145
- // ScrollableListPanel implementation (filter is only enabled in 'all' mode)
146
- // ---------------------------------------------------------------------------
147
-
148
- /** `q` arrives already trimmed + lower-cased from ScrollableListPanel.getVisibleItems(). */
149
- protected override filterMatches(record: MemoryRecord, q: string): boolean {
150
- const haystack = [
151
- record.summary,
152
- record.detail ?? '',
153
- record.cls,
154
- record.scope,
155
- record.tags.join(' '),
156
- ].join(' ').toLowerCase();
157
- return haystack.includes(q);
158
- }
159
-
160
- protected renderItem(record: MemoryRecord, index: number, selected: boolean, width: number): Line {
161
- if (this.filterMode === 'review') {
162
- // Review-mode row: reviewState + confidence (matches former KnowledgePanel row)
163
- const bg = selected ? C.selectBg : undefined;
164
- return buildPanelLine(width, [
165
- [' ', C.label, bg],
166
- [record.reviewState.padEnd(13), reviewStateColor(record.reviewState), bg],
167
- [` ${formatConfidence(record.confidence)} `, C.value, bg],
168
- [truncateDisplay(record.summary, Math.max(0, width - 26)), C.value, bg],
169
- ]);
170
- }
171
- // All-mode row: scope/class + id + time + summary (matches former MemoryPanel row)
172
- const bg = selected ? C.selectBg : undefined;
173
- return buildPanelLine(width, [
174
- [' ', C.label, bg],
175
- [`[${record.scope.slice(0, 1).toUpperCase()}/${record.cls.slice(0, 3).toUpperCase()}] `, classColor(record.cls), bg],
176
- [record.id.slice(-8), C.dim, bg],
177
- [' ', C.label, bg],
178
- [fmtTime(record.createdAt), C.dim, bg],
179
- [' ', C.label, bg],
180
- [truncateDisplay(record.summary, Math.max(0, width - 33)), C.value, bg],
181
- ]);
182
- }
183
-
184
- protected override getPalette() { return C; }
185
-
186
- protected override getEmptyStateMessage() {
187
- if (this.filterMode === 'review') return 'No records in the review queue.';
188
- // The filter-narrowed-to-empty case is handled by ScrollableListPanel's
189
- // own 'No matches for "..."' message (renderList) before this is reached.
190
- return ' No memory records. Use /recall add <class> <summary> to create one.';
191
- }
192
-
193
- protected override getEmptyStateActions() {
194
- return [
195
- { command: '/recall add fact <summary>', summary: 'capture a durable fact directly' },
196
- { command: '/recall capture incident latest', summary: 'promote the latest incident into memory' },
197
- { command: '/recall capture policy', summary: 'store the current policy posture as durable evidence' },
198
- ];
199
- }
200
-
201
- // ---------------------------------------------------------------------------
202
- // Internal helpers
203
- // ---------------------------------------------------------------------------
204
-
205
- /**
206
- * getItems() so that ScrollableListPanel infrastructure (renderList,
207
- * clampSelection, navigation bounds, and — in 'all' mode — the modal '/'
208
- * filter) all operate on the right data set per mode — preventing the
209
- * render/action index desync where rendered list index N did not
210
- * correspond to reviewRecords[N] when the two lists differ.
211
- */
212
- protected getItems(): readonly MemoryRecord[] {
213
- if (this.filterMode === 'review') return this.reviewRecords;
214
- return this.registry.search({ limit: 100 });
215
- }
216
-
217
- private refreshReviewRecords(): void {
218
- this.reviewRecords = this.registry.reviewQueue(24);
219
- this.clampSelection();
220
- }
221
-
222
- /** Count of records beyond the 100-record fetch cap in getItems(). */
223
- private getHiddenCount(): number {
224
- const total = this.registry.search({}).length;
225
- return Math.max(0, total - 100);
226
- }
227
-
228
- private cycleFilter(): void {
229
- const modes: FilterMode[] = ['all', 'review'];
230
- const next = modes[(modes.indexOf(this.filterMode) + 1) % modes.length];
231
- this.filterMode = next;
232
- // The modal '/' filter only applies in 'all' mode — review mode uses
233
- // r/s/c/f/d single-letter actions instead of text filtering.
234
- this.filterEnabled = next === 'all';
235
- this.refreshReviewRecords();
236
- this.markDirty();
237
- }
238
-
239
- // ---------------------------------------------------------------------------
240
- // Input
241
- // ---------------------------------------------------------------------------
242
-
243
- handleInput(key: string): boolean {
244
- // Review confirm dialog intercepts all input
245
- if (this.confirm) {
246
- const result = handleConfirmInput(this.confirm, key);
247
- if (result === 'confirmed') {
248
- const { id, action } = this.confirm.subject;
249
- this.confirm = null;
250
- if (action === 'delete') {
251
- try {
252
- this.registry.delete(id);
253
- } catch (e) {
254
- this.setError(`Delete failed: ${summarizeError(e)}`);
255
- }
256
- this.refreshReviewRecords();
257
- this.markDirty();
258
- return true;
259
- }
260
- const record = this.reviewRecords.find((r) => r.id === id);
261
- if (record) {
262
- try {
263
- if (action === 'stale') {
264
- this.registry.review(id, {
265
- state: 'stale',
266
- confidence: Math.min(record.confidence, 40),
267
- reviewedBy: 'operator',
268
- staleReason: 'marked stale from the memory panel',
269
- });
270
- } else {
271
- this.registry.review(id, {
272
- state: 'contradicted',
273
- confidence: 0,
274
- reviewedBy: 'operator',
275
- staleReason: 'marked contradicted from the memory panel',
276
- });
277
- }
278
- } catch (e) {
279
- this.setError(`Review update failed: ${summarizeError(e)}`);
280
- }
281
- }
282
- this.refreshReviewRecords();
283
- this.markDirty();
284
- return true;
285
- }
286
- if (result === 'cancelled') {
287
- this.confirm = null;
288
- this.markDirty();
289
- return true;
290
- }
291
- if (result === 'absorbed') return true;
292
- }
293
-
294
- // Tab cycles filter modes
295
- if (key === 'tab') {
296
- this.cycleFilter();
297
- return true;
298
- }
299
-
300
- // Review-mode specific actions (r/s/c/f)
301
- if (this.filterMode === 'review') {
302
- // In review mode getItems() returns reviewRecords and filterEnabled is
303
- // false, so getSelectedItem() resolves the highlighted review record.
304
- const selected = this.getSelectedItem();
305
-
306
- if (key === 'enter' || key === 'return' || key === 'r') {
307
- if (!selected) return false;
308
- this.registry.review(selected.id, {
309
- state: 'reviewed',
310
- confidence: Math.max(selected.confidence, 85),
311
- reviewedBy: 'operator',
312
- });
313
- this.refreshReviewRecords();
314
- this.markDirty();
315
- return true;
316
- }
317
- if (key === 's') {
318
- if (!selected) return false;
319
- this.confirm = { subject: { id: selected.id, action: 'stale' }, label: truncateDisplay(selected.summary, 40) };
320
- this.markDirty();
321
- return true;
322
- }
323
- if (key === 'c') {
324
- if (!selected) return false;
325
- this.confirm = { subject: { id: selected.id, action: 'contradicted' }, label: truncateDisplay(selected.summary, 40) };
326
- this.markDirty();
327
- return true;
328
- }
329
- if (key === 'f') {
330
- if (!selected) return false;
331
- this.registry.review(selected.id, {
332
- state: 'fresh',
333
- confidence: Math.max(selected.confidence, 60),
334
- reviewedBy: 'operator',
335
- });
336
- this.refreshReviewRecords();
337
- this.markDirty();
338
- return true;
339
- }
340
- if (key === 'd') {
341
- if (!selected) return false;
342
- this.confirm = { subject: { id: selected.id, action: 'delete' }, label: truncateDisplay(selected.summary, 40) };
343
- this.markDirty();
344
- return true;
345
- }
346
- }
347
-
348
- // All-mode: 'r' reload and 'd' delete are single-letter actions that
349
- // coexist with the modal '/' filter — they only fire outside filter
350
- // mode, so they remain typeable into the filter query while it's active.
351
- if (this.filterMode === 'all' && !this.filterActive) {
352
- if (key === 'r') {
353
- this.markDirty();
354
- return true;
355
- }
356
-
357
- if (key === 'd') {
358
- const selected = this.getSelectedItem();
359
- if (!selected) return false;
360
- this.confirm = { subject: { id: selected.id, action: 'delete' }, label: truncateDisplay(selected.summary, 40) };
361
- this.markDirty();
362
- return true;
363
- }
364
- }
365
-
366
- // Navigation + (in 'all' mode) the modal '/' filter: delegate to
367
- // ScrollableListPanel. filterEnabled is false in review mode, so '/' is
368
- // inert there and single-char keys like 'j'/'k' navigate normally.
369
- return super.handleInput(key);
370
- }
371
-
372
- // ---------------------------------------------------------------------------
373
- // Render
374
- // ---------------------------------------------------------------------------
375
-
376
- render(width: number, height: number): Line[] {
377
- this.clampSelection();
378
-
379
- // Review confirm dialog takes over the full panel
380
- if (this.confirm) {
381
- return buildPanelWorkspace(width, height, {
382
- title: 'Memory',
383
- intro: '',
384
- sections: [{ title: 'Confirmation', lines: renderConfirmLines(width, this.confirm) }],
385
- footerLines: [buildPanelLine(width, [[' y confirm n / Esc cancel', C.dim]])],
386
- palette: C,
387
- });
388
- }
389
-
390
- const filterLabel = FILTER_LABELS[this.filterMode];
391
- const filterToggleLine = buildPanelLine(width, [
392
- [' Filter: ', C.label],
393
- [filterLabel, C.info],
394
- [' (Tab to toggle)', C.dim],
395
- ]);
396
-
397
- if (this.filterMode === 'review') {
398
- return this.renderReviewMode(width, height, filterToggleLine);
399
- }
400
- return this.renderAllMode(width, height, filterToggleLine);
401
- }
402
-
403
- private renderAllMode(width: number, height: number, filterToggleLine: Line): Line[] {
404
- // getVisibleItems() applies the modal '/' filter (getItems() is the
405
- // unfiltered 100-cap fetch); the filter input line itself is
406
- // auto-injected by renderList() below (filterEnabled=true in 'all' mode).
407
- const records = this.getVisibleItems();
408
- const byClass = new Map<MemoryClass, number>();
409
- for (const record of records) {
410
- byClass.set(record.cls, (byClass.get(record.cls) ?? 0) + 1);
411
- }
412
-
413
- const hiddenCount = this.getHiddenCount();
414
-
415
- const summaryLines: Line[] = [
416
- buildKeyValueLine(width, [
417
- { label: 'records', value: String(records.length), valueColor: C.value },
418
- { label: 'facts', value: String(byClass.get('fact') ?? 0), valueColor: C.fact },
419
- { label: 'decisions', value: String(byClass.get('decision') ?? 0), valueColor: C.decision },
420
- { label: 'incidents', value: String(byClass.get('incident') ?? 0), valueColor: C.incident },
421
- { label: 'runbooks', value: String(byClass.get('runbook') ?? 0), valueColor: C.runbook },
422
- ], C),
423
- filterToggleLine,
424
- ...(hiddenCount > 0
425
- ? [buildPanelLine(width, [[` +${hiddenCount} hidden (100-record cap)`, C.dim]])]
426
- : []),
427
- ];
428
-
429
- const selected = this.getSelectedItem();
430
- const selectedLines: Line[] = [];
431
- if (selected) {
432
- selectedLines.push(buildKeyValueLine(width, [
433
- { label: 'scope', value: selected.scope, valueColor: C.info },
434
- { label: 'class', value: selected.cls, valueColor: classColor(selected.cls) },
435
- { label: 'created', value: fmtTime(selected.createdAt), valueColor: C.dim },
436
- ], C));
437
- selectedLines.push(...buildBodyText(width, selected.summary, C, C.value));
438
- if (selected.detail) selectedLines.push(...buildBodyText(width, `Detail: ${selected.detail}`, C, C.dim));
439
- if (selected.tags.length) selectedLines.push(buildPanelLine(width, [[` Tags: ${selected.tags.join(', ')}`, C.good]]));
440
- if (selected.provenance.length) {
441
- selectedLines.push(...buildBodyText(
442
- width,
443
- `Provenance: ${selected.provenance.map((p) => `${p.kind}:${p.ref}`).join(' ')}`,
444
- C,
445
- C.dim,
446
- ));
447
- }
448
- }
449
-
450
- return this.renderList(width, height, {
451
- title: 'Memory',
452
- header: summaryLines,
453
- footer: [
454
- ...selectedLines,
455
- buildPanelLine(width, [[' / search j/k or Up/Down move r reload d delete Tab: Review Queue Esc clear search', C.dim]]),
456
- ],
457
- });
458
- }
459
-
460
- private renderReviewMode(width: number, height: number, filterToggleLine: Line): Line[] {
461
- if (this.reviewRecords.length === 0) this.refreshReviewRecords();
462
-
463
- const allRecords = this.registry.search({ limit: 200 });
464
- const queue = this.registry.reviewQueue(24);
465
- const byClass = new Map<MemoryClass, number>();
466
- const byReview = new Map<MemoryReviewState, number>();
467
- const byScope = new Map<string, number>();
468
- for (const record of allRecords) {
469
- byClass.set(record.cls, (byClass.get(record.cls) ?? 0) + 1);
470
- byReview.set(record.reviewState, (byReview.get(record.reviewState) ?? 0) + 1);
471
- byScope.set(record.scope, (byScope.get(record.scope) ?? 0) + 1);
472
- }
473
-
474
- const classLines: Line[] = [
475
- buildPanelLine(width, [
476
- [' facts ', C.label], [String(byClass.get('fact') ?? 0), C.good],
477
- [' risks ', C.label], [String(byClass.get('risk') ?? 0), (byClass.get('risk') ?? 0) > 0 ? C.warn : C.good],
478
- [' runbooks ', C.label], [String(byClass.get('runbook') ?? 0), C.info],
479
- [' architecture ', C.label], [String(byClass.get('architecture') ?? 0), C.info],
480
- [' incidents ', C.label], [String(byClass.get('incident') ?? 0), (byClass.get('incident') ?? 0) > 0 ? C.bad : C.good],
481
- ]),
482
- buildPanelLine(width, [
483
- [' decisions ', C.label], [String(byClass.get('decision') ?? 0), C.value],
484
- [' constraints ', C.label], [String(byClass.get('constraint') ?? 0), C.value],
485
- [' ownership ', C.label], [String(byClass.get('ownership') ?? 0), C.value],
486
- [' patterns ', C.label], [String(byClass.get('pattern') ?? 0), C.value],
487
- [' total ', C.label], [String(allRecords.length), C.value],
488
- ]),
489
- ];
490
-
491
- const reviewLines: Line[] = [
492
- buildPanelLine(width, [
493
- [' reviewed ', C.label], [String(byReview.get('reviewed') ?? 0), C.good],
494
- [' fresh ', C.label], [String(byReview.get('fresh') ?? 0), C.info],
495
- [' stale ', C.label], [String(byReview.get('stale') ?? 0), C.warn],
496
- [' contradicted ', C.label], [String(byReview.get('contradicted') ?? 0), C.bad],
497
- [' Queue ', C.label], [String(queue.length), queue.length > 0 ? C.warn : C.good],
498
- ]),
499
- buildPanelLine(width, [
500
- [' session ', C.label], [String(byScope.get('session') ?? 0), C.info],
501
- [' project ', C.label], [String(byScope.get('project') ?? 0), C.value],
502
- [' team ', C.label], [String(byScope.get('team') ?? 0), C.good],
503
- ]),
504
- filterToggleLine,
505
- ];
506
-
507
- const selectedRecord = this.getSelectedItem();
508
- const selectedLines: Line[] = [];
509
- if (selectedRecord) {
510
- selectedLines.push(buildPanelLine(width, [[' Selected', C.label]]));
511
- selectedLines.push(buildKeyValueLine(width, [
512
- { label: 'Class', value: selectedRecord.cls, valueColor: C.value },
513
- { label: 'Scope', value: selectedRecord.scope, valueColor: C.info },
514
- { label: 'Review', value: selectedRecord.reviewState, valueColor: reviewStateColor(selectedRecord.reviewState) },
515
- { label: 'Confidence', value: formatConfidence(selectedRecord.confidence), valueColor: C.value },
516
- ], C));
517
- selectedLines.push(...buildBodyText(width, `Summary: ${selectedRecord.summary}`, C, C.value));
518
- if (selectedRecord.detail) selectedLines.push(...buildBodyText(width, `Detail: ${selectedRecord.detail}`, C, C.dim));
519
- if (selectedRecord.provenance.length) {
520
- selectedLines.push(...buildBodyText(
521
- width,
522
- `Provenance: ${selectedRecord.provenance.map((p) => `${p.kind}:${p.ref}`).join(', ')}`,
523
- C,
524
- C.dim,
525
- ));
526
- }
527
- if (selectedRecord.staleReason) {
528
- selectedLines.push(...buildBodyText(
529
- width,
530
- `Stale reason: ${selectedRecord.staleReason}`,
531
- C,
532
- selectedRecord.reviewState === 'contradicted' ? C.bad : C.warn,
533
- ));
534
- }
535
- if (selectedRecord.reviewedAt) {
536
- selectedLines.push(buildPanelLine(width, [
537
- [' Reviewed: ', C.label],
538
- [new Date(selectedRecord.reviewedAt).toLocaleString(), C.dim],
539
- ]));
540
- if (selectedRecord.reviewedBy) {
541
- selectedLines.push(buildPanelLine(width, [
542
- [' Reviewer: ', C.label],
543
- [selectedRecord.reviewedBy, C.dim],
544
- ]));
545
- }
546
- }
547
- }
548
-
549
- return this.renderList(width, height, {
550
- title: 'Memory',
551
- header: [...classLines, ...reviewLines],
552
- footer: [
553
- ...(selectedLines.length > 0 ? selectedLines : []),
554
- buildPanelLine(width, [[' Tab: All Records Up/Down move r/Enter reviewed s stale c contradicted f fresh d delete', C.dim]]),
555
- ],
556
- });
557
- }
558
- }