@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,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { promises as fsPromises, watch, type FSWatcher } from 'fs';
|
|
2
2
|
import type { Line } from '../types/grid.ts';
|
|
3
3
|
import { createEmptyLine, createStyledCell } from '../types/grid.ts';
|
|
4
4
|
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
@@ -61,6 +61,7 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
61
61
|
|
|
62
62
|
constructor(agentEvents: UiEventFeed<AgentEvent>, private readonly deps: AgentLogsPanelDeps) {
|
|
63
63
|
super('agent-logs', 'Agents', 'A', 'agent');
|
|
64
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
64
65
|
this.agentEvents = agentEvents;
|
|
65
66
|
this._refreshAgents();
|
|
66
67
|
this._startPolling();
|
|
@@ -256,14 +257,22 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
256
257
|
}
|
|
257
258
|
|
|
258
259
|
private _pollCurrentAgent(): void {
|
|
260
|
+
void this._pollCurrentAgentAsync();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
private async _pollCurrentAgentAsync(): Promise<void> {
|
|
259
264
|
const agent = this._selectedAgent();
|
|
260
265
|
if (!agent) return;
|
|
261
266
|
|
|
262
267
|
const sessionFile = this._sessionFilePath(agent.id);
|
|
263
|
-
|
|
268
|
+
try {
|
|
269
|
+
await fsPromises.access(sessionFile);
|
|
270
|
+
} catch {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
264
273
|
|
|
265
274
|
try {
|
|
266
|
-
const content =
|
|
275
|
+
const content = await fsPromises.readFile(sessionFile, 'utf-8');
|
|
267
276
|
if (content.length === this.lastFileSize) return;
|
|
268
277
|
this.lastFileSize = content.length;
|
|
269
278
|
|
|
@@ -284,7 +293,9 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
284
293
|
private _watchAgent(agentId: string): void {
|
|
285
294
|
this._stopWatcher();
|
|
286
295
|
const sessionFile = this._sessionFilePath(agentId);
|
|
287
|
-
|
|
296
|
+
// Start watching immediately; the watcher setup itself is synchronous,
|
|
297
|
+
// the file-existence check is skipped to avoid blocking — if the file
|
|
298
|
+
// does not yet exist watch() will throw and we catch it below.
|
|
288
299
|
try {
|
|
289
300
|
this.fsWatcher = watch(sessionFile, () => {
|
|
290
301
|
if (!this.paused) {
|
|
@@ -392,24 +403,33 @@ export class AgentLogsPanel extends ScrollableListPanel<LogEntry> {
|
|
|
392
403
|
}
|
|
393
404
|
|
|
394
405
|
private _reloadAgent(agent: AgentRecord): void {
|
|
406
|
+
void this._reloadAgentAsync(agent);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private async _reloadAgentAsync(agent: AgentRecord): Promise<void> {
|
|
395
410
|
const sessionFile = this._sessionFilePath(agent.id);
|
|
396
|
-
|
|
411
|
+
try {
|
|
412
|
+
await fsPromises.access(sessionFile);
|
|
413
|
+
} catch {
|
|
397
414
|
this.allEntries = [];
|
|
398
415
|
this.filteredEntries = [];
|
|
399
416
|
this.lastFileSize = 0;
|
|
417
|
+
this.markDirty();
|
|
400
418
|
return;
|
|
401
419
|
}
|
|
402
420
|
try {
|
|
403
|
-
const content =
|
|
421
|
+
const content = await fsPromises.readFile(sessionFile, 'utf-8');
|
|
404
422
|
this.lastFileSize = content.length;
|
|
405
423
|
this.allEntries = parseAgentJsonl(content);
|
|
406
424
|
this._applyFilter();
|
|
407
425
|
if (this.autoFollow) {
|
|
408
426
|
this.selectedIndex = Math.max(0, this.filteredEntries.length - 1);
|
|
409
427
|
}
|
|
428
|
+
this.markDirty();
|
|
410
429
|
} catch {
|
|
411
430
|
this.allEntries = [];
|
|
412
431
|
this.filteredEntries = [];
|
|
432
|
+
this.markDirty();
|
|
413
433
|
}
|
|
414
434
|
}
|
|
415
435
|
|
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import {
|
|
3
|
-
import { BasePanel } from './base-panel.ts';
|
|
2
|
+
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
3
|
import {
|
|
5
4
|
buildGuidanceLine,
|
|
6
5
|
buildKeyValueLine,
|
|
7
6
|
buildPanelLine,
|
|
8
|
-
buildPanelWorkspace,
|
|
9
7
|
DEFAULT_PANEL_PALETTE,
|
|
10
|
-
resolvePrimaryScrollableSection,
|
|
11
|
-
type PanelWorkspaceSection,
|
|
12
8
|
} from './polish.ts';
|
|
13
9
|
import type { PolicyRuntimeState } from '@pellux/goodvibes-sdk/platform/runtime/permissions/policy-runtime';
|
|
14
10
|
import { buildPermissionRuleSuggestions } from '@pellux/goodvibes-sdk/platform/runtime/permissions/rule-suggestions';
|
|
@@ -30,27 +26,34 @@ const APPROVAL_ROWS = [
|
|
|
30
26
|
['sandbox', 'why prompted: WSL/VM isolation changes alter host risk posture', 'review via /sandbox preset and /sandbox review'],
|
|
31
27
|
] as const;
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
type ApprovalRow = (typeof APPROVAL_ROWS)[number];
|
|
30
|
+
|
|
31
|
+
export class ApprovalPanel extends ScrollableListPanel<ApprovalRow> {
|
|
36
32
|
private readonly policyRuntimeState: Pick<PolicyRuntimeState, 'getSnapshot'>;
|
|
37
33
|
|
|
38
34
|
public constructor(policyRuntimeState: Pick<PolicyRuntimeState, 'getSnapshot'>) {
|
|
39
35
|
super('approval', 'Approval', 'A', 'monitoring');
|
|
36
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
40
37
|
this.policyRuntimeState = policyRuntimeState;
|
|
41
38
|
}
|
|
42
39
|
|
|
40
|
+
protected override getPalette() { return C; }
|
|
41
|
+
protected override getEmptyStateMessage() { return ' No approval lanes defined.'; }
|
|
42
|
+
|
|
43
|
+
protected getItems(): readonly ApprovalRow[] {
|
|
44
|
+
return APPROVAL_ROWS;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected renderItem(row: ApprovalRow, index: number, selected: boolean, width: number): Line {
|
|
48
|
+
const bg = selected ? C.selectBg : undefined;
|
|
49
|
+
return buildPanelLine(width, [
|
|
50
|
+
[' ', C.label],
|
|
51
|
+
[row[0].padEnd(10), C.info, bg],
|
|
52
|
+
[row[1].slice(0, Math.max(0, width - 18)), C.value, bg],
|
|
53
|
+
]);
|
|
54
|
+
}
|
|
55
|
+
|
|
43
56
|
public handleInput(key: string): boolean {
|
|
44
|
-
if (key === 'up' || key === 'k') {
|
|
45
|
-
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
46
|
-
this.markDirty();
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
if (key === 'down' || key === 'j') {
|
|
50
|
-
this.selectedIndex = Math.min(APPROVAL_ROWS.length - 1, this.selectedIndex + 1);
|
|
51
|
-
this.markDirty();
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
57
|
if (key === 'home') {
|
|
55
58
|
this.selectedIndex = 0;
|
|
56
59
|
this.markDirty();
|
|
@@ -64,7 +67,7 @@ export class ApprovalPanel extends BasePanel {
|
|
|
64
67
|
if (key === 'enter' || key === 'return') {
|
|
65
68
|
return true;
|
|
66
69
|
}
|
|
67
|
-
return
|
|
70
|
+
return super.handleInput(key);
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
public getSelectedCommand(): string | null {
|
|
@@ -73,33 +76,16 @@ export class ApprovalPanel extends BasePanel {
|
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
public render(width: number, height: number): Line[] {
|
|
76
|
-
this.
|
|
79
|
+
this.clampSelection();
|
|
77
80
|
const policySnapshot = this.policyRuntimeState.getSnapshot();
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
{ label: 'operator', value: '/security + /cockpit', valueColor: C.good },
|
|
83
|
-
], C),
|
|
84
|
-
(() => {
|
|
85
|
-
const approvalCount = policySnapshot.recentPermissionAudit.filter((entry) => entry.approved === true).length;
|
|
86
|
-
const denialCount = policySnapshot.recentPermissionAudit.filter((entry) => entry.approved === false).length;
|
|
87
|
-
const pendingCount = policySnapshot.recentPermissionAudit.filter((entry) => entry.approved === undefined).length;
|
|
88
|
-
return buildPanelLine(width, [
|
|
89
|
-
[' \u2713 ', C.good],
|
|
90
|
-
[`approvals (${approvalCount}) `, C.good],
|
|
91
|
-
['\u2715 ', C.bad],
|
|
92
|
-
[`denials (${denialCount}) `, C.bad],
|
|
93
|
-
['\u25cb ', C.info],
|
|
94
|
-
[`pending (${pendingCount})`, C.info],
|
|
95
|
-
]);
|
|
96
|
-
})(),
|
|
97
|
-
buildGuidanceLine(width, '/approval review shell', 'inspect the highest-risk approval lane and refine scoped review posture', C),
|
|
98
|
-
];
|
|
99
|
-
const footerLines = [buildPanelLine(width, [[` Up/Down move Home/End jump selected lane opens the next command path`, C.dim]])];
|
|
81
|
+
const approvalCount = policySnapshot.recentPermissionAudit.filter((e) => e.approved === true).length;
|
|
82
|
+
const denialCount = policySnapshot.recentPermissionAudit.filter((e) => e.approved === false).length;
|
|
83
|
+
const pendingCount = policySnapshot.recentPermissionAudit.filter((e) => e.approved === undefined).length;
|
|
84
|
+
|
|
100
85
|
const selected = APPROVAL_ROWS[this.selectedIndex] ?? null;
|
|
101
86
|
const detailLines: Line[] = [];
|
|
102
87
|
if (selected) {
|
|
88
|
+
detailLines.push(buildPanelLine(width, [[' Selected Lane', C.label]]));
|
|
103
89
|
detailLines.push(buildKeyValueLine(width, [
|
|
104
90
|
{ label: 'lane', value: selected[0], valueColor: C.info },
|
|
105
91
|
{ label: 'next review', value: selected[2], valueColor: C.dim },
|
|
@@ -107,6 +93,7 @@ export class ApprovalPanel extends BasePanel {
|
|
|
107
93
|
detailLines.push(buildPanelLine(width, [[` ${selected[1]}`, C.value]]));
|
|
108
94
|
detailLines.push(buildGuidanceLine(width, selected[2].replace('review via ', ''), `open the ${selected[0]} review path`, C));
|
|
109
95
|
}
|
|
96
|
+
|
|
110
97
|
const recentAuditLines: Line[] = [];
|
|
111
98
|
for (const entry of policySnapshot.recentPermissionAudit.slice(0, 5)) {
|
|
112
99
|
const decision = entry.approved === undefined ? 'pending' : entry.approved ? 'approved' : 'denied';
|
|
@@ -123,6 +110,7 @@ export class ApprovalPanel extends BasePanel {
|
|
|
123
110
|
if (recentAuditLines.length === 0) {
|
|
124
111
|
recentAuditLines.push(buildPanelLine(width, [[` No recent approval pressure. Live requests and decisions will appear here.`, C.dim]]));
|
|
125
112
|
}
|
|
113
|
+
|
|
126
114
|
const ruleSuggestionLines: Line[] = [];
|
|
127
115
|
for (const suggestion of buildPermissionRuleSuggestions(policySnapshot.recentPermissionAudit).slice(0, 3)) {
|
|
128
116
|
ruleSuggestionLines.push(buildPanelLine(width, [[` ${suggestion.summary}`, C.info]]));
|
|
@@ -131,47 +119,32 @@ export class ApprovalPanel extends BasePanel {
|
|
|
131
119
|
if (ruleSuggestionLines.length === 0) {
|
|
132
120
|
ruleSuggestionLines.push(buildPanelLine(width, [[` No repeated denials currently suggest a durable rule.`, C.dim]]));
|
|
133
121
|
}
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
appendWindowSummary: { dimColor: C.dim },
|
|
158
|
-
},
|
|
159
|
-
});
|
|
160
|
-
this.scrollOffset = resolvedLanesSection.scrollOffset;
|
|
161
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
122
|
+
|
|
123
|
+
const headerLines: Line[] = [
|
|
124
|
+
buildPanelLine(width, [[' Approval posture', C.label]]),
|
|
125
|
+
buildKeyValueLine(width, [
|
|
126
|
+
{ label: 'why prompted', value: 'risk summary', valueColor: C.value },
|
|
127
|
+
{ label: 'what-if', value: '/policy simulate + preflight', valueColor: C.info },
|
|
128
|
+
{ label: 'operator', value: '/security + /cockpit', valueColor: C.good },
|
|
129
|
+
], C),
|
|
130
|
+
buildPanelLine(width, [
|
|
131
|
+
[' \u2713 ', C.good],
|
|
132
|
+
[`approvals (${approvalCount}) `, C.good],
|
|
133
|
+
['\u2715 ', C.bad],
|
|
134
|
+
[`denials (${denialCount}) `, C.bad],
|
|
135
|
+
['\u25cb ', C.info],
|
|
136
|
+
[`pending (${pendingCount})`, C.info],
|
|
137
|
+
]),
|
|
138
|
+
buildGuidanceLine(width, '/approval review shell', 'inspect the highest-risk approval lane and refine scoped review posture', C),
|
|
139
|
+
...detailLines,
|
|
140
|
+
...recentAuditLines,
|
|
141
|
+
...ruleSuggestionLines,
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
return this.renderList(width, height, {
|
|
162
145
|
title: 'Approval Control Room',
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
postureSection,
|
|
166
|
-
selectedSection,
|
|
167
|
-
pressureSection,
|
|
168
|
-
rulesSection,
|
|
169
|
-
resolvedLanesSection.section,
|
|
170
|
-
],
|
|
171
|
-
footerLines,
|
|
172
|
-
palette: C,
|
|
146
|
+
header: headerLines,
|
|
147
|
+
footer: [buildPanelLine(width, [[` Up/Down move Home/End jump selected lane opens the next command path`, C.dim]])],
|
|
173
148
|
});
|
|
174
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
175
|
-
return lines.slice(0, height);
|
|
176
149
|
}
|
|
177
150
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
-
import {
|
|
3
|
+
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
4
|
import type { UiAutomationSnapshot, UiReadModel } from '../runtime/ui-read-models.ts';
|
|
5
5
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
6
6
|
import {
|
|
@@ -10,8 +10,7 @@ import {
|
|
|
10
10
|
buildPanelLine,
|
|
11
11
|
buildPanelWorkspace,
|
|
12
12
|
DEFAULT_PANEL_PALETTE,
|
|
13
|
-
|
|
14
|
-
type PanelWorkspaceSection,
|
|
13
|
+
type PanelPalette,
|
|
15
14
|
} from './polish.ts';
|
|
16
15
|
|
|
17
16
|
const C = {
|
|
@@ -37,14 +36,16 @@ function runStatusColor(status: string): string {
|
|
|
37
36
|
return C.info;
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
|
|
39
|
+
type AutomationRun = UiAutomationSnapshot['runs'][number];
|
|
40
|
+
type AutomationJob = UiAutomationSnapshot['jobs'][number];
|
|
41
|
+
|
|
42
|
+
export class AutomationControlPanel extends ScrollableListPanel<AutomationRun> {
|
|
41
43
|
private readonly readModel?: UiReadModel<UiAutomationSnapshot>;
|
|
42
44
|
private readonly unsub: (() => void) | null;
|
|
43
|
-
private selectedIndex = 0;
|
|
44
|
-
private scrollOffset = 0;
|
|
45
45
|
|
|
46
46
|
public constructor(readModel?: UiReadModel<UiAutomationSnapshot>) {
|
|
47
47
|
super('automation', 'Automation', 'M', 'monitoring');
|
|
48
|
+
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
48
49
|
this.readModel = readModel;
|
|
49
50
|
this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
|
|
50
51
|
}
|
|
@@ -53,29 +54,45 @@ export class AutomationControlPanel extends BasePanel {
|
|
|
53
54
|
this.unsub?.();
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
if (key === 'down' || key === 'j') {
|
|
65
|
-
this.selectedIndex = Math.min(runs.length - 1, this.selectedIndex + 1);
|
|
66
|
-
this.markDirty();
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
return false;
|
|
57
|
+
protected override getPalette(): PanelPalette {
|
|
58
|
+
return C;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private getJobs(): readonly AutomationJob[] {
|
|
62
|
+
if (!this.readModel) return [];
|
|
63
|
+
return this.readModel.getSnapshot().jobs;
|
|
70
64
|
}
|
|
71
65
|
|
|
72
|
-
|
|
66
|
+
protected getItems(): readonly AutomationRun[] {
|
|
73
67
|
if (!this.readModel) return [];
|
|
74
|
-
return
|
|
68
|
+
return this.readModel.getSnapshot().runs;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
protected renderItem(run: AutomationRun, _index: number, selected: boolean, width: number): Line {
|
|
72
|
+
const bg = selected ? C.selectBg : undefined;
|
|
73
|
+
const jobs = this.getJobs();
|
|
74
|
+
const name = jobs.find((job) => job.id === run.jobId)?.name ?? run.jobId;
|
|
75
|
+
return buildPanelLine(width, [
|
|
76
|
+
[' ', C.label, bg],
|
|
77
|
+
[run.status.padEnd(11), runStatusColor(run.status), bg],
|
|
78
|
+
[` ${truncateDisplay(name, 22).padEnd(22)}`, C.value, bg],
|
|
79
|
+
[` ${truncateDisplay(run.target.kind, 12).padEnd(12)}`, C.info, bg],
|
|
80
|
+
[` ${truncateDisplay(formatTime(run.queuedAt), Math.max(0, width - 49))}`, C.dim, bg],
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
protected override getEmptyStateMessage(): string {
|
|
85
|
+
return ' No automation activity recorded.';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
|
|
89
|
+
return [
|
|
90
|
+
{ command: '/schedule add cron 0 * * * * repo sweep', summary: 'create a recurring automation job' },
|
|
91
|
+
{ command: '/schedule list', summary: 'inspect jobs and run history from the shell' },
|
|
92
|
+
];
|
|
75
93
|
}
|
|
76
94
|
|
|
77
95
|
public render(width: number, height: number): Line[] {
|
|
78
|
-
this.needsRender = false;
|
|
79
96
|
const intro = 'Automation jobs, active runs, deliveries, and failure posture across the shared control plane.';
|
|
80
97
|
|
|
81
98
|
if (!this.readModel) {
|
|
@@ -99,155 +116,97 @@ export class AutomationControlPanel extends BasePanel {
|
|
|
99
116
|
|
|
100
117
|
const snapshot = this.readModel.getSnapshot();
|
|
101
118
|
const jobs = [...snapshot.jobs];
|
|
102
|
-
const runs = this.
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
buildGuidanceLine(width, '/schedule list', 'manage jobs and use the web or surface controls for retries, delivery, and cross-surface sessions', C),
|
|
120
|
-
],
|
|
121
|
-
};
|
|
119
|
+
const runs = this.getItems();
|
|
120
|
+
|
|
121
|
+
const headerLines: Line[] = [
|
|
122
|
+
buildKeyValueLine(width, [
|
|
123
|
+
{ label: 'jobs', value: String(snapshot.totalJobs), valueColor: snapshot.totalJobs > 0 ? C.info : C.dim },
|
|
124
|
+
{ label: 'runs', value: String(snapshot.totalRuns), valueColor: snapshot.totalRuns > 0 ? C.value : C.dim },
|
|
125
|
+
{ label: 'active', value: String(snapshot.activeRunIds.length), valueColor: snapshot.activeRunIds.length > 0 ? C.warn : C.dim },
|
|
126
|
+
{ label: 'failed', value: String(snapshot.totalFailed), valueColor: snapshot.totalFailed > 0 ? C.error : C.dim },
|
|
127
|
+
], C),
|
|
128
|
+
buildKeyValueLine(width, [
|
|
129
|
+
{ label: 'deliveries ok', value: String(snapshot.deliveryTotals.succeeded), valueColor: snapshot.deliveryTotals.succeeded > 0 ? C.ok : C.dim },
|
|
130
|
+
{ label: 'delivery fail', value: String(snapshot.deliveryTotals.failed), valueColor: snapshot.deliveryTotals.failed > 0 ? C.error : C.dim },
|
|
131
|
+
{ label: 'dead letters', value: String(snapshot.deliveryTotals.deadLettered), valueColor: snapshot.deliveryTotals.deadLettered > 0 ? C.warn : C.dim },
|
|
132
|
+
{ label: 'sources', value: String(snapshot.sourceCount), valueColor: snapshot.sourceCount > 0 ? C.info : C.dim },
|
|
133
|
+
], C),
|
|
134
|
+
buildGuidanceLine(width, '/schedule list', 'manage jobs and use the web or surface controls for retries, delivery, and cross-surface sessions', C),
|
|
135
|
+
];
|
|
122
136
|
|
|
123
137
|
if (jobs.length === 0 && runs.length === 0) {
|
|
124
|
-
|
|
138
|
+
return this.renderList(width, height, {
|
|
125
139
|
title: 'Automation Control',
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
summarySection,
|
|
129
|
-
{
|
|
130
|
-
lines: buildEmptyState(
|
|
131
|
-
width,
|
|
132
|
-
' No automation activity recorded.',
|
|
133
|
-
'Create a job, run one manually, or let a watcher/surface trigger automation to populate this control room.',
|
|
134
|
-
[
|
|
135
|
-
{ command: '/schedule add cron 0 * * * * repo sweep', summary: 'create a recurring automation job' },
|
|
136
|
-
{ command: '/schedule list', summary: 'inspect jobs and run history from the shell' },
|
|
137
|
-
],
|
|
138
|
-
C,
|
|
139
|
-
),
|
|
140
|
-
},
|
|
141
|
-
],
|
|
142
|
-
palette: C,
|
|
140
|
+
header: headerLines,
|
|
141
|
+
emptyMessage: ' No automation activity recorded.',
|
|
143
142
|
});
|
|
144
|
-
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
145
|
-
return workspace;
|
|
146
143
|
}
|
|
147
144
|
|
|
148
|
-
this.
|
|
145
|
+
this.clampSelection();
|
|
149
146
|
const selectedRun = runs[this.selectedIndex];
|
|
150
147
|
const jobName = selectedRun ? (jobs.find((job) => job.id === selectedRun.jobId)?.name ?? selectedRun.jobId) : 'n/a';
|
|
151
148
|
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
]),
|
|
196
|
-
...(selectedRun.error ? [
|
|
197
|
-
buildPanelLine(width, [
|
|
198
|
-
[' Error: ', C.label],
|
|
199
|
-
[truncateDisplay(selectedRun.error, Math.max(0, width - 10)), C.error],
|
|
200
|
-
]),
|
|
201
|
-
] : []),
|
|
202
|
-
],
|
|
203
|
-
}
|
|
204
|
-
: {
|
|
205
|
-
title: 'Selected Run',
|
|
206
|
-
lines: [buildPanelLine(width, [[' No run selected.', C.dim]])],
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
const resolvedRuns = resolvePrimaryScrollableSection(width, height, {
|
|
210
|
-
intro,
|
|
211
|
-
footerLines: [buildPanelLine(width, [[' Up/Down move through runs', C.dim]])],
|
|
212
|
-
palette: C,
|
|
213
|
-
beforeSections: [summarySection],
|
|
214
|
-
section: {
|
|
215
|
-
title: 'Recent Runs',
|
|
216
|
-
scrollableLines: runs.map((run, absolute) => {
|
|
217
|
-
const bg = absolute === this.selectedIndex ? C.selectBg : undefined;
|
|
218
|
-
const name = jobs.find((job) => job.id === run.jobId)?.name ?? run.jobId;
|
|
219
|
-
return buildPanelLine(width, [
|
|
220
|
-
[' ', C.label, bg],
|
|
221
|
-
[run.status.padEnd(11), runStatusColor(run.status), bg],
|
|
222
|
-
[` ${truncateDisplay(name, 22).padEnd(22)}`, C.value, bg],
|
|
223
|
-
[` ${truncateDisplay(run.target.kind, 12).padEnd(12)}`, C.info, bg],
|
|
224
|
-
[` ${truncateDisplay(formatTime(run.queuedAt), Math.max(0, width - 49))}`, C.dim, bg],
|
|
225
|
-
]);
|
|
226
|
-
}),
|
|
227
|
-
selectedIndex: this.selectedIndex,
|
|
228
|
-
scrollOffset: this.scrollOffset,
|
|
229
|
-
guardRows: 1,
|
|
230
|
-
minRows: 5,
|
|
231
|
-
appendWindowSummary: { dimColor: C.dim },
|
|
232
|
-
},
|
|
233
|
-
afterSections: [detailSection, jobSection],
|
|
234
|
-
});
|
|
235
|
-
this.scrollOffset = resolvedRuns.scrollOffset;
|
|
149
|
+
const footerLines: Line[] = [];
|
|
150
|
+
if (selectedRun) {
|
|
151
|
+
footerLines.push(
|
|
152
|
+
buildPanelLine(width, [
|
|
153
|
+
[' Run: ', C.label],
|
|
154
|
+
[selectedRun.id, C.value],
|
|
155
|
+
[' Status: ', C.label],
|
|
156
|
+
[selectedRun.status, runStatusColor(selectedRun.status)],
|
|
157
|
+
]),
|
|
158
|
+
buildPanelLine(width, [
|
|
159
|
+
[' Job: ', C.label],
|
|
160
|
+
[jobName, C.value],
|
|
161
|
+
[' Agent: ', C.label],
|
|
162
|
+
[selectedRun.agentId ?? 'n/a', C.info],
|
|
163
|
+
]),
|
|
164
|
+
buildPanelLine(width, [
|
|
165
|
+
[' Queue: ', C.label],
|
|
166
|
+
[formatTime(selectedRun.queuedAt), C.dim],
|
|
167
|
+
[' End: ', C.label],
|
|
168
|
+
[formatTime(selectedRun.endedAt), C.dim],
|
|
169
|
+
]),
|
|
170
|
+
buildPanelLine(width, [
|
|
171
|
+
[' Trigger: ', C.label],
|
|
172
|
+
[selectedRun.triggeredBy.kind, C.info],
|
|
173
|
+
[' Target: ', C.label],
|
|
174
|
+
[selectedRun.target.kind, C.value],
|
|
175
|
+
]),
|
|
176
|
+
buildPanelLine(width, [
|
|
177
|
+
[' Deliveries: ', C.label],
|
|
178
|
+
[String(selectedRun.deliveryIds.length), selectedRun.deliveryIds.length > 0 ? C.info : C.dim],
|
|
179
|
+
[' Route: ', C.label],
|
|
180
|
+
[selectedRun.routeId ?? 'n/a', C.dim],
|
|
181
|
+
]),
|
|
182
|
+
);
|
|
183
|
+
if (selectedRun.error) {
|
|
184
|
+
footerLines.push(buildPanelLine(width, [
|
|
185
|
+
[' Error: ', C.label],
|
|
186
|
+
[truncateDisplay(selectedRun.error, Math.max(0, width - 10)), C.error],
|
|
187
|
+
]));
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
footerLines.push(buildPanelLine(width, [[' No run selected.', C.dim]]));
|
|
191
|
+
}
|
|
236
192
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
193
|
+
// Jobs quick view
|
|
194
|
+
if (jobs.length > 0) {
|
|
195
|
+
footerLines.push(
|
|
196
|
+
...jobs.slice(0, 6).map((job) => buildPanelLine(width, [
|
|
197
|
+
[' ', C.label],
|
|
198
|
+
[job.enabled ? 'ENABLED ' : 'PAUSED ', job.enabled ? C.ok : C.warn],
|
|
199
|
+
[truncateDisplay(job.name, 24).padEnd(24), C.value],
|
|
200
|
+
[` next ${truncateDisplay(formatTime(job.nextRunAt), Math.max(0, width - 43))}`, C.dim],
|
|
201
|
+
])),
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
footerLines.push(buildPanelLine(width, [[' Up/Down move through runs', C.dim]]));
|
|
205
|
+
|
|
206
|
+
return this.renderList(width, height, {
|
|
244
207
|
title: 'Automation Control',
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
footerLines: [buildPanelLine(width, [[' Up/Down move through runs', C.dim]])],
|
|
248
|
-
palette: C,
|
|
208
|
+
header: headerLines,
|
|
209
|
+
footer: footerLines,
|
|
249
210
|
});
|
|
250
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
251
|
-
return lines.slice(0, height);
|
|
252
211
|
}
|
|
253
212
|
}
|