@pellux/goodvibes-tui 0.18.20 → 0.18.23
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 +120 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/core/conversation-rendering.ts +20 -6
- package/src/input/commands/session.ts +0 -1
- package/src/input/feed-context-factory.ts +236 -0
- package/src/input/handler-feed.ts +44 -6
- package/src/input/handler-shortcuts.ts +138 -125
- package/src/input/handler.ts +121 -119
- package/src/input/keybindings.ts +30 -0
- package/src/panels/approval-panel.ts +54 -82
- package/src/panels/automation-control-panel.ts +119 -161
- package/src/panels/communication-panel.ts +68 -107
- package/src/panels/control-plane-panel.ts +116 -172
- package/src/panels/hooks-panel.ts +101 -138
- package/src/panels/incident-review-panel.ts +55 -107
- package/src/panels/local-auth-panel.ts +76 -93
- package/src/panels/mcp-panel.ts +108 -155
- package/src/panels/ops-control-panel.ts +50 -85
- package/src/panels/panel-manager.ts +22 -2
- package/src/panels/plugins-panel.ts +36 -60
- package/src/panels/routes-panel.ts +89 -141
- package/src/panels/scrollable-list-panel.ts +45 -14
- package/src/panels/security-panel.ts +101 -137
- package/src/panels/services-panel.ts +58 -102
- package/src/panels/settings-sync-panel.ts +76 -122
- package/src/panels/subscription-panel.ts +63 -86
- package/src/panels/tasks-panel.ts +129 -179
- package/src/panels/watchers-panel.ts +88 -137
- package/src/renderer/buffer.ts +11 -0
- package/src/renderer/diff.ts +8 -0
- package/src/renderer/help-overlay.ts +37 -28
- package/src/renderer/markdown.ts +3 -145
- package/src/version.ts +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
-
import {
|
|
3
|
+
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
4
|
import type { RuntimeTask, TaskLifecycleState } from '@pellux/goodvibes-sdk/platform/runtime/store/domains/tasks';
|
|
5
5
|
import type { ManagedWorktreeMeta } from '@pellux/goodvibes-sdk/platform/runtime/worktree/registry';
|
|
6
6
|
import type { UiReadModel, UiTasksSnapshot, UiWorktreeSnapshot } from '../runtime/ui-read-models.ts';
|
|
@@ -13,8 +13,6 @@ import {
|
|
|
13
13
|
buildSummaryBlock,
|
|
14
14
|
buildPanelWorkspace,
|
|
15
15
|
DEFAULT_PANEL_PALETTE,
|
|
16
|
-
resolvePrimaryScrollableSection,
|
|
17
|
-
type PanelWorkspaceSection,
|
|
18
16
|
} from './polish.ts';
|
|
19
17
|
|
|
20
18
|
const C = {
|
|
@@ -148,12 +146,10 @@ function reviewTaskWorktreeAttachments(
|
|
|
148
146
|
});
|
|
149
147
|
}
|
|
150
148
|
|
|
151
|
-
export class TasksPanel extends
|
|
149
|
+
export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
|
|
152
150
|
private readonly readModel?: UiReadModel<UiTasksSnapshot>;
|
|
153
151
|
private readonly worktrees?: UiReadModel<UiWorktreeSnapshot>;
|
|
154
152
|
private readonly unsubscribers: readonly (() => void)[];
|
|
155
|
-
private selectedIndex = 0;
|
|
156
|
-
private scrollOffset = 0;
|
|
157
153
|
|
|
158
154
|
public constructor(
|
|
159
155
|
readModel: UiReadModel<UiTasksSnapshot> | undefined,
|
|
@@ -177,39 +173,46 @@ export class TasksPanel extends BasePanel {
|
|
|
177
173
|
for (const unsubscribe of this.unsubscribers) unsubscribe();
|
|
178
174
|
}
|
|
179
175
|
|
|
176
|
+
protected override getPalette() { return C; }
|
|
177
|
+
protected override getEmptyStateMessage() { return ' No tasks recorded yet.'; }
|
|
178
|
+
protected override getEmptyStateActions() {
|
|
179
|
+
return [
|
|
180
|
+
{ command: '/tasks create', summary: 'create a tracked task from the shell' },
|
|
181
|
+
{ command: '/orchestration', summary: 'review graph-native task execution and WRFC flows' },
|
|
182
|
+
];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
protected getItems(): readonly RuntimeTask[] {
|
|
186
|
+
if (!this.readModel) return [];
|
|
187
|
+
return sortTasks([...this.readModel.getSnapshot().tasks]);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
protected renderItem(task: RuntimeTask, index: number, selected: boolean, width: number): Line {
|
|
191
|
+
return buildPanelListRow(width, [
|
|
192
|
+
{ text: task.status.padEnd(10), fg: statusColor(task.status) },
|
|
193
|
+
{ text: ` ${kindLabel(task.kind).padEnd(12)}`, fg: C.value },
|
|
194
|
+
{ text: ` ${task.id.slice(0, 8)} `, fg: C.dim },
|
|
195
|
+
{ text: task.title.slice(0, Math.max(0, width - 37)), fg: C.value },
|
|
196
|
+
], C, { selected });
|
|
197
|
+
}
|
|
198
|
+
|
|
180
199
|
public handleInput(key: string): boolean {
|
|
181
|
-
const tasks = this._tasks();
|
|
182
|
-
if (tasks.length === 0) return false;
|
|
183
|
-
if (key === 'up' || key === 'k') {
|
|
184
|
-
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
185
|
-
this.markDirty();
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
if (key === 'down' || key === 'j') {
|
|
189
|
-
this.selectedIndex = Math.min(tasks.length - 1, this.selectedIndex + 1);
|
|
190
|
-
this.markDirty();
|
|
191
|
-
return true;
|
|
192
|
-
}
|
|
193
200
|
if (key === 'home') {
|
|
194
201
|
this.selectedIndex = 0;
|
|
195
202
|
this.markDirty();
|
|
196
203
|
return true;
|
|
197
204
|
}
|
|
198
205
|
if (key === 'end') {
|
|
199
|
-
|
|
206
|
+
const tasks = this.getItems();
|
|
207
|
+
this.selectedIndex = Math.max(0, tasks.length - 1);
|
|
200
208
|
this.markDirty();
|
|
201
209
|
return true;
|
|
202
210
|
}
|
|
203
|
-
return
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
private _tasks(): RuntimeTask[] {
|
|
207
|
-
if (!this.readModel) return [];
|
|
208
|
-
return sortTasks([...this.readModel.getSnapshot().tasks]);
|
|
211
|
+
return super.handleInput(key);
|
|
209
212
|
}
|
|
210
213
|
|
|
211
214
|
public render(width: number, height: number): Line[] {
|
|
212
|
-
this.
|
|
215
|
+
this.clampSelection();
|
|
213
216
|
const intro = 'Live task lifecycle, ownership, retries, and result/error details across runtime execution domains.';
|
|
214
217
|
const footerLines = [buildPanelLine(width, [[' Up/Down move Home/End jump', C.dim]])];
|
|
215
218
|
|
|
@@ -232,34 +235,7 @@ export class TasksPanel extends BasePanel {
|
|
|
232
235
|
return workspace;
|
|
233
236
|
}
|
|
234
237
|
|
|
235
|
-
const tasks = this.
|
|
236
|
-
if (tasks.length === 0) {
|
|
237
|
-
const workspace = buildPanelWorkspace(width, height, {
|
|
238
|
-
title: 'Task Control Room',
|
|
239
|
-
intro,
|
|
240
|
-
sections: [{
|
|
241
|
-
title: 'Overview',
|
|
242
|
-
lines: [
|
|
243
|
-
buildPanelLine(width, [[' queued:0 running:0 blocked:0 failed:0 completed:0', C.dim]]),
|
|
244
|
-
...buildEmptyState(
|
|
245
|
-
width,
|
|
246
|
-
' No tasks recorded yet.',
|
|
247
|
-
'Tasks will appear here as exec, agent, ACP, scheduler, daemon, MCP, plugin, and integration work starts.',
|
|
248
|
-
[
|
|
249
|
-
{ command: '/tasks create', summary: 'create a tracked task from the shell' },
|
|
250
|
-
{ command: '/orchestration', summary: 'review graph-native task execution and WRFC flows' },
|
|
251
|
-
],
|
|
252
|
-
C,
|
|
253
|
-
),
|
|
254
|
-
],
|
|
255
|
-
}],
|
|
256
|
-
palette: C,
|
|
257
|
-
});
|
|
258
|
-
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
259
|
-
return workspace;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
this.selectedIndex = Math.min(this.selectedIndex, tasks.length - 1);
|
|
238
|
+
const tasks = this.getItems();
|
|
263
239
|
const counts = STATUS_ORDER.map((status) => ({
|
|
264
240
|
status,
|
|
265
241
|
count: tasks.filter((task) => task.status === status).length,
|
|
@@ -270,7 +246,6 @@ export class TasksPanel extends BasePanel {
|
|
|
270
246
|
const queuedCount = counts.find((entry) => entry.status === 'queued')?.count ?? 0;
|
|
271
247
|
const completedCount = counts.find((entry) => entry.status === 'completed')?.count ?? 0;
|
|
272
248
|
|
|
273
|
-
const selected = tasks[this.selectedIndex]!;
|
|
274
249
|
const postureLines: Line[] = [
|
|
275
250
|
buildPanelLine(width, [
|
|
276
251
|
[' queued ', C.label],
|
|
@@ -284,7 +259,11 @@ export class TasksPanel extends BasePanel {
|
|
|
284
259
|
[' completed ', C.label],
|
|
285
260
|
[String(completedCount), completedCount > 0 ? C.completed : C.dim],
|
|
286
261
|
]),
|
|
287
|
-
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
const selected = tasks[this.selectedIndex];
|
|
265
|
+
if (selected) {
|
|
266
|
+
postureLines.push(buildPanelLine(width, [
|
|
288
267
|
[' selected ', C.label],
|
|
289
268
|
[selected.id, C.info],
|
|
290
269
|
[' status ', C.label],
|
|
@@ -293,156 +272,127 @@ export class TasksPanel extends BasePanel {
|
|
|
293
272
|
[selected.kind, C.value],
|
|
294
273
|
[' owner ', C.label],
|
|
295
274
|
[selected.owner.slice(0, Math.max(0, width - 46)), C.dim],
|
|
296
|
-
])
|
|
275
|
+
]));
|
|
276
|
+
}
|
|
277
|
+
postureLines.push(
|
|
297
278
|
buildGuidanceLine(width, '/teamwork review', 'inspect task-family posture, archetype metadata, and recovery options for active work', C),
|
|
298
279
|
buildGuidanceLine(width, '/worktree task <task-id>', 'review worktree ownership, restore, and merge posture for the selected task', C),
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const detailRows: Line[] = [
|
|
302
|
-
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const detailRows: Line[] = [];
|
|
283
|
+
if (selected) {
|
|
284
|
+
const descriptor = selected.description ? parseTaskDescriptor(selected.description) : null;
|
|
285
|
+
detailRows.push(buildPanelLine(width, [
|
|
303
286
|
[' Title: ', C.label],
|
|
304
287
|
[selected.title, C.value],
|
|
305
288
|
[' Status: ', C.label],
|
|
306
289
|
[selected.status, statusColor(selected.status)],
|
|
307
290
|
[' Kind: ', C.label],
|
|
308
291
|
[selected.kind, C.value],
|
|
309
|
-
])
|
|
310
|
-
buildPanelLine(width, [
|
|
292
|
+
]));
|
|
293
|
+
detailRows.push(buildPanelLine(width, [
|
|
311
294
|
[' Owner: ', C.label],
|
|
312
295
|
[selected.owner, C.value],
|
|
313
296
|
[' Cancellable: ', C.label],
|
|
314
297
|
[selected.cancellable ? 'yes' : 'no', selected.cancellable ? C.running : C.failed],
|
|
315
298
|
[' Queue: ', C.label],
|
|
316
299
|
[formatWhen(selected.queuedAt), C.dim],
|
|
317
|
-
])
|
|
318
|
-
buildPanelLine(width, [
|
|
300
|
+
]));
|
|
301
|
+
detailRows.push(buildPanelLine(width, [
|
|
319
302
|
[' Started: ', C.label],
|
|
320
303
|
[formatWhen(selected.startedAt), C.dim],
|
|
321
304
|
[' Ended: ', C.label],
|
|
322
305
|
[formatWhen(selected.endedAt), C.dim],
|
|
323
306
|
[' Duration: ', C.label],
|
|
324
307
|
[formatDuration(selected.startedAt, selected.endedAt), C.dim],
|
|
325
|
-
]),
|
|
326
|
-
];
|
|
327
|
-
if (descriptor?.mode || descriptor?.family || descriptor?.source) {
|
|
328
|
-
detailRows.push(buildPanelLine(width, [
|
|
329
|
-
[' Mode: ', C.label],
|
|
330
|
-
[descriptor?.mode ?? 'n/a', C.value],
|
|
331
|
-
[' Family: ', C.label],
|
|
332
|
-
[descriptor?.family ?? 'n/a', C.info],
|
|
333
|
-
[' Source: ', C.label],
|
|
334
|
-
[descriptor?.source ?? 'builtin/runtime', C.dim],
|
|
335
|
-
]));
|
|
336
|
-
}
|
|
337
|
-
if (descriptor?.reviewMode || descriptor?.executionProtocol || descriptor?.template) {
|
|
338
|
-
detailRows.push(buildPanelLine(width, [
|
|
339
|
-
[' Review: ', C.label],
|
|
340
|
-
[descriptor?.reviewMode ?? 'n/a', C.value],
|
|
341
|
-
[' Protocol: ', C.label],
|
|
342
|
-
[descriptor?.executionProtocol ?? 'n/a', C.value],
|
|
343
|
-
[' Template: ', C.label],
|
|
344
|
-
[descriptor?.template ?? 'n/a', C.dim],
|
|
345
|
-
]));
|
|
346
|
-
}
|
|
347
|
-
if (selected.correlationId || selected.turnId) {
|
|
348
|
-
detailRows.push(buildPanelLine(width, [
|
|
349
|
-
[' Correlation: ', C.label],
|
|
350
|
-
[selected.correlationId ?? 'n/a', C.dim],
|
|
351
|
-
[' Turn: ', C.label],
|
|
352
|
-
[selected.turnId ?? 'n/a', C.dim],
|
|
353
|
-
]));
|
|
354
|
-
}
|
|
355
|
-
if (selected.parentTaskId || selected.childTaskIds.length > 0) {
|
|
356
|
-
detailRows.push(buildPanelLine(width, [
|
|
357
|
-
[' Parent: ', C.label],
|
|
358
|
-
[selected.parentTaskId ?? 'none', C.dim],
|
|
359
|
-
[' Children: ', C.label],
|
|
360
|
-
[selected.childTaskIds.length > 0 ? selected.childTaskIds.join(', ') : 'none', C.dim],
|
|
361
|
-
]));
|
|
362
|
-
}
|
|
363
|
-
const attachedWorktrees = reviewTaskWorktreeAttachments(selected.id, this.worktrees);
|
|
364
|
-
if (attachedWorktrees.total > 0) {
|
|
365
|
-
detailRows.push(buildPanelLine(width, [
|
|
366
|
-
[' Worktrees: ', C.label],
|
|
367
|
-
[`${attachedWorktrees.total} tracked`, C.info],
|
|
368
|
-
[' Active: ', C.label],
|
|
369
|
-
[String(attachedWorktrees.active), attachedWorktrees.active > 0 ? C.running : C.dim],
|
|
370
|
-
[' Paused: ', C.label],
|
|
371
|
-
[String(attachedWorktrees.paused), attachedWorktrees.paused > 0 ? C.blocked : C.dim],
|
|
372
308
|
]));
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
309
|
+
if (descriptor?.mode || descriptor?.family || descriptor?.source) {
|
|
310
|
+
detailRows.push(buildPanelLine(width, [
|
|
311
|
+
[' Mode: ', C.label],
|
|
312
|
+
[descriptor?.mode ?? 'n/a', C.value],
|
|
313
|
+
[' Family: ', C.label],
|
|
314
|
+
[descriptor?.family ?? 'n/a', C.info],
|
|
315
|
+
[' Source: ', C.label],
|
|
316
|
+
[descriptor?.source ?? 'builtin/runtime', C.dim],
|
|
317
|
+
]));
|
|
318
|
+
}
|
|
319
|
+
if (descriptor?.reviewMode || descriptor?.executionProtocol || descriptor?.template) {
|
|
320
|
+
detailRows.push(buildPanelLine(width, [
|
|
321
|
+
[' Review: ', C.label],
|
|
322
|
+
[descriptor?.reviewMode ?? 'n/a', C.value],
|
|
323
|
+
[' Protocol: ', C.label],
|
|
324
|
+
[descriptor?.executionProtocol ?? 'n/a', C.value],
|
|
325
|
+
[' Template: ', C.label],
|
|
326
|
+
[descriptor?.template ?? 'n/a', C.dim],
|
|
327
|
+
]));
|
|
328
|
+
}
|
|
329
|
+
if (selected.correlationId || selected.turnId) {
|
|
330
|
+
detailRows.push(buildPanelLine(width, [
|
|
331
|
+
[' Correlation: ', C.label],
|
|
332
|
+
[selected.correlationId ?? 'n/a', C.dim],
|
|
333
|
+
[' Turn: ', C.label],
|
|
334
|
+
[selected.turnId ?? 'n/a', C.dim],
|
|
335
|
+
]));
|
|
336
|
+
}
|
|
337
|
+
if (selected.parentTaskId || selected.childTaskIds.length > 0) {
|
|
338
|
+
detailRows.push(buildPanelLine(width, [
|
|
339
|
+
[' Parent: ', C.label],
|
|
340
|
+
[selected.parentTaskId ?? 'none', C.dim],
|
|
341
|
+
[' Children: ', C.label],
|
|
342
|
+
[selected.childTaskIds.length > 0 ? selected.childTaskIds.join(', ') : 'none', C.dim],
|
|
343
|
+
]));
|
|
344
|
+
}
|
|
345
|
+
const attachedWorktrees = reviewTaskWorktreeAttachments(selected.id, this.worktrees);
|
|
346
|
+
if (attachedWorktrees.total > 0) {
|
|
347
|
+
detailRows.push(buildPanelLine(width, [
|
|
348
|
+
[' Worktrees: ', C.label],
|
|
349
|
+
[`${attachedWorktrees.total} tracked`, C.info],
|
|
350
|
+
[' Active: ', C.label],
|
|
351
|
+
[String(attachedWorktrees.active), attachedWorktrees.active > 0 ? C.running : C.dim],
|
|
352
|
+
[' Paused: ', C.label],
|
|
353
|
+
[String(attachedWorktrees.paused), attachedWorktrees.paused > 0 ? C.blocked : C.dim],
|
|
354
|
+
]));
|
|
378
355
|
detailRows.push(buildPanelLine(width, [[
|
|
379
|
-
` ${
|
|
380
|
-
|
|
356
|
+
` Next: /worktree task ${selected.id} /worktree recover task ${selected.id}`,
|
|
357
|
+
C.dim,
|
|
381
358
|
]]));
|
|
359
|
+
for (const record of attachedWorktrees.records.slice(0, 2)) {
|
|
360
|
+
detailRows.push(buildPanelLine(width, [[
|
|
361
|
+
` ${record.state.padEnd(15)} ${record.path}`.slice(0, Math.max(0, width - 2)),
|
|
362
|
+
record.state === 'active' ? C.running : record.state === 'paused' ? C.blocked : C.dim,
|
|
363
|
+
]]));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (selected.retryPolicy) {
|
|
367
|
+
detailRows.push(buildPanelLine(width, [
|
|
368
|
+
[' Retry: ', C.label],
|
|
369
|
+
[`${selected.retryPolicy.currentAttempt}/${selected.retryPolicy.maxAttempts} ${selected.retryPolicy.backoff}`, C.value],
|
|
370
|
+
]));
|
|
371
|
+
}
|
|
372
|
+
if (selected.error) {
|
|
373
|
+
detailRows.push(buildPanelLine(width, [
|
|
374
|
+
[' Error: ', C.label],
|
|
375
|
+
[selected.error.slice(0, Math.max(0, width - 10)), C.failed],
|
|
376
|
+
]));
|
|
377
|
+
}
|
|
378
|
+
if (selected.result !== undefined) {
|
|
379
|
+
const resultText = safeJson(selected.result);
|
|
380
|
+
detailRows.push(buildPanelLine(width, [
|
|
381
|
+
[' Result: ', C.label],
|
|
382
|
+
[resultText.slice(0, Math.max(0, width - 11)), C.dim],
|
|
383
|
+
]));
|
|
382
384
|
}
|
|
383
385
|
}
|
|
384
|
-
if (selected.retryPolicy) {
|
|
385
|
-
detailRows.push(buildPanelLine(width, [
|
|
386
|
-
[' Retry: ', C.label],
|
|
387
|
-
[`${selected.retryPolicy.currentAttempt}/${selected.retryPolicy.maxAttempts} ${selected.retryPolicy.backoff}`, C.value],
|
|
388
|
-
]));
|
|
389
|
-
}
|
|
390
|
-
if (selected.error) {
|
|
391
|
-
detailRows.push(buildPanelLine(width, [
|
|
392
|
-
[' Error: ', C.label],
|
|
393
|
-
[selected.error.slice(0, Math.max(0, width - 10)), C.failed],
|
|
394
|
-
]));
|
|
395
|
-
}
|
|
396
|
-
if (selected.result !== undefined) {
|
|
397
|
-
const resultText = safeJson(selected.result);
|
|
398
|
-
detailRows.push(buildPanelLine(width, [
|
|
399
|
-
[' Result: ', C.label],
|
|
400
|
-
[resultText.slice(0, Math.max(0, width - 11)), C.dim],
|
|
401
|
-
]));
|
|
402
|
-
}
|
|
403
|
-
const postureSection: PanelWorkspaceSection = { lines: buildSummaryBlock(width, 'Task posture', postureLines, C) };
|
|
404
|
-
const selectedSection: PanelWorkspaceSection = { lines: buildDetailBlock(width, 'Selected task', detailRows, C) };
|
|
405
|
-
const rawTaskLines: Line[] = [];
|
|
406
|
-
for (let absolute = 0; absolute < tasks.length; absolute++) {
|
|
407
|
-
const task = tasks[absolute]!;
|
|
408
|
-
rawTaskLines.push(buildPanelListRow(width, [
|
|
409
|
-
{ text: task.status.padEnd(10), fg: statusColor(task.status) },
|
|
410
|
-
{ text: ` ${kindLabel(task.kind).padEnd(12)}`, fg: C.value },
|
|
411
|
-
{ text: ` ${task.id.slice(0, 8)} `, fg: C.dim },
|
|
412
|
-
{ text: task.title.slice(0, Math.max(0, width - 37)), fg: C.value },
|
|
413
|
-
], C, { selected: absolute === this.selectedIndex }));
|
|
414
|
-
}
|
|
415
|
-
const resolvedTasksSection = resolvePrimaryScrollableSection(width, height, {
|
|
416
|
-
intro,
|
|
417
|
-
footerLines,
|
|
418
|
-
palette: C,
|
|
419
|
-
beforeSections: [postureSection],
|
|
420
|
-
section: {
|
|
421
|
-
title: 'Tasks',
|
|
422
|
-
scrollableLines: rawTaskLines,
|
|
423
|
-
selectedIndex: this.selectedIndex,
|
|
424
|
-
scrollOffset: this.scrollOffset,
|
|
425
|
-
guardRows: 1,
|
|
426
|
-
minRows: 4,
|
|
427
|
-
appendWindowSummary: { dimColor: C.dim },
|
|
428
|
-
},
|
|
429
|
-
afterSections: [selectedSection],
|
|
430
|
-
});
|
|
431
|
-
this.scrollOffset = resolvedTasksSection.scrollOffset;
|
|
432
386
|
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
selectedSection,
|
|
437
|
-
];
|
|
438
|
-
const lines = buildPanelWorkspace(width, height, {
|
|
387
|
+
const headerLines: Line[] = buildSummaryBlock(width, 'Task posture', postureLines, C);
|
|
388
|
+
|
|
389
|
+
return this.renderList(width, height, {
|
|
439
390
|
title: 'Task Control Room',
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
391
|
+
header: headerLines,
|
|
392
|
+
footer: [
|
|
393
|
+
...buildDetailBlock(width, 'Selected task', detailRows, C),
|
|
394
|
+
...footerLines,
|
|
395
|
+
],
|
|
444
396
|
});
|
|
445
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
446
|
-
return lines.slice(0, height);
|
|
447
397
|
}
|
|
448
398
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Line } from '../types/grid.ts';
|
|
2
2
|
import { createEmptyLine } from '../types/grid.ts';
|
|
3
|
-
import {
|
|
3
|
+
import { ScrollableListPanel } from './scrollable-list-panel.ts';
|
|
4
4
|
import type { UiReadModel, UiWatchersSnapshot } from '../runtime/ui-read-models.ts';
|
|
5
5
|
import { truncateDisplay } from '../utils/terminal-width.ts';
|
|
6
6
|
import {
|
|
@@ -10,8 +10,7 @@ import {
|
|
|
10
10
|
buildPanelLine,
|
|
11
11
|
buildPanelWorkspace,
|
|
12
12
|
DEFAULT_PANEL_PALETTE,
|
|
13
|
-
|
|
14
|
-
type PanelWorkspaceSection,
|
|
13
|
+
type PanelPalette,
|
|
15
14
|
} from './polish.ts';
|
|
16
15
|
|
|
17
16
|
const C = {
|
|
@@ -51,11 +50,11 @@ function formatTime(value?: number): string {
|
|
|
51
50
|
return new Date(value).toLocaleString();
|
|
52
51
|
}
|
|
53
52
|
|
|
54
|
-
|
|
53
|
+
type WatcherEntry = UiWatchersSnapshot['watchers'][number];
|
|
54
|
+
|
|
55
|
+
export class WatchersPanel extends ScrollableListPanel<WatcherEntry> {
|
|
55
56
|
private readonly readModel?: UiReadModel<UiWatchersSnapshot>;
|
|
56
57
|
private readonly unsub: (() => void) | null;
|
|
57
|
-
private selectedIndex = 0;
|
|
58
|
-
private scrollOffset = 0;
|
|
59
58
|
|
|
60
59
|
public constructor(readModel?: UiReadModel<UiWatchersSnapshot>) {
|
|
61
60
|
super('watchers', 'Watchers', 'W', 'monitoring');
|
|
@@ -67,29 +66,38 @@ export class WatchersPanel extends BasePanel {
|
|
|
67
66
|
this.unsub?.();
|
|
68
67
|
}
|
|
69
68
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (watchers.length === 0) return false;
|
|
73
|
-
if (key === 'up' || key === 'k') {
|
|
74
|
-
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
|
|
75
|
-
this.markDirty();
|
|
76
|
-
return true;
|
|
77
|
-
}
|
|
78
|
-
if (key === 'down' || key === 'j') {
|
|
79
|
-
this.selectedIndex = Math.min(watchers.length - 1, this.selectedIndex + 1);
|
|
80
|
-
this.markDirty();
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
return false;
|
|
69
|
+
protected override getPalette(): PanelPalette {
|
|
70
|
+
return C;
|
|
84
71
|
}
|
|
85
72
|
|
|
86
|
-
|
|
73
|
+
protected getItems(): readonly WatcherEntry[] {
|
|
87
74
|
if (!this.readModel) return [];
|
|
88
|
-
return
|
|
75
|
+
return this.readModel.getSnapshot().watchers;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
protected renderItem(watcher: WatcherEntry, _index: number, selected: boolean, width: number): Line {
|
|
79
|
+
const bg = selected ? C.selectBg : undefined;
|
|
80
|
+
return buildPanelLine(width, [
|
|
81
|
+
[' ', C.label, bg],
|
|
82
|
+
[watcher.state.padEnd(10), stateColor(watcher.state), bg],
|
|
83
|
+
[` ${truncateDisplay(watcher.label, 18).padEnd(18)}`, C.value, bg],
|
|
84
|
+
[` ${String(watcher.sourceStatus ?? 'unknown').padEnd(10)}`, sourceStatusColor(watcher.sourceStatus), bg],
|
|
85
|
+
[` ${truncateDisplay(formatLag(watcher.sourceLagMs), Math.max(0, width - 43))}`, C.dim, bg],
|
|
86
|
+
]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
protected override getEmptyStateMessage(): string {
|
|
90
|
+
return ' No watchers registered.';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
protected override getEmptyStateActions(): Array<{ command: string; summary: string }> {
|
|
94
|
+
return [
|
|
95
|
+
{ command: '/schedule list', summary: 'review automation that will consume watcher events' },
|
|
96
|
+
{ command: '/services auth-review', summary: 'validate integration credentials before enabling remote watchers' },
|
|
97
|
+
];
|
|
89
98
|
}
|
|
90
99
|
|
|
91
100
|
public render(width: number, height: number): Line[] {
|
|
92
|
-
this.needsRender = false;
|
|
93
101
|
const intro = 'Managed watchers and source health used to trigger automation, refresh routes, and surface degraded upstream conditions.';
|
|
94
102
|
|
|
95
103
|
if (!this.readModel) {
|
|
@@ -112,130 +120,73 @@ export class WatchersPanel extends BasePanel {
|
|
|
112
120
|
}
|
|
113
121
|
|
|
114
122
|
const snapshot = this.readModel.getSnapshot();
|
|
115
|
-
const watchers = this.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
],
|
|
127
|
-
};
|
|
123
|
+
const watchers = this.getItems();
|
|
124
|
+
|
|
125
|
+
const headerLines: Line[] = [
|
|
126
|
+
buildKeyValueLine(width, [
|
|
127
|
+
{ label: 'watchers', value: String(snapshot.totalWatchers), valueColor: snapshot.totalWatchers > 0 ? C.info : C.dim },
|
|
128
|
+
{ label: 'active', value: String(snapshot.activeWatcherIds.length), valueColor: snapshot.activeWatcherIds.length > 0 ? C.ok : C.dim },
|
|
129
|
+
{ label: 'degraded', value: String(snapshot.totalDegraded), valueColor: snapshot.totalDegraded > 0 ? C.warn : C.dim },
|
|
130
|
+
{ label: 'lagged', value: String(snapshot.totalLagged), valueColor: snapshot.totalLagged > 0 ? C.warn : C.dim },
|
|
131
|
+
], C),
|
|
132
|
+
buildGuidanceLine(width, '/schedule list', 'verify jobs consuming these sources and use daemon APIs for watcher lifecycle control', C),
|
|
133
|
+
];
|
|
128
134
|
|
|
129
135
|
if (watchers.length === 0) {
|
|
130
|
-
|
|
136
|
+
return this.renderList(width, height, {
|
|
131
137
|
title: 'Watchers',
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
summarySection,
|
|
135
|
-
{
|
|
136
|
-
lines: buildEmptyState(
|
|
137
|
-
width,
|
|
138
|
-
' No watchers registered.',
|
|
139
|
-
'Register daemon watchers or enable polling/integration sources to populate this control room.',
|
|
140
|
-
[
|
|
141
|
-
{ command: '/schedule list', summary: 'review automation that will consume watcher events' },
|
|
142
|
-
{ command: '/services auth-review', summary: 'validate integration credentials before enabling remote watchers' },
|
|
143
|
-
],
|
|
144
|
-
C,
|
|
145
|
-
),
|
|
146
|
-
},
|
|
147
|
-
],
|
|
148
|
-
palette: C,
|
|
138
|
+
header: headerLines,
|
|
139
|
+
emptyMessage: ' No watchers registered.',
|
|
149
140
|
});
|
|
150
|
-
while (workspace.length < height) workspace.push(createEmptyLine(width));
|
|
151
|
-
return workspace;
|
|
152
141
|
}
|
|
153
142
|
|
|
154
|
-
this.
|
|
143
|
+
this.clampSelection();
|
|
155
144
|
const selected = watchers[this.selectedIndex]!;
|
|
156
145
|
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
]
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
]
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
]
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
[truncateDisplay(selected.lastCheckpoint ?? 'n/a', Math.max(0, width - 38)), C.dim],
|
|
183
|
-
]),
|
|
184
|
-
...(selected.degradedReason ? [
|
|
185
|
-
buildPanelLine(width, [
|
|
186
|
-
[' Reason: ', C.label],
|
|
187
|
-
[truncateDisplay(selected.degradedReason, Math.max(0, width - 11)), C.warn],
|
|
188
|
-
]),
|
|
189
|
-
] : []),
|
|
190
|
-
...(selected.lastError ? [
|
|
191
|
-
buildPanelLine(width, [
|
|
192
|
-
[' Error: ', C.label],
|
|
193
|
-
[truncateDisplay(selected.lastError, Math.max(0, width - 10)), C.error],
|
|
194
|
-
]),
|
|
195
|
-
] : []),
|
|
196
|
-
],
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
const resolvedWatchers = resolvePrimaryScrollableSection(width, height, {
|
|
200
|
-
intro,
|
|
201
|
-
footerLines: [buildPanelLine(width, [[' Up/Down move through watchers', C.dim]])],
|
|
202
|
-
palette: C,
|
|
203
|
-
beforeSections: [summarySection],
|
|
204
|
-
section: {
|
|
205
|
-
title: 'Watchers',
|
|
206
|
-
scrollableLines: watchers.map((watcher, absolute) => {
|
|
207
|
-
const bg = absolute === this.selectedIndex ? C.selectBg : undefined;
|
|
208
|
-
return buildPanelLine(width, [
|
|
209
|
-
[' ', C.label, bg],
|
|
210
|
-
[watcher.state.padEnd(10), stateColor(watcher.state), bg],
|
|
211
|
-
[` ${truncateDisplay(watcher.label, 18).padEnd(18)}`, C.value, bg],
|
|
212
|
-
[` ${String(watcher.sourceStatus ?? 'unknown').padEnd(10)}`, sourceStatusColor(watcher.sourceStatus), bg],
|
|
213
|
-
[` ${truncateDisplay(formatLag(watcher.sourceLagMs), Math.max(0, width - 43))}`, C.dim, bg],
|
|
214
|
-
]);
|
|
215
|
-
}),
|
|
216
|
-
selectedIndex: this.selectedIndex,
|
|
217
|
-
scrollOffset: this.scrollOffset,
|
|
218
|
-
guardRows: 1,
|
|
219
|
-
minRows: 5,
|
|
220
|
-
appendWindowSummary: { dimColor: C.dim },
|
|
221
|
-
},
|
|
222
|
-
afterSections: [detailSection],
|
|
223
|
-
});
|
|
224
|
-
this.scrollOffset = resolvedWatchers.scrollOffset;
|
|
225
|
-
|
|
226
|
-
const sections: PanelWorkspaceSection[] = [
|
|
227
|
-
summarySection,
|
|
228
|
-
resolvedWatchers.section,
|
|
229
|
-
detailSection,
|
|
146
|
+
const footerLines: Line[] = [
|
|
147
|
+
buildPanelLine(width, [
|
|
148
|
+
[' Watcher: ', C.label],
|
|
149
|
+
[selected.label, C.value],
|
|
150
|
+
[' Kind: ', C.label],
|
|
151
|
+
[selected.kind, C.info],
|
|
152
|
+
]),
|
|
153
|
+
buildPanelLine(width, [
|
|
154
|
+
[' State: ', C.label],
|
|
155
|
+
[selected.state, stateColor(selected.state)],
|
|
156
|
+
[' Source: ', C.label],
|
|
157
|
+
[selected.source.kind, C.value],
|
|
158
|
+
]),
|
|
159
|
+
buildPanelLine(width, [
|
|
160
|
+
[' Source status: ', C.label],
|
|
161
|
+
[selected.sourceStatus ?? 'unknown', sourceStatusColor(selected.sourceStatus)],
|
|
162
|
+
[' Lag: ', C.label],
|
|
163
|
+
[formatLag(selected.sourceLagMs), selected.sourceLagMs ? C.warn : C.dim],
|
|
164
|
+
]),
|
|
165
|
+
buildPanelLine(width, [
|
|
166
|
+
[' Heartbeat: ', C.label],
|
|
167
|
+
[formatTime(selected.lastHeartbeatAt), C.dim],
|
|
168
|
+
[' Checkpoint: ', C.label],
|
|
169
|
+
[truncateDisplay(selected.lastCheckpoint ?? 'n/a', Math.max(0, width - 38)), C.dim],
|
|
170
|
+
]),
|
|
230
171
|
];
|
|
231
|
-
|
|
172
|
+
if (selected.degradedReason) {
|
|
173
|
+
footerLines.push(buildPanelLine(width, [
|
|
174
|
+
[' Reason: ', C.label],
|
|
175
|
+
[truncateDisplay(selected.degradedReason, Math.max(0, width - 11)), C.warn],
|
|
176
|
+
]));
|
|
177
|
+
}
|
|
178
|
+
if (selected.lastError) {
|
|
179
|
+
footerLines.push(buildPanelLine(width, [
|
|
180
|
+
[' Error: ', C.label],
|
|
181
|
+
[truncateDisplay(selected.lastError, Math.max(0, width - 10)), C.error],
|
|
182
|
+
]));
|
|
183
|
+
}
|
|
184
|
+
footerLines.push(buildPanelLine(width, [[' Up/Down move through watchers', C.dim]]));
|
|
185
|
+
|
|
186
|
+
return this.renderList(width, height, {
|
|
232
187
|
title: 'Watchers',
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
footerLines: [buildPanelLine(width, [[' Up/Down move through watchers', C.dim]])],
|
|
236
|
-
palette: C,
|
|
188
|
+
header: headerLines,
|
|
189
|
+
footer: footerLines,
|
|
237
190
|
});
|
|
238
|
-
while (lines.length < height) lines.push(createEmptyLine(width));
|
|
239
|
-
return lines.slice(0, height);
|
|
240
191
|
}
|
|
241
192
|
}
|