@pellux/goodvibes-tui 0.18.20 → 0.19.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 +154 -0
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +7 -3
- package/src/core/conversation-rendering.ts +22 -6
- package/src/core/orchestrator.ts +1 -1
- package/src/input/commands/diff-runtime.ts +6 -5
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +2 -2
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/session-content.ts +1 -1
- package/src/input/commands/session.ts +0 -1
- package/src/input/commands/shell-core.ts +3 -2
- package/src/input/commands/skills-runtime.ts +2 -2
- package/src/input/commands/subscription-runtime.ts +4 -4
- package/src/input/feed-context-factory.ts +236 -0
- package/src/input/handler-feed.ts +44 -6
- package/src/input/handler-shortcuts.ts +138 -125
- package/src/input/handler.ts +119 -119
- package/src/input/keybindings.ts +30 -0
- package/src/input/panel-integration-actions.ts +2 -1
- package/src/input/settings-modal-types.ts +60 -0
- package/src/input/settings-modal.ts +83 -65
- package/src/panels/agent-inspector-panel.ts +10 -9
- package/src/panels/agent-logs-panel.ts +26 -6
- package/src/panels/approval-panel.ts +55 -82
- package/src/panels/automation-control-panel.ts +120 -161
- package/src/panels/base-panel.ts +108 -3
- package/src/panels/communication-panel.ts +69 -107
- package/src/panels/context-visualizer-panel.ts +2 -0
- package/src/panels/control-plane-panel.ts +117 -172
- package/src/panels/diff-panel.ts +2 -0
- package/src/panels/file-explorer-panel.ts +51 -31
- package/src/panels/file-preview-panel.ts +57 -35
- package/src/panels/git-panel.ts +12 -13
- package/src/panels/hooks-panel.ts +103 -138
- package/src/panels/incident-review-panel.ts +59 -109
- package/src/panels/knowledge-panel.ts +75 -107
- package/src/panels/local-auth-panel.ts +77 -93
- package/src/panels/marketplace-panel.ts +51 -69
- package/src/panels/mcp-panel.ts +110 -155
- package/src/panels/memory-panel.ts +90 -158
- package/src/panels/ops-control-panel.ts +51 -85
- package/src/panels/orchestration-panel.ts +70 -51
- package/src/panels/panel-list-panel.ts +5 -4
- package/src/panels/panel-manager.ts +25 -2
- package/src/panels/plan-dashboard-panel.ts +2 -0
- package/src/panels/plugins-panel.ts +37 -60
- package/src/panels/polish.ts +51 -2
- package/src/panels/provider-accounts-panel.ts +1 -0
- package/src/panels/provider-health-panel.ts +6 -8
- package/src/panels/routes-panel.ts +91 -141
- package/src/panels/schedule-panel.ts +7 -6
- package/src/panels/scrollable-list-panel.ts +64 -16
- package/src/panels/security-panel.ts +118 -152
- package/src/panels/services-panel.ts +63 -105
- package/src/panels/session-browser-panel.ts +19 -18
- package/src/panels/settings-sync-panel.ts +79 -123
- package/src/panels/skills-panel.ts +114 -230
- package/src/panels/subscription-panel.ts +64 -86
- package/src/panels/system-messages-panel.ts +147 -141
- package/src/panels/tasks-panel.ts +130 -179
- package/src/panels/token-budget-panel.ts +2 -0
- package/src/panels/watchers-panel.ts +89 -137
- package/src/panels/worktree-panel.ts +1 -0
- package/src/panels/wrfc-panel.ts +2 -0
- package/src/renderer/agent-detail-modal.ts +2 -2
- package/src/renderer/ansi-sanitize.ts +76 -0
- package/src/renderer/buffer.ts +23 -1
- package/src/renderer/diff.ts +8 -0
- package/src/renderer/help-overlay.ts +48 -28
- package/src/renderer/markdown.ts +3 -145
- package/src/renderer/settings-modal-helpers.ts +27 -0
- package/src/renderer/settings-modal.ts +18 -1
- package/src/renderer/status-glyphs.ts +21 -0
- package/src/renderer/status-token.ts +4 -8
- package/src/renderer/tool-call.ts +4 -3
- package/src/runtime/bootstrap-core.ts +1 -1
- package/src/runtime/bootstrap-hook-bridge.ts +1 -1
- package/src/runtime/bootstrap.ts +7 -8
- package/src/runtime/diagnostics/panels/policy.ts +2 -1
- package/src/shell/ui-openers.ts +1 -1
- package/src/version.ts +1 -1
|
@@ -1,33 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MemoryPanel — project memory substrate TUI panel.
|
|
3
|
+
*
|
|
4
|
+
* Migrated to SearchableListPanel<MemoryRecord> (Wave B1).
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
7
|
import type { Line } from '../types/grid.ts';
|
|
6
8
|
import type { MemoryRegistry } from '@pellux/goodvibes-sdk/platform/state/memory-store';
|
|
7
9
|
import type { MemoryRecord, MemoryClass } from '@pellux/goodvibes-sdk/platform/state/memory-store';
|
|
8
|
-
import {
|
|
10
|
+
import { SearchableListPanel } from './scrollable-list-panel.ts';
|
|
9
11
|
import {
|
|
10
12
|
buildBodyText,
|
|
11
|
-
buildEmptyState,
|
|
12
13
|
buildGuidanceLine,
|
|
13
14
|
buildKeyValueLine,
|
|
14
15
|
buildPanelLine,
|
|
15
|
-
|
|
16
|
-
buildPanelWorkspace,
|
|
17
|
-
resolveScrollablePanelSection,
|
|
16
|
+
extendPalette,
|
|
18
17
|
DEFAULT_PANEL_PALETTE,
|
|
19
|
-
type PanelWorkspaceSection,
|
|
20
18
|
} from './polish.ts';
|
|
21
19
|
import {
|
|
22
20
|
getPanelSearchFocusTransition,
|
|
23
|
-
isPanelSearchBackspace,
|
|
24
21
|
isPanelSearchCancel,
|
|
25
|
-
isPanelSearchCommit,
|
|
26
|
-
isPanelSearchPrintable,
|
|
27
22
|
} from './search-focus.ts';
|
|
28
23
|
|
|
29
|
-
const C = {
|
|
30
|
-
...DEFAULT_PANEL_PALETTE,
|
|
24
|
+
const C = extendPalette(DEFAULT_PANEL_PALETTE, {
|
|
31
25
|
header: '#94a3b8',
|
|
32
26
|
headerBg: '#1e293b',
|
|
33
27
|
decision: '#38bdf8',
|
|
@@ -42,7 +36,7 @@ const C = {
|
|
|
42
36
|
selected: '#1e3a5f',
|
|
43
37
|
searchBg: '#0f172a',
|
|
44
38
|
searchFg: '#e2e8f0',
|
|
45
|
-
}
|
|
39
|
+
});
|
|
46
40
|
|
|
47
41
|
function fmtTime(ts: number): string {
|
|
48
42
|
const d = new Date(ts);
|
|
@@ -63,13 +57,9 @@ function classColor(cls: MemoryClass): string {
|
|
|
63
57
|
}
|
|
64
58
|
}
|
|
65
59
|
|
|
66
|
-
export class MemoryPanel extends
|
|
60
|
+
export class MemoryPanel extends SearchableListPanel<MemoryRecord> {
|
|
67
61
|
private registry: MemoryRegistry;
|
|
68
|
-
private
|
|
69
|
-
private selectedIdx = 0;
|
|
70
|
-
private scrollOffset = 0;
|
|
71
|
-
private searchMode = false;
|
|
72
|
-
private searchQuery = '';
|
|
62
|
+
private filterFocused = false;
|
|
73
63
|
private unsubscribe?: () => void;
|
|
74
64
|
|
|
75
65
|
constructor(registry: MemoryRegistry) {
|
|
@@ -79,9 +69,11 @@ export class MemoryPanel extends BasePanel {
|
|
|
79
69
|
|
|
80
70
|
onActivate(): void {
|
|
81
71
|
super.onActivate();
|
|
82
|
-
this.
|
|
72
|
+
this.searchQuery = '';
|
|
73
|
+
this.invalidateFilter();
|
|
74
|
+
this.filterFocused = false;
|
|
83
75
|
this.unsubscribe = this.registry.subscribe(() => {
|
|
84
|
-
this.
|
|
76
|
+
this.invalidateFilter();
|
|
85
77
|
this.markDirty();
|
|
86
78
|
});
|
|
87
79
|
}
|
|
@@ -95,134 +87,112 @@ export class MemoryPanel extends BasePanel {
|
|
|
95
87
|
this.unsubscribe = undefined;
|
|
96
88
|
}
|
|
97
89
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// SearchableListPanel implementation
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
protected getAllItems(): readonly MemoryRecord[] {
|
|
95
|
+
return this.registry.search({ limit: 100 });
|
|
104
96
|
}
|
|
105
97
|
|
|
106
|
-
|
|
107
|
-
|
|
98
|
+
protected matchesSearch(record: MemoryRecord, query: string): boolean {
|
|
99
|
+
const q = query.trim().toLowerCase();
|
|
100
|
+
if (!q) return true;
|
|
101
|
+
const haystack = [
|
|
102
|
+
record.summary,
|
|
103
|
+
record.detail ?? '',
|
|
104
|
+
record.cls,
|
|
105
|
+
record.scope,
|
|
106
|
+
record.tags.join(' '),
|
|
107
|
+
].join(' ').toLowerCase();
|
|
108
|
+
return haystack.includes(q);
|
|
109
|
+
}
|
|
108
110
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
protected renderItem(record: MemoryRecord, index: number, selected: boolean, width: number): Line {
|
|
112
|
+
const bg = selected ? C.selected : undefined;
|
|
113
|
+
return buildPanelLine(width, [
|
|
114
|
+
[' ', C.label, bg],
|
|
115
|
+
[`[${record.scope.slice(0, 1).toUpperCase()}/${record.cls.slice(0, 3).toUpperCase()}] `, classColor(record.cls), bg],
|
|
116
|
+
[record.id.slice(-8), C.dim, bg],
|
|
117
|
+
[' ', C.label, bg],
|
|
118
|
+
[fmtTime(record.createdAt), C.dim, bg],
|
|
119
|
+
[' ', C.label, bg],
|
|
120
|
+
[record.summary.slice(0, Math.max(0, width - 33)), C.value, bg],
|
|
121
|
+
]);
|
|
122
|
+
}
|
|
115
123
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
return true;
|
|
138
|
-
case 'r':
|
|
139
|
-
this.reload();
|
|
124
|
+
protected override getPalette() { return C; }
|
|
125
|
+
protected override getEmptyStateMessage() {
|
|
126
|
+
return this.searchQuery
|
|
127
|
+
? ` No records matching "${this.searchQuery}"`
|
|
128
|
+
: ' No memory records. Use /recall add <class> <summary> to create one.';
|
|
129
|
+
}
|
|
130
|
+
protected override getEmptyStateActions() {
|
|
131
|
+
return [
|
|
132
|
+
{ command: '/recall add fact <summary>', summary: 'capture a durable fact directly' },
|
|
133
|
+
{ command: '/recall capture incident latest', summary: 'promote the latest incident into memory' },
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
handleInput(key: string): boolean {
|
|
138
|
+
// Filter-focus mode: typing goes into the search query
|
|
139
|
+
if (this.filterFocused) {
|
|
140
|
+
const items = this.getItems();
|
|
141
|
+
const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.selectedIndex, itemCount: items.length });
|
|
142
|
+
if (transition === 'focus-list') {
|
|
143
|
+
this.filterFocused = false;
|
|
140
144
|
this.markDirty();
|
|
141
145
|
return true;
|
|
146
|
+
}
|
|
147
|
+
if (isPanelSearchCancel(key)) {
|
|
148
|
+
this.filterFocused = false;
|
|
149
|
+
return super.handleInput(key);
|
|
150
|
+
}
|
|
151
|
+
return super.handleInput(key);
|
|
142
152
|
}
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
153
|
|
|
146
|
-
|
|
147
|
-
const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.
|
|
148
|
-
if (transition === 'focus-
|
|
149
|
-
this.
|
|
150
|
-
this.selectedIdx = 0;
|
|
151
|
-
this.markDirty();
|
|
152
|
-
return true;
|
|
153
|
-
}
|
|
154
|
-
if (isPanelSearchCommit(key) || isPanelSearchCancel(key)) {
|
|
155
|
-
this.searchMode = false;
|
|
156
|
-
this.reload();
|
|
157
|
-
this.markDirty();
|
|
158
|
-
return true;
|
|
159
|
-
}
|
|
160
|
-
if (isPanelSearchBackspace(key)) {
|
|
161
|
-
this.searchQuery = this.searchQuery.slice(0, -1);
|
|
154
|
+
const items = this.getItems();
|
|
155
|
+
const transition = getPanelSearchFocusTransition(key, { selectedIndex: this.selectedIndex, itemCount: items.length });
|
|
156
|
+
if (transition === 'focus-search') {
|
|
157
|
+
this.filterFocused = true;
|
|
162
158
|
this.markDirty();
|
|
163
159
|
return true;
|
|
164
160
|
}
|
|
165
|
-
|
|
166
|
-
|
|
161
|
+
|
|
162
|
+
if (key === 'r') {
|
|
163
|
+
this.invalidateFilter();
|
|
167
164
|
this.markDirty();
|
|
168
165
|
return true;
|
|
169
166
|
}
|
|
170
|
-
|
|
167
|
+
|
|
168
|
+
return super.handleInput(key);
|
|
171
169
|
}
|
|
172
170
|
|
|
173
171
|
render(width: number, height: number): Line[] {
|
|
172
|
+
this.clampSelection();
|
|
174
173
|
const intro = 'Durable project memory across decisions, constraints, incidents, patterns, risks, runbooks, and related provenance.';
|
|
175
174
|
|
|
176
|
-
|
|
177
|
-
this.reload();
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (!this.records.length) {
|
|
181
|
-
const message = this.searchQuery
|
|
182
|
-
? `No records matching "${this.searchQuery}"`
|
|
183
|
-
: 'No memory records. Use /recall add <class> <summary> to create one.';
|
|
184
|
-
return buildPanelWorkspace(width, height, {
|
|
185
|
-
title: 'Memory',
|
|
186
|
-
intro,
|
|
187
|
-
sections: [{
|
|
188
|
-
lines: buildEmptyState(
|
|
189
|
-
width,
|
|
190
|
-
` ${message}`,
|
|
191
|
-
'Memory becomes useful once durable facts, incidents, and decisions are promoted into the project substrate.',
|
|
192
|
-
[
|
|
193
|
-
{ command: '/recall add fact <summary>', summary: 'capture a durable fact directly' },
|
|
194
|
-
{ command: '/recall capture incident latest', summary: 'promote the latest incident into memory' },
|
|
195
|
-
],
|
|
196
|
-
C,
|
|
197
|
-
),
|
|
198
|
-
}],
|
|
199
|
-
footerLines: [
|
|
200
|
-
buildPanelLine(width, [[' / search j/k or Up/Down move r reload', C.dim]]),
|
|
201
|
-
],
|
|
202
|
-
palette: C,
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
175
|
+
const records = this.getItems();
|
|
206
176
|
const byClass = new Map<MemoryClass, number>();
|
|
207
|
-
for (const record of
|
|
177
|
+
for (const record of records) {
|
|
208
178
|
byClass.set(record.cls, (byClass.get(record.cls) ?? 0) + 1);
|
|
209
179
|
}
|
|
210
180
|
|
|
211
|
-
const
|
|
181
|
+
const filterLine = this.buildFilterInputLine(width, 'Filter', this.filterFocused);
|
|
182
|
+
|
|
183
|
+
const summaryLines: Line[] = [
|
|
212
184
|
buildKeyValueLine(width, [
|
|
213
|
-
{ label: 'records', value: String(
|
|
185
|
+
{ label: 'records', value: String(records.length), valueColor: C.value },
|
|
214
186
|
{ label: 'facts', value: String(byClass.get('fact') ?? 0), valueColor: C.fact },
|
|
215
187
|
{ label: 'decisions', value: String(byClass.get('decision') ?? 0), valueColor: C.decision },
|
|
216
188
|
{ label: 'incidents', value: String(byClass.get('incident') ?? 0), valueColor: C.incident },
|
|
217
189
|
{ label: 'runbooks', value: String(byClass.get('runbook') ?? 0), valueColor: C.runbook },
|
|
218
190
|
], C),
|
|
219
|
-
|
|
220
|
-
? [buildSearchInputLine(width, '', `${this.searchMode ? '/ ' : '~ '}${this.searchQuery}${this.searchMode ? '_' : ''}`, C, { active: this.searchMode, bg: C.searchBg, valueColor: C.searchFg })]
|
|
221
|
-
: []),
|
|
191
|
+
filterLine,
|
|
222
192
|
buildGuidanceLine(width, '/recall review', 'review durable knowledge and queue posture from the command surface', C),
|
|
223
193
|
];
|
|
224
194
|
|
|
225
|
-
const selected =
|
|
195
|
+
const selected = records[this.selectedIndex];
|
|
226
196
|
const selectedLines: Line[] = [];
|
|
227
197
|
if (selected) {
|
|
228
198
|
selectedLines.push(buildKeyValueLine(width, [
|
|
@@ -243,51 +213,13 @@ export class MemoryPanel extends BasePanel {
|
|
|
243
213
|
}
|
|
244
214
|
}
|
|
245
215
|
|
|
246
|
-
|
|
247
|
-
const selectedSection: PanelWorkspaceSection = selectedLines.length > 0 ? { title: 'Selected', lines: selectedLines } : { title: 'Selected', lines: [] };
|
|
248
|
-
const recordsSection = resolveScrollablePanelSection(width, height, {
|
|
249
|
-
intro,
|
|
250
|
-
footerLines: [
|
|
251
|
-
buildPanelLine(width, [[' / search j/k or Up/Down move r reload Esc clear search', C.dim]]),
|
|
252
|
-
],
|
|
253
|
-
palette: C,
|
|
254
|
-
beforeSections: [summarySection],
|
|
255
|
-
section: {
|
|
256
|
-
title: 'Records',
|
|
257
|
-
scrollableLines: this.records.map((record, globalIndex) => {
|
|
258
|
-
const bg = globalIndex === this.selectedIdx ? C.selected : undefined;
|
|
259
|
-
return buildPanelLine(width, [
|
|
260
|
-
[' ', C.label, bg],
|
|
261
|
-
[`[${record.scope.slice(0, 1).toUpperCase()}/${record.cls.slice(0, 3).toUpperCase()}] `, classColor(record.cls), bg],
|
|
262
|
-
[record.id.slice(-8), C.dim, bg],
|
|
263
|
-
[' ', C.label, bg],
|
|
264
|
-
[fmtTime(record.createdAt), C.dim, bg],
|
|
265
|
-
[' ', C.label, bg],
|
|
266
|
-
[record.summary.slice(0, Math.max(0, width - 33)), C.value, bg],
|
|
267
|
-
]);
|
|
268
|
-
}),
|
|
269
|
-
selectedIndex: this.selectedIdx,
|
|
270
|
-
scrollOffset: this.scrollOffset,
|
|
271
|
-
minRows: 4,
|
|
272
|
-
appendWindowSummary: { dimColor: C.dim },
|
|
273
|
-
},
|
|
274
|
-
afterSections: selectedLines.length > 0 ? [selectedSection] : [],
|
|
275
|
-
});
|
|
276
|
-
this.scrollOffset = recordsSection.scrollOffset;
|
|
277
|
-
const sections: PanelWorkspaceSection[] = [
|
|
278
|
-
summarySection,
|
|
279
|
-
recordsSection.section,
|
|
280
|
-
];
|
|
281
|
-
if (selectedLines.length > 0) sections.push(selectedSection);
|
|
282
|
-
|
|
283
|
-
return buildPanelWorkspace(width, height, {
|
|
216
|
+
return this.renderList(width, height, {
|
|
284
217
|
title: 'Memory',
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
218
|
+
header: summaryLines,
|
|
219
|
+
footer: [
|
|
220
|
+
...selectedLines,
|
|
288
221
|
buildPanelLine(width, [[' / search j/k or Up/Down move r reload Esc clear search', C.dim]]),
|
|
289
222
|
],
|
|
290
|
-
palette: C,
|
|
291
223
|
});
|
|
292
224
|
}
|
|
293
225
|
}
|
|
@@ -12,15 +12,11 @@ import type { OpsEvent } from '@pellux/goodvibes-sdk/platform/runtime/events/ind
|
|
|
12
12
|
import type { UiEventFeed } from '../runtime/ui-events.ts';
|
|
13
13
|
import type { OpsAuditEntry } from '../runtime/diagnostics/panels/ops.ts';
|
|
14
14
|
import { OpsPanel } from '../runtime/diagnostics/panels/ops.ts';
|
|
15
|
-
import {
|
|
16
|
-
import { createEmptyLine } from '../types/grid.ts';
|
|
15
|
+
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
17
16
|
import {
|
|
18
|
-
buildEmptyState,
|
|
19
17
|
buildPanelLine,
|
|
20
|
-
buildPanelWorkspace,
|
|
21
|
-
resolveScrollablePanelSection,
|
|
22
18
|
DEFAULT_PANEL_PALETTE,
|
|
23
|
-
type
|
|
19
|
+
type PanelPalette,
|
|
24
20
|
} from './polish.ts';
|
|
25
21
|
|
|
26
22
|
// ── Colour palette ──────────────────────────────────────────────────────────
|
|
@@ -74,26 +70,20 @@ function targetColor(kind: OpsAuditEntry['targetKind']): string {
|
|
|
74
70
|
|
|
75
71
|
// ── OpsControlPanel ──────────────────────────────────────────────────────────
|
|
76
72
|
|
|
77
|
-
export class OpsControlPanel extends
|
|
73
|
+
export class OpsControlPanel extends ScrollableListPanel<OpsAuditEntry> {
|
|
78
74
|
private readonly _opsPanel: OpsPanel;
|
|
79
75
|
private _unsub: (() => void) | null = null;
|
|
80
|
-
private _scrollOffset = 0;
|
|
81
76
|
|
|
82
77
|
public constructor(eventFeed: UiEventFeed<OpsEvent>) {
|
|
83
78
|
super('ops-control', 'Ops Control', 'Q', 'agent');
|
|
79
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
84
80
|
this._opsPanel = new OpsPanel(eventFeed);
|
|
85
81
|
this._unsub = this._opsPanel.subscribe(() => this.markDirty());
|
|
86
82
|
}
|
|
87
83
|
|
|
88
84
|
public override onActivate(): void {
|
|
89
85
|
super.onActivate();
|
|
90
|
-
this.
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
public handleInput(key: string): boolean {
|
|
94
|
-
if (key === 'up' || key === 'k') { this._scrollOffset = Math.max(0, this._scrollOffset - 1); return true; }
|
|
95
|
-
if (key === 'down' || key === 'j') { this._scrollOffset++; return true; }
|
|
96
|
-
return false;
|
|
86
|
+
this.selectedIndex = 0;
|
|
97
87
|
}
|
|
98
88
|
|
|
99
89
|
public override onDestroy(): void {
|
|
@@ -104,81 +94,57 @@ export class OpsControlPanel extends BasePanel {
|
|
|
104
94
|
this._opsPanel.dispose();
|
|
105
95
|
}
|
|
106
96
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
97
|
+
protected override getPalette(): PanelPalette {
|
|
98
|
+
return C;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
protected getItems(): readonly OpsAuditEntry[] {
|
|
102
|
+
// Return reversed so newest entries appear at top
|
|
103
|
+
return [...this._opsPanel.getSnapshot()].reverse();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
protected renderItem(entry: OpsAuditEntry, _index: number, _selected: boolean, width: number): Line {
|
|
107
|
+
const seqStr = String(entry.seq).padStart(4, ' ');
|
|
108
|
+
const timeStr = fmtTime(entry.ts);
|
|
109
|
+
const action = entry.action.slice(0, 15).padEnd(15, ' ');
|
|
110
|
+
const kindTag = entry.targetKind === 'task' ? 'T:' : 'A:';
|
|
111
|
+
// Truncation is intentional: TUI column width limits target ID display to 14 chars
|
|
112
|
+
const shortId = entry.targetId.slice(-10);
|
|
113
|
+
const target = (kindTag + shortId).slice(0, 14).padEnd(14, ' ');
|
|
114
|
+
const outLabel = outcomeLabel(entry.outcome);
|
|
115
|
+
const noteRaw = (entry.note ?? entry.errorMessage ?? '').slice(0, Math.max(0, width - 63));
|
|
116
|
+
|
|
117
|
+
const segs: Array<[string, string, string?]> = [
|
|
118
|
+
[` ${seqStr} `, C.seq],
|
|
119
|
+
[`${timeStr} `, C.dim],
|
|
120
|
+
[`${action} `, C.value],
|
|
121
|
+
[`${target} `, targetColor(entry.targetKind)],
|
|
122
|
+
[outLabel, outcomeColor(entry.outcome)],
|
|
123
|
+
];
|
|
124
|
+
if (noteRaw) segs.push([` ${noteRaw}`, C.note]);
|
|
125
|
+
return buildPanelLine(width, segs);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
protected override getEmptyStateMessage(): string {
|
|
129
|
+
return ' No operator interventions recorded.';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
|
|
133
|
+
return [{ command: '/cockpit', summary: 'open the cockpit and drive runtime interventions from the control rooms' }];
|
|
134
|
+
}
|
|
130
135
|
|
|
131
|
-
|
|
132
|
-
const
|
|
136
|
+
public render(width: number, height: number): Line[] {
|
|
137
|
+
const headerLines: Line[] = [
|
|
133
138
|
buildPanelLine(width, [[' SEQ TIME ACTION TARGET OUT NOTE', C.label]]),
|
|
134
139
|
];
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const action = entry.action.slice(0, 15).padEnd(15, ' ');
|
|
139
|
-
const kindTag = entry.targetKind === 'task' ? 'T:' : 'A:';
|
|
140
|
-
// Truncation is intentional: TUI column width limits target ID display to 14 chars
|
|
141
|
-
const shortId = entry.targetId.slice(-10);
|
|
142
|
-
const target = (kindTag + shortId).slice(0, 14).padEnd(14, ' ');
|
|
143
|
-
const outLabel = outcomeLabel(entry.outcome);
|
|
144
|
-
const noteRaw = (entry.note ?? entry.errorMessage ?? '').slice(0, Math.max(0, width - 63));
|
|
145
|
-
|
|
146
|
-
const segs: Array<[string, string, string?]> = [
|
|
147
|
-
[` ${seqStr} `, C.seq],
|
|
148
|
-
[`${timeStr} `, C.dim],
|
|
149
|
-
[`${action} `, C.value],
|
|
150
|
-
[`${target} `, targetColor(entry.targetKind)],
|
|
151
|
-
[outLabel, outcomeColor(entry.outcome)],
|
|
152
|
-
];
|
|
153
|
-
if (noteRaw) segs.push([` ${noteRaw}`, C.note]);
|
|
154
|
-
entryRows.push(buildPanelLine(width, segs));
|
|
155
|
-
}
|
|
156
|
-
const logSection = resolveScrollablePanelSection(width, height, {
|
|
157
|
-
intro,
|
|
158
|
-
footerLines: [buildPanelLine(width, [[' Up/Down scroll the intervention log', C.dim]])],
|
|
159
|
-
palette: C,
|
|
160
|
-
section: {
|
|
161
|
-
title: 'Audit Log',
|
|
162
|
-
scrollableLines: entryRows,
|
|
163
|
-
scrollOffset: this._scrollOffset,
|
|
164
|
-
minRows: 4,
|
|
165
|
-
appendWindowSummary: {
|
|
166
|
-
dimColor: C.label,
|
|
167
|
-
formatter: (window) => buildPanelLine(width, [[` [${window.start + 1}-${window.end}/${window.total}] Up/Down to scroll`.slice(0, width), C.label]]),
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
this._scrollOffset = logSection.scrollOffset;
|
|
140
|
+
const footerLines: Line[] = [
|
|
141
|
+
buildPanelLine(width, [[' Up/Down scroll the intervention log', C.dim]]),
|
|
142
|
+
];
|
|
172
143
|
|
|
173
|
-
|
|
174
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
144
|
+
return this.renderList(width, height, {
|
|
175
145
|
title: 'Operator Control Plane',
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
footerLines: [buildPanelLine(width, [[' Up/Down scroll the intervention log', C.dim]])],
|
|
179
|
-
palette: C,
|
|
146
|
+
header: headerLines,
|
|
147
|
+
footer: footerLines,
|
|
180
148
|
});
|
|
181
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
182
|
-
return lines;
|
|
183
149
|
}
|
|
184
150
|
}
|