@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,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
- }