@pellux/goodvibes-tui 1.0.0 → 1.7.0

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