@pellux/goodvibes-tui 1.1.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/CHANGELOG.md +85 -21
  2. package/README.md +20 -11
  3. package/docs/foundation-artifacts/operator-contract.json +1 -1
  4. package/package.json +2 -2
  5. package/src/core/alert-gating.ts +67 -0
  6. package/src/core/approval-alert.ts +72 -0
  7. package/src/core/budget-breach-notifier.ts +106 -0
  8. package/src/core/conversation-rendering.ts +19 -0
  9. package/src/core/conversation.ts +18 -0
  10. package/src/core/focus-tracker.ts +41 -0
  11. package/src/core/long-task-notifier.ts +33 -6
  12. package/src/core/system-message-router.ts +37 -51
  13. package/src/core/turn-cancellation.ts +20 -0
  14. package/src/core/turn-event-wiring.ts +64 -3
  15. package/src/export/cost-utils.ts +36 -0
  16. package/src/input/command-registry.ts +38 -1
  17. package/src/input/commands/checkpoint-runtime.ts +280 -0
  18. package/src/input/commands/codebase-runtime.ts +192 -0
  19. package/src/input/commands/eval.ts +1 -1
  20. package/src/input/commands/health-runtime.ts +1 -1
  21. package/src/input/commands/image-runtime.ts +112 -0
  22. package/src/input/commands/intelligence-runtime.ts +11 -1
  23. package/src/input/commands/local-auth-runtime.ts +4 -1
  24. package/src/input/commands/local-runtime.ts +9 -3
  25. package/src/input/commands/marketplace-runtime.ts +2 -2
  26. package/src/input/commands/memory.ts +6 -1
  27. package/src/input/commands/operator-panel-runtime.ts +40 -24
  28. package/src/input/commands/planning-runtime.ts +26 -3
  29. package/src/input/commands/platform-sandbox-runtime.ts +1 -6
  30. package/src/input/commands/plugin-runtime.ts +2 -2
  31. package/src/input/commands/policy-dispatch.ts +21 -0
  32. package/src/input/commands/provider-accounts-runtime.ts +1 -1
  33. package/src/input/commands/qrcode-runtime.ts +3 -3
  34. package/src/input/commands/recall-review.ts +35 -0
  35. package/src/input/commands/remote-runtime.ts +3 -3
  36. package/src/input/commands/runtime-services.ts +54 -13
  37. package/src/input/commands/services-runtime.ts +1 -1
  38. package/src/input/commands/session-workflow.ts +16 -1
  39. package/src/input/commands/settings-sync-runtime.ts +1 -1
  40. package/src/input/commands/shell-core.ts +15 -7
  41. package/src/input/commands/skills-runtime.ts +2 -8
  42. package/src/input/commands/subscription-runtime.ts +2 -2
  43. package/src/input/commands/test-runtime.ts +277 -0
  44. package/src/input/commands/websearch-runtime.ts +101 -0
  45. package/src/input/commands/work-plan-runtime.ts +36 -10
  46. package/src/input/commands/workstream-runtime.ts +338 -0
  47. package/src/input/commands.ts +12 -0
  48. package/src/input/config-modal-types.ts +161 -0
  49. package/src/input/config-modal.ts +377 -0
  50. package/src/input/feed-context-factory.ts +7 -8
  51. package/src/input/handler-command-route.ts +27 -17
  52. package/src/input/handler-feed-routes.ts +61 -23
  53. package/src/input/handler-feed.ts +47 -23
  54. package/src/input/handler-interactions.ts +1 -3
  55. package/src/input/handler-modal-routes.ts +65 -0
  56. package/src/input/handler-modal-stack.ts +3 -5
  57. package/src/input/handler-modal-token-routes.ts +16 -49
  58. package/src/input/handler-picker-routes.ts +11 -94
  59. package/src/input/handler-shortcuts.ts +24 -14
  60. package/src/input/handler-types.ts +2 -6
  61. package/src/input/handler-ui-state.ts +10 -22
  62. package/src/input/handler.ts +6 -28
  63. package/src/input/keybindings.ts +22 -8
  64. package/src/input/model-picker-types.ts +24 -6
  65. package/src/input/model-picker.ts +58 -0
  66. package/src/input/panel-integration-actions.ts +9 -170
  67. package/src/input/settings-modal-data.ts +95 -0
  68. package/src/input/settings-modal-mutations.ts +6 -4
  69. package/src/main.ts +24 -27
  70. package/src/panels/agent-inspector-shared.ts +2 -1
  71. package/src/panels/base-panel.ts +22 -1
  72. package/src/panels/builtin/agent.ts +21 -107
  73. package/src/panels/builtin/development.ts +15 -61
  74. package/src/panels/builtin/knowledge.ts +8 -32
  75. package/src/panels/builtin/operations.ts +84 -428
  76. package/src/panels/builtin/session.ts +21 -112
  77. package/src/panels/builtin/shared.ts +9 -43
  78. package/src/panels/builtin-modals.ts +218 -0
  79. package/src/panels/builtin-panels.ts +5 -0
  80. package/src/panels/cost-tracker-panel.ts +36 -10
  81. package/src/panels/diff-panel.ts +12 -43
  82. package/src/panels/eval-registry.ts +60 -0
  83. package/src/panels/fleet-panel.ts +800 -0
  84. package/src/panels/fleet-read-model.ts +477 -0
  85. package/src/panels/fleet-steer.ts +92 -0
  86. package/src/panels/fleet-stop.ts +125 -0
  87. package/src/panels/fleet-tabs.ts +230 -0
  88. package/src/panels/fleet-transcript.ts +356 -0
  89. package/src/panels/index.ts +7 -31
  90. package/src/panels/modals/hooks-modal.ts +187 -0
  91. package/src/panels/modals/keybindings-modal.ts +151 -0
  92. package/src/panels/modals/knowledge-modal.ts +158 -0
  93. package/src/panels/modals/local-auth-modal.ts +132 -0
  94. package/src/panels/modals/marketplace-modal.ts +218 -0
  95. package/src/panels/modals/memory-modal.ts +180 -0
  96. package/src/panels/modals/modal-surface-helpers.ts +54 -0
  97. package/src/panels/modals/modal-theme.ts +36 -0
  98. package/src/panels/modals/pairing-modal.ts +144 -0
  99. package/src/panels/modals/planning-modal.ts +282 -0
  100. package/src/panels/modals/plugins-modal.ts +174 -0
  101. package/src/panels/modals/policy-modal.ts +256 -0
  102. package/src/panels/modals/provider-health-modal.ts +136 -0
  103. package/src/panels/modals/remote-modal.ts +115 -0
  104. package/src/panels/modals/sandbox-modal.ts +150 -0
  105. package/src/panels/modals/security-modal.ts +217 -0
  106. package/src/panels/modals/services-modal.ts +179 -0
  107. package/src/panels/modals/settings-sync-modal.ts +142 -0
  108. package/src/panels/modals/skills-modal.ts +189 -0
  109. package/src/panels/modals/subscription-modal.ts +172 -0
  110. package/src/panels/modals/work-plan-modal.ts +149 -0
  111. package/src/panels/panel-confirm-overlay.ts +72 -0
  112. package/src/panels/panel-manager.ts +108 -5
  113. package/src/panels/project-planning-answer-actions.ts +4 -2
  114. package/src/panels/skills-panel.ts +7 -51
  115. package/src/panels/types.ts +13 -0
  116. package/src/permissions/hunk-selection.ts +179 -0
  117. package/src/permissions/prompt.ts +60 -4
  118. package/src/renderer/compaction-history-modal.ts +19 -3
  119. package/src/renderer/compaction-preview.ts +13 -2
  120. package/src/renderer/compaction-quality.ts +100 -0
  121. package/src/renderer/config-modal.ts +81 -0
  122. package/src/renderer/conversation-overlays.ts +10 -17
  123. package/src/renderer/fleet-tab-strip.ts +56 -0
  124. package/src/renderer/footer-tips.ts +10 -2
  125. package/src/renderer/git-status.ts +40 -0
  126. package/src/renderer/help-overlay.ts +2 -2
  127. package/src/renderer/model-workspace.ts +44 -1
  128. package/src/renderer/panel-workspace-bar.ts +8 -2
  129. package/src/renderer/turn-injection.ts +114 -0
  130. package/src/runtime/bootstrap-command-context.ts +21 -1
  131. package/src/runtime/bootstrap-command-parts.ts +34 -7
  132. package/src/runtime/bootstrap-core.ts +14 -4
  133. package/src/runtime/bootstrap-shell.ts +29 -16
  134. package/src/runtime/bootstrap.ts +11 -17
  135. package/src/runtime/code-index-services.ts +112 -0
  136. package/src/runtime/orchestrator-core-services.ts +49 -0
  137. package/src/runtime/process-lifecycle.ts +3 -1
  138. package/src/runtime/services.ts +91 -43
  139. package/src/runtime/ui-services.ts +8 -0
  140. package/src/runtime/workstream-services.ts +195 -0
  141. package/src/shell/blocking-input.ts +22 -1
  142. package/src/shell/ui-openers.ts +129 -17
  143. package/src/utils/format-duration.ts +8 -18
  144. package/src/utils/splash-lines.ts +1 -1
  145. package/src/version.ts +1 -1
  146. package/src/panels/agent-inspector-panel.ts +0 -786
  147. package/src/panels/approval-panel.ts +0 -252
  148. package/src/panels/automation-control-panel.ts +0 -479
  149. package/src/panels/cockpit-panel.ts +0 -481
  150. package/src/panels/cockpit-read-model.ts +0 -238
  151. package/src/panels/communication-panel.ts +0 -256
  152. package/src/panels/control-plane-panel.ts +0 -470
  153. package/src/panels/debug-panel.ts +0 -615
  154. package/src/panels/docs-panel.ts +0 -384
  155. package/src/panels/eval-panel.ts +0 -627
  156. package/src/panels/file-explorer-panel.ts +0 -673
  157. package/src/panels/file-preview-panel.ts +0 -517
  158. package/src/panels/hooks-panel.ts +0 -401
  159. package/src/panels/incident-review-panel.ts +0 -406
  160. package/src/panels/intelligence-panel.ts +0 -383
  161. package/src/panels/knowledge-graph-panel.ts +0 -515
  162. package/src/panels/marketplace-panel.ts +0 -399
  163. package/src/panels/memory-panel.ts +0 -558
  164. package/src/panels/ops-control-panel.ts +0 -329
  165. package/src/panels/ops-strategy-panel.ts +0 -321
  166. package/src/panels/orchestration-panel.ts +0 -465
  167. package/src/panels/panel-list-panel.ts +0 -566
  168. package/src/panels/plan-dashboard-panel.ts +0 -707
  169. package/src/panels/policy-panel.ts +0 -517
  170. package/src/panels/project-planning-panel.ts +0 -731
  171. package/src/panels/provider-health-panel.ts +0 -678
  172. package/src/panels/provider-health-tracker.ts +0 -310
  173. package/src/panels/provider-health-views.ts +0 -567
  174. package/src/panels/qr-panel.ts +0 -280
  175. package/src/panels/remote-panel.ts +0 -534
  176. package/src/panels/routes-panel.ts +0 -241
  177. package/src/panels/sandbox-panel.ts +0 -456
  178. package/src/panels/security-panel.ts +0 -447
  179. package/src/panels/services-panel.ts +0 -329
  180. package/src/panels/session-browser-panel.ts +0 -496
  181. package/src/panels/settings-sync-panel.ts +0 -398
  182. package/src/panels/subscription-panel.ts +0 -342
  183. package/src/panels/symbol-outline-panel.ts +0 -619
  184. package/src/panels/system-messages-panel.ts +0 -364
  185. package/src/panels/tasks-panel.ts +0 -608
  186. package/src/panels/thinking-panel.ts +0 -333
  187. package/src/panels/tool-inspector-panel.ts +0 -537
  188. package/src/panels/work-plan-panel.ts +0 -540
  189. package/src/panels/worktree-panel.ts +0 -360
  190. package/src/panels/wrfc-panel.ts +0 -790
  191. package/src/renderer/agent-detail-modal.ts +0 -466
  192. package/src/renderer/live-tail-modal.ts +0 -156
  193. package/src/renderer/process-modal.ts +0 -671
  194. package/src/renderer/qr-renderer.ts +0 -120
@@ -0,0 +1,282 @@
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
+ // and submitting an answer to the current open question, approving execution,
24
+ // or refreshing. Submit/approve route to the `/plan` command.
25
+ //
26
+ // KNOWN GAP (flagged in the report): `/plan` has no subcommand that answers the
27
+ // CURRENT open question. The live app calls a submitPlanningAnswer callback that
28
+ // feeds the answer through the normal chat prompt — the action context has no
29
+ // such seam. Lacking a slash-command equivalent, submit routes the chosen answer
30
+ // text through `/plan <answer text>` (the free-form branch, which RESEEDS the
31
+ // goal rather than resolving the specific question) — a known-imprecise
32
+ // stand-in, surfaced honestly in an in-modal note.
33
+ // ---------------------------------------------------------------------------
34
+
35
+ export type PlanningModalService = Pick<ProjectPlanningService, 'status' | 'getState' | 'listDecisions' | 'getLanguage' | 'evaluate'>;
36
+
37
+ export interface PlanningModalDeps {
38
+ readonly service: PlanningModalService;
39
+ readonly projectId: string;
40
+ readonly requestRender?: () => void;
41
+ }
42
+
43
+ interface PlanningModalSnapshot {
44
+ readonly status: ProjectPlanningStatus | null;
45
+ readonly state: ProjectPlanningState | null;
46
+ readonly evaluation: ProjectPlanningEvaluation | null;
47
+ readonly decisions: readonly ProjectPlanningDecision[];
48
+ readonly language: ProjectPlanningLanguageArtifact | null;
49
+ }
50
+
51
+ interface TextLine { readonly content: string; readonly fg?: string; }
52
+
53
+ const GOOD = MODAL_TONES.good;
54
+ const WARN = MODAL_TONES.warn;
55
+ const BAD = MODAL_TONES.bad;
56
+ const INFO = MODAL_TONES.info;
57
+
58
+ function getCurrentQuestion(state: ProjectPlanningState, evaluation: ProjectPlanningEvaluation | null): ProjectPlanningQuestion | null {
59
+ const open = state.openQuestions.find((question) => (question.status ?? 'open') === 'open');
60
+ return open ?? evaluation?.nextQuestion ?? null;
61
+ }
62
+
63
+ function buildStateLines(state: ProjectPlanningState, evaluation: ProjectPlanningEvaluation | null): TextLine[] {
64
+ const readiness = evaluation?.readiness ?? state.readiness;
65
+ const readinessColor = readiness === 'executable' ? GOOD : readiness === 'needs-user-input' ? WARN : undefined;
66
+ const blockingGaps = (evaluation?.gaps ?? []).filter((gap) => gap.severity === 'blocking').length;
67
+ const lines: TextLine[] = [
68
+ { content: `readiness ${readiness} approved ${state.executionApproved ? 'yes' : 'no'} questions ${state.openQuestions.length} open / ${state.answeredQuestions.length} answered`, ...(readinessColor ? { fg: readinessColor } : {}) },
69
+ { content: `blocking gaps ${blockingGaps} tasks ${state.tasks.length} gates ${state.verificationGates.length}` },
70
+ { content: `goal: ${state.goal || '(not set)'}` },
71
+ ];
72
+ if (state.scope) lines.push({ content: `scope: ${state.scope}` });
73
+ if (state.knownContext.length) lines.push({ content: `known context: ${state.knownContext.join(' | ')}` });
74
+ if (evaluation?.nextQuestion) lines.push({ content: `next question: ${evaluation.nextQuestion.prompt}`, fg: INFO });
75
+ return lines;
76
+ }
77
+
78
+ function buildGapsLines(evaluation: ProjectPlanningEvaluation | null): TextLine[] {
79
+ const gaps = evaluation?.gaps ?? [];
80
+ if (gaps.length === 0) return [{ content: 'Readiness gaps: none.', fg: GOOD }];
81
+ return [{ content: 'Readiness gaps:' }, ...gaps.slice(0, 12).map((gap) => ({ content: ` ${gap.severity.toUpperCase()} ${gap.kind}: ${gap.message}`, fg: gap.severity === 'blocking' ? BAD : WARN }))];
82
+ }
83
+
84
+ function buildTasksLines(state: ProjectPlanningState): TextLine[] {
85
+ const lines: TextLine[] = [{ content: 'Task graph:' }];
86
+ if (state.tasks.length === 0) {
87
+ lines.push({ content: ' No decomposed tasks recorded yet.' });
88
+ } else {
89
+ for (const task of state.tasks) {
90
+ lines.push({ content: ` ${task.id}: ${task.title} [${task.status ?? 'pending'}]${task.canRunConcurrently ? ' - concurrent' : ''}`, ...(task.blockedOnUserInput ? { fg: WARN } : {}) });
91
+ if (task.dependencies?.length) lines.push({ content: ` dependencies: ${task.dependencies.join(', ')}` });
92
+ if (task.verification?.length) lines.push({ content: ` verification: ${task.verification.join(' | ')}`, fg: GOOD });
93
+ }
94
+ }
95
+ if (state.verificationGates.length) {
96
+ lines.push({ content: 'Verification gates:' });
97
+ for (const gate of state.verificationGates) lines.push({ content: ` ${gate.id}: ${gate.description} [${gate.status ?? 'pending'}]`, fg: gate.required === false ? undefined : GOOD });
98
+ }
99
+ if (state.agentAssignments.length) {
100
+ lines.push({ content: 'Agent handoff candidates:' });
101
+ for (const assignment of state.agentAssignments) lines.push({ content: ` ${assignment.taskId}: ${assignment.agentType ?? 'none'}${assignment.canRunConcurrently ? ' - can run concurrently' : ''}`, fg: INFO });
102
+ }
103
+ return lines;
104
+ }
105
+
106
+ function buildDecisionsLines(state: ProjectPlanningState, storedDecisions: readonly ProjectPlanningDecision[]): TextLine[] {
107
+ const byId = new Map<string, ProjectPlanningDecision>();
108
+ for (const decision of [...storedDecisions, ...state.decisions]) byId.set(decision.id, decision);
109
+ const decisions = [...byId.values()];
110
+ if (decisions.length === 0) return [{ content: 'Decisions: none recorded yet.' }];
111
+ return [{ content: 'Decisions:' }, ...decisions.slice(0, 12).map((decision) => ({ content: ` ${decision.title}: ${decision.decision} [${decision.status ?? 'accepted'}]`, fg: decision.status === 'rejected' ? BAD : undefined }))];
112
+ }
113
+
114
+ function buildLanguageLines(language: ProjectPlanningLanguageArtifact | null): TextLine[] {
115
+ if (!language || (language.terms.length === 0 && language.ambiguities.length === 0)) return [{ content: 'Project language: no terms or ambiguity resolutions recorded yet.' }];
116
+ const lines: TextLine[] = [{ content: 'Project language:' }];
117
+ for (const term of language.terms.slice(0, 8)) {
118
+ lines.push({ content: ` ${term.term}: ${term.definition}` });
119
+ if (term.avoid?.length) lines.push({ content: ` avoid: ${term.avoid.join(', ')}`, fg: BAD });
120
+ }
121
+ for (const ambiguity of language.ambiguities.slice(0, 8)) lines.push({ content: ` resolved ambiguity - ${ambiguity.phrase}: ${ambiguity.resolution}`, fg: INFO });
122
+ return lines;
123
+ }
124
+
125
+ class PlanningModalSurface implements ConfigModalSurface {
126
+ readonly name = 'planning-modal';
127
+ readonly title = 'Planning';
128
+ private snapshot: PlanningModalSnapshot | null = null;
129
+ private loading = false;
130
+ private requestRender: () => void = () => {};
131
+
132
+ constructor(private readonly deps: PlanningModalDeps) {}
133
+
134
+ readonly actions = [
135
+ { key: 'enter', id: 'submit', label: 'submit', enabledFor: () => this.currentAnswerActions().actions.length > 0 },
136
+ { key: 'a', id: 'approve', label: 'approve execution' },
137
+ { key: 'r', id: 'refresh', label: 'refresh' },
138
+ ];
139
+
140
+ onOpen(requestRender: () => void): void { this.requestRender = requestRender; this.refresh(); }
141
+
142
+ private refresh(): void {
143
+ if (this.loading) return;
144
+ this.loading = true;
145
+ void this.load();
146
+ }
147
+
148
+ private async load(): Promise<void> {
149
+ try {
150
+ const [status, stateResult, decisionsResult, languageResult] = await Promise.all([
151
+ this.deps.service.status({ projectId: this.deps.projectId }),
152
+ this.deps.service.getState({ projectId: this.deps.projectId }),
153
+ this.deps.service.listDecisions({ projectId: this.deps.projectId }),
154
+ this.deps.service.getLanguage({ projectId: this.deps.projectId }),
155
+ ]);
156
+ const evaluation = await this.deps.service.evaluate({ projectId: this.deps.projectId });
157
+ this.snapshot = { status, state: stateResult.state, evaluation, decisions: decisionsResult.decisions, language: languageResult.language };
158
+ } catch {
159
+ // Leave the previous snapshot (if any) in place.
160
+ } finally {
161
+ this.loading = false;
162
+ this.requestRender();
163
+ this.deps.requestRender?.();
164
+ }
165
+ }
166
+
167
+ private currentAnswerActions(): { readonly question: ProjectPlanningQuestion | null; readonly actions: readonly PlanningAnswerAction[] } {
168
+ if (!this.snapshot?.state) return { question: null, actions: [] };
169
+ const question = getCurrentQuestion(this.snapshot.state, this.snapshot.evaluation);
170
+ if (!question) return { question: null, actions: [] };
171
+ return { question, actions: buildAnswerActions(question, '') };
172
+ }
173
+
174
+ buildView(): ConfigModalView {
175
+ if (!this.snapshot) {
176
+ 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'] };
177
+ }
178
+
179
+ const { status, state, evaluation, decisions, language } = this.snapshot;
180
+ const header = [`project ${this.deps.projectId} space ${status?.knowledgeSpaceId ?? `project:${this.deps.projectId}`}`];
181
+ const rows: ConfigModalRow[] = [];
182
+ let n = 0;
183
+ const line = (l: TextLine): void => { rows.push({ id: `p:${n++}`, label: l.content, selectable: false, ...(l.fg ? { style: { fg: l.fg } } : {}) }); };
184
+
185
+ if (!state) {
186
+ line({ content: 'No project planning state has been saved for this workspace.' });
187
+ line({ content: 'Describe the intended change in normal chat to start the planning interview.', fg: undefined });
188
+ return { title: 'Planning', tabs: [{ id: 'planning', label: 'Planning', header, rows, emptyText: '' }], hints: ['r refresh'] };
189
+ }
190
+
191
+ for (const l of buildStateLines(state, evaluation)) line(l);
192
+
193
+ const { question, actions } = this.currentAnswerActions();
194
+ if (question) {
195
+ line({ content: 'Answer Current Question' });
196
+ line({ content: question.prompt, fg: INFO });
197
+ if (question.whyItMatters) line({ content: `Why this matters: ${question.whyItMatters}` });
198
+ if (question.recommendedAnswer && !isGenericRecommendation(question.recommendedAnswer)) line({ content: `Recommendation: ${question.recommendedAnswer}`, fg: GOOD });
199
+ for (const action of actions) {
200
+ rows.push({ id: action.id, label: `${action.label} - ${action.detail}`, ...(action.disabled ? { selectable: false } : {}) });
201
+ }
202
+ line({ content: 'Note: submit reseeds the plan goal via /plan <text> (no per-question answer command exists yet).', fg: undefined });
203
+ }
204
+
205
+ for (const l of buildGapsLines(evaluation)) line(l);
206
+ for (const l of buildTasksLines(state)) line(l);
207
+ for (const l of buildDecisionsLines(state, decisions)) line(l);
208
+ for (const l of buildLanguageLines(language)) line(l);
209
+
210
+ return {
211
+ title: 'Planning',
212
+ tabs: [{ id: 'planning', label: 'Planning', header, rows, hints: [...(question ? ['enter submit'] : []), 'a approve execution'] }],
213
+ };
214
+ }
215
+
216
+ onAction(id: string, ctx: ConfigModalActionContext): void {
217
+ if (id === 'refresh') { this.refresh(); ctx.setStatus('Reloading project planning state…'); return; }
218
+ if (id === 'approve') { void ctx.executeCommand?.('plan', ['approve']); ctx.setStatus('Dispatched /plan approve.'); return; }
219
+ if (id !== 'submit') return;
220
+ const { question, actions } = this.currentAnswerActions();
221
+ if (!question || actions.length === 0) return;
222
+ const action = ctx.row ? actions.find((a) => a.id === ctx.row!.id) : undefined;
223
+ if (!action || action.disabled || !action.answer.trim()) { ctx.print('Choose a non-empty answer option.'); return; }
224
+ if (action.kind === 'approve') { void ctx.executeCommand?.('plan', ['approve']); ctx.setStatus('Dispatched /plan approve.'); return; }
225
+ if (action.kind === 'dismiss') {
226
+ // There is NO `/plan dismiss` subcommand. Dispatching it fell through to
227
+ // planning-runtime's free-form goal-seed branch and OVERWROTE the project
228
+ // goal with the literal string "dismiss". Do not dispatch and do not
229
+ // mutate any state — print an honest note and close the panel. Planning
230
+ // is untouched (matching the relabeled "Close (planning unchanged)" row).
231
+ ctx.print('Pausing project planning isn\'t available as a command yet; planning state left unchanged. Closing the panel.');
232
+ ctx.close();
233
+ return;
234
+ }
235
+ void ctx.executeCommand?.('plan', action.answer.trim().split(/\s+/));
236
+ ctx.setStatus(`Dispatched /plan ${action.answer.trim()} (reseeds the goal — see the in-modal note).`);
237
+ }
238
+ }
239
+
240
+ export function createPlanningModalSurface(deps: PlanningModalDeps): ConfigModalSurface {
241
+ return new PlanningModalSurface(deps);
242
+ }
243
+
244
+ function buildGoldenService(): PlanningModalService {
245
+ const state: ProjectPlanningState = {
246
+ id: 'golden-state-1', projectId: 'golden-project', knowledgeSpaceId: 'project:golden-project',
247
+ goal: 'Ship the golden fixture end-to-end.', scope: 'Golden fixture scope only.', knownContext: ['Seeded for a deterministic golden render.'],
248
+ 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' }],
249
+ answeredQuestions: [{ id: 'golden-question-0', prompt: 'Is this a golden fixture?', status: 'answered', answer: 'Yes.', answeredAt: 0 }],
250
+ decisions: [], assumptions: [], constraints: [], risks: [],
251
+ tasks: [{ id: 'golden-task-1', title: 'Implement the golden fixture', status: 'pending', dependencies: [], verification: ['bun test src/test/panels/modals/planning-modal.test.ts'] }],
252
+ dependencies: [], verificationGates: [{ id: 'golden-gate-1', description: 'Golden render is byte-stable.', status: 'pending', required: true }],
253
+ agentAssignments: [{ taskId: 'golden-task-1', agentType: 'worker', canRunConcurrently: false }],
254
+ readiness: 'needs-user-input', executionApproved: false, createdAt: 0, updatedAt: 0,
255
+ };
256
+ const evaluation: ProjectPlanningEvaluation = {
257
+ ok: true, projectId: 'golden-project', knowledgeSpaceId: 'project:golden-project', readiness: 'needs-user-input',
258
+ gaps: [{ id: 'golden-gap-1', kind: 'open-question', severity: 'blocking', message: 'One open question remains.' }],
259
+ nextQuestion: state.openQuestions[0], state,
260
+ };
261
+ const decision: ProjectPlanningDecision = { id: 'golden-decision-1', title: 'Use a golden fixture', decision: 'Freeze all ids/timestamps for a byte-stable render.', status: 'accepted' };
262
+ 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 };
263
+ return {
264
+ 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: [] }),
265
+ getState: async () => ({ ok: true, projectId: 'golden-project', knowledgeSpaceId: 'project:golden-project', state }),
266
+ listDecisions: async () => ({ ok: true, projectId: 'golden-project', knowledgeSpaceId: 'project:golden-project', decisions: [decision] }),
267
+ getLanguage: async () => ({ ok: true, projectId: 'golden-project', knowledgeSpaceId: 'project:golden-project', language }),
268
+ evaluate: async () => evaluation,
269
+ };
270
+ }
271
+
272
+ /**
273
+ * Deterministic golden fixture. All ids/timestamps are frozen literals. Because
274
+ * every service method is Promise-based, this helper is async — it opens the
275
+ * surface, waits a macrotask so the fire-and-forget load resolves, then returns.
276
+ */
277
+ export async function planningModalGoldenSurface(): Promise<ConfigModalSurface> {
278
+ const surface = createPlanningModalSurface({ service: buildGoldenService(), projectId: 'golden-project' });
279
+ surface.onOpen?.(() => {});
280
+ await new Promise((resolve) => setTimeout(resolve, 0));
281
+ return surface;
282
+ }
@@ -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
+ }