@pellux/goodvibes-tui 0.28.0 → 1.0.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 (144) hide show
  1. package/CHANGELOG.md +133 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/conversation-line-cache.ts +432 -0
  5. package/src/core/conversation-rendering.ts +11 -3
  6. package/src/core/conversation.ts +50 -4
  7. package/src/input/commands/cost-runtime.ts +49 -0
  8. package/src/input/commands/operator-runtime.ts +5 -1
  9. package/src/input/commands.ts +2 -0
  10. package/src/input/handler-content-actions.ts +8 -3
  11. package/src/input/handler-feed-routes.ts +12 -35
  12. package/src/input/handler-feed.ts +4 -8
  13. package/src/input/handler-shortcuts.ts +51 -0
  14. package/src/input/handler.ts +43 -6
  15. package/src/input/input-history.ts +17 -4
  16. package/src/input/keybindings.ts +48 -8
  17. package/src/input/panel-integration-actions.ts +107 -1
  18. package/src/main.ts +19 -11
  19. package/src/panels/agent-inspector-panel.ts +125 -36
  20. package/src/panels/agent-inspector-shared.ts +144 -0
  21. package/src/panels/approval-panel.ts +67 -16
  22. package/src/panels/automation-control-panel.ts +368 -124
  23. package/src/panels/base-panel.ts +1 -1
  24. package/src/panels/builtin/agent.ts +28 -37
  25. package/src/panels/builtin/development.ts +40 -32
  26. package/src/panels/builtin/knowledge.ts +15 -6
  27. package/src/panels/builtin/operations.ts +178 -117
  28. package/src/panels/builtin/session.ts +35 -9
  29. package/src/panels/builtin/shared.ts +98 -6
  30. package/src/panels/cockpit-panel.ts +232 -73
  31. package/src/panels/communication-panel.ts +58 -20
  32. package/src/panels/confirm-state.ts +8 -1
  33. package/src/panels/control-plane-panel.ts +351 -118
  34. package/src/panels/cost-tracker-panel.ts +165 -7
  35. package/src/panels/debug-panel.ts +312 -159
  36. package/src/panels/diff-panel.ts +209 -57
  37. package/src/panels/docs-panel.ts +152 -66
  38. package/src/panels/eval-panel.ts +220 -42
  39. package/src/panels/file-explorer-panel.ts +202 -122
  40. package/src/panels/file-preview-panel.ts +132 -57
  41. package/src/panels/git-panel.ts +310 -129
  42. package/src/panels/hooks-panel.ts +150 -22
  43. package/src/panels/incident-review-panel.ts +223 -35
  44. package/src/panels/index.ts +0 -4
  45. package/src/panels/intelligence-panel.ts +212 -86
  46. package/src/panels/knowledge-graph-panel.ts +461 -101
  47. package/src/panels/local-auth-panel.ts +240 -28
  48. package/src/panels/marketplace-panel.ts +193 -26
  49. package/src/panels/memory-panel.ts +78 -77
  50. package/src/panels/ops-control-panel.ts +146 -29
  51. package/src/panels/ops-strategy-panel.ts +72 -4
  52. package/src/panels/orchestration-panel.ts +231 -69
  53. package/src/panels/panel-list-panel.ts +154 -131
  54. package/src/panels/panel-manager.ts +97 -9
  55. package/src/panels/plan-dashboard-panel.ts +333 -17
  56. package/src/panels/plugins-panel.ts +184 -29
  57. package/src/panels/policy-panel.ts +210 -38
  58. package/src/panels/polish-core.ts +7 -2
  59. package/src/panels/polish.ts +23 -4
  60. package/src/panels/project-planning-answer-actions.ts +134 -0
  61. package/src/panels/project-planning-panel.ts +62 -113
  62. package/src/panels/provider-health-panel.ts +434 -518
  63. package/src/panels/provider-health-routes.ts +203 -0
  64. package/src/panels/provider-health-tracker.ts +194 -6
  65. package/src/panels/provider-health-views.ts +560 -0
  66. package/src/panels/qr-panel.ts +116 -30
  67. package/src/panels/remote-panel.ts +114 -36
  68. package/src/panels/routes-panel.ts +63 -22
  69. package/src/panels/sandbox-panel.ts +174 -42
  70. package/src/panels/scrollable-list-panel.ts +19 -135
  71. package/src/panels/security-panel.ts +133 -33
  72. package/src/panels/services-panel.ts +116 -64
  73. package/src/panels/session-browser-panel.ts +73 -2
  74. package/src/panels/session-maintenance.ts +4 -122
  75. package/src/panels/settings-sync-panel.ts +335 -72
  76. package/src/panels/skills-panel.ts +157 -89
  77. package/src/panels/subscription-panel.ts +86 -33
  78. package/src/panels/symbol-outline-panel.ts +248 -108
  79. package/src/panels/system-messages-panel.ts +114 -13
  80. package/src/panels/tasks-panel.ts +326 -139
  81. package/src/panels/thinking-panel.ts +43 -10
  82. package/src/panels/token-budget-panel.ts +194 -18
  83. package/src/panels/tool-inspector-panel.ts +144 -34
  84. package/src/panels/types.ts +40 -4
  85. package/src/panels/work-plan-panel.ts +280 -17
  86. package/src/panels/worktree-panel.ts +175 -42
  87. package/src/panels/wrfc-panel.ts +116 -24
  88. package/src/renderer/agent-detail-modal.ts +37 -8
  89. package/src/renderer/code-block.ts +58 -28
  90. package/src/renderer/context-inspector.ts +3 -6
  91. package/src/renderer/conversation-overlays.ts +25 -11
  92. package/src/renderer/conversation-surface.ts +1 -21
  93. package/src/renderer/diff-view.ts +6 -4
  94. package/src/renderer/footer-tips.ts +41 -0
  95. package/src/renderer/fullscreen-primitives.ts +23 -17
  96. package/src/renderer/fullscreen-workspace.ts +2 -7
  97. package/src/renderer/help-overlay.ts +91 -14
  98. package/src/renderer/hint-grammar.ts +52 -0
  99. package/src/renderer/history-search-overlay.ts +10 -16
  100. package/src/renderer/layout.ts +7 -7
  101. package/src/renderer/markdown.ts +9 -1
  102. package/src/renderer/modal-factory.ts +23 -15
  103. package/src/renderer/model-picker-overlay.ts +8 -485
  104. package/src/renderer/model-workspace.ts +9 -24
  105. package/src/renderer/overlay-box.ts +23 -19
  106. package/src/renderer/process-indicator.ts +15 -25
  107. package/src/renderer/profile-picker-modal.ts +13 -14
  108. package/src/renderer/prompt-content-width.ts +16 -0
  109. package/src/renderer/search-overlay.ts +27 -6
  110. package/src/renderer/selection-modal-overlay.ts +3 -1
  111. package/src/renderer/semantic-diff.ts +1 -1
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal-helpers.ts +10 -34
  114. package/src/renderer/settings-modal.ts +70 -10
  115. package/src/renderer/shell-surface.ts +54 -15
  116. package/src/renderer/status-glyphs.ts +11 -9
  117. package/src/renderer/surface-layout.ts +0 -12
  118. package/src/renderer/term-caps.ts +1 -1
  119. package/src/renderer/theme.ts +60 -3
  120. package/src/renderer/tool-call.ts +6 -20
  121. package/src/renderer/ui-factory.ts +48 -37
  122. package/src/renderer/ui-primitives.ts +41 -3
  123. package/src/runtime/bootstrap-shell.ts +35 -18
  124. package/src/runtime/diagnostics/panels/index.ts +0 -3
  125. package/src/runtime/render-scheduler.ts +80 -0
  126. package/src/shell/ui-openers.ts +14 -22
  127. package/src/utils/format-duration.ts +2 -2
  128. package/src/utils/splash-lines.ts +54 -5
  129. package/src/version.ts +1 -1
  130. package/src/work-plans/work-plan-store.ts +13 -0
  131. package/src/panels/agent-logs-panel.ts +0 -635
  132. package/src/panels/agent-logs-shared.ts +0 -129
  133. package/src/panels/context-visualizer-panel.ts +0 -238
  134. package/src/panels/forensics-panel.ts +0 -378
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -255
  137. package/src/panels/provider-stats-panel.ts +0 -391
  138. package/src/panels/schedule-panel.ts +0 -365
  139. package/src/panels/watchers-panel.ts +0 -213
  140. package/src/renderer/file-tree.ts +0 -153
  141. package/src/renderer/progress.ts +0 -99
  142. package/src/renderer/status-token.ts +0 -67
  143. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  144. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -1,365 +0,0 @@
1
- import { BasePanel } from './base-panel.ts';
2
- import { type Line } from '../types/grid.ts';
3
- import { fitDisplay, truncateDisplay } from '../utils/terminal-width.ts';
4
- import type { AutomationManager } from '@pellux/goodvibes-sdk/platform/automation';
5
- import type { AutomationJob } from '@pellux/goodvibes-sdk/platform/automation';
6
- import type { AutomationRun } from '@pellux/goodvibes-sdk/platform/automation';
7
- import type { AutomationScheduleDefinition } from '@pellux/goodvibes-sdk/platform/automation';
8
- import {
9
- buildEmptyState,
10
- buildKeyboardHints,
11
- buildPanelLine,
12
- buildPanelWorkspace,
13
- extendPalette,
14
- resolveScrollablePanelSection,
15
- DEFAULT_PANEL_PALETTE,
16
- type PanelWorkspaceSection,
17
- } from './polish.ts';
18
-
19
- // ---------------------------------------------------------------------------
20
- // Colors
21
- // ---------------------------------------------------------------------------
22
-
23
- const C = extendPalette(DEFAULT_PANEL_PALETTE, {
24
- enabled: '#5fd700',
25
- disabled: '#6c6c6c',
26
- selectedFg: '#ffffff',
27
- id: '238',
28
- cron: '#af87ff',
29
- prompt: '250',
30
- nextRun: '#87afff',
31
- lastRun: '244',
32
- runCount: '#ffaf00',
33
- statusRunning: '#5fd700',
34
- statusFailed: '#ff5f5f',
35
- });
36
-
37
- // ---------------------------------------------------------------------------
38
- // View items
39
- // ---------------------------------------------------------------------------
40
-
41
- type ViewItem =
42
- | { kind: 'header' }
43
- | { kind: 'task'; task: AutomationJob; history: AutomationRun[] }
44
- | { kind: 'empty' };
45
-
46
- type ScheduleAutomationManager = Pick<
47
- AutomationManager,
48
- 'start' | 'listJobs' | 'listRuns' | 'setEnabled' | 'runNow'
49
- >;
50
-
51
- function formatSchedule(schedule: AutomationScheduleDefinition): string {
52
- switch (schedule.kind) {
53
- case 'cron':
54
- return schedule.timezone ? `${schedule.expression} [${schedule.timezone}]` : schedule.expression;
55
- case 'every':
56
- return formatEveryInterval(schedule.intervalMs);
57
- case 'at':
58
- return new Date(schedule.at).toLocaleString();
59
- }
60
- }
61
-
62
- function formatEveryInterval(intervalMs: number): string {
63
- const units: ReadonlyArray<readonly [number, string]> = [
64
- [86_400_000, 'd'],
65
- [3_600_000, 'h'],
66
- [60_000, 'm'],
67
- [1_000, 's'],
68
- ];
69
- for (const [size, unit] of units) {
70
- if (intervalMs >= size && intervalMs % size === 0) {
71
- return `${intervalMs / size}${unit}`;
72
- }
73
- }
74
- return `${intervalMs}ms`;
75
- }
76
-
77
- // ---------------------------------------------------------------------------
78
- // SchedulePanel
79
- // ---------------------------------------------------------------------------
80
-
81
- /**
82
- * SchedulePanel — displays all scheduled tasks with next run time,
83
- * enable/disable toggle, and run history.
84
- */
85
- export class SchedulePanel extends BasePanel {
86
- private items: ViewItem[] = [];
87
- private selectedIndex = 0;
88
- private scrollOffset = 0;
89
- private refreshTimerId: ReturnType<typeof setInterval> | null = null;
90
- private readonly automationManager: ScheduleAutomationManager;
91
-
92
- constructor(automationManager: ScheduleAutomationManager, private readonly requestRender: () => void = () => {}) {
93
- super('schedule', 'Schedule', 'Z', 'agent');
94
- this.automationManager = automationManager;
95
- }
96
-
97
- private _markDirtyAndRender(): void {
98
- this.markDirty();
99
- this.requestRender();
100
- }
101
-
102
- // -------------------------------------------------------------------------
103
- // Lifecycle
104
- // -------------------------------------------------------------------------
105
-
106
- override onActivate(): void {
107
- super.onActivate();
108
- void this.automationManager.start().then(() => {
109
- this.rebuild();
110
- this._markDirtyAndRender();
111
- });
112
- this.rebuild();
113
- this.refreshTimerId = this.registerTimer(setInterval(() => {
114
- this.rebuild();
115
- this._markDirtyAndRender();
116
- }, 5_000));
117
- }
118
-
119
- override onDeactivate(): void {
120
- if (this.refreshTimerId !== null) {
121
- this.clearTimer(this.refreshTimerId);
122
- this.refreshTimerId = null;
123
- }
124
- }
125
-
126
- override onDestroy(): void {
127
- this.onDeactivate();
128
- super.onDestroy();
129
- }
130
-
131
- // -------------------------------------------------------------------------
132
- // Data
133
- // -------------------------------------------------------------------------
134
-
135
- private rebuild(): void {
136
- const manager = this.automationManager;
137
- const tasks = manager.listJobs();
138
-
139
- const items: ViewItem[] = [{ kind: 'header' }];
140
- if (tasks.length === 0) {
141
- items.push({ kind: 'empty' });
142
- } else {
143
- for (const task of tasks) {
144
- items.push({
145
- kind: 'task',
146
- task,
147
- history: manager.listRuns(task.id),
148
- });
149
- }
150
- }
151
- this.items = items;
152
-
153
- if (this.selectedIndex >= this.items.length) {
154
- this.selectedIndex = Math.max(0, this.items.length - 1);
155
- }
156
- }
157
-
158
- // -------------------------------------------------------------------------
159
- // Input
160
- // -------------------------------------------------------------------------
161
-
162
- handleInput(key: string): boolean {
163
- switch (key) {
164
- case 'up':
165
- case 'k': {
166
- if (this.selectedIndex > 0) {
167
- this.selectedIndex--;
168
- this.markDirty();
169
- }
170
- return true;
171
- }
172
- case 'down':
173
- case 'j': {
174
- if (this.selectedIndex < this.items.length - 1) {
175
- this.selectedIndex++;
176
- this.markDirty();
177
- }
178
- return true;
179
- }
180
- case 'return':
181
- case ' ': {
182
- // Toggle enabled/disabled on selected task
183
- const item = this.items[this.selectedIndex];
184
- if (item?.kind === 'task') {
185
- void this.automationManager.setEnabled(item.task.id, !item.task.enabled).then(() => {
186
- this.rebuild();
187
- this.markDirty();
188
- });
189
- }
190
- return true;
191
- }
192
- case 'r': {
193
- // Trigger immediate run of selected task
194
- const item = this.items[this.selectedIndex];
195
- if (item?.kind === 'task') {
196
- this.automationManager.runNow(item.task.id).catch(() => {
197
- // Non-fatal — error logged by scheduler
198
- });
199
- this.rebuild();
200
- this.markDirty();
201
- }
202
- return true;
203
- }
204
- case 'R': {
205
- // Refresh the view
206
- this.rebuild();
207
- this.markDirty();
208
- return true;
209
- }
210
- default:
211
- return false;
212
- }
213
- }
214
-
215
- // -------------------------------------------------------------------------
216
- // Rendering
217
- // -------------------------------------------------------------------------
218
-
219
- override render(width: number, height: number): Line[] {
220
- const tasks = this.automationManager.listJobs();
221
- const enabled = tasks.filter((task: AutomationJob) => task.enabled).length;
222
- if (tasks.length === 0) {
223
- return buildPanelWorkspace(width, height, {
224
- title: ' Schedule',
225
- intro: 'Review recurring scheduled tasks, next run timing, recent history, and enablement state.',
226
- sections: [
227
- {
228
- lines: buildEmptyState(
229
- width,
230
- ' No scheduled tasks',
231
- 'Create a recurring task and its next-run timing, run history, and enablement state will appear here.',
232
- [
233
- { command: '/schedule add cron 0 * * * * repo sweep', summary: 'create a recurring cron task' },
234
- { command: '/schedule list', summary: 'inspect scheduled tasks from the shell' },
235
- ],
236
- DEFAULT_PANEL_PALETTE,
237
- ),
238
- },
239
- ],
240
- footerLines: [
241
- buildKeyboardHints(width, [
242
- { keys: '/schedule add', label: 'create a task' },
243
- ], DEFAULT_PANEL_PALETTE),
244
- ],
245
- palette: DEFAULT_PANEL_PALETTE,
246
- });
247
- }
248
-
249
- const taskItems = this.items.filter((item): item is Extract<ViewItem, { kind: 'task' }> => item.kind === 'task');
250
- this.selectedIndex = Math.max(0, Math.min(this.selectedIndex, taskItems.length - 1));
251
- const dueSoon = tasks.filter((task: AutomationJob) => typeof task.nextRunAt === 'number').length;
252
- const summarySection: PanelWorkspaceSection = {
253
- title: 'Summary',
254
- lines: [
255
- buildPanelLine(width, [
256
- [' Tasks ', DEFAULT_PANEL_PALETTE.label],
257
- [String(tasks.length), DEFAULT_PANEL_PALETTE.value],
258
- [' Enabled ', DEFAULT_PANEL_PALETTE.label],
259
- [String(enabled), enabled > 0 ? DEFAULT_PANEL_PALETTE.good : DEFAULT_PANEL_PALETTE.dim],
260
- [' Paused ', DEFAULT_PANEL_PALETTE.label],
261
- [String(tasks.length - enabled), tasks.length - enabled > 0 ? DEFAULT_PANEL_PALETTE.warn : DEFAULT_PANEL_PALETTE.dim],
262
- [' Scheduled ', DEFAULT_PANEL_PALETTE.label],
263
- [String(dueSoon), dueSoon > 0 ? DEFAULT_PANEL_PALETTE.info : DEFAULT_PANEL_PALETTE.dim],
264
- ]),
265
- ],
266
- };
267
- const selectedTask = taskItems[this.selectedIndex]?.task;
268
- const toggleLabel = selectedTask
269
- ? (selectedTask.enabled ? 'pause task' : 'enable task')
270
- : 'toggle';
271
- const footerLines = [
272
- buildKeyboardHints(width, [
273
- { keys: 'Up/Down', label: 'navigate' },
274
- { keys: 'Space', label: toggleLabel },
275
- { keys: 'r', label: 'run now' },
276
- { keys: 'R', label: 'refresh' },
277
- ], DEFAULT_PANEL_PALETTE),
278
- ];
279
- const scheduledTasksSection = resolveScrollablePanelSection(width, height, {
280
- intro: 'Review recurring scheduled tasks, next run timing, recent history, and enablement state.',
281
- footerLines,
282
- palette: DEFAULT_PANEL_PALETTE,
283
- beforeSections: [summarySection],
284
- section: {
285
- title: 'Scheduled Tasks',
286
- scrollableLines: taskItems.flatMap((item, index) => this.renderTask(item.task, item.history, index === this.selectedIndex, width)),
287
- selectedIndex: this.selectedIndex * 3,
288
- scrollOffset: this.scrollOffset,
289
- minRows: 6,
290
- },
291
- });
292
- this.scrollOffset = scheduledTasksSection.scrollOffset;
293
- const sections: PanelWorkspaceSection[] = [
294
- summarySection,
295
- scheduledTasksSection.section,
296
- ];
297
-
298
- return buildPanelWorkspace(width, height, {
299
- title: ' Schedule',
300
- intro: 'Review recurring scheduled tasks, next run timing, recent history, and enablement state.',
301
- sections,
302
- footerLines,
303
- palette: DEFAULT_PANEL_PALETTE,
304
- });
305
- }
306
-
307
- /**
308
- * Render a task as 3 rows:
309
- * Row 1: [status] id name schedule
310
- * Row 2: next: <date> last: <date> runs: N
311
- * Row 3: prompt preview [history]
312
- */
313
- private renderTask(task: AutomationJob, history: AutomationRun[], selected: boolean, width: number): Line[] {
314
- const bg = selected ? C.selectBg : undefined;
315
- const fgBase = selected ? C.selectedFg : undefined;
316
-
317
- const bullet = task.enabled ? '* ' : 'o ';
318
- const bulletFg = task.enabled ? C.enabled : C.disabled;
319
- const nameStr = fitDisplay(task.name, 28);
320
- const scheduleText = formatSchedule(task.schedule);
321
- const row1 = buildPanelLine(width, [
322
- [bullet, bulletFg, bg],
323
- [fitDisplay(task.id, 12), fgBase ?? C.id, bg],
324
- [' ', fgBase ?? C.prompt, bg],
325
- [nameStr, fgBase ?? C.prompt, bg],
326
- [' ', fgBase ?? C.prompt, bg],
327
- [scheduleText, fgBase ?? C.cron, bg],
328
- ]);
329
-
330
- const indent = ' ';
331
- const nextStr = task.nextRunAt
332
- ? `next: ${new Date(task.nextRunAt).toLocaleString()}`
333
- : 'next: unknown';
334
- const lastStr = task.lastRunAt
335
- ? `last: ${new Date(task.lastRunAt).toLocaleString()}`
336
- : 'last: never';
337
- const row2 = buildPanelLine(width, [
338
- [indent, fgBase ?? C.prompt, bg],
339
- [nextStr.padEnd(36), fgBase ?? C.nextRun, bg],
340
- [lastStr.padEnd(32), fgBase ?? C.lastRun, bg],
341
- [`runs: ${task.runCount}`, fgBase ?? C.runCount, bg],
342
- ]);
343
-
344
- const maxPromptLen = Math.max(20, width - indent.length - 30);
345
- const prompt = task.execution.prompt ?? task.description ?? '';
346
- const promptPreview = truncateDisplay(prompt, maxPromptLen);
347
-
348
- // Show last 3 run statuses as colored dots
349
- const recentRuns = history.slice(-3);
350
- const runSegments = recentRuns.flatMap((run) => {
351
- const dotFg = run.status === 'failed' ? C.statusFailed : C.statusRunning;
352
- return [['\u25cf', dotFg, bg] as [string, string, string?]];
353
- });
354
- const row3 = buildPanelLine(width, [
355
- [indent, fgBase ?? C.prompt, bg],
356
- [promptPreview, fgBase ?? C.prompt, bg],
357
- ...(runSegments.length > 0 ? [[' ', fgBase ?? C.prompt, bg] as [string, string, string?], ...runSegments] : []),
358
- ]);
359
-
360
- // spacer row between tasks
361
- const spacer = buildPanelLine(width, [['', fgBase ?? C.prompt, bg]]);
362
-
363
- return [row1, row2, row3, spacer];
364
- }
365
- }
@@ -1,213 +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 { UiReadModel, UiWatchersSnapshot } from '../runtime/ui-read-models.ts';
5
- import { truncateDisplay } from '../utils/terminal-width.ts';
6
- import {
7
- buildDetailBlock,
8
- buildEmptyState,
9
- buildGuidanceLine,
10
- buildKeyValueLine,
11
- buildKeyboardHints,
12
- buildPanelLine,
13
- buildPanelWorkspace,
14
- DEFAULT_PANEL_PALETTE,
15
- type PanelPalette,
16
- } from './polish.ts';
17
-
18
- const C = {
19
- ...DEFAULT_PANEL_PALETTE,
20
- header: '#94a3b8',
21
- headerBg: '#1e293b',
22
- ok: '#22c55e',
23
- warn: '#eab308',
24
- error: '#ef4444',
25
- info: '#38bdf8',
26
- selectBg: '#0f172a',
27
- } as const;
28
-
29
- function stateColor(state: string): string {
30
- if (state === 'running') return C.ok;
31
- if (state === 'degraded') return C.warn;
32
- if (state === 'failed') return C.error;
33
- return C.dim;
34
- }
35
-
36
- function sourceStatusColor(state?: string): string {
37
- if (state === 'healthy') return C.ok;
38
- if (state === 'lagging' || state === 'stale' || state === 'degraded') return C.warn;
39
- if (state === 'failed') return C.error;
40
- return C.dim;
41
- }
42
-
43
- function formatLag(value?: number): string {
44
- if (!value || value <= 0) return 'n/a';
45
- if (value < 1000) return `${value}ms`;
46
- if (value < 60_000) return `${Math.round(value / 1000)}s`;
47
- return `${Math.round(value / 60_000)}m`;
48
- }
49
-
50
- function formatTime(value?: number): string {
51
- if (!value) return 'n/a';
52
- return new Date(value).toLocaleString();
53
- }
54
-
55
- type WatcherEntry = UiWatchersSnapshot['watchers'][number];
56
-
57
- export class WatchersPanel extends ScrollableListPanel<WatcherEntry> {
58
- private readonly readModel?: UiReadModel<UiWatchersSnapshot>;
59
- private readonly unsub: (() => void) | null;
60
-
61
- public constructor(readModel?: UiReadModel<UiWatchersSnapshot>) {
62
- super('watchers', 'Watchers', 'W', 'monitoring');
63
- this.showSelectionGutter = true; // I5: non-color selection affordance
64
- this.filterEnabled = true;
65
- this.filterLabel = 'Filter watchers';
66
- this.readModel = readModel;
67
- this.unsub = readModel ? readModel.subscribe(() => this.markDirty()) : null;
68
- }
69
-
70
- protected override filterMatches(watcher: WatcherEntry, q: string): boolean {
71
- return watcher.label.toLowerCase().includes(q)
72
- || watcher.state.toLowerCase().includes(q)
73
- || String(watcher.sourceStatus ?? '').toLowerCase().includes(q);
74
- }
75
-
76
- public override onDestroy(): void {
77
- this.unsub?.();
78
- }
79
-
80
- protected override getPalette(): PanelPalette {
81
- return C;
82
- }
83
-
84
- protected getItems(): readonly WatcherEntry[] {
85
- if (!this.readModel) return [];
86
- return this.readModel.getSnapshot().watchers;
87
- }
88
-
89
- protected renderItem(watcher: WatcherEntry, _index: number, selected: boolean, width: number): Line {
90
- const bg = selected ? C.selectBg : undefined;
91
- return buildPanelLine(width, [
92
- [' ', C.label, bg],
93
- [watcher.state.padEnd(10), stateColor(watcher.state), bg],
94
- [` ${truncateDisplay(watcher.label, 18).padEnd(18)}`, C.value, bg],
95
- [` ${String(watcher.sourceStatus ?? 'unknown').padEnd(10)}`, sourceStatusColor(watcher.sourceStatus), bg],
96
- [` ${truncateDisplay(formatLag(watcher.sourceLagMs), Math.max(0, width - 43))}`, C.dim, bg],
97
- ]);
98
- }
99
-
100
- protected override getEmptyStateMessage(): string {
101
- return ' No watchers registered.';
102
- }
103
-
104
- protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
105
- return [
106
- { command: '/schedule list', summary: 'review automation that will consume watcher events' },
107
- { command: '/services auth-review', summary: 'validate integration credentials before enabling remote watchers' },
108
- ];
109
- }
110
-
111
- public render(width: number, height: number): Line[] {
112
- const intro = 'Managed watchers and source health used to trigger automation, refresh routes, and surface degraded upstream conditions.';
113
-
114
- if (!this.readModel) {
115
- const workspace = buildPanelWorkspace(width, height, {
116
- title: 'Watchers',
117
- intro,
118
- sections: [{
119
- lines: buildEmptyState(
120
- width,
121
- ' Runtime store not wired.',
122
- 'This panel needs the shared runtime store to inspect watcher health and source lag.',
123
- [{ command: '/services auth-review', summary: 'inspect supporting services until watcher wiring is available' }],
124
- C,
125
- ),
126
- }],
127
- palette: C,
128
- });
129
- while (workspace.length < height) workspace.push(createEmptyLine(width));
130
- return workspace;
131
- }
132
-
133
- const snapshot = this.readModel.getSnapshot();
134
- const watchers = this.getItems();
135
-
136
- const headerLines: Line[] = [
137
- buildKeyValueLine(width, [
138
- { label: 'watchers', value: String(snapshot.totalWatchers), valueColor: snapshot.totalWatchers > 0 ? C.info : C.dim },
139
- { label: 'active', value: String(snapshot.activeWatcherIds.length), valueColor: snapshot.activeWatcherIds.length > 0 ? C.ok : C.dim },
140
- { label: 'degraded', value: String(snapshot.totalDegraded), valueColor: snapshot.totalDegraded > 0 ? C.warn : C.dim },
141
- { label: 'lagged', value: String(snapshot.totalLagged), valueColor: snapshot.totalLagged > 0 ? C.warn : C.dim },
142
- ], C),
143
- buildGuidanceLine(width, '/schedule list', 'verify jobs consuming these sources and use daemon APIs for watcher lifecycle control', C),
144
- ];
145
-
146
- if (watchers.length === 0) {
147
- return this.renderList(width, height, {
148
- title: 'Watchers',
149
- header: headerLines,
150
- emptyMessage: ' No watchers registered.',
151
- });
152
- }
153
-
154
- this.clampSelection();
155
- const selected = watchers[this.selectedIndex]!;
156
-
157
- const detailRows: Line[] = [
158
- buildPanelLine(width, [
159
- [' Watcher: ', C.label],
160
- [selected.label, C.value],
161
- [' Kind: ', C.label],
162
- [selected.kind, C.info],
163
- ]),
164
- buildPanelLine(width, [
165
- [' State: ', C.label],
166
- [selected.state, stateColor(selected.state)],
167
- [' Source: ', C.label],
168
- [selected.source.kind, C.value],
169
- ]),
170
- buildPanelLine(width, [
171
- [' Source status: ', C.label],
172
- [selected.sourceStatus ?? 'unknown', sourceStatusColor(selected.sourceStatus)],
173
- [' Lag: ', C.label],
174
- [formatLag(selected.sourceLagMs), selected.sourceLagMs ? C.warn : C.dim],
175
- ]),
176
- buildPanelLine(width, [
177
- [' Heartbeat: ', C.label],
178
- [formatTime(selected.lastHeartbeatAt), C.dim],
179
- [' Checkpoint: ', C.label],
180
- [truncateDisplay(selected.lastCheckpoint ?? 'n/a', Math.max(0, width - 38)), C.dim],
181
- ]),
182
- ];
183
- if (selected.degradedReason) {
184
- detailRows.push(buildPanelLine(width, [
185
- [' Reason: ', C.label],
186
- [truncateDisplay(selected.degradedReason, Math.max(0, width - 11)), C.warn],
187
- ]));
188
- }
189
- if (selected.lastError) {
190
- detailRows.push(buildPanelLine(width, [
191
- [' Error: ', C.label],
192
- [truncateDisplay(selected.lastError, Math.max(0, width - 10)), C.error],
193
- ]));
194
- }
195
-
196
- const hints = this.filterActive
197
- ? [{ keys: 'type', label: 'filter' }, { keys: 'Enter', label: 'apply' }, { keys: 'Esc', label: 'clear' }]
198
- : [
199
- { keys: 'Up/Down', label: 'move' },
200
- { keys: '/schedule list', label: 'consumers' },
201
- { keys: '/', label: 'filter' },
202
- ];
203
-
204
- return this.renderList(width, height, {
205
- title: 'Watchers',
206
- header: headerLines,
207
- footer: [
208
- ...buildDetailBlock(width, `Watcher · ${selected.label}`, detailRows, C),
209
- buildKeyboardHints(width, hints, C),
210
- ],
211
- });
212
- }
213
- }