@pellux/goodvibes-tui 1.1.0 → 1.7.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 +85 -21
- package/README.md +20 -11
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/core/alert-gating.ts +67 -0
- package/src/core/approval-alert.ts +72 -0
- package/src/core/budget-breach-notifier.ts +106 -0
- package/src/core/conversation-rendering.ts +19 -0
- package/src/core/conversation.ts +18 -0
- package/src/core/focus-tracker.ts +41 -0
- package/src/core/long-task-notifier.ts +33 -6
- package/src/core/system-message-router.ts +37 -51
- package/src/core/turn-cancellation.ts +20 -0
- package/src/core/turn-event-wiring.ts +64 -3
- package/src/export/cost-utils.ts +36 -0
- package/src/input/command-registry.ts +38 -1
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +192 -0
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +1 -1
- package/src/input/commands/image-runtime.ts +112 -0
- package/src/input/commands/intelligence-runtime.ts +11 -1
- package/src/input/commands/local-auth-runtime.ts +4 -1
- package/src/input/commands/local-runtime.ts +9 -3
- package/src/input/commands/marketplace-runtime.ts +2 -2
- package/src/input/commands/memory.ts +6 -1
- package/src/input/commands/operator-panel-runtime.ts +40 -24
- package/src/input/commands/planning-runtime.ts +26 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -6
- package/src/input/commands/plugin-runtime.ts +2 -2
- package/src/input/commands/policy-dispatch.ts +21 -0
- package/src/input/commands/provider-accounts-runtime.ts +1 -1
- package/src/input/commands/qrcode-runtime.ts +3 -3
- package/src/input/commands/recall-review.ts +35 -0
- package/src/input/commands/remote-runtime.ts +3 -3
- package/src/input/commands/runtime-services.ts +54 -13
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +16 -1
- package/src/input/commands/settings-sync-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +15 -7
- package/src/input/commands/skills-runtime.ts +2 -8
- package/src/input/commands/subscription-runtime.ts +2 -2
- package/src/input/commands/test-runtime.ts +277 -0
- package/src/input/commands/websearch-runtime.ts +101 -0
- package/src/input/commands/work-plan-runtime.ts +36 -10
- package/src/input/commands/workstream-runtime.ts +338 -0
- package/src/input/commands.ts +12 -0
- package/src/input/config-modal-types.ts +161 -0
- package/src/input/config-modal.ts +377 -0
- package/src/input/feed-context-factory.ts +7 -8
- package/src/input/handler-command-route.ts +27 -17
- package/src/input/handler-feed-routes.ts +61 -23
- package/src/input/handler-feed.ts +47 -23
- package/src/input/handler-interactions.ts +1 -3
- package/src/input/handler-modal-routes.ts +65 -0
- package/src/input/handler-modal-stack.ts +3 -5
- package/src/input/handler-modal-token-routes.ts +16 -49
- package/src/input/handler-picker-routes.ts +11 -94
- package/src/input/handler-shortcuts.ts +24 -14
- package/src/input/handler-types.ts +2 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +6 -28
- package/src/input/keybindings.ts +22 -8
- package/src/input/model-picker-types.ts +24 -6
- package/src/input/model-picker.ts +58 -0
- package/src/input/panel-integration-actions.ts +9 -170
- package/src/input/settings-modal-data.ts +95 -0
- package/src/input/settings-modal-mutations.ts +6 -4
- package/src/main.ts +24 -27
- package/src/panels/agent-inspector-shared.ts +2 -1
- package/src/panels/base-panel.ts +22 -1
- package/src/panels/builtin/agent.ts +21 -107
- package/src/panels/builtin/development.ts +15 -61
- package/src/panels/builtin/knowledge.ts +8 -32
- package/src/panels/builtin/operations.ts +84 -428
- package/src/panels/builtin/session.ts +21 -112
- package/src/panels/builtin/shared.ts +9 -43
- package/src/panels/builtin-modals.ts +218 -0
- package/src/panels/builtin-panels.ts +5 -0
- package/src/panels/cost-tracker-panel.ts +36 -10
- package/src/panels/diff-panel.ts +12 -43
- package/src/panels/eval-registry.ts +60 -0
- package/src/panels/fleet-panel.ts +800 -0
- package/src/panels/fleet-read-model.ts +477 -0
- package/src/panels/fleet-steer.ts +92 -0
- package/src/panels/fleet-stop.ts +125 -0
- package/src/panels/fleet-tabs.ts +230 -0
- package/src/panels/fleet-transcript.ts +356 -0
- package/src/panels/index.ts +7 -31
- package/src/panels/modals/hooks-modal.ts +187 -0
- package/src/panels/modals/keybindings-modal.ts +151 -0
- package/src/panels/modals/knowledge-modal.ts +158 -0
- package/src/panels/modals/local-auth-modal.ts +132 -0
- package/src/panels/modals/marketplace-modal.ts +218 -0
- package/src/panels/modals/memory-modal.ts +180 -0
- package/src/panels/modals/modal-surface-helpers.ts +54 -0
- package/src/panels/modals/modal-theme.ts +36 -0
- package/src/panels/modals/pairing-modal.ts +144 -0
- package/src/panels/modals/planning-modal.ts +282 -0
- package/src/panels/modals/plugins-modal.ts +174 -0
- package/src/panels/modals/policy-modal.ts +256 -0
- package/src/panels/modals/provider-health-modal.ts +136 -0
- package/src/panels/modals/remote-modal.ts +115 -0
- package/src/panels/modals/sandbox-modal.ts +150 -0
- package/src/panels/modals/security-modal.ts +217 -0
- package/src/panels/modals/services-modal.ts +179 -0
- package/src/panels/modals/settings-sync-modal.ts +142 -0
- package/src/panels/modals/skills-modal.ts +189 -0
- package/src/panels/modals/subscription-modal.ts +172 -0
- package/src/panels/modals/work-plan-modal.ts +149 -0
- package/src/panels/panel-confirm-overlay.ts +72 -0
- package/src/panels/panel-manager.ts +108 -5
- package/src/panels/project-planning-answer-actions.ts +4 -2
- package/src/panels/skills-panel.ts +7 -51
- package/src/panels/types.ts +13 -0
- package/src/permissions/hunk-selection.ts +179 -0
- package/src/permissions/prompt.ts +60 -4
- package/src/renderer/compaction-history-modal.ts +19 -3
- package/src/renderer/compaction-preview.ts +13 -2
- package/src/renderer/compaction-quality.ts +100 -0
- package/src/renderer/config-modal.ts +81 -0
- package/src/renderer/conversation-overlays.ts +10 -17
- package/src/renderer/fleet-tab-strip.ts +56 -0
- package/src/renderer/footer-tips.ts +10 -2
- package/src/renderer/git-status.ts +40 -0
- package/src/renderer/help-overlay.ts +2 -2
- package/src/renderer/model-workspace.ts +44 -1
- package/src/renderer/panel-workspace-bar.ts +8 -2
- package/src/renderer/turn-injection.ts +114 -0
- package/src/runtime/bootstrap-command-context.ts +21 -1
- package/src/runtime/bootstrap-command-parts.ts +34 -7
- package/src/runtime/bootstrap-core.ts +14 -4
- package/src/runtime/bootstrap-shell.ts +29 -16
- package/src/runtime/bootstrap.ts +11 -17
- package/src/runtime/code-index-services.ts +112 -0
- package/src/runtime/orchestrator-core-services.ts +49 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/services.ts +91 -43
- package/src/runtime/ui-services.ts +8 -0
- package/src/runtime/workstream-services.ts +195 -0
- package/src/shell/blocking-input.ts +22 -1
- package/src/shell/ui-openers.ts +129 -17
- package/src/utils/format-duration.ts +8 -18
- package/src/utils/splash-lines.ts +1 -1
- package/src/version.ts +1 -1
- package/src/panels/agent-inspector-panel.ts +0 -786
- package/src/panels/approval-panel.ts +0 -252
- package/src/panels/automation-control-panel.ts +0 -479
- package/src/panels/cockpit-panel.ts +0 -481
- package/src/panels/cockpit-read-model.ts +0 -238
- package/src/panels/communication-panel.ts +0 -256
- package/src/panels/control-plane-panel.ts +0 -470
- package/src/panels/debug-panel.ts +0 -615
- package/src/panels/docs-panel.ts +0 -384
- package/src/panels/eval-panel.ts +0 -627
- package/src/panels/file-explorer-panel.ts +0 -673
- package/src/panels/file-preview-panel.ts +0 -517
- package/src/panels/hooks-panel.ts +0 -401
- package/src/panels/incident-review-panel.ts +0 -406
- package/src/panels/intelligence-panel.ts +0 -383
- package/src/panels/knowledge-graph-panel.ts +0 -515
- package/src/panels/marketplace-panel.ts +0 -399
- package/src/panels/memory-panel.ts +0 -558
- package/src/panels/ops-control-panel.ts +0 -329
- package/src/panels/ops-strategy-panel.ts +0 -321
- package/src/panels/orchestration-panel.ts +0 -465
- package/src/panels/panel-list-panel.ts +0 -566
- package/src/panels/plan-dashboard-panel.ts +0 -707
- package/src/panels/policy-panel.ts +0 -517
- package/src/panels/project-planning-panel.ts +0 -731
- package/src/panels/provider-health-panel.ts +0 -678
- package/src/panels/provider-health-tracker.ts +0 -310
- package/src/panels/provider-health-views.ts +0 -567
- package/src/panels/qr-panel.ts +0 -280
- package/src/panels/remote-panel.ts +0 -534
- package/src/panels/routes-panel.ts +0 -241
- package/src/panels/sandbox-panel.ts +0 -456
- package/src/panels/security-panel.ts +0 -447
- package/src/panels/services-panel.ts +0 -329
- package/src/panels/session-browser-panel.ts +0 -496
- package/src/panels/settings-sync-panel.ts +0 -398
- package/src/panels/subscription-panel.ts +0 -342
- package/src/panels/symbol-outline-panel.ts +0 -619
- package/src/panels/system-messages-panel.ts +0 -364
- package/src/panels/tasks-panel.ts +0 -608
- package/src/panels/thinking-panel.ts +0 -333
- package/src/panels/tool-inspector-panel.ts +0 -537
- package/src/panels/work-plan-panel.ts +0 -540
- package/src/panels/worktree-panel.ts +0 -360
- package/src/panels/wrfc-panel.ts +0 -790
- package/src/renderer/agent-detail-modal.ts +0 -466
- package/src/renderer/live-tail-modal.ts +0 -156
- package/src/renderer/process-modal.ts +0 -671
- package/src/renderer/qr-renderer.ts +0 -120
|
@@ -1,479 +0,0 @@
|
|
|
1
|
-
import type { Line } from '../types/grid.ts';
|
|
2
|
-
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
-
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
|
-
import type { UiAutomationSnapshot, UiReadModel, UiWatchersSnapshot } from '../runtime/ui-read-models.ts';
|
|
5
|
-
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
6
|
-
import type { AutomationManager } from '@pellux/goodvibes-sdk/platform/automation';
|
|
7
|
-
import type { AutomationScheduleDefinition } from '@pellux/goodvibes-sdk/platform/automation';
|
|
8
|
-
import type { WatcherRegistry } from '@pellux/goodvibes-sdk/platform/watchers';
|
|
9
|
-
import {
|
|
10
|
-
type ConfirmState,
|
|
11
|
-
handleConfirmInput,
|
|
12
|
-
renderConfirmLines,
|
|
13
|
-
} from './confirm-state.ts';
|
|
14
|
-
import {
|
|
15
|
-
buildDetailBlock,
|
|
16
|
-
buildEmptyState,
|
|
17
|
-
buildKeyValueLine,
|
|
18
|
-
buildPanelLine,
|
|
19
|
-
buildPanelWorkspace,
|
|
20
|
-
DEFAULT_PANEL_PALETTE,
|
|
21
|
-
type PanelPalette,
|
|
22
|
-
} from './polish.ts';
|
|
23
|
-
|
|
24
|
-
// Base chrome only — title band, state colors, and text tokens all come
|
|
25
|
-
// straight from DEFAULT_PANEL_PALETTE (WO-002).
|
|
26
|
-
const C = DEFAULT_PANEL_PALETTE;
|
|
27
|
-
|
|
28
|
-
function formatTime(value?: number): string {
|
|
29
|
-
if (!value) return 'n/a';
|
|
30
|
-
return new Date(value).toLocaleString();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function runStatusColor(status: string): string {
|
|
34
|
-
if (status === 'completed') return C.good;
|
|
35
|
-
if (status === 'failed' || status === 'dead_lettered') return C.bad;
|
|
36
|
-
if (status === 'cancelled') return C.warn;
|
|
37
|
-
return C.info;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function stateColor(state: string): string {
|
|
41
|
-
if (state === 'running') return C.good;
|
|
42
|
-
if (state === 'degraded' || state === 'starting') return C.warn;
|
|
43
|
-
if (state === 'failed') return C.bad;
|
|
44
|
-
return C.dim;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function sourceStatusColor(state?: string): string {
|
|
48
|
-
if (state === 'healthy') return C.good;
|
|
49
|
-
if (state === 'lagging' || state === 'stale' || state === 'degraded') return C.warn;
|
|
50
|
-
if (state === 'failed') return C.bad;
|
|
51
|
-
return C.dim;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function formatLag(value?: number): string {
|
|
55
|
-
if (!value || value <= 0) return 'n/a';
|
|
56
|
-
if (value < 1000) return `${value}ms`;
|
|
57
|
-
if (value < 60_000) return `${Math.round(value / 1000)}s`;
|
|
58
|
-
return `${Math.round(value / 60_000)}m`;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function formatEveryInterval(intervalMs: number): string {
|
|
62
|
-
const units: ReadonlyArray<readonly [number, string]> = [
|
|
63
|
-
[86_400_000, 'd'],
|
|
64
|
-
[3_600_000, 'h'],
|
|
65
|
-
[60_000, 'm'],
|
|
66
|
-
[1_000, 's'],
|
|
67
|
-
];
|
|
68
|
-
for (const [size, unit] of units) {
|
|
69
|
-
if (intervalMs >= size && intervalMs % size === 0) {
|
|
70
|
-
return `${intervalMs / size}${unit}`;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return `${intervalMs}ms`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function formatSchedule(schedule: AutomationScheduleDefinition): string {
|
|
77
|
-
switch (schedule.kind) {
|
|
78
|
-
case 'cron':
|
|
79
|
-
return schedule.timezone ? `${schedule.expression} [${schedule.timezone}]` : schedule.expression;
|
|
80
|
-
case 'every':
|
|
81
|
-
return formatEveryInterval(schedule.intervalMs);
|
|
82
|
-
case 'at':
|
|
83
|
-
return new Date(schedule.at).toLocaleString();
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function isWatcherActive(state: string): boolean {
|
|
88
|
-
return state === 'running' || state === 'degraded' || state === 'starting';
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
type AutomationRun = UiAutomationSnapshot['runs'][number];
|
|
92
|
-
type AutomationJob = UiAutomationSnapshot['jobs'][number];
|
|
93
|
-
type WatcherEntry = UiWatchersSnapshot['watchers'][number];
|
|
94
|
-
|
|
95
|
-
type FocusSection = 'runs' | 'jobs' | 'sources';
|
|
96
|
-
|
|
97
|
-
type AutomationItem =
|
|
98
|
-
| { readonly kind: 'run'; readonly run: AutomationRun }
|
|
99
|
-
| { readonly kind: 'job'; readonly job: AutomationJob }
|
|
100
|
-
| { readonly kind: 'source'; readonly watcher: WatcherEntry };
|
|
101
|
-
|
|
102
|
-
type ConfirmSubject = { readonly kind: 'job' | 'watcher'; readonly id: string };
|
|
103
|
-
|
|
104
|
-
type AutomationActions = Pick<AutomationManager, 'setEnabled' | 'runNow'>;
|
|
105
|
-
type WatcherLifecycle = Pick<WatcherRegistry, 'startWatcher' | 'stopWatcher'>;
|
|
106
|
-
|
|
107
|
-
export interface AutomationControlPanelDeps {
|
|
108
|
-
/** Drives real job actions (enable/disable, run now). Absent = jobs render read-only. */
|
|
109
|
-
readonly automationManager?: AutomationActions;
|
|
110
|
-
/** Drives real watcher lifecycle actions (start/stop). Absent = sources render read-only. */
|
|
111
|
-
readonly watcherRegistry?: WatcherLifecycle;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const FOCUS_SECTIONS: readonly FocusSection[] = ['runs', 'jobs', 'sources'];
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* AutomationControlPanel — merged automation console (WO-111).
|
|
118
|
-
*
|
|
119
|
-
* Absorbs the former SchedulePanel (jobs, enable/disable, run-now) and
|
|
120
|
-
* WatchersPanel (watcher health as a "Sources" section) into one dense,
|
|
121
|
-
* actionable view: jobs + runs + deliveries + sources, with Tab cycling
|
|
122
|
-
* between the three list sections.
|
|
123
|
-
*/
|
|
124
|
-
export class AutomationControlPanel extends ScrollableListPanel<AutomationItem> {
|
|
125
|
-
private readonly readModel?: UiReadModel<UiAutomationSnapshot>;
|
|
126
|
-
private readonly watchersReadModel?: UiReadModel<UiWatchersSnapshot>;
|
|
127
|
-
private readonly automationManager?: AutomationActions;
|
|
128
|
-
private readonly watcherRegistry?: WatcherLifecycle;
|
|
129
|
-
private readonly unsub: (() => void) | null;
|
|
130
|
-
private focusSection: FocusSection = 'runs';
|
|
131
|
-
private confirmAction: ConfirmState<ConfirmSubject> | null = null;
|
|
132
|
-
|
|
133
|
-
public constructor(
|
|
134
|
-
readModel?: UiReadModel<UiAutomationSnapshot>,
|
|
135
|
-
watchersReadModel?: UiReadModel<UiWatchersSnapshot>,
|
|
136
|
-
deps: AutomationControlPanelDeps = {},
|
|
137
|
-
) {
|
|
138
|
-
super('automation', 'Automation', '◨', 'automation-control');
|
|
139
|
-
this.showSelectionGutter = true; // I5: non-color selection affordance
|
|
140
|
-
this.filterEnabled = true;
|
|
141
|
-
this.filterLabel = 'Filter';
|
|
142
|
-
this.readModel = readModel;
|
|
143
|
-
this.watchersReadModel = watchersReadModel;
|
|
144
|
-
this.automationManager = deps.automationManager;
|
|
145
|
-
this.watcherRegistry = deps.watcherRegistry;
|
|
146
|
-
const unsubAutomation = readModel?.subscribe(() => this.markDirty());
|
|
147
|
-
const unsubWatchers = watchersReadModel?.subscribe(() => this.markDirty());
|
|
148
|
-
this.unsub = (unsubAutomation || unsubWatchers)
|
|
149
|
-
? () => { unsubAutomation?.(); unsubWatchers?.(); }
|
|
150
|
-
: null;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
public override onDestroy(): void {
|
|
154
|
-
this.unsub?.();
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
protected override getPalette(): PanelPalette {
|
|
158
|
-
return C;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// -------------------------------------------------------------------------
|
|
162
|
-
// Data
|
|
163
|
-
// -------------------------------------------------------------------------
|
|
164
|
-
|
|
165
|
-
private getJobs(): readonly AutomationJob[] {
|
|
166
|
-
return this.readModel?.getSnapshot().jobs ?? [];
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
private getRuns(): readonly AutomationRun[] {
|
|
170
|
-
return this.readModel?.getSnapshot().runs ?? [];
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
private getWatchers(): readonly WatcherEntry[] {
|
|
174
|
-
return this.watchersReadModel?.getSnapshot().watchers ?? [];
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
protected getItems(): readonly AutomationItem[] {
|
|
178
|
-
if (this.focusSection === 'jobs') return this.getJobs().map((job): AutomationItem => ({ kind: 'job', job }));
|
|
179
|
-
if (this.focusSection === 'sources') return this.getWatchers().map((watcher): AutomationItem => ({ kind: 'source', watcher }));
|
|
180
|
-
return this.getRuns().map((run): AutomationItem => ({ kind: 'run', run }));
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
protected override filterMatches(item: AutomationItem, q: string): boolean {
|
|
184
|
-
if (item.kind === 'run') {
|
|
185
|
-
return item.run.jobId.toLowerCase().includes(q)
|
|
186
|
-
|| item.run.status.toLowerCase().includes(q)
|
|
187
|
-
|| item.run.target.kind.toLowerCase().includes(q);
|
|
188
|
-
}
|
|
189
|
-
if (item.kind === 'job') {
|
|
190
|
-
return item.job.name.toLowerCase().includes(q) || item.job.id.toLowerCase().includes(q);
|
|
191
|
-
}
|
|
192
|
-
return item.watcher.label.toLowerCase().includes(q)
|
|
193
|
-
|| item.watcher.state.toLowerCase().includes(q)
|
|
194
|
-
|| String(item.watcher.sourceStatus ?? '').toLowerCase().includes(q);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
protected override getEmptyStateMessage(): string {
|
|
198
|
-
if (this.focusSection === 'jobs') return ' No automation jobs configured.';
|
|
199
|
-
if (this.focusSection === 'sources') return ' No watchers registered.';
|
|
200
|
-
return ' No automation activity recorded.';
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
|
|
204
|
-
if (this.focusSection === 'sources') {
|
|
205
|
-
return [{ command: '/services auth-review', summary: 'validate integration credentials before enabling remote watchers' }];
|
|
206
|
-
}
|
|
207
|
-
return [{ command: '/schedule add cron 0 * * * * repo sweep', summary: 'create a recurring automation job' }];
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// -------------------------------------------------------------------------
|
|
211
|
-
// Input
|
|
212
|
-
// -------------------------------------------------------------------------
|
|
213
|
-
|
|
214
|
-
handleInput(key: string): boolean {
|
|
215
|
-
if (this.lastError !== null) this.clearError();
|
|
216
|
-
|
|
217
|
-
if (this.confirmAction) {
|
|
218
|
-
const result = handleConfirmInput(this.confirmAction, key);
|
|
219
|
-
if (result === 'confirmed') {
|
|
220
|
-
this._executeConfirmed(this.confirmAction.subject);
|
|
221
|
-
this.confirmAction = null;
|
|
222
|
-
this.markDirty();
|
|
223
|
-
return true;
|
|
224
|
-
}
|
|
225
|
-
if (result === 'cancelled') {
|
|
226
|
-
this.confirmAction = null;
|
|
227
|
-
this.markDirty();
|
|
228
|
-
}
|
|
229
|
-
return true;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (!this.filterActive) {
|
|
233
|
-
switch (key) {
|
|
234
|
-
case 'tab':
|
|
235
|
-
this._cycleFocus();
|
|
236
|
-
return true;
|
|
237
|
-
case 'e':
|
|
238
|
-
this._toggleSelected();
|
|
239
|
-
return true;
|
|
240
|
-
case 'r':
|
|
241
|
-
this._runSelected();
|
|
242
|
-
return true;
|
|
243
|
-
case 'R':
|
|
244
|
-
this.markDirty();
|
|
245
|
-
return true;
|
|
246
|
-
default:
|
|
247
|
-
break;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return super.handleInput(key);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
private _cycleFocus(): void {
|
|
255
|
-
const idx = FOCUS_SECTIONS.indexOf(this.focusSection);
|
|
256
|
-
this.focusSection = FOCUS_SECTIONS[(idx + 1) % FOCUS_SECTIONS.length]!;
|
|
257
|
-
this.selectedIndex = 0;
|
|
258
|
-
this.markDirty();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
private _toggleSelected(): void {
|
|
262
|
-
const item = this.getSelectedItem();
|
|
263
|
-
if (!item) return;
|
|
264
|
-
if (item.kind === 'job') {
|
|
265
|
-
if (item.job.enabled) {
|
|
266
|
-
this.confirmAction = { subject: { kind: 'job', id: item.job.id }, label: item.job.name, verb: 'Disable' };
|
|
267
|
-
} else if (this.automationManager) {
|
|
268
|
-
void this.automationManager.setEnabled(item.job.id, true).catch(() => {});
|
|
269
|
-
}
|
|
270
|
-
this.markDirty();
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
if (item.kind === 'source' && this.watcherRegistry) {
|
|
274
|
-
if (isWatcherActive(item.watcher.state)) {
|
|
275
|
-
this.confirmAction = { subject: { kind: 'watcher', id: item.watcher.id }, label: item.watcher.label, verb: 'Stop' };
|
|
276
|
-
} else {
|
|
277
|
-
this.watcherRegistry.startWatcher(item.watcher.id);
|
|
278
|
-
}
|
|
279
|
-
this.markDirty();
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
private _runSelected(): void {
|
|
284
|
-
const item = this.getSelectedItem();
|
|
285
|
-
if (item?.kind === 'job' && this.automationManager) {
|
|
286
|
-
void this.automationManager.runNow(item.job.id).catch(() => {});
|
|
287
|
-
this.markDirty();
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
private _executeConfirmed(subject: ConfirmSubject): void {
|
|
292
|
-
if (subject.kind === 'job') {
|
|
293
|
-
void this.automationManager?.setEnabled(subject.id, false).catch(() => {});
|
|
294
|
-
} else {
|
|
295
|
-
this.watcherRegistry?.stopWatcher(subject.id, 'user requested');
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// -------------------------------------------------------------------------
|
|
300
|
-
// Rendering — item rows
|
|
301
|
-
// -------------------------------------------------------------------------
|
|
302
|
-
|
|
303
|
-
protected renderItem(item: AutomationItem, _index: number, selected: boolean, width: number): Line {
|
|
304
|
-
if (item.kind === 'run') return this._renderRunRow(item.run, selected, width);
|
|
305
|
-
if (item.kind === 'job') return this._renderJobRow(item.job, selected, width);
|
|
306
|
-
return this._renderSourceRow(item.watcher, selected, width);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
private _renderRunRow(run: AutomationRun, selected: boolean, width: number): Line {
|
|
310
|
-
const bg = selected ? C.selectBg : undefined;
|
|
311
|
-
const jobs = this.getJobs();
|
|
312
|
-
const name = jobs.find((job) => job.id === run.jobId)?.name ?? run.jobId;
|
|
313
|
-
return buildPanelLine(width, [
|
|
314
|
-
[' ', C.label, bg],
|
|
315
|
-
[run.status.padEnd(11), runStatusColor(run.status), bg],
|
|
316
|
-
[` ${truncateDisplay(name, 22).padEnd(22)}`, C.value, bg],
|
|
317
|
-
[` ${truncateDisplay(run.target.kind, 12).padEnd(12)}`, C.info, bg],
|
|
318
|
-
[` ${truncateDisplay(formatTime(run.queuedAt), Math.max(0, width - 49))}`, C.dim, bg],
|
|
319
|
-
]);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
private _renderJobRow(job: AutomationJob, selected: boolean, width: number): Line {
|
|
323
|
-
const bg = selected ? C.selectBg : undefined;
|
|
324
|
-
const bullet = job.enabled ? '● ' : '○ ';
|
|
325
|
-
const bulletFg = job.enabled ? C.good : C.warn;
|
|
326
|
-
return buildPanelLine(width, [
|
|
327
|
-
[bullet, bulletFg, bg],
|
|
328
|
-
[truncateDisplay(job.name, 24).padEnd(24), C.value, bg],
|
|
329
|
-
[` ${truncateDisplay(formatSchedule(job.schedule), 18).padEnd(18)}`, C.info, bg],
|
|
330
|
-
[` next ${truncateDisplay(formatTime(job.nextRunAt), Math.max(0, width - 51))}`, C.dim, bg],
|
|
331
|
-
]);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
private _renderSourceRow(watcher: WatcherEntry, selected: boolean, width: number): Line {
|
|
335
|
-
const bg = selected ? C.selectBg : undefined;
|
|
336
|
-
return buildPanelLine(width, [
|
|
337
|
-
[' ', C.label, bg],
|
|
338
|
-
[watcher.state.padEnd(10), stateColor(watcher.state), bg],
|
|
339
|
-
[` ${truncateDisplay(watcher.label, 18).padEnd(18)}`, C.value, bg],
|
|
340
|
-
[` ${String(watcher.sourceStatus ?? 'unknown').padEnd(10)}`, sourceStatusColor(watcher.sourceStatus), bg],
|
|
341
|
-
[` ${truncateDisplay(formatLag(watcher.sourceLagMs), Math.max(0, width - 43))}`, C.dim, bg],
|
|
342
|
-
]);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// -------------------------------------------------------------------------
|
|
346
|
-
// Rendering — workspace
|
|
347
|
-
// -------------------------------------------------------------------------
|
|
348
|
-
|
|
349
|
-
public render(width: number, height: number): Line[] {
|
|
350
|
-
const intro = 'Automation jobs, active runs, deliveries, and watcher-fed sources across the shared control plane.';
|
|
351
|
-
|
|
352
|
-
if (!this.readModel) {
|
|
353
|
-
const workspace = buildPanelWorkspace(width, height, {
|
|
354
|
-
title: 'Automation Control',
|
|
355
|
-
intro,
|
|
356
|
-
sections: [{
|
|
357
|
-
lines: buildEmptyState(
|
|
358
|
-
width,
|
|
359
|
-
' Runtime store not wired.',
|
|
360
|
-
'This panel needs the shared runtime store to inspect automation jobs, runs, and deliveries.',
|
|
361
|
-
[],
|
|
362
|
-
C,
|
|
363
|
-
),
|
|
364
|
-
}],
|
|
365
|
-
palette: C,
|
|
366
|
-
});
|
|
367
|
-
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
368
|
-
return workspace;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const snapshot = this.readModel.getSnapshot();
|
|
372
|
-
|
|
373
|
-
const headerLines: Line[] = [
|
|
374
|
-
buildKeyValueLine(width, [
|
|
375
|
-
{ label: 'jobs', value: String(snapshot.totalJobs), valueColor: snapshot.totalJobs > 0 ? C.info : C.dim },
|
|
376
|
-
{ label: 'runs', value: String(snapshot.totalRuns), valueColor: snapshot.totalRuns > 0 ? C.value : C.dim },
|
|
377
|
-
{ label: 'active', value: String(snapshot.activeRunIds.length), valueColor: snapshot.activeRunIds.length > 0 ? C.warn : C.dim },
|
|
378
|
-
{ label: 'failed', value: String(snapshot.totalFailed), valueColor: snapshot.totalFailed > 0 ? C.bad : C.dim },
|
|
379
|
-
], C),
|
|
380
|
-
buildKeyValueLine(width, [
|
|
381
|
-
{ label: 'deliveries ok', value: String(snapshot.deliveryTotals.succeeded), valueColor: snapshot.deliveryTotals.succeeded > 0 ? C.good : C.dim },
|
|
382
|
-
{ label: 'delivery fail', value: String(snapshot.deliveryTotals.failed), valueColor: snapshot.deliveryTotals.failed > 0 ? C.bad : C.dim },
|
|
383
|
-
{ label: 'dead letters', value: String(snapshot.deliveryTotals.deadLettered), valueColor: snapshot.deliveryTotals.deadLettered > 0 ? C.warn : C.dim },
|
|
384
|
-
{ label: 'sources', value: String(snapshot.sourceCount), valueColor: snapshot.sourceCount > 0 ? C.info : C.dim },
|
|
385
|
-
], C),
|
|
386
|
-
this._buildFocusTabsLine(width),
|
|
387
|
-
];
|
|
388
|
-
|
|
389
|
-
const items = this.getVisibleItems();
|
|
390
|
-
this.clampSelection();
|
|
391
|
-
const selected = this.getSelectedItem();
|
|
392
|
-
|
|
393
|
-
const footerLines: Line[] = [];
|
|
394
|
-
if (items.length > 0 && selected) {
|
|
395
|
-
footerLines.push(...this._buildDetailLines(width, selected, snapshot));
|
|
396
|
-
if (this.confirmAction) footerLines.push(...renderConfirmLines(width, this.confirmAction));
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
return this.renderList(width, height, {
|
|
400
|
-
title: 'Automation Control',
|
|
401
|
-
header: headerLines,
|
|
402
|
-
footer: footerLines,
|
|
403
|
-
hints: this._buildHints(selected),
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
private _buildFocusTabsLine(width: number): Line {
|
|
408
|
-
const seg = (label: string, section: FocusSection): [string, string] => [
|
|
409
|
-
` ${label} `,
|
|
410
|
-
this.focusSection === section ? C.value : C.dim,
|
|
411
|
-
];
|
|
412
|
-
return buildPanelLine(width, [
|
|
413
|
-
seg('Runs', 'runs'),
|
|
414
|
-
seg('Jobs', 'jobs'),
|
|
415
|
-
seg('Sources', 'sources'),
|
|
416
|
-
[' (Tab to switch)', C.dim],
|
|
417
|
-
]);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
private _buildHints(selected: AutomationItem | undefined): ReadonlyArray<{ keys: string; label: string }> {
|
|
421
|
-
if (this.filterActive) {
|
|
422
|
-
return [{ keys: 'type', label: 'filter' }, { keys: 'Enter', label: 'apply' }, { keys: 'Esc', label: 'clear' }];
|
|
423
|
-
}
|
|
424
|
-
const hints: Array<{ keys: string; label: string }> = [
|
|
425
|
-
{ keys: 'Up/Down', label: `select ${this.focusSection === 'jobs' ? 'job' : this.focusSection === 'sources' ? 'source' : 'run'}` },
|
|
426
|
-
{ keys: 'Tab', label: 'section' },
|
|
427
|
-
];
|
|
428
|
-
if (this.focusSection === 'jobs' && selected?.kind === 'job' && this.automationManager) {
|
|
429
|
-
hints.push({ keys: 'e', label: selected.job.enabled ? 'disable' : 'enable' });
|
|
430
|
-
hints.push({ keys: 'r', label: 'run now' });
|
|
431
|
-
}
|
|
432
|
-
if (this.focusSection === 'sources' && this.watcherRegistry && selected?.kind === 'source') {
|
|
433
|
-
hints.push({ keys: 'e', label: isWatcherActive(selected.watcher.state) ? 'stop' : 'start' });
|
|
434
|
-
}
|
|
435
|
-
hints.push({ keys: 'R', label: 'refresh' }, { keys: '/', label: 'filter' });
|
|
436
|
-
return hints;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
private _buildDetailLines(width: number, item: AutomationItem, snapshot: UiAutomationSnapshot): Line[] {
|
|
440
|
-
if (item.kind === 'run') {
|
|
441
|
-
const jobName = this.getJobs().find((job) => job.id === item.run.jobId)?.name ?? item.run.jobId;
|
|
442
|
-
const rows: Line[] = [
|
|
443
|
-
buildPanelLine(width, [[' Run: ', C.label], [item.run.id, C.value], [' Status: ', C.label], [item.run.status, runStatusColor(item.run.status)]]),
|
|
444
|
-
buildPanelLine(width, [[' Job: ', C.label], [jobName, C.value], [' Agent: ', C.label], [item.run.agentId ?? 'n/a', C.info]]),
|
|
445
|
-
buildPanelLine(width, [[' Queue: ', C.label], [formatTime(item.run.queuedAt), C.dim], [' End: ', C.label], [formatTime(item.run.endedAt), C.dim]]),
|
|
446
|
-
buildPanelLine(width, [[' Trigger: ', C.label], [item.run.triggeredBy.kind, C.info], [' Target: ', C.label], [item.run.target.kind, C.value]]),
|
|
447
|
-
buildPanelLine(width, [[' Deliveries: ', C.label], [String(item.run.deliveryIds.length), item.run.deliveryIds.length > 0 ? C.info : C.dim], [' Route: ', C.label], [item.run.routeId ?? 'n/a', C.dim]]),
|
|
448
|
-
];
|
|
449
|
-
if (item.run.error) {
|
|
450
|
-
rows.push(buildPanelLine(width, [[' Error: ', C.label], [truncateDisplay(item.run.error, Math.max(0, width - 10)), C.bad]]));
|
|
451
|
-
}
|
|
452
|
-
return buildDetailBlock(width, `Run · ${item.run.id}`, rows, C);
|
|
453
|
-
}
|
|
454
|
-
if (item.kind === 'job') {
|
|
455
|
-
const recent = snapshot.runs.filter((run) => run.jobId === item.job.id).slice(0, 3).reverse();
|
|
456
|
-
const dots = recent.length > 0
|
|
457
|
-
? recent.map((run) => (run.status === 'failed' ? '✗' : run.status === 'completed' ? '✓' : '●')).join(' ')
|
|
458
|
-
: 'none';
|
|
459
|
-
const rows: Line[] = [
|
|
460
|
-
buildPanelLine(width, [[' Job: ', C.label], [item.job.name, C.value], [' State: ', C.label], [item.job.enabled ? 'enabled' : 'paused', item.job.enabled ? C.good : C.warn]]),
|
|
461
|
-
buildPanelLine(width, [[' Schedule: ', C.label], [formatSchedule(item.job.schedule), C.info]]),
|
|
462
|
-
buildPanelLine(width, [[' Next: ', C.label], [formatTime(item.job.nextRunAt), C.dim], [' Last: ', C.label], [formatTime(item.job.lastRunAt), C.dim]]),
|
|
463
|
-
buildPanelLine(width, [[' Runs: ', C.label], [String(item.job.runCount), C.value], [' Recent: ', C.label], [dots, C.dim]]),
|
|
464
|
-
buildPanelLine(width, [[' Prompt: ', C.label], [truncateDisplay(item.job.execution.prompt ?? item.job.description ?? '', Math.max(0, width - 12)), C.dim]]),
|
|
465
|
-
];
|
|
466
|
-
return buildDetailBlock(width, `Job · ${item.job.name}`, rows, C);
|
|
467
|
-
}
|
|
468
|
-
const w = item.watcher;
|
|
469
|
-
const rows: Line[] = [
|
|
470
|
-
buildPanelLine(width, [[' Watcher: ', C.label], [w.label, C.value], [' Kind: ', C.label], [w.kind, C.info]]),
|
|
471
|
-
buildPanelLine(width, [[' State: ', C.label], [w.state, stateColor(w.state)], [' Source: ', C.label], [w.source.kind, C.value]]),
|
|
472
|
-
buildPanelLine(width, [[' Source status: ', C.label], [w.sourceStatus ?? 'unknown', sourceStatusColor(w.sourceStatus)], [' Lag: ', C.label], [formatLag(w.sourceLagMs), w.sourceLagMs ? C.warn : C.dim]]),
|
|
473
|
-
buildPanelLine(width, [[' Heartbeat: ', C.label], [formatTime(w.lastHeartbeatAt), C.dim], [' Checkpoint: ', C.label], [truncateDisplay(w.lastCheckpoint ?? 'n/a', Math.max(0, width - 38)), C.dim]]),
|
|
474
|
-
];
|
|
475
|
-
if (w.degradedReason) rows.push(buildPanelLine(width, [[' Reason: ', C.label], [truncateDisplay(w.degradedReason, Math.max(0, width - 11)), C.warn]]));
|
|
476
|
-
if (w.lastError) rows.push(buildPanelLine(width, [[' Error: ', C.label], [truncateDisplay(w.lastError, Math.max(0, width - 10)), C.bad]]));
|
|
477
|
-
return buildDetailBlock(width, `Source · ${w.label}`, rows, C);
|
|
478
|
-
}
|
|
479
|
-
}
|