@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
@@ -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
- }