@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.
- package/CHANGELOG.md +50 -0
- package/README.md +23 -2
- package/docs/foundation-artifacts/operator-contract.json +78 -1
- package/package.json +4 -2
- package/src/audio/spoken-turn-controller.ts +31 -1
- package/src/audio/spoken-turn-wiring.ts +26 -4
- package/src/cli/bundle-command.ts +1 -1
- package/src/cli/completions/generate.ts +658 -0
- package/src/cli/config-overrides.ts +68 -0
- package/src/cli/entrypoint.ts +6 -0
- package/src/cli/help.ts +4 -2
- package/src/cli/management-commands.ts +1 -1
- package/src/cli/management.ts +1 -8
- package/src/cli/parser.ts +31 -18
- package/src/cli/service-command.ts +1 -1
- package/src/cli/surface-command.ts +1 -1
- package/src/cli/tui-startup.ts +72 -10
- package/src/cli/types.ts +14 -3
- package/src/cli-flags.ts +1 -0
- package/src/config/atomic-write.ts +70 -0
- package/src/config/goodvibes-home-audit.ts +2 -0
- package/src/config/read-versioned.ts +115 -0
- package/src/core/context-auto-compact.ts +77 -0
- package/src/core/conversation-rendering.ts +49 -15
- package/src/core/conversation.ts +101 -16
- package/src/core/format-user-error.ts +192 -0
- package/src/core/stream-event-wiring.ts +144 -0
- package/src/core/stream-stall-watchdog.ts +103 -0
- package/src/core/system-message-router.ts +5 -1
- package/src/core/turn-event-wiring.ts +124 -0
- package/src/daemon/cli.ts +5 -0
- package/src/export/cost-utils.ts +71 -0
- package/src/export/gist-uploader.ts +136 -0
- package/src/input/command-registry.ts +32 -1
- package/src/input/commands/control-room-runtime.ts +10 -10
- package/src/input/commands/experience-runtime.ts +5 -4
- package/src/input/commands/knowledge.ts +1 -1
- package/src/input/commands/local-auth-runtime.ts +27 -5
- package/src/input/commands/local-setup.ts +4 -6
- package/src/input/commands/memory-product-runtime.ts +8 -6
- package/src/input/commands/operator-panel-runtime.ts +1 -1
- package/src/input/commands/operator-runtime.ts +3 -10
- package/src/input/commands/{integration-runtime.ts → plugin-runtime.ts} +1 -1
- package/src/input/commands/provider.ts +57 -3
- package/src/input/commands/recall-review.ts +26 -2
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-workflow.ts +8 -16
- package/src/input/commands/session.ts +70 -20
- package/src/input/commands/share-runtime.ts +99 -12
- package/src/input/commands/tts-runtime.ts +30 -4
- package/src/input/commands.ts +2 -4
- package/src/input/delete-key-policy.ts +46 -0
- package/src/input/feed-context-factory.ts +2 -0
- package/src/input/handler-feed.ts +3 -0
- package/src/input/handler-interactions.ts +2 -15
- package/src/input/handler-modal-routes.ts +128 -12
- package/src/input/handler-modal-token-routes.ts +22 -5
- package/src/input/handler-onboarding-cloudflare.ts +1 -1
- package/src/input/handler-onboarding.ts +73 -69
- package/src/input/handler-types.ts +163 -0
- package/src/input/handler.ts +6 -2
- package/src/input/input-history.ts +76 -6
- package/src/input/model-picker-filter.ts +265 -0
- package/src/input/model-picker-items.ts +208 -0
- package/src/input/model-picker.ts +92 -325
- package/src/input/onboarding/handler-onboarding-routes.ts +7 -2
- package/src/input/onboarding/onboarding-verification-helpers.ts +76 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +14 -4
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +16 -2
- package/src/input/onboarding/onboarding-wizard-cloudflare.ts +8 -8
- package/src/input/onboarding/onboarding-wizard-external-surface-extra-specs.ts +1 -1
- package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +2 -29
- package/src/input/onboarding/onboarding-wizard-rules.ts +28 -28
- package/src/input/onboarding/onboarding-wizard-state.ts +20 -20
- package/src/input/onboarding/onboarding-wizard-steps.ts +24 -25
- package/src/input/onboarding/onboarding-wizard-types.ts +145 -3
- package/src/input/onboarding/onboarding-wizard-validation.ts +77 -0
- package/src/input/onboarding/onboarding-wizard.ts +3 -3
- package/src/input/settings-modal-behavior.ts +5 -0
- package/src/input/settings-modal-data.ts +378 -0
- package/src/input/settings-modal-mutations.ts +157 -0
- package/src/input/settings-modal-reset.ts +154 -0
- package/src/input/settings-modal.ts +236 -232
- package/src/main.ts +93 -85
- package/src/panels/agent-inspector-panel.ts +120 -18
- package/src/panels/agent-inspector-shared.ts +29 -0
- package/src/panels/builtin/agent.ts +4 -1
- package/src/panels/builtin/development.ts +5 -1
- package/src/panels/builtin/knowledge.ts +14 -13
- package/src/panels/builtin/operations.ts +22 -1
- package/src/panels/builtin/shared.ts +7 -0
- package/src/panels/cockpit-panel.ts +123 -3
- package/src/panels/cockpit-read-model.ts +232 -0
- package/src/panels/confirm-state.ts +27 -12
- package/src/panels/cost-tracker-panel.ts +23 -67
- package/src/panels/eval-panel.ts +10 -9
- package/src/panels/index.ts +1 -1
- package/src/panels/knowledge-graph-panel.ts +84 -0
- package/src/panels/local-auth-panel.ts +124 -4
- package/src/panels/memory-panel.ts +370 -40
- package/src/panels/project-planning-panel.ts +42 -4
- package/src/panels/search-focus.ts +11 -5
- package/src/panels/session-maintenance.ts +66 -15
- package/src/panels/subscription-panel.ts +33 -25
- package/src/panels/types.ts +28 -1
- package/src/panels/wrfc-panel.ts +224 -41
- package/src/renderer/agent-detail-modal.ts +118 -13
- package/src/renderer/code-block.ts +10 -2
- package/src/renderer/compositor.ts +18 -4
- package/src/renderer/context-inspector.ts +1 -5
- package/src/renderer/context-status-hint.ts +54 -0
- package/src/renderer/diff.ts +94 -21
- package/src/renderer/markdown.ts +29 -13
- package/src/renderer/settings-modal-helpers.ts +1 -1
- package/src/renderer/settings-modal.ts +90 -10
- package/src/renderer/shell-surface.ts +10 -0
- package/src/renderer/syntax-highlighter.ts +10 -3
- package/src/renderer/term-caps.ts +318 -0
- package/src/renderer/theme.ts +158 -0
- package/src/renderer/tool-call.ts +12 -2
- package/src/renderer/ui-factory.ts +50 -6
- package/src/runtime/bootstrap-command-context.ts +1 -0
- package/src/runtime/bootstrap-command-parts.ts +18 -0
- package/src/runtime/bootstrap-core.ts +145 -13
- package/src/runtime/bootstrap-shell.ts +11 -0
- package/src/runtime/bootstrap.ts +9 -0
- package/src/runtime/onboarding/apply.ts +4 -6
- package/src/runtime/onboarding/index.ts +1 -0
- package/src/runtime/onboarding/markers.ts +42 -49
- package/src/runtime/onboarding/progress.ts +148 -0
- package/src/runtime/onboarding/state.ts +133 -55
- package/src/runtime/onboarding/types.ts +20 -0
- package/src/runtime/services.ts +27 -1
- package/src/runtime/wrfc-persistence.ts +237 -0
- package/src/shell/blocking-input.ts +20 -5
- package/src/tools/wrfc-agent-guard.ts +64 -3
- package/src/utils/format-elapsed.ts +30 -0
- package/src/utils/terminal-width.ts +45 -0
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +4 -6
- package/src/panels/knowledge-panel.ts +0 -345
- package/src/planning/project-planning-coordinator.ts +0 -543
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MemoryPanel — project memory substrate
|
|
2
|
+
* MemoryPanel — merged project memory substrate panel.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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,
|
|
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':
|
|
49
|
-
case 'constraint':
|
|
50
|
-
case 'incident':
|
|
51
|
-
case 'pattern':
|
|
52
|
-
case 'fact':
|
|
53
|
-
case 'risk':
|
|
54
|
-
case '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':
|
|
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
|
-
|
|
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
|
-
//
|
|
139
|
-
if (this.
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
this.
|
|
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 (
|
|
148
|
-
this.
|
|
149
|
-
|
|
277
|
+
if (result === 'cancelled') {
|
|
278
|
+
this.confirm = null;
|
|
279
|
+
this.markDirty();
|
|
280
|
+
return true;
|
|
150
281
|
}
|
|
151
|
-
|
|
282
|
+
if (result === 'absorbed') return true;
|
|
152
283
|
}
|
|
153
284
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
this.
|
|
165
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
131
|
+
if (isTextBackspace(key)) {
|
|
108
132
|
this.draftAnswer = this.draftAnswer.slice(0, -1);
|
|
109
133
|
this.markDirty();
|
|
110
134
|
return true;
|
|
111
135
|
}
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
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 (
|
|
11
|
+
if (key === 'up' && options.selectedIndex <= 0) {
|
|
10
12
|
return 'focus-search';
|
|
11
13
|
}
|
|
12
|
-
if (
|
|
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
|
|
25
|
+
return isTextBackspace(key);
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
export function isPanelSearchCancel(key: string): boolean {
|
|
23
|
-
return key === 'escape'
|
|
29
|
+
return key === 'escape';
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
export function isPanelSearchCommit(key: string): boolean {
|
|
27
|
-
return key === 'return' || key === 'enter'
|
|
33
|
+
return key === 'return' || key === 'enter';
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
export function isPanelSearchPrintable(key: string): boolean {
|