@pellux/goodvibes-tui 0.27.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 +23 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/context-usage.ts +53 -0
- package/src/core/conversation.ts +4 -6
- package/src/core/session-recovery.ts +2 -0
- package/src/core/turn-event-wiring.ts +1 -0
- package/src/input/commands/eval.ts +4 -3
- package/src/input/handler-feed-routes.ts +61 -4
- package/src/input/handler-feed.ts +13 -0
- package/src/input/handler-picker-routes.ts +18 -13
- package/src/input/handler-ui-state.ts +4 -0
- 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/main.ts +0 -1
- package/src/panels/agent-inspector-panel.ts +18 -13
- package/src/panels/agent-inspector-shared.ts +3 -3
- package/src/panels/agent-logs-panel.ts +22 -53
- package/src/panels/approval-panel.ts +146 -95
- package/src/panels/automation-control-panel.ts +24 -1
- package/src/panels/base-panel.ts +26 -2
- package/src/panels/builtin/development.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 +25 -1
- package/src/panels/control-plane-panel.ts +27 -1
- package/src/panels/cost-tracker-panel.ts +82 -54
- package/src/panels/debug-panel.ts +51 -16
- package/src/panels/diff-panel.ts +59 -30
- package/src/panels/docs-panel.ts +19 -3
- package/src/panels/eval-panel.ts +130 -81
- package/src/panels/expandable-list-panel.ts +189 -0
- package/src/panels/file-explorer-panel.ts +40 -41
- package/src/panels/file-preview-panel.ts +9 -2
- package/src/panels/forensics-panel.ts +27 -13
- package/src/panels/git-panel.ts +58 -70
- 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 +31 -12
- package/src/panels/memory-panel.ts +5 -4
- 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 +22 -0
- package/src/panels/provider-accounts-panel.ts +55 -18
- package/src/panels/provider-health-panel.ts +65 -23
- 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 +40 -17
- 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 +29 -5
- package/src/panels/session-maintenance.ts +2 -2
- package/src/panels/settings-sync-panel.ts +30 -15
- 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 +34 -7
- package/src/panels/thinking-panel.ts +26 -25
- package/src/panels/token-budget-panel.ts +49 -38
- package/src/panels/tool-inspector-panel.ts +27 -21
- package/src/panels/types.ts +25 -0
- package/src/panels/watchers-panel.ts +25 -5
- package/src/panels/work-plan-panel.ts +125 -34
- package/src/panels/worktree-panel.ts +65 -20
- package/src/panels/wrfc-panel-format.ts +133 -0
- package/src/panels/wrfc-panel.ts +31 -127
- package/src/renderer/compaction-preview.ts +2 -1
- package/src/renderer/compositor.ts +46 -43
- 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/progress.ts +3 -2
- package/src/renderer/tab-strip.ts +148 -34
- package/src/renderer/ui-factory.ts +4 -6
- package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
- package/src/runtime/ui/model-picker/types.ts +4 -0
- package/src/utils/format-duration.ts +55 -0
- package/src/utils/format-number.ts +71 -0
- package/src/version.ts +1 -1
- 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
|
@@ -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
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
+
import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
|
|
3
4
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
5
|
import {
|
|
5
6
|
buildDetailBlock,
|
|
@@ -136,8 +137,8 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
|
136
137
|
|
|
137
138
|
protected renderItem(user: LocalAuthUser, _index: number, selected: boolean, width: number): Line {
|
|
138
139
|
return buildPanelListRow(width, [
|
|
139
|
-
{ text: user.username
|
|
140
|
-
{ text: ` roles=${formatRoles(user.roles)}
|
|
140
|
+
{ text: fitDisplay(user.username, 20), fg: C.value },
|
|
141
|
+
{ text: truncateDisplay(` roles=${formatRoles(user.roles)}`, Math.max(0, width - 24)), fg: C.info },
|
|
141
142
|
], C, { selected });
|
|
142
143
|
}
|
|
143
144
|
|
|
@@ -171,10 +172,10 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
|
171
172
|
[' bootstrap ', C.label],
|
|
172
173
|
[snapshot.bootstrapCredentialPresent ? 'present' : 'cleared', snapshot.bootstrapCredentialPresent ? C.warn : C.good],
|
|
173
174
|
]),
|
|
174
|
-
buildPanelLine(width, [[' user store ', C.label], [snapshot.userStorePath
|
|
175
|
-
buildPanelLine(width, [[' bootstrap file ', C.label], [snapshot.bootstrapCredentialPath
|
|
175
|
+
buildPanelLine(width, [[' user store ', C.label], [truncateDisplay(snapshot.userStorePath, Math.max(0, width - 13)), C.dim]]),
|
|
176
|
+
buildPanelLine(width, [[' bootstrap file ', C.label], [truncateDisplay(snapshot.bootstrapCredentialPath, Math.max(0, width - 18)), C.dim]]),
|
|
176
177
|
...(issueMessages.length > 0
|
|
177
|
-
? issueMessages.map((issue) => buildPanelLine(width, [[` issue: ${issue}
|
|
178
|
+
? issueMessages.map((issue) => buildPanelLine(width, [[truncateDisplay(` issue: ${issue}`, width), C.warn]]))
|
|
178
179
|
: [buildPanelLine(width, [[' local auth posture looks healthy.', C.good]])]),
|
|
179
180
|
buildGuidanceLine(width, '/auth local rotate-password <user>', 'open masked password entry for the selected user (no plaintext in history)', C),
|
|
180
181
|
], C),
|
|
@@ -198,19 +199,20 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
|
198
199
|
if (selected) {
|
|
199
200
|
footerLines.push(
|
|
200
201
|
...buildDetailBlock(width, 'Selected user', [
|
|
201
|
-
buildPanelLine(width, [[' username ', C.label], [selected.username, C.value], [' roles ', C.label], [formatRoles(selected.roles)
|
|
202
|
-
buildPanelLine(width, [[` next: /auth local rotate-password ${selected.username}
|
|
203
|
-
buildPanelLine(width, [[` next: /auth local delete-user ${selected.username}
|
|
202
|
+
buildPanelLine(width, [[' username ', C.label], [selected.username, C.value], [' roles ', C.label], [truncateDisplay(formatRoles(selected.roles), Math.max(0, width - 23)), C.info]]),
|
|
203
|
+
buildPanelLine(width, [[truncateDisplay(` next: /auth local rotate-password ${selected.username}`, width), C.dim]]),
|
|
204
|
+
buildPanelLine(width, [[truncateDisplay(` next: /auth local delete-user ${selected.username}`, width), C.dim]]),
|
|
204
205
|
], C),
|
|
205
206
|
);
|
|
206
207
|
}
|
|
207
208
|
|
|
208
209
|
if (snapshot.sessions.length > 0) {
|
|
210
|
+
footerLines.push(buildPanelLine(width, [[` Active sessions (${snapshot.sessions.length})`, C.label]]));
|
|
209
211
|
footerLines.push(
|
|
210
212
|
...snapshot.sessions.slice(0, 8).map((session) => buildPanelLine(width, [
|
|
211
213
|
[' ', C.label],
|
|
212
|
-
[session.username
|
|
213
|
-
[` expires ${new Date(session.expiresAt).toLocaleString()}
|
|
214
|
+
[fitDisplay(session.username, 18), C.value],
|
|
215
|
+
[truncateDisplay(` expires ${new Date(session.expiresAt).toLocaleString()}`, Math.max(0, width - 20)), C.dim],
|
|
214
216
|
])),
|
|
215
217
|
);
|
|
216
218
|
}
|
|
@@ -220,6 +222,10 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
|
220
222
|
title: 'Local Auth Control Room',
|
|
221
223
|
header: headerLines,
|
|
222
224
|
footer: footerLines,
|
|
225
|
+
hints: [
|
|
226
|
+
{ keys: '↑/↓', label: 'select user' },
|
|
227
|
+
{ keys: '/auth local rotate-password', label: 'masked entry' },
|
|
228
|
+
],
|
|
223
229
|
});
|
|
224
230
|
}
|
|
225
231
|
|
|
@@ -233,7 +239,7 @@ export class LocalAuthPanel extends ScrollableListPanel<LocalAuthUser> {
|
|
|
233
239
|
const promptLines: Line[] = [
|
|
234
240
|
buildPanelLine(width, [[` ${actionLabel}: ${state.username}`, C.value]]),
|
|
235
241
|
buildPanelLine(width, [['', C.label]]),
|
|
236
|
-
buildPanelLine(width, [[' Password ', C.label], [maskedDisplay
|
|
242
|
+
buildPanelLine(width, [[' Password ', C.label], [truncateDisplay(maskedDisplay, Math.max(0, width - 12)), C.info]]),
|
|
237
243
|
buildPanelLine(width, [['', C.label]]),
|
|
238
244
|
buildPanelLine(width, [[' [Enter] Confirm [Esc] Cancel [Backspace] Delete char', C.dim]]),
|
|
239
245
|
];
|