@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.
- package/CHANGELOG.md +133 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-line-cache.ts +432 -0
- package/src/core/conversation-rendering.ts +11 -3
- package/src/core/conversation.ts +50 -4
- package/src/input/commands/cost-runtime.ts +49 -0
- package/src/input/commands/operator-runtime.ts +5 -1
- package/src/input/commands.ts +2 -0
- package/src/input/handler-content-actions.ts +8 -3
- package/src/input/handler-feed-routes.ts +12 -35
- package/src/input/handler-feed.ts +4 -8
- package/src/input/handler-shortcuts.ts +51 -0
- package/src/input/handler.ts +43 -6
- package/src/input/input-history.ts +17 -4
- package/src/input/keybindings.ts +48 -8
- package/src/input/panel-integration-actions.ts +107 -1
- package/src/main.ts +19 -11
- package/src/panels/agent-inspector-panel.ts +125 -36
- package/src/panels/agent-inspector-shared.ts +144 -0
- package/src/panels/approval-panel.ts +67 -16
- package/src/panels/automation-control-panel.ts +368 -124
- package/src/panels/base-panel.ts +1 -1
- package/src/panels/builtin/agent.ts +28 -37
- package/src/panels/builtin/development.ts +40 -32
- package/src/panels/builtin/knowledge.ts +15 -6
- package/src/panels/builtin/operations.ts +178 -117
- package/src/panels/builtin/session.ts +35 -9
- package/src/panels/builtin/shared.ts +98 -6
- package/src/panels/cockpit-panel.ts +232 -73
- package/src/panels/communication-panel.ts +58 -20
- package/src/panels/confirm-state.ts +8 -1
- package/src/panels/control-plane-panel.ts +351 -118
- package/src/panels/cost-tracker-panel.ts +165 -7
- package/src/panels/debug-panel.ts +312 -159
- package/src/panels/diff-panel.ts +209 -57
- package/src/panels/docs-panel.ts +152 -66
- package/src/panels/eval-panel.ts +220 -42
- package/src/panels/file-explorer-panel.ts +202 -122
- package/src/panels/file-preview-panel.ts +132 -57
- package/src/panels/git-panel.ts +310 -129
- package/src/panels/hooks-panel.ts +150 -22
- package/src/panels/incident-review-panel.ts +223 -35
- package/src/panels/index.ts +0 -4
- package/src/panels/intelligence-panel.ts +212 -86
- package/src/panels/knowledge-graph-panel.ts +461 -101
- package/src/panels/local-auth-panel.ts +240 -28
- package/src/panels/marketplace-panel.ts +193 -26
- package/src/panels/memory-panel.ts +78 -77
- package/src/panels/ops-control-panel.ts +146 -29
- package/src/panels/ops-strategy-panel.ts +72 -4
- package/src/panels/orchestration-panel.ts +231 -69
- package/src/panels/panel-list-panel.ts +154 -131
- package/src/panels/panel-manager.ts +97 -9
- package/src/panels/plan-dashboard-panel.ts +333 -17
- package/src/panels/plugins-panel.ts +184 -29
- package/src/panels/policy-panel.ts +210 -38
- package/src/panels/polish-core.ts +7 -2
- package/src/panels/polish.ts +23 -4
- package/src/panels/project-planning-answer-actions.ts +134 -0
- package/src/panels/project-planning-panel.ts +62 -113
- package/src/panels/provider-health-panel.ts +434 -518
- package/src/panels/provider-health-routes.ts +203 -0
- package/src/panels/provider-health-tracker.ts +194 -6
- package/src/panels/provider-health-views.ts +560 -0
- package/src/panels/qr-panel.ts +116 -30
- package/src/panels/remote-panel.ts +114 -36
- package/src/panels/routes-panel.ts +63 -22
- package/src/panels/sandbox-panel.ts +174 -42
- package/src/panels/scrollable-list-panel.ts +19 -135
- package/src/panels/security-panel.ts +133 -33
- package/src/panels/services-panel.ts +116 -64
- package/src/panels/session-browser-panel.ts +73 -2
- package/src/panels/session-maintenance.ts +4 -122
- package/src/panels/settings-sync-panel.ts +335 -72
- package/src/panels/skills-panel.ts +157 -89
- package/src/panels/subscription-panel.ts +86 -33
- package/src/panels/symbol-outline-panel.ts +248 -108
- package/src/panels/system-messages-panel.ts +114 -13
- package/src/panels/tasks-panel.ts +326 -139
- package/src/panels/thinking-panel.ts +43 -10
- package/src/panels/token-budget-panel.ts +194 -18
- package/src/panels/tool-inspector-panel.ts +144 -34
- package/src/panels/types.ts +40 -4
- package/src/panels/work-plan-panel.ts +280 -17
- package/src/panels/worktree-panel.ts +175 -42
- package/src/panels/wrfc-panel.ts +116 -24
- package/src/renderer/agent-detail-modal.ts +37 -8
- package/src/renderer/code-block.ts +58 -28
- package/src/renderer/context-inspector.ts +3 -6
- package/src/renderer/conversation-overlays.ts +25 -11
- package/src/renderer/conversation-surface.ts +1 -21
- package/src/renderer/diff-view.ts +6 -4
- package/src/renderer/footer-tips.ts +41 -0
- package/src/renderer/fullscreen-primitives.ts +23 -17
- package/src/renderer/fullscreen-workspace.ts +2 -7
- package/src/renderer/help-overlay.ts +91 -14
- package/src/renderer/hint-grammar.ts +52 -0
- package/src/renderer/history-search-overlay.ts +10 -16
- package/src/renderer/layout.ts +7 -7
- package/src/renderer/markdown.ts +9 -1
- package/src/renderer/modal-factory.ts +23 -15
- package/src/renderer/model-picker-overlay.ts +8 -485
- package/src/renderer/model-workspace.ts +9 -24
- package/src/renderer/overlay-box.ts +23 -19
- package/src/renderer/process-indicator.ts +15 -25
- package/src/renderer/profile-picker-modal.ts +13 -14
- package/src/renderer/prompt-content-width.ts +16 -0
- package/src/renderer/search-overlay.ts +27 -6
- package/src/renderer/selection-modal-overlay.ts +3 -1
- package/src/renderer/semantic-diff.ts +1 -1
- package/src/renderer/session-picker-modal.ts +6 -4
- package/src/renderer/settings-modal-helpers.ts +10 -34
- package/src/renderer/settings-modal.ts +70 -10
- package/src/renderer/shell-surface.ts +54 -15
- package/src/renderer/status-glyphs.ts +11 -9
- package/src/renderer/surface-layout.ts +0 -12
- package/src/renderer/term-caps.ts +1 -1
- package/src/renderer/theme.ts +60 -3
- package/src/renderer/tool-call.ts +6 -20
- package/src/renderer/ui-factory.ts +48 -37
- package/src/renderer/ui-primitives.ts +41 -3
- package/src/runtime/bootstrap-shell.ts +35 -18
- package/src/runtime/diagnostics/panels/index.ts +0 -3
- package/src/runtime/render-scheduler.ts +80 -0
- package/src/shell/ui-openers.ts +14 -22
- package/src/utils/format-duration.ts +2 -2
- package/src/utils/splash-lines.ts +54 -5
- package/src/version.ts +1 -1
- package/src/work-plans/work-plan-store.ts +13 -0
- package/src/panels/agent-logs-panel.ts +0 -635
- package/src/panels/agent-logs-shared.ts +0 -129
- package/src/panels/context-visualizer-panel.ts +0 -238
- package/src/panels/forensics-panel.ts +0 -378
- package/src/panels/provider-account-snapshot.ts +0 -259
- package/src/panels/provider-accounts-panel.ts +0 -255
- package/src/panels/provider-stats-panel.ts +0 -391
- package/src/panels/schedule-panel.ts +0 -365
- package/src/panels/watchers-panel.ts +0 -213
- package/src/renderer/file-tree.ts +0 -153
- package/src/renderer/progress.ts +0 -99
- package/src/renderer/status-token.ts +0 -67
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -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
|
-
}
|