@pellux/goodvibes-tui 1.1.0 → 1.7.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 (194) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +20 -11
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/core/alert-gating.ts +67 -0
  6. package/src/core/approval-alert.ts +72 -0
  7. package/src/core/budget-breach-notifier.ts +106 -0
  8. package/src/core/conversation-rendering.ts +19 -0
  9. package/src/core/conversation.ts +18 -0
  10. package/src/core/focus-tracker.ts +41 -0
  11. package/src/core/long-task-notifier.ts +33 -6
  12. package/src/core/system-message-router.ts +37 -51
  13. package/src/core/turn-cancellation.ts +20 -0
  14. package/src/core/turn-event-wiring.ts +64 -3
  15. package/src/export/cost-utils.ts +36 -0
  16. package/src/input/command-registry.ts +38 -1
  17. package/src/input/commands/checkpoint-runtime.ts +280 -0
  18. package/src/input/commands/codebase-runtime.ts +192 -0
  19. package/src/input/commands/eval.ts +1 -1
  20. package/src/input/commands/health-runtime.ts +1 -1
  21. package/src/input/commands/image-runtime.ts +112 -0
  22. package/src/input/commands/intelligence-runtime.ts +11 -1
  23. package/src/input/commands/local-auth-runtime.ts +4 -1
  24. package/src/input/commands/local-runtime.ts +9 -3
  25. package/src/input/commands/marketplace-runtime.ts +2 -2
  26. package/src/input/commands/memory.ts +6 -1
  27. package/src/input/commands/operator-panel-runtime.ts +40 -24
  28. package/src/input/commands/planning-runtime.ts +26 -3
  29. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  30. package/src/input/commands/plugin-runtime.ts +2 -2
  31. package/src/input/commands/policy-dispatch.ts +21 -0
  32. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  33. package/src/input/commands/qrcode-runtime.ts +3 -3
  34. package/src/input/commands/recall-review.ts +35 -0
  35. package/src/input/commands/remote-runtime.ts +3 -3
  36. package/src/input/commands/runtime-services.ts +54 -13
  37. package/src/input/commands/services-runtime.ts +1 -1
  38. package/src/input/commands/session-workflow.ts +16 -1
  39. package/src/input/commands/settings-sync-runtime.ts +1 -1
  40. package/src/input/commands/shell-core.ts +15 -7
  41. package/src/input/commands/skills-runtime.ts +2 -8
  42. package/src/input/commands/subscription-runtime.ts +2 -2
  43. package/src/input/commands/test-runtime.ts +277 -0
  44. package/src/input/commands/websearch-runtime.ts +101 -0
  45. package/src/input/commands/work-plan-runtime.ts +36 -10
  46. package/src/input/commands/workstream-runtime.ts +338 -0
  47. package/src/input/commands.ts +12 -0
  48. package/src/input/config-modal-types.ts +161 -0
  49. package/src/input/config-modal.ts +377 -0
  50. package/src/input/feed-context-factory.ts +7 -8
  51. package/src/input/handler-command-route.ts +27 -17
  52. package/src/input/handler-feed-routes.ts +61 -23
  53. package/src/input/handler-feed.ts +47 -23
  54. package/src/input/handler-interactions.ts +1 -3
  55. package/src/input/handler-modal-routes.ts +65 -0
  56. package/src/input/handler-modal-stack.ts +3 -5
  57. package/src/input/handler-modal-token-routes.ts +16 -49
  58. package/src/input/handler-picker-routes.ts +11 -94
  59. package/src/input/handler-shortcuts.ts +24 -14
  60. package/src/input/handler-types.ts +2 -6
  61. package/src/input/handler-ui-state.ts +10 -22
  62. package/src/input/handler.ts +6 -28
  63. package/src/input/keybindings.ts +22 -8
  64. package/src/input/model-picker-types.ts +24 -6
  65. package/src/input/model-picker.ts +58 -0
  66. package/src/input/panel-integration-actions.ts +9 -170
  67. package/src/input/settings-modal-data.ts +95 -0
  68. package/src/input/settings-modal-mutations.ts +6 -4
  69. package/src/main.ts +24 -27
  70. package/src/panels/agent-inspector-shared.ts +2 -1
  71. package/src/panels/base-panel.ts +22 -1
  72. package/src/panels/builtin/agent.ts +21 -107
  73. package/src/panels/builtin/development.ts +15 -61
  74. package/src/panels/builtin/knowledge.ts +8 -32
  75. package/src/panels/builtin/operations.ts +84 -428
  76. package/src/panels/builtin/session.ts +21 -112
  77. package/src/panels/builtin/shared.ts +9 -43
  78. package/src/panels/builtin-modals.ts +218 -0
  79. package/src/panels/builtin-panels.ts +5 -0
  80. package/src/panels/cost-tracker-panel.ts +36 -10
  81. package/src/panels/diff-panel.ts +12 -43
  82. package/src/panels/eval-registry.ts +60 -0
  83. package/src/panels/fleet-panel.ts +800 -0
  84. package/src/panels/fleet-read-model.ts +477 -0
  85. package/src/panels/fleet-steer.ts +92 -0
  86. package/src/panels/fleet-stop.ts +125 -0
  87. package/src/panels/fleet-tabs.ts +230 -0
  88. package/src/panels/fleet-transcript.ts +356 -0
  89. package/src/panels/index.ts +7 -31
  90. package/src/panels/modals/hooks-modal.ts +187 -0
  91. package/src/panels/modals/keybindings-modal.ts +151 -0
  92. package/src/panels/modals/knowledge-modal.ts +158 -0
  93. package/src/panels/modals/local-auth-modal.ts +132 -0
  94. package/src/panels/modals/marketplace-modal.ts +218 -0
  95. package/src/panels/modals/memory-modal.ts +180 -0
  96. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  97. package/src/panels/modals/modal-theme.ts +36 -0
  98. package/src/panels/modals/pairing-modal.ts +144 -0
  99. package/src/panels/modals/planning-modal.ts +282 -0
  100. package/src/panels/modals/plugins-modal.ts +174 -0
  101. package/src/panels/modals/policy-modal.ts +256 -0
  102. package/src/panels/modals/provider-health-modal.ts +136 -0
  103. package/src/panels/modals/remote-modal.ts +115 -0
  104. package/src/panels/modals/sandbox-modal.ts +150 -0
  105. package/src/panels/modals/security-modal.ts +217 -0
  106. package/src/panels/modals/services-modal.ts +179 -0
  107. package/src/panels/modals/settings-sync-modal.ts +142 -0
  108. package/src/panels/modals/skills-modal.ts +189 -0
  109. package/src/panels/modals/subscription-modal.ts +172 -0
  110. package/src/panels/modals/work-plan-modal.ts +149 -0
  111. package/src/panels/panel-confirm-overlay.ts +72 -0
  112. package/src/panels/panel-manager.ts +108 -5
  113. package/src/panels/project-planning-answer-actions.ts +4 -2
  114. package/src/panels/skills-panel.ts +7 -51
  115. package/src/panels/types.ts +13 -0
  116. package/src/permissions/hunk-selection.ts +179 -0
  117. package/src/permissions/prompt.ts +60 -4
  118. package/src/renderer/compaction-history-modal.ts +19 -3
  119. package/src/renderer/compaction-preview.ts +13 -2
  120. package/src/renderer/compaction-quality.ts +100 -0
  121. package/src/renderer/config-modal.ts +81 -0
  122. package/src/renderer/conversation-overlays.ts +10 -17
  123. package/src/renderer/fleet-tab-strip.ts +56 -0
  124. package/src/renderer/footer-tips.ts +10 -2
  125. package/src/renderer/git-status.ts +40 -0
  126. package/src/renderer/help-overlay.ts +2 -2
  127. package/src/renderer/model-workspace.ts +44 -1
  128. package/src/renderer/panel-workspace-bar.ts +8 -2
  129. package/src/renderer/turn-injection.ts +114 -0
  130. package/src/runtime/bootstrap-command-context.ts +21 -1
  131. package/src/runtime/bootstrap-command-parts.ts +34 -7
  132. package/src/runtime/bootstrap-core.ts +14 -4
  133. package/src/runtime/bootstrap-shell.ts +29 -16
  134. package/src/runtime/bootstrap.ts +11 -17
  135. package/src/runtime/code-index-services.ts +112 -0
  136. package/src/runtime/orchestrator-core-services.ts +49 -0
  137. package/src/runtime/process-lifecycle.ts +3 -1
  138. package/src/runtime/services.ts +91 -43
  139. package/src/runtime/ui-services.ts +8 -0
  140. package/src/runtime/workstream-services.ts +195 -0
  141. package/src/shell/blocking-input.ts +22 -1
  142. package/src/shell/ui-openers.ts +129 -17
  143. package/src/utils/format-duration.ts +8 -18
  144. package/src/utils/splash-lines.ts +1 -1
  145. package/src/version.ts +1 -1
  146. package/src/panels/agent-inspector-panel.ts +0 -786
  147. package/src/panels/approval-panel.ts +0 -252
  148. package/src/panels/automation-control-panel.ts +0 -479
  149. package/src/panels/cockpit-panel.ts +0 -481
  150. package/src/panels/cockpit-read-model.ts +0 -238
  151. package/src/panels/communication-panel.ts +0 -256
  152. package/src/panels/control-plane-panel.ts +0 -470
  153. package/src/panels/debug-panel.ts +0 -615
  154. package/src/panels/docs-panel.ts +0 -384
  155. package/src/panels/eval-panel.ts +0 -627
  156. package/src/panels/file-explorer-panel.ts +0 -673
  157. package/src/panels/file-preview-panel.ts +0 -517
  158. package/src/panels/hooks-panel.ts +0 -401
  159. package/src/panels/incident-review-panel.ts +0 -406
  160. package/src/panels/intelligence-panel.ts +0 -383
  161. package/src/panels/knowledge-graph-panel.ts +0 -515
  162. package/src/panels/marketplace-panel.ts +0 -399
  163. package/src/panels/memory-panel.ts +0 -558
  164. package/src/panels/ops-control-panel.ts +0 -329
  165. package/src/panels/ops-strategy-panel.ts +0 -321
  166. package/src/panels/orchestration-panel.ts +0 -465
  167. package/src/panels/panel-list-panel.ts +0 -566
  168. package/src/panels/plan-dashboard-panel.ts +0 -707
  169. package/src/panels/policy-panel.ts +0 -517
  170. package/src/panels/project-planning-panel.ts +0 -731
  171. package/src/panels/provider-health-panel.ts +0 -678
  172. package/src/panels/provider-health-tracker.ts +0 -310
  173. package/src/panels/provider-health-views.ts +0 -567
  174. package/src/panels/qr-panel.ts +0 -280
  175. package/src/panels/remote-panel.ts +0 -534
  176. package/src/panels/routes-panel.ts +0 -241
  177. package/src/panels/sandbox-panel.ts +0 -456
  178. package/src/panels/security-panel.ts +0 -447
  179. package/src/panels/services-panel.ts +0 -329
  180. package/src/panels/session-browser-panel.ts +0 -496
  181. package/src/panels/settings-sync-panel.ts +0 -398
  182. package/src/panels/subscription-panel.ts +0 -342
  183. package/src/panels/symbol-outline-panel.ts +0 -619
  184. package/src/panels/system-messages-panel.ts +0 -364
  185. package/src/panels/tasks-panel.ts +0 -608
  186. package/src/panels/thinking-panel.ts +0 -333
  187. package/src/panels/tool-inspector-panel.ts +0 -537
  188. package/src/panels/work-plan-panel.ts +0 -540
  189. package/src/panels/worktree-panel.ts +0 -360
  190. package/src/panels/wrfc-panel.ts +0 -790
  191. package/src/renderer/agent-detail-modal.ts +0 -466
  192. package/src/renderer/live-tail-modal.ts +0 -156
  193. package/src/renderer/process-modal.ts +0 -671
  194. package/src/renderer/qr-renderer.ts +0 -120
@@ -1,608 +0,0 @@
1
- import type { Line } from '../types/grid.ts';
2
- import { createEmptyLine } from '../types/grid.ts';
3
- import { truncateDisplay } from '../utils/terminal-width.ts';
4
- import { ScrollableListPanel } from './scrollable-list-panel.ts';
5
- import type { OpsApi, RuntimeTask, TaskLifecycleState } from '@/runtime/index.ts';
6
- import type { ManagedWorktreeMeta } from '@/runtime/index.ts';
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';
10
- import {
11
- buildEmptyState,
12
- buildGuidanceLine,
13
- buildKeyboardHints,
14
- buildPanelListRow,
15
- buildPanelLine,
16
- buildSummaryBlock,
17
- buildPanelWorkspace,
18
- resolveScrollablePanelSection,
19
- DEFAULT_PANEL_PALETTE,
20
- extendPalette,
21
- } from './polish.ts';
22
- import { formatElapsed } from '../utils/format-elapsed.ts';
23
-
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);
29
-
30
- const STATUS_ORDER: TaskLifecycleState[] = ['queued', 'running', 'blocked', 'failed', 'completed'];
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
-
38
- function formatWhen(value?: number): string {
39
- return value ? new Date(value).toLocaleString() : 'n/a';
40
- }
41
-
42
- function formatDuration(startedAt?: number, endedAt?: number): string {
43
- if (!startedAt) return 'n/a';
44
- return formatElapsed(Math.max(0, (endedAt ?? Date.now()) - startedAt));
45
- }
46
-
47
- function kindLabel(kind: RuntimeTask['kind']): string {
48
- switch (kind) {
49
- case 'exec': return 'exec';
50
- case 'agent': return 'agent';
51
- case 'acp': return 'acp';
52
- case 'scheduler': return 'scheduler';
53
- case 'daemon': return 'daemon';
54
- case 'mcp': return 'mcp';
55
- case 'plugin': return 'plugin';
56
- case 'integration': return 'integration';
57
- }
58
- }
59
-
60
- function statusColor(status: TaskLifecycleState): string {
61
- switch (status) {
62
- case 'queued':
63
- return C.info;
64
- case 'running':
65
- return C.good;
66
- case 'blocked':
67
- return C.warn;
68
- case 'failed':
69
- return C.bad;
70
- case 'completed':
71
- return C.completed;
72
- case 'cancelled':
73
- return C.dim;
74
- }
75
- }
76
-
77
- function sortTasks(tasks: RuntimeTask[]): RuntimeTask[] {
78
- const order = new Map(STATUS_ORDER.map((status, index) => [status, index] as const));
79
- return [...tasks].sort((a, b) => {
80
- const statusDelta = (order.get(a.status) ?? 99) - (order.get(b.status) ?? 99);
81
- if (statusDelta !== 0) return statusDelta;
82
- const aTime = a.startedAt ?? a.queuedAt;
83
- const bTime = b.startedAt ?? b.queuedAt;
84
- return bTime - aTime || a.title.localeCompare(b.title);
85
- });
86
- }
87
-
88
- function safeJson(value: unknown): string {
89
- try {
90
- return JSON.stringify(value);
91
- } catch {
92
- return String(value);
93
- }
94
- }
95
-
96
- interface TaskDescriptorMeta {
97
- readonly mode?: string;
98
- readonly template?: string;
99
- readonly reviewMode?: string;
100
- readonly executionProtocol?: string;
101
- readonly source?: string;
102
- readonly family?: string;
103
- }
104
-
105
- function parseTaskDescriptor(description: string): TaskDescriptorMeta | null {
106
- try {
107
- const parsed = JSON.parse(description) as TaskDescriptorMeta;
108
- if (!parsed || typeof parsed !== 'object') return null;
109
- return parsed;
110
- } catch {
111
- return null;
112
- }
113
- }
114
-
115
- interface TaskWorktreeAttachmentReview {
116
- readonly total: number;
117
- readonly active: number;
118
- readonly paused: number;
119
- readonly kept: number;
120
- readonly discard: number;
121
- readonly pendingCleanup: number;
122
- readonly records: readonly ManagedWorktreeMeta[];
123
- }
124
-
125
- function reviewTaskWorktreeAttachments(
126
- taskId: string,
127
- worktrees?: UiReadModel<UiWorktreeSnapshot>,
128
- ): TaskWorktreeAttachmentReview {
129
- const records = (worktrees?.getSnapshot().records ?? []).filter((record) => record.taskId === taskId);
130
- return records.reduce<TaskWorktreeAttachmentReview>((summary, record) => ({
131
- total: summary.total + 1,
132
- active: summary.active + (record.state === 'active' ? 1 : 0),
133
- paused: summary.paused + (record.state === 'paused' ? 1 : 0),
134
- kept: summary.kept + (record.state === 'kept' ? 1 : 0),
135
- discard: summary.discard + (record.state === 'discard' ? 1 : 0),
136
- pendingCleanup: summary.pendingCleanup + (record.state === 'pending-cleanup' ? 1 : 0),
137
- records: [...summary.records, record],
138
- }), {
139
- total: 0,
140
- active: 0,
141
- paused: 0,
142
- kept: 0,
143
- discard: 0,
144
- pendingCleanup: 0,
145
- records: [],
146
- });
147
- }
148
-
149
- export class TasksPanel extends ScrollableListPanel<RuntimeTask> {
150
- private readonly readModel?: UiReadModel<UiTasksSnapshot>;
151
- private readonly worktrees?: UiReadModel<UiWorktreeSnapshot>;
152
- private readonly opsApi?: OpsApi;
153
- private readonly unsubscribers: readonly (() => void)[];
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
-
165
- public constructor(
166
- readModel: UiReadModel<UiTasksSnapshot> | undefined,
167
- worktrees?: UiReadModel<UiWorktreeSnapshot>,
168
- opsApi?: OpsApi,
169
- ) {
170
- super('tasks', 'Tasks', '▦', 'runtime-ops');
171
- this.showSelectionGutter = true; // I5: non-color selection affordance
172
- this.filterEnabled = true;
173
- this.filterLabel = 'Filter tasks';
174
- this.readModel = readModel;
175
- this.worktrees = worktrees;
176
- this.opsApi = opsApi;
177
- this.unsubscribers = [
178
- readModel?.subscribe(() => this.markDirty()),
179
- worktrees?.subscribe(() => this.markDirty()),
180
- ].filter((unsubscribe): unsubscribe is () => void => Boolean(unsubscribe));
181
- }
182
-
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.
185
-
186
- public override onDestroy(): void {
187
- for (const unsubscribe of this.unsubscribers) unsubscribe();
188
- }
189
-
190
- protected override getPalette() { return C; }
191
- protected override getEmptyStateMessage() { return ' No tasks recorded yet.'; }
192
- protected override getEmptyStateActions() {
193
- return [
194
- { command: '/tasks create', summary: 'create a tracked task from the shell' },
195
- { command: '/orchestration', summary: 'review graph-native task execution and WRFC flows' },
196
- ];
197
- }
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
-
206
- protected getItems(): readonly RuntimeTask[] {
207
- if (!this.readModel) return [];
208
- return sortTasks([...this.readModel.getSnapshot().tasks]);
209
- }
210
-
211
- protected renderItem(task: RuntimeTask, index: number, selected: boolean, width: number): Line {
212
- return buildPanelListRow(width, [
213
- { text: task.status.padEnd(10), fg: statusColor(task.status) },
214
- { text: ` ${kindLabel(task.kind).padEnd(12)}`, fg: C.value },
215
- { text: ` ${task.id.slice(0, 8)} `, fg: C.dim },
216
- { text: truncateDisplay(task.title, Math.max(0, width - 37)), fg: C.value },
217
- ], C, { selected });
218
- }
219
-
220
- // ---------------------------------------------------------------------------
221
- // Input
222
- // ---------------------------------------------------------------------------
223
-
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 ---
291
- if (key === 'home') {
292
- this.selectedIndex = 0;
293
- this.markDirty();
294
- return true;
295
- }
296
- if (key === 'end') {
297
- this.selectedIndex = Math.max(0, tasks.length - 1);
298
- this.markDirty();
299
- return true;
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
-
334
- return super.handleInput(key);
335
- }
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
-
364
- public render(width: number, height: number): Line[] {
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[] {
377
- const intro = 'Live task lifecycle, ownership, retries, and result/error details across runtime execution domains.';
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
- ];
402
-
403
- if (!this.readModel) {
404
- const workspace = buildPanelWorkspace(width, height, {
405
- title: 'Task Control Room',
406
- intro,
407
- sections: [{
408
- lines: buildEmptyState(
409
- width,
410
- ' Runtime store not wired into this panel yet.',
411
- 'Use the Tasks panel with a live runtime store to review active execution, cancellations, retries, and completion results.',
412
- [{ command: '/tasks', summary: 'open or operate the task surface from a shell-owned runtime' }],
413
- C,
414
- ),
415
- }],
416
- palette: C,
417
- });
418
- while (workspace.length < height) workspace.push(createEmptyLine(width));
419
- return workspace;
420
- }
421
-
422
- const tasks = this.getItems();
423
- const counts = STATUS_ORDER.map((status) => ({
424
- status,
425
- count: tasks.filter((task) => task.status === status).length,
426
- }));
427
- const blockedCount = counts.find((entry) => entry.status === 'blocked')?.count ?? 0;
428
- const failedCount = counts.find((entry) => entry.status === 'failed')?.count ?? 0;
429
- const runningCount = counts.find((entry) => entry.status === 'running')?.count ?? 0;
430
- const queuedCount = counts.find((entry) => entry.status === 'queued')?.count ?? 0;
431
- const completedCount = counts.find((entry) => entry.status === 'completed')?.count ?? 0;
432
-
433
- const postureLines: Line[] = [
434
- buildPanelLine(width, [
435
- [' queued ', C.label],
436
- [String(queuedCount), queuedCount > 0 ? C.info : C.dim],
437
- [' running ', C.label],
438
- [String(runningCount), runningCount > 0 ? C.good : C.dim],
439
- [' blocked ', C.label],
440
- [String(blockedCount), blockedCount > 0 ? C.warn : C.dim],
441
- [' failed ', C.label],
442
- [String(failedCount), failedCount > 0 ? C.bad : C.dim],
443
- [' completed ', C.label],
444
- [String(completedCount), completedCount > 0 ? C.completed : C.dim],
445
- ]),
446
- ];
447
-
448
- if (selected) {
449
- postureLines.push(buildPanelLine(width, [
450
- [' selected ', C.label],
451
- [selected.id, C.info],
452
- [' status ', C.label],
453
- [selected.status, statusColor(selected.status)],
454
- [' kind ', C.label],
455
- [selected.kind, C.value],
456
- [' owner ', C.label],
457
- [truncateDisplay(selected.owner, Math.max(0, width - 46)), C.dim],
458
- ]));
459
- }
460
- postureLines.push(
461
- buildGuidanceLine(width, '/teamwork review', 'inspect task-family posture, archetype metadata, and recovery options for active work — press w to run it', C),
462
- );
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;
481
- const detailRows: Line[] = [];
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) {
507
- detailRows.push(buildPanelLine(width, [
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],
514
- ]));
515
- }
516
- if (descriptor?.reviewMode || descriptor?.executionProtocol || descriptor?.template) {
517
- detailRows.push(buildPanelLine(width, [
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],
524
- ]));
525
- }
526
- if (task.correlationId || task.turnId) {
527
- detailRows.push(buildPanelLine(width, [
528
- [' Correlation: ', C.label],
529
- [task.correlationId ?? 'n/a', C.dim],
530
- [' Turn: ', C.label],
531
- [task.turnId ?? 'n/a', C.dim],
532
- ]));
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)) {
553
- detailRows.push(buildPanelLine(width, [[
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,
556
- ]]));
557
- }
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
- }
578
-
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));
582
-
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,
604
- });
605
- while (lines.length < height) lines.push(createEmptyLine(width));
606
- return lines.slice(0, height);
607
- }
608
- }