@pellux/goodvibes-tui 0.21.0 → 0.23.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 +45 -0
- package/README.md +1 -1
- package/package.json +2 -1
- package/src/cli/completions/generate.ts +4 -8
- package/src/cli/entrypoint.ts +6 -0
- package/src/cli/management-commands.ts +1 -1
- package/src/cli/management-utils.ts +352 -0
- package/src/cli/management.ts +36 -334
- package/src/cli/parser.ts +17 -0
- package/src/cli/surface-command.ts +1 -1
- package/src/cli/types.ts +2 -0
- package/src/config/goodvibes-home-audit.ts +2 -0
- package/src/core/context-auto-compact.ts +110 -0
- package/src/core/conversation-rendering.ts +5 -2
- package/src/core/conversation-types.ts +24 -0
- package/src/core/conversation.ts +7 -12
- package/src/core/stream-event-wiring.ts +125 -7
- package/src/core/turn-event-wiring.ts +124 -0
- package/src/daemon/cli.ts +5 -0
- package/src/input/command-registry.ts +1 -0
- package/src/input/commands/channel-runtime.ts +139 -0
- package/src/input/commands/control-room-runtime.ts +5 -5
- package/src/input/commands/provider.ts +57 -3
- package/src/input/commands/runtime-services.ts +30 -1
- package/src/input/commands/session-workflow.ts +8 -16
- package/src/input/commands/session.ts +70 -20
- package/src/input/commands/share-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +54 -4
- package/src/input/commands.ts +2 -2
- package/src/input/handler-modal-routes.ts +37 -0
- package/src/input/handler-modal-token-routes.ts +19 -5
- package/src/input/handler-onboarding.ts +18 -0
- package/src/input/handler.ts +1 -0
- package/src/input/onboarding/onboarding-wizard-apply.ts +10 -0
- package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +14 -0
- package/src/input/onboarding/onboarding-wizard-steps.ts +6 -0
- package/src/input/onboarding/onboarding-wizard-validation.ts +77 -0
- package/src/input/settings-modal-behavior.ts +5 -0
- package/src/input/settings-modal-data.ts +77 -3
- package/src/input/settings-modal-mutations.ts +3 -0
- package/src/input/settings-modal-reset.ts +154 -0
- package/src/input/settings-modal.ts +55 -13
- package/src/main.ts +58 -50
- package/src/panels/agent-inspector-panel.ts +120 -18
- package/src/panels/agent-inspector-shared.ts +29 -0
- package/src/panels/builtin/development.ts +1 -0
- 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/index.ts +1 -1
- package/src/panels/knowledge-graph-panel.ts +84 -0
- package/src/panels/memory-panel.ts +370 -40
- package/src/panels/session-maintenance.ts +66 -15
- package/src/renderer/agent-detail-modal.ts +107 -3
- package/src/renderer/compaction-history-modal.ts +55 -0
- package/src/renderer/compaction-preview.ts +146 -0
- package/src/renderer/context-status-hint.ts +54 -0
- package/src/renderer/settings-modal-helpers.ts +2 -2
- package/src/renderer/settings-modal.ts +14 -3
- package/src/renderer/shell-surface.ts +10 -0
- package/src/runtime/bootstrap-command-parts.ts +4 -0
- package/src/runtime/bootstrap-core.ts +116 -0
- package/src/runtime/bootstrap-shell.ts +11 -0
- package/src/runtime/bootstrap.ts +7 -0
- package/src/runtime/services.ts +6 -1
- package/src/utils/browser.ts +29 -0
- package/src/version.ts +1 -1
- package/src/panels/knowledge-panel.ts +0 -343
|
@@ -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
|
}
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session maintenance types and local evaluator.
|
|
3
|
+
*
|
|
4
|
+
* The canonical evaluator is the SDK version exported from @/runtime/index.ts
|
|
5
|
+
* (via operations.evaluateSessionMaintenance), which reads from configManager.
|
|
6
|
+
*
|
|
7
|
+
* This module provides:
|
|
8
|
+
* - The shared type surface used across TUI panels.
|
|
9
|
+
* - A thin local evaluator kept in sync with the SDK signature so panel code
|
|
10
|
+
* that passes configManager has a coherent call site.
|
|
11
|
+
*/
|
|
12
|
+
import type { ConfigManager } from '@pellux/goodvibes-sdk/platform/config';
|
|
13
|
+
|
|
1
14
|
export type PanelGuidanceMode = 'off' | 'minimal' | 'guided';
|
|
2
15
|
export type PanelSessionMaintenanceLevel = 'stable' | 'watch' | 'suggest-compact' | 'compacting' | 'needs-repair' | 'unknown';
|
|
3
16
|
|
|
@@ -9,9 +22,12 @@ export interface PanelSessionMaintenanceSession {
|
|
|
9
22
|
readonly lineage?: readonly PanelSessionMaintenanceLineageEntry[];
|
|
10
23
|
readonly lastCompactedAt?: number;
|
|
11
24
|
readonly compactionMessageCount?: number;
|
|
25
|
+
readonly compactionState?: string;
|
|
12
26
|
}
|
|
13
27
|
|
|
14
28
|
export interface PanelSessionMaintenanceInput {
|
|
29
|
+
/** ConfigManager used to read behavior.autoCompactThreshold and related keys. */
|
|
30
|
+
readonly configManager: Pick<ConfigManager, 'get'>;
|
|
15
31
|
readonly currentTokens: number;
|
|
16
32
|
readonly contextWindow: number;
|
|
17
33
|
readonly messageCount?: number;
|
|
@@ -27,7 +43,15 @@ export interface PanelSessionMaintenanceStatus {
|
|
|
27
43
|
readonly guidanceMode: PanelGuidanceMode;
|
|
28
44
|
readonly usagePct: number;
|
|
29
45
|
readonly remainingTokens: number;
|
|
46
|
+
/**
|
|
47
|
+
* Compact threshold as a percent integer.
|
|
48
|
+
* SDK schema range is [10, 100]; default is 80.
|
|
49
|
+
*/
|
|
30
50
|
readonly thresholdPct: number;
|
|
51
|
+
/**
|
|
52
|
+
* True when autoCompactThreshold is in its valid range (>0).
|
|
53
|
+
* With SDK default (80), auto-compact is active unless explicitly lowered below 10.
|
|
54
|
+
*/
|
|
31
55
|
readonly autoCompactEnabled: boolean;
|
|
32
56
|
readonly sessionMemoryCount: number;
|
|
33
57
|
readonly compactionCount: number;
|
|
@@ -35,19 +59,33 @@ export interface PanelSessionMaintenanceStatus {
|
|
|
35
59
|
readonly compactRecommended: boolean;
|
|
36
60
|
}
|
|
37
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Evaluate session maintenance from config-driven thresholds.
|
|
64
|
+
*
|
|
65
|
+
* behavior.autoCompactThreshold (percent integer, SDK schema range [10, 100], default 80):
|
|
66
|
+
* - [10, 100] → threshold at that percent; autoCompactEnabled = true.
|
|
67
|
+
* - 0 (defensive fallback for null/missing config only; not a valid schema value).
|
|
68
|
+
*
|
|
69
|
+
* NOTE: The SDK's evaluateSessionMaintenance (from @/runtime/index.ts) is the
|
|
70
|
+
* canonical implementation used in production. This local version exists so
|
|
71
|
+
* panel tests can import types without crossing the SDK boundary.
|
|
72
|
+
*/
|
|
38
73
|
export function evaluateSessionMaintenance(input: PanelSessionMaintenanceInput): PanelSessionMaintenanceStatus {
|
|
39
|
-
const guidanceMode: PanelGuidanceMode = 'minimal';
|
|
40
|
-
const
|
|
41
|
-
const
|
|
74
|
+
const guidanceMode: PanelGuidanceMode = (input.configManager.get('behavior.guidanceMode') as PanelGuidanceMode | undefined) ?? 'minimal';
|
|
75
|
+
const rawThreshold = Number(input.configManager.get('behavior.autoCompactThreshold') ?? 0);
|
|
76
|
+
const thresholdPct = Math.max(0, Number.isFinite(rawThreshold) ? rawThreshold : 0);
|
|
77
|
+
const autoCompactEnabled = thresholdPct > 0;
|
|
78
|
+
|
|
42
79
|
const usagePct = input.contextWindow > 0 ? Math.min(100, Math.round((Math.max(0, input.currentTokens) / input.contextWindow) * 100)) : 0;
|
|
43
80
|
const remainingTokens = Math.max(0, input.contextWindow - input.currentTokens);
|
|
44
81
|
const sessionMemoryCount = Math.max(0, input.sessionMemoryCount ?? 0);
|
|
45
82
|
const compactionCount = Math.max(0, input.session?.lineage?.filter((entry) => entry.branchReason === 'compaction').length ?? 0);
|
|
46
83
|
const lastCompactedAt = input.session?.lastCompactedAt;
|
|
47
84
|
const messageCount = Math.max(0, input.messageCount ?? 0);
|
|
48
|
-
const staleByMessageGrowth =
|
|
85
|
+
const staleByMessageGrowth = (input.session?.compactionMessageCount ?? 0) > 0
|
|
49
86
|
? messageCount - (input.session?.compactionMessageCount ?? 0) >= 12
|
|
50
87
|
: messageCount >= 24;
|
|
88
|
+
|
|
51
89
|
if (input.contextWindow <= 0) {
|
|
52
90
|
return {
|
|
53
91
|
level: 'unknown',
|
|
@@ -66,32 +104,45 @@ export function evaluateSessionMaintenance(input: PanelSessionMaintenanceInput):
|
|
|
66
104
|
};
|
|
67
105
|
}
|
|
68
106
|
|
|
107
|
+
if (input.session?.compactionState === 'failed') {
|
|
108
|
+
return {
|
|
109
|
+
level: 'needs-repair',
|
|
110
|
+
summary: 'Compaction needs operator repair.',
|
|
111
|
+
reasons: ['Compaction failed and the session may need manual recovery.'],
|
|
112
|
+
nextSteps: ['/compact', '/health review'],
|
|
113
|
+
guidanceMode,
|
|
114
|
+
usagePct,
|
|
115
|
+
remainingTokens,
|
|
116
|
+
thresholdPct,
|
|
117
|
+
autoCompactEnabled,
|
|
118
|
+
sessionMemoryCount,
|
|
119
|
+
compactionCount,
|
|
120
|
+
lastCompactedAt,
|
|
121
|
+
compactRecommended: true,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
69
125
|
const reasons: string[] = [];
|
|
70
126
|
const nextSteps: string[] = [];
|
|
71
127
|
let summary = 'Session maintenance is stable.';
|
|
72
128
|
let compactRecommended = false;
|
|
73
129
|
let level: PanelSessionMaintenanceLevel = 'stable';
|
|
74
130
|
|
|
75
|
-
|
|
76
|
-
|
|
131
|
+
const atThreshold = autoCompactEnabled ? usagePct >= thresholdPct : usagePct >= 80;
|
|
132
|
+
if (atThreshold || remainingTokens <= 15_000) {
|
|
133
|
+
level = 'suggest-compact';
|
|
77
134
|
summary = `Compact now to recover context headroom (${usagePct}% used).`;
|
|
78
135
|
reasons.push(`Context pressure is high at ${usagePct}% usage.`);
|
|
79
136
|
nextSteps.push('/compact', '/panel tokens');
|
|
80
137
|
compactRecommended = true;
|
|
81
|
-
} else if (usagePct >= thresholdPct
|
|
82
|
-
level = 'suggest-compact';
|
|
83
|
-
summary = `Watch context growth (${usagePct}% used).`;
|
|
84
|
-
reasons.push(`Context pressure is climbing at ${usagePct}% usage.`);
|
|
85
|
-
nextSteps.push('/panel tokens');
|
|
86
|
-
compactRecommended = true;
|
|
87
|
-
} else if (usagePct >= 70 || staleByMessageGrowth) {
|
|
138
|
+
} else if (usagePct >= Math.max(70, autoCompactEnabled ? thresholdPct - 10 : 70) || staleByMessageGrowth) {
|
|
88
139
|
level = 'watch';
|
|
89
140
|
summary = staleByMessageGrowth
|
|
90
141
|
? `Conversation has grown ${messageCount.toLocaleString()} messages since the last maintenance checkpoint.`
|
|
91
142
|
: `Watch context growth (${usagePct}% used, threshold ${thresholdPct}%).`;
|
|
92
143
|
reasons.push(staleByMessageGrowth
|
|
93
144
|
? `Conversation has grown ${messageCount.toLocaleString()} messages since the last maintenance checkpoint.`
|
|
94
|
-
: `Context usage is climbing toward the ${thresholdPct}% maintenance
|
|
145
|
+
: `Context usage is climbing toward the ${thresholdPct > 0 ? `${thresholdPct}% auto-compact threshold` : 'maintenance band'}.`);
|
|
95
146
|
nextSteps.push('/panel tokens');
|
|
96
147
|
} else {
|
|
97
148
|
reasons.push('Context pressure is currently within the stable operating band.');
|
|
@@ -104,7 +155,7 @@ export function evaluateSessionMaintenance(input: PanelSessionMaintenanceInput):
|
|
|
104
155
|
reasons.push(`Last compaction ran ${lastCompactedAt ? new Date(lastCompactedAt).toISOString() : 'recently'}.`);
|
|
105
156
|
}
|
|
106
157
|
if (!autoCompactEnabled) {
|
|
107
|
-
reasons.push('Auto-
|
|
158
|
+
reasons.push('Auto-compaction is disabled; maintenance stays fully manual.');
|
|
108
159
|
}
|
|
109
160
|
|
|
110
161
|
return {
|