@pellux/goodvibes-tui 0.18.20 → 0.18.23
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 +120 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-rendering.ts +20 -6
- package/src/input/commands/session.ts +0 -1
- 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 +121 -119
- package/src/input/keybindings.ts +30 -0
- package/src/panels/approval-panel.ts +54 -82
- package/src/panels/automation-control-panel.ts +119 -161
- package/src/panels/communication-panel.ts +68 -107
- package/src/panels/control-plane-panel.ts +116 -172
- package/src/panels/hooks-panel.ts +101 -138
- package/src/panels/incident-review-panel.ts +55 -107
- package/src/panels/local-auth-panel.ts +76 -93
- package/src/panels/mcp-panel.ts +108 -155
- package/src/panels/ops-control-panel.ts +50 -85
- package/src/panels/panel-manager.ts +22 -2
- package/src/panels/plugins-panel.ts +36 -60
- package/src/panels/routes-panel.ts +89 -141
- package/src/panels/scrollable-list-panel.ts +45 -14
- package/src/panels/security-panel.ts +101 -137
- package/src/panels/services-panel.ts +58 -102
- package/src/panels/settings-sync-panel.ts +76 -122
- package/src/panels/subscription-panel.ts +63 -86
- package/src/panels/tasks-panel.ts +129 -179
- package/src/panels/watchers-panel.ts +88 -137
- package/src/renderer/buffer.ts +11 -0
- package/src/renderer/diff.ts +8 -0
- package/src/renderer/help-overlay.ts +37 -28
- package/src/renderer/markdown.ts +3 -145
- package/src/version.ts +1 -1
|
@@ -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,9 +26,9 @@ 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'>) {
|
|
@@ -40,17 +36,23 @@ export class ApprovalPanel extends BasePanel {
|
|
|
40
36
|
this.policyRuntimeState = policyRuntimeState;
|
|
41
37
|
}
|
|
42
38
|
|
|
39
|
+
protected override getPalette() { return C; }
|
|
40
|
+
protected override getEmptyStateMessage() { return ' No approval lanes defined.'; }
|
|
41
|
+
|
|
42
|
+
protected getItems(): readonly ApprovalRow[] {
|
|
43
|
+
return APPROVAL_ROWS;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
protected renderItem(row: ApprovalRow, index: number, selected: boolean, width: number): Line {
|
|
47
|
+
const bg = selected ? C.selectBg : undefined;
|
|
48
|
+
return buildPanelLine(width, [
|
|
49
|
+
[' ', C.label],
|
|
50
|
+
[row[0].padEnd(10), C.info, bg],
|
|
51
|
+
[row[1].slice(0, Math.max(0, width - 18)), C.value, bg],
|
|
52
|
+
]);
|
|
53
|
+
}
|
|
54
|
+
|
|
43
55
|
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
56
|
if (key === 'home') {
|
|
55
57
|
this.selectedIndex = 0;
|
|
56
58
|
this.markDirty();
|
|
@@ -64,7 +66,7 @@ export class ApprovalPanel extends BasePanel {
|
|
|
64
66
|
if (key === 'enter' || key === 'return') {
|
|
65
67
|
return true;
|
|
66
68
|
}
|
|
67
|
-
return
|
|
69
|
+
return super.handleInput(key);
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
public getSelectedCommand(): string | null {
|
|
@@ -73,33 +75,16 @@ export class ApprovalPanel extends BasePanel {
|
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
public render(width: number, height: number): Line[] {
|
|
76
|
-
this.
|
|
78
|
+
this.clampSelection();
|
|
77
79
|
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]])];
|
|
80
|
+
const approvalCount = policySnapshot.recentPermissionAudit.filter((e) => e.approved === true).length;
|
|
81
|
+
const denialCount = policySnapshot.recentPermissionAudit.filter((e) => e.approved === false).length;
|
|
82
|
+
const pendingCount = policySnapshot.recentPermissionAudit.filter((e) => e.approved === undefined).length;
|
|
83
|
+
|
|
100
84
|
const selected = APPROVAL_ROWS[this.selectedIndex] ?? null;
|
|
101
85
|
const detailLines: Line[] = [];
|
|
102
86
|
if (selected) {
|
|
87
|
+
detailLines.push(buildPanelLine(width, [[' Selected Lane', C.label]]));
|
|
103
88
|
detailLines.push(buildKeyValueLine(width, [
|
|
104
89
|
{ label: 'lane', value: selected[0], valueColor: C.info },
|
|
105
90
|
{ label: 'next review', value: selected[2], valueColor: C.dim },
|
|
@@ -107,6 +92,7 @@ export class ApprovalPanel extends BasePanel {
|
|
|
107
92
|
detailLines.push(buildPanelLine(width, [[` ${selected[1]}`, C.value]]));
|
|
108
93
|
detailLines.push(buildGuidanceLine(width, selected[2].replace('review via ', ''), `open the ${selected[0]} review path`, C));
|
|
109
94
|
}
|
|
95
|
+
|
|
110
96
|
const recentAuditLines: Line[] = [];
|
|
111
97
|
for (const entry of policySnapshot.recentPermissionAudit.slice(0, 5)) {
|
|
112
98
|
const decision = entry.approved === undefined ? 'pending' : entry.approved ? 'approved' : 'denied';
|
|
@@ -123,6 +109,7 @@ export class ApprovalPanel extends BasePanel {
|
|
|
123
109
|
if (recentAuditLines.length === 0) {
|
|
124
110
|
recentAuditLines.push(buildPanelLine(width, [[` No recent approval pressure. Live requests and decisions will appear here.`, C.dim]]));
|
|
125
111
|
}
|
|
112
|
+
|
|
126
113
|
const ruleSuggestionLines: Line[] = [];
|
|
127
114
|
for (const suggestion of buildPermissionRuleSuggestions(policySnapshot.recentPermissionAudit).slice(0, 3)) {
|
|
128
115
|
ruleSuggestionLines.push(buildPanelLine(width, [[` ${suggestion.summary}`, C.info]]));
|
|
@@ -131,47 +118,32 @@ export class ApprovalPanel extends BasePanel {
|
|
|
131
118
|
if (ruleSuggestionLines.length === 0) {
|
|
132
119
|
ruleSuggestionLines.push(buildPanelLine(width, [[` No repeated denials currently suggest a durable rule.`, C.dim]]));
|
|
133
120
|
}
|
|
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, {
|
|
121
|
+
|
|
122
|
+
const headerLines: Line[] = [
|
|
123
|
+
buildPanelLine(width, [[' Approval posture', C.label]]),
|
|
124
|
+
buildKeyValueLine(width, [
|
|
125
|
+
{ label: 'why prompted', value: 'risk summary', valueColor: C.value },
|
|
126
|
+
{ label: 'what-if', value: '/policy simulate + preflight', valueColor: C.info },
|
|
127
|
+
{ label: 'operator', value: '/security + /cockpit', valueColor: C.good },
|
|
128
|
+
], C),
|
|
129
|
+
buildPanelLine(width, [
|
|
130
|
+
[' \u2713 ', C.good],
|
|
131
|
+
[`approvals (${approvalCount}) `, C.good],
|
|
132
|
+
['\u2715 ', C.bad],
|
|
133
|
+
[`denials (${denialCount}) `, C.bad],
|
|
134
|
+
['\u25cb ', C.info],
|
|
135
|
+
[`pending (${pendingCount})`, C.info],
|
|
136
|
+
]),
|
|
137
|
+
buildGuidanceLine(width, '/approval review shell', 'inspect the highest-risk approval lane and refine scoped review posture', C),
|
|
138
|
+
...detailLines,
|
|
139
|
+
...recentAuditLines,
|
|
140
|
+
...ruleSuggestionLines,
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
return this.renderList(width, height, {
|
|
162
144
|
title: 'Approval Control Room',
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
postureSection,
|
|
166
|
-
selectedSection,
|
|
167
|
-
pressureSection,
|
|
168
|
-
rulesSection,
|
|
169
|
-
resolvedLanesSection.section,
|
|
170
|
-
],
|
|
171
|
-
footerLines,
|
|
172
|
-
palette: C,
|
|
145
|
+
header: headerLines,
|
|
146
|
+
footer: [buildPanelLine(width, [[` Up/Down move Home/End jump selected lane opens the next command path`, C.dim]])],
|
|
173
147
|
});
|
|
174
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
175
|
-
return lines.slice(0, height);
|
|
176
148
|
}
|
|
177
149
|
}
|
|
@@ -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,11 +36,12 @@ 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');
|
|
@@ -53,29 +53,45 @@ export class AutomationControlPanel extends BasePanel {
|
|
|
53
53
|
this.unsub?.();
|
|
54
54
|
}
|
|
55
55
|
|
|
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;
|
|
56
|
+
protected override getPalette(): PanelPalette {
|
|
57
|
+
return C;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private getJobs(): readonly AutomationJob[] {
|
|
61
|
+
if (!this.readModel) return [];
|
|
62
|
+
return this.readModel.getSnapshot().jobs;
|
|
70
63
|
}
|
|
71
64
|
|
|
72
|
-
|
|
65
|
+
protected getItems(): readonly AutomationRun[] {
|
|
73
66
|
if (!this.readModel) return [];
|
|
74
|
-
return
|
|
67
|
+
return this.readModel.getSnapshot().runs;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
protected renderItem(run: AutomationRun, _index: number, selected: boolean, width: number): Line {
|
|
71
|
+
const bg = selected ? C.selectBg : undefined;
|
|
72
|
+
const jobs = this.getJobs();
|
|
73
|
+
const name = jobs.find((job) => job.id === run.jobId)?.name ?? run.jobId;
|
|
74
|
+
return buildPanelLine(width, [
|
|
75
|
+
[' ', C.label, bg],
|
|
76
|
+
[run.status.padEnd(11), runStatusColor(run.status), bg],
|
|
77
|
+
[` ${truncateDisplay(name, 22).padEnd(22)}`, C.value, bg],
|
|
78
|
+
[` ${truncateDisplay(run.target.kind, 12).padEnd(12)}`, C.info, bg],
|
|
79
|
+
[` ${truncateDisplay(formatTime(run.queuedAt), Math.max(0, width - 49))}`, C.dim, bg],
|
|
80
|
+
]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
protected override getEmptyStateMessage(): string {
|
|
84
|
+
return ' No automation activity recorded.';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
|
|
88
|
+
return [
|
|
89
|
+
{ command: '/schedule add cron 0 * * * * repo sweep', summary: 'create a recurring automation job' },
|
|
90
|
+
{ command: '/schedule list', summary: 'inspect jobs and run history from the shell' },
|
|
91
|
+
];
|
|
75
92
|
}
|
|
76
93
|
|
|
77
94
|
public render(width: number, height: number): Line[] {
|
|
78
|
-
this.needsRender = false;
|
|
79
95
|
const intro = 'Automation jobs, active runs, deliveries, and failure posture across the shared control plane.';
|
|
80
96
|
|
|
81
97
|
if (!this.readModel) {
|
|
@@ -99,155 +115,97 @@ export class AutomationControlPanel extends BasePanel {
|
|
|
99
115
|
|
|
100
116
|
const snapshot = this.readModel.getSnapshot();
|
|
101
117
|
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
|
-
};
|
|
118
|
+
const runs = this.getItems();
|
|
119
|
+
|
|
120
|
+
const headerLines: Line[] = [
|
|
121
|
+
buildKeyValueLine(width, [
|
|
122
|
+
{ label: 'jobs', value: String(snapshot.totalJobs), valueColor: snapshot.totalJobs > 0 ? C.info : C.dim },
|
|
123
|
+
{ label: 'runs', value: String(snapshot.totalRuns), valueColor: snapshot.totalRuns > 0 ? C.value : C.dim },
|
|
124
|
+
{ label: 'active', value: String(snapshot.activeRunIds.length), valueColor: snapshot.activeRunIds.length > 0 ? C.warn : C.dim },
|
|
125
|
+
{ label: 'failed', value: String(snapshot.totalFailed), valueColor: snapshot.totalFailed > 0 ? C.error : C.dim },
|
|
126
|
+
], C),
|
|
127
|
+
buildKeyValueLine(width, [
|
|
128
|
+
{ label: 'deliveries ok', value: String(snapshot.deliveryTotals.succeeded), valueColor: snapshot.deliveryTotals.succeeded > 0 ? C.ok : C.dim },
|
|
129
|
+
{ label: 'delivery fail', value: String(snapshot.deliveryTotals.failed), valueColor: snapshot.deliveryTotals.failed > 0 ? C.error : C.dim },
|
|
130
|
+
{ label: 'dead letters', value: String(snapshot.deliveryTotals.deadLettered), valueColor: snapshot.deliveryTotals.deadLettered > 0 ? C.warn : C.dim },
|
|
131
|
+
{ label: 'sources', value: String(snapshot.sourceCount), valueColor: snapshot.sourceCount > 0 ? C.info : C.dim },
|
|
132
|
+
], C),
|
|
133
|
+
buildGuidanceLine(width, '/schedule list', 'manage jobs and use the web or surface controls for retries, delivery, and cross-surface sessions', C),
|
|
134
|
+
];
|
|
122
135
|
|
|
123
136
|
if (jobs.length === 0 && runs.length === 0) {
|
|
124
|
-
|
|
137
|
+
return this.renderList(width, height, {
|
|
125
138
|
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,
|
|
139
|
+
header: headerLines,
|
|
140
|
+
emptyMessage: ' No automation activity recorded.',
|
|
143
141
|
});
|
|
144
|
-
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
145
|
-
return workspace;
|
|
146
142
|
}
|
|
147
143
|
|
|
148
|
-
this.
|
|
144
|
+
this.clampSelection();
|
|
149
145
|
const selectedRun = runs[this.selectedIndex];
|
|
150
146
|
const jobName = selectedRun ? (jobs.find((job) => job.id === selectedRun.jobId)?.name ?? selectedRun.jobId) : 'n/a';
|
|
151
147
|
|
|
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;
|
|
148
|
+
const footerLines: Line[] = [];
|
|
149
|
+
if (selectedRun) {
|
|
150
|
+
footerLines.push(
|
|
151
|
+
buildPanelLine(width, [
|
|
152
|
+
[' Run: ', C.label],
|
|
153
|
+
[selectedRun.id, C.value],
|
|
154
|
+
[' Status: ', C.label],
|
|
155
|
+
[selectedRun.status, runStatusColor(selectedRun.status)],
|
|
156
|
+
]),
|
|
157
|
+
buildPanelLine(width, [
|
|
158
|
+
[' Job: ', C.label],
|
|
159
|
+
[jobName, C.value],
|
|
160
|
+
[' Agent: ', C.label],
|
|
161
|
+
[selectedRun.agentId ?? 'n/a', C.info],
|
|
162
|
+
]),
|
|
163
|
+
buildPanelLine(width, [
|
|
164
|
+
[' Queue: ', C.label],
|
|
165
|
+
[formatTime(selectedRun.queuedAt), C.dim],
|
|
166
|
+
[' End: ', C.label],
|
|
167
|
+
[formatTime(selectedRun.endedAt), C.dim],
|
|
168
|
+
]),
|
|
169
|
+
buildPanelLine(width, [
|
|
170
|
+
[' Trigger: ', C.label],
|
|
171
|
+
[selectedRun.triggeredBy.kind, C.info],
|
|
172
|
+
[' Target: ', C.label],
|
|
173
|
+
[selectedRun.target.kind, C.value],
|
|
174
|
+
]),
|
|
175
|
+
buildPanelLine(width, [
|
|
176
|
+
[' Deliveries: ', C.label],
|
|
177
|
+
[String(selectedRun.deliveryIds.length), selectedRun.deliveryIds.length > 0 ? C.info : C.dim],
|
|
178
|
+
[' Route: ', C.label],
|
|
179
|
+
[selectedRun.routeId ?? 'n/a', C.dim],
|
|
180
|
+
]),
|
|
181
|
+
);
|
|
182
|
+
if (selectedRun.error) {
|
|
183
|
+
footerLines.push(buildPanelLine(width, [
|
|
184
|
+
[' Error: ', C.label],
|
|
185
|
+
[truncateDisplay(selectedRun.error, Math.max(0, width - 10)), C.error],
|
|
186
|
+
]));
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
footerLines.push(buildPanelLine(width, [[' No run selected.', C.dim]]));
|
|
190
|
+
}
|
|
236
191
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
192
|
+
// Jobs quick view
|
|
193
|
+
if (jobs.length > 0) {
|
|
194
|
+
footerLines.push(
|
|
195
|
+
...jobs.slice(0, 6).map((job) => buildPanelLine(width, [
|
|
196
|
+
[' ', C.label],
|
|
197
|
+
[job.enabled ? 'ENABLED ' : 'PAUSED ', job.enabled ? C.ok : C.warn],
|
|
198
|
+
[truncateDisplay(job.name, 24).padEnd(24), C.value],
|
|
199
|
+
[` next ${truncateDisplay(formatTime(job.nextRunAt), Math.max(0, width - 43))}`, C.dim],
|
|
200
|
+
])),
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
footerLines.push(buildPanelLine(width, [[' Up/Down move through runs', C.dim]]));
|
|
204
|
+
|
|
205
|
+
return this.renderList(width, height, {
|
|
244
206
|
title: 'Automation Control',
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
footerLines: [buildPanelLine(width, [[' Up/Down move through runs', C.dim]])],
|
|
248
|
-
palette: C,
|
|
207
|
+
header: headerLines,
|
|
208
|
+
footer: footerLines,
|
|
249
209
|
});
|
|
250
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
251
|
-
return lines.slice(0, height);
|
|
252
210
|
}
|
|
253
211
|
}
|