@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,731 +0,0 @@
1
- import type {
2
- ProjectPlanningDecision,
3
- ProjectPlanningEvaluation,
4
- ProjectPlanningLanguageArtifact,
5
- ProjectPlanningQuestion,
6
- ProjectPlanningService,
7
- ProjectPlanningState,
8
- ProjectPlanningStatus,
9
- } from '@pellux/goodvibes-sdk/platform/knowledge';
10
- import type { Line } from '../types/grid.ts';
11
- import { BasePanel } from './base-panel.ts';
12
- import { handleConfirmInput, renderConfirmLines, type ConfirmState } from './confirm-state.ts';
13
- import { isTextBackspace, isTextForwardDelete } from '../input/delete-key-policy.ts';
14
- import {
15
- buildBodyText,
16
- buildEmptyState,
17
- buildKeyValueLine,
18
- buildPanelLine,
19
- buildPanelListRow,
20
- buildPanelWorkspace,
21
- DEFAULT_PANEL_PALETTE,
22
- extendPalette,
23
- resolveScrollablePanelSection,
24
- type PanelWorkspaceSection,
25
- } from './polish.ts';
26
- import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
27
- import { buildAnswerActions, isGenericRecommendation, type PlanningAnswerAction } from './project-planning-answer-actions.ts';
28
-
29
- const C = extendPalette(DEFAULT_PANEL_PALETTE, {
30
- planning: '#38bdf8',
31
- blocked: '#f97316',
32
- approved: '#22c55e',
33
- rejected: '#ef4444',
34
- });
35
-
36
- interface ProjectPlanningPanelSnapshot {
37
- readonly status: ProjectPlanningStatus | null;
38
- readonly state: ProjectPlanningState | null;
39
- readonly evaluation: ProjectPlanningEvaluation | null;
40
- readonly decisions: readonly ProjectPlanningDecision[];
41
- readonly language: ProjectPlanningLanguageArtifact | null;
42
- }
43
-
44
- export interface ProjectPlanningPanelOptions {
45
- readonly service: ProjectPlanningService;
46
- readonly projectId: string;
47
- readonly requestRender?: () => void;
48
- readonly submitAnswer?: (answer: string) => void;
49
- readonly dismissPlanning?: () => void;
50
- }
51
-
52
- interface RenderedPlanningSection extends PanelWorkspaceSection {
53
- readonly selectedLineIndex?: number;
54
- }
55
-
56
- export class ProjectPlanningPanel extends BasePanel {
57
- private readonly service: ProjectPlanningService;
58
- private readonly projectId: string;
59
- private readonly requestRender: () => void;
60
- private readonly submitAnswer: ((answer: string) => void) | undefined;
61
- private readonly dismissPlanning: (() => void) | undefined;
62
- private snapshot: ProjectPlanningPanelSnapshot | null = null;
63
- private loading = false;
64
- private scrollOffset = 0;
65
- private selectedActionIndex = 0;
66
- private draftAnswer = '';
67
- // Pending confirmation for Delete (clear draft). Null when inactive.
68
- private clearDraftConfirm: ConfirmState<'clear-draft'> | null = null;
69
-
70
- public constructor(options: ProjectPlanningPanelOptions) {
71
- super('project-planning', 'Planning', 'P', 'agent');
72
- this.service = options.service;
73
- this.projectId = options.projectId;
74
- this.requestRender = options.requestRender ?? (() => {});
75
- this.submitAnswer = options.submitAnswer;
76
- this.dismissPlanning = options.dismissPlanning;
77
- }
78
-
79
- public override onActivate(): void {
80
- super.onActivate();
81
- this.refresh();
82
- }
83
-
84
- /**
85
- * The draft-answer text field wants every character of a burst (paste, or
86
- * fast typing landing in one input.feed() call) delivered one at a time,
87
- * same as it always has — see the interface doc on
88
- * `Panel.isCapturingTextBurst`.
89
- */
90
- public isCapturingTextBurst(): boolean {
91
- return this.getCurrentQuestion() !== null;
92
- }
93
-
94
- public handleInput(key: string): boolean {
95
- if (this.lastError !== null) this.clearError();
96
-
97
- // ConfirmState gate: Delete (clear draft) requires y/n confirmation.
98
- // handleConfirmInput absorbs all keys while a confirmation is pending.
99
- const confirmResult = handleConfirmInput(this.clearDraftConfirm, key);
100
- if (confirmResult === 'confirmed') {
101
- this.draftAnswer = '';
102
- this.clearDraftConfirm = null;
103
- this.markDirty();
104
- return true;
105
- }
106
- if (confirmResult === 'cancelled') {
107
- this.clearDraftConfirm = null;
108
- this.markDirty();
109
- return true;
110
- }
111
- if (confirmResult === 'absorbed') {
112
- return true;
113
- }
114
- // confirmResult === 'inactive': proceed with normal dispatch.
115
-
116
- // Ctrl+R (refresh) / Ctrl+A (approve) are alternate bindings for 'r'/'a'
117
- // that stay reachable while a question is active, where plain 'r'/'a'
118
- // are swallowed into the draft-answer text below instead. They are
119
- // checked before the question gate so they work in both modes.
120
- //
121
- // NOTE: the input-routing layer (src/input/handler-feed-routes.ts, owned
122
- // by WO-150 in this wave) does not yet forward ctrl-modified keys to the
123
- // active panel — it currently intercepts all `token.ctrl` input before
124
- // panel.handleInput is ever called, and a bare Ctrl+R is already bound
125
- // globally to 'history-search'. This handler recognizes the logical key
126
- // strings 'ctrl+r'/'ctrl+a' so the panel-side behavior is correct and
127
- // testable now; wiring a real Ctrl+R/Ctrl+A keypress through to these
128
- // strings is an input-handler change outside this work order's file
129
- // scope.
130
- if (key === 'ctrl+r') {
131
- this.refresh(true);
132
- return true;
133
- }
134
- if (key === 'ctrl+a') {
135
- this.approveExecution();
136
- return true;
137
- }
138
-
139
- const question = this.getCurrentQuestion();
140
- if (question) {
141
- const actions = buildAnswerActions(question, this.draftAnswer);
142
- this.selectedActionIndex = this.clampActionIndex(actions.length);
143
- if (key === 'up') {
144
- this.selectedActionIndex = Math.max(0, this.selectedActionIndex - 1);
145
- this.markDirty();
146
- return true;
147
- }
148
- if (key === 'down') {
149
- this.selectedActionIndex = Math.min(Math.max(0, actions.length - 1), this.selectedActionIndex + 1);
150
- this.markDirty();
151
- return true;
152
- }
153
- if (key === 'enter' || key === 'return') {
154
- this.submitSelectedAction(question, actions);
155
- return true;
156
- }
157
- if (isTextBackspace(key)) {
158
- this.draftAnswer = this.draftAnswer.slice(0, -1);
159
- this.markDirty();
160
- return true;
161
- }
162
- // 'delete' opens the clear-draft confirmation gate (per delete-key policy).
163
- // The draft is not wiped until the user confirms with y/Enter.
164
- if (isTextForwardDelete(key)) {
165
- this.clearDraftConfirm = { subject: 'clear-draft', label: 'draft answer' };
166
- this.markDirty();
167
- return true;
168
- }
169
- if (key === 'space') {
170
- this.draftAnswer += ' ';
171
- this.markDirty();
172
- return true;
173
- }
174
- if (key === 'pageup') {
175
- this.scrollOffset = Math.max(0, this.scrollOffset - 6);
176
- this.markDirty();
177
- return true;
178
- }
179
- if (key === 'pagedown') {
180
- this.scrollOffset += 6;
181
- this.markDirty();
182
- return true;
183
- }
184
- if (this.isPrintableKey(key)) {
185
- this.draftAnswer += key;
186
- this.markDirty();
187
- return true;
188
- }
189
- return false;
190
- }
191
-
192
- if (key === 'r' || key === 'R') {
193
- this.refresh(true);
194
- return true;
195
- }
196
- if (key === 'a' || key === 'A') {
197
- this.approveExecution();
198
- return true;
199
- }
200
- if (key === 'up' || key === 'k') {
201
- this.scrollOffset = Math.max(0, this.scrollOffset - 1);
202
- this.markDirty();
203
- return true;
204
- }
205
- if (key === 'down' || key === 'j') {
206
- this.scrollOffset += 1;
207
- this.markDirty();
208
- return true;
209
- }
210
- return false;
211
- }
212
-
213
- public render(width: number, height: number): Line[] {
214
- return this.trackedRender(() => {
215
- if (!this.snapshot && !this.loading) this.refresh();
216
-
217
- const sections: RenderedPlanningSection[] = [];
218
- const status = this.snapshot?.status;
219
- const state = this.snapshot?.state;
220
- const evaluation = this.snapshot?.evaluation ?? null;
221
- const language = this.snapshot?.language ?? null;
222
- const decisions = this.snapshot?.decisions ?? [];
223
-
224
- sections.push({
225
- title: 'Workspace',
226
- lines: [
227
- buildKeyValueLine(width, [
228
- { label: 'project', value: this.projectId, valueColor: C.planning },
229
- { label: 'space', value: status?.knowledgeSpaceId ?? `project:${this.projectId}`, valueColor: C.value },
230
- { label: 'mode', value: 'TUI-owned passive backing store', valueColor: C.info },
231
- ], C),
232
- // Live artifact counts from the SDK status route (same counts /plan
233
- // prints at planning-runtime.ts:90) instead of static filler prose.
234
- status
235
- ? buildKeyValueLine(width, [
236
- { label: 'states', value: String(status.counts.states), valueColor: status.counts.states > 0 ? C.value : C.dim },
237
- { label: 'decisions', value: String(status.counts.decisions), valueColor: status.counts.decisions > 0 ? C.value : C.dim },
238
- { label: 'language', value: String(status.counts.languageArtifacts), valueColor: status.counts.languageArtifacts > 0 ? C.value : C.dim },
239
- ], C)
240
- : buildPanelLine(width, [[' Artifact counts unavailable — refreshing…', C.dim]]),
241
- ],
242
- });
243
-
244
- if (!state) {
245
- sections.push({
246
- title: 'No Active Planning State',
247
- lines: buildEmptyState(
248
- width,
249
- 'No project planning state has been saved for this workspace.',
250
- 'Describe the intended change in normal chat to let the TUI start the planning interview. The SDK only stores and evaluates artifacts.',
251
- [],
252
- C,
253
- ),
254
- });
255
- } else {
256
- sections.push(this.buildStateSection(width, state, evaluation));
257
- const question = this.getCurrentQuestion();
258
- if (question) sections.push(this.buildQuestionSection(width, question));
259
- sections.push(this.buildGapsSection(width, evaluation));
260
- sections.push(this.buildTasksSection(width, state));
261
- sections.push(this.buildAnsweredHistorySection(width, state));
262
- sections.push(this.buildDecisionsSection(width, state, decisions));
263
- sections.push(this.buildLanguageSection(width, language));
264
- }
265
-
266
- if (this.lastError) {
267
- const line = this.renderErrorLine(width);
268
- if (line) sections.push({ title: 'Error', lines: [line] });
269
- }
270
-
271
- const { lines: flattened, selectedIndex } = this.flattenSections(width, sections);
272
- const scroll = resolveScrollablePanelSection(width, height, {
273
- intro: 'Project planning state, readiness gaps, decisions, language, task graph, verification gates, and agent handoff metadata.',
274
- footerLines: this.footerLines(width),
275
- palette: C,
276
- section: {
277
- title: 'Project Planning',
278
- scrollableLines: flattened,
279
- selectedIndex,
280
- scrollOffset: this.scrollOffset,
281
- appendWindowSummary: {},
282
- minRows: 8,
283
- },
284
- });
285
- this.scrollOffset = scroll.scrollOffset;
286
-
287
- return buildPanelWorkspace(width, height, {
288
- title: this.loading ? 'Project Planning - loading' : 'Project Planning',
289
- intro: 'Passive SDK-backed planning artifacts for the current workspace. Conversation control stays inside this TUI.',
290
- sections: [scroll.section],
291
- footerLines: this.footerLines(width),
292
- palette: C,
293
- });
294
- });
295
- }
296
-
297
- private footerLines(width: number): Line[] {
298
- const hasQuestion = this.getCurrentQuestion() !== null;
299
- if (hasQuestion) {
300
- return [
301
- buildPanelLine(width, [
302
- [' Up/Down', C.info],
303
- [' choose answer ', C.dim],
304
- ['type', C.info],
305
- [' draft ', C.dim],
306
- ['Backspace', C.info],
307
- [' edit ', C.dim],
308
- ['Del', C.info],
309
- [' clear draft ', C.dim],
310
- ['Enter', C.info],
311
- [' submit Esc prompt focus Ctrl+X close panel', C.dim],
312
- ]),
313
- buildPanelLine(width, [
314
- [' Ctrl+R', C.info],
315
- [' refresh ', C.dim],
316
- ['Ctrl+A', C.info],
317
- [' approve (r/a type into the draft while a question is active)', C.dim],
318
- ]),
319
- ];
320
- }
321
- return [
322
- buildPanelLine(width, [
323
- [' Up/Down', C.info],
324
- [' scroll ', C.dim],
325
- ['r', C.info],
326
- [' refresh ', C.dim],
327
- ['a', C.info],
328
- [' approve execution-ready plan Esc prompt focus Ctrl+X close panel', C.dim],
329
- ]),
330
- ];
331
- }
332
-
333
- private flattenSections(
334
- width: number,
335
- sections: readonly RenderedPlanningSection[],
336
- ): { readonly lines: Line[]; readonly selectedIndex: number } {
337
- const lines: Line[] = [];
338
- let selectedIndex = 0;
339
- for (const section of sections) {
340
- const sectionStart = lines.length;
341
- const titleOffset = section.title ? 1 : 0;
342
- if (section.title) lines.push(buildPanelLine(width, [[` ${section.title}`, C.label]]));
343
- lines.push(...section.lines);
344
- if (section.selectedLineIndex !== undefined) {
345
- selectedIndex = sectionStart + titleOffset + section.selectedLineIndex;
346
- }
347
- }
348
- return { lines, selectedIndex };
349
- }
350
-
351
- private buildQuestionSection(width: number, question: ProjectPlanningQuestion): RenderedPlanningSection {
352
- const actions = buildAnswerActions(question, this.draftAnswer);
353
- this.selectedActionIndex = this.clampActionIndex(actions.length);
354
- // When a clear-draft confirmation is pending, show the confirm prompt
355
- // inline above the draft line instead of the normal content.
356
- if (this.clearDraftConfirm) {
357
- const confirmLines = renderConfirmLines(width, this.clearDraftConfirm);
358
- const lines: Line[] = [
359
- ...buildBodyText(width, question.prompt, C, C.planning),
360
- ...confirmLines,
361
- ];
362
- return { title: 'Answer Current Question', lines, selectedLineIndex: undefined };
363
- }
364
- const lines: Line[] = [
365
- ...buildBodyText(width, question.prompt, C, C.planning),
366
- ];
367
- if (question.whyItMatters) {
368
- lines.push(...buildBodyText(width, `Why this matters: ${question.whyItMatters}`, C, C.dim));
369
- }
370
- if (question.recommendedAnswer && !isGenericRecommendation(question.recommendedAnswer)) {
371
- lines.push(...buildBodyText(width, `Recommendation: ${question.recommendedAnswer}`, C, C.good));
372
- }
373
- lines.push(...buildBodyText(
374
- width,
375
- `Typed answer: ${this.draftAnswer || '(type here while this panel is focused)'}`,
376
- C,
377
- this.draftAnswer ? C.value : C.dim,
378
- ));
379
- lines.push(buildPanelLine(width, [[
380
- ' Select an answer below or type your own. Enter sends it through the normal planning chat path.',
381
- C.dim,
382
- ]]));
383
- const selectedLineIndex = lines.length + this.selectedActionIndex;
384
- actions.forEach((action, index) => {
385
- const selected = index === this.selectedActionIndex;
386
- lines.push(buildPanelListRow(width, [
387
- { text: action.label, fg: action.disabled ? C.dim : C.value, bold: selected },
388
- { text: ` ${action.detail}`, fg: C.dim },
389
- ], C, {
390
- selected,
391
- marker: selected ? '▶' : ' ',
392
- }));
393
- });
394
- return { title: 'Answer Current Question', lines, selectedLineIndex };
395
- }
396
-
397
- private buildStateSection(
398
- width: number,
399
- state: ProjectPlanningState,
400
- evaluation: ProjectPlanningEvaluation | null,
401
- ): PanelWorkspaceSection {
402
- const readiness = evaluation?.readiness ?? state.readiness;
403
- const readinessColor = readiness === 'executable'
404
- ? C.approved
405
- : readiness === 'needs-user-input'
406
- ? C.blocked
407
- : C.dim;
408
- const blockingGaps = (evaluation?.gaps ?? []).filter((gap) => gap.severity === 'blocking').length;
409
- const openQuestions = state.openQuestions.filter((q) => (q.status ?? 'open') === 'open').length;
410
- // Surface the most important thing first: is this plan executable, and what
411
- // is the single blocker to getting there.
412
- const nextStep = state.executionApproved
413
- ? 'approved — execution may proceed'
414
- : openQuestions > 0
415
- ? `answer ${openQuestions} open question${openQuestions === 1 ? '' : 's'}`
416
- : blockingGaps > 0
417
- ? `resolve ${blockingGaps} blocking gap${blockingGaps === 1 ? '' : 's'}`
418
- : readiness === 'executable'
419
- ? 'press a to approve execution'
420
- : 'continue the planning interview';
421
- const lines: Line[] = [
422
- buildKeyValueLine(width, [
423
- { label: 'readiness', value: readiness, valueColor: readinessColor },
424
- { label: 'approved', value: state.executionApproved ? 'yes' : 'no', valueColor: state.executionApproved ? C.approved : C.blocked },
425
- { label: 'questions', value: `${state.openQuestions.length} open / ${state.answeredQuestions.length} answered`, valueColor: C.value },
426
- ], C),
427
- buildKeyValueLine(width, [
428
- { label: 'blocking gaps', value: String(blockingGaps), valueColor: blockingGaps > 0 ? C.blocked : C.good },
429
- { label: 'tasks', value: String(state.tasks.length), valueColor: state.tasks.length > 0 ? C.value : C.dim },
430
- { label: 'gates', value: String(state.verificationGates.length), valueColor: state.verificationGates.length > 0 ? C.good : C.dim },
431
- ], C),
432
- buildPanelLine(width, [
433
- [' Next step ', C.label],
434
- [nextStep, state.executionApproved ? C.approved : C.planning],
435
- ]),
436
- ...buildBodyText(width, `Goal: ${state.goal || '(not set)'}`, C, state.goal ? C.value : C.blocked),
437
- ];
438
- if (state.scope) lines.push(...buildBodyText(width, `Scope: ${state.scope}`, C, C.value));
439
- if (state.knownContext.length) {
440
- lines.push(...buildBodyText(width, `Known context: ${state.knownContext.join(' | ')}`, C, C.dim));
441
- }
442
- if (evaluation?.nextQuestion) {
443
- lines.push(...buildBodyText(width, `Next question: ${evaluation.nextQuestion.prompt}`, C, C.planning));
444
- if (evaluation.nextQuestion.whyItMatters) {
445
- lines.push(...buildBodyText(width, `Why it matters: ${evaluation.nextQuestion.whyItMatters}`, C, C.dim));
446
- }
447
- if (evaluation.nextQuestion.recommendedAnswer && !isGenericRecommendation(evaluation.nextQuestion.recommendedAnswer)) {
448
- lines.push(...buildBodyText(width, `Recommended answer: ${evaluation.nextQuestion.recommendedAnswer}`, C, C.good));
449
- }
450
- }
451
- return { title: 'State', lines };
452
- }
453
-
454
- private buildGapsSection(width: number, evaluation: ProjectPlanningEvaluation | null): PanelWorkspaceSection {
455
- const gaps = evaluation?.gaps ?? [];
456
- if (gaps.length === 0) {
457
- return {
458
- title: 'Readiness Gaps',
459
- lines: [buildPanelLine(width, [[' No readiness gaps.', C.good]])],
460
- };
461
- }
462
- return {
463
- title: 'Readiness Gaps',
464
- lines: gaps.slice(0, 12).flatMap((gap) => buildBodyText(
465
- width,
466
- `${gap.severity.toUpperCase()} ${gap.kind}: ${gap.message}`,
467
- C,
468
- gap.severity === 'blocking' ? C.blocked : C.warn,
469
- )),
470
- };
471
- }
472
-
473
- private buildTasksSection(width: number, state: ProjectPlanningState): PanelWorkspaceSection {
474
- const lines: Line[] = [];
475
- if (state.tasks.length === 0) {
476
- lines.push(buildPanelLine(width, [[' No decomposed tasks recorded yet.', C.dim]]));
477
- } else {
478
- for (const task of state.tasks) {
479
- lines.push(...buildBodyText(
480
- width,
481
- `${task.id}: ${task.title} [${task.status ?? 'pending'}]${task.canRunConcurrently ? ' - concurrent' : ''}`,
482
- C,
483
- task.blockedOnUserInput ? C.blocked : C.value,
484
- ));
485
- if (task.dependencies?.length) lines.push(...buildBodyText(width, `Dependencies: ${task.dependencies.join(', ')}`, C, C.dim));
486
- if (task.verification?.length) lines.push(...buildBodyText(width, `Verification: ${task.verification.join(' | ')}`, C, C.good));
487
- }
488
- }
489
-
490
- if (state.verificationGates.length) {
491
- lines.push(buildPanelLine(width, [[' Verification gates:', C.label]]));
492
- for (const gate of state.verificationGates) {
493
- lines.push(...buildBodyText(width, `${gate.id}: ${gate.description} [${gate.status ?? 'pending'}]`, C, gate.required === false ? C.dim : C.good));
494
- }
495
- }
496
- if (state.agentAssignments.length) {
497
- lines.push(buildPanelLine(width, [[' Agent handoff candidates:', C.label]]));
498
- for (const assignment of state.agentAssignments) {
499
- lines.push(...buildBodyText(
500
- width,
501
- `${assignment.taskId}: ${assignment.agentType ?? 'none'}${assignment.canRunConcurrently ? ' - can run concurrently' : ''}`,
502
- C,
503
- C.info,
504
- ));
505
- }
506
- }
507
- return { title: 'Task Graph', lines };
508
- }
509
-
510
- private buildAnsweredHistorySection(width: number, state: ProjectPlanningState): PanelWorkspaceSection {
511
- const answered = state.answeredQuestions;
512
- if (answered.length === 0) {
513
- return {
514
- title: 'Answered Questions',
515
- lines: [buildPanelLine(width, [[' No questions answered yet.', C.dim]])],
516
- };
517
- }
518
- const ordered = [...answered].sort((a, b) => (b.answeredAt ?? 0) - (a.answeredAt ?? 0));
519
- return {
520
- title: 'Answered Questions',
521
- lines: ordered.slice(0, 12).flatMap((entry) => [
522
- ...buildBodyText(width, `Q: ${entry.prompt}`, C, C.value),
523
- ...buildBodyText(width, `A: ${entry.answer?.trim() || '(no answer recorded)'}`, C, entry.answer?.trim() ? C.good : C.dim),
524
- ]),
525
- };
526
- }
527
-
528
- private buildDecisionsSection(
529
- width: number,
530
- state: ProjectPlanningState,
531
- storedDecisions: readonly ProjectPlanningDecision[],
532
- ): PanelWorkspaceSection {
533
- const byId = new Map<string, ProjectPlanningDecision>();
534
- for (const decision of [...storedDecisions, ...state.decisions]) byId.set(decision.id, decision);
535
- const decisions = [...byId.values()];
536
- if (decisions.length === 0) {
537
- return {
538
- title: 'Decisions',
539
- lines: [buildPanelLine(width, [[' No durable planning decisions recorded yet.', C.dim]])],
540
- };
541
- }
542
- return {
543
- title: 'Decisions',
544
- lines: decisions.slice(0, 12).flatMap((decision) => buildBodyText(
545
- width,
546
- `${decision.title}: ${decision.decision} [${decision.status ?? 'accepted'}]`,
547
- C,
548
- decision.status === 'rejected' ? C.rejected : C.value,
549
- )),
550
- };
551
- }
552
-
553
- private buildLanguageSection(width: number, language: ProjectPlanningLanguageArtifact | null): PanelWorkspaceSection {
554
- if (!language || (language.terms.length === 0 && language.ambiguities.length === 0)) {
555
- return {
556
- title: 'Project Language',
557
- lines: [buildPanelLine(width, [[' No project language terms or ambiguity resolutions recorded yet.', C.dim]])],
558
- };
559
- }
560
- const lines: Line[] = [];
561
- for (const term of language.terms.slice(0, 8)) {
562
- lines.push(...buildBodyText(width, `${term.term}: ${term.definition}`, C, C.value));
563
- if (term.avoid?.length) lines.push(...buildBodyText(width, `Avoid: ${term.avoid.join(', ')}`, C, C.blocked));
564
- }
565
- for (const ambiguity of language.ambiguities.slice(0, 8)) {
566
- lines.push(...buildBodyText(width, `Resolved ambiguity - ${ambiguity.phrase}: ${ambiguity.resolution}`, C, C.info));
567
- }
568
- return { title: 'Project Language', lines };
569
- }
570
-
571
- private refresh(force = false): void {
572
- if (this.loading && !force) return;
573
- this.loading = true;
574
- this.markDirty();
575
- this.requestRender();
576
- void (async () => {
577
- try {
578
- const [status, stateResult, decisionsResult, languageResult] = await Promise.all([
579
- this.service.status({ projectId: this.projectId }),
580
- this.service.getState({ projectId: this.projectId }),
581
- this.service.listDecisions({ projectId: this.projectId }),
582
- this.service.getLanguage({ projectId: this.projectId }),
583
- ]);
584
- const evaluation = await this.service.evaluate({ projectId: this.projectId });
585
- this.snapshot = {
586
- status,
587
- state: stateResult.state,
588
- evaluation,
589
- decisions: decisionsResult.decisions,
590
- language: languageResult.language,
591
- };
592
- this.clearError();
593
- } catch (err) {
594
- this.setError(summarizeError(err));
595
- } finally {
596
- this.loading = false;
597
- this.markDirty();
598
- this.requestRender();
599
- }
600
- })();
601
- }
602
-
603
- private getCurrentQuestion(): ProjectPlanningQuestion | null {
604
- const state = this.snapshot?.state;
605
- const open = state?.openQuestions.find((question) => (question.status ?? 'open') === 'open');
606
- return open ?? this.snapshot?.evaluation?.nextQuestion ?? null;
607
- }
608
-
609
- private submitSelectedAction(question: ProjectPlanningQuestion, actions: readonly PlanningAnswerAction[]): void {
610
- const action = actions[this.clampActionIndex(actions.length)];
611
- if (!action || action.disabled || !action.answer.trim()) {
612
- this.setError('Type an answer or choose a non-empty answer option.');
613
- this.requestRender();
614
- return;
615
- }
616
- if (action.kind === 'approve') {
617
- this.approveExecution();
618
- return;
619
- }
620
- if (action.kind === 'dismiss') {
621
- this.pausePlanning(question);
622
- return;
623
- }
624
- if (!this.submitAnswer) {
625
- this.setError('Planning answer submission is not wired in this runtime.');
626
- this.requestRender();
627
- return;
628
- }
629
- void (async () => {
630
- try {
631
- await this.persistQuestionIfNeeded(question);
632
- this.draftAnswer = '';
633
- this.submitAnswer?.(action.answer.trim());
634
- this.refresh(true);
635
- this.registerTimer(setTimeout(() => this.refresh(true), 250));
636
- } catch (err) {
637
- this.setError(summarizeError(err));
638
- this.requestRender();
639
- }
640
- })();
641
- }
642
-
643
- private async persistQuestionIfNeeded(question: ProjectPlanningQuestion): Promise<void> {
644
- const state = this.snapshot?.state;
645
- if (!state) return;
646
- if (state.openQuestions.some((entry) => entry.id === question.id)) return;
647
- await this.service.upsertState({
648
- projectId: this.projectId,
649
- state: {
650
- ...state,
651
- openQuestions: [
652
- { ...question, status: question.status ?? 'open' },
653
- ...state.openQuestions,
654
- ],
655
- },
656
- });
657
- }
658
-
659
- private pausePlanning(question: ProjectPlanningQuestion): void {
660
- const state = this.snapshot?.state;
661
- if (!state) {
662
- this.dismissPlanning?.();
663
- this.requestRender();
664
- return;
665
- }
666
- void (async () => {
667
- try {
668
- await this.service.upsertState({
669
- projectId: this.projectId,
670
- state: {
671
- ...state,
672
- openQuestions: state.openQuestions.map((entry) =>
673
- entry.id === question.id
674
- ? { ...entry, status: entry.status ?? 'open' }
675
- : entry,
676
- ),
677
- metadata: {
678
- ...(state.metadata ?? {}),
679
- active: false,
680
- pausedAt: Date.now(),
681
- pausedFrom: 'project-planning-panel',
682
- },
683
- },
684
- });
685
- this.dismissPlanning?.();
686
- this.refresh(true);
687
- } catch (err) {
688
- this.setError(summarizeError(err));
689
- this.requestRender();
690
- }
691
- })();
692
- }
693
-
694
- private clampActionIndex(count: number): number {
695
- if (count <= 0) return 0;
696
- return Math.max(0, Math.min(count - 1, this.selectedActionIndex));
697
- }
698
-
699
- private isPrintableKey(key: string): boolean {
700
- return key.length === 1 && key >= ' ';
701
- }
702
-
703
- private approveExecution(): void {
704
- const state = this.snapshot?.state;
705
- if (!state) {
706
- this.setError('No planning state exists to approve.');
707
- this.requestRender();
708
- return;
709
- }
710
- void (async () => {
711
- try {
712
- await this.service.upsertState({
713
- projectId: this.projectId,
714
- state: {
715
- ...state,
716
- executionApproved: true,
717
- metadata: {
718
- ...(state.metadata ?? {}),
719
- approvedFrom: 'project-planning-panel',
720
- approvedAt: Date.now(),
721
- },
722
- },
723
- });
724
- this.refresh(true);
725
- } catch (err) {
726
- this.setError(summarizeError(err));
727
- this.requestRender();
728
- }
729
- })();
730
- }
731
- }