@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
|
@@ -40,8 +40,30 @@ type AgentTaskArgs = {
|
|
|
40
40
|
readonly [key: string]: unknown;
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Guard trace
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Emitted whenever the guard changes the effective routing decision:
|
|
49
|
+
* - 'spawn-forced-wrfc' — implementation-like spawn promoted to WRFC
|
|
50
|
+
* - 'spawn-suppressed-wrfc' — spawn judged read-only; WRFC suppressed
|
|
51
|
+
* - 'batch-collapsed-to-wrfc'— batch-spawn collapsed into a single WRFC owner chain
|
|
52
|
+
*/
|
|
53
|
+
export type WrfcGuardTraceKind =
|
|
54
|
+
| 'spawn-forced-wrfc'
|
|
55
|
+
| 'spawn-suppressed-wrfc'
|
|
56
|
+
| 'batch-collapsed-to-wrfc';
|
|
57
|
+
|
|
58
|
+
export type WrfcGuardTrace = {
|
|
59
|
+
readonly kind: WrfcGuardTraceKind;
|
|
60
|
+
readonly reason: string;
|
|
61
|
+
readonly task: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
43
64
|
type WrfcAgentToolGuardOptions = {
|
|
44
65
|
readonly getLastUserMessage?: () => string | null;
|
|
66
|
+
readonly onTrace?: (trace: WrfcGuardTrace) => void;
|
|
45
67
|
};
|
|
46
68
|
|
|
47
69
|
export function installWrfcAgentToolGuard(registry: ToolRegistry, options: WrfcAgentToolGuardOptions = {}): void {
|
|
@@ -68,8 +90,15 @@ export function validateWrfcAgentToolInvocation(args: AgentToolArgs): string | n
|
|
|
68
90
|
|
|
69
91
|
export function normalizeWrfcAgentToolInvocation(args: AgentToolArgs, options: WrfcAgentToolGuardOptions = {}): AgentToolArgs {
|
|
70
92
|
const lastUserMessage = cleanText(options.getLastUserMessage?.() ?? null);
|
|
93
|
+
const trace = options.onTrace;
|
|
71
94
|
if (args.mode === 'spawn') {
|
|
72
95
|
if (shouldRouteSpawnToWrfc(args)) {
|
|
96
|
+
const reason = resolveSpawnWrfcReason(args);
|
|
97
|
+
trace?.({
|
|
98
|
+
kind: 'spawn-forced-wrfc',
|
|
99
|
+
reason,
|
|
100
|
+
task: cleanText(args.task) || '(no task)',
|
|
101
|
+
});
|
|
73
102
|
const normalized: AgentToolArgs = {
|
|
74
103
|
...args,
|
|
75
104
|
task: selectAuthoritativeTask(args.task, lastUserMessage),
|
|
@@ -78,12 +107,22 @@ export function normalizeWrfcAgentToolInvocation(args: AgentToolArgs, options: W
|
|
|
78
107
|
};
|
|
79
108
|
return cleanText(args.template) ? normalized : { ...normalized, template: 'engineer' };
|
|
80
109
|
}
|
|
110
|
+
trace?.({
|
|
111
|
+
kind: 'spawn-suppressed-wrfc',
|
|
112
|
+
reason: isReadOnlyTask(cleanText(args.task)) ? 'task judged read-only' : 'no implementation signal detected',
|
|
113
|
+
task: cleanText(args.task) || '(no task)',
|
|
114
|
+
});
|
|
81
115
|
return { ...args, reviewMode: 'none', dangerously_disable_wrfc: true };
|
|
82
116
|
}
|
|
83
117
|
|
|
84
118
|
if (args.mode !== 'batch-spawn') return args;
|
|
85
119
|
const tasks = Array.isArray(args.tasks) ? args.tasks.filter(isRecord) : [];
|
|
86
120
|
if (shouldCollapseBatchToAuthoritativeWrfc(args, tasks) && lastUserMessage) {
|
|
121
|
+
trace?.({
|
|
122
|
+
kind: 'batch-collapsed-to-wrfc',
|
|
123
|
+
reason: `WRFC: collapsed ${tasks.length}-agent batch into one reviewed chain`,
|
|
124
|
+
task: lastUserMessage,
|
|
125
|
+
});
|
|
87
126
|
return buildAuthoritativeWrfcSpawn(args, tasks, lastUserMessage);
|
|
88
127
|
}
|
|
89
128
|
const wrfcTasks = tasks.filter((task) => isExplicitWrfcTask(task, args));
|
|
@@ -190,17 +229,39 @@ function isRootReviewRoleTask(task: AgentTaskArgs): boolean {
|
|
|
190
229
|
|| /\b(?:test|tests|testing|review|reviews|reviewing|verify|verifies|verifying|verification|validate|validates|validating|validation|qa)\s+(?:the|this|that|implementation|solution|feature|deliverable|code|changes|work|output|result|patch|diff)\b/i.test(text);
|
|
191
230
|
}
|
|
192
231
|
|
|
232
|
+
function resolveSpawnWrfcReason(args: AgentTaskArgs): string {
|
|
233
|
+
if (containsWrfcSignal(args.task)) return 'task contains explicit WRFC signal';
|
|
234
|
+
if (args.reviewMode === 'wrfc') return 'reviewMode explicitly set to wrfc';
|
|
235
|
+
if (isRootReviewRoleTask(args)) return 'task identified as root review-role (reviewer/tester/verifier)';
|
|
236
|
+
return 'task judged implementation-like';
|
|
237
|
+
}
|
|
238
|
+
|
|
193
239
|
function isImplementationLikeTask(task: AgentTaskArgs): boolean {
|
|
194
240
|
const text = cleanText(task.task);
|
|
195
241
|
if (!text) return false;
|
|
196
|
-
|
|
242
|
+
// Broad verb set: explicit action words + short imperative phrasings like
|
|
243
|
+
// "make the button blue", "wire up X", "connect X", "rename X", "move X"
|
|
244
|
+
return /\b(?:build|implement|create|add|write|fix|repair|update|refactor|change|modify|deliver|make|patch|wire|connect|rename|move|delete|remove|migrate|configure|set\s+up|set\s+the|turn\s+on|turn\s+off|enable|disable|initialize|init|register|replace|swap|convert|transform|extend|integrate|embed|inject|port|rewrite|restructure)\b/i.test(text)
|
|
197
245
|
&& !isReadOnlyTask(text);
|
|
198
246
|
}
|
|
199
247
|
|
|
200
248
|
function isReadOnlyTask(text: string): boolean {
|
|
249
|
+
// Branch A: explicit do-not-write guards
|
|
201
250
|
if (/\bdo\s+not\s+(?:write|edit|modify|change|create)\b|\bread[-\s]*only\b|\bwithout\s+(?:writing|editing|modifying|changing|creating)\b/i.test(text)) return true;
|
|
202
|
-
|
|
203
|
-
|
|
251
|
+
// Branch B: task leads with an analysis/reporting verb — treat it as read-only regardless
|
|
252
|
+
// of any action verbs that appear later in the sentence. A task that LEADS with
|
|
253
|
+
// "report", "investigate", "describe", "audit", etc. is describing or evaluating an
|
|
254
|
+
// action, not performing it. Examples that must NOT reach WRFC:
|
|
255
|
+
// "report on how to migrate the auth module"
|
|
256
|
+
// "investigate what to remove"
|
|
257
|
+
// "document how we would convert X to Y"
|
|
258
|
+
// "describe the steps to disable telemetry"
|
|
259
|
+
// "audit which modules to delete"
|
|
260
|
+
// "evaluate whether to migrate"
|
|
261
|
+
// The negative-lookahead is intentionally absent: the leading verb is authoritative.
|
|
262
|
+
// Note: 'review' is intentionally excluded — tasks leading with 'review' are caught
|
|
263
|
+
// by isRootReviewRoleTask() and routed to a WRFC chain as a reviewer role.
|
|
264
|
+
return /^\s*(?:inspect|research|read|find|list|summarize|analy[sz]e|explain|report|investigate|document|describe|audit|evaluate|assess|check|compare|tell|show)\b/i.test(text);
|
|
204
265
|
}
|
|
205
266
|
|
|
206
267
|
function selectAuthoritativeTask(candidate: unknown, lastUserMessage: string): string {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format elapsed time (in milliseconds) as a compact human-readable string.
|
|
3
|
+
*
|
|
4
|
+
* Ranges:
|
|
5
|
+
* < 1 second → "0.Xs" (one decimal, e.g. "0.4s")
|
|
6
|
+
* 1-59s → "Xs" (e.g. "3s", "59s")
|
|
7
|
+
* 1m-59m59s → "Xm YYs" (e.g. "1m04s", "59m59s")
|
|
8
|
+
* ≥ 1 hour → "Xh YYm" (e.g. "1h02m")
|
|
9
|
+
*
|
|
10
|
+
* Used by the thinking indicator and live tool timer.
|
|
11
|
+
*/
|
|
12
|
+
export function formatElapsed(ms: number): string {
|
|
13
|
+
if (ms < 0) ms = 0;
|
|
14
|
+
if (ms < 1000) {
|
|
15
|
+
// Truncate (not round) to one decimal so 999ms stays "0.9s" not "1.0s".
|
|
16
|
+
return `${(Math.floor(ms / 100) / 10).toFixed(1)}s`;
|
|
17
|
+
}
|
|
18
|
+
const totalSecs = Math.floor(ms / 1000);
|
|
19
|
+
if (totalSecs < 60) {
|
|
20
|
+
return `${totalSecs}s`;
|
|
21
|
+
}
|
|
22
|
+
const totalMins = Math.floor(totalSecs / 60);
|
|
23
|
+
if (totalMins < 60) {
|
|
24
|
+
const remSecs = totalSecs % 60;
|
|
25
|
+
return `${totalMins}m${String(remSecs).padStart(2, '0')}s`;
|
|
26
|
+
}
|
|
27
|
+
const hours = Math.floor(totalMins / 60);
|
|
28
|
+
const remMins = totalMins % 60;
|
|
29
|
+
return `${hours}h${String(remMins).padStart(2, '0')}m`;
|
|
30
|
+
}
|
|
@@ -1,9 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip ANSI SGR/CSI escape sequences and OSC-8 hyperlink sequences
|
|
3
|
+
* from a string so that only visible characters remain for width measurement.
|
|
4
|
+
*
|
|
5
|
+
* Covers:
|
|
6
|
+
* - CSI sequences: ESC [ ... <final byte 0x40-0x7E> (includes SGR \x1b[...m)
|
|
7
|
+
* - OSC sequences: ESC ] ... ST where ST is ESC\\ or BEL (0x07)
|
|
8
|
+
* - Simple ESC followed by a single non-bracket/non-] character (e.g. ESC c)
|
|
9
|
+
*/
|
|
10
|
+
function stripAnsi(text: string): string {
|
|
11
|
+
let result = '';
|
|
12
|
+
let i = 0;
|
|
13
|
+
while (i < text.length) {
|
|
14
|
+
if (text[i] === '\x1b') {
|
|
15
|
+
const next = text[i + 1];
|
|
16
|
+
if (next === '[') {
|
|
17
|
+
// CSI: skip until final byte (0x40-0x7E)
|
|
18
|
+
i += 2;
|
|
19
|
+
while (i < text.length) {
|
|
20
|
+
const c = text.charCodeAt(i);
|
|
21
|
+
i++;
|
|
22
|
+
if (c >= 0x40 && c <= 0x7e) break;
|
|
23
|
+
}
|
|
24
|
+
} else if (next === ']') {
|
|
25
|
+
// OSC: skip until ST (ESC\\ or BEL)
|
|
26
|
+
i += 2;
|
|
27
|
+
while (i < text.length) {
|
|
28
|
+
if (text[i] === '\x07') { i++; break; }
|
|
29
|
+
if (text[i] === '\x1b' && text[i + 1] === '\\') { i += 2; break; }
|
|
30
|
+
i++;
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
// Simple two-byte escape (ESC + one char)
|
|
34
|
+
i += next !== undefined ? 2 : 1;
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
result += text[i];
|
|
38
|
+
i++;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
1
44
|
/**
|
|
2
45
|
* Calculates the visual width of a string in the terminal.
|
|
3
46
|
* Handles CJK characters, emoji (including ZWJ sequences), and
|
|
4
47
|
* variation selectors correctly as double-width.
|
|
48
|
+
* ANSI escape sequences (SGR/CSI/OSC-8) are stripped before measurement.
|
|
5
49
|
*/
|
|
6
50
|
export function getDisplayWidth(text: string): number {
|
|
51
|
+
text = stripAnsi(text);
|
|
7
52
|
let width = 0;
|
|
8
53
|
let i = 0;
|
|
9
54
|
while (i < text.length) {
|
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.22.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,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync
|
|
3
|
-
import {
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { atomicWriteFileSync } from '@/config/atomic-write.ts';
|
|
4
|
+
import { join } from 'node:path';
|
|
4
5
|
|
|
5
6
|
export const WORK_PLAN_STATUSES = [
|
|
6
7
|
'pending',
|
|
@@ -331,10 +332,7 @@ export class WorkPlanStore {
|
|
|
331
332
|
}
|
|
332
333
|
|
|
333
334
|
private writePlan(plan: WorkPlan): void {
|
|
334
|
-
|
|
335
|
-
const tmp = `${this.filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
336
|
-
writeFileSync(tmp, `${JSON.stringify(plan, null, 2)}\n`, { mode: 0o600 });
|
|
337
|
-
renameSync(tmp, this.filePath);
|
|
335
|
+
atomicWriteFileSync(this.filePath, `${JSON.stringify(plan, null, 2)}\n`, { mkdirp: true });
|
|
338
336
|
}
|
|
339
337
|
|
|
340
338
|
private resolveItem(plan: WorkPlan, idOrPrefix: string): WorkPlanItem {
|
|
@@ -1,345 +0,0 @@
|
|
|
1
|
-
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
3
|
-
import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
|
|
4
|
-
import type { MemoryClass, MemoryRecord, MemoryRegistry, MemoryReviewState } from '@pellux/goodvibes-sdk/platform/state';
|
|
5
|
-
import {
|
|
6
|
-
buildBodyText,
|
|
7
|
-
buildEmptyState,
|
|
8
|
-
buildGuidanceLine,
|
|
9
|
-
buildKeyValueLine,
|
|
10
|
-
buildPanelLine,
|
|
11
|
-
buildPanelWorkspace,
|
|
12
|
-
DEFAULT_PANEL_PALETTE,
|
|
13
|
-
} from './polish.ts';
|
|
14
|
-
|
|
15
|
-
function summarize(records: MemoryRecord[], cls: MemoryClass): MemoryRecord[] {
|
|
16
|
-
return records.filter((record) => record.cls === cls).slice(0, 3);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const C = {
|
|
20
|
-
...DEFAULT_PANEL_PALETTE,
|
|
21
|
-
header: '#94a3b8',
|
|
22
|
-
headerBg: '#1e293b',
|
|
23
|
-
} as const;
|
|
24
|
-
|
|
25
|
-
function reviewStateColor(state: MemoryReviewState): string {
|
|
26
|
-
switch (state) {
|
|
27
|
-
case 'reviewed':
|
|
28
|
-
return C.good;
|
|
29
|
-
case 'stale':
|
|
30
|
-
return C.warn;
|
|
31
|
-
case 'contradicted':
|
|
32
|
-
return C.bad;
|
|
33
|
-
case 'fresh':
|
|
34
|
-
default:
|
|
35
|
-
return C.info;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function formatConfidence(confidence: number): string {
|
|
40
|
-
return `${confidence.toString().padStart(3, ' ')}%`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export class KnowledgePanel extends ScrollableListPanel<MemoryRecord> {
|
|
44
|
-
private readonly registry: MemoryRegistry;
|
|
45
|
-
private unsubscribe?: () => void;
|
|
46
|
-
private records: MemoryRecord[] = [];
|
|
47
|
-
// I1: confirm for destructive review-state mutations
|
|
48
|
-
private confirm: ConfirmState<{ id: string; action: 'stale' | 'contradicted' }> | null = null;
|
|
49
|
-
|
|
50
|
-
public constructor(registry: MemoryRegistry) {
|
|
51
|
-
super('knowledge', 'Knowledge', 'K', 'agent');
|
|
52
|
-
this.registry = registry;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
public override onActivate(): void {
|
|
56
|
-
super.onActivate();
|
|
57
|
-
this.refresh();
|
|
58
|
-
this.unsubscribe = this.registry.subscribe(() => {
|
|
59
|
-
this.refresh();
|
|
60
|
-
this.markDirty();
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
public override onDeactivate(): void {
|
|
65
|
-
super.onDeactivate();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
public override onDestroy(): void {
|
|
69
|
-
this.unsubscribe?.();
|
|
70
|
-
this.unsubscribe = undefined;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
// ScrollableListPanel implementation
|
|
75
|
-
// ---------------------------------------------------------------------------
|
|
76
|
-
|
|
77
|
-
protected getItems(): readonly MemoryRecord[] {
|
|
78
|
-
return this.records;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
protected renderItem(record: MemoryRecord, index: number, selected: boolean, width: number): Line {
|
|
82
|
-
const bg = selected ? C.selectBg : undefined;
|
|
83
|
-
return buildPanelLine(width, [
|
|
84
|
-
[' ', C.label, bg],
|
|
85
|
-
[record.reviewState.padEnd(13), reviewStateColor(record.reviewState), bg],
|
|
86
|
-
[` ${formatConfidence(record.confidence)} `, C.value, bg],
|
|
87
|
-
[record.summary.slice(0, Math.max(0, width - 26)), C.value, bg],
|
|
88
|
-
]);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
protected override getPalette() { return C; }
|
|
92
|
-
protected override getEmptyStateMessage() { return 'No durable project knowledge'; }
|
|
93
|
-
protected override getEmptyStateActions() {
|
|
94
|
-
return [
|
|
95
|
-
{ command: '/recall add fact <summary>', summary: 'capture a durable fact directly' },
|
|
96
|
-
{ command: '/recall capture incident latest', summary: 'promote the latest incident into project memory' },
|
|
97
|
-
{ command: '/recall capture policy', summary: 'store the current policy posture as durable evidence' },
|
|
98
|
-
];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ---------------------------------------------------------------------------
|
|
102
|
-
// Input
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
|
-
|
|
105
|
-
public handleInput(key: string): boolean {
|
|
106
|
-
// I1: y/n confirm for stale/contradict
|
|
107
|
-
if (this.confirm) {
|
|
108
|
-
const result = handleConfirmInput(this.confirm, key);
|
|
109
|
-
if (result === 'confirmed') {
|
|
110
|
-
const { id, action } = this.confirm.subject;
|
|
111
|
-
this.confirm = null;
|
|
112
|
-
const selected = this.records.find((r) => r.id === id);
|
|
113
|
-
if (selected) {
|
|
114
|
-
try {
|
|
115
|
-
if (action === 'stale') {
|
|
116
|
-
this.registry.review(id, {
|
|
117
|
-
state: 'stale',
|
|
118
|
-
confidence: Math.min(selected.confidence, 40),
|
|
119
|
-
reviewedBy: 'operator',
|
|
120
|
-
staleReason: 'marked stale from the knowledge panel',
|
|
121
|
-
});
|
|
122
|
-
} else {
|
|
123
|
-
this.registry.review(id, {
|
|
124
|
-
state: 'contradicted',
|
|
125
|
-
confidence: 0,
|
|
126
|
-
reviewedBy: 'operator',
|
|
127
|
-
staleReason: 'marked contradicted from the knowledge panel',
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
} catch (e) {
|
|
131
|
-
// I2: surface async failure
|
|
132
|
-
this.setError(`Review update failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
this.refresh();
|
|
136
|
-
this.markDirty();
|
|
137
|
-
return true;
|
|
138
|
-
}
|
|
139
|
-
if (result === 'cancelled') {
|
|
140
|
-
this.confirm = null;
|
|
141
|
-
this.markDirty();
|
|
142
|
-
return true;
|
|
143
|
-
}
|
|
144
|
-
if (result === 'absorbed') return true;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// I2: auto-clear error on next keypress (inherited via super.handleInput)
|
|
148
|
-
if (this.records.length === 0) return super.handleInput(key);
|
|
149
|
-
|
|
150
|
-
const selected = this.records[this.selectedIndex];
|
|
151
|
-
|
|
152
|
-
if (key === 'Enter' || key === 'return' || key === 'r') {
|
|
153
|
-
if (!selected) return false;
|
|
154
|
-
this.registry.review(selected.id, {
|
|
155
|
-
state: 'reviewed',
|
|
156
|
-
confidence: Math.max(selected.confidence, 85),
|
|
157
|
-
reviewedBy: 'operator',
|
|
158
|
-
});
|
|
159
|
-
this.refresh();
|
|
160
|
-
this.markDirty();
|
|
161
|
-
return true;
|
|
162
|
-
}
|
|
163
|
-
if (key === 's') {
|
|
164
|
-
if (!selected) return false;
|
|
165
|
-
// I1: prompt confirm before marking stale
|
|
166
|
-
this.confirm = { subject: { id: selected.id, action: 'stale' }, label: selected.summary.slice(0, 40) };
|
|
167
|
-
this.markDirty();
|
|
168
|
-
return true;
|
|
169
|
-
}
|
|
170
|
-
if (key === 'c') {
|
|
171
|
-
if (!selected) return false;
|
|
172
|
-
// I1: prompt confirm before marking contradicted
|
|
173
|
-
this.confirm = { subject: { id: selected.id, action: 'contradicted' }, label: selected.summary.slice(0, 40) };
|
|
174
|
-
this.markDirty();
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
if (key === 'f') {
|
|
178
|
-
if (!selected) return false;
|
|
179
|
-
this.registry.review(selected.id, {
|
|
180
|
-
state: 'fresh',
|
|
181
|
-
confidence: Math.max(selected.confidence, 60),
|
|
182
|
-
reviewedBy: 'operator',
|
|
183
|
-
});
|
|
184
|
-
this.refresh();
|
|
185
|
-
this.markDirty();
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Normalize arrow keys to base class format
|
|
190
|
-
if (key === 'ArrowUp') return super.handleInput('up');
|
|
191
|
-
if (key === 'ArrowDown') return super.handleInput('down');
|
|
192
|
-
return super.handleInput(key);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
private refresh(): void {
|
|
196
|
-
const queue = this.registry.reviewQueue(24);
|
|
197
|
-
this.records = queue.length > 0 ? queue : this.registry.search({ limit: 24 });
|
|
198
|
-
this.clampSelection();
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
public render(width: number, height: number): Line[] {
|
|
202
|
-
this.clampSelection();
|
|
203
|
-
|
|
204
|
-
// I1: show confirm dialog in place of normal content
|
|
205
|
-
if (this.confirm) {
|
|
206
|
-
return buildPanelWorkspace(width, height, {
|
|
207
|
-
title: 'Knowledge Control Room',
|
|
208
|
-
intro: '',
|
|
209
|
-
sections: [{ title: 'Confirmation', lines: renderConfirmLines(width, this.confirm) }],
|
|
210
|
-
footerLines: [buildPanelLine(width, [[' y confirm n / Esc cancel', C.dim]])],
|
|
211
|
-
palette: C,
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (this.records.length === 0) this.refresh();
|
|
216
|
-
|
|
217
|
-
const intro = 'Typed project knowledge, reviewed evidence, and operator-governed memory across session, project, and team scopes.';
|
|
218
|
-
const records = this.registry.search({ limit: 200 });
|
|
219
|
-
|
|
220
|
-
if (records.length === 0) {
|
|
221
|
-
return this.renderList(width, height, {
|
|
222
|
-
title: 'Knowledge Control Room',
|
|
223
|
-
footer: [buildPanelLine(width, [[' Review keys: Up/Down move r/Enter review s stale c contradicted f fresh', C.dim]])],
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const queue = this.registry.reviewQueue(24);
|
|
228
|
-
const byClass = new Map<MemoryClass, number>();
|
|
229
|
-
const byReview = new Map<MemoryReviewState, number>();
|
|
230
|
-
const byScope = new Map<string, number>();
|
|
231
|
-
for (const record of records) {
|
|
232
|
-
byClass.set(record.cls, (byClass.get(record.cls) ?? 0) + 1);
|
|
233
|
-
byReview.set(record.reviewState, (byReview.get(record.reviewState) ?? 0) + 1);
|
|
234
|
-
byScope.set(record.scope, (byScope.get(record.scope) ?? 0) + 1);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const classLines: Line[] = [
|
|
238
|
-
buildPanelLine(width, [
|
|
239
|
-
[' facts ', C.label], [String(byClass.get('fact') ?? 0), C.good],
|
|
240
|
-
[' risks ', C.label], [String(byClass.get('risk') ?? 0), (byClass.get('risk') ?? 0) > 0 ? C.warn : C.good],
|
|
241
|
-
[' runbooks ', C.label], [String(byClass.get('runbook') ?? 0), C.info],
|
|
242
|
-
[' architecture ', C.label], [String(byClass.get('architecture') ?? 0), C.info],
|
|
243
|
-
[' incidents ', C.label], [String(byClass.get('incident') ?? 0), (byClass.get('incident') ?? 0) > 0 ? C.bad : C.good],
|
|
244
|
-
]),
|
|
245
|
-
buildPanelLine(width, [
|
|
246
|
-
[' decisions ', C.label], [String(byClass.get('decision') ?? 0), C.value],
|
|
247
|
-
[' constraints ', C.label], [String(byClass.get('constraint') ?? 0), C.value],
|
|
248
|
-
[' ownership ', C.label], [String(byClass.get('ownership') ?? 0), C.value],
|
|
249
|
-
[' patterns ', C.label], [String(byClass.get('pattern') ?? 0), C.value],
|
|
250
|
-
[' total ', C.label], [String(records.length), C.value],
|
|
251
|
-
]),
|
|
252
|
-
];
|
|
253
|
-
|
|
254
|
-
const reviewLines: Line[] = [
|
|
255
|
-
buildPanelLine(width, [
|
|
256
|
-
[' reviewed ', C.label], [String(byReview.get('reviewed') ?? 0), C.good],
|
|
257
|
-
[' fresh ', C.label], [String(byReview.get('fresh') ?? 0), C.info],
|
|
258
|
-
[' stale ', C.label], [String(byReview.get('stale') ?? 0), C.warn],
|
|
259
|
-
[' contradicted ', C.label], [String(byReview.get('contradicted') ?? 0), C.bad],
|
|
260
|
-
[' Review Queue ', C.label], [String(queue.length), queue.length > 0 ? C.warn : C.good],
|
|
261
|
-
]),
|
|
262
|
-
buildPanelLine(width, [
|
|
263
|
-
[' session ', C.label], [String(byScope.get('session') ?? 0), C.info],
|
|
264
|
-
[' project ', C.label], [String(byScope.get('project') ?? 0), C.value],
|
|
265
|
-
[' team ', C.label], [String(byScope.get('team') ?? 0), C.good],
|
|
266
|
-
]),
|
|
267
|
-
buildGuidanceLine(width, '/recall review', 'work the stale and contradicted queue from the command surface', C),
|
|
268
|
-
];
|
|
269
|
-
|
|
270
|
-
const recentSummaryLines: Line[] = [];
|
|
271
|
-
for (const [title, items, color] of [
|
|
272
|
-
['Recent Risks', summarize(records, 'risk'), C.warn],
|
|
273
|
-
['Runbooks', summarize(records, 'runbook'), C.info],
|
|
274
|
-
['Architecture Notes', summarize(records, 'architecture'), C.info],
|
|
275
|
-
['Recent Incidents', summarize(records, 'incident'), C.bad],
|
|
276
|
-
] as const) {
|
|
277
|
-
if (recentSummaryLines.length > 0) {
|
|
278
|
-
recentSummaryLines.push(buildPanelLine(width, [['', C.dim]]));
|
|
279
|
-
}
|
|
280
|
-
recentSummaryLines.push(buildPanelLine(width, [[` ${title}`, C.label]]));
|
|
281
|
-
if (items.length === 0) {
|
|
282
|
-
recentSummaryLines.push(buildPanelLine(width, [[' none recorded', C.dim]]));
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
for (const item of items) {
|
|
286
|
-
recentSummaryLines.push(buildPanelLine(width, [
|
|
287
|
-
[' ', C.label],
|
|
288
|
-
[item.summary.slice(0, Math.max(0, width - 2)), color],
|
|
289
|
-
]));
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const selectedRecord = this.records[this.selectedIndex];
|
|
294
|
-
const selectedLines: Line[] = [];
|
|
295
|
-
if (selectedRecord) {
|
|
296
|
-
selectedLines.push(buildPanelLine(width, [[' Selected', C.label]]));
|
|
297
|
-
selectedLines.push(buildKeyValueLine(width, [
|
|
298
|
-
{ label: 'Class', value: selectedRecord.cls, valueColor: C.value },
|
|
299
|
-
{ label: 'Scope', value: selectedRecord.scope, valueColor: C.info },
|
|
300
|
-
{ label: 'Review', value: selectedRecord.reviewState, valueColor: reviewStateColor(selectedRecord.reviewState) },
|
|
301
|
-
{ label: 'Confidence', value: formatConfidence(selectedRecord.confidence), valueColor: C.value },
|
|
302
|
-
], C));
|
|
303
|
-
selectedLines.push(...buildBodyText(width, `Summary: ${selectedRecord.summary}`, C, C.value));
|
|
304
|
-
if (selectedRecord.detail) selectedLines.push(...buildBodyText(width, `Detail: ${selectedRecord.detail}`, C, C.dim));
|
|
305
|
-
if (selectedRecord.provenance.length) {
|
|
306
|
-
selectedLines.push(...buildBodyText(
|
|
307
|
-
width,
|
|
308
|
-
`Provenance: ${selectedRecord.provenance.map((p) => `${p.kind}:${p.ref}`).join(', ')}`,
|
|
309
|
-
C,
|
|
310
|
-
C.dim,
|
|
311
|
-
));
|
|
312
|
-
}
|
|
313
|
-
if (selectedRecord.staleReason) {
|
|
314
|
-
selectedLines.push(...buildBodyText(
|
|
315
|
-
width,
|
|
316
|
-
`Stale reason: ${selectedRecord.staleReason}`,
|
|
317
|
-
C,
|
|
318
|
-
selectedRecord.reviewState === 'contradicted' ? C.bad : C.warn,
|
|
319
|
-
));
|
|
320
|
-
}
|
|
321
|
-
if (selectedRecord.reviewedAt) {
|
|
322
|
-
selectedLines.push(buildPanelLine(width, [
|
|
323
|
-
[' Reviewed: ', C.label],
|
|
324
|
-
[new Date(selectedRecord.reviewedAt).toLocaleString(), C.dim],
|
|
325
|
-
]));
|
|
326
|
-
if (selectedRecord.reviewedBy) {
|
|
327
|
-
selectedLines.push(buildPanelLine(width, [
|
|
328
|
-
[' Reviewer: ', C.label],
|
|
329
|
-
[selectedRecord.reviewedBy, C.dim],
|
|
330
|
-
]));
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
return this.renderList(width, height, {
|
|
336
|
-
title: 'Knowledge Control Room',
|
|
337
|
-
header: [...classLines, ...reviewLines],
|
|
338
|
-
footer: [
|
|
339
|
-
...(selectedLines.length > 0 ? selectedLines : []),
|
|
340
|
-
...recentSummaryLines,
|
|
341
|
-
buildPanelLine(width, [[' Up/Down move r/Enter reviewed s stale c contradicted f fresh', C.dim]]),
|
|
342
|
-
],
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
}
|