@pellux/goodvibes-tui 0.28.0 → 0.29.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 (118) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/input/commands/cost-runtime.ts +49 -0
  5. package/src/input/commands/operator-runtime.ts +5 -1
  6. package/src/input/commands.ts +2 -0
  7. package/src/input/handler-feed-routes.ts +12 -35
  8. package/src/input/handler-feed.ts +4 -8
  9. package/src/input/handler-shortcuts.ts +51 -0
  10. package/src/input/handler.ts +43 -6
  11. package/src/input/keybindings.ts +48 -8
  12. package/src/input/panel-integration-actions.ts +107 -1
  13. package/src/main.ts +5 -1
  14. package/src/panels/agent-inspector-panel.ts +125 -36
  15. package/src/panels/agent-inspector-shared.ts +144 -0
  16. package/src/panels/approval-panel.ts +67 -16
  17. package/src/panels/automation-control-panel.ts +368 -124
  18. package/src/panels/builtin/agent.ts +28 -37
  19. package/src/panels/builtin/development.ts +40 -32
  20. package/src/panels/builtin/knowledge.ts +15 -6
  21. package/src/panels/builtin/operations.ts +178 -117
  22. package/src/panels/builtin/session.ts +35 -9
  23. package/src/panels/builtin/shared.ts +98 -6
  24. package/src/panels/cockpit-panel.ts +232 -73
  25. package/src/panels/communication-panel.ts +58 -20
  26. package/src/panels/confirm-state.ts +8 -1
  27. package/src/panels/control-plane-panel.ts +351 -118
  28. package/src/panels/cost-tracker-panel.ts +165 -7
  29. package/src/panels/debug-panel.ts +312 -159
  30. package/src/panels/diff-panel.ts +203 -57
  31. package/src/panels/docs-panel.ts +152 -66
  32. package/src/panels/eval-panel.ts +220 -42
  33. package/src/panels/file-explorer-panel.ts +202 -122
  34. package/src/panels/file-preview-panel.ts +132 -57
  35. package/src/panels/git-panel.ts +309 -128
  36. package/src/panels/hooks-panel.ts +150 -22
  37. package/src/panels/incident-review-panel.ts +223 -35
  38. package/src/panels/index.ts +0 -4
  39. package/src/panels/intelligence-panel.ts +212 -86
  40. package/src/panels/knowledge-graph-panel.ts +461 -101
  41. package/src/panels/local-auth-panel.ts +240 -28
  42. package/src/panels/marketplace-panel.ts +193 -26
  43. package/src/panels/memory-panel.ts +78 -77
  44. package/src/panels/ops-control-panel.ts +146 -29
  45. package/src/panels/ops-strategy-panel.ts +72 -4
  46. package/src/panels/orchestration-panel.ts +231 -69
  47. package/src/panels/panel-list-panel.ts +154 -131
  48. package/src/panels/panel-manager.ts +97 -9
  49. package/src/panels/plan-dashboard-panel.ts +333 -17
  50. package/src/panels/plugins-panel.ts +184 -29
  51. package/src/panels/policy-panel.ts +210 -38
  52. package/src/panels/polish-core.ts +7 -2
  53. package/src/panels/polish.ts +23 -4
  54. package/src/panels/project-planning-answer-actions.ts +134 -0
  55. package/src/panels/project-planning-panel.ts +62 -113
  56. package/src/panels/provider-health-panel.ts +434 -518
  57. package/src/panels/provider-health-routes.ts +203 -0
  58. package/src/panels/provider-health-tracker.ts +194 -6
  59. package/src/panels/provider-health-views.ts +560 -0
  60. package/src/panels/qr-panel.ts +116 -30
  61. package/src/panels/remote-panel.ts +114 -36
  62. package/src/panels/routes-panel.ts +63 -22
  63. package/src/panels/sandbox-panel.ts +174 -42
  64. package/src/panels/scrollable-list-panel.ts +19 -135
  65. package/src/panels/security-panel.ts +133 -33
  66. package/src/panels/services-panel.ts +116 -64
  67. package/src/panels/session-browser-panel.ts +73 -2
  68. package/src/panels/session-maintenance.ts +4 -122
  69. package/src/panels/settings-sync-panel.ts +335 -72
  70. package/src/panels/skills-panel.ts +157 -89
  71. package/src/panels/subscription-panel.ts +86 -33
  72. package/src/panels/symbol-outline-panel.ts +248 -108
  73. package/src/panels/system-messages-panel.ts +114 -13
  74. package/src/panels/tasks-panel.ts +326 -139
  75. package/src/panels/thinking-panel.ts +43 -10
  76. package/src/panels/token-budget-panel.ts +194 -18
  77. package/src/panels/tool-inspector-panel.ts +144 -34
  78. package/src/panels/types.ts +40 -4
  79. package/src/panels/work-plan-panel.ts +280 -17
  80. package/src/panels/worktree-panel.ts +175 -42
  81. package/src/panels/wrfc-panel.ts +116 -24
  82. package/src/renderer/conversation-overlays.ts +25 -11
  83. package/src/renderer/footer-tips.ts +41 -0
  84. package/src/renderer/fullscreen-primitives.ts +22 -16
  85. package/src/renderer/help-overlay.ts +91 -14
  86. package/src/renderer/hint-grammar.ts +52 -0
  87. package/src/renderer/layout.ts +7 -7
  88. package/src/renderer/modal-factory.ts +23 -15
  89. package/src/renderer/model-picker-overlay.ts +21 -7
  90. package/src/renderer/overlay-box.ts +16 -10
  91. package/src/renderer/process-indicator.ts +3 -1
  92. package/src/renderer/progress.ts +5 -4
  93. package/src/renderer/search-overlay.ts +27 -6
  94. package/src/renderer/session-picker-modal.ts +6 -4
  95. package/src/renderer/settings-modal.ts +70 -10
  96. package/src/renderer/shell-surface.ts +41 -15
  97. package/src/renderer/status-glyphs.ts +11 -9
  98. package/src/renderer/theme.ts +60 -3
  99. package/src/renderer/ui-factory.ts +41 -30
  100. package/src/renderer/ui-primitives.ts +23 -2
  101. package/src/runtime/bootstrap-shell.ts +35 -18
  102. package/src/runtime/diagnostics/panels/index.ts +0 -3
  103. package/src/shell/ui-openers.ts +14 -22
  104. package/src/utils/format-duration.ts +2 -2
  105. package/src/utils/splash-lines.ts +44 -3
  106. package/src/version.ts +1 -1
  107. package/src/work-plans/work-plan-store.ts +13 -0
  108. package/src/panels/agent-logs-panel.ts +0 -635
  109. package/src/panels/agent-logs-shared.ts +0 -129
  110. package/src/panels/context-visualizer-panel.ts +0 -238
  111. package/src/panels/forensics-panel.ts +0 -378
  112. package/src/panels/provider-account-snapshot.ts +0 -259
  113. package/src/panels/provider-accounts-panel.ts +0 -255
  114. package/src/panels/provider-stats-panel.ts +0 -391
  115. package/src/panels/schedule-panel.ts +0 -365
  116. package/src/panels/watchers-panel.ts +0 -213
  117. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  118. 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
- }
@@ -1,118 +0,0 @@
1
- /**
2
- * Panel resource diagnostics panel data provider.
3
- *
4
- * Polls the shared TUI-owned ComponentHealthMonitor and produces PanelResourceSnapshot
5
- * values for the diagnostics panel to render.
6
- */
7
- import type { ComponentHealthMonitor } from '../../perf/panel-health-monitor.ts';
8
- import type {
9
- ComponentResourceEntry,
10
- ComponentResourceSnapshot,
11
- } from '@/runtime/index.ts';
12
-
13
- const DEFAULT_POLL_INTERVAL_MS = 500;
14
-
15
- const HEALTH_ORDER: Record<string, number> = {
16
- overloaded: 0,
17
- warning: 1,
18
- healthy: 2,
19
- };
20
-
21
- export class PanelResourcesPanel {
22
- private readonly _pollIntervalMs: number;
23
- private readonly _monitor: ComponentHealthMonitor;
24
- private _current: ComponentResourceSnapshot;
25
- private _timerId: ReturnType<typeof setInterval> | null = null;
26
- private readonly _subscribers = new Set<() => void>();
27
-
28
- constructor(monitor: ComponentHealthMonitor, pollIntervalMs: number = DEFAULT_POLL_INTERVAL_MS) {
29
- this._monitor = monitor;
30
- this._pollIntervalMs = pollIntervalMs;
31
- this._current = this._buildSnapshot(Date.now());
32
- }
33
-
34
- start(): void {
35
- if (this._timerId !== null) return;
36
- this._timerId = setInterval(() => {
37
- this._current = this._buildSnapshot(Date.now());
38
- this._notify();
39
- }, this._pollIntervalMs);
40
- }
41
-
42
- stop(): void {
43
- if (this._timerId !== null) {
44
- clearInterval(this._timerId);
45
- this._timerId = null;
46
- }
47
- }
48
-
49
- getSnapshot(): ComponentResourceSnapshot {
50
- return this._current;
51
- }
52
-
53
- refresh(now: number = Date.now()): ComponentResourceSnapshot {
54
- this._current = this._buildSnapshot(now);
55
- return this._current;
56
- }
57
-
58
- subscribe(callback: () => void): () => void {
59
- this._subscribers.add(callback);
60
- return () => this._subscribers.delete(callback);
61
- }
62
-
63
- dispose(): void {
64
- this.stop();
65
- this._subscribers.clear();
66
- }
67
-
68
- private _buildSnapshot(capturedAt: number): ComponentResourceSnapshot {
69
- const healthStates = this._monitor.getAllHealth();
70
-
71
- const panels: ComponentResourceEntry[] = healthStates.map((health) => {
72
- const contract = this._monitor.getContract(health.componentId);
73
- return {
74
- componentId: health.componentId,
75
- throttleStatus: health.throttleStatus,
76
- healthStatus: health.healthStatus,
77
- renderP95Ms: health.renderP95Ms,
78
- maxRenderMs: contract?.maxRenderMs ?? 0,
79
- rendersInWindow: health.rendersInWindow,
80
- maxUpdatesPerSecond: contract?.maxUpdatesPerSecond ?? 0,
81
- consecutiveViolations: health.consecutiveViolations,
82
- totalSuppressed: health.totalSuppressed,
83
- totalPermitted: health.totalPermitted,
84
- lastRenderAt: health.lastRenderAt,
85
- nextAllowedAt: health.nextAllowedAt,
86
- };
87
- });
88
-
89
- panels.sort((left, right) => {
90
- const diff = (HEALTH_ORDER[left.healthStatus] ?? 2) - (HEALTH_ORDER[right.healthStatus] ?? 2);
91
- return diff !== 0 ? diff : left.componentId.localeCompare(right.componentId);
92
- });
93
-
94
- const overloadedCount = panels.filter((panel) => panel.healthStatus === 'overloaded').length;
95
- const warningCount = panels.filter((panel) => panel.healthStatus === 'warning').length;
96
- const healthyCount = panels.filter((panel) => panel.healthStatus === 'healthy').length;
97
- const totalSuppressed = panels.reduce((sum, panel) => sum + panel.totalSuppressed, 0);
98
-
99
- return {
100
- panels,
101
- overloadedCount,
102
- warningCount,
103
- healthyCount,
104
- totalSuppressed,
105
- capturedAt,
106
- };
107
- }
108
-
109
- private _notify(): void {
110
- for (const callback of this._subscribers) {
111
- try {
112
- callback();
113
- } catch {
114
- // Subscriber failures must not take down diagnostics polling.
115
- }
116
- }
117
- }
118
- }