@pellux/goodvibes-tui 1.1.0 → 1.9.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 (267) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +28 -18
  3. package/docs/foundation-artifacts/operator-contract.json +4045 -1763
  4. package/package.json +2 -2
  5. package/src/audio/spoken-turn-controller.ts +12 -2
  6. package/src/audio/spoken-turn-wiring.ts +2 -1
  7. package/src/cli/help.ts +8 -1
  8. package/src/cli/service-posture.ts +2 -1
  9. package/src/cli/status.ts +2 -1
  10. package/src/cli/surface-command.ts +2 -2
  11. package/src/core/alert-gating.ts +67 -0
  12. package/src/core/approval-alert.ts +72 -0
  13. package/src/core/budget-breach-notifier.ts +106 -0
  14. package/src/core/composer-state.ts +11 -3
  15. package/src/core/conversation-line-cache.ts +27 -3
  16. package/src/core/conversation-rendering.ts +90 -14
  17. package/src/core/conversation.ts +18 -0
  18. package/src/core/focus-tracker.ts +41 -0
  19. package/src/core/long-task-notifier.ts +33 -6
  20. package/src/core/stream-event-wiring.ts +20 -1
  21. package/src/core/system-message-noise.ts +87 -0
  22. package/src/core/system-message-router.ts +104 -51
  23. package/src/core/turn-cancellation.ts +25 -0
  24. package/src/core/turn-event-wiring.ts +72 -3
  25. package/src/daemon/cli.ts +29 -2
  26. package/src/daemon/handlers/register.ts +8 -1
  27. package/src/daemon/service-commands.ts +329 -0
  28. package/src/export/cost-utils.ts +36 -0
  29. package/src/input/autocomplete.ts +27 -1
  30. package/src/input/command-registry.ts +84 -5
  31. package/src/input/commands/checkpoint-runtime.ts +280 -0
  32. package/src/input/commands/codebase-runtime.ts +232 -0
  33. package/src/input/commands/config.ts +43 -3
  34. package/src/input/commands/eval.ts +1 -1
  35. package/src/input/commands/health-runtime.ts +10 -2
  36. package/src/input/commands/image-runtime.ts +112 -0
  37. package/src/input/commands/intelligence-runtime.ts +11 -1
  38. package/src/input/commands/local-auth-runtime.ts +4 -1
  39. package/src/input/commands/local-runtime.ts +9 -3
  40. package/src/input/commands/marketplace-runtime.ts +2 -2
  41. package/src/input/commands/memory.ts +73 -35
  42. package/src/input/commands/operator-panel-runtime.ts +71 -31
  43. package/src/input/commands/planning-runtime.ts +116 -4
  44. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  45. package/src/input/commands/plugin-runtime.ts +2 -2
  46. package/src/input/commands/policy-dispatch.ts +21 -0
  47. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  48. package/src/input/commands/qrcode-runtime.ts +26 -6
  49. package/src/input/commands/recall-review.ts +35 -0
  50. package/src/input/commands/remote-runtime-setup.ts +5 -3
  51. package/src/input/commands/remote-runtime.ts +3 -3
  52. package/src/input/commands/runtime-services.ts +54 -13
  53. package/src/input/commands/services-runtime.ts +1 -1
  54. package/src/input/commands/session-content.ts +20 -9
  55. package/src/input/commands/session-workflow.ts +16 -1
  56. package/src/input/commands/settings-sync-runtime.ts +16 -4
  57. package/src/input/commands/shell-core.ts +25 -8
  58. package/src/input/commands/skills-runtime.ts +2 -8
  59. package/src/input/commands/subscription-runtime.ts +2 -2
  60. package/src/input/commands/test-runtime.ts +277 -0
  61. package/src/input/commands/websearch-runtime.ts +101 -0
  62. package/src/input/commands/work-plan-runtime.ts +36 -10
  63. package/src/input/commands/workstream-runtime.ts +488 -0
  64. package/src/input/commands.ts +12 -0
  65. package/src/input/config-modal-types.ts +175 -0
  66. package/src/input/config-modal.ts +592 -0
  67. package/src/input/feed-context-factory.ts +10 -8
  68. package/src/input/handler-command-route.ts +37 -20
  69. package/src/input/handler-content-actions.ts +17 -2
  70. package/src/input/handler-feed-routes.ts +74 -114
  71. package/src/input/handler-feed.ts +79 -27
  72. package/src/input/handler-interactions.ts +1 -3
  73. package/src/input/handler-modal-routes.ts +136 -6
  74. package/src/input/handler-modal-stack.ts +14 -7
  75. package/src/input/handler-modal-token-routes.ts +16 -49
  76. package/src/input/handler-onboarding-daemon-adopt.ts +149 -0
  77. package/src/input/handler-onboarding.ts +71 -59
  78. package/src/input/handler-picker-routes.ts +26 -102
  79. package/src/input/handler-shortcuts.ts +74 -14
  80. package/src/input/handler-types.ts +2 -6
  81. package/src/input/handler-ui-state.ts +10 -22
  82. package/src/input/handler.ts +12 -29
  83. package/src/input/keybindings.ts +23 -8
  84. package/src/input/model-picker-types.ts +24 -6
  85. package/src/input/model-picker.ts +77 -2
  86. package/src/input/onboarding/onboarding-runtime-status.ts +10 -1
  87. package/src/input/onboarding/onboarding-wizard-apply.ts +8 -1
  88. package/src/input/onboarding/onboarding-wizard-constants.ts +4 -1
  89. package/src/input/onboarding/onboarding-wizard-network-adopt.ts +136 -0
  90. package/src/input/onboarding/onboarding-wizard-steps.ts +9 -6
  91. package/src/input/onboarding/onboarding-wizard-types.ts +3 -1
  92. package/src/input/panel-integration-actions.ts +9 -170
  93. package/src/input/panel-mouse-geometry.ts +97 -0
  94. package/src/input/panel-paste-flood-guard.ts +86 -0
  95. package/src/input/selection-modal.ts +11 -0
  96. package/src/input/session-picker-modal.ts +44 -4
  97. package/src/input/settings-modal-data.ts +146 -2
  98. package/src/input/settings-modal-mutations.ts +6 -4
  99. package/src/input/settings-modal-types.ts +4 -2
  100. package/src/main.ts +49 -51
  101. package/src/panels/agent-inspector-shared.ts +2 -1
  102. package/src/panels/base-panel.ts +22 -1
  103. package/src/panels/builtin/agent.ts +21 -107
  104. package/src/panels/builtin/development.ts +15 -61
  105. package/src/panels/builtin/knowledge.ts +8 -32
  106. package/src/panels/builtin/operations.ts +84 -428
  107. package/src/panels/builtin/session.ts +21 -112
  108. package/src/panels/builtin/shared.ts +18 -46
  109. package/src/panels/builtin-modals.ts +218 -0
  110. package/src/panels/builtin-panels.ts +5 -0
  111. package/src/panels/cost-tracker-panel.ts +36 -10
  112. package/src/panels/diff-panel.ts +12 -43
  113. package/src/panels/eval-registry.ts +60 -0
  114. package/src/panels/fleet-deep-link.ts +31 -0
  115. package/src/panels/fleet-panel-format.ts +62 -0
  116. package/src/panels/fleet-panel-worktree-detail.ts +48 -0
  117. package/src/panels/fleet-panel.ts +758 -0
  118. package/src/panels/fleet-read-model.ts +520 -0
  119. package/src/panels/fleet-steer.ts +125 -0
  120. package/src/panels/fleet-stop.ts +153 -0
  121. package/src/panels/fleet-tabs.ts +230 -0
  122. package/src/panels/fleet-transcript.ts +356 -0
  123. package/src/panels/index.ts +7 -31
  124. package/src/panels/modals/hooks-modal.ts +187 -0
  125. package/src/panels/modals/keybindings-modal.ts +166 -0
  126. package/src/panels/modals/knowledge-modal.ts +158 -0
  127. package/src/panels/modals/local-auth-modal.ts +132 -0
  128. package/src/panels/modals/marketplace-modal.ts +218 -0
  129. package/src/panels/modals/memory-modal.ts +180 -0
  130. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  131. package/src/panels/modals/modal-theme.ts +42 -0
  132. package/src/panels/modals/pairing-modal.ts +163 -0
  133. package/src/panels/modals/planning-modal.ts +304 -0
  134. package/src/panels/modals/plugins-modal.ts +174 -0
  135. package/src/panels/modals/policy-modal.ts +256 -0
  136. package/src/panels/modals/provider-health-modal.ts +136 -0
  137. package/src/panels/modals/remote-modal.ts +115 -0
  138. package/src/panels/modals/sandbox-modal.ts +150 -0
  139. package/src/panels/modals/security-modal.ts +217 -0
  140. package/src/panels/modals/services-modal.ts +179 -0
  141. package/src/panels/modals/settings-sync-modal.ts +142 -0
  142. package/src/panels/modals/skills-modal.ts +189 -0
  143. package/src/panels/modals/subscription-modal.ts +172 -0
  144. package/src/panels/modals/work-plan-modal.ts +177 -0
  145. package/src/panels/panel-confirm-overlay.ts +72 -0
  146. package/src/panels/panel-manager.ts +123 -9
  147. package/src/panels/polish-core.ts +38 -25
  148. package/src/panels/project-planning-answer-actions.ts +8 -11
  149. package/src/panels/skills-panel.ts +7 -51
  150. package/src/panels/types.ts +37 -0
  151. package/src/permissions/hunk-selection.ts +179 -0
  152. package/src/permissions/prompt.ts +216 -13
  153. package/src/renderer/autocomplete-overlay.ts +28 -2
  154. package/src/renderer/compaction-history-modal.ts +19 -3
  155. package/src/renderer/compaction-preview.ts +13 -2
  156. package/src/renderer/compaction-quality.ts +100 -0
  157. package/src/renderer/compositor.ts +2 -3
  158. package/src/renderer/config-modal.ts +95 -0
  159. package/src/renderer/conversation-overlays.ts +10 -17
  160. package/src/renderer/fleet-tab-strip.ts +56 -0
  161. package/src/renderer/footer-tips.ts +15 -3
  162. package/src/renderer/fullscreen-primitives.ts +32 -22
  163. package/src/renderer/git-status.ts +43 -1
  164. package/src/renderer/help-overlay.ts +2 -2
  165. package/src/renderer/layout.ts +0 -4
  166. package/src/renderer/markdown.ts +7 -3
  167. package/src/renderer/modal-factory.ts +25 -20
  168. package/src/renderer/model-workspace.ts +62 -4
  169. package/src/renderer/overlay-box.ts +21 -17
  170. package/src/renderer/panel-workspace-bar.ts +8 -2
  171. package/src/renderer/process-indicator.ts +14 -3
  172. package/src/renderer/selection-modal-overlay.ts +6 -1
  173. package/src/renderer/session-picker-modal.ts +196 -3
  174. package/src/renderer/settings-modal-helpers.ts +2 -0
  175. package/src/renderer/settings-modal.ts +7 -0
  176. package/src/renderer/shell-surface.ts +8 -1
  177. package/src/renderer/status-glyphs.ts +14 -15
  178. package/src/renderer/system-message.ts +15 -3
  179. package/src/renderer/terminal-bg-probe.ts +339 -0
  180. package/src/renderer/terminal-escapes.ts +20 -0
  181. package/src/renderer/theme-mode-config.ts +67 -0
  182. package/src/renderer/theme.ts +91 -1
  183. package/src/renderer/thinking.ts +11 -3
  184. package/src/renderer/tool-call.ts +15 -9
  185. package/src/renderer/tool-result-summary.ts +148 -0
  186. package/src/renderer/turn-injection.ts +133 -0
  187. package/src/renderer/ui-factory.ts +154 -85
  188. package/src/renderer/ui-primitives.ts +30 -129
  189. package/src/runtime/bootstrap-command-context.ts +27 -1
  190. package/src/runtime/bootstrap-command-parts.ts +41 -10
  191. package/src/runtime/bootstrap-core.ts +47 -15
  192. package/src/runtime/bootstrap-hook-bridge.ts +7 -0
  193. package/src/runtime/bootstrap-shell.ts +48 -17
  194. package/src/runtime/bootstrap.ts +129 -22
  195. package/src/runtime/code-index-services.ts +135 -0
  196. package/src/runtime/legacy-daemon-migration.ts +516 -0
  197. package/src/runtime/memory-fold.ts +26 -0
  198. package/src/runtime/onboarding/derivation.ts +7 -2
  199. package/src/runtime/onboarding/snapshot.ts +27 -1
  200. package/src/runtime/onboarding/types.ts +31 -1
  201. package/src/runtime/operator-token-cleanup.ts +82 -1
  202. package/src/runtime/orchestrator-core-services.ts +59 -0
  203. package/src/runtime/process-lifecycle.ts +3 -1
  204. package/src/runtime/resume-notice.ts +209 -0
  205. package/src/runtime/services.ts +98 -46
  206. package/src/runtime/session-inbound-inputs.ts +252 -0
  207. package/src/runtime/session-spine-transport.ts +64 -0
  208. package/src/runtime/terminal-output-guard.ts +15 -8
  209. package/src/runtime/ui-services.ts +27 -3
  210. package/src/runtime/workstream-services.ts +327 -0
  211. package/src/runtime/wrfc-persistence.ts +124 -17
  212. package/src/shell/blocking-input.ts +68 -4
  213. package/src/shell/recovery-input-helpers.ts +170 -1
  214. package/src/shell/ui-openers.ts +171 -26
  215. package/src/utils/format-duration.ts +8 -18
  216. package/src/utils/splash-lines.ts +1 -1
  217. package/src/utils/terminal-width.ts +52 -0
  218. package/src/version.ts +1 -1
  219. package/src/panels/agent-inspector-panel.ts +0 -786
  220. package/src/panels/approval-panel.ts +0 -252
  221. package/src/panels/automation-control-panel.ts +0 -479
  222. package/src/panels/cockpit-panel.ts +0 -481
  223. package/src/panels/cockpit-read-model.ts +0 -238
  224. package/src/panels/communication-panel.ts +0 -256
  225. package/src/panels/control-plane-panel.ts +0 -470
  226. package/src/panels/debug-panel.ts +0 -615
  227. package/src/panels/docs-panel.ts +0 -384
  228. package/src/panels/eval-panel.ts +0 -627
  229. package/src/panels/file-explorer-panel.ts +0 -673
  230. package/src/panels/file-preview-panel.ts +0 -517
  231. package/src/panels/hooks-panel.ts +0 -401
  232. package/src/panels/incident-review-panel.ts +0 -406
  233. package/src/panels/intelligence-panel.ts +0 -383
  234. package/src/panels/knowledge-graph-panel.ts +0 -515
  235. package/src/panels/marketplace-panel.ts +0 -399
  236. package/src/panels/memory-panel.ts +0 -558
  237. package/src/panels/ops-control-panel.ts +0 -329
  238. package/src/panels/ops-strategy-panel.ts +0 -321
  239. package/src/panels/orchestration-panel.ts +0 -465
  240. package/src/panels/panel-list-panel.ts +0 -566
  241. package/src/panels/plan-dashboard-panel.ts +0 -707
  242. package/src/panels/policy-panel.ts +0 -517
  243. package/src/panels/project-planning-panel.ts +0 -731
  244. package/src/panels/provider-health-panel.ts +0 -678
  245. package/src/panels/provider-health-tracker.ts +0 -310
  246. package/src/panels/provider-health-views.ts +0 -567
  247. package/src/panels/qr-panel.ts +0 -280
  248. package/src/panels/remote-panel.ts +0 -534
  249. package/src/panels/routes-panel.ts +0 -241
  250. package/src/panels/sandbox-panel.ts +0 -456
  251. package/src/panels/security-panel.ts +0 -447
  252. package/src/panels/services-panel.ts +0 -329
  253. package/src/panels/session-browser-panel.ts +0 -496
  254. package/src/panels/settings-sync-panel.ts +0 -398
  255. package/src/panels/subscription-panel.ts +0 -342
  256. package/src/panels/symbol-outline-panel.ts +0 -619
  257. package/src/panels/system-messages-panel.ts +0 -364
  258. package/src/panels/tasks-panel.ts +0 -608
  259. package/src/panels/thinking-panel.ts +0 -333
  260. package/src/panels/tool-inspector-panel.ts +0 -537
  261. package/src/panels/work-plan-panel.ts +0 -540
  262. package/src/panels/worktree-panel.ts +0 -360
  263. package/src/panels/wrfc-panel.ts +0 -790
  264. package/src/renderer/agent-detail-modal.ts +0 -466
  265. package/src/renderer/live-tail-modal.ts +0 -156
  266. package/src/renderer/process-modal.ts +0 -671
  267. 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
- }