@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
|
@@ -40,6 +40,11 @@ export interface BootstrapShellState {
|
|
|
40
40
|
readonly lastGitInfoRef: { value: GitHeaderInfo | undefined };
|
|
41
41
|
readonly inputHistory: InputHistory;
|
|
42
42
|
readonly systemMessageRouter: SystemMessageRouter;
|
|
43
|
+
/**
|
|
44
|
+
* Wire the agent detail modal opener after InputHandler is constructed.
|
|
45
|
+
* Call with `(id) => input.agentDetailModal.open(id)` from main.ts.
|
|
46
|
+
*/
|
|
47
|
+
readonly setOpenAgentDetail: (fn: (agentId: string) => void) => void;
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
export interface BootstrapShellOptions {
|
|
@@ -103,6 +108,8 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
|
|
|
103
108
|
providerRegistry: services.providerRegistry,
|
|
104
109
|
});
|
|
105
110
|
|
|
111
|
+
const openAgentDetailRef: { fn: (agentId: string) => void } = { fn: (_agentId: string) => {} };
|
|
112
|
+
|
|
106
113
|
let commandContextRef: CommandContext | null = null;
|
|
107
114
|
registerBuiltinPanels(services.panelManager, {
|
|
108
115
|
configManager,
|
|
@@ -143,6 +150,7 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
|
|
|
143
150
|
hookActivityTracker: services.hookActivityTracker,
|
|
144
151
|
hookWorkbench: services.hookWorkbench,
|
|
145
152
|
mcpRegistry: services.mcpRegistry,
|
|
153
|
+
openAgentDetail: (agentId: string) => openAgentDetailRef.fn(agentId),
|
|
146
154
|
daemonHomeDir: join(services.homeDirectory, '.goodvibes', 'daemon'),
|
|
147
155
|
});
|
|
148
156
|
services.panelManager.prewarmRegistered();
|
|
@@ -278,5 +286,8 @@ export function createBootstrapShell(options: BootstrapShellOptions): BootstrapS
|
|
|
278
286
|
lastGitInfoRef,
|
|
279
287
|
inputHistory,
|
|
280
288
|
systemMessageRouter,
|
|
289
|
+
setOpenAgentDetail: (fn: (agentId: string) => void) => {
|
|
290
|
+
openAgentDetailRef.fn = fn;
|
|
291
|
+
},
|
|
281
292
|
};
|
|
282
293
|
}
|
package/src/runtime/bootstrap.ts
CHANGED
|
@@ -118,6 +118,11 @@ export type BootstrapContext = RuntimeContext & {
|
|
|
118
118
|
* stay out of the main conversation and go to the SystemMessagesPanel instead.
|
|
119
119
|
*/
|
|
120
120
|
systemMessageRouter: SystemMessageRouter;
|
|
121
|
+
/**
|
|
122
|
+
* Wire the agent detail modal opener after InputHandler is constructed in main.ts.
|
|
123
|
+
* Call with `(id) => input.agentDetailModal.open(id)` once the InputHandler is ready.
|
|
124
|
+
*/
|
|
125
|
+
setOpenAgentDetail: (fn: (agentId: string) => void) => void;
|
|
121
126
|
};
|
|
122
127
|
|
|
123
128
|
// ── Bootstrap function ────────────────────────────────────────────────────
|
|
@@ -292,6 +297,7 @@ export async function bootstrapRuntime(
|
|
|
292
297
|
const gitStatusProvider = shell.gitStatusProvider;
|
|
293
298
|
const inputHistory = shell.inputHistory;
|
|
294
299
|
const lastGitInfoRef = shell.lastGitInfoRef;
|
|
300
|
+
const setOpenAgentDetail = shell.setOpenAgentDetail;
|
|
295
301
|
const pluginCommandRegistry = {
|
|
296
302
|
register(command: {
|
|
297
303
|
readonly name: string;
|
|
@@ -622,6 +628,7 @@ export async function bootstrapRuntime(
|
|
|
622
628
|
_getConfiguredProviderIds: () => services.providerRegistry.getConfiguredProviderIds(),
|
|
623
629
|
commandRegistry,
|
|
624
630
|
systemMessageRouter,
|
|
631
|
+
setOpenAgentDetail,
|
|
625
632
|
shutdown: async (sessionData) => {
|
|
626
633
|
// Clear bootstrap-owned subscriptions
|
|
627
634
|
bootstrapUnsubs.forEach(fn => fn());
|
package/src/runtime/services.ts
CHANGED
|
@@ -78,6 +78,7 @@ import { ComponentHealthMonitor } from '@/runtime/index.ts';
|
|
|
78
78
|
import { WorktreeRegistry } from '@/runtime/index.ts';
|
|
79
79
|
import { SandboxSessionRegistry } from '@/runtime/index.ts';
|
|
80
80
|
import { createShellPathService, type ShellPathService } from '@/runtime/index.ts';
|
|
81
|
+
import { isFeatureFlagEnabled } from './surface-feature-flags.ts';
|
|
81
82
|
import type { FeatureFlagManager } from '@/runtime/index.ts';
|
|
82
83
|
import { createFeatureFlagManager } from '@/runtime/index.ts';
|
|
83
84
|
import { PolicyRuntimeState } from '@/runtime/index.ts';
|
|
@@ -530,7 +531,11 @@ export function createRuntimeServices(options: RuntimeServicesOptions): RuntimeS
|
|
|
530
531
|
const worktreeRegistry = new WorktreeRegistry(workingDirectory);
|
|
531
532
|
const webhookNotifier = new WebhookNotifier();
|
|
532
533
|
const replayEngine = new DeterministicReplayEngine(workingDirectory);
|
|
533
|
-
const providerOptimizer = new ProviderOptimizer(
|
|
534
|
+
const providerOptimizer = new ProviderOptimizer(
|
|
535
|
+
providerRegistry,
|
|
536
|
+
providerCapabilityRegistry,
|
|
537
|
+
isFeatureFlagEnabled(configManager, 'provider-optimizer'),
|
|
538
|
+
);
|
|
534
539
|
const sessionMemoryStore = new SessionMemoryStore();
|
|
535
540
|
const sessionLineageTracker = new SessionLineageTracker();
|
|
536
541
|
const sessionChangeTracker = new SessionChangeTracker();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* browser.ts — cross-platform browser launcher utility.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from cli/management.ts so it can be used by input-layer commands
|
|
5
|
+
* without creating an upward cli→input dependency. Lives in utils (Layer 0)
|
|
6
|
+
* and has no imports from shell-UI or entrypoint layers.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Open the given URL in the user's default browser.
|
|
14
|
+
* Returns a status string (success or error description).
|
|
15
|
+
* Does not throw — errors are returned as a descriptive string.
|
|
16
|
+
*/
|
|
17
|
+
export function openBrowser(url: string): string {
|
|
18
|
+
const platform = process.platform;
|
|
19
|
+
const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
20
|
+
const args = platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
21
|
+
try {
|
|
22
|
+
const child = spawn(command, args, { detached: true, stdio: 'ignore' });
|
|
23
|
+
child.once('error', () => {});
|
|
24
|
+
child.unref();
|
|
25
|
+
return 'browser open requested';
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return `browser open failed: ${summarizeError(error)}`;
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/version.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { join } from 'node:path';
|
|
|
6
6
|
// The prebuild script updates the fallback value before compilation.
|
|
7
7
|
// Uses import.meta.dir (Bun) to locate package.json relative to this file,
|
|
8
8
|
// which is correct regardless of the process working directory.
|
|
9
|
-
let _version = '0.
|
|
9
|
+
let _version = '0.23.0';
|
|
10
10
|
try {
|
|
11
11
|
const pkg = JSON.parse(readFileSync(join(import.meta.dir, '..', 'package.json'), 'utf-8'));
|
|
12
12
|
_version = pkg.version ?? _version;
|
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
3
|
-
import type { KeyName } from './types.ts';
|
|
4
|
-
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
5
|
-
import type { MemoryClass, MemoryRecord, MemoryRegistry, MemoryReviewState } from '@pellux/goodvibes-sdk/platform/state';
|
|
6
|
-
import {
|
|
7
|
-
buildBodyText,
|
|
8
|
-
buildEmptyState,
|
|
9
|
-
buildGuidanceLine,
|
|
10
|
-
buildKeyValueLine,
|
|
11
|
-
buildPanelLine,
|
|
12
|
-
buildPanelWorkspace,
|
|
13
|
-
DEFAULT_PANEL_PALETTE,
|
|
14
|
-
} from './polish.ts';
|
|
15
|
-
|
|
16
|
-
function summarize(records: MemoryRecord[], cls: MemoryClass): MemoryRecord[] {
|
|
17
|
-
return records.filter((record) => record.cls === cls).slice(0, 3);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const C = {
|
|
21
|
-
...DEFAULT_PANEL_PALETTE,
|
|
22
|
-
header: '#94a3b8',
|
|
23
|
-
headerBg: '#1e293b',
|
|
24
|
-
} as const;
|
|
25
|
-
|
|
26
|
-
function reviewStateColor(state: MemoryReviewState): string {
|
|
27
|
-
switch (state) {
|
|
28
|
-
case 'reviewed':
|
|
29
|
-
return C.good;
|
|
30
|
-
case 'stale':
|
|
31
|
-
return C.warn;
|
|
32
|
-
case 'contradicted':
|
|
33
|
-
return C.bad;
|
|
34
|
-
case 'fresh':
|
|
35
|
-
default:
|
|
36
|
-
return C.info;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function formatConfidence(confidence: number): string {
|
|
41
|
-
return `${confidence.toString().padStart(3, ' ')}%`;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export class KnowledgePanel extends ScrollableListPanel<MemoryRecord> {
|
|
45
|
-
private readonly registry: MemoryRegistry;
|
|
46
|
-
private unsubscribe?: () => void;
|
|
47
|
-
private records: MemoryRecord[] = [];
|
|
48
|
-
// I1: confirm for destructive review-state mutations
|
|
49
|
-
private confirm: ConfirmState<{ id: string; action: 'stale' | 'contradicted' }> | null = null;
|
|
50
|
-
|
|
51
|
-
public constructor(registry: MemoryRegistry) {
|
|
52
|
-
super('knowledge', 'Knowledge', 'K', 'agent');
|
|
53
|
-
this.registry = registry;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
public override onActivate(): void {
|
|
57
|
-
super.onActivate();
|
|
58
|
-
this.refresh();
|
|
59
|
-
this.unsubscribe = this.registry.subscribe(() => {
|
|
60
|
-
this.refresh();
|
|
61
|
-
this.markDirty();
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
public override onDeactivate(): void {
|
|
66
|
-
super.onDeactivate();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
public override onDestroy(): void {
|
|
70
|
-
this.unsubscribe?.();
|
|
71
|
-
this.unsubscribe = undefined;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
// ScrollableListPanel implementation
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
|
|
78
|
-
protected getItems(): readonly MemoryRecord[] {
|
|
79
|
-
return this.records;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
protected renderItem(record: MemoryRecord, index: number, selected: boolean, width: number): Line {
|
|
83
|
-
const bg = selected ? C.selectBg : undefined;
|
|
84
|
-
return buildPanelLine(width, [
|
|
85
|
-
[' ', C.label, bg],
|
|
86
|
-
[record.reviewState.padEnd(13), reviewStateColor(record.reviewState), bg],
|
|
87
|
-
[` ${formatConfidence(record.confidence)} `, C.value, bg],
|
|
88
|
-
[record.summary.slice(0, Math.max(0, width - 26)), C.value, bg],
|
|
89
|
-
]);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
protected override getPalette() { return C; }
|
|
93
|
-
protected override getEmptyStateMessage() { return 'No durable project knowledge'; }
|
|
94
|
-
protected override getEmptyStateActions() {
|
|
95
|
-
return [
|
|
96
|
-
{ command: '/recall add fact <summary>', summary: 'capture a durable fact directly' },
|
|
97
|
-
{ command: '/recall capture incident latest', summary: 'promote the latest incident into project memory' },
|
|
98
|
-
{ command: '/recall capture policy', summary: 'store the current policy posture as durable evidence' },
|
|
99
|
-
];
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ---------------------------------------------------------------------------
|
|
103
|
-
// Input
|
|
104
|
-
// ---------------------------------------------------------------------------
|
|
105
|
-
|
|
106
|
-
public handleInput(key: KeyName): boolean {
|
|
107
|
-
// I1: y/n confirm for stale/contradict
|
|
108
|
-
if (this.confirm) {
|
|
109
|
-
const result = handleConfirmInput(this.confirm, key);
|
|
110
|
-
if (result === 'confirmed') {
|
|
111
|
-
const { id, action } = this.confirm.subject;
|
|
112
|
-
this.confirm = null;
|
|
113
|
-
const selected = this.records.find((r) => r.id === id);
|
|
114
|
-
if (selected) {
|
|
115
|
-
try {
|
|
116
|
-
if (action === 'stale') {
|
|
117
|
-
this.registry.review(id, {
|
|
118
|
-
state: 'stale',
|
|
119
|
-
confidence: Math.min(selected.confidence, 40),
|
|
120
|
-
reviewedBy: 'operator',
|
|
121
|
-
staleReason: 'marked stale from the knowledge panel',
|
|
122
|
-
});
|
|
123
|
-
} else {
|
|
124
|
-
this.registry.review(id, {
|
|
125
|
-
state: 'contradicted',
|
|
126
|
-
confidence: 0,
|
|
127
|
-
reviewedBy: 'operator',
|
|
128
|
-
staleReason: 'marked contradicted from the knowledge panel',
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
} catch (e) {
|
|
132
|
-
// I2: surface async failure
|
|
133
|
-
this.setError(`Review update failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
this.refresh();
|
|
137
|
-
this.markDirty();
|
|
138
|
-
return true;
|
|
139
|
-
}
|
|
140
|
-
if (result === 'cancelled') {
|
|
141
|
-
this.confirm = null;
|
|
142
|
-
this.markDirty();
|
|
143
|
-
return true;
|
|
144
|
-
}
|
|
145
|
-
if (result === 'absorbed') return true;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// I2: auto-clear error on next keypress (inherited via super.handleInput)
|
|
149
|
-
if (this.records.length === 0) return super.handleInput(key);
|
|
150
|
-
|
|
151
|
-
const selected = this.records[this.selectedIndex];
|
|
152
|
-
|
|
153
|
-
if (key === 'enter' || key === 'return' || key === 'r') {
|
|
154
|
-
if (!selected) return false;
|
|
155
|
-
this.registry.review(selected.id, {
|
|
156
|
-
state: 'reviewed',
|
|
157
|
-
confidence: Math.max(selected.confidence, 85),
|
|
158
|
-
reviewedBy: 'operator',
|
|
159
|
-
});
|
|
160
|
-
this.refresh();
|
|
161
|
-
this.markDirty();
|
|
162
|
-
return true;
|
|
163
|
-
}
|
|
164
|
-
if (key === 's') {
|
|
165
|
-
if (!selected) return false;
|
|
166
|
-
// I1: prompt confirm before marking stale
|
|
167
|
-
this.confirm = { subject: { id: selected.id, action: 'stale' }, label: selected.summary.slice(0, 40) };
|
|
168
|
-
this.markDirty();
|
|
169
|
-
return true;
|
|
170
|
-
}
|
|
171
|
-
if (key === 'c') {
|
|
172
|
-
if (!selected) return false;
|
|
173
|
-
// I1: prompt confirm before marking contradicted
|
|
174
|
-
this.confirm = { subject: { id: selected.id, action: 'contradicted' }, label: selected.summary.slice(0, 40) };
|
|
175
|
-
this.markDirty();
|
|
176
|
-
return true;
|
|
177
|
-
}
|
|
178
|
-
if (key === 'f') {
|
|
179
|
-
if (!selected) return false;
|
|
180
|
-
this.registry.review(selected.id, {
|
|
181
|
-
state: 'fresh',
|
|
182
|
-
confidence: Math.max(selected.confidence, 60),
|
|
183
|
-
reviewedBy: 'operator',
|
|
184
|
-
});
|
|
185
|
-
this.refresh();
|
|
186
|
-
this.markDirty();
|
|
187
|
-
return true;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return super.handleInput(key);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
private refresh(): void {
|
|
194
|
-
const queue = this.registry.reviewQueue(24);
|
|
195
|
-
this.records = queue.length > 0 ? queue : this.registry.search({ limit: 24 });
|
|
196
|
-
this.clampSelection();
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
public render(width: number, height: number): Line[] {
|
|
200
|
-
this.clampSelection();
|
|
201
|
-
|
|
202
|
-
// I1: show confirm dialog in place of normal content
|
|
203
|
-
if (this.confirm) {
|
|
204
|
-
return buildPanelWorkspace(width, height, {
|
|
205
|
-
title: 'Knowledge Control Room',
|
|
206
|
-
intro: '',
|
|
207
|
-
sections: [{ title: 'Confirmation', lines: renderConfirmLines(width, this.confirm) }],
|
|
208
|
-
footerLines: [buildPanelLine(width, [[' y confirm n / Esc cancel', C.dim]])],
|
|
209
|
-
palette: C,
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
if (this.records.length === 0) this.refresh();
|
|
214
|
-
|
|
215
|
-
const intro = 'Typed project knowledge, reviewed evidence, and operator-governed memory across session, project, and team scopes.';
|
|
216
|
-
const records = this.registry.search({ limit: 200 });
|
|
217
|
-
|
|
218
|
-
if (records.length === 0) {
|
|
219
|
-
return this.renderList(width, height, {
|
|
220
|
-
title: 'Knowledge Control Room',
|
|
221
|
-
footer: [buildPanelLine(width, [[' Review keys: Up/Down move r/Enter review s stale c contradicted f fresh', C.dim]])],
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const queue = this.registry.reviewQueue(24);
|
|
226
|
-
const byClass = new Map<MemoryClass, number>();
|
|
227
|
-
const byReview = new Map<MemoryReviewState, number>();
|
|
228
|
-
const byScope = new Map<string, number>();
|
|
229
|
-
for (const record of records) {
|
|
230
|
-
byClass.set(record.cls, (byClass.get(record.cls) ?? 0) + 1);
|
|
231
|
-
byReview.set(record.reviewState, (byReview.get(record.reviewState) ?? 0) + 1);
|
|
232
|
-
byScope.set(record.scope, (byScope.get(record.scope) ?? 0) + 1);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const classLines: Line[] = [
|
|
236
|
-
buildPanelLine(width, [
|
|
237
|
-
[' facts ', C.label], [String(byClass.get('fact') ?? 0), C.good],
|
|
238
|
-
[' risks ', C.label], [String(byClass.get('risk') ?? 0), (byClass.get('risk') ?? 0) > 0 ? C.warn : C.good],
|
|
239
|
-
[' runbooks ', C.label], [String(byClass.get('runbook') ?? 0), C.info],
|
|
240
|
-
[' architecture ', C.label], [String(byClass.get('architecture') ?? 0), C.info],
|
|
241
|
-
[' incidents ', C.label], [String(byClass.get('incident') ?? 0), (byClass.get('incident') ?? 0) > 0 ? C.bad : C.good],
|
|
242
|
-
]),
|
|
243
|
-
buildPanelLine(width, [
|
|
244
|
-
[' decisions ', C.label], [String(byClass.get('decision') ?? 0), C.value],
|
|
245
|
-
[' constraints ', C.label], [String(byClass.get('constraint') ?? 0), C.value],
|
|
246
|
-
[' ownership ', C.label], [String(byClass.get('ownership') ?? 0), C.value],
|
|
247
|
-
[' patterns ', C.label], [String(byClass.get('pattern') ?? 0), C.value],
|
|
248
|
-
[' total ', C.label], [String(records.length), C.value],
|
|
249
|
-
]),
|
|
250
|
-
];
|
|
251
|
-
|
|
252
|
-
const reviewLines: Line[] = [
|
|
253
|
-
buildPanelLine(width, [
|
|
254
|
-
[' reviewed ', C.label], [String(byReview.get('reviewed') ?? 0), C.good],
|
|
255
|
-
[' fresh ', C.label], [String(byReview.get('fresh') ?? 0), C.info],
|
|
256
|
-
[' stale ', C.label], [String(byReview.get('stale') ?? 0), C.warn],
|
|
257
|
-
[' contradicted ', C.label], [String(byReview.get('contradicted') ?? 0), C.bad],
|
|
258
|
-
[' Review Queue ', C.label], [String(queue.length), queue.length > 0 ? C.warn : C.good],
|
|
259
|
-
]),
|
|
260
|
-
buildPanelLine(width, [
|
|
261
|
-
[' session ', C.label], [String(byScope.get('session') ?? 0), C.info],
|
|
262
|
-
[' project ', C.label], [String(byScope.get('project') ?? 0), C.value],
|
|
263
|
-
[' team ', C.label], [String(byScope.get('team') ?? 0), C.good],
|
|
264
|
-
]),
|
|
265
|
-
buildGuidanceLine(width, '/recall review', 'work the stale and contradicted queue from the command surface', C),
|
|
266
|
-
];
|
|
267
|
-
|
|
268
|
-
const recentSummaryLines: Line[] = [];
|
|
269
|
-
for (const [title, items, color] of [
|
|
270
|
-
['Recent Risks', summarize(records, 'risk'), C.warn],
|
|
271
|
-
['Runbooks', summarize(records, 'runbook'), C.info],
|
|
272
|
-
['Architecture Notes', summarize(records, 'architecture'), C.info],
|
|
273
|
-
['Recent Incidents', summarize(records, 'incident'), C.bad],
|
|
274
|
-
] as const) {
|
|
275
|
-
if (recentSummaryLines.length > 0) {
|
|
276
|
-
recentSummaryLines.push(buildPanelLine(width, [['', C.dim]]));
|
|
277
|
-
}
|
|
278
|
-
recentSummaryLines.push(buildPanelLine(width, [[` ${title}`, C.label]]));
|
|
279
|
-
if (items.length === 0) {
|
|
280
|
-
recentSummaryLines.push(buildPanelLine(width, [[' none recorded', C.dim]]));
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
for (const item of items) {
|
|
284
|
-
recentSummaryLines.push(buildPanelLine(width, [
|
|
285
|
-
[' ', C.label],
|
|
286
|
-
[item.summary.slice(0, Math.max(0, width - 2)), color],
|
|
287
|
-
]));
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const selectedRecord = this.records[this.selectedIndex];
|
|
292
|
-
const selectedLines: Line[] = [];
|
|
293
|
-
if (selectedRecord) {
|
|
294
|
-
selectedLines.push(buildPanelLine(width, [[' Selected', C.label]]));
|
|
295
|
-
selectedLines.push(buildKeyValueLine(width, [
|
|
296
|
-
{ label: 'Class', value: selectedRecord.cls, valueColor: C.value },
|
|
297
|
-
{ label: 'Scope', value: selectedRecord.scope, valueColor: C.info },
|
|
298
|
-
{ label: 'Review', value: selectedRecord.reviewState, valueColor: reviewStateColor(selectedRecord.reviewState) },
|
|
299
|
-
{ label: 'Confidence', value: formatConfidence(selectedRecord.confidence), valueColor: C.value },
|
|
300
|
-
], C));
|
|
301
|
-
selectedLines.push(...buildBodyText(width, `Summary: ${selectedRecord.summary}`, C, C.value));
|
|
302
|
-
if (selectedRecord.detail) selectedLines.push(...buildBodyText(width, `Detail: ${selectedRecord.detail}`, C, C.dim));
|
|
303
|
-
if (selectedRecord.provenance.length) {
|
|
304
|
-
selectedLines.push(...buildBodyText(
|
|
305
|
-
width,
|
|
306
|
-
`Provenance: ${selectedRecord.provenance.map((p) => `${p.kind}:${p.ref}`).join(', ')}`,
|
|
307
|
-
C,
|
|
308
|
-
C.dim,
|
|
309
|
-
));
|
|
310
|
-
}
|
|
311
|
-
if (selectedRecord.staleReason) {
|
|
312
|
-
selectedLines.push(...buildBodyText(
|
|
313
|
-
width,
|
|
314
|
-
`Stale reason: ${selectedRecord.staleReason}`,
|
|
315
|
-
C,
|
|
316
|
-
selectedRecord.reviewState === 'contradicted' ? C.bad : C.warn,
|
|
317
|
-
));
|
|
318
|
-
}
|
|
319
|
-
if (selectedRecord.reviewedAt) {
|
|
320
|
-
selectedLines.push(buildPanelLine(width, [
|
|
321
|
-
[' Reviewed: ', C.label],
|
|
322
|
-
[new Date(selectedRecord.reviewedAt).toLocaleString(), C.dim],
|
|
323
|
-
]));
|
|
324
|
-
if (selectedRecord.reviewedBy) {
|
|
325
|
-
selectedLines.push(buildPanelLine(width, [
|
|
326
|
-
[' Reviewer: ', C.label],
|
|
327
|
-
[selectedRecord.reviewedBy, C.dim],
|
|
328
|
-
]));
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return this.renderList(width, height, {
|
|
334
|
-
title: 'Knowledge Control Room',
|
|
335
|
-
header: [...classLines, ...reviewLines],
|
|
336
|
-
footer: [
|
|
337
|
-
...(selectedLines.length > 0 ? selectedLines : []),
|
|
338
|
-
...recentSummaryLines,
|
|
339
|
-
buildPanelLine(width, [[' Up/Down move r/Enter reviewed s stale c contradicted f fresh', C.dim]]),
|
|
340
|
-
],
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
}
|