@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.
Files changed (194) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +20 -11
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/core/alert-gating.ts +67 -0
  6. package/src/core/approval-alert.ts +72 -0
  7. package/src/core/budget-breach-notifier.ts +106 -0
  8. package/src/core/conversation-rendering.ts +19 -0
  9. package/src/core/conversation.ts +18 -0
  10. package/src/core/focus-tracker.ts +41 -0
  11. package/src/core/long-task-notifier.ts +33 -6
  12. package/src/core/system-message-router.ts +37 -51
  13. package/src/core/turn-cancellation.ts +20 -0
  14. package/src/core/turn-event-wiring.ts +64 -3
  15. package/src/export/cost-utils.ts +36 -0
  16. package/src/input/command-registry.ts +38 -1
  17. package/src/input/commands/checkpoint-runtime.ts +280 -0
  18. package/src/input/commands/codebase-runtime.ts +192 -0
  19. package/src/input/commands/eval.ts +1 -1
  20. package/src/input/commands/health-runtime.ts +1 -1
  21. package/src/input/commands/image-runtime.ts +112 -0
  22. package/src/input/commands/intelligence-runtime.ts +11 -1
  23. package/src/input/commands/local-auth-runtime.ts +4 -1
  24. package/src/input/commands/local-runtime.ts +9 -3
  25. package/src/input/commands/marketplace-runtime.ts +2 -2
  26. package/src/input/commands/memory.ts +6 -1
  27. package/src/input/commands/operator-panel-runtime.ts +40 -24
  28. package/src/input/commands/planning-runtime.ts +26 -3
  29. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  30. package/src/input/commands/plugin-runtime.ts +2 -2
  31. package/src/input/commands/policy-dispatch.ts +21 -0
  32. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  33. package/src/input/commands/qrcode-runtime.ts +3 -3
  34. package/src/input/commands/recall-review.ts +35 -0
  35. package/src/input/commands/remote-runtime.ts +3 -3
  36. package/src/input/commands/runtime-services.ts +54 -13
  37. package/src/input/commands/services-runtime.ts +1 -1
  38. package/src/input/commands/session-workflow.ts +16 -1
  39. package/src/input/commands/settings-sync-runtime.ts +1 -1
  40. package/src/input/commands/shell-core.ts +15 -7
  41. package/src/input/commands/skills-runtime.ts +2 -8
  42. package/src/input/commands/subscription-runtime.ts +2 -2
  43. package/src/input/commands/test-runtime.ts +277 -0
  44. package/src/input/commands/websearch-runtime.ts +101 -0
  45. package/src/input/commands/work-plan-runtime.ts +36 -10
  46. package/src/input/commands/workstream-runtime.ts +338 -0
  47. package/src/input/commands.ts +12 -0
  48. package/src/input/config-modal-types.ts +161 -0
  49. package/src/input/config-modal.ts +377 -0
  50. package/src/input/feed-context-factory.ts +7 -8
  51. package/src/input/handler-command-route.ts +27 -17
  52. package/src/input/handler-feed-routes.ts +61 -23
  53. package/src/input/handler-feed.ts +47 -23
  54. package/src/input/handler-interactions.ts +1 -3
  55. package/src/input/handler-modal-routes.ts +65 -0
  56. package/src/input/handler-modal-stack.ts +3 -5
  57. package/src/input/handler-modal-token-routes.ts +16 -49
  58. package/src/input/handler-picker-routes.ts +11 -94
  59. package/src/input/handler-shortcuts.ts +24 -14
  60. package/src/input/handler-types.ts +2 -6
  61. package/src/input/handler-ui-state.ts +10 -22
  62. package/src/input/handler.ts +6 -28
  63. package/src/input/keybindings.ts +22 -8
  64. package/src/input/model-picker-types.ts +24 -6
  65. package/src/input/model-picker.ts +58 -0
  66. package/src/input/panel-integration-actions.ts +9 -170
  67. package/src/input/settings-modal-data.ts +95 -0
  68. package/src/input/settings-modal-mutations.ts +6 -4
  69. package/src/main.ts +24 -27
  70. package/src/panels/agent-inspector-shared.ts +2 -1
  71. package/src/panels/base-panel.ts +22 -1
  72. package/src/panels/builtin/agent.ts +21 -107
  73. package/src/panels/builtin/development.ts +15 -61
  74. package/src/panels/builtin/knowledge.ts +8 -32
  75. package/src/panels/builtin/operations.ts +84 -428
  76. package/src/panels/builtin/session.ts +21 -112
  77. package/src/panels/builtin/shared.ts +9 -43
  78. package/src/panels/builtin-modals.ts +218 -0
  79. package/src/panels/builtin-panels.ts +5 -0
  80. package/src/panels/cost-tracker-panel.ts +36 -10
  81. package/src/panels/diff-panel.ts +12 -43
  82. package/src/panels/eval-registry.ts +60 -0
  83. package/src/panels/fleet-panel.ts +800 -0
  84. package/src/panels/fleet-read-model.ts +477 -0
  85. package/src/panels/fleet-steer.ts +92 -0
  86. package/src/panels/fleet-stop.ts +125 -0
  87. package/src/panels/fleet-tabs.ts +230 -0
  88. package/src/panels/fleet-transcript.ts +356 -0
  89. package/src/panels/index.ts +7 -31
  90. package/src/panels/modals/hooks-modal.ts +187 -0
  91. package/src/panels/modals/keybindings-modal.ts +151 -0
  92. package/src/panels/modals/knowledge-modal.ts +158 -0
  93. package/src/panels/modals/local-auth-modal.ts +132 -0
  94. package/src/panels/modals/marketplace-modal.ts +218 -0
  95. package/src/panels/modals/memory-modal.ts +180 -0
  96. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  97. package/src/panels/modals/modal-theme.ts +36 -0
  98. package/src/panels/modals/pairing-modal.ts +144 -0
  99. package/src/panels/modals/planning-modal.ts +282 -0
  100. package/src/panels/modals/plugins-modal.ts +174 -0
  101. package/src/panels/modals/policy-modal.ts +256 -0
  102. package/src/panels/modals/provider-health-modal.ts +136 -0
  103. package/src/panels/modals/remote-modal.ts +115 -0
  104. package/src/panels/modals/sandbox-modal.ts +150 -0
  105. package/src/panels/modals/security-modal.ts +217 -0
  106. package/src/panels/modals/services-modal.ts +179 -0
  107. package/src/panels/modals/settings-sync-modal.ts +142 -0
  108. package/src/panels/modals/skills-modal.ts +189 -0
  109. package/src/panels/modals/subscription-modal.ts +172 -0
  110. package/src/panels/modals/work-plan-modal.ts +149 -0
  111. package/src/panels/panel-confirm-overlay.ts +72 -0
  112. package/src/panels/panel-manager.ts +108 -5
  113. package/src/panels/project-planning-answer-actions.ts +4 -2
  114. package/src/panels/skills-panel.ts +7 -51
  115. package/src/panels/types.ts +13 -0
  116. package/src/permissions/hunk-selection.ts +179 -0
  117. package/src/permissions/prompt.ts +60 -4
  118. package/src/renderer/compaction-history-modal.ts +19 -3
  119. package/src/renderer/compaction-preview.ts +13 -2
  120. package/src/renderer/compaction-quality.ts +100 -0
  121. package/src/renderer/config-modal.ts +81 -0
  122. package/src/renderer/conversation-overlays.ts +10 -17
  123. package/src/renderer/fleet-tab-strip.ts +56 -0
  124. package/src/renderer/footer-tips.ts +10 -2
  125. package/src/renderer/git-status.ts +40 -0
  126. package/src/renderer/help-overlay.ts +2 -2
  127. package/src/renderer/model-workspace.ts +44 -1
  128. package/src/renderer/panel-workspace-bar.ts +8 -2
  129. package/src/renderer/turn-injection.ts +114 -0
  130. package/src/runtime/bootstrap-command-context.ts +21 -1
  131. package/src/runtime/bootstrap-command-parts.ts +34 -7
  132. package/src/runtime/bootstrap-core.ts +14 -4
  133. package/src/runtime/bootstrap-shell.ts +29 -16
  134. package/src/runtime/bootstrap.ts +11 -17
  135. package/src/runtime/code-index-services.ts +112 -0
  136. package/src/runtime/orchestrator-core-services.ts +49 -0
  137. package/src/runtime/process-lifecycle.ts +3 -1
  138. package/src/runtime/services.ts +91 -43
  139. package/src/runtime/ui-services.ts +8 -0
  140. package/src/runtime/workstream-services.ts +195 -0
  141. package/src/shell/blocking-input.ts +22 -1
  142. package/src/shell/ui-openers.ts +129 -17
  143. package/src/utils/format-duration.ts +8 -18
  144. package/src/utils/splash-lines.ts +1 -1
  145. package/src/version.ts +1 -1
  146. package/src/panels/agent-inspector-panel.ts +0 -786
  147. package/src/panels/approval-panel.ts +0 -252
  148. package/src/panels/automation-control-panel.ts +0 -479
  149. package/src/panels/cockpit-panel.ts +0 -481
  150. package/src/panels/cockpit-read-model.ts +0 -238
  151. package/src/panels/communication-panel.ts +0 -256
  152. package/src/panels/control-plane-panel.ts +0 -470
  153. package/src/panels/debug-panel.ts +0 -615
  154. package/src/panels/docs-panel.ts +0 -384
  155. package/src/panels/eval-panel.ts +0 -627
  156. package/src/panels/file-explorer-panel.ts +0 -673
  157. package/src/panels/file-preview-panel.ts +0 -517
  158. package/src/panels/hooks-panel.ts +0 -401
  159. package/src/panels/incident-review-panel.ts +0 -406
  160. package/src/panels/intelligence-panel.ts +0 -383
  161. package/src/panels/knowledge-graph-panel.ts +0 -515
  162. package/src/panels/marketplace-panel.ts +0 -399
  163. package/src/panels/memory-panel.ts +0 -558
  164. package/src/panels/ops-control-panel.ts +0 -329
  165. package/src/panels/ops-strategy-panel.ts +0 -321
  166. package/src/panels/orchestration-panel.ts +0 -465
  167. package/src/panels/panel-list-panel.ts +0 -566
  168. package/src/panels/plan-dashboard-panel.ts +0 -707
  169. package/src/panels/policy-panel.ts +0 -517
  170. package/src/panels/project-planning-panel.ts +0 -731
  171. package/src/panels/provider-health-panel.ts +0 -678
  172. package/src/panels/provider-health-tracker.ts +0 -310
  173. package/src/panels/provider-health-views.ts +0 -567
  174. package/src/panels/qr-panel.ts +0 -280
  175. package/src/panels/remote-panel.ts +0 -534
  176. package/src/panels/routes-panel.ts +0 -241
  177. package/src/panels/sandbox-panel.ts +0 -456
  178. package/src/panels/security-panel.ts +0 -447
  179. package/src/panels/services-panel.ts +0 -329
  180. package/src/panels/session-browser-panel.ts +0 -496
  181. package/src/panels/settings-sync-panel.ts +0 -398
  182. package/src/panels/subscription-panel.ts +0 -342
  183. package/src/panels/symbol-outline-panel.ts +0 -619
  184. package/src/panels/system-messages-panel.ts +0 -364
  185. package/src/panels/tasks-panel.ts +0 -608
  186. package/src/panels/thinking-panel.ts +0 -333
  187. package/src/panels/tool-inspector-panel.ts +0 -537
  188. package/src/panels/work-plan-panel.ts +0 -540
  189. package/src/panels/worktree-panel.ts +0 -360
  190. package/src/panels/wrfc-panel.ts +0 -790
  191. package/src/renderer/agent-detail-modal.ts +0 -466
  192. package/src/renderer/live-tail-modal.ts +0 -156
  193. package/src/renderer/process-modal.ts +0 -671
  194. 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
- }