@pellux/goodvibes-tui 1.1.0 → 1.9.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 (267) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +28 -18
  3. package/docs/foundation-artifacts/operator-contract.json +4045 -1763
  4. package/package.json +2 -2
  5. package/src/audio/spoken-turn-controller.ts +12 -2
  6. package/src/audio/spoken-turn-wiring.ts +2 -1
  7. package/src/cli/help.ts +8 -1
  8. package/src/cli/service-posture.ts +2 -1
  9. package/src/cli/status.ts +2 -1
  10. package/src/cli/surface-command.ts +2 -2
  11. package/src/core/alert-gating.ts +67 -0
  12. package/src/core/approval-alert.ts +72 -0
  13. package/src/core/budget-breach-notifier.ts +106 -0
  14. package/src/core/composer-state.ts +11 -3
  15. package/src/core/conversation-line-cache.ts +27 -3
  16. package/src/core/conversation-rendering.ts +90 -14
  17. package/src/core/conversation.ts +18 -0
  18. package/src/core/focus-tracker.ts +41 -0
  19. package/src/core/long-task-notifier.ts +33 -6
  20. package/src/core/stream-event-wiring.ts +20 -1
  21. package/src/core/system-message-noise.ts +87 -0
  22. package/src/core/system-message-router.ts +104 -51
  23. package/src/core/turn-cancellation.ts +25 -0
  24. package/src/core/turn-event-wiring.ts +72 -3
  25. package/src/daemon/cli.ts +29 -2
  26. package/src/daemon/handlers/register.ts +8 -1
  27. package/src/daemon/service-commands.ts +329 -0
  28. package/src/export/cost-utils.ts +36 -0
  29. package/src/input/autocomplete.ts +27 -1
  30. package/src/input/command-registry.ts +84 -5
  31. package/src/input/commands/checkpoint-runtime.ts +280 -0
  32. package/src/input/commands/codebase-runtime.ts +232 -0
  33. package/src/input/commands/config.ts +43 -3
  34. package/src/input/commands/eval.ts +1 -1
  35. package/src/input/commands/health-runtime.ts +10 -2
  36. package/src/input/commands/image-runtime.ts +112 -0
  37. package/src/input/commands/intelligence-runtime.ts +11 -1
  38. package/src/input/commands/local-auth-runtime.ts +4 -1
  39. package/src/input/commands/local-runtime.ts +9 -3
  40. package/src/input/commands/marketplace-runtime.ts +2 -2
  41. package/src/input/commands/memory.ts +73 -35
  42. package/src/input/commands/operator-panel-runtime.ts +71 -31
  43. package/src/input/commands/planning-runtime.ts +116 -4
  44. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  45. package/src/input/commands/plugin-runtime.ts +2 -2
  46. package/src/input/commands/policy-dispatch.ts +21 -0
  47. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  48. package/src/input/commands/qrcode-runtime.ts +26 -6
  49. package/src/input/commands/recall-review.ts +35 -0
  50. package/src/input/commands/remote-runtime-setup.ts +5 -3
  51. package/src/input/commands/remote-runtime.ts +3 -3
  52. package/src/input/commands/runtime-services.ts +54 -13
  53. package/src/input/commands/services-runtime.ts +1 -1
  54. package/src/input/commands/session-content.ts +20 -9
  55. package/src/input/commands/session-workflow.ts +16 -1
  56. package/src/input/commands/settings-sync-runtime.ts +16 -4
  57. package/src/input/commands/shell-core.ts +25 -8
  58. package/src/input/commands/skills-runtime.ts +2 -8
  59. package/src/input/commands/subscription-runtime.ts +2 -2
  60. package/src/input/commands/test-runtime.ts +277 -0
  61. package/src/input/commands/websearch-runtime.ts +101 -0
  62. package/src/input/commands/work-plan-runtime.ts +36 -10
  63. package/src/input/commands/workstream-runtime.ts +488 -0
  64. package/src/input/commands.ts +12 -0
  65. package/src/input/config-modal-types.ts +175 -0
  66. package/src/input/config-modal.ts +592 -0
  67. package/src/input/feed-context-factory.ts +10 -8
  68. package/src/input/handler-command-route.ts +37 -20
  69. package/src/input/handler-content-actions.ts +17 -2
  70. package/src/input/handler-feed-routes.ts +74 -114
  71. package/src/input/handler-feed.ts +79 -27
  72. package/src/input/handler-interactions.ts +1 -3
  73. package/src/input/handler-modal-routes.ts +136 -6
  74. package/src/input/handler-modal-stack.ts +14 -7
  75. package/src/input/handler-modal-token-routes.ts +16 -49
  76. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  77. package/src/input/handler-onboarding.ts +71 -59
  78. package/src/input/handler-picker-routes.ts +26 -102
  79. package/src/input/handler-shortcuts.ts +74 -14
  80. package/src/input/handler-types.ts +2 -6
  81. package/src/input/handler-ui-state.ts +10 -22
  82. package/src/input/handler.ts +12 -29
  83. package/src/input/keybindings.ts +23 -8
  84. package/src/input/model-picker-types.ts +24 -6
  85. package/src/input/model-picker.ts +77 -2
  86. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  87. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  88. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  89. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  90. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  91. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  92. package/src/input/panel-integration-actions.ts +9 -170
  93. package/src/input/panel-mouse-geometry.ts +97 -0
  94. package/src/input/panel-paste-flood-guard.ts +86 -0
  95. package/src/input/selection-modal.ts +11 -0
  96. package/src/input/session-picker-modal.ts +44 -4
  97. package/src/input/settings-modal-data.ts +146 -2
  98. package/src/input/settings-modal-mutations.ts +6 -4
  99. package/src/input/settings-modal-types.ts +4 -2
  100. package/src/main.ts +49 -51
  101. package/src/panels/agent-inspector-shared.ts +2 -1
  102. package/src/panels/base-panel.ts +22 -1
  103. package/src/panels/builtin/agent.ts +21 -107
  104. package/src/panels/builtin/development.ts +15 -61
  105. package/src/panels/builtin/knowledge.ts +8 -32
  106. package/src/panels/builtin/operations.ts +84 -428
  107. package/src/panels/builtin/session.ts +21 -112
  108. package/src/panels/builtin/shared.ts +18 -46
  109. package/src/panels/builtin-modals.ts +218 -0
  110. package/src/panels/builtin-panels.ts +5 -0
  111. package/src/panels/cost-tracker-panel.ts +36 -10
  112. package/src/panels/diff-panel.ts +12 -43
  113. package/src/panels/eval-registry.ts +60 -0
  114. package/src/panels/fleet-deep-link.ts +31 -0
  115. package/src/panels/fleet-panel-format.ts +62 -0
  116. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  117. package/src/panels/fleet-panel.ts +758 -0
  118. package/src/panels/fleet-read-model.ts +520 -0
  119. package/src/panels/fleet-steer.ts +125 -0
  120. package/src/panels/fleet-stop.ts +153 -0
  121. package/src/panels/fleet-tabs.ts +230 -0
  122. package/src/panels/fleet-transcript.ts +356 -0
  123. package/src/panels/index.ts +7 -31
  124. package/src/panels/modals/hooks-modal.ts +187 -0
  125. package/src/panels/modals/keybindings-modal.ts +166 -0
  126. package/src/panels/modals/knowledge-modal.ts +158 -0
  127. package/src/panels/modals/local-auth-modal.ts +132 -0
  128. package/src/panels/modals/marketplace-modal.ts +218 -0
  129. package/src/panels/modals/memory-modal.ts +180 -0
  130. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  131. package/src/panels/modals/modal-theme.ts +42 -0
  132. package/src/panels/modals/pairing-modal.ts +163 -0
  133. package/src/panels/modals/planning-modal.ts +304 -0
  134. package/src/panels/modals/plugins-modal.ts +174 -0
  135. package/src/panels/modals/policy-modal.ts +256 -0
  136. package/src/panels/modals/provider-health-modal.ts +136 -0
  137. package/src/panels/modals/remote-modal.ts +115 -0
  138. package/src/panels/modals/sandbox-modal.ts +150 -0
  139. package/src/panels/modals/security-modal.ts +217 -0
  140. package/src/panels/modals/services-modal.ts +179 -0
  141. package/src/panels/modals/settings-sync-modal.ts +142 -0
  142. package/src/panels/modals/skills-modal.ts +189 -0
  143. package/src/panels/modals/subscription-modal.ts +172 -0
  144. package/src/panels/modals/work-plan-modal.ts +177 -0
  145. package/src/panels/panel-confirm-overlay.ts +72 -0
  146. package/src/panels/panel-manager.ts +123 -9
  147. package/src/panels/polish-core.ts +38 -25
  148. package/src/panels/project-planning-answer-actions.ts +8 -11
  149. package/src/panels/skills-panel.ts +7 -51
  150. package/src/panels/types.ts +37 -0
  151. package/src/permissions/hunk-selection.ts +179 -0
  152. package/src/permissions/prompt.ts +216 -13
  153. package/src/renderer/autocomplete-overlay.ts +28 -2
  154. package/src/renderer/compaction-history-modal.ts +19 -3
  155. package/src/renderer/compaction-preview.ts +13 -2
  156. package/src/renderer/compaction-quality.ts +100 -0
  157. package/src/renderer/compositor.ts +2 -3
  158. package/src/renderer/config-modal.ts +95 -0
  159. package/src/renderer/conversation-overlays.ts +10 -17
  160. package/src/renderer/fleet-tab-strip.ts +56 -0
  161. package/src/renderer/footer-tips.ts +15 -3
  162. package/src/renderer/fullscreen-primitives.ts +32 -22
  163. package/src/renderer/git-status.ts +43 -1
  164. package/src/renderer/help-overlay.ts +2 -2
  165. package/src/renderer/layout.ts +0 -4
  166. package/src/renderer/markdown.ts +7 -3
  167. package/src/renderer/modal-factory.ts +25 -20
  168. package/src/renderer/model-workspace.ts +62 -4
  169. package/src/renderer/overlay-box.ts +21 -17
  170. package/src/renderer/panel-workspace-bar.ts +8 -2
  171. package/src/renderer/process-indicator.ts +14 -3
  172. package/src/renderer/selection-modal-overlay.ts +6 -1
  173. package/src/renderer/session-picker-modal.ts +196 -3
  174. package/src/renderer/settings-modal-helpers.ts +2 -0
  175. package/src/renderer/settings-modal.ts +7 -0
  176. package/src/renderer/shell-surface.ts +8 -1
  177. package/src/renderer/status-glyphs.ts +14 -15
  178. package/src/renderer/system-message.ts +15 -3
  179. package/src/renderer/terminal-bg-probe.ts +339 -0
  180. package/src/renderer/terminal-escapes.ts +20 -0
  181. package/src/renderer/theme-mode-config.ts +67 -0
  182. package/src/renderer/theme.ts +91 -1
  183. package/src/renderer/thinking.ts +11 -3
  184. package/src/renderer/tool-call.ts +15 -9
  185. package/src/renderer/tool-result-summary.ts +148 -0
  186. package/src/renderer/turn-injection.ts +133 -0
  187. package/src/renderer/ui-factory.ts +154 -85
  188. package/src/renderer/ui-primitives.ts +30 -129
  189. package/src/runtime/bootstrap-command-context.ts +27 -1
  190. package/src/runtime/bootstrap-command-parts.ts +41 -10
  191. package/src/runtime/bootstrap-core.ts +47 -15
  192. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  193. package/src/runtime/bootstrap-shell.ts +48 -17
  194. package/src/runtime/bootstrap.ts +129 -22
  195. package/src/runtime/code-index-services.ts +135 -0
  196. package/src/runtime/legacy-daemon-migration.ts +516 -0
  197. package/src/runtime/memory-fold.ts +26 -0
  198. package/src/runtime/onboarding/derivation.ts +7 -2
  199. package/src/runtime/onboarding/snapshot.ts +27 -1
  200. package/src/runtime/onboarding/types.ts +31 -1
  201. package/src/runtime/operator-token-cleanup.ts +82 -1
  202. package/src/runtime/orchestrator-core-services.ts +59 -0
  203. package/src/runtime/process-lifecycle.ts +3 -1
  204. package/src/runtime/resume-notice.ts +209 -0
  205. package/src/runtime/services.ts +98 -46
  206. package/src/runtime/session-inbound-inputs.ts +252 -0
  207. package/src/runtime/session-spine-transport.ts +64 -0
  208. package/src/runtime/terminal-output-guard.ts +15 -8
  209. package/src/runtime/ui-services.ts +27 -3
  210. package/src/runtime/workstream-services.ts +327 -0
  211. package/src/runtime/wrfc-persistence.ts +124 -17
  212. package/src/shell/blocking-input.ts +68 -4
  213. package/src/shell/recovery-input-helpers.ts +170 -1
  214. package/src/shell/ui-openers.ts +171 -26
  215. package/src/utils/format-duration.ts +8 -18
  216. package/src/utils/splash-lines.ts +1 -1
  217. package/src/utils/terminal-width.ts +52 -0
  218. package/src/version.ts +1 -1
  219. package/src/panels/agent-inspector-panel.ts +0 -786
  220. package/src/panels/approval-panel.ts +0 -252
  221. package/src/panels/automation-control-panel.ts +0 -479
  222. package/src/panels/cockpit-panel.ts +0 -481
  223. package/src/panels/cockpit-read-model.ts +0 -238
  224. package/src/panels/communication-panel.ts +0 -256
  225. package/src/panels/control-plane-panel.ts +0 -470
  226. package/src/panels/debug-panel.ts +0 -615
  227. package/src/panels/docs-panel.ts +0 -384
  228. package/src/panels/eval-panel.ts +0 -627
  229. package/src/panels/file-explorer-panel.ts +0 -673
  230. package/src/panels/file-preview-panel.ts +0 -517
  231. package/src/panels/hooks-panel.ts +0 -401
  232. package/src/panels/incident-review-panel.ts +0 -406
  233. package/src/panels/intelligence-panel.ts +0 -383
  234. package/src/panels/knowledge-graph-panel.ts +0 -515
  235. package/src/panels/marketplace-panel.ts +0 -399
  236. package/src/panels/memory-panel.ts +0 -558
  237. package/src/panels/ops-control-panel.ts +0 -329
  238. package/src/panels/ops-strategy-panel.ts +0 -321
  239. package/src/panels/orchestration-panel.ts +0 -465
  240. package/src/panels/panel-list-panel.ts +0 -566
  241. package/src/panels/plan-dashboard-panel.ts +0 -707
  242. package/src/panels/policy-panel.ts +0 -517
  243. package/src/panels/project-planning-panel.ts +0 -731
  244. package/src/panels/provider-health-panel.ts +0 -678
  245. package/src/panels/provider-health-tracker.ts +0 -310
  246. package/src/panels/provider-health-views.ts +0 -567
  247. package/src/panels/qr-panel.ts +0 -280
  248. package/src/panels/remote-panel.ts +0 -534
  249. package/src/panels/routes-panel.ts +0 -241
  250. package/src/panels/sandbox-panel.ts +0 -456
  251. package/src/panels/security-panel.ts +0 -447
  252. package/src/panels/services-panel.ts +0 -329
  253. package/src/panels/session-browser-panel.ts +0 -496
  254. package/src/panels/settings-sync-panel.ts +0 -398
  255. package/src/panels/subscription-panel.ts +0 -342
  256. package/src/panels/symbol-outline-panel.ts +0 -619
  257. package/src/panels/system-messages-panel.ts +0 -364
  258. package/src/panels/tasks-panel.ts +0 -608
  259. package/src/panels/thinking-panel.ts +0 -333
  260. package/src/panels/tool-inspector-panel.ts +0 -537
  261. package/src/panels/work-plan-panel.ts +0 -540
  262. package/src/panels/worktree-panel.ts +0 -360
  263. package/src/panels/wrfc-panel.ts +0 -790
  264. package/src/renderer/agent-detail-modal.ts +0 -466
  265. package/src/renderer/live-tail-modal.ts +0 -156
  266. package/src/renderer/process-modal.ts +0 -671
  267. 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
- }