@pellux/goodvibes-tui 1.0.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 (209) hide show
  1. package/CHANGELOG.md +130 -127
  2. package/README.md +32 -14
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/cli/ensure-goodvibes-gitignore.ts +32 -0
  6. package/src/cli/entrypoint.ts +2 -0
  7. package/src/core/alert-gating.ts +67 -0
  8. package/src/core/approval-alert.ts +72 -0
  9. package/src/core/budget-breach-notifier.ts +106 -0
  10. package/src/core/conversation-rendering.ts +19 -0
  11. package/src/core/conversation.ts +58 -0
  12. package/src/core/focus-tracker.ts +41 -0
  13. package/src/core/long-task-notifier.ts +33 -6
  14. package/src/core/stream-event-wiring.ts +104 -2
  15. package/src/core/stream-stall-watchdog.ts +41 -17
  16. package/src/core/system-message-router.ts +37 -51
  17. package/src/core/turn-cancellation.ts +20 -0
  18. package/src/core/turn-event-wiring.ts +64 -3
  19. package/src/export/cost-utils.ts +126 -8
  20. package/src/input/command-registry.ts +54 -1
  21. package/src/input/commands/checkpoint-runtime.ts +280 -0
  22. package/src/input/commands/codebase-runtime.ts +192 -0
  23. package/src/input/commands/diff-runtime.ts +61 -30
  24. package/src/input/commands/eval.ts +1 -1
  25. package/src/input/commands/health-runtime.ts +1 -1
  26. package/src/input/commands/image-runtime.ts +112 -0
  27. package/src/input/commands/intelligence-runtime.ts +11 -1
  28. package/src/input/commands/local-auth-runtime.ts +4 -1
  29. package/src/input/commands/local-runtime.ts +9 -3
  30. package/src/input/commands/marketplace-runtime.ts +2 -2
  31. package/src/input/commands/memory.ts +6 -1
  32. package/src/input/commands/operator-panel-runtime.ts +40 -24
  33. package/src/input/commands/planning-runtime.ts +26 -3
  34. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  35. package/src/input/commands/plugin-runtime.ts +2 -2
  36. package/src/input/commands/policy-dispatch.ts +21 -0
  37. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  38. package/src/input/commands/qrcode-runtime.ts +3 -3
  39. package/src/input/commands/recall-review.ts +35 -0
  40. package/src/input/commands/remote-runtime.ts +3 -3
  41. package/src/input/commands/runtime-services.ts +54 -13
  42. package/src/input/commands/services-runtime.ts +1 -1
  43. package/src/input/commands/session-content.ts +12 -0
  44. package/src/input/commands/session-workflow.ts +19 -1
  45. package/src/input/commands/settings-sync-runtime.ts +1 -1
  46. package/src/input/commands/share-runtime.ts +9 -2
  47. package/src/input/commands/shell-core.ts +15 -7
  48. package/src/input/commands/skills-runtime.ts +2 -8
  49. package/src/input/commands/subscription-runtime.ts +2 -2
  50. package/src/input/commands/test-runtime.ts +277 -0
  51. package/src/input/commands/websearch-runtime.ts +101 -0
  52. package/src/input/commands/work-plan-runtime.ts +36 -10
  53. package/src/input/commands/workstream-runtime.ts +338 -0
  54. package/src/input/commands.ts +12 -0
  55. package/src/input/config-modal-types.ts +161 -0
  56. package/src/input/config-modal.ts +377 -0
  57. package/src/input/feed-context-factory.ts +7 -8
  58. package/src/input/handler-command-route.ts +27 -17
  59. package/src/input/handler-content-actions.ts +22 -8
  60. package/src/input/handler-feed-routes.ts +107 -7
  61. package/src/input/handler-feed.ts +49 -13
  62. package/src/input/handler-interactions.ts +3 -3
  63. package/src/input/handler-modal-routes.ts +65 -0
  64. package/src/input/handler-modal-stack.ts +3 -5
  65. package/src/input/handler-modal-token-routes.ts +16 -49
  66. package/src/input/handler-picker-routes.ts +11 -94
  67. package/src/input/handler-shortcuts.ts +24 -14
  68. package/src/input/handler-types.ts +3 -6
  69. package/src/input/handler-ui-state.ts +10 -22
  70. package/src/input/handler.ts +10 -28
  71. package/src/input/keybindings.ts +22 -8
  72. package/src/input/model-picker-types.ts +24 -6
  73. package/src/input/model-picker.ts +58 -0
  74. package/src/input/panel-integration-actions.ts +9 -170
  75. package/src/input/settings-modal-data.ts +95 -0
  76. package/src/input/settings-modal-mutations.ts +6 -4
  77. package/src/main.ts +41 -44
  78. package/src/panels/agent-inspector-shared.ts +35 -11
  79. package/src/panels/base-panel.ts +22 -1
  80. package/src/panels/builtin/agent.ts +21 -107
  81. package/src/panels/builtin/development.ts +16 -62
  82. package/src/panels/builtin/knowledge.ts +8 -32
  83. package/src/panels/builtin/operations.ts +84 -428
  84. package/src/panels/builtin/session.ts +21 -112
  85. package/src/panels/builtin/shared.ts +9 -43
  86. package/src/panels/builtin-modals.ts +218 -0
  87. package/src/panels/builtin-panels.ts +5 -0
  88. package/src/panels/cost-tracker-panel.ts +63 -14
  89. package/src/panels/diff-panel.ts +123 -60
  90. package/src/panels/eval-registry.ts +60 -0
  91. package/src/panels/fleet-panel.ts +800 -0
  92. package/src/panels/fleet-read-model.ts +477 -0
  93. package/src/panels/fleet-steer.ts +92 -0
  94. package/src/panels/fleet-stop.ts +125 -0
  95. package/src/panels/fleet-tabs.ts +230 -0
  96. package/src/panels/fleet-transcript.ts +356 -0
  97. package/src/panels/git-panel.ts +9 -9
  98. package/src/panels/index.ts +7 -31
  99. package/src/panels/local-auth-panel.ts +10 -0
  100. package/src/panels/modals/hooks-modal.ts +187 -0
  101. package/src/panels/modals/keybindings-modal.ts +151 -0
  102. package/src/panels/modals/knowledge-modal.ts +158 -0
  103. package/src/panels/modals/local-auth-modal.ts +132 -0
  104. package/src/panels/modals/marketplace-modal.ts +218 -0
  105. package/src/panels/modals/memory-modal.ts +180 -0
  106. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  107. package/src/panels/modals/modal-theme.ts +36 -0
  108. package/src/panels/modals/pairing-modal.ts +144 -0
  109. package/src/panels/modals/planning-modal.ts +282 -0
  110. package/src/panels/modals/plugins-modal.ts +174 -0
  111. package/src/panels/modals/policy-modal.ts +256 -0
  112. package/src/panels/modals/provider-health-modal.ts +136 -0
  113. package/src/panels/modals/remote-modal.ts +115 -0
  114. package/src/panels/modals/sandbox-modal.ts +150 -0
  115. package/src/panels/modals/security-modal.ts +217 -0
  116. package/src/panels/modals/services-modal.ts +179 -0
  117. package/src/panels/modals/settings-sync-modal.ts +142 -0
  118. package/src/panels/modals/skills-modal.ts +189 -0
  119. package/src/panels/modals/subscription-modal.ts +172 -0
  120. package/src/panels/modals/work-plan-modal.ts +149 -0
  121. package/src/panels/panel-confirm-overlay.ts +72 -0
  122. package/src/panels/panel-manager.ts +108 -5
  123. package/src/panels/project-planning-answer-actions.ts +4 -2
  124. package/src/panels/scrollable-list-panel.ts +9 -0
  125. package/src/panels/skills-panel.ts +7 -51
  126. package/src/panels/token-budget-panel.ts +5 -3
  127. package/src/panels/types.ts +26 -0
  128. package/src/permissions/hunk-selection.ts +179 -0
  129. package/src/permissions/prompt.ts +60 -4
  130. package/src/renderer/compaction-history-modal.ts +19 -3
  131. package/src/renderer/compaction-preview.ts +13 -2
  132. package/src/renderer/compaction-quality.ts +100 -0
  133. package/src/renderer/config-modal.ts +81 -0
  134. package/src/renderer/conversation-overlays.ts +10 -17
  135. package/src/renderer/fleet-tab-strip.ts +56 -0
  136. package/src/renderer/footer-tips.ts +10 -2
  137. package/src/renderer/git-status.ts +40 -0
  138. package/src/renderer/help-overlay.ts +2 -2
  139. package/src/renderer/model-workspace.ts +44 -1
  140. package/src/renderer/panel-workspace-bar.ts +8 -2
  141. package/src/renderer/shell-surface.ts +8 -1
  142. package/src/renderer/turn-injection.ts +114 -0
  143. package/src/renderer/ui-factory.ts +91 -7
  144. package/src/runtime/bootstrap-command-context.ts +24 -1
  145. package/src/runtime/bootstrap-command-parts.ts +36 -7
  146. package/src/runtime/bootstrap-core.ts +19 -4
  147. package/src/runtime/bootstrap-hook-bridge.ts +5 -0
  148. package/src/runtime/bootstrap-shell.ts +41 -17
  149. package/src/runtime/bootstrap.ts +11 -17
  150. package/src/runtime/code-index-services.ts +112 -0
  151. package/src/runtime/orchestrator-core-services.ts +49 -0
  152. package/src/runtime/process-lifecycle.ts +3 -1
  153. package/src/runtime/services.ts +91 -43
  154. package/src/runtime/ui-services.ts +8 -0
  155. package/src/runtime/workstream-services.ts +195 -0
  156. package/src/shell/blocking-input.ts +22 -1
  157. package/src/shell/ui-openers.ts +129 -17
  158. package/src/utils/format-duration.ts +8 -18
  159. package/src/utils/splash-lines.ts +1 -1
  160. package/src/version.ts +1 -1
  161. package/src/panels/agent-inspector-panel.ts +0 -786
  162. package/src/panels/approval-panel.ts +0 -252
  163. package/src/panels/automation-control-panel.ts +0 -479
  164. package/src/panels/cockpit-panel.ts +0 -481
  165. package/src/panels/cockpit-read-model.ts +0 -233
  166. package/src/panels/communication-panel.ts +0 -256
  167. package/src/panels/control-plane-panel.ts +0 -470
  168. package/src/panels/debug-panel.ts +0 -609
  169. package/src/panels/docs-panel.ts +0 -375
  170. package/src/panels/eval-panel.ts +0 -627
  171. package/src/panels/file-explorer-panel.ts +0 -664
  172. package/src/panels/file-preview-panel.ts +0 -517
  173. package/src/panels/hooks-panel.ts +0 -401
  174. package/src/panels/incident-review-panel.ts +0 -406
  175. package/src/panels/intelligence-panel.ts +0 -383
  176. package/src/panels/knowledge-graph-panel.ts +0 -506
  177. package/src/panels/marketplace-panel.ts +0 -399
  178. package/src/panels/memory-panel.ts +0 -558
  179. package/src/panels/ops-control-panel.ts +0 -329
  180. package/src/panels/ops-strategy-panel.ts +0 -321
  181. package/src/panels/orchestration-panel.ts +0 -465
  182. package/src/panels/panel-list-panel.ts +0 -557
  183. package/src/panels/plan-dashboard-panel.ts +0 -707
  184. package/src/panels/policy-panel.ts +0 -517
  185. package/src/panels/project-planning-panel.ts +0 -721
  186. package/src/panels/provider-health-panel.ts +0 -678
  187. package/src/panels/provider-health-tracker.ts +0 -306
  188. package/src/panels/provider-health-views.ts +0 -560
  189. package/src/panels/qr-panel.ts +0 -280
  190. package/src/panels/remote-panel.ts +0 -534
  191. package/src/panels/routes-panel.ts +0 -241
  192. package/src/panels/sandbox-panel.ts +0 -456
  193. package/src/panels/security-panel.ts +0 -447
  194. package/src/panels/services-panel.ts +0 -329
  195. package/src/panels/session-browser-panel.ts +0 -487
  196. package/src/panels/settings-sync-panel.ts +0 -398
  197. package/src/panels/subscription-panel.ts +0 -342
  198. package/src/panels/symbol-outline-panel.ts +0 -619
  199. package/src/panels/system-messages-panel.ts +0 -364
  200. package/src/panels/tasks-panel.ts +0 -608
  201. package/src/panels/thinking-panel.ts +0 -333
  202. package/src/panels/tool-inspector-panel.ts +0 -537
  203. package/src/panels/work-plan-panel.ts +0 -530
  204. package/src/panels/worktree-panel.ts +0 -360
  205. package/src/panels/wrfc-panel.ts +0 -790
  206. package/src/renderer/agent-detail-modal.ts +0 -465
  207. package/src/renderer/live-tail-modal.ts +0 -156
  208. package/src/renderer/process-modal.ts +0 -656
  209. 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
- }