@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
@@ -2,11 +2,12 @@ import type { Line } from '../types/grid.ts';
2
2
  import { createEmptyLine } from '../types/grid.ts';
3
3
  import { truncateDisplay } from '../utils/terminal-width.ts';
4
4
  import { ScrollableListPanel } from './scrollable-list-panel.ts';
5
- import type { RuntimeTask, TaskLifecycleState } from '@/runtime/index.ts';
5
+ import type { OpsApi, RuntimeTask, TaskLifecycleState } from '@/runtime/index.ts';
6
6
  import type { ManagedWorktreeMeta } from '@/runtime/index.ts';
7
7
  import type { UiReadModel, UiTasksSnapshot, UiWorktreeSnapshot } from '../runtime/ui-read-models.ts';
8
+ import { type ConfirmState, handleConfirmInput, renderConfirmLines } from './confirm-state.ts';
9
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
8
10
  import {
9
- buildDetailBlock,
10
11
  buildEmptyState,
11
12
  buildGuidanceLine,
12
13
  buildKeyboardHints,
@@ -14,24 +15,26 @@ import {
14
15
  buildPanelLine,
15
16
  buildSummaryBlock,
16
17
  buildPanelWorkspace,
18
+ resolveScrollablePanelSection,
17
19
  DEFAULT_PANEL_PALETTE,
20
+ extendPalette,
18
21
  } from './polish.ts';
19
22
  import { formatElapsed } from '../utils/format-elapsed.ts';
20
23
 
21
- const C = {
22
- ...DEFAULT_PANEL_PALETTE,
23
- header: '#94a3b8',
24
- headerBg: '#1e293b',
25
- queued: '#38bdf8',
26
- running: '#22c55e',
27
- blocked: '#f59e0b',
28
- failed: '#ef4444',
29
- completed: '#a78bfa',
30
- selectBg: '#0f172a',
31
- } as const;
24
+ // Domain accents only; base chrome (header/headerBg/info/good/warn/bad/
25
+ // selectBg) comes from DEFAULT_PANEL_PALETTE.
26
+ const C = extendPalette(DEFAULT_PANEL_PALETTE, {
27
+ completed: '#a78bfa', // completed-task badge — distinct from running/good
28
+ } as const);
32
29
 
33
30
  const STATUS_ORDER: TaskLifecycleState[] = ['queued', 'running', 'blocked', 'failed', 'completed'];
34
31
 
32
+ /** The follow-up action Enter dispatches once the panel-integration router runs, per WO-131. */
33
+ type TaskFollowUp =
34
+ | { readonly kind: 'agent-jump'; readonly agentId: string }
35
+ | { readonly kind: 'worktree-review'; readonly taskId: string }
36
+ | { readonly kind: 'teamwork-review' };
37
+
35
38
  function formatWhen(value?: number): string {
36
39
  return value ? new Date(value).toLocaleString() : 'n/a';
37
40
  }
@@ -57,13 +60,13 @@ function kindLabel(kind: RuntimeTask['kind']): string {
57
60
  function statusColor(status: TaskLifecycleState): string {
58
61
  switch (status) {
59
62
  case 'queued':
60
- return C.queued;
63
+ return C.info;
61
64
  case 'running':
62
- return C.running;
65
+ return C.good;
63
66
  case 'blocked':
64
- return C.blocked;
67
+ return C.warn;
65
68
  case 'failed':
66
- return C.failed;
69
+ return C.bad;
67
70
  case 'completed':
68
71
  return C.completed;
69
72
  case 'cancelled':
@@ -146,28 +149,39 @@ function reviewTaskWorktreeAttachments(
146
149
  export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
147
150
  private readonly readModel?: UiReadModel<UiTasksSnapshot>;
148
151
  private readonly worktrees?: UiReadModel<UiWorktreeSnapshot>;
152
+ private readonly opsApi?: OpsApi;
149
153
  private readonly unsubscribers: readonly (() => void)[];
150
154
 
155
+ /** Task id currently expanded into the detail view, or null while browsing the list. */
156
+ private _detailTaskId: string | null = null;
157
+ private _detailScroll = 0;
158
+
159
+ /** Pending cancel confirmation — subject is the task id being cancelled. */
160
+ private _confirm: ConfirmState<string> | null = null;
161
+
162
+ /** Set by handleInput on Enter; consumed by the panel-integration router immediately after. */
163
+ private _pendingFollowUp: TaskFollowUp | null = null;
164
+
151
165
  public constructor(
152
166
  readModel: UiReadModel<UiTasksSnapshot> | undefined,
153
167
  worktrees?: UiReadModel<UiWorktreeSnapshot>,
168
+ opsApi?: OpsApi,
154
169
  ) {
155
- super('tasks', 'Tasks', 'J', 'monitoring');
170
+ super('tasks', 'Tasks', '', 'runtime-ops');
156
171
  this.showSelectionGutter = true; // I5: non-color selection affordance
157
172
  this.filterEnabled = true;
158
173
  this.filterLabel = 'Filter tasks';
159
174
  this.readModel = readModel;
160
175
  this.worktrees = worktrees;
176
+ this.opsApi = opsApi;
161
177
  this.unsubscribers = [
162
178
  readModel?.subscribe(() => this.markDirty()),
163
179
  worktrees?.subscribe(() => this.markDirty()),
164
180
  ].filter((unsubscribe): unsubscribe is () => void => Boolean(unsubscribe));
165
181
  }
166
182
 
167
- public override onActivate(): void {
168
- super.onActivate();
169
- this.selectedIndex = 0;
170
- }
183
+ // WO-131: selection is preserved across onActivate BasePanel's default
184
+ // (mark dirty, no index reset) is exactly what we want, so no override.
171
185
 
172
186
  public override onDestroy(): void {
173
187
  for (const unsubscribe of this.unsubscribers) unsubscribe();
@@ -203,27 +217,179 @@ export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
203
217
  ], C, { selected });
204
218
  }
205
219
 
220
+ // ---------------------------------------------------------------------------
221
+ // Input
222
+ // ---------------------------------------------------------------------------
223
+
206
224
  public handleInput(key: string): boolean {
225
+ if (this.lastError !== null) this.clearError();
226
+
227
+ if (this._confirm) {
228
+ const outcome = handleConfirmInput(this._confirm, key);
229
+ if (outcome === 'confirmed') {
230
+ this._executeCancel(this._confirm.subject);
231
+ this._confirm = null;
232
+ this.markDirty();
233
+ return true;
234
+ }
235
+ if (outcome === 'cancelled') {
236
+ this._confirm = null;
237
+ this.markDirty();
238
+ return true;
239
+ }
240
+ return true; // absorbed — keep the confirm dialog pending
241
+ }
242
+
243
+ const tasks = this.getVisibleItems();
244
+
245
+ // --- Detail mode: WO-131 moves the deep per-task field dump behind an
246
+ // Enter-toggled view so the list itself keeps full viewport in normal
247
+ // browsing. Esc/Left collapses back to the list; Enter (again, while
248
+ // already expanded) dispatches the task's advertised worktree follow-up.
249
+ if (this._detailTaskId !== null) {
250
+ if (key === 'escape' || key === 'left') {
251
+ this._detailTaskId = null;
252
+ this._detailScroll = 0;
253
+ this.markDirty();
254
+ return true;
255
+ }
256
+ if (key === 'enter' || key === 'return') {
257
+ const task = tasks.find((t) => t.id === this._detailTaskId);
258
+ if (task) this._pendingFollowUp = { kind: 'worktree-review', taskId: task.id };
259
+ return true;
260
+ }
261
+ switch (key) {
262
+ case 'up':
263
+ case 'k':
264
+ this._detailScroll = Math.max(0, this._detailScroll - 1);
265
+ this.markDirty();
266
+ return true;
267
+ case 'down':
268
+ case 'j':
269
+ this._detailScroll += 1;
270
+ this.markDirty();
271
+ return true;
272
+ case 'pageup':
273
+ this._detailScroll = Math.max(0, this._detailScroll - this.getPageSize());
274
+ this.markDirty();
275
+ return true;
276
+ case 'pagedown':
277
+ this._detailScroll += this.getPageSize();
278
+ this.markDirty();
279
+ return true;
280
+ case 'home':
281
+ case 'g':
282
+ this._detailScroll = 0;
283
+ this.markDirty();
284
+ return true;
285
+ default:
286
+ return false;
287
+ }
288
+ }
289
+
290
+ // --- List mode ---
207
291
  if (key === 'home') {
208
292
  this.selectedIndex = 0;
209
293
  this.markDirty();
210
294
  return true;
211
295
  }
212
296
  if (key === 'end') {
213
- const tasks = this.getVisibleItems();
214
297
  this.selectedIndex = Math.max(0, tasks.length - 1);
215
298
  this.markDirty();
216
299
  return true;
217
300
  }
301
+
302
+ if (key === 'c' && !this.filterActive) {
303
+ const task = this.getSelectedItem();
304
+ if (!task || !task.cancellable) return false;
305
+ this._confirm = { subject: task.id, label: `task "${task.title}"`, verb: 'Cancel' };
306
+ this.markDirty();
307
+ return true;
308
+ }
309
+
310
+ // The task-family posture surface is bound for real: w stages a follow-up
311
+ // the integration router dispatches as '/teamwork review' via executeCommand.
312
+ if (key === 'w' && !this.filterActive) {
313
+ this._pendingFollowUp = { kind: 'teamwork-review' };
314
+ return true;
315
+ }
316
+
317
+ // Enter commits the filter query while actively typing (ScrollableListPanel
318
+ // contract) — only treated as detail-mode/agent-jump once filtering isn't active.
319
+ if ((key === 'enter' || key === 'return') && !this.filterActive) {
320
+ const task = this.getSelectedItem();
321
+ if (!task) return false;
322
+ // Agent-kind tasks jump straight to the Agent Inspector — that panel
323
+ // already owns the deep per-agent timeline, so Tasks does not duplicate it.
324
+ if (task.kind === 'agent') {
325
+ this._pendingFollowUp = { kind: 'agent-jump', agentId: task.owner };
326
+ return true;
327
+ }
328
+ this._detailTaskId = task.id;
329
+ this._detailScroll = 0;
330
+ this.markDirty();
331
+ return true;
332
+ }
333
+
218
334
  return super.handleInput(key);
219
335
  }
220
336
 
337
+ /**
338
+ * Consumed by the panel-integration router immediately after handleInput
339
+ * returns true for the same Enter keystroke. Returns and clears whatever
340
+ * follow-up (if any) that keystroke queued.
341
+ */
342
+ public consumePendingFollowUp(): TaskFollowUp | null {
343
+ const pending = this._pendingFollowUp;
344
+ this._pendingFollowUp = null;
345
+ return pending;
346
+ }
347
+
348
+ private _executeCancel(taskId: string): void {
349
+ if (!this.opsApi) {
350
+ this.setError('Ops API is not wired for this runtime.');
351
+ return;
352
+ }
353
+ try {
354
+ this.opsApi.tasks.cancel(taskId);
355
+ } catch (err) {
356
+ this.setError(summarizeError(err));
357
+ }
358
+ }
359
+
360
+ // ---------------------------------------------------------------------------
361
+ // Render
362
+ // ---------------------------------------------------------------------------
363
+
221
364
  public render(width: number, height: number): Line[] {
222
365
  this.clampSelection();
366
+
367
+ if (this._detailTaskId !== null) {
368
+ const task = this.getVisibleItems().find((t) => t.id === this._detailTaskId);
369
+ if (task) return this._renderDetail(width, height, task);
370
+ this._detailTaskId = null; // task vanished (pruned/completed away) — fall back to list
371
+ }
372
+
373
+ return this._renderListView(width, height);
374
+ }
375
+
376
+ private _renderListView(width: number, height: number): Line[] {
223
377
  const intro = 'Live task lifecycle, ownership, retries, and result/error details across runtime execution domains.';
224
- const visibleCount = this.getVisibleItems().length;
378
+ const visibleTasks = this.getVisibleItems();
379
+ const selected = this.getSelectedItem();
225
380
  // Context-aware footer: position + only the keys that apply in the current
226
- // (filtering vs browsing) state.
381
+ // (filtering vs browsing) state, and to the selected task's kind/cancellable flag.
382
+ const browseHints: Array<{ keys: string; label: string }> = [
383
+ { keys: visibleTasks.length > 0 ? `${this.selectedIndex + 1}/${visibleTasks.length}` : '0/0', label: 'task' },
384
+ { keys: '↑/↓', label: 'move' },
385
+ { keys: 'Home/End', label: 'jump' },
386
+ { keys: '/', label: 'filter' },
387
+ { keys: 'w', label: 'teamwork review' },
388
+ ];
389
+ if (selected) {
390
+ browseHints.push({ keys: 'Enter', label: selected.kind === 'agent' ? 'agent detail' : 'task detail' });
391
+ if (selected.cancellable) browseHints.push({ keys: 'c', label: 'cancel' });
392
+ }
227
393
  const footerLines = [
228
394
  this.filterActive
229
395
  ? buildKeyboardHints(width, [
@@ -231,12 +397,7 @@ export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
231
397
  { keys: 'Enter', label: 'apply' },
232
398
  { keys: 'Esc', label: 'clear' },
233
399
  ], C)
234
- : buildKeyboardHints(width, [
235
- { keys: visibleCount > 0 ? `${this.selectedIndex + 1}/${visibleCount}` : '0/0', label: 'task' },
236
- { keys: '↑/↓', label: 'move' },
237
- { keys: 'Home/End', label: 'jump' },
238
- { keys: '/', label: 'filter' },
239
- ], C),
400
+ : buildKeyboardHints(width, browseHints, C),
240
401
  ];
241
402
 
242
403
  if (!this.readModel) {
@@ -272,19 +433,18 @@ export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
272
433
  const postureLines: Line[] = [
273
434
  buildPanelLine(width, [
274
435
  [' queued ', C.label],
275
- [String(queuedCount), queuedCount > 0 ? C.queued : C.dim],
436
+ [String(queuedCount), queuedCount > 0 ? C.info : C.dim],
276
437
  [' running ', C.label],
277
- [String(runningCount), runningCount > 0 ? C.running : C.dim],
438
+ [String(runningCount), runningCount > 0 ? C.good : C.dim],
278
439
  [' blocked ', C.label],
279
- [String(blockedCount), blockedCount > 0 ? C.blocked : C.dim],
440
+ [String(blockedCount), blockedCount > 0 ? C.warn : C.dim],
280
441
  [' failed ', C.label],
281
- [String(failedCount), failedCount > 0 ? C.failed : C.dim],
442
+ [String(failedCount), failedCount > 0 ? C.bad : C.dim],
282
443
  [' completed ', C.label],
283
444
  [String(completedCount), completedCount > 0 ? C.completed : C.dim],
284
445
  ]),
285
446
  ];
286
447
 
287
- const selected = tasks[this.selectedIndex];
288
448
  if (selected) {
289
449
  postureLines.push(buildPanelLine(width, [
290
450
  [' selected ', C.label],
@@ -298,124 +458,151 @@ export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
298
458
  ]));
299
459
  }
300
460
  postureLines.push(
301
- buildGuidanceLine(width, '/teamwork review', 'inspect task-family posture, archetype metadata, and recovery options for active work', C),
302
- buildGuidanceLine(width, '/worktree task <task-id>', 'review worktree ownership, restore, and merge posture for the selected task', C),
461
+ buildGuidanceLine(width, '/teamwork review', 'inspect task-family posture, archetype metadata, and recovery options for active work — press w to run it', C),
303
462
  );
304
463
 
464
+ const headerLines: Line[] = buildSummaryBlock(width, 'Task posture', postureLines, C);
465
+
466
+ const effectiveFooter = this._confirm
467
+ ? [...renderConfirmLines(width, this._confirm), ...footerLines]
468
+ : footerLines;
469
+
470
+ return this.renderList(width, height, {
471
+ title: 'Task Control Room',
472
+ header: headerLines,
473
+ footer: effectiveFooter,
474
+ });
475
+ }
476
+
477
+ /** Detail-mode body for one task — the deep field dump previously always shown inline. */
478
+ private _renderDetail(width: number, height: number, task: RuntimeTask): Line[] {
479
+ this.needsRender = false;
480
+ const descriptor = task.description ? parseTaskDescriptor(task.description) : null;
305
481
  const detailRows: Line[] = [];
306
- if (selected) {
307
- const descriptor = selected.description ? parseTaskDescriptor(selected.description) : null;
482
+ detailRows.push(buildPanelLine(width, [
483
+ [' Title: ', C.label],
484
+ [task.title, C.value],
485
+ [' Status: ', C.label],
486
+ [task.status, statusColor(task.status)],
487
+ [' Kind: ', C.label],
488
+ [task.kind, C.value],
489
+ ]));
490
+ detailRows.push(buildPanelLine(width, [
491
+ [' Owner: ', C.label],
492
+ [task.owner, C.value],
493
+ [' Cancellable: ', C.label],
494
+ [task.cancellable ? 'yes' : 'no', task.cancellable ? C.good : C.bad],
495
+ [' Queue: ', C.label],
496
+ [formatWhen(task.queuedAt), C.dim],
497
+ ]));
498
+ detailRows.push(buildPanelLine(width, [
499
+ [' Started: ', C.label],
500
+ [formatWhen(task.startedAt), C.dim],
501
+ [' Ended: ', C.label],
502
+ [formatWhen(task.endedAt), C.dim],
503
+ [' Duration: ', C.label],
504
+ [formatDuration(task.startedAt, task.endedAt), C.dim],
505
+ ]));
506
+ if (descriptor?.mode || descriptor?.family || descriptor?.source) {
308
507
  detailRows.push(buildPanelLine(width, [
309
- [' Title: ', C.label],
310
- [selected.title, C.value],
311
- [' Status: ', C.label],
312
- [selected.status, statusColor(selected.status)],
313
- [' Kind: ', C.label],
314
- [selected.kind, C.value],
508
+ [' Mode: ', C.label],
509
+ [descriptor?.mode ?? 'n/a', C.value],
510
+ [' Family: ', C.label],
511
+ [descriptor?.family ?? 'n/a', C.info],
512
+ [' Source: ', C.label],
513
+ [descriptor?.source ?? 'builtin/runtime', C.dim],
315
514
  ]));
515
+ }
516
+ if (descriptor?.reviewMode || descriptor?.executionProtocol || descriptor?.template) {
316
517
  detailRows.push(buildPanelLine(width, [
317
- [' Owner: ', C.label],
318
- [selected.owner, C.value],
319
- [' Cancellable: ', C.label],
320
- [selected.cancellable ? 'yes' : 'no', selected.cancellable ? C.running : C.failed],
321
- [' Queue: ', C.label],
322
- [formatWhen(selected.queuedAt), C.dim],
518
+ [' Review: ', C.label],
519
+ [descriptor?.reviewMode ?? 'n/a', C.value],
520
+ [' Protocol: ', C.label],
521
+ [descriptor?.executionProtocol ?? 'n/a', C.value],
522
+ [' Template: ', C.label],
523
+ [descriptor?.template ?? 'n/a', C.dim],
323
524
  ]));
525
+ }
526
+ if (task.correlationId || task.turnId) {
324
527
  detailRows.push(buildPanelLine(width, [
325
- [' Started: ', C.label],
326
- [formatWhen(selected.startedAt), C.dim],
327
- [' Ended: ', C.label],
328
- [formatWhen(selected.endedAt), C.dim],
329
- [' Duration: ', C.label],
330
- [formatDuration(selected.startedAt, selected.endedAt), C.dim],
528
+ [' Correlation: ', C.label],
529
+ [task.correlationId ?? 'n/a', C.dim],
530
+ [' Turn: ', C.label],
531
+ [task.turnId ?? 'n/a', C.dim],
331
532
  ]));
332
- if (descriptor?.mode || descriptor?.family || descriptor?.source) {
333
- detailRows.push(buildPanelLine(width, [
334
- [' Mode: ', C.label],
335
- [descriptor?.mode ?? 'n/a', C.value],
336
- [' Family: ', C.label],
337
- [descriptor?.family ?? 'n/a', C.info],
338
- [' Source: ', C.label],
339
- [descriptor?.source ?? 'builtin/runtime', C.dim],
340
- ]));
341
- }
342
- if (descriptor?.reviewMode || descriptor?.executionProtocol || descriptor?.template) {
343
- detailRows.push(buildPanelLine(width, [
344
- [' Review: ', C.label],
345
- [descriptor?.reviewMode ?? 'n/a', C.value],
346
- [' Protocol: ', C.label],
347
- [descriptor?.executionProtocol ?? 'n/a', C.value],
348
- [' Template: ', C.label],
349
- [descriptor?.template ?? 'n/a', C.dim],
350
- ]));
351
- }
352
- if (selected.correlationId || selected.turnId) {
353
- detailRows.push(buildPanelLine(width, [
354
- [' Correlation: ', C.label],
355
- [selected.correlationId ?? 'n/a', C.dim],
356
- [' Turn: ', C.label],
357
- [selected.turnId ?? 'n/a', C.dim],
358
- ]));
359
- }
360
- if (selected.parentTaskId || selected.childTaskIds.length > 0) {
361
- detailRows.push(buildPanelLine(width, [
362
- [' Parent: ', C.label],
363
- [selected.parentTaskId ?? 'none', C.dim],
364
- [' Children: ', C.label],
365
- [selected.childTaskIds.length > 0 ? selected.childTaskIds.join(', ') : 'none', C.dim],
366
- ]));
367
- }
368
- const attachedWorktrees = reviewTaskWorktreeAttachments(selected.id, this.worktrees);
369
- if (attachedWorktrees.total > 0) {
370
- detailRows.push(buildPanelLine(width, [
371
- [' Worktrees: ', C.label],
372
- [`${attachedWorktrees.total} tracked`, C.info],
373
- [' Active: ', C.label],
374
- [String(attachedWorktrees.active), attachedWorktrees.active > 0 ? C.running : C.dim],
375
- [' Paused: ', C.label],
376
- [String(attachedWorktrees.paused), attachedWorktrees.paused > 0 ? C.blocked : C.dim],
377
- ]));
533
+ }
534
+ if (task.parentTaskId || task.childTaskIds.length > 0) {
535
+ detailRows.push(buildPanelLine(width, [
536
+ [' Parent: ', C.label],
537
+ [task.parentTaskId ?? 'none', C.dim],
538
+ [' Children: ', C.label],
539
+ [task.childTaskIds.length > 0 ? task.childTaskIds.join(', ') : 'none', C.dim],
540
+ ]));
541
+ }
542
+ const attachedWorktrees = reviewTaskWorktreeAttachments(task.id, this.worktrees);
543
+ if (attachedWorktrees.total > 0) {
544
+ detailRows.push(buildPanelLine(width, [
545
+ [' Worktrees: ', C.label],
546
+ [`${attachedWorktrees.total} tracked`, C.info],
547
+ [' Active: ', C.label],
548
+ [String(attachedWorktrees.active), attachedWorktrees.active > 0 ? C.good : C.dim],
549
+ [' Paused: ', C.label],
550
+ [String(attachedWorktrees.paused), attachedWorktrees.paused > 0 ? C.warn : C.dim],
551
+ ]));
552
+ for (const record of attachedWorktrees.records.slice(0, 2)) {
378
553
  detailRows.push(buildPanelLine(width, [[
379
- ` Next: /worktree task ${selected.id} /worktree recover task ${selected.id}`,
380
- C.dim,
554
+ truncateDisplay(` ${record.state.padEnd(15)} ${record.path}`, Math.max(0, width - 2)),
555
+ record.state === 'active' ? C.good : record.state === 'paused' ? C.warn : C.dim,
381
556
  ]]));
382
- for (const record of attachedWorktrees.records.slice(0, 2)) {
383
- detailRows.push(buildPanelLine(width, [[
384
- truncateDisplay(` ${record.state.padEnd(15)} ${record.path}`, Math.max(0, width - 2)),
385
- record.state === 'active' ? C.running : record.state === 'paused' ? C.blocked : C.dim,
386
- ]]));
387
- }
388
- }
389
- if (selected.retryPolicy) {
390
- detailRows.push(buildPanelLine(width, [
391
- [' Retry: ', C.label],
392
- [`${selected.retryPolicy.currentAttempt}/${selected.retryPolicy.maxAttempts} ${selected.retryPolicy.backoff}`, C.value],
393
- ]));
394
- }
395
- if (selected.error) {
396
- detailRows.push(buildPanelLine(width, [
397
- [' Error: ', C.label],
398
- [truncateDisplay(selected.error, Math.max(0, width - 10)), C.failed],
399
- ]));
400
- }
401
- if (selected.result !== undefined) {
402
- const resultText = safeJson(selected.result);
403
- detailRows.push(buildPanelLine(width, [
404
- [' Result: ', C.label],
405
- [truncateDisplay(resultText, Math.max(0, width - 11)), C.dim],
406
- ]));
407
557
  }
408
558
  }
559
+ if (task.retryPolicy) {
560
+ detailRows.push(buildPanelLine(width, [
561
+ [' Retry: ', C.label],
562
+ [`${task.retryPolicy.currentAttempt}/${task.retryPolicy.maxAttempts} ${task.retryPolicy.backoff}`, C.value],
563
+ ]));
564
+ }
565
+ if (task.error) {
566
+ detailRows.push(buildPanelLine(width, [
567
+ [' Error: ', C.label],
568
+ [truncateDisplay(task.error, Math.max(0, width - 10)), C.bad],
569
+ ]));
570
+ }
571
+ if (task.result !== undefined) {
572
+ const resultText = safeJson(task.result);
573
+ detailRows.push(buildPanelLine(width, [
574
+ [' Result: ', C.label],
575
+ [truncateDisplay(resultText, Math.max(0, width - 11)), C.dim],
576
+ ]));
577
+ }
409
578
 
410
- const headerLines: Line[] = buildSummaryBlock(width, 'Task posture', postureLines, C);
579
+ // The actionable follow-up replaces the old printed "/worktree task <task-id>"
580
+ // signpost — pressing Enter here dispatches it for real via ctx.executeCommand.
581
+ detailRows.push(buildGuidanceLine(width, `/worktree task ${task.id}`, 'review worktree ownership, restore, and merge posture for this task — press Enter to run it', C));
411
582
 
412
- return this.renderList(width, height, {
413
- title: 'Task Control Room',
414
- header: headerLines,
415
- footer: [
416
- ...buildDetailBlock(width, 'Selected task', detailRows, C),
417
- ...footerLines,
418
- ],
583
+ const footer = buildKeyboardHints(width, [
584
+ { keys: '↑/↓', label: 'scroll' },
585
+ { keys: 'Enter', label: 'review worktree ownership' },
586
+ { keys: 'Esc', label: 'back' },
587
+ ], C);
588
+
589
+ const resolved = resolveScrollablePanelSection(width, height, {
590
+ palette: C,
591
+ afterSections: [{ lines: [footer] }],
592
+ section: {
593
+ scrollableLines: detailRows,
594
+ scrollOffset: this._detailScroll,
595
+ appendWindowSummary: detailRows.length > 5 ? { dimColor: C.dim } : undefined,
596
+ },
597
+ });
598
+ this._detailScroll = resolved.scrollOffset;
599
+
600
+ const lines = buildPanelWorkspace(width, height, {
601
+ title: `Task · ${task.title}`,
602
+ sections: [resolved.section, { lines: [footer] }],
603
+ palette: C,
419
604
  });
605
+ while (lines.length < height) lines.push(createEmptyLine(width));
606
+ return lines.slice(0, height);
420
607
  }
421
608
  }