@pellux/goodvibes-tui 0.26.0 → 0.28.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 +40 -0
- package/README.md +12 -7
- package/docs/foundation-artifacts/operator-contract.json +4 -1
- package/package.json +2 -2
- package/src/cli/bundle-command.ts +2 -1
- package/src/cli/service-posture.ts +2 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation-rendering.ts +2 -2
- package/src/core/conversation.ts +53 -8
- package/src/core/session-recovery.ts +26 -18
- package/src/core/system-message-router.ts +42 -26
- package/src/core/turn-event-wiring.ts +13 -16
- package/src/daemon/handlers/calendar/caldav-client.ts +2 -1
- package/src/daemon/handlers/inbox/index.ts +2 -1
- package/src/daemon/handlers/inbox/poller.ts +2 -1
- package/src/daemon/handlers/inbox/providers/discord.ts +2 -1
- package/src/daemon/handlers/inbox/providers/email.ts +2 -1
- package/src/daemon/handlers/inbox/providers/route-util.ts +2 -1
- package/src/daemon/handlers/inbox/providers/slack.ts +2 -1
- package/src/daemon/handlers/triage/integration.ts +2 -1
- package/src/daemon/handlers/triage/pipeline.ts +2 -1
- package/src/daemon/handlers/triage/tagger/discord.ts +2 -1
- package/src/daemon/handlers/triage/tagger/imap.ts +4 -3
- package/src/export/gist-uploader.ts +3 -1
- package/src/input/command-registry.ts +1 -0
- package/src/input/commands/cloudflare-runtime.ts +2 -1
- package/src/input/commands/eval.ts +4 -3
- package/src/input/commands/guidance-runtime.ts +2 -4
- package/src/input/commands/health-runtime.ts +13 -7
- package/src/input/commands/knowledge.ts +2 -1
- package/src/input/commands/runtime-services.ts +40 -10
- package/src/input/handler-command-route.ts +1 -1
- package/src/input/handler-feed-routes.ts +61 -4
- package/src/input/handler-feed.ts +13 -0
- package/src/input/handler-interactions.ts +2 -1
- package/src/input/handler-modal-routes.ts +2 -1
- package/src/input/handler-onboarding-cloudflare.ts +2 -1
- package/src/input/handler-onboarding.ts +8 -8
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-ui-state.ts +5 -1
- package/src/input/keybindings.ts +5 -0
- package/src/input/model-picker.ts +5 -0
- package/src/input/panel-integration-actions.ts +10 -0
- package/src/input/tts-settings-actions.ts +2 -1
- package/src/main.ts +52 -60
- package/src/panels/agent-inspector-panel.ts +90 -15
- package/src/panels/agent-inspector-shared.ts +3 -3
- package/src/panels/agent-logs-panel.ts +149 -72
- package/src/panels/approval-panel.ts +146 -95
- package/src/panels/automation-control-panel.ts +24 -1
- package/src/panels/base-panel.ts +28 -3
- package/src/panels/builtin/agent.ts +3 -1
- package/src/panels/builtin/development.ts +2 -1
- package/src/panels/builtin/session.ts +1 -1
- package/src/panels/cockpit-panel.ts +24 -5
- package/src/panels/cockpit-read-model.ts +2 -1
- package/src/panels/communication-panel.ts +108 -43
- package/src/panels/context-visualizer-panel.ts +49 -15
- package/src/panels/control-plane-panel.ts +27 -1
- package/src/panels/cost-tracker-panel.ts +87 -65
- package/src/panels/debug-panel.ts +61 -37
- package/src/panels/diff-panel.ts +59 -30
- package/src/panels/docs-panel.ts +30 -24
- package/src/panels/eval-panel.ts +130 -81
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +42 -42
- package/src/panels/file-preview-panel.ts +11 -3
- package/src/panels/forensics-panel.ts +27 -13
- package/src/panels/git-panel.ts +65 -84
- package/src/panels/hooks-panel.ts +36 -2
- package/src/panels/incident-review-panel.ts +31 -10
- package/src/panels/intelligence-panel.ts +152 -71
- package/src/panels/knowledge-graph-panel.ts +89 -27
- package/src/panels/local-auth-panel.ts +17 -11
- package/src/panels/marketplace-panel.ts +33 -13
- package/src/panels/memory-panel.ts +7 -5
- package/src/panels/ops-control-panel.ts +69 -7
- package/src/panels/ops-strategy-panel.ts +61 -43
- package/src/panels/orchestration-panel.ts +34 -4
- package/src/panels/panel-list-panel.ts +33 -8
- package/src/panels/panel-manager.ts +23 -4
- package/src/panels/plan-dashboard-panel.ts +183 -66
- package/src/panels/plugins-panel.ts +48 -10
- package/src/panels/policy-panel.ts +60 -23
- package/src/panels/polish-core.ts +157 -0
- package/src/panels/polish-tables.ts +258 -0
- package/src/panels/polish.ts +44 -145
- package/src/panels/project-planning-panel.ts +27 -4
- package/src/panels/provider-accounts-panel.ts +55 -18
- package/src/panels/provider-health-panel.ts +118 -87
- package/src/panels/provider-stats-panel.ts +47 -22
- package/src/panels/qr-panel.ts +23 -11
- package/src/panels/remote-panel.ts +12 -5
- package/src/panels/routes-panel.ts +27 -5
- package/src/panels/sandbox-panel.ts +62 -27
- package/src/panels/schedule-panel.ts +44 -24
- package/src/panels/scrollable-list-panel.ts +124 -10
- package/src/panels/security-panel.ts +72 -20
- package/src/panels/services-panel.ts +68 -22
- package/src/panels/session-browser-panel.ts +42 -26
- package/src/panels/session-maintenance.ts +2 -2
- package/src/panels/settings-sync-panel.ts +30 -15
- package/src/panels/skills-panel.ts +2 -1
- package/src/panels/subscription-panel.ts +21 -3
- package/src/panels/symbol-outline-panel.ts +40 -47
- package/src/panels/system-messages-panel.ts +60 -27
- package/src/panels/tasks-panel.ts +36 -14
- package/src/panels/thinking-panel.ts +32 -36
- package/src/panels/token-budget-panel.ts +80 -53
- package/src/panels/tool-inspector-panel.ts +37 -39
- package/src/panels/types.ts +25 -0
- package/src/panels/watchers-panel.ts +25 -5
- package/src/panels/work-plan-panel.ts +127 -35
- package/src/panels/worktree-panel.ts +65 -20
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +53 -147
- package/src/renderer/agent-detail-modal.ts +2 -2
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- package/src/renderer/modal-utils.ts +0 -10
- package/src/renderer/model-picker-overlay.ts +9 -4
- package/src/renderer/model-workspace.ts +2 -3
- package/src/renderer/panel-composite.ts +7 -20
- package/src/renderer/panel-workspace-bar.ts +14 -8
- package/src/renderer/process-modal.ts +2 -2
- package/src/renderer/progress.ts +3 -2
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/ui-factory.ts +4 -6
- package/src/runtime/bootstrap-command-context.ts +3 -0
- package/src/runtime/bootstrap-command-parts.ts +3 -1
- package/src/runtime/bootstrap-core.ts +31 -31
- package/src/runtime/bootstrap-shell.ts +1 -0
- package/src/runtime/onboarding/apply.ts +4 -3
- package/src/runtime/onboarding/snapshot.ts +4 -3
- package/src/runtime/process-lifecycle.ts +195 -0
- package/src/runtime/services.ts +4 -1
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/runtime/wrfc-persistence.ts +20 -5
- package/src/shell/ui-openers.ts +3 -2
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/verification/live-verifier.ts +4 -3
- package/src/version.ts +1 -1
- package/src/core/context-auto-compact.ts +0 -110
- package/src/panels/panel-picker.ts +0 -106
- package/src/renderer/panel-picker-overlay.ts +0 -202
- package/src/renderer/panel-tab-bar.ts +0 -69
|
@@ -9,6 +9,8 @@ import type { HookChain, HookDefinition } from '@pellux/goodvibes-sdk/platform/h
|
|
|
9
9
|
import type { HookWorkbench } from '@pellux/goodvibes-sdk/platform/hooks';
|
|
10
10
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
11
11
|
import {
|
|
12
|
+
buildKeyValueLine,
|
|
13
|
+
buildKeyboardHints,
|
|
12
14
|
buildPanelLine,
|
|
13
15
|
buildStatusPill,
|
|
14
16
|
DEFAULT_PANEL_PALETTE,
|
|
@@ -68,9 +70,17 @@ export class HooksPanel extends ScrollableListPanel<HookEntry> {
|
|
|
68
70
|
) {
|
|
69
71
|
super('hooks', 'Hooks', 'H', 'monitoring');
|
|
70
72
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
73
|
+
this.filterEnabled = true;
|
|
74
|
+
this.filterLabel = 'Filter hooks';
|
|
71
75
|
this.dataSource = dataSource;
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
protected override filterMatches(entry: HookEntry, q: string): boolean {
|
|
79
|
+
return (entry.hook.name ?? '').toLowerCase().includes(q)
|
|
80
|
+
|| entry.pattern.toLowerCase().includes(q)
|
|
81
|
+
|| entry.hook.type.toLowerCase().includes(q);
|
|
82
|
+
}
|
|
83
|
+
|
|
74
84
|
protected override getPalette() { return C; }
|
|
75
85
|
protected override getEmptyStateMessage() { return ' No hooks are currently registered.'; }
|
|
76
86
|
protected override getEmptyStateActions() {
|
|
@@ -96,7 +106,7 @@ export class HooksPanel extends ScrollableListPanel<HookEntry> {
|
|
|
96
106
|
}
|
|
97
107
|
|
|
98
108
|
public handleInput(key: string): boolean {
|
|
99
|
-
if (key === 'r') {
|
|
109
|
+
if (!this.filterActive && key === 'r') {
|
|
100
110
|
this.markDirty();
|
|
101
111
|
return true;
|
|
102
112
|
}
|
|
@@ -224,15 +234,39 @@ export class HooksPanel extends ScrollableListPanel<HookEntry> {
|
|
|
224
234
|
});
|
|
225
235
|
}
|
|
226
236
|
|
|
237
|
+
// Summary header — surface registration + activity counts first so the most
|
|
238
|
+
// important "what's firing" signal is visible without scrolling to the footer.
|
|
239
|
+
const denials = recentActivity.filter((r) => r.ok && r.decision === 'deny').length;
|
|
240
|
+
const errors = recentActivity.filter((r) => !r.ok).length;
|
|
241
|
+
const headerLines: Line[] = [
|
|
242
|
+
buildKeyValueLine(width, [
|
|
243
|
+
{ label: 'hooks', value: String(hooks.length), valueColor: C.info },
|
|
244
|
+
{ label: 'chains', value: String(chains.length), valueColor: C.value },
|
|
245
|
+
{ label: 'contracts', value: String(contracts.length), valueColor: C.value },
|
|
246
|
+
{ label: 'recent denials', value: String(denials), valueColor: denials > 0 ? C.warn : C.dim },
|
|
247
|
+
{ label: 'errors', value: String(errors), valueColor: errors > 0 ? C.error : C.dim },
|
|
248
|
+
], C),
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
const hints = this.filterActive
|
|
252
|
+
? [{ keys: 'type', label: 'filter' }, { keys: 'Enter', label: 'apply' }, { keys: 'Esc', label: 'clear' }]
|
|
253
|
+
: [
|
|
254
|
+
{ keys: 'Up/Down', label: 'move' },
|
|
255
|
+
{ keys: 'r', label: 'refresh' },
|
|
256
|
+
{ keys: '/hooks', label: 'full listing' },
|
|
257
|
+
{ keys: '/', label: 'filter' },
|
|
258
|
+
];
|
|
259
|
+
|
|
227
260
|
return this.renderList(width, height, {
|
|
228
261
|
title: 'Hooks Control Room',
|
|
262
|
+
header: headerLines,
|
|
229
263
|
footer: [
|
|
230
264
|
...detailLines,
|
|
231
265
|
buildPanelLine(width, [[' Recent Activity', C.label]]),
|
|
232
266
|
...activityLines,
|
|
233
267
|
buildPanelLine(width, [[' Authoring', C.label]]),
|
|
234
268
|
...authoringLines,
|
|
235
|
-
|
|
269
|
+
buildKeyboardHints(width, hints, C),
|
|
236
270
|
],
|
|
237
271
|
});
|
|
238
272
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
|
+
import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
|
|
2
3
|
import type { ForensicsRegistry } from '@/runtime/index.ts';
|
|
3
4
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
5
|
import {
|
|
@@ -42,6 +43,8 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
|
|
|
42
43
|
public constructor(registry?: ForensicsRegistry) {
|
|
43
44
|
super('incident', 'Incident Review', 'N', 'monitoring');
|
|
44
45
|
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
46
|
+
this.filterEnabled = true;
|
|
47
|
+
this.filterLabel = 'Filter incidents';
|
|
45
48
|
this.registry = registry;
|
|
46
49
|
this.unsub = registry ? registry.subscribe(() => this.markDirty()) : null;
|
|
47
50
|
}
|
|
@@ -58,13 +61,19 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
|
|
|
58
61
|
return this.registry?.getAll() ?? [];
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
protected override filterMatches(report: FailureReport, q: string): boolean {
|
|
65
|
+
return report.classification.toLowerCase().includes(q)
|
|
66
|
+
|| report.id.toLowerCase().includes(q)
|
|
67
|
+
|| (report.summary ?? '').toLowerCase().includes(q);
|
|
68
|
+
}
|
|
69
|
+
|
|
61
70
|
protected renderItem(report: FailureReport, index: number, selected: boolean, width: number): Line {
|
|
62
71
|
const bg = selected ? C.selectBg : undefined;
|
|
63
72
|
return buildPanelLine(width, [
|
|
64
73
|
[' ', C.label, bg],
|
|
65
|
-
[report.id
|
|
66
|
-
[report.classification
|
|
67
|
-
[report.summary
|
|
74
|
+
[fitDisplay(report.id, 9), C.dim, bg],
|
|
75
|
+
[fitDisplay(report.classification, 20), classificationColor(report.classification), bg],
|
|
76
|
+
[truncateDisplay(report.summary, Math.max(0, width - 31)), C.value, bg],
|
|
68
77
|
]);
|
|
69
78
|
}
|
|
70
79
|
|
|
@@ -117,7 +126,6 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
|
|
|
117
126
|
{ label: 'selected', value: `${this.selectedIndex + 1}/${reports.length}`, valueColor: C.info },
|
|
118
127
|
{ label: 'classification', value: selected.classification, valueColor: classificationColor(selected.classification) },
|
|
119
128
|
], C),
|
|
120
|
-
buildPanelLine(width, [[' Up/Down move Home/End jump selected incident drives the action rail below', C.dim]]),
|
|
121
129
|
];
|
|
122
130
|
|
|
123
131
|
const footerLines: Line[] = [];
|
|
@@ -134,26 +142,26 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
|
|
|
134
142
|
], C));
|
|
135
143
|
footerLines.push(buildPanelLine(width, [
|
|
136
144
|
[' Related IDs: ', C.label],
|
|
137
|
-
[`turn=${bundle.evidence.relatedIds.turnId ?? 'n/a'} task=${bundle.evidence.relatedIds.taskId ?? 'n/a'} agent=${bundle.evidence.relatedIds.agentId ?? 'n/a'}
|
|
145
|
+
[truncateDisplay(`turn=${bundle.evidence.relatedIds.turnId ?? 'n/a'} task=${bundle.evidence.relatedIds.taskId ?? 'n/a'} agent=${bundle.evidence.relatedIds.agentId ?? 'n/a'}`, Math.max(0, width - 14)), C.info],
|
|
138
146
|
]));
|
|
139
147
|
if (bundle.evidence.slowPhases.length > 0) {
|
|
140
148
|
footerLines.push(buildPanelLine(width, [
|
|
141
149
|
[' Slow phases: ', C.label],
|
|
142
|
-
...buildStatusPill('warn', bundle.evidence.slowPhases.join(', ')
|
|
150
|
+
...buildStatusPill('warn', truncateDisplay(bundle.evidence.slowPhases.join(', '), Math.max(0, width - 15))),
|
|
143
151
|
]));
|
|
144
152
|
}
|
|
145
153
|
const rootCause = selected.causalChain.find((entry) => entry.isRootCause);
|
|
146
154
|
if (rootCause) {
|
|
147
155
|
footerLines.push(buildPanelLine(width, [
|
|
148
156
|
[' Root event: ', C.label],
|
|
149
|
-
[`${rootCause.sourceEventType} - ${rootCause.description}
|
|
157
|
+
[truncateDisplay(`${rootCause.sourceEventType} - ${rootCause.description}`, Math.max(0, width - 14)), C.dim],
|
|
150
158
|
]));
|
|
151
159
|
}
|
|
152
160
|
const denied = selected.permissionEvidence.find((entry) => entry.approved === false);
|
|
153
161
|
if (denied) {
|
|
154
162
|
footerLines.push(buildPanelLine(width, [
|
|
155
163
|
[' Permission: ', C.label],
|
|
156
|
-
[`${denied.tool} denied${denied.riskLevel ? ` (${denied.riskLevel})` : ''}${denied.summary ? ` - ${denied.summary}` : ''}
|
|
164
|
+
[truncateDisplay(`${denied.tool} denied${denied.riskLevel ? ` (${denied.riskLevel})` : ''}${denied.summary ? ` - ${denied.summary}` : ''}`, Math.max(0, width - 14)), C.warn],
|
|
157
165
|
]));
|
|
158
166
|
}
|
|
159
167
|
if (bundle.replay.relatedMismatches.length > 0) {
|
|
@@ -168,7 +176,7 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
|
|
|
168
176
|
: `Replay link: ${mismatch.kind}${mismatch.ownerDomain ? `/${mismatch.ownerDomain}` : ''} - ${mismatch.description}`;
|
|
169
177
|
footerLines.push(buildPanelLine(width, [
|
|
170
178
|
[' ', C.label],
|
|
171
|
-
...buildStatusPill('bad', replayDetail
|
|
179
|
+
...buildStatusPill('bad', truncateDisplay(replayDetail, Math.max(0, width - 2))),
|
|
172
180
|
]));
|
|
173
181
|
} else {
|
|
174
182
|
const ownerBreakdown = Object.entries(bundle.replay.mismatchBreakdown.byOwnerDomain)
|
|
@@ -179,7 +187,7 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
|
|
|
179
187
|
if (ownerBreakdown.length > 0) {
|
|
180
188
|
footerLines.push(buildPanelLine(width, [
|
|
181
189
|
[' Replay owners: ', C.label],
|
|
182
|
-
[ownerBreakdown
|
|
190
|
+
[truncateDisplay(ownerBreakdown, Math.max(0, width - 17)), C.info],
|
|
183
191
|
]));
|
|
184
192
|
}
|
|
185
193
|
}
|
|
@@ -188,10 +196,23 @@ export class IncidentReviewPanel extends ScrollableListPanel<FailureReport> {
|
|
|
188
196
|
footerLines.push(buildPanelLine(width, [[` /incident latest /incident export ${selected.id} /recall capture incident ${selected.id}`, C.info]]));
|
|
189
197
|
footerLines.push(buildGuidanceLine(width, '/security', 'open the broader trust and incident posture control room', C));
|
|
190
198
|
|
|
199
|
+
const hints = this.filterActive
|
|
200
|
+
? [
|
|
201
|
+
{ keys: 'type', label: 'filter incidents' },
|
|
202
|
+
{ keys: 'Enter', label: 'apply' },
|
|
203
|
+
{ keys: 'Esc', label: 'clear' },
|
|
204
|
+
]
|
|
205
|
+
: [
|
|
206
|
+
{ keys: '↑/↓', label: 'select incident' },
|
|
207
|
+
{ keys: 'Home/End', label: 'jump' },
|
|
208
|
+
{ keys: '/', label: 'filter' },
|
|
209
|
+
];
|
|
210
|
+
|
|
191
211
|
return this.renderList(width, height, {
|
|
192
212
|
title: 'Incident Review Workspace',
|
|
193
213
|
header: headerLines,
|
|
194
214
|
footer: footerLines,
|
|
215
|
+
hints,
|
|
195
216
|
});
|
|
196
217
|
}
|
|
197
218
|
}
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
+
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
4
|
import { BasePanel } from './base-panel.ts';
|
|
4
5
|
import type { UiIntelligenceSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
|
|
5
6
|
import {
|
|
7
|
+
buildDetailBlock,
|
|
6
8
|
buildEmptyState,
|
|
7
9
|
buildGuidanceLine,
|
|
10
|
+
buildKeyboardHints,
|
|
8
11
|
buildKeyValueLine,
|
|
9
12
|
buildPanelLine,
|
|
10
13
|
buildPanelWorkspace,
|
|
14
|
+
buildStatusPill,
|
|
11
15
|
DEFAULT_PANEL_PALETTE,
|
|
12
16
|
type PanelWorkspaceSection,
|
|
17
|
+
type StyledPanelSegment,
|
|
13
18
|
} from './polish.ts';
|
|
14
19
|
|
|
15
20
|
const C = {
|
|
@@ -21,22 +26,29 @@ const C = {
|
|
|
21
26
|
headerBg: '#1e293b',
|
|
22
27
|
} as const;
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
29
|
+
interface DiagnosticFile {
|
|
30
|
+
readonly filePath: string;
|
|
31
|
+
readonly errors: number;
|
|
32
|
+
readonly warnings: number;
|
|
33
|
+
readonly diagnostics: ReadonlyArray<{
|
|
34
|
+
readonly line: number;
|
|
35
|
+
readonly column: number;
|
|
36
|
+
readonly severity: string;
|
|
37
|
+
readonly message: string;
|
|
38
|
+
readonly source?: string | undefined;
|
|
39
|
+
readonly code?: string | undefined;
|
|
40
|
+
}>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Map an intelligence-surface status string to a status-pill segment. */
|
|
44
|
+
function postureSegments(label: string, status: string): StyledPanelSegment[] {
|
|
45
|
+
const state = status === 'ready' ? 'good' : status === 'degraded' ? 'warn' : status === 'loading' ? 'info' : 'bad';
|
|
46
|
+
return buildStatusPill(state, `${label} ${status}`);
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
export class IntelligencePanel extends BasePanel {
|
|
39
50
|
private readonly unsub: (() => void) | null;
|
|
51
|
+
private selectedIndex = 0;
|
|
40
52
|
|
|
41
53
|
public constructor(private readonly readModel?: UiReadModel<UiIntelligenceSnapshot>) {
|
|
42
54
|
super('intelligence', 'Intelligence', 'J', 'development');
|
|
@@ -47,6 +59,53 @@ export class IntelligencePanel extends BasePanel {
|
|
|
47
59
|
this.unsub?.();
|
|
48
60
|
}
|
|
49
61
|
|
|
62
|
+
/** Diagnostic files sorted error-first so the most actionable file is first. */
|
|
63
|
+
private diagnosticFiles(): DiagnosticFile[] {
|
|
64
|
+
if (!this.readModel) return [];
|
|
65
|
+
const state = this.readModel.getSnapshot();
|
|
66
|
+
return [...state.diagnostics.entries()]
|
|
67
|
+
.map(([filePath, diagnostics]) => ({
|
|
68
|
+
filePath,
|
|
69
|
+
errors: diagnostics.filter((entry) => entry.severity === 'error').length,
|
|
70
|
+
warnings: diagnostics.filter((entry) => entry.severity === 'warning').length,
|
|
71
|
+
diagnostics: diagnostics.map((entry) => ({
|
|
72
|
+
line: entry.line,
|
|
73
|
+
column: entry.column,
|
|
74
|
+
severity: entry.severity,
|
|
75
|
+
message: entry.message,
|
|
76
|
+
source: entry.source,
|
|
77
|
+
code: entry.code,
|
|
78
|
+
})),
|
|
79
|
+
}))
|
|
80
|
+
.sort((a, b) => (b.errors - a.errors) || (b.warnings - a.warnings) || a.filePath.localeCompare(b.filePath));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public handleInput(key: string): boolean {
|
|
84
|
+
const files = this.diagnosticFiles();
|
|
85
|
+
if (files.length === 0) return false;
|
|
86
|
+
if (key === 'up' || key === 'k') {
|
|
87
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
88
|
+
this.markDirty();
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
if (key === 'down' || key === 'j') {
|
|
92
|
+
this.selectedIndex = Math.min(files.length - 1, this.selectedIndex + 1);
|
|
93
|
+
this.markDirty();
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
if (key === 'home' || key === 'g') {
|
|
97
|
+
this.selectedIndex = 0;
|
|
98
|
+
this.markDirty();
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
if (key === 'end' || key === 'G') {
|
|
102
|
+
this.selectedIndex = files.length - 1;
|
|
103
|
+
this.markDirty();
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
50
109
|
public render(width: number, height: number): Line[] {
|
|
51
110
|
this.needsRender = false;
|
|
52
111
|
if (!this.readModel) {
|
|
@@ -76,16 +135,25 @@ export class IntelligencePanel extends BasePanel {
|
|
|
76
135
|
state.hoverStatus,
|
|
77
136
|
].filter((status) => status !== 'ready').length;
|
|
78
137
|
|
|
138
|
+
const diagnosticFiles = this.diagnosticFiles();
|
|
139
|
+
this.selectedIndex = Math.max(0, Math.min(this.selectedIndex, Math.max(0, diagnosticFiles.length - 1)));
|
|
140
|
+
|
|
79
141
|
const sections: PanelWorkspaceSection[] = [
|
|
80
142
|
{
|
|
81
143
|
title: 'Intelligence posture',
|
|
82
144
|
lines: [
|
|
83
|
-
|
|
84
|
-
{
|
|
85
|
-
|
|
86
|
-
{
|
|
87
|
-
|
|
88
|
-
]
|
|
145
|
+
buildPanelLine(width, [
|
|
146
|
+
{ text: ' ', fg: C.dim },
|
|
147
|
+
...postureSegments('diagnostics', state.diagnosticsStatus),
|
|
148
|
+
{ text: ' ', fg: C.dim },
|
|
149
|
+
...postureSegments('symbols', state.symbolSearchStatus),
|
|
150
|
+
]),
|
|
151
|
+
buildPanelLine(width, [
|
|
152
|
+
{ text: ' ', fg: C.dim },
|
|
153
|
+
...postureSegments('completions', state.completionsStatus),
|
|
154
|
+
{ text: ' ', fg: C.dim },
|
|
155
|
+
...postureSegments('hover', state.hoverStatus),
|
|
156
|
+
]),
|
|
89
157
|
buildKeyValueLine(width, [
|
|
90
158
|
{ label: 'errors', value: String(state.errorCount), valueColor: state.errorCount > 0 ? C.bad : C.dim },
|
|
91
159
|
{ label: 'warnings', value: String(state.warningCount), valueColor: state.warningCount > 0 ? C.warn : C.dim },
|
|
@@ -94,80 +162,93 @@ export class IntelligencePanel extends BasePanel {
|
|
|
94
162
|
], C),
|
|
95
163
|
],
|
|
96
164
|
},
|
|
97
|
-
{
|
|
98
|
-
title: 'Next Actions',
|
|
99
|
-
lines: [
|
|
100
|
-
buildGuidanceLine(width, '/intelligence diagnostics', 'review readiness posture and current diagnostics activity', C),
|
|
101
|
-
buildGuidanceLine(width, '/intelligence repair', 'surface repair-oriented guidance when symbols, hover, or completions degrade', C),
|
|
102
|
-
],
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
title: 'Readiness',
|
|
106
|
-
lines: [
|
|
107
|
-
buildPanelLine(width, [[` Diagnostics are ${state.diagnosticsStatus}. Symbol search is ${state.symbolSearchStatus}.`, state.diagnosticsStatus === 'ready' && state.symbolSearchStatus === 'ready' ? C.dim : C.warn]]),
|
|
108
|
-
buildPanelLine(width, [[` Hover is ${state.hoverStatus}. Completions are ${state.completionsStatus}.`, state.hoverStatus === 'ready' && state.completionsStatus === 'ready' ? C.dim : C.warn]]),
|
|
109
|
-
...(state.hover.active && state.hover.filePath
|
|
110
|
-
? [buildPanelLine(width, [[` Active hover: ${state.hover.filePath}`, C.info]])]
|
|
111
|
-
: []),
|
|
112
|
-
],
|
|
113
|
-
},
|
|
114
165
|
];
|
|
115
166
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
167
|
+
// Diagnostics — the most actionable surface. Render as a selectable list so
|
|
168
|
+
// the operator can drill into a specific file's findings.
|
|
169
|
+
const visibleFiles = diagnosticFiles.slice(0, 6);
|
|
170
|
+
const diagnosticsLines: Line[] = diagnosticFiles.length > 0
|
|
171
|
+
? visibleFiles.map((entry, idx) => {
|
|
172
|
+
const selected = idx === this.selectedIndex;
|
|
173
|
+
const tone = entry.errors > 0 ? C.bad : entry.warnings > 0 ? C.warn : C.dim;
|
|
174
|
+
const counts = ` ${entry.errors} err ${entry.warnings} warn`;
|
|
175
|
+
const pathBudget = Math.max(8, width - counts.length - 4);
|
|
176
|
+
return buildPanelLine(width, [
|
|
177
|
+
[selected ? ' ▸ ' : ' ', selected ? C.info : C.dim, selected ? C.headerBg : undefined],
|
|
178
|
+
[truncateDisplay(entry.filePath, pathBudget), selected ? C.value : C.label, selected ? C.headerBg : undefined],
|
|
179
|
+
[counts, tone, selected ? C.headerBg : undefined],
|
|
180
|
+
]);
|
|
181
|
+
})
|
|
182
|
+
: [buildPanelLine(width, [[' No tracked diagnostics. Symbol/diagnostic surfaces are clear.', C.dim]])];
|
|
183
|
+
if (diagnosticFiles.length > visibleFiles.length) {
|
|
184
|
+
diagnosticsLines.push(buildPanelLine(width, [[` +${diagnosticFiles.length - visibleFiles.length} more files`, C.dim]]));
|
|
185
|
+
}
|
|
186
|
+
sections.push({ title: `Diagnostics (${diagnosticFiles.length})`, lines: diagnosticsLines });
|
|
133
187
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
]
|
|
143
|
-
|
|
188
|
+
// Drill-down detail for the selected diagnostic file.
|
|
189
|
+
const selectedFile = diagnosticFiles[this.selectedIndex];
|
|
190
|
+
if (selectedFile) {
|
|
191
|
+
const detailRows: Line[] = [
|
|
192
|
+
buildKeyValueLine(width, [
|
|
193
|
+
{ label: 'errors', value: String(selectedFile.errors), valueColor: selectedFile.errors > 0 ? C.bad : C.dim },
|
|
194
|
+
{ label: 'warnings', value: String(selectedFile.warnings), valueColor: selectedFile.warnings > 0 ? C.warn : C.dim },
|
|
195
|
+
], C),
|
|
196
|
+
];
|
|
197
|
+
const topFindings = [...selectedFile.diagnostics]
|
|
198
|
+
.sort((a, b) => (a.severity === 'error' ? 0 : 1) - (b.severity === 'error' ? 0 : 1))
|
|
199
|
+
.slice(0, 3);
|
|
200
|
+
for (const finding of topFindings) {
|
|
201
|
+
const loc = `${finding.line + 1}:${finding.column + 1}`;
|
|
202
|
+
const tag = finding.source ? `${finding.source}${finding.code ? `(${finding.code})` : ''}` : finding.severity;
|
|
203
|
+
const prefix = ` ${loc} ${tag}: `;
|
|
204
|
+
detailRows.push(buildPanelLine(width, [
|
|
205
|
+
[prefix, finding.severity === 'error' ? C.bad : C.warn],
|
|
206
|
+
[truncateDisplay(finding.message, Math.max(8, width - prefix.length - 1)), C.value],
|
|
207
|
+
]));
|
|
208
|
+
}
|
|
209
|
+
if (selectedFile.diagnostics.length > topFindings.length) {
|
|
210
|
+
detailRows.push(buildPanelLine(width, [[` +${selectedFile.diagnostics.length - topFindings.length} more findings`, C.dim]]));
|
|
211
|
+
}
|
|
212
|
+
detailRows.push(buildGuidanceLine(width, `/intelligence diagnostics ${selectedFile.filePath}`, 'open the full diagnostic list for this file', C));
|
|
213
|
+
sections.push({ lines: buildDetailBlock(width, truncateDisplay(selectedFile.filePath, Math.max(8, width - 4)), detailRows, C) });
|
|
214
|
+
}
|
|
144
215
|
|
|
145
216
|
if (degraded > 0) {
|
|
146
217
|
sections.push({
|
|
147
218
|
title: 'Recovery',
|
|
148
219
|
lines: [
|
|
149
|
-
buildPanelLine(width, [[' Workspace intelligence is not fully ready. Review LSP/tree-sitter setup and
|
|
150
|
-
buildGuidanceLine(width, '/
|
|
151
|
-
buildGuidanceLine(width, '/
|
|
152
|
-
buildGuidanceLine(width, '/intelligence repair', 'show repair-oriented commands for diagnostics, symbols, hover, and completions', C),
|
|
153
|
-
buildGuidanceLine(width, '/health repair intelligence', 'show repair commands plus post-repair verification for the intelligence domain', C),
|
|
220
|
+
buildPanelLine(width, [[' Workspace intelligence is not fully ready. Review LSP/tree-sitter setup and language configuration.', C.warn]]),
|
|
221
|
+
buildGuidanceLine(width, '/intelligence repair', 'show repair commands for diagnostics, symbols, hover, and completions', C),
|
|
222
|
+
buildGuidanceLine(width, '/health repair intelligence', 'show repair commands plus post-repair verification', C),
|
|
154
223
|
],
|
|
155
224
|
});
|
|
156
225
|
} else {
|
|
157
226
|
sections.push({
|
|
158
|
-
title: '
|
|
227
|
+
title: 'Workflows',
|
|
159
228
|
lines: [
|
|
160
|
-
|
|
161
|
-
buildGuidanceLine(width, '/
|
|
229
|
+
buildGuidanceLine(width, '/intelligence symbols <file>', 'inspect document symbols and verify symbol-surface readiness', C),
|
|
230
|
+
buildGuidanceLine(width, '/intelligence definition <file> <line> <column>', 'check definition lookup for an exact source position', C),
|
|
231
|
+
buildGuidanceLine(width, '/intelligence hover <file> <line> <column>', 'inspect hover/details posture for a source position', C),
|
|
162
232
|
],
|
|
163
233
|
});
|
|
164
234
|
}
|
|
165
235
|
|
|
236
|
+
const footerLines = diagnosticFiles.length > 0
|
|
237
|
+
? [buildKeyboardHints(width, [
|
|
238
|
+
{ keys: '↑/↓', label: 'select file' },
|
|
239
|
+
{ keys: 'Home/End', label: 'jump' },
|
|
240
|
+
{ keys: '/intelligence', label: 'commands' },
|
|
241
|
+
], C)]
|
|
242
|
+
: [buildKeyboardHints(width, [
|
|
243
|
+
{ keys: '/intelligence diagnostics', label: 'review' },
|
|
244
|
+
{ keys: '/intelligence symbols <file>', label: 'inspect symbols' },
|
|
245
|
+
], C)];
|
|
246
|
+
|
|
166
247
|
const lines = buildPanelWorkspace(width, height, {
|
|
167
248
|
title: 'Intelligence Control Room',
|
|
168
249
|
intro: 'Workspace intelligence posture across diagnostics, symbol search, hover, and completion readiness.',
|
|
169
250
|
sections,
|
|
170
|
-
footerLines
|
|
251
|
+
footerLines,
|
|
171
252
|
palette: C,
|
|
172
253
|
});
|
|
173
254
|
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
@@ -8,13 +8,20 @@
|
|
|
8
8
|
* This panel is a thin information surface that explains the graph's capabilities
|
|
9
9
|
* and routes the user to the /knowledge command suite for ingest/RAG operations.
|
|
10
10
|
* The full graph UI is command-driven (/knowledge ask, ingest-url, list, search…).
|
|
11
|
+
*
|
|
12
|
+
* It is selectable: the operator can move a cursor across the command catalogue
|
|
13
|
+
* so the currently-highlighted command is unambiguous and a one-line "what this
|
|
14
|
+
* does" detail is surfaced for it.
|
|
11
15
|
*/
|
|
12
16
|
|
|
13
17
|
import type { Line } from '../types/grid.ts';
|
|
18
|
+
import { createEmptyLine } from '../types/grid.ts';
|
|
14
19
|
import { BasePanel } from './base-panel.ts';
|
|
15
20
|
import {
|
|
16
21
|
buildBodyText,
|
|
22
|
+
buildDetailBlock,
|
|
17
23
|
buildGuidanceLine,
|
|
24
|
+
buildKeyboardHints,
|
|
18
25
|
buildPanelLine,
|
|
19
26
|
buildPanelWorkspace,
|
|
20
27
|
DEFAULT_PANEL_PALETTE,
|
|
@@ -26,59 +33,114 @@ const C = {
|
|
|
26
33
|
headerBg: '#1e293b',
|
|
27
34
|
} as const;
|
|
28
35
|
|
|
36
|
+
interface CommandEntry {
|
|
37
|
+
readonly command: string;
|
|
38
|
+
readonly summary: string;
|
|
39
|
+
readonly detail: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const GRAPH_COMMANDS: readonly CommandEntry[] = [
|
|
43
|
+
{ command: '/knowledge status', summary: 'check the graph status and source counts', detail: 'Reports node/edge totals, ingested source count, and embedding-provider readiness.' },
|
|
44
|
+
{ command: '/knowledge ask <query>', summary: 'ask a question against the ingested knowledge', detail: 'Runs retrieval-augmented Q&A over ingested sources and returns a cited answer.' },
|
|
45
|
+
{ command: '/knowledge ingest-url <url>', summary: 'ingest a URL as a knowledge source', detail: 'Fetches a URL, extracts content, and stores it as graph nodes and edges.' },
|
|
46
|
+
{ command: '/knowledge list', summary: 'list ingested sources or graph nodes', detail: 'Enumerates the current sources and top-level nodes in the graph.' },
|
|
47
|
+
{ command: '/knowledge search <query>', summary: 'search the graph for nodes and sources', detail: 'Keyword/semantic search across stored nodes without generating an answer.' },
|
|
48
|
+
{ command: '/knowledge packet <task>', summary: 'build a compact prompt packet for a task', detail: 'Assembles a token-budgeted context packet of the most relevant facts for a task.' },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const MEMORY_COMMANDS: readonly CommandEntry[] = [
|
|
52
|
+
{ command: '/recall add <class> <summary>', summary: 'capture a new memory record', detail: 'Stores a durable decision, risk, runbook, incident, or architecture record.' },
|
|
53
|
+
{ command: '/recall queue', summary: 'show the operator review queue', detail: 'Lists stale and contradicted records awaiting operator review.' },
|
|
54
|
+
{ command: '/project-memory (pmem)', summary: 'project-memory alias for /recall front-door', detail: 'Shorthand entry point to the same durable project-memory surface.' },
|
|
55
|
+
];
|
|
56
|
+
|
|
29
57
|
export class KnowledgeGraphPanel extends BasePanel {
|
|
58
|
+
private selectedIndex = 0;
|
|
59
|
+
|
|
30
60
|
constructor() {
|
|
31
61
|
super('knowledge', 'Knowledge', 'K', 'agent');
|
|
32
62
|
}
|
|
33
63
|
|
|
34
|
-
|
|
64
|
+
private get entries(): readonly CommandEntry[] {
|
|
65
|
+
return [...GRAPH_COMMANDS, ...MEMORY_COMMANDS];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
handleInput(key: string): boolean {
|
|
69
|
+
const count = this.entries.length;
|
|
70
|
+
if (key === 'up' || key === 'k') {
|
|
71
|
+
this.selectedIndex = (this.selectedIndex - 1 + count) % count;
|
|
72
|
+
this.markDirty();
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
if (key === 'down' || key === 'j') {
|
|
76
|
+
this.selectedIndex = (this.selectedIndex + 1) % count;
|
|
77
|
+
this.markDirty();
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
if (key === 'home' || key === 'g') {
|
|
81
|
+
this.selectedIndex = 0;
|
|
82
|
+
this.markDirty();
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
if (key === 'end' || key === 'G') {
|
|
86
|
+
this.selectedIndex = count - 1;
|
|
87
|
+
this.markDirty();
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
35
90
|
return false;
|
|
36
91
|
}
|
|
37
92
|
|
|
93
|
+
private commandLines(width: number, group: readonly CommandEntry[], offset: number): Line[] {
|
|
94
|
+
return group.map((entry, idx) => {
|
|
95
|
+
const absIdx = offset + idx;
|
|
96
|
+
const selected = absIdx === this.selectedIndex;
|
|
97
|
+
if (selected) {
|
|
98
|
+
return buildPanelLine(width, [
|
|
99
|
+
[' ▸ ', C.info, C.headerBg],
|
|
100
|
+
[`${entry.command} `, C.value, C.headerBg],
|
|
101
|
+
[entry.summary, C.dim, C.headerBg],
|
|
102
|
+
]);
|
|
103
|
+
}
|
|
104
|
+
return buildGuidanceLine(width, entry.command, entry.summary, C);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
38
108
|
render(width: number, height: number): Line[] {
|
|
109
|
+
this.selectedIndex = Math.max(0, Math.min(this.selectedIndex, this.entries.length - 1));
|
|
110
|
+
const selected = this.entries[this.selectedIndex]!;
|
|
111
|
+
|
|
39
112
|
const sections = [
|
|
40
113
|
{
|
|
41
114
|
title: 'SDK Knowledge Graph',
|
|
42
115
|
lines: [
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
'The knowledge graph stores ingested URLs, bookmarks, and structured facts as nodes and edges. ' +
|
|
46
|
-
'Use /knowledge commands to ingest sources, search the graph, and build task-context packets.',
|
|
47
|
-
C,
|
|
48
|
-
C.value,
|
|
49
|
-
),
|
|
50
|
-
buildPanelLine(width, [['', C.dim]]),
|
|
51
|
-
buildGuidanceLine(width, '/knowledge status', 'check the graph status and source counts', C),
|
|
52
|
-
buildGuidanceLine(width, '/knowledge ask <query>', 'ask a question against the ingested knowledge', C),
|
|
53
|
-
buildGuidanceLine(width, '/knowledge ingest-url <url>', 'ingest a URL as a knowledge source', C),
|
|
54
|
-
buildGuidanceLine(width, '/knowledge list', 'list ingested sources or graph nodes', C),
|
|
55
|
-
buildGuidanceLine(width, '/knowledge search <query>', 'search the graph for nodes and sources', C),
|
|
56
|
-
buildGuidanceLine(width, '/knowledge packet <task>', 'build a compact prompt packet for a task', C),
|
|
116
|
+
buildPanelLine(width, [[' Ingest URLs and facts as graph nodes; search and build task-context packets.', C.dim]]),
|
|
117
|
+
...this.commandLines(width, GRAPH_COMMANDS, 0),
|
|
57
118
|
],
|
|
58
119
|
},
|
|
59
120
|
{
|
|
60
121
|
title: 'Project Memory',
|
|
61
122
|
lines: [
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
'For durable decisions, risks, runbooks, incidents, and architecture records, use the Memory panel ' +
|
|
65
|
-
'or the /recall command surface. Durable memory is a sub-namespace of the knowledge graph.',
|
|
66
|
-
C,
|
|
67
|
-
C.dim,
|
|
68
|
-
),
|
|
69
|
-
buildPanelLine(width, [['', C.dim]]),
|
|
70
|
-
buildGuidanceLine(width, '/recall add <class> <summary>', 'capture a new memory record', C),
|
|
71
|
-
buildGuidanceLine(width, '/recall queue', 'show the operator review queue', C),
|
|
72
|
-
buildGuidanceLine(width, '/project-memory (pmem)', 'project-memory alias for /recall front-door', C),
|
|
123
|
+
buildPanelLine(width, [[' Durable decisions, risks, runbooks, and incidents — a sub-namespace of the graph.', C.dim]]),
|
|
124
|
+
...this.commandLines(width, MEMORY_COMMANDS, GRAPH_COMMANDS.length),
|
|
73
125
|
],
|
|
74
126
|
},
|
|
127
|
+
{
|
|
128
|
+
lines: buildDetailBlock(width, selected.command, buildBodyText(width, selected.detail, C, C.value), C),
|
|
129
|
+
},
|
|
75
130
|
];
|
|
76
131
|
|
|
77
|
-
|
|
132
|
+
const lines = buildPanelWorkspace(width, height, {
|
|
78
133
|
title: 'Knowledge Graph',
|
|
79
134
|
intro: 'Ingested sources, graph nodes, and the durable memory bridge.',
|
|
80
135
|
sections,
|
|
136
|
+
footerLines: [buildKeyboardHints(width, [
|
|
137
|
+
{ keys: '↑/↓', label: 'browse commands' },
|
|
138
|
+
{ keys: 'Home/End', label: 'jump' },
|
|
139
|
+
{ keys: 'M', label: 'Memory panel' },
|
|
140
|
+
], C)],
|
|
81
141
|
palette: C,
|
|
82
142
|
});
|
|
143
|
+
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
144
|
+
return lines.slice(0, height);
|
|
83
145
|
}
|
|
84
146
|
}
|