@pellux/goodvibes-tui 0.20.3 → 0.22.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 (142) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +23 -2
  3. package/docs/foundation-artifacts/operator-contract.json +78 -1
  4. package/package.json +4 -2
  5. package/src/audio/spoken-turn-controller.ts +31 -1
  6. package/src/audio/spoken-turn-wiring.ts +26 -4
  7. package/src/cli/bundle-command.ts +1 -1
  8. package/src/cli/completions/generate.ts +658 -0
  9. package/src/cli/config-overrides.ts +68 -0
  10. package/src/cli/entrypoint.ts +6 -0
  11. package/src/cli/help.ts +4 -2
  12. package/src/cli/management-commands.ts +1 -1
  13. package/src/cli/management.ts +1 -8
  14. package/src/cli/parser.ts +31 -18
  15. package/src/cli/service-command.ts +1 -1
  16. package/src/cli/surface-command.ts +1 -1
  17. package/src/cli/tui-startup.ts +72 -10
  18. package/src/cli/types.ts +14 -3
  19. package/src/cli-flags.ts +1 -0
  20. package/src/config/atomic-write.ts +70 -0
  21. package/src/config/goodvibes-home-audit.ts +2 -0
  22. package/src/config/read-versioned.ts +115 -0
  23. package/src/core/context-auto-compact.ts +77 -0
  24. package/src/core/conversation-rendering.ts +49 -15
  25. package/src/core/conversation.ts +101 -16
  26. package/src/core/format-user-error.ts +192 -0
  27. package/src/core/stream-event-wiring.ts +144 -0
  28. package/src/core/stream-stall-watchdog.ts +103 -0
  29. package/src/core/system-message-router.ts +5 -1
  30. package/src/core/turn-event-wiring.ts +124 -0
  31. package/src/daemon/cli.ts +5 -0
  32. package/src/export/cost-utils.ts +71 -0
  33. package/src/export/gist-uploader.ts +136 -0
  34. package/src/input/command-registry.ts +32 -1
  35. package/src/input/commands/control-room-runtime.ts +10 -10
  36. package/src/input/commands/experience-runtime.ts +5 -4
  37. package/src/input/commands/knowledge.ts +1 -1
  38. package/src/input/commands/local-auth-runtime.ts +27 -5
  39. package/src/input/commands/local-setup.ts +4 -6
  40. package/src/input/commands/memory-product-runtime.ts +8 -6
  41. package/src/input/commands/operator-panel-runtime.ts +1 -1
  42. package/src/input/commands/operator-runtime.ts +3 -10
  43. package/src/input/commands/{integration-runtime.ts → plugin-runtime.ts} +1 -1
  44. package/src/input/commands/provider.ts +57 -3
  45. package/src/input/commands/recall-review.ts +26 -2
  46. package/src/input/commands/services-runtime.ts +2 -2
  47. package/src/input/commands/session-workflow.ts +8 -16
  48. package/src/input/commands/session.ts +70 -20
  49. package/src/input/commands/share-runtime.ts +99 -12
  50. package/src/input/commands/tts-runtime.ts +30 -4
  51. package/src/input/commands.ts +2 -4
  52. package/src/input/delete-key-policy.ts +46 -0
  53. package/src/input/feed-context-factory.ts +2 -0
  54. package/src/input/handler-feed.ts +3 -0
  55. package/src/input/handler-interactions.ts +2 -15
  56. package/src/input/handler-modal-routes.ts +128 -12
  57. package/src/input/handler-modal-token-routes.ts +22 -5
  58. package/src/input/handler-onboarding-cloudflare.ts +1 -1
  59. package/src/input/handler-onboarding.ts +73 -69
  60. package/src/input/handler-types.ts +163 -0
  61. package/src/input/handler.ts +6 -2
  62. package/src/input/input-history.ts +76 -6
  63. package/src/input/model-picker-filter.ts +265 -0
  64. package/src/input/model-picker-items.ts +208 -0
  65. package/src/input/model-picker.ts +92 -325
  66. package/src/input/onboarding/handler-onboarding-routes.ts +7 -2
  67. package/src/input/onboarding/onboarding-verification-helpers.ts +76 -0
  68. package/src/input/onboarding/onboarding-wizard-apply.ts +14 -4
  69. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +16 -2
  70. package/src/input/onboarding/onboarding-wizard-cloudflare.ts +8 -8
  71. package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +1 -1
  72. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +2 -29
  73. package/src/input/onboarding/onboarding-wizard-rules.ts +28 -28
  74. package/src/input/onboarding/onboarding-wizard-state.ts +20 -20
  75. package/src/input/onboarding/onboarding-wizard-steps.ts +24 -25
  76. package/src/input/onboarding/onboarding-wizard-types.ts +145 -3
  77. package/src/input/onboarding/onboarding-wizard-validation.ts +77 -0
  78. package/src/input/onboarding/onboarding-wizard.ts +3 -3
  79. package/src/input/settings-modal-behavior.ts +5 -0
  80. package/src/input/settings-modal-data.ts +378 -0
  81. package/src/input/settings-modal-mutations.ts +157 -0
  82. package/src/input/settings-modal-reset.ts +154 -0
  83. package/src/input/settings-modal.ts +236 -232
  84. package/src/main.ts +93 -85
  85. package/src/panels/agent-inspector-panel.ts +120 -18
  86. package/src/panels/agent-inspector-shared.ts +29 -0
  87. package/src/panels/builtin/agent.ts +4 -1
  88. package/src/panels/builtin/development.ts +5 -1
  89. package/src/panels/builtin/knowledge.ts +14 -13
  90. package/src/panels/builtin/operations.ts +22 -1
  91. package/src/panels/builtin/shared.ts +7 -0
  92. package/src/panels/cockpit-panel.ts +123 -3
  93. package/src/panels/cockpit-read-model.ts +232 -0
  94. package/src/panels/confirm-state.ts +27 -12
  95. package/src/panels/cost-tracker-panel.ts +23 -67
  96. package/src/panels/eval-panel.ts +10 -9
  97. package/src/panels/index.ts +1 -1
  98. package/src/panels/knowledge-graph-panel.ts +84 -0
  99. package/src/panels/local-auth-panel.ts +124 -4
  100. package/src/panels/memory-panel.ts +370 -40
  101. package/src/panels/project-planning-panel.ts +42 -4
  102. package/src/panels/search-focus.ts +11 -5
  103. package/src/panels/session-maintenance.ts +66 -15
  104. package/src/panels/subscription-panel.ts +33 -25
  105. package/src/panels/types.ts +28 -1
  106. package/src/panels/wrfc-panel.ts +224 -41
  107. package/src/renderer/agent-detail-modal.ts +118 -13
  108. package/src/renderer/code-block.ts +10 -2
  109. package/src/renderer/compositor.ts +18 -4
  110. package/src/renderer/context-inspector.ts +1 -5
  111. package/src/renderer/context-status-hint.ts +54 -0
  112. package/src/renderer/diff.ts +94 -21
  113. package/src/renderer/markdown.ts +29 -13
  114. package/src/renderer/settings-modal-helpers.ts +1 -1
  115. package/src/renderer/settings-modal.ts +90 -10
  116. package/src/renderer/shell-surface.ts +10 -0
  117. package/src/renderer/syntax-highlighter.ts +10 -3
  118. package/src/renderer/term-caps.ts +318 -0
  119. package/src/renderer/theme.ts +158 -0
  120. package/src/renderer/tool-call.ts +12 -2
  121. package/src/renderer/ui-factory.ts +50 -6
  122. package/src/runtime/bootstrap-command-context.ts +1 -0
  123. package/src/runtime/bootstrap-command-parts.ts +18 -0
  124. package/src/runtime/bootstrap-core.ts +145 -13
  125. package/src/runtime/bootstrap-shell.ts +11 -0
  126. package/src/runtime/bootstrap.ts +9 -0
  127. package/src/runtime/onboarding/apply.ts +4 -6
  128. package/src/runtime/onboarding/index.ts +1 -0
  129. package/src/runtime/onboarding/markers.ts +42 -49
  130. package/src/runtime/onboarding/progress.ts +148 -0
  131. package/src/runtime/onboarding/state.ts +133 -55
  132. package/src/runtime/onboarding/types.ts +20 -0
  133. package/src/runtime/services.ts +27 -1
  134. package/src/runtime/wrfc-persistence.ts +237 -0
  135. package/src/shell/blocking-input.ts +20 -5
  136. package/src/tools/wrfc-agent-guard.ts +64 -3
  137. package/src/utils/format-elapsed.ts +30 -0
  138. package/src/utils/terminal-width.ts +45 -0
  139. package/src/version.ts +1 -1
  140. package/src/work-plans/work-plan-store.ts +4 -6
  141. package/src/panels/knowledge-panel.ts +0 -345
  142. package/src/planning/project-planning-coordinator.ts +0 -543
@@ -1,18 +1,26 @@
1
1
  /**
2
- * MemoryPanel — project memory substrate TUI panel.
2
+ * MemoryPanel — merged project memory substrate panel.
3
3
  *
4
- * Migrated to SearchableListPanel<MemoryRecord> (Wave B1).
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.
5
11
  */
6
12
 
7
13
  import type { Line } from '../types/grid.ts';
8
14
  import type { MemoryRegistry } from '@pellux/goodvibes-sdk/platform/state';
9
- import type { MemoryRecord, MemoryClass } from '@pellux/goodvibes-sdk/platform/state';
10
- import { SearchableListPanel } from './scrollable-list-panel.ts';
15
+ import type { MemoryClass, MemoryRecord, MemoryReviewState } from '@pellux/goodvibes-sdk/platform/state';
16
+ import { ScrollableListPanel, SearchableListPanel } from './scrollable-list-panel.ts';
17
+ import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
11
18
  import {
12
19
  buildBodyText,
13
20
  buildGuidanceLine,
14
21
  buildKeyValueLine,
15
22
  buildPanelLine,
23
+ buildPanelWorkspace,
16
24
  extendPalette,
17
25
  DEFAULT_PANEL_PALETTE,
18
26
  } from './polish.ts';
@@ -21,6 +29,10 @@ import {
21
29
  isPanelSearchCancel,
22
30
  } from './search-focus.ts';
23
31
 
32
+ // ---------------------------------------------------------------------------
33
+ // Colour palette
34
+ // ---------------------------------------------------------------------------
35
+
24
36
  const C = extendPalette(DEFAULT_PANEL_PALETTE, {
25
37
  header: '#94a3b8',
26
38
  headerBg: '#1e293b',
@@ -38,6 +50,21 @@ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
38
50
  searchFg: '#e2e8f0',
39
51
  });
40
52
 
53
+ // ---------------------------------------------------------------------------
54
+ // Filter modes
55
+ // ---------------------------------------------------------------------------
56
+
57
+ type FilterMode = 'all' | 'review';
58
+
59
+ const FILTER_LABELS: Record<FilterMode, string> = {
60
+ all: 'All Records',
61
+ review: 'Review Queue',
62
+ };
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Helpers
66
+ // ---------------------------------------------------------------------------
67
+
41
68
  function fmtTime(ts: number): string {
42
69
  const d = new Date(ts);
43
70
  return d.toISOString().slice(0, 16).replace('T', ' ');
@@ -45,50 +72,82 @@ function fmtTime(ts: number): string {
45
72
 
46
73
  function classColor(cls: MemoryClass): string {
47
74
  switch (cls) {
48
- case 'decision': return C.decision;
49
- case 'constraint': return C.constraint;
50
- case 'incident': return C.incident;
51
- case 'pattern': return C.pattern;
52
- case 'fact': return C.fact;
53
- case 'risk': return C.risk;
54
- case 'runbook': return C.runbook;
75
+ case 'decision': return C.decision;
76
+ case 'constraint': return C.constraint;
77
+ case 'incident': return C.incident;
78
+ case 'pattern': return C.pattern;
79
+ case 'fact': return C.fact;
80
+ case 'risk': return C.risk;
81
+ case 'runbook': return C.runbook;
55
82
  case 'architecture': return C.architecture;
56
- case 'ownership': return C.ownership;
83
+ case 'ownership': return C.ownership;
57
84
  }
58
85
  }
59
86
 
87
+ function reviewStateColor(state: MemoryReviewState): string {
88
+ switch (state) {
89
+ case 'reviewed': return C.good;
90
+ case 'stale': return C.warn;
91
+ case 'contradicted': return C.bad;
92
+ case 'fresh':
93
+ default: return C.info;
94
+ }
95
+ }
96
+
97
+ function formatConfidence(confidence: number): string {
98
+ return `${confidence.toString().padStart(3, ' ')}%`;
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // MemoryPanel
103
+ // ---------------------------------------------------------------------------
104
+
60
105
  export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
61
- private registry: MemoryRegistry;
106
+ private readonly registry: MemoryRegistry;
107
+ private filterMode: FilterMode = 'all';
62
108
  private filterFocused = false;
63
109
  private unsubscribe?: () => void;
64
110
 
111
+ // Review-mode confirm state (for destructive stale/contradict actions)
112
+ private confirm: ConfirmState<{ id: string; action: 'stale' | 'contradicted' }> | null = null;
113
+
114
+ // Cached records for review-mode (reviewQueue-first, same as former KnowledgePanel)
115
+ private reviewRecords: MemoryRecord[] = [];
116
+
65
117
  constructor(registry: MemoryRegistry) {
66
118
  super('memory', 'Memory', 'M', 'agent');
67
119
  this.registry = registry;
68
120
  }
69
121
 
70
- onActivate(): void {
122
+ // ---------------------------------------------------------------------------
123
+ // Lifecycle
124
+ // ---------------------------------------------------------------------------
125
+
126
+ override onActivate(): void {
71
127
  super.onActivate();
72
128
  this.searchQuery = '';
73
129
  this.invalidateFilter();
74
130
  this.filterFocused = false;
131
+ this.confirm = null;
132
+ this.refreshReviewRecords();
75
133
  this.unsubscribe = this.registry.subscribe(() => {
76
134
  this.invalidateFilter();
135
+ this.refreshReviewRecords();
77
136
  this.markDirty();
78
137
  });
79
138
  }
80
139
 
81
- onDeactivate(): void {
140
+ override onDeactivate(): void {
82
141
  super.onDeactivate();
83
142
  }
84
143
 
85
- onDestroy(): void {
144
+ override onDestroy(): void {
86
145
  this.unsubscribe?.();
87
146
  this.unsubscribe = undefined;
88
147
  }
89
148
 
90
149
  // ---------------------------------------------------------------------------
91
- // SearchableListPanel implementation
150
+ // SearchableListPanel implementation (used in 'all' filter mode)
92
151
  // ---------------------------------------------------------------------------
93
152
 
94
153
  protected getAllItems(): readonly MemoryRecord[] {
@@ -109,6 +168,17 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
109
168
  }
110
169
 
111
170
  protected renderItem(record: MemoryRecord, index: number, selected: boolean, width: number): Line {
171
+ if (this.filterMode === 'review') {
172
+ // Review-mode row: reviewState + confidence (matches former KnowledgePanel row)
173
+ const bg = selected ? C.selectBg : undefined;
174
+ return buildPanelLine(width, [
175
+ [' ', C.label, bg],
176
+ [record.reviewState.padEnd(13), reviewStateColor(record.reviewState), bg],
177
+ [` ${formatConfidence(record.confidence)} `, C.value, bg],
178
+ [record.summary.slice(0, Math.max(0, width - 26)), C.value, bg],
179
+ ]);
180
+ }
181
+ // All-mode row: scope/class + id + time + summary (matches former MemoryPanel row)
112
182
  const bg = selected ? C.selected : undefined;
113
183
  return buildPanelLine(width, [
114
184
  [' ', C.label, bg],
@@ -122,63 +192,222 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
122
192
  }
123
193
 
124
194
  protected override getPalette() { return C; }
195
+
125
196
  protected override getEmptyStateMessage() {
197
+ if (this.filterMode === 'review') return 'No records in the review queue.';
126
198
  return this.searchQuery
127
199
  ? ` No records matching "${this.searchQuery}"`
128
200
  : ' No memory records. Use /recall add <class> <summary> to create one.';
129
201
  }
202
+
130
203
  protected override getEmptyStateActions() {
131
204
  return [
132
205
  { command: '/recall add fact <summary>', summary: 'capture a durable fact directly' },
133
206
  { command: '/recall capture incident latest', summary: 'promote the latest incident into memory' },
207
+ { command: '/recall capture policy', summary: 'store the current policy posture as durable evidence' },
134
208
  ];
135
209
  }
136
210
 
211
+ // ---------------------------------------------------------------------------
212
+ // Internal helpers
213
+ // ---------------------------------------------------------------------------
214
+
215
+ /**
216
+ * Override getItems() so that ScrollableListPanel infrastructure (renderList,
217
+ * clampSelection, navigation bounds) all operate on reviewRecords in review
218
+ * mode — preventing the render/action index desync where rendered list index N
219
+ * did not correspond to reviewRecords[N] when the two lists differ.
220
+ */
221
+ protected override getItems(): readonly MemoryRecord[] {
222
+ if (this.filterMode === 'review') return this.reviewRecords;
223
+ return super.getItems();
224
+ }
225
+
226
+ private refreshReviewRecords(): void {
227
+ this.reviewRecords = this.registry.reviewQueue(24);
228
+ this.clampSelection();
229
+ }
230
+
231
+ private cycleFilter(): void {
232
+ const modes: FilterMode[] = ['all', 'review'];
233
+ const next = modes[(modes.indexOf(this.filterMode) + 1) % modes.length];
234
+ this.filterMode = next;
235
+ this.invalidateFilter();
236
+ this.refreshReviewRecords();
237
+ this.markDirty();
238
+ }
239
+
240
+ // ---------------------------------------------------------------------------
241
+ // Input
242
+ // ---------------------------------------------------------------------------
243
+
137
244
  handleInput(key: string): boolean {
138
- // Filter-focus mode: typing goes into the search query
139
- if (this.filterFocused) {
140
- const items = this.getItems();
141
- const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.selectedIndex, itemCount: items.length });
142
- if (transition === 'focus-list') {
143
- this.filterFocused = false;
245
+ // Review confirm dialog intercepts all input
246
+ if (this.confirm) {
247
+ const result = handleConfirmInput(this.confirm, key);
248
+ if (result === 'confirmed') {
249
+ const { id, action } = this.confirm.subject;
250
+ this.confirm = null;
251
+ const record = this.reviewRecords.find((r) => r.id === id);
252
+ if (record) {
253
+ try {
254
+ if (action === 'stale') {
255
+ this.registry.review(id, {
256
+ state: 'stale',
257
+ confidence: Math.min(record.confidence, 40),
258
+ reviewedBy: 'operator',
259
+ staleReason: 'marked stale from the memory panel',
260
+ });
261
+ } else {
262
+ this.registry.review(id, {
263
+ state: 'contradicted',
264
+ confidence: 0,
265
+ reviewedBy: 'operator',
266
+ staleReason: 'marked contradicted from the memory panel',
267
+ });
268
+ }
269
+ } catch (e) {
270
+ this.setError(`Review update failed: ${e instanceof Error ? e.message : String(e)}`);
271
+ }
272
+ }
273
+ this.refreshReviewRecords();
144
274
  this.markDirty();
145
275
  return true;
146
276
  }
147
- if (isPanelSearchCancel(key)) {
148
- this.filterFocused = false;
149
- return super.handleInput(key);
277
+ if (result === 'cancelled') {
278
+ this.confirm = null;
279
+ this.markDirty();
280
+ return true;
150
281
  }
151
- return super.handleInput(key);
282
+ if (result === 'absorbed') return true;
152
283
  }
153
284
 
154
- const items = this.getItems();
155
- const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.selectedIndex, itemCount: items.length });
156
- if (transition === 'focus-search') {
157
- this.filterFocused = true;
158
- this.markDirty();
285
+ // Tab cycles filter modes
286
+ if (key === 'tab') {
287
+ this.cycleFilter();
159
288
  return true;
160
289
  }
161
290
 
162
- if (key === 'r') {
163
- this.invalidateFilter();
164
- this.markDirty();
165
- return true;
291
+ // Review-mode specific actions (r/s/c/f)
292
+ if (this.filterMode === 'review') {
293
+ const selected = this.reviewRecords[this.selectedIndex];
294
+
295
+ if (key === 'enter' || key === 'return' || key === 'r') {
296
+ if (!selected) return false;
297
+ this.registry.review(selected.id, {
298
+ state: 'reviewed',
299
+ confidence: Math.max(selected.confidence, 85),
300
+ reviewedBy: 'operator',
301
+ });
302
+ this.refreshReviewRecords();
303
+ this.markDirty();
304
+ return true;
305
+ }
306
+ if (key === 's') {
307
+ if (!selected) return false;
308
+ this.confirm = { subject: { id: selected.id, action: 'stale' }, label: selected.summary.slice(0, 40) };
309
+ this.markDirty();
310
+ return true;
311
+ }
312
+ if (key === 'c') {
313
+ if (!selected) return false;
314
+ this.confirm = { subject: { id: selected.id, action: 'contradicted' }, label: selected.summary.slice(0, 40) };
315
+ this.markDirty();
316
+ return true;
317
+ }
318
+ if (key === 'f') {
319
+ if (!selected) return false;
320
+ this.registry.review(selected.id, {
321
+ state: 'fresh',
322
+ confidence: Math.max(selected.confidence, 60),
323
+ reviewedBy: 'operator',
324
+ });
325
+ this.refreshReviewRecords();
326
+ this.markDirty();
327
+ return true;
328
+ }
329
+ }
330
+
331
+ // All-mode: search filter focus
332
+ if (this.filterMode === 'all') {
333
+ if (this.filterFocused) {
334
+ const items = this.getItems();
335
+ const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.selectedIndex, itemCount: items.length });
336
+ if (transition === 'focus-list') {
337
+ this.filterFocused = false;
338
+ this.markDirty();
339
+ return true;
340
+ }
341
+ if (isPanelSearchCancel(key)) {
342
+ this.filterFocused = false;
343
+ return super.handleInput(key);
344
+ }
345
+ return super.handleInput(key);
346
+ }
347
+
348
+ const items = this.getItems();
349
+ const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.selectedIndex, itemCount: items.length });
350
+ if (transition === 'focus-search') {
351
+ this.filterFocused = true;
352
+ this.markDirty();
353
+ return true;
354
+ }
355
+
356
+ if (key === 'r') {
357
+ this.invalidateFilter();
358
+ this.markDirty();
359
+ return true;
360
+ }
166
361
  }
167
362
 
363
+ // In review mode, navigation keys (j/k/up/down/etc.) must bypass
364
+ // SearchableListPanel's printable-character interception, which would
365
+ // otherwise swallow single-char keys like 'j' and 'k' as search input.
366
+ if (this.filterMode === 'review') {
367
+ return ScrollableListPanel.prototype.handleInput.call(this, key);
368
+ }
168
369
  return super.handleInput(key);
169
370
  }
170
371
 
372
+ // ---------------------------------------------------------------------------
373
+ // Render
374
+ // ---------------------------------------------------------------------------
375
+
171
376
  render(width: number, height: number): Line[] {
172
377
  this.clampSelection();
173
- const intro = 'Durable project memory across decisions, constraints, incidents, patterns, risks, runbooks, and related provenance.';
174
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[] {
175
404
  const records = this.getItems();
176
405
  const byClass = new Map<MemoryClass, number>();
177
406
  for (const record of records) {
178
407
  byClass.set(record.cls, (byClass.get(record.cls) ?? 0) + 1);
179
408
  }
180
409
 
181
- const filterLine = this.buildFilterInputLine(width, 'Filter', this.filterFocused);
410
+ const filterInputLine = this.buildFilterInputLine(width, 'Search', this.filterFocused);
182
411
 
183
412
  const summaryLines: Line[] = [
184
413
  buildKeyValueLine(width, [
@@ -188,7 +417,8 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
188
417
  { label: 'incidents', value: String(byClass.get('incident') ?? 0), valueColor: C.incident },
189
418
  { label: 'runbooks', value: String(byClass.get('runbook') ?? 0), valueColor: C.runbook },
190
419
  ], C),
191
- filterLine,
420
+ filterToggleLine,
421
+ filterInputLine,
192
422
  buildGuidanceLine(width, '/recall review', 'review durable knowledge and queue posture from the command surface', C),
193
423
  ];
194
424
 
@@ -218,7 +448,107 @@ export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
218
448
  header: summaryLines,
219
449
  footer: [
220
450
  ...selectedLines,
221
- buildPanelLine(width, [[' / search j/k or Up/Down move r reload Esc clear search', C.dim]]),
451
+ buildPanelLine(width, [[' / search j/k or Up/Down move r reload Tab: Review Queue Esc clear search', C.dim]]),
452
+ ],
453
+ });
454
+ }
455
+
456
+ private renderReviewMode(width: number, height: number, filterToggleLine: Line): Line[] {
457
+ if (this.reviewRecords.length === 0) this.refreshReviewRecords();
458
+
459
+ const allRecords = this.registry.search({ limit: 200 });
460
+ const queue = this.registry.reviewQueue(24);
461
+ const byClass = new Map<MemoryClass, number>();
462
+ const byReview = new Map<MemoryReviewState, number>();
463
+ const byScope = new Map<string, number>();
464
+ for (const record of allRecords) {
465
+ byClass.set(record.cls, (byClass.get(record.cls) ?? 0) + 1);
466
+ byReview.set(record.reviewState, (byReview.get(record.reviewState) ?? 0) + 1);
467
+ byScope.set(record.scope, (byScope.get(record.scope) ?? 0) + 1);
468
+ }
469
+
470
+ const classLines: Line[] = [
471
+ buildPanelLine(width, [
472
+ [' facts ', C.label], [String(byClass.get('fact') ?? 0), C.good],
473
+ [' risks ', C.label], [String(byClass.get('risk') ?? 0), (byClass.get('risk') ?? 0) > 0 ? C.warn : C.good],
474
+ [' runbooks ', C.label], [String(byClass.get('runbook') ?? 0), C.info],
475
+ [' architecture ', C.label], [String(byClass.get('architecture') ?? 0), C.info],
476
+ [' incidents ', C.label], [String(byClass.get('incident') ?? 0), (byClass.get('incident') ?? 0) > 0 ? C.bad : C.good],
477
+ ]),
478
+ buildPanelLine(width, [
479
+ [' decisions ', C.label], [String(byClass.get('decision') ?? 0), C.value],
480
+ [' constraints ', C.label], [String(byClass.get('constraint') ?? 0), C.value],
481
+ [' ownership ', C.label], [String(byClass.get('ownership') ?? 0), C.value],
482
+ [' patterns ', C.label], [String(byClass.get('pattern') ?? 0), C.value],
483
+ [' total ', C.label], [String(allRecords.length), C.value],
484
+ ]),
485
+ ];
486
+
487
+ const reviewLines: Line[] = [
488
+ buildPanelLine(width, [
489
+ [' reviewed ', C.label], [String(byReview.get('reviewed') ?? 0), C.good],
490
+ [' fresh ', C.label], [String(byReview.get('fresh') ?? 0), C.info],
491
+ [' stale ', C.label], [String(byReview.get('stale') ?? 0), C.warn],
492
+ [' contradicted ', C.label], [String(byReview.get('contradicted') ?? 0), C.bad],
493
+ [' Queue ', C.label], [String(queue.length), queue.length > 0 ? C.warn : C.good],
494
+ ]),
495
+ buildPanelLine(width, [
496
+ [' session ', C.label], [String(byScope.get('session') ?? 0), C.info],
497
+ [' project ', C.label], [String(byScope.get('project') ?? 0), C.value],
498
+ [' team ', C.label], [String(byScope.get('team') ?? 0), C.good],
499
+ ]),
500
+ filterToggleLine,
501
+ buildGuidanceLine(width, '/recall review', 'work the stale and contradicted queue from the command surface', C),
502
+ ];
503
+
504
+ const selectedRecord = this.reviewRecords[this.selectedIndex];
505
+ const selectedLines: Line[] = [];
506
+ if (selectedRecord) {
507
+ selectedLines.push(buildPanelLine(width, [[' Selected', C.label]]));
508
+ selectedLines.push(buildKeyValueLine(width, [
509
+ { label: 'Class', value: selectedRecord.cls, valueColor: C.value },
510
+ { label: 'Scope', value: selectedRecord.scope, valueColor: C.info },
511
+ { label: 'Review', value: selectedRecord.reviewState, valueColor: reviewStateColor(selectedRecord.reviewState) },
512
+ { label: 'Confidence', value: formatConfidence(selectedRecord.confidence), valueColor: C.value },
513
+ ], C));
514
+ selectedLines.push(...buildBodyText(width, `Summary: ${selectedRecord.summary}`, C, C.value));
515
+ if (selectedRecord.detail) selectedLines.push(...buildBodyText(width, `Detail: ${selectedRecord.detail}`, C, C.dim));
516
+ if (selectedRecord.provenance.length) {
517
+ selectedLines.push(...buildBodyText(
518
+ width,
519
+ `Provenance: ${selectedRecord.provenance.map((p) => `${p.kind}:${p.ref}`).join(', ')}`,
520
+ C,
521
+ C.dim,
522
+ ));
523
+ }
524
+ if (selectedRecord.staleReason) {
525
+ selectedLines.push(...buildBodyText(
526
+ width,
527
+ `Stale reason: ${selectedRecord.staleReason}`,
528
+ C,
529
+ selectedRecord.reviewState === 'contradicted' ? C.bad : C.warn,
530
+ ));
531
+ }
532
+ if (selectedRecord.reviewedAt) {
533
+ selectedLines.push(buildPanelLine(width, [
534
+ [' Reviewed: ', C.label],
535
+ [new Date(selectedRecord.reviewedAt).toLocaleString(), C.dim],
536
+ ]));
537
+ if (selectedRecord.reviewedBy) {
538
+ selectedLines.push(buildPanelLine(width, [
539
+ [' Reviewer: ', C.label],
540
+ [selectedRecord.reviewedBy, C.dim],
541
+ ]));
542
+ }
543
+ }
544
+ }
545
+
546
+ return this.renderList(width, height, {
547
+ title: 'Memory',
548
+ header: [...classLines, ...reviewLines],
549
+ footer: [
550
+ ...(selectedLines.length > 0 ? selectedLines : []),
551
+ buildPanelLine(width, [[' Tab: All Records Up/Down move r/Enter reviewed s stale c contradicted f fresh', C.dim]]),
222
552
  ],
223
553
  });
224
554
  }
@@ -9,6 +9,8 @@ import type {
9
9
  } from '@pellux/goodvibes-sdk/platform/knowledge';
10
10
  import type { Line } from '../types/grid.ts';
11
11
  import { BasePanel } from './base-panel.ts';
12
+ import { handleConfirmInput, renderConfirmLines, type ConfirmState } from './confirm-state.ts';
13
+ import { isTextBackspace, isTextForwardDelete } from '../input/delete-key-policy.ts';
12
14
  import {
13
15
  buildBodyText,
14
16
  buildEmptyState,
@@ -69,6 +71,8 @@ export class ProjectPlanningPanel extends BasePanel {
69
71
  private scrollOffset = 0;
70
72
  private selectedActionIndex = 0;
71
73
  private draftAnswer = '';
74
+ // Pending confirmation for Delete (clear draft). Null when inactive.
75
+ private clearDraftConfirm: ConfirmState<'clear-draft'> | null = null;
72
76
 
73
77
  public constructor(options: ProjectPlanningPanelOptions) {
74
78
  super('project-planning', 'Planning', 'P', 'agent');
@@ -86,6 +90,26 @@ export class ProjectPlanningPanel extends BasePanel {
86
90
 
87
91
  public handleInput(key: string): boolean {
88
92
  if (this.lastError !== null) this.clearError();
93
+
94
+ // ConfirmState gate: Delete (clear draft) requires y/n confirmation.
95
+ // handleConfirmInput absorbs all keys while a confirmation is pending.
96
+ const confirmResult = handleConfirmInput(this.clearDraftConfirm, key);
97
+ if (confirmResult === 'confirmed') {
98
+ this.draftAnswer = '';
99
+ this.clearDraftConfirm = null;
100
+ this.markDirty();
101
+ return true;
102
+ }
103
+ if (confirmResult === 'cancelled') {
104
+ this.clearDraftConfirm = null;
105
+ this.markDirty();
106
+ return true;
107
+ }
108
+ if (confirmResult === 'absorbed') {
109
+ return true;
110
+ }
111
+ // confirmResult === 'inactive': proceed with normal dispatch.
112
+
89
113
  const question = this.getCurrentQuestion();
90
114
  if (question) {
91
115
  const actions = this.getAnswerActions(question);
@@ -104,13 +128,15 @@ export class ProjectPlanningPanel extends BasePanel {
104
128
  this.submitSelectedAction(question, actions);
105
129
  return true;
106
130
  }
107
- if (key === 'backspace') {
131
+ if (isTextBackspace(key)) {
108
132
  this.draftAnswer = this.draftAnswer.slice(0, -1);
109
133
  this.markDirty();
110
134
  return true;
111
135
  }
112
- if (key === 'delete') {
113
- this.draftAnswer = '';
136
+ // 'delete' opens the clear-draft confirmation gate (per delete-key policy).
137
+ // The draft is not wiped until the user confirms with y/Enter.
138
+ if (isTextForwardDelete(key)) {
139
+ this.clearDraftConfirm = { subject: 'clear-draft', label: 'draft answer' };
114
140
  this.markDirty();
115
141
  return true;
116
142
  }
@@ -244,8 +270,10 @@ export class ProjectPlanningPanel extends BasePanel {
244
270
  [' choose answer ', C.dim],
245
271
  ['type', C.info],
246
272
  [' draft ', C.dim],
247
- ['Backspace/Delete', C.info],
273
+ ['Backspace', C.info],
248
274
  [' edit ', C.dim],
275
+ ['Del', C.info],
276
+ [' clear draft ', C.dim],
249
277
  ['Enter', C.info],
250
278
  [' submit Esc prompt focus Ctrl+X close panel', C.dim],
251
279
  ]),
@@ -284,6 +312,16 @@ export class ProjectPlanningPanel extends BasePanel {
284
312
  private buildQuestionSection(width: number, question: ProjectPlanningQuestion): RenderedPlanningSection {
285
313
  const actions = this.getAnswerActions(question);
286
314
  this.selectedActionIndex = this.clampActionIndex(actions.length);
315
+ // When a clear-draft confirmation is pending, show the confirm prompt
316
+ // inline above the draft line instead of the normal content.
317
+ if (this.clearDraftConfirm) {
318
+ const confirmLines = renderConfirmLines(width, this.clearDraftConfirm);
319
+ const lines: Line[] = [
320
+ ...buildBodyText(width, question.prompt, C, C.planning),
321
+ ...confirmLines,
322
+ ];
323
+ return { title: 'Answer Current Question', lines, selectedLineIndex: undefined };
324
+ }
287
325
  const lines: Line[] = [
288
326
  ...buildBodyText(width, question.prompt, C, C.planning),
289
327
  ];
@@ -1,3 +1,5 @@
1
+ import { isTextBackspace } from '../input/delete-key-policy.ts';
2
+
1
3
  export type PanelSearchFocusTransition = 'focus-search' | 'focus-list' | null;
2
4
 
3
5
  export function getPanelSearchFocusTransition(
@@ -6,25 +8,29 @@ export function getPanelSearchFocusTransition(
6
8
  ): PanelSearchFocusTransition {
7
9
  const focusKeys = options.focusKeys ?? ['/'];
8
10
  if (focusKeys.includes(key)) return 'focus-search';
9
- if ((key === 'up' || key === 'ArrowUp') && options.selectedIndex <= 0) {
11
+ if (key === 'up' && options.selectedIndex <= 0) {
10
12
  return 'focus-search';
11
13
  }
12
- if ((key === 'down' || key === 'ArrowDown') && options.itemCount > 0) {
14
+ if (key === 'down' && options.itemCount > 0) {
13
15
  return 'focus-list';
14
16
  }
15
17
  return null;
16
18
  }
17
19
 
20
+ // Panel search filters are end-anchored (no moveable cursor).
21
+ // Per the delete-key policy (src/input/delete-key-policy.ts):
22
+ // 'backspace' removes the last character.
23
+ // 'delete' is a no-op — there is no cursor, so forward-delete is meaningless.
18
24
  export function isPanelSearchBackspace(key: string): boolean {
19
- return key === 'backspace' || key === 'delete' || key === 'Backspace' || key === 'Delete';
25
+ return isTextBackspace(key);
20
26
  }
21
27
 
22
28
  export function isPanelSearchCancel(key: string): boolean {
23
- return key === 'escape' || key === 'Escape';
29
+ return key === 'escape';
24
30
  }
25
31
 
26
32
  export function isPanelSearchCommit(key: string): boolean {
27
- return key === 'return' || key === 'enter' || key === 'Enter';
33
+ return key === 'return' || key === 'enter';
28
34
  }
29
35
 
30
36
  export function isPanelSearchPrintable(key: string): boolean {