@pellux/goodvibes-tui 0.27.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 (143) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/context-usage.ts +53 -0
  5. package/src/core/conversation.ts +4 -6
  6. package/src/core/session-recovery.ts +2 -0
  7. package/src/core/turn-event-wiring.ts +1 -0
  8. package/src/input/commands/cost-runtime.ts +49 -0
  9. package/src/input/commands/eval.ts +4 -3
  10. package/src/input/commands/operator-runtime.ts +5 -1
  11. package/src/input/commands.ts +2 -0
  12. package/src/input/handler-feed-routes.ts +60 -26
  13. package/src/input/handler-feed.ts +17 -8
  14. package/src/input/handler-picker-routes.ts +18 -13
  15. package/src/input/handler-shortcuts.ts +51 -0
  16. package/src/input/handler-ui-state.ts +4 -0
  17. package/src/input/handler.ts +43 -6
  18. package/src/input/keybindings.ts +53 -8
  19. package/src/input/model-picker.ts +5 -0
  20. package/src/input/panel-integration-actions.ts +117 -1
  21. package/src/main.ts +5 -2
  22. package/src/panels/agent-inspector-panel.ts +139 -45
  23. package/src/panels/agent-inspector-shared.ts +147 -3
  24. package/src/panels/approval-panel.ts +204 -102
  25. package/src/panels/automation-control-panel.ts +370 -103
  26. package/src/panels/base-panel.ts +26 -2
  27. package/src/panels/builtin/agent.ts +28 -37
  28. package/src/panels/builtin/development.ts +40 -32
  29. package/src/panels/builtin/knowledge.ts +15 -6
  30. package/src/panels/builtin/operations.ts +178 -117
  31. package/src/panels/builtin/session.ts +35 -9
  32. package/src/panels/builtin/shared.ts +98 -6
  33. package/src/panels/cockpit-panel.ts +253 -75
  34. package/src/panels/cockpit-read-model.ts +2 -1
  35. package/src/panels/communication-panel.ts +159 -56
  36. package/src/panels/confirm-state.ts +8 -1
  37. package/src/panels/control-plane-panel.ts +354 -95
  38. package/src/panels/cost-tracker-panel.ts +243 -57
  39. package/src/panels/debug-panel.ts +347 -159
  40. package/src/panels/diff-panel.ts +257 -82
  41. package/src/panels/docs-panel.ts +166 -64
  42. package/src/panels/eval-panel.ts +336 -109
  43. package/src/panels/expandable-list-panel.ts +189 -0
  44. package/src/panels/file-explorer-panel.ts +238 -159
  45. package/src/panels/file-preview-panel.ts +141 -59
  46. package/src/panels/git-panel.ts +360 -191
  47. package/src/panels/hooks-panel.ts +181 -19
  48. package/src/panels/incident-review-panel.ts +252 -43
  49. package/src/panels/index.ts +0 -4
  50. package/src/panels/intelligence-panel.ts +310 -103
  51. package/src/panels/knowledge-graph-panel.ts +480 -58
  52. package/src/panels/local-auth-panel.ts +251 -33
  53. package/src/panels/marketplace-panel.ts +219 -33
  54. package/src/panels/memory-panel.ts +83 -81
  55. package/src/panels/ops-control-panel.ts +211 -32
  56. package/src/panels/ops-strategy-panel.ts +131 -45
  57. package/src/panels/orchestration-panel.ts +259 -67
  58. package/src/panels/panel-list-panel.ts +184 -136
  59. package/src/panels/panel-manager.ts +120 -13
  60. package/src/panels/plan-dashboard-panel.ts +507 -74
  61. package/src/panels/plugins-panel.ts +227 -34
  62. package/src/panels/policy-panel.ts +264 -55
  63. package/src/panels/polish-core.ts +162 -0
  64. package/src/panels/polish-tables.ts +258 -0
  65. package/src/panels/polish.ts +67 -149
  66. package/src/panels/project-planning-answer-actions.ts +134 -0
  67. package/src/panels/project-planning-panel.ts +84 -113
  68. package/src/panels/provider-health-panel.ts +438 -480
  69. package/src/panels/provider-health-routes.ts +203 -0
  70. package/src/panels/provider-health-tracker.ts +194 -6
  71. package/src/panels/provider-health-views.ts +560 -0
  72. package/src/panels/qr-panel.ts +136 -38
  73. package/src/panels/remote-panel.ts +123 -38
  74. package/src/panels/routes-panel.ts +87 -24
  75. package/src/panels/sandbox-panel.ts +232 -65
  76. package/src/panels/scrollable-list-panel.ts +143 -145
  77. package/src/panels/security-panel.ts +204 -52
  78. package/src/panels/services-panel.ts +174 -76
  79. package/src/panels/session-browser-panel.ts +101 -6
  80. package/src/panels/session-maintenance.ts +4 -122
  81. package/src/panels/settings-sync-panel.ts +346 -68
  82. package/src/panels/skills-panel.ts +157 -89
  83. package/src/panels/subscription-panel.ts +105 -34
  84. package/src/panels/symbol-outline-panel.ts +287 -154
  85. package/src/panels/system-messages-panel.ts +172 -38
  86. package/src/panels/tasks-panel.ts +348 -134
  87. package/src/panels/thinking-panel.ts +66 -32
  88. package/src/panels/token-budget-panel.ts +233 -46
  89. package/src/panels/tool-inspector-panel.ts +170 -54
  90. package/src/panels/types.ts +65 -4
  91. package/src/panels/work-plan-panel.ts +393 -39
  92. package/src/panels/worktree-panel.ts +239 -61
  93. package/src/panels/wrfc-panel-format.ts +133 -0
  94. package/src/panels/wrfc-panel.ts +146 -150
  95. package/src/renderer/compaction-preview.ts +2 -1
  96. package/src/renderer/compositor.ts +46 -43
  97. package/src/renderer/conversation-overlays.ts +25 -11
  98. package/src/renderer/footer-tips.ts +41 -0
  99. package/src/renderer/fullscreen-primitives.ts +22 -16
  100. package/src/renderer/help-overlay.ts +91 -14
  101. package/src/renderer/hint-grammar.ts +52 -0
  102. package/src/renderer/layout.ts +7 -7
  103. package/src/renderer/modal-factory.ts +23 -15
  104. package/src/renderer/model-picker-overlay.ts +30 -11
  105. package/src/renderer/model-workspace.ts +2 -3
  106. package/src/renderer/overlay-box.ts +16 -10
  107. package/src/renderer/panel-composite.ts +7 -20
  108. package/src/renderer/panel-workspace-bar.ts +14 -8
  109. package/src/renderer/process-indicator.ts +3 -1
  110. package/src/renderer/progress.ts +8 -6
  111. package/src/renderer/search-overlay.ts +27 -6
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal.ts +70 -10
  114. package/src/renderer/shell-surface.ts +41 -15
  115. package/src/renderer/status-glyphs.ts +11 -9
  116. package/src/renderer/tab-strip.ts +148 -34
  117. package/src/renderer/theme.ts +60 -3
  118. package/src/renderer/ui-factory.ts +45 -36
  119. package/src/renderer/ui-primitives.ts +23 -2
  120. package/src/runtime/bootstrap-shell.ts +35 -18
  121. package/src/runtime/diagnostics/panels/index.ts +0 -3
  122. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  123. package/src/runtime/ui/model-picker/types.ts +4 -0
  124. package/src/shell/ui-openers.ts +14 -22
  125. package/src/utils/format-duration.ts +55 -0
  126. package/src/utils/format-number.ts +71 -0
  127. package/src/utils/splash-lines.ts +44 -3
  128. package/src/version.ts +1 -1
  129. package/src/work-plans/work-plan-store.ts +13 -0
  130. package/src/panels/agent-logs-panel.ts +0 -666
  131. package/src/panels/agent-logs-shared.ts +0 -129
  132. package/src/panels/context-visualizer-panel.ts +0 -214
  133. package/src/panels/forensics-panel.ts +0 -364
  134. package/src/panels/panel-picker.ts +0 -106
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -218
  137. package/src/panels/provider-stats-panel.ts +0 -366
  138. package/src/panels/schedule-panel.ts +0 -342
  139. package/src/panels/watchers-panel.ts +0 -193
  140. package/src/renderer/panel-picker-overlay.ts +0 -202
  141. package/src/renderer/panel-tab-bar.ts +0 -69
  142. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  143. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -1,35 +1,40 @@
1
1
  import type { Line } from '../types/grid.ts';
2
2
  import { createEmptyLine } from '../types/grid.ts';
3
+ import { truncateDisplay } from '../utils/terminal-width.ts';
3
4
  import { ScrollableListPanel } from './scrollable-list-panel.ts';
4
- import type { RuntimeTask, TaskLifecycleState } from '@/runtime/index.ts';
5
+ import type { OpsApi, RuntimeTask, TaskLifecycleState } from '@/runtime/index.ts';
5
6
  import type { ManagedWorktreeMeta } from '@/runtime/index.ts';
6
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';
7
10
  import {
8
- buildDetailBlock,
9
11
  buildEmptyState,
10
12
  buildGuidanceLine,
13
+ buildKeyboardHints,
11
14
  buildPanelListRow,
12
15
  buildPanelLine,
13
16
  buildSummaryBlock,
14
17
  buildPanelWorkspace,
18
+ resolveScrollablePanelSection,
15
19
  DEFAULT_PANEL_PALETTE,
20
+ extendPalette,
16
21
  } from './polish.ts';
17
22
  import { formatElapsed } from '../utils/format-elapsed.ts';
18
23
 
19
- const C = {
20
- ...DEFAULT_PANEL_PALETTE,
21
- header: '#94a3b8',
22
- headerBg: '#1e293b',
23
- queued: '#38bdf8',
24
- running: '#22c55e',
25
- blocked: '#f59e0b',
26
- failed: '#ef4444',
27
- completed: '#a78bfa',
28
- selectBg: '#0f172a',
29
- } 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);
30
29
 
31
30
  const STATUS_ORDER: TaskLifecycleState[] = ['queued', 'running', 'blocked', 'failed', 'completed'];
32
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
+
33
38
  function formatWhen(value?: number): string {
34
39
  return value ? new Date(value).toLocaleString() : 'n/a';
35
40
  }
@@ -55,13 +60,13 @@ function kindLabel(kind: RuntimeTask['kind']): string {
55
60
  function statusColor(status: TaskLifecycleState): string {
56
61
  switch (status) {
57
62
  case 'queued':
58
- return C.queued;
63
+ return C.info;
59
64
  case 'running':
60
- return C.running;
65
+ return C.good;
61
66
  case 'blocked':
62
- return C.blocked;
67
+ return C.warn;
63
68
  case 'failed':
64
- return C.failed;
69
+ return C.bad;
65
70
  case 'completed':
66
71
  return C.completed;
67
72
  case 'cancelled':
@@ -144,26 +149,39 @@ function reviewTaskWorktreeAttachments(
144
149
  export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
145
150
  private readonly readModel?: UiReadModel<UiTasksSnapshot>;
146
151
  private readonly worktrees?: UiReadModel<UiWorktreeSnapshot>;
152
+ private readonly opsApi?: OpsApi;
147
153
  private readonly unsubscribers: readonly (() => void)[];
148
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
+
149
165
  public constructor(
150
166
  readModel: UiReadModel<UiTasksSnapshot> | undefined,
151
167
  worktrees?: UiReadModel<UiWorktreeSnapshot>,
168
+ opsApi?: OpsApi,
152
169
  ) {
153
- super('tasks', 'Tasks', 'J', 'monitoring');
170
+ super('tasks', 'Tasks', '', 'runtime-ops');
154
171
  this.showSelectionGutter = true; // I5: non-color selection affordance
172
+ this.filterEnabled = true;
173
+ this.filterLabel = 'Filter tasks';
155
174
  this.readModel = readModel;
156
175
  this.worktrees = worktrees;
176
+ this.opsApi = opsApi;
157
177
  this.unsubscribers = [
158
178
  readModel?.subscribe(() => this.markDirty()),
159
179
  worktrees?.subscribe(() => this.markDirty()),
160
180
  ].filter((unsubscribe): unsubscribe is () => void => Boolean(unsubscribe));
161
181
  }
162
182
 
163
- public override onActivate(): void {
164
- super.onActivate();
165
- this.selectedIndex = 0;
166
- }
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.
167
185
 
168
186
  public override onDestroy(): void {
169
187
  for (const unsubscribe of this.unsubscribers) unsubscribe();
@@ -178,6 +196,13 @@ export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
178
196
  ];
179
197
  }
180
198
 
199
+ protected override filterMatches(task: RuntimeTask, q: string): boolean {
200
+ return task.title.toLowerCase().includes(q)
201
+ || task.status.toLowerCase().includes(q)
202
+ || task.id.toLowerCase().includes(q)
203
+ || String(task.kind).toLowerCase().includes(q);
204
+ }
205
+
181
206
  protected getItems(): readonly RuntimeTask[] {
182
207
  if (!this.readModel) return [];
183
208
  return sortTasks([...this.readModel.getSnapshot().tasks]);
@@ -188,29 +213,192 @@ export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
188
213
  { text: task.status.padEnd(10), fg: statusColor(task.status) },
189
214
  { text: ` ${kindLabel(task.kind).padEnd(12)}`, fg: C.value },
190
215
  { text: ` ${task.id.slice(0, 8)} `, fg: C.dim },
191
- { text: task.title.slice(0, Math.max(0, width - 37)), fg: C.value },
216
+ { text: truncateDisplay(task.title, Math.max(0, width - 37)), fg: C.value },
192
217
  ], C, { selected });
193
218
  }
194
219
 
220
+ // ---------------------------------------------------------------------------
221
+ // Input
222
+ // ---------------------------------------------------------------------------
223
+
195
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 ---
196
291
  if (key === 'home') {
197
292
  this.selectedIndex = 0;
198
293
  this.markDirty();
199
294
  return true;
200
295
  }
201
296
  if (key === 'end') {
202
- const tasks = this.getItems();
203
297
  this.selectedIndex = Math.max(0, tasks.length - 1);
204
298
  this.markDirty();
205
299
  return true;
206
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
+
207
334
  return super.handleInput(key);
208
335
  }
209
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
+
210
364
  public render(width: number, height: number): Line[] {
211
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[] {
212
377
  const intro = 'Live task lifecycle, ownership, retries, and result/error details across runtime execution domains.';
213
- const footerLines = [buildPanelLine(width, [[' Up/Down move Home/End jump', C.dim]])];
378
+ const visibleTasks = this.getVisibleItems();
379
+ const selected = this.getSelectedItem();
380
+ // Context-aware footer: position + only the keys that apply in the current
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
+ }
393
+ const footerLines = [
394
+ this.filterActive
395
+ ? buildKeyboardHints(width, [
396
+ { keys: 'type', label: 'filter tasks' },
397
+ { keys: 'Enter', label: 'apply' },
398
+ { keys: 'Esc', label: 'clear' },
399
+ ], C)
400
+ : buildKeyboardHints(width, browseHints, C),
401
+ ];
214
402
 
215
403
  if (!this.readModel) {
216
404
  const workspace = buildPanelWorkspace(width, height, {
@@ -245,19 +433,18 @@ export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
245
433
  const postureLines: Line[] = [
246
434
  buildPanelLine(width, [
247
435
  [' queued ', C.label],
248
- [String(queuedCount), queuedCount > 0 ? C.queued : C.dim],
436
+ [String(queuedCount), queuedCount > 0 ? C.info : C.dim],
249
437
  [' running ', C.label],
250
- [String(runningCount), runningCount > 0 ? C.running : C.dim],
438
+ [String(runningCount), runningCount > 0 ? C.good : C.dim],
251
439
  [' blocked ', C.label],
252
- [String(blockedCount), blockedCount > 0 ? C.blocked : C.dim],
440
+ [String(blockedCount), blockedCount > 0 ? C.warn : C.dim],
253
441
  [' failed ', C.label],
254
- [String(failedCount), failedCount > 0 ? C.failed : C.dim],
442
+ [String(failedCount), failedCount > 0 ? C.bad : C.dim],
255
443
  [' completed ', C.label],
256
444
  [String(completedCount), completedCount > 0 ? C.completed : C.dim],
257
445
  ]),
258
446
  ];
259
447
 
260
- const selected = tasks[this.selectedIndex];
261
448
  if (selected) {
262
449
  postureLines.push(buildPanelLine(width, [
263
450
  [' selected ', C.label],
@@ -267,128 +454,155 @@ export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
267
454
  [' kind ', C.label],
268
455
  [selected.kind, C.value],
269
456
  [' owner ', C.label],
270
- [selected.owner.slice(0, Math.max(0, width - 46)), C.dim],
457
+ [truncateDisplay(selected.owner, Math.max(0, width - 46)), C.dim],
271
458
  ]));
272
459
  }
273
460
  postureLines.push(
274
- buildGuidanceLine(width, '/teamwork review', 'inspect task-family posture, archetype metadata, and recovery options for active work', C),
275
- 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),
276
462
  );
277
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;
278
481
  const detailRows: Line[] = [];
279
- if (selected) {
280
- 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) {
281
507
  detailRows.push(buildPanelLine(width, [
282
- [' Title: ', C.label],
283
- [selected.title, C.value],
284
- [' Status: ', C.label],
285
- [selected.status, statusColor(selected.status)],
286
- [' Kind: ', C.label],
287
- [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],
288
514
  ]));
515
+ }
516
+ if (descriptor?.reviewMode || descriptor?.executionProtocol || descriptor?.template) {
289
517
  detailRows.push(buildPanelLine(width, [
290
- [' Owner: ', C.label],
291
- [selected.owner, C.value],
292
- [' Cancellable: ', C.label],
293
- [selected.cancellable ? 'yes' : 'no', selected.cancellable ? C.running : C.failed],
294
- [' Queue: ', C.label],
295
- [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],
296
524
  ]));
525
+ }
526
+ if (task.correlationId || task.turnId) {
297
527
  detailRows.push(buildPanelLine(width, [
298
- [' Started: ', C.label],
299
- [formatWhen(selected.startedAt), C.dim],
300
- [' Ended: ', C.label],
301
- [formatWhen(selected.endedAt), C.dim],
302
- [' Duration: ', C.label],
303
- [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],
304
532
  ]));
305
- if (descriptor?.mode || descriptor?.family || descriptor?.source) {
306
- detailRows.push(buildPanelLine(width, [
307
- [' Mode: ', C.label],
308
- [descriptor?.mode ?? 'n/a', C.value],
309
- [' Family: ', C.label],
310
- [descriptor?.family ?? 'n/a', C.info],
311
- [' Source: ', C.label],
312
- [descriptor?.source ?? 'builtin/runtime', C.dim],
313
- ]));
314
- }
315
- if (descriptor?.reviewMode || descriptor?.executionProtocol || descriptor?.template) {
316
- detailRows.push(buildPanelLine(width, [
317
- [' Review: ', C.label],
318
- [descriptor?.reviewMode ?? 'n/a', C.value],
319
- [' Protocol: ', C.label],
320
- [descriptor?.executionProtocol ?? 'n/a', C.value],
321
- [' Template: ', C.label],
322
- [descriptor?.template ?? 'n/a', C.dim],
323
- ]));
324
- }
325
- if (selected.correlationId || selected.turnId) {
326
- detailRows.push(buildPanelLine(width, [
327
- [' Correlation: ', C.label],
328
- [selected.correlationId ?? 'n/a', C.dim],
329
- [' Turn: ', C.label],
330
- [selected.turnId ?? 'n/a', C.dim],
331
- ]));
332
- }
333
- if (selected.parentTaskId || selected.childTaskIds.length > 0) {
334
- detailRows.push(buildPanelLine(width, [
335
- [' Parent: ', C.label],
336
- [selected.parentTaskId ?? 'none', C.dim],
337
- [' Children: ', C.label],
338
- [selected.childTaskIds.length > 0 ? selected.childTaskIds.join(', ') : 'none', C.dim],
339
- ]));
340
- }
341
- const attachedWorktrees = reviewTaskWorktreeAttachments(selected.id, this.worktrees);
342
- if (attachedWorktrees.total > 0) {
343
- detailRows.push(buildPanelLine(width, [
344
- [' Worktrees: ', C.label],
345
- [`${attachedWorktrees.total} tracked`, C.info],
346
- [' Active: ', C.label],
347
- [String(attachedWorktrees.active), attachedWorktrees.active > 0 ? C.running : C.dim],
348
- [' Paused: ', C.label],
349
- [String(attachedWorktrees.paused), attachedWorktrees.paused > 0 ? C.blocked : C.dim],
350
- ]));
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)) {
351
553
  detailRows.push(buildPanelLine(width, [[
352
- ` Next: /worktree task ${selected.id} /worktree recover task ${selected.id}`,
353
- 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,
354
556
  ]]));
355
- for (const record of attachedWorktrees.records.slice(0, 2)) {
356
- detailRows.push(buildPanelLine(width, [[
357
- ` ${record.state.padEnd(15)} ${record.path}`.slice(0, Math.max(0, width - 2)),
358
- record.state === 'active' ? C.running : record.state === 'paused' ? C.blocked : C.dim,
359
- ]]));
360
- }
361
- }
362
- if (selected.retryPolicy) {
363
- detailRows.push(buildPanelLine(width, [
364
- [' Retry: ', C.label],
365
- [`${selected.retryPolicy.currentAttempt}/${selected.retryPolicy.maxAttempts} ${selected.retryPolicy.backoff}`, C.value],
366
- ]));
367
- }
368
- if (selected.error) {
369
- detailRows.push(buildPanelLine(width, [
370
- [' Error: ', C.label],
371
- [selected.error.slice(0, Math.max(0, width - 10)), C.failed],
372
- ]));
373
- }
374
- if (selected.result !== undefined) {
375
- const resultText = safeJson(selected.result);
376
- detailRows.push(buildPanelLine(width, [
377
- [' Result: ', C.label],
378
- [resultText.slice(0, Math.max(0, width - 11)), C.dim],
379
- ]));
380
557
  }
381
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
+ }
382
578
 
383
- 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));
384
582
 
385
- return this.renderList(width, height, {
386
- title: 'Task Control Room',
387
- header: headerLines,
388
- footer: [
389
- ...buildDetailBlock(width, 'Selected task', detailRows, C),
390
- ...footerLines,
391
- ],
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,
392
604
  });
605
+ while (lines.length < height) lines.push(createEmptyLine(width));
606
+ return lines.slice(0, height);
393
607
  }
394
608
  }