@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,540 +0,0 @@
1
- import type { Line } from '../types/grid.ts';
2
- import { truncateDisplay } from '../utils/terminal-width.ts';
3
- import { ScrollableListPanel } from './scrollable-list-panel.ts';
4
- import {
5
- buildDetailBlock,
6
- buildKeyboardHints,
7
- buildKeyValueLine,
8
- buildMeterLine,
9
- buildPanelLine,
10
- buildPanelListRow,
11
- buildSummaryBlock,
12
- DEFAULT_PANEL_PALETTE,
13
- extendPalette,
14
- type PanelPalette,
15
- } from './polish.ts';
16
- import type { WorkPlanItem, WorkPlanItemStatus, WorkPlanStore } from '../work-plans/work-plan-store.ts';
17
- import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
18
- import { isTextBackspace } from '../input/delete-key-policy.ts';
19
- import { AgentInspectorPanel } from './agent-inspector-panel.ts';
20
- import { WrfcPanel } from './wrfc-panel.ts';
21
- import type { PanelIntegrationContext } from './types.ts';
22
-
23
- type WorkPlanDraftField = 'title' | 'owner' | 'notes';
24
-
25
- const C = extendPalette(DEFAULT_PANEL_PALETTE, {
26
- pending: '#94a3b8',
27
- inProgress: '#38bdf8',
28
- blocked: '#f59e0b',
29
- done: '#22c55e',
30
- failed: '#ef4444',
31
- cancelled: '#64748b',
32
- accent: '#a5b4fc',
33
- selectBg: '#1e293b',
34
- });
35
-
36
- const STATUS_LABEL: Record<WorkPlanItemStatus, string> = {
37
- pending: '[ ]',
38
- in_progress: '[>]',
39
- blocked: '[!]',
40
- done: '[x]',
41
- failed: '[x]',
42
- cancelled: '[-]',
43
- };
44
-
45
- const STATUS_COLOR: Record<WorkPlanItemStatus, string> = {
46
- pending: C.pending,
47
- in_progress: C.inProgress,
48
- blocked: C.blocked,
49
- done: C.done,
50
- failed: C.failed,
51
- cancelled: C.cancelled,
52
- };
53
-
54
- const STATUS_ORDER: WorkPlanItemStatus[] = ['pending', 'in_progress', 'blocked', 'done', 'failed', 'cancelled'];
55
-
56
- function compactDate(value: number): string {
57
- return new Date(value).toISOString().replace('T', ' ').slice(0, 16);
58
- }
59
-
60
- function statusName(status: WorkPlanItemStatus): string {
61
- return status.replace(/_/g, ' ');
62
- }
63
-
64
- export class WorkPlanPanel extends ScrollableListPanel<WorkPlanItem> {
65
- private items: readonly WorkPlanItem[] = [];
66
- private lastPlanUpdatedAt = 0;
67
- private lastStatus: string | null = null;
68
-
69
- // Draft-input state for 'a' (add) / 'e' (edit): a small modal-like field
70
- // editor inline in the header, following the same draft-buffer pattern as
71
- // ProjectPlanningPanel's typed-answer draft (Tab cycles fields, Enter
72
- // saves, Esc cancels, Backspace edits the active field).
73
- private draftMode: 'add' | 'edit' | null = null;
74
- private draftField: WorkPlanDraftField = 'title';
75
- private draftTitle = '';
76
- private draftOwner = '';
77
- private draftNotes = '';
78
- private draftEditingItemId: string | null = null;
79
-
80
- constructor(private readonly store: WorkPlanStore) {
81
- super('work-plan', 'Work Plan', '◧', 'agent');
82
- this.showSelectionGutter = true;
83
- }
84
-
85
- onActivate(): void {
86
- super.onActivate();
87
- this.refresh();
88
- }
89
-
90
- protected override getPalette(): PanelPalette {
91
- return C;
92
- }
93
-
94
- render(width: number, height: number): Line[] {
95
- this.refresh();
96
- return this.renderList(width, height, {
97
- title: 'Work Plan',
98
- header: this.renderHeader(width),
99
- footer: this.renderFooter(width),
100
- });
101
- }
102
-
103
- /**
104
- * The add/edit draft form's title/owner/notes fields want every character
105
- * of a burst (paste, or fast typing landing in one input.feed() call)
106
- * delivered one at a time, same as it always has — see the interface doc
107
- * on `Panel.isCapturingTextBurst`.
108
- */
109
- isCapturingTextBurst(): boolean {
110
- return this.draftMode !== null;
111
- }
112
-
113
- handleInput(key: string): boolean {
114
- if (this.lastError !== null) this.clearError();
115
-
116
- if (this.draftMode) {
117
- return this.handleDraftInput(key);
118
- }
119
-
120
- const item = this.getSelectedItem();
121
- try {
122
- switch (key) {
123
- case ' ':
124
- case 'return':
125
- case 'enter':
126
- if (!item) return false;
127
- this.store.cycleItemStatus(item.id);
128
- this.refresh();
129
- return true;
130
- case '1':
131
- return this.setSelectedStatus('pending');
132
- case '2':
133
- return this.setSelectedStatus('in_progress');
134
- case '3':
135
- return this.setSelectedStatus('blocked');
136
- case '4':
137
- return this.setSelectedStatus('done');
138
- case '5':
139
- return this.setSelectedStatus('failed');
140
- case '6':
141
- return this.setSelectedStatus('cancelled');
142
- case 'd':
143
- case 'delete':
144
- if (!item) return false;
145
- this.store.removeItem(item.id);
146
- this.refresh();
147
- return true;
148
- case 'c':
149
- this.store.clearCompleted();
150
- this.refresh();
151
- return true;
152
- case 'r':
153
- this.refresh(true);
154
- return true;
155
- case 'a':
156
- this.beginAddDraft();
157
- return true;
158
- case 'e':
159
- if (!item) return false;
160
- this.beginEditDraft(item);
161
- return true;
162
- case 'x':
163
- this.exportMarkdown();
164
- return true;
165
- // 'i'/'w' jump to the selected item's linked agent/WRFC chain. The
166
- // actual navigation happens in handlePanelIntegrationAction (it
167
- // needs the PanelManager); consuming the key here just requires the
168
- // linked target to exist so the router below fires next.
169
- case 'i':
170
- return item?.linked?.agentId !== undefined;
171
- case 'w':
172
- return item?.linked?.wrfcId !== undefined;
173
- default:
174
- return super.handleInput(key);
175
- }
176
- } catch (error) {
177
- this.setError(summarizeError(error));
178
- return true;
179
- }
180
- }
181
-
182
- /**
183
- * Cross-panel jumps: 'i' opens the Inspector focused on the selected
184
- * item's linked agent; 'w' opens the WRFC panel focused on its linked
185
- * chain.
186
- */
187
- handlePanelIntegrationAction(key: string, ctx: PanelIntegrationContext): boolean {
188
- const item = this.getSelectedItem();
189
- if (!item) return false;
190
- if (key === 'i') {
191
- const agentId = item.linked?.agentId;
192
- if (!agentId) return false;
193
- const inspector = ctx.panelManager.open('inspector');
194
- if (inspector instanceof AgentInspectorPanel) {
195
- inspector.inspectAgent(agentId);
196
- return true;
197
- }
198
- return false;
199
- }
200
- if (key === 'w') {
201
- const wrfcId = item.linked?.wrfcId;
202
- if (!wrfcId) return false;
203
- const wrfc = ctx.panelManager.open('wrfc');
204
- if (wrfc instanceof WrfcPanel) {
205
- wrfc.selectChain(wrfcId);
206
- return true;
207
- }
208
- return false;
209
- }
210
- return false;
211
- }
212
-
213
- // -------------------------------------------------------------------------
214
- // Draft-input (add/edit)
215
- // -------------------------------------------------------------------------
216
-
217
- private handleDraftInput(key: string): boolean {
218
- if (key === 'escape') {
219
- this.cancelDraft();
220
- return true;
221
- }
222
- if (key === 'tab') {
223
- this.cycleDraftField();
224
- return true;
225
- }
226
- if (key === 'enter' || key === 'return') {
227
- this.commitDraft();
228
- return true;
229
- }
230
- if (isTextBackspace(key)) {
231
- this.setDraftFieldValue(this.getDraftFieldValue().slice(0, -1));
232
- return true;
233
- }
234
- if (key === 'space') {
235
- this.setDraftFieldValue(`${this.getDraftFieldValue()} `);
236
- return true;
237
- }
238
- if (this.isPrintableKey(key)) {
239
- this.setDraftFieldValue(`${this.getDraftFieldValue()}${key}`);
240
- return true;
241
- }
242
- // Absorb everything else (up/down/pageup/etc.) while drafting.
243
- return true;
244
- }
245
-
246
- private beginAddDraft(): void {
247
- this.draftMode = 'add';
248
- this.draftField = 'title';
249
- this.draftTitle = '';
250
- this.draftOwner = '';
251
- this.draftNotes = '';
252
- this.draftEditingItemId = null;
253
- this.lastStatus = null;
254
- this.needsRender = true;
255
- }
256
-
257
- private beginEditDraft(item: WorkPlanItem): void {
258
- this.draftMode = 'edit';
259
- this.draftField = 'title';
260
- this.draftTitle = item.title;
261
- this.draftOwner = item.owner ?? '';
262
- this.draftNotes = item.notes ?? '';
263
- this.draftEditingItemId = item.id;
264
- this.lastStatus = null;
265
- this.needsRender = true;
266
- }
267
-
268
- private cancelDraft(): void {
269
- this.draftMode = null;
270
- this.draftEditingItemId = null;
271
- this.needsRender = true;
272
- }
273
-
274
- private cycleDraftField(): void {
275
- const order: WorkPlanDraftField[] = ['title', 'owner', 'notes'];
276
- const next = order[(order.indexOf(this.draftField) + 1) % order.length];
277
- if (next) this.draftField = next;
278
- this.needsRender = true;
279
- }
280
-
281
- private getDraftFieldValue(): string {
282
- switch (this.draftField) {
283
- case 'title': return this.draftTitle;
284
- case 'owner': return this.draftOwner;
285
- case 'notes': return this.draftNotes;
286
- }
287
- }
288
-
289
- private setDraftFieldValue(value: string): void {
290
- switch (this.draftField) {
291
- case 'title': this.draftTitle = value; break;
292
- case 'owner': this.draftOwner = value; break;
293
- case 'notes': this.draftNotes = value; break;
294
- }
295
- this.needsRender = true;
296
- }
297
-
298
- private commitDraft(): void {
299
- const title = this.draftTitle.trim();
300
- if (!title) {
301
- this.setError('Work plan item title is required.');
302
- return;
303
- }
304
- try {
305
- if (this.draftMode === 'add') {
306
- this.store.addItem(title, {
307
- owner: this.draftOwner.trim() || undefined,
308
- notes: this.draftNotes.trim() || undefined,
309
- source: 'tui-panel',
310
- });
311
- this.lastStatus = 'Item added.';
312
- } else if (this.draftMode === 'edit' && this.draftEditingItemId) {
313
- this.store.updateItem(this.draftEditingItemId, {
314
- title,
315
- owner: this.draftOwner.trim() || null,
316
- notes: this.draftNotes.trim() || null,
317
- });
318
- this.lastStatus = 'Item updated.';
319
- }
320
- this.draftMode = null;
321
- this.draftEditingItemId = null;
322
- this.refresh(true);
323
- } catch (error) {
324
- this.setError(summarizeError(error));
325
- }
326
- }
327
-
328
- private isPrintableKey(key: string): boolean {
329
- return key.length === 1 && key >= ' ';
330
- }
331
-
332
- private exportMarkdown(): void {
333
- try {
334
- const { path } = this.store.exportMarkdown();
335
- this.lastStatus = `Exported to ${path}`;
336
- } catch (error) {
337
- this.setError(summarizeError(error));
338
- }
339
- }
340
-
341
- protected getItems(): readonly WorkPlanItem[] {
342
- return this.items;
343
- }
344
-
345
- protected override getEmptyStateMessage(): string {
346
- return ' No work plan items yet';
347
- }
348
-
349
- protected getEmptyStateActions(): Array<{ command: string; summary: string }> {
350
- // WO-160: '/work-plan add <title>' dropped — 'a' already opens an
351
- // in-panel add-item draft even from this empty state (see
352
- // beginAddDraft), so the printed command was a pure action substitute.
353
- return [
354
- { command: '/work-plan list', summary: 'print the current plan to the shell' },
355
- ];
356
- }
357
-
358
- protected renderItem(item: WorkPlanItem, _index: number, selected: boolean, width: number): Line {
359
- const status = STATUS_LABEL[item.status];
360
- const owner = item.owner ? ` @${item.owner}` : '';
361
- const source = item.source ? ` (${item.source})` : '';
362
- const reserve = status.length + owner.length + source.length + 4;
363
- const title = truncateDisplay(item.title, Math.max(6, width - reserve));
364
- return buildPanelListRow(width, [
365
- { text: `${status} `, fg: STATUS_COLOR[item.status], bold: item.status === 'in_progress' },
366
- { text: title, fg: selected ? C.value : C.label, bold: selected || item.status === 'in_progress' },
367
- { text: owner, fg: C.accent },
368
- { text: source, fg: C.dim },
369
- ], C, { selected, selectedBg: C.selectBg });
370
- }
371
-
372
- private setSelectedStatus(status: WorkPlanItemStatus): boolean {
373
- const item = this.getSelectedItem();
374
- if (!item) return false;
375
- this.store.setItemStatus(item.id, status);
376
- this.refresh();
377
- return true;
378
- }
379
-
380
- private refresh(force = false): void {
381
- const plan = this.store.getActivePlan();
382
- // updatedAt is bumped by every store mutation (addItem/updateItem/
383
- // removeItem/clearCompleted), so it alone is a sufficient staleness
384
- // check — no need to also compare item counts.
385
- if (!force && plan.updatedAt === this.lastPlanUpdatedAt) return;
386
- this.items = plan.items;
387
- this.lastPlanUpdatedAt = plan.updatedAt;
388
- this.clampSelection();
389
- this.needsRender = true;
390
- }
391
-
392
- private renderHeader(width: number): Line[] {
393
- if (this.draftMode) return this.renderDraftForm(width);
394
- const plan = this.store.getActivePlan();
395
- const counts = new Map<WorkPlanItemStatus, number>();
396
- for (const status of STATUS_ORDER) counts.set(status, 0);
397
- for (const item of plan.items) counts.set(item.status, (counts.get(item.status) ?? 0) + 1);
398
-
399
- const total = plan.items.length;
400
- const done = (counts.get('done') ?? 0) + (counts.get('cancelled') ?? 0);
401
- const pct = total > 0 ? Math.round((done / total) * 100) : 0;
402
- const meterWidth = Math.max(10, Math.min(26, width - 34));
403
-
404
- const postureLines: Line[] = [
405
- buildPanelLine(width, [
406
- [' Project ', C.label],
407
- [truncateDisplay(plan.projectRoot, Math.max(8, width - 11)), C.value],
408
- ]),
409
- buildMeterLine(width, Math.round((pct / 100) * meterWidth), meterWidth, {
410
- filled: pct === 100 ? C.done : C.inProgress,
411
- empty: C.empty,
412
- label: C.label,
413
- }, { prefix: ' Progress ', suffix: ` ${done}/${total} (${pct}%) ` }),
414
- buildKeyValueLine(width, [
415
- { label: 'pending', value: String(counts.get('pending') ?? 0), valueColor: (counts.get('pending') ?? 0) > 0 ? C.pending : C.dim },
416
- { label: 'active', value: String(counts.get('in_progress') ?? 0), valueColor: (counts.get('in_progress') ?? 0) > 0 ? C.inProgress : C.dim },
417
- { label: 'blocked', value: String(counts.get('blocked') ?? 0), valueColor: (counts.get('blocked') ?? 0) > 0 ? C.blocked : C.dim },
418
- { label: 'done', value: String(counts.get('done') ?? 0), valueColor: (counts.get('done') ?? 0) > 0 ? C.done : C.dim },
419
- ], C),
420
- // WO-160: dropped the printed '/work-plan add <title>' guidance line —
421
- // 'a' already opens an in-panel add-item draft (see beginAddDraft /
422
- // handleDraftInput) and is advertised in the footer's 'a: add' hint,
423
- // so the printed command was a pure action substitute.
424
- ];
425
-
426
- const header: Line[] = buildSummaryBlock(width, 'Persistent Work Plan', postureLines, C);
427
-
428
- const active = this.getSelectedItem();
429
- if (active) {
430
- const detailRows: Line[] = [
431
- buildPanelLine(width, [
432
- [' ', C.label],
433
- [STATUS_LABEL[active.status], STATUS_COLOR[active.status]],
434
- [' ', C.label],
435
- [truncateDisplay(active.title, Math.max(8, width - 6)), C.value],
436
- ]),
437
- buildKeyValueLine(width, [
438
- { label: 'status', value: statusName(active.status), valueColor: STATUS_COLOR[active.status] },
439
- ...(active.owner ? [{ label: 'owner', value: active.owner, valueColor: C.accent }] : []),
440
- { label: 'updated', value: compactDate(active.updatedAt), valueColor: C.dim },
441
- ], C),
442
- ];
443
- if (active.source) {
444
- detailRows.push(buildPanelLine(width, [[' source ', C.label], [active.source, C.info]]));
445
- }
446
- if (active.notes) {
447
- detailRows.push(buildPanelLine(width, [
448
- [' notes ', C.label],
449
- [truncateDisplay(active.notes, Math.max(8, width - 8)), C.value],
450
- ]));
451
- }
452
- const linkedSegments = this.buildLinkedSegments(active);
453
- if (linkedSegments.length > 0) {
454
- detailRows.push(buildPanelLine(width, [[' linked ', C.label], ...linkedSegments]));
455
- }
456
- header.push(...buildDetailBlock(width, 'Selected item', detailRows, C));
457
- }
458
- return header;
459
- }
460
-
461
- /** Renders item.linked (agentId/wrfcId/taskId/sessionId) with their jump keys. */
462
- private buildLinkedSegments(item: WorkPlanItem): Array<[string, string]> {
463
- const linked = item.linked;
464
- if (!linked) return [];
465
- const segments: Array<[string, string]> = [];
466
- if (linked.agentId) segments.push([`agent:${linked.agentId} (i) `, C.info]);
467
- if (linked.wrfcId) segments.push([`wrfc:${linked.wrfcId} (w) `, C.info]);
468
- if (linked.taskId) segments.push([`task:${linked.taskId} `, C.dim]);
469
- if (linked.sessionId) segments.push([`session:${linked.sessionId} `, C.dim]);
470
- return segments;
471
- }
472
-
473
- private renderDraftForm(width: number): Line[] {
474
- const title = this.draftMode === 'add' ? 'Add Work Plan Item' : 'Edit Work Plan Item';
475
- const field = (label: string, value: string, key: WorkPlanDraftField): Line => {
476
- const active = key === this.draftField;
477
- const text = active ? `${value}▏` : (value || '(empty)');
478
- return buildPanelLine(width, [
479
- [` ${label.padEnd(6)} `, C.label],
480
- [truncateDisplay(text, Math.max(8, width - 10)), active ? C.value : C.dim],
481
- ]);
482
- };
483
- return buildSummaryBlock(width, title, [
484
- buildPanelLine(width, [[' Tab next field Enter save Esc cancel', C.dim]]),
485
- field('Title', this.draftTitle, 'title'),
486
- field('Owner', this.draftOwner, 'owner'),
487
- field('Notes', this.draftNotes, 'notes'),
488
- ], C);
489
- }
490
-
491
- private renderFooter(width: number): Line[] {
492
- if (this.draftMode) {
493
- return [
494
- buildKeyboardHints(width, [
495
- { keys: 'Tab', label: 'next field' },
496
- { keys: 'Enter', label: 'save' },
497
- { keys: 'Esc', label: 'cancel' },
498
- ], C),
499
- ];
500
- }
501
- const hasItem = this.items.length > 0;
502
- if (!hasItem) {
503
- return [
504
- buildKeyboardHints(width, [
505
- { keys: '↑/↓', label: 'navigate' },
506
- { keys: 'a', label: 'add' },
507
- ], C),
508
- ];
509
- }
510
- const active = this.getSelectedItem();
511
- const hints: Array<{ keys: string; label: string }> = [
512
- { keys: this.items.length > 0 ? `${this.selectedIndex + 1}/${this.items.length}` : '0/0', label: 'item' },
513
- { keys: 'Enter', label: 'cycle status' },
514
- { keys: '1-6', label: 'set status' },
515
- { keys: 'a', label: 'add' },
516
- { keys: 'e', label: 'edit' },
517
- { keys: 'd', label: 'delete' },
518
- { keys: 'c', label: 'clear done' },
519
- { keys: 'x', label: 'export' },
520
- { keys: 'r', label: 'refresh' },
521
- ];
522
- if (active?.linked?.agentId) hints.push({ keys: 'i', label: 'jump agent' });
523
- if (active?.linked?.wrfcId) hints.push({ keys: 'w', label: 'jump wrfc' });
524
- const lines: Line[] = [
525
- buildKeyboardHints(width, hints, C),
526
- buildPanelLine(width, [
527
- [' 1', C.info], [' pending ', C.dim],
528
- ['2', C.info], [' active ', C.dim],
529
- ['3', C.info], [' blocked ', C.dim],
530
- ['4', C.info], [' done ', C.dim],
531
- ['5', C.info], [' failed ', C.dim],
532
- ['6', C.info], [' cancelled', C.dim],
533
- ]),
534
- ];
535
- if (this.lastStatus) {
536
- lines.push(buildPanelLine(width, [[` ${this.lastStatus}`, C.good]]));
537
- }
538
- return lines;
539
- }
540
- }