@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
@@ -0,0 +1,304 @@
1
+ import { MODAL_TONES } from './modal-theme.ts';
2
+ import { infoRow } from './modal-surface-helpers.ts';
3
+ import type {
4
+ ProjectPlanningDecision,
5
+ ProjectPlanningEvaluation,
6
+ ProjectPlanningLanguageArtifact,
7
+ ProjectPlanningQuestion,
8
+ ProjectPlanningService,
9
+ ProjectPlanningState,
10
+ ProjectPlanningStatus,
11
+ } from '@pellux/goodvibes-sdk/platform/knowledge';
12
+ import type {
13
+ ConfigModalActionContext,
14
+ ConfigModalRow,
15
+ ConfigModalSurface,
16
+ ConfigModalView,
17
+ } from '../../input/config-modal-types.ts';
18
+ import { buildAnswerActions, isGenericRecommendation, type PlanningAnswerAction } from '../project-planning-answer-actions.ts';
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Project Planning → 'planning' config-modal surface (W6.1 group-B port). Shows
22
+ // readiness/questions/decisions/task-graph/handoff — read-only except choosing
23
+ // an answer to the current open question, approving execution, dismissing the
24
+ // plan, or refreshing.
25
+ //
26
+ // DEBT-3 — the seams are now real:
27
+ // - A CANNED answer to a real open question dispatches `/plan answer <id> <text>`
28
+ // (records it; the open-question gap clears on the next refine).
29
+ // - The CUSTOM free-form typed answer (and any answer to a synthetic readiness
30
+ // question with no open-question record) is submitted to chat via the generic
31
+ // `submitInput` seam — a real model turn. ORDERING GUARD: the modal closes
32
+ // BEFORE the turn starts (a turn under a live modal is the modal-liveness
33
+ // hazard). No more `/plan <text>` reseed approximation.
34
+ // - Dismiss is a first-class CONFIRMED action (`d`) dispatching `/plan dismiss`,
35
+ // plus the plain Esc close (planning unchanged).
36
+ // ---------------------------------------------------------------------------
37
+
38
+ export type PlanningModalService = Pick<ProjectPlanningService, 'status' | 'getState' | 'listDecisions' | 'getLanguage' | 'evaluate'>;
39
+
40
+ export interface PlanningModalDeps {
41
+ readonly service: PlanningModalService;
42
+ readonly projectId: string;
43
+ readonly requestRender?: () => void;
44
+ }
45
+
46
+ interface PlanningModalSnapshot {
47
+ readonly status: ProjectPlanningStatus | null;
48
+ readonly state: ProjectPlanningState | null;
49
+ readonly evaluation: ProjectPlanningEvaluation | null;
50
+ readonly decisions: readonly ProjectPlanningDecision[];
51
+ readonly language: ProjectPlanningLanguageArtifact | null;
52
+ }
53
+
54
+ interface TextLine { readonly content: string; readonly fg?: string; }
55
+
56
+ const GOOD = MODAL_TONES.good;
57
+ const WARN = MODAL_TONES.warn;
58
+ const BAD = MODAL_TONES.bad;
59
+ const INFO = MODAL_TONES.info;
60
+
61
+ function getCurrentQuestion(state: ProjectPlanningState, evaluation: ProjectPlanningEvaluation | null): ProjectPlanningQuestion | null {
62
+ const open = state.openQuestions.find((question) => (question.status ?? 'open') === 'open');
63
+ return open ?? evaluation?.nextQuestion ?? null;
64
+ }
65
+
66
+ function buildStateLines(state: ProjectPlanningState, evaluation: ProjectPlanningEvaluation | null): TextLine[] {
67
+ const readiness = evaluation?.readiness ?? state.readiness;
68
+ const readinessColor = readiness === 'executable' ? GOOD : readiness === 'needs-user-input' ? WARN : undefined;
69
+ const blockingGaps = (evaluation?.gaps ?? []).filter((gap) => gap.severity === 'blocking').length;
70
+ const lines: TextLine[] = [
71
+ { content: `readiness ${readiness} approved ${state.executionApproved ? 'yes' : 'no'} questions ${state.openQuestions.length} open / ${state.answeredQuestions.length} answered`, ...(readinessColor ? { fg: readinessColor } : {}) },
72
+ { content: `blocking gaps ${blockingGaps} tasks ${state.tasks.length} gates ${state.verificationGates.length}` },
73
+ { content: `goal: ${state.goal || '(not set)'}` },
74
+ ];
75
+ if (state.scope) lines.push({ content: `scope: ${state.scope}` });
76
+ if (state.knownContext.length) lines.push({ content: `known context: ${state.knownContext.join(' | ')}` });
77
+ if (evaluation?.nextQuestion) lines.push({ content: `next question: ${evaluation.nextQuestion.prompt}`, fg: INFO });
78
+ return lines;
79
+ }
80
+
81
+ function buildGapsLines(evaluation: ProjectPlanningEvaluation | null): TextLine[] {
82
+ const gaps = evaluation?.gaps ?? [];
83
+ if (gaps.length === 0) return [{ content: 'Readiness gaps: none.', fg: GOOD }];
84
+ return [{ content: 'Readiness gaps:' }, ...gaps.slice(0, 12).map((gap) => ({ content: ` ${gap.severity.toUpperCase()} ${gap.kind}: ${gap.message}`, fg: gap.severity === 'blocking' ? BAD : WARN }))];
85
+ }
86
+
87
+ function buildTasksLines(state: ProjectPlanningState): TextLine[] {
88
+ const lines: TextLine[] = [{ content: 'Task graph:' }];
89
+ if (state.tasks.length === 0) {
90
+ lines.push({ content: ' No decomposed tasks recorded yet.' });
91
+ } else {
92
+ for (const task of state.tasks) {
93
+ lines.push({ content: ` ${task.id}: ${task.title} [${task.status ?? 'pending'}]${task.canRunConcurrently ? ' - concurrent' : ''}`, ...(task.blockedOnUserInput ? { fg: WARN } : {}) });
94
+ if (task.dependencies?.length) lines.push({ content: ` dependencies: ${task.dependencies.join(', ')}` });
95
+ if (task.verification?.length) lines.push({ content: ` verification: ${task.verification.join(' | ')}`, fg: GOOD });
96
+ }
97
+ }
98
+ if (state.verificationGates.length) {
99
+ lines.push({ content: 'Verification gates:' });
100
+ for (const gate of state.verificationGates) lines.push({ content: ` ${gate.id}: ${gate.description} [${gate.status ?? 'pending'}]`, fg: gate.required === false ? undefined : GOOD });
101
+ }
102
+ if (state.agentAssignments.length) {
103
+ lines.push({ content: 'Agent handoff candidates:' });
104
+ for (const assignment of state.agentAssignments) lines.push({ content: ` ${assignment.taskId}: ${assignment.agentType ?? 'none'}${assignment.canRunConcurrently ? ' - can run concurrently' : ''}`, fg: INFO });
105
+ }
106
+ return lines;
107
+ }
108
+
109
+ function buildDecisionsLines(state: ProjectPlanningState, storedDecisions: readonly ProjectPlanningDecision[]): TextLine[] {
110
+ const byId = new Map<string, ProjectPlanningDecision>();
111
+ for (const decision of [...storedDecisions, ...state.decisions]) byId.set(decision.id, decision);
112
+ const decisions = [...byId.values()];
113
+ if (decisions.length === 0) return [{ content: 'Decisions: none recorded yet.' }];
114
+ return [{ content: 'Decisions:' }, ...decisions.slice(0, 12).map((decision) => ({ content: ` ${decision.title}: ${decision.decision} [${decision.status ?? 'accepted'}]`, fg: decision.status === 'rejected' ? BAD : undefined }))];
115
+ }
116
+
117
+ function buildLanguageLines(language: ProjectPlanningLanguageArtifact | null): TextLine[] {
118
+ if (!language || (language.terms.length === 0 && language.ambiguities.length === 0)) return [{ content: 'Project language: no terms or ambiguity resolutions recorded yet.' }];
119
+ const lines: TextLine[] = [{ content: 'Project language:' }];
120
+ for (const term of language.terms.slice(0, 8)) {
121
+ lines.push({ content: ` ${term.term}: ${term.definition}` });
122
+ if (term.avoid?.length) lines.push({ content: ` avoid: ${term.avoid.join(', ')}`, fg: BAD });
123
+ }
124
+ for (const ambiguity of language.ambiguities.slice(0, 8)) lines.push({ content: ` resolved ambiguity - ${ambiguity.phrase}: ${ambiguity.resolution}`, fg: INFO });
125
+ return lines;
126
+ }
127
+
128
+ class PlanningModalSurface implements ConfigModalSurface {
129
+ readonly name = 'planning-modal';
130
+ readonly title = 'Planning';
131
+ private snapshot: PlanningModalSnapshot | null = null;
132
+ private loading = false;
133
+ private requestRender: () => void = () => {};
134
+
135
+ constructor(private readonly deps: PlanningModalDeps) {}
136
+
137
+ readonly actions = [
138
+ { key: 'enter', id: 'submit', label: 'submit', enabledFor: () => this.currentAnswerActions().actions.length > 0 },
139
+ { key: 'a', id: 'approve', label: 'approve execution' },
140
+ { key: 'd', id: 'dismiss', label: 'dismiss planning', confirm: true },
141
+ { key: 'r', id: 'refresh', label: 'refresh' },
142
+ ];
143
+
144
+ onOpen(requestRender: () => void): void { this.requestRender = requestRender; this.refresh(); }
145
+
146
+ private refresh(): void {
147
+ if (this.loading) return;
148
+ this.loading = true;
149
+ void this.load();
150
+ }
151
+
152
+ private async load(): Promise<void> {
153
+ try {
154
+ const [status, stateResult, decisionsResult, languageResult] = await Promise.all([
155
+ this.deps.service.status({ projectId: this.deps.projectId }),
156
+ this.deps.service.getState({ projectId: this.deps.projectId }),
157
+ this.deps.service.listDecisions({ projectId: this.deps.projectId }),
158
+ this.deps.service.getLanguage({ projectId: this.deps.projectId }),
159
+ ]);
160
+ const evaluation = await this.deps.service.evaluate({ projectId: this.deps.projectId });
161
+ this.snapshot = { status, state: stateResult.state, evaluation, decisions: decisionsResult.decisions, language: languageResult.language };
162
+ } catch {
163
+ // Leave the previous snapshot (if any) in place.
164
+ } finally {
165
+ this.loading = false;
166
+ this.requestRender();
167
+ this.deps.requestRender?.();
168
+ }
169
+ }
170
+
171
+ private currentAnswerActions(): { readonly question: ProjectPlanningQuestion | null; readonly actions: readonly PlanningAnswerAction[] } {
172
+ if (!this.snapshot?.state) return { question: null, actions: [] };
173
+ const question = getCurrentQuestion(this.snapshot.state, this.snapshot.evaluation);
174
+ if (!question) return { question: null, actions: [] };
175
+ return { question, actions: buildAnswerActions(question, '') };
176
+ }
177
+
178
+ buildView(): ConfigModalView {
179
+ if (!this.snapshot) {
180
+ return { title: 'Planning', tabs: [{ id: 'planning', label: 'Planning', rows: [infoRow('load', this.loading ? 'Loading project planning state...' : 'Project planning state unavailable.', { dim: true })] }], hints: ['r refresh'] };
181
+ }
182
+
183
+ const { status, state, evaluation, decisions, language } = this.snapshot;
184
+ const header = [`project ${this.deps.projectId} space ${status?.knowledgeSpaceId ?? `project:${this.deps.projectId}`}`];
185
+ const rows: ConfigModalRow[] = [];
186
+ let n = 0;
187
+ const line = (l: TextLine): void => { rows.push({ id: `p:${n++}`, label: l.content, selectable: false, ...(l.fg ? { style: { fg: l.fg } } : {}) }); };
188
+
189
+ if (!state) {
190
+ line({ content: 'No project planning state has been saved for this workspace.' });
191
+ line({ content: 'Describe the intended change in normal chat to start the planning interview.', fg: undefined });
192
+ return { title: 'Planning', tabs: [{ id: 'planning', label: 'Planning', header, rows, emptyText: '' }], hints: ['r refresh'] };
193
+ }
194
+
195
+ for (const l of buildStateLines(state, evaluation)) line(l);
196
+
197
+ const { question, actions } = this.currentAnswerActions();
198
+ if (question) {
199
+ line({ content: 'Answer Current Question' });
200
+ line({ content: question.prompt, fg: INFO });
201
+ if (question.whyItMatters) line({ content: `Why this matters: ${question.whyItMatters}` });
202
+ if (question.recommendedAnswer && !isGenericRecommendation(question.recommendedAnswer)) line({ content: `Recommendation: ${question.recommendedAnswer}`, fg: GOOD });
203
+ for (const action of actions) {
204
+ rows.push({ id: action.id, label: `${action.label} - ${action.detail}`, ...(action.disabled ? { selectable: false } : {}) });
205
+ }
206
+ line({ content: 'Enter records a canned answer against this question; the custom row submits your typed text to chat.', fg: undefined });
207
+ }
208
+
209
+ for (const l of buildGapsLines(evaluation)) line(l);
210
+ for (const l of buildTasksLines(state)) line(l);
211
+ for (const l of buildDecisionsLines(state, decisions)) line(l);
212
+ for (const l of buildLanguageLines(language)) line(l);
213
+
214
+ return {
215
+ title: 'Planning',
216
+ tabs: [{ id: 'planning', label: 'Planning', header, rows }],
217
+ };
218
+ }
219
+
220
+ onAction(id: string, ctx: ConfigModalActionContext): void {
221
+ if (id === 'refresh') { this.refresh(); ctx.setStatus('Reloading project planning state…'); return; }
222
+ if (id === 'approve') { void ctx.executeCommand?.('plan', ['approve']); ctx.setStatus('Dispatched /plan approve.'); return; }
223
+ if (id === 'dismiss') {
224
+ // First-class, confirmed (host two-press) mutating dismiss.
225
+ void ctx.executeCommand?.('plan', ['dismiss']);
226
+ ctx.setStatus('Dispatched /plan dismiss.');
227
+ ctx.close();
228
+ return;
229
+ }
230
+ if (id !== 'submit') return;
231
+ const { question, actions } = this.currentAnswerActions();
232
+ if (!question || actions.length === 0) return;
233
+ const action = ctx.row ? actions.find((a) => a.id === ctx.row!.id) : undefined;
234
+ if (!action || action.disabled) { ctx.print('Choose an answer option.'); return; }
235
+ if (action.kind === 'approve') { void ctx.executeCommand?.('plan', ['approve']); ctx.setStatus('Dispatched /plan approve.'); return; }
236
+
237
+ const answerText = action.answer.trim();
238
+ if (!answerText) { ctx.print('Choose a non-empty answer, or type an answer for the custom row.'); return; }
239
+
240
+ // A canned answer to a REAL open question records structurally via /plan answer.
241
+ const isOpenQuestion = this.snapshot?.state?.openQuestions.some(
242
+ (q) => q.id === question.id && (q.status ?? 'open') === 'open',
243
+ ) ?? false;
244
+ if (action.id !== 'custom' && isOpenQuestion) {
245
+ void ctx.executeCommand?.('plan', ['answer', question.id, ...answerText.split(/\s+/)]);
246
+ ctx.setStatus('Dispatched /plan answer for the current question.');
247
+ return;
248
+ }
249
+
250
+ // Free-form (custom typed) answer, or an answer to a synthetic readiness
251
+ // question with no open-question record → submit to chat as a real turn.
252
+ // ORDERING GUARD: close the modal BEFORE the turn starts (modal-liveness).
253
+ if (ctx.submitInput) {
254
+ ctx.close();
255
+ ctx.submitInput(answerText);
256
+ return;
257
+ }
258
+ ctx.print('Submitting to chat is unavailable in this runtime; answer left unsent.');
259
+ }
260
+ }
261
+
262
+ export function createPlanningModalSurface(deps: PlanningModalDeps): ConfigModalSurface {
263
+ return new PlanningModalSurface(deps);
264
+ }
265
+
266
+ function buildGoldenService(): PlanningModalService {
267
+ const state: ProjectPlanningState = {
268
+ id: 'golden-state-1', projectId: 'golden-project', knowledgeSpaceId: 'project:golden-project',
269
+ goal: 'Ship the golden fixture end-to-end.', scope: 'Golden fixture scope only.', knownContext: ['Seeded for a deterministic golden render.'],
270
+ openQuestions: [{ id: 'golden-question-1', prompt: 'What is in scope for this golden fixture?', whyItMatters: 'Keeps the render deterministic.', recommendedAnswer: 'Use a focused first-pass scope for this goal.', status: 'open' }],
271
+ answeredQuestions: [{ id: 'golden-question-0', prompt: 'Is this a golden fixture?', status: 'answered', answer: 'Yes.', answeredAt: 0 }],
272
+ decisions: [], assumptions: [], constraints: [], risks: [],
273
+ tasks: [{ id: 'golden-task-1', title: 'Implement the golden fixture', status: 'pending', dependencies: [], verification: ['bun test src/test/panels/modals/planning-modal.test.ts'] }],
274
+ dependencies: [], verificationGates: [{ id: 'golden-gate-1', description: 'Golden render is byte-stable.', status: 'pending', required: true }],
275
+ agentAssignments: [{ taskId: 'golden-task-1', agentType: 'worker', canRunConcurrently: false }],
276
+ readiness: 'needs-user-input', executionApproved: false, createdAt: 0, updatedAt: 0,
277
+ };
278
+ const evaluation: ProjectPlanningEvaluation = {
279
+ ok: true, projectId: 'golden-project', knowledgeSpaceId: 'project:golden-project', readiness: 'needs-user-input',
280
+ gaps: [{ id: 'golden-gap-1', kind: 'open-question', severity: 'blocking', message: 'One open question remains.' }],
281
+ nextQuestion: state.openQuestions[0], state,
282
+ };
283
+ const decision: ProjectPlanningDecision = { id: 'golden-decision-1', title: 'Use a golden fixture', decision: 'Freeze all ids/timestamps for a byte-stable render.', status: 'accepted' };
284
+ const language: ProjectPlanningLanguageArtifact = { projectId: 'golden-project', knowledgeSpaceId: 'project:golden-project', terms: [{ term: 'golden fixture', definition: 'A deterministic, frozen input used for byte-stable tests.' }], ambiguities: [], updatedAt: 0 };
285
+ return {
286
+ status: async () => ({ ok: true, projectId: 'golden-project', knowledgeSpaceId: 'project:golden-project', passiveOnly: true, counts: { states: 1, decisions: 1, languageArtifacts: 1, workPlans: 0, workPlanTasks: 0 }, capabilities: [] }),
287
+ getState: async () => ({ ok: true, projectId: 'golden-project', knowledgeSpaceId: 'project:golden-project', state }),
288
+ listDecisions: async () => ({ ok: true, projectId: 'golden-project', knowledgeSpaceId: 'project:golden-project', decisions: [decision] }),
289
+ getLanguage: async () => ({ ok: true, projectId: 'golden-project', knowledgeSpaceId: 'project:golden-project', language }),
290
+ evaluate: async () => evaluation,
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Deterministic golden fixture. All ids/timestamps are frozen literals. Because
296
+ * every service method is Promise-based, this helper is async — it opens the
297
+ * surface, waits a macrotask so the fire-and-forget load resolves, then returns.
298
+ */
299
+ export async function planningModalGoldenSurface(): Promise<ConfigModalSurface> {
300
+ const surface = createPlanningModalSurface({ service: buildGoldenService(), projectId: 'golden-project' });
301
+ surface.onOpen?.(() => {});
302
+ await new Promise((resolve) => setTimeout(resolve, 0));
303
+ return surface;
304
+ }
@@ -0,0 +1,174 @@
1
+ import { MODAL_TONES } from './modal-theme.ts';
2
+ import { infoRow } from './modal-surface-helpers.ts';
3
+ import type {
4
+ ConfigModalActionContext,
5
+ ConfigModalRow,
6
+ ConfigModalSurface,
7
+ ConfigModalView,
8
+ } from '../../input/config-modal-types.ts';
9
+ import type {
10
+ PluginManager,
11
+ PluginManagerObserver,
12
+ PluginStatus,
13
+ } from '@pellux/goodvibes-sdk/platform/plugins';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Plugins → config-modal surface (W6.1 group-B port). Mirrors the retired
17
+ // PluginsPanel's read surface (list/capabilities/getTrustRecord/
18
+ // getQuarantineRecord) plus `verify` (a pure read-only signature check).
19
+ // enable/disable/liftQuarantine are DROPPED from the dep shape on purpose:
20
+ // those verbs are destructive/interactive mutations and route to the `/plugin`
21
+ // command path (action-parity charter rule). Selection-blind port: the panel's
22
+ // selected-plugin signature/quarantine/verify detail is folded into row labels.
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /** Minimal structural read surface this modal needs from the live plugin manager. */
26
+ export type PluginsModalManager = Pick<PluginManagerObserver, 'list' | 'capabilities' | 'getTrustRecord' | 'getQuarantineRecord'>
27
+ & Pick<PluginManager, 'verify'>;
28
+
29
+ export interface PluginsModalDeps {
30
+ readonly pluginManager: PluginsModalManager;
31
+ }
32
+
33
+ interface VerifyResult {
34
+ readonly valid: boolean;
35
+ readonly fingerprint?: string | undefined;
36
+ readonly reason?: string | undefined;
37
+ }
38
+
39
+ function statusLabel(plugin: PluginStatus): string {
40
+ if (plugin.quarantined) return 'QUARANTINED';
41
+ if (plugin.active) return 'ACTIVE';
42
+ if (plugin.enabled) return 'ENABLED';
43
+ return 'DISABLED';
44
+ }
45
+
46
+ function statusColor(plugin: PluginStatus): string {
47
+ if (plugin.quarantined) return MODAL_TONES.bad;
48
+ if (plugin.active) return MODAL_TONES.good;
49
+ if (plugin.enabled) return MODAL_TONES.warn;
50
+ return MODAL_TONES.muted;
51
+ }
52
+
53
+ /**
54
+ * Plugins → modal. Trust, capabilities, signature, and quarantine posture for
55
+ * the active plugin registry. `pluginManager.list()` etc. are in-memory reads
56
+ * (no disk I/O per call), so buildView reads live and refresh() is a no-op.
57
+ * Enable/disable/lift-quarantine/capture-to-memory route to their command
58
+ * paths rather than mutating the manager directly from the modal.
59
+ */
60
+ class PluginsModalSurface implements ConfigModalSurface {
61
+ readonly name = 'plugins-modal';
62
+ readonly title = 'Plugins';
63
+ private readonly verifyResults = new Map<string, VerifyResult>();
64
+ private requestRender: () => void = () => {};
65
+
66
+ constructor(private readonly deps: PluginsModalDeps) {}
67
+
68
+ readonly actions = [
69
+ { key: 'e', id: 'enable', label: 'enable', enabledFor: (row: ConfigModalRow | null) => this.pluginFrom(row)?.active === false },
70
+ { key: 'd', id: 'disable', label: 'disable', enabledFor: (row: ConfigModalRow | null) => this.pluginFrom(row)?.enabled === true },
71
+ { key: 'v', id: 'verify', label: 'verify' },
72
+ { key: 'q', id: 'liftQuarantine', label: 'lift quarantine', enabledFor: (row: ConfigModalRow | null) => this.pluginFrom(row)?.quarantined === true },
73
+ { key: 'm', id: 'captureToMemory', label: 'capture to memory', enabledFor: (row: ConfigModalRow | null) => this.pluginFrom(row)?.quarantined === true },
74
+ { key: 'r', id: 'refresh', label: 'refresh' },
75
+ ];
76
+
77
+ onOpen(requestRender: () => void): void { this.requestRender = requestRender; }
78
+
79
+ private pluginFrom(row: ConfigModalRow | null): PluginStatus | undefined {
80
+ if (!row) return undefined;
81
+ return this.deps.pluginManager.list().find((p) => p.name === row.id);
82
+ }
83
+
84
+ buildView(): ConfigModalView {
85
+ const all = this.deps.pluginManager.list();
86
+ const rows: ConfigModalRow[] = [];
87
+
88
+ if (all.length === 0) {
89
+ rows.push(infoRow('empty:0', 'No plugins discovered.'));
90
+ rows.push(infoRow('empty:title', 'Next steps'));
91
+ rows.push(infoRow('empty:list', '/plugin list — inspect plugin discovery paths and current registry state', { dim: true }));
92
+ rows.push(infoRow('empty:market', '/marketplace — review curated ecosystem entries and provenance posture', { dim: true }));
93
+ return { title: 'Plugins', tabs: [{ id: 'plugins', label: 'Plugins', rows, emptyText: '' }] };
94
+ }
95
+
96
+ const active = all.filter((p) => p.active).length;
97
+ const untrusted = all.filter((p) => p.trustTier === 'untrusted').length;
98
+ const quarantined = all.filter((p) => p.quarantined).length;
99
+ const header = [`plugins ${all.length} active ${active} untrusted ${untrusted} quarantined ${quarantined}`];
100
+
101
+ for (const plugin of all) {
102
+ const caps = this.deps.pluginManager.capabilities(plugin.name);
103
+ const trustRecord = this.deps.pluginManager.getTrustRecord(plugin.name);
104
+ const quarantineRecord = this.deps.pluginManager.getQuarantineRecord(plugin.name);
105
+ const verify = this.verifyResults.get(plugin.name);
106
+ const suffix = [
107
+ trustRecord?.signatureFingerprint ? `sig ${trustRecord.signatureFingerprint}` : 'unsigned',
108
+ caps ? `caps ${caps.requested.length}/${caps.highRisk.length}hr/${caps.blocked.length}blk` : null,
109
+ quarantineRecord ? `quarantine: ${quarantineRecord.reason}` : null,
110
+ verify ? `verify ${verify.valid ? 'VALID' : 'INVALID'}${verify.fingerprint ? ` fp=${verify.fingerprint}` : (!verify.valid && verify.reason ? ` — ${verify.reason}` : '')}` : null,
111
+ ].filter((s): s is string => s !== null).join(' · ');
112
+ rows.push({
113
+ id: plugin.name,
114
+ label: `${plugin.name.padEnd(22)} ${statusLabel(plugin).padEnd(11)} ${plugin.trustTier.toUpperCase().padEnd(10)} v${plugin.version} · ${suffix}`,
115
+ style: { fg: statusColor(plugin) },
116
+ });
117
+ }
118
+
119
+ return { title: 'Plugins', tabs: [{ id: 'plugins', label: 'Plugins', header, rows }] };
120
+ }
121
+
122
+ onAction(id: string, ctx: ConfigModalActionContext): void {
123
+ if (id === 'refresh') { ctx.setStatus('Plugins are read live.'); ctx.requestRender(); return; }
124
+ const plugin = this.pluginFrom(ctx.row);
125
+ if (!plugin) return;
126
+ switch (id) {
127
+ case 'enable':
128
+ if (!plugin.active) { void ctx.executeCommand?.('plugin', ['enable', plugin.name]); ctx.setStatus(`Dispatched /plugin enable ${plugin.name}.`); }
129
+ break;
130
+ case 'disable':
131
+ if (plugin.enabled) { void ctx.executeCommand?.('plugin', ['disable', plugin.name]); ctx.setStatus(`Dispatched /plugin disable ${plugin.name}.`); }
132
+ break;
133
+ case 'verify': {
134
+ const result = this.deps.pluginManager.verify(plugin.name);
135
+ this.verifyResults.set(plugin.name, { valid: result.valid, fingerprint: result.fingerprint, reason: result.reason });
136
+ ctx.setStatus(`verify ${result.valid ? 'VALID' : 'INVALID'}`);
137
+ this.requestRender();
138
+ break;
139
+ }
140
+ case 'liftQuarantine':
141
+ if (plugin.quarantined) { void ctx.executeCommand?.('plugin', ['quarantine', plugin.name, 'lift']); ctx.setStatus(`Dispatched /plugin quarantine ${plugin.name} lift.`); }
142
+ break;
143
+ case 'captureToMemory':
144
+ if (plugin.quarantined) { void ctx.executeCommand?.('recall', ['capture', 'plugin', plugin.name]); ctx.setStatus(`Dispatched /recall capture plugin ${plugin.name}.`); }
145
+ break;
146
+ }
147
+ }
148
+ }
149
+
150
+ export function createPluginsModalSurface(deps: PluginsModalDeps): ConfigModalSurface {
151
+ return new PluginsModalSurface(deps);
152
+ }
153
+
154
+ /**
155
+ * Deterministic golden fixture: a fixed two-plugin roster (one active/trusted,
156
+ * one quarantined/untrusted) so both the base list rendering and the quarantine
157
+ * suffix are exercised without any disk I/O, wall-clock, or random ids.
158
+ */
159
+ export function pluginsModalGoldenSurface(): ConfigModalSurface {
160
+ const fixedPlugins: PluginStatus[] = [
161
+ { name: 'formatter', version: '1.0.0', description: 'Deterministic golden fixture plugin.', enabled: true, active: true, trustTier: 'trusted', quarantined: false },
162
+ { name: 'risky-tool', version: '0.2.0', description: 'Second golden fixture plugin, quarantined.', enabled: false, active: false, trustTier: 'untrusted', quarantined: true },
163
+ ];
164
+ const manager: PluginsModalManager = {
165
+ list: () => fixedPlugins,
166
+ capabilities: () => null,
167
+ getTrustRecord: () => undefined,
168
+ getQuarantineRecord: (name) => (name === 'risky-tool'
169
+ ? { pluginName: name, quarantinedAt: 0, reason: 'golden fixture quarantine', revokedCapabilities: [], lifted: false }
170
+ : undefined),
171
+ verify: () => ({ ok: true, valid: true, fingerprint: 'golden-fixture-fp' }),
172
+ };
173
+ return createPluginsModalSurface({ pluginManager: manager });
174
+ }