@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.
- package/CHANGELOG.md +85 -21
- package/README.md +20 -11
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +2 -2
- package/src/core/alert-gating.ts +67 -0
- package/src/core/approval-alert.ts +72 -0
- package/src/core/budget-breach-notifier.ts +106 -0
- package/src/core/conversation-rendering.ts +19 -0
- package/src/core/conversation.ts +18 -0
- package/src/core/focus-tracker.ts +41 -0
- package/src/core/long-task-notifier.ts +33 -6
- package/src/core/system-message-router.ts +37 -51
- package/src/core/turn-cancellation.ts +20 -0
- package/src/core/turn-event-wiring.ts +64 -3
- package/src/export/cost-utils.ts +36 -0
- package/src/input/command-registry.ts +38 -1
- package/src/input/commands/checkpoint-runtime.ts +280 -0
- package/src/input/commands/codebase-runtime.ts +192 -0
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/health-runtime.ts +1 -1
- package/src/input/commands/image-runtime.ts +112 -0
- package/src/input/commands/intelligence-runtime.ts +11 -1
- package/src/input/commands/local-auth-runtime.ts +4 -1
- package/src/input/commands/local-runtime.ts +9 -3
- package/src/input/commands/marketplace-runtime.ts +2 -2
- package/src/input/commands/memory.ts +6 -1
- package/src/input/commands/operator-panel-runtime.ts +40 -24
- package/src/input/commands/planning-runtime.ts +26 -3
- package/src/input/commands/platform-sandbox-runtime.ts +1 -6
- package/src/input/commands/plugin-runtime.ts +2 -2
- package/src/input/commands/policy-dispatch.ts +21 -0
- package/src/input/commands/provider-accounts-runtime.ts +1 -1
- package/src/input/commands/qrcode-runtime.ts +3 -3
- package/src/input/commands/recall-review.ts +35 -0
- package/src/input/commands/remote-runtime.ts +3 -3
- package/src/input/commands/runtime-services.ts +54 -13
- package/src/input/commands/services-runtime.ts +1 -1
- package/src/input/commands/session-workflow.ts +16 -1
- package/src/input/commands/settings-sync-runtime.ts +1 -1
- package/src/input/commands/shell-core.ts +15 -7
- package/src/input/commands/skills-runtime.ts +2 -8
- package/src/input/commands/subscription-runtime.ts +2 -2
- package/src/input/commands/test-runtime.ts +277 -0
- package/src/input/commands/websearch-runtime.ts +101 -0
- package/src/input/commands/work-plan-runtime.ts +36 -10
- package/src/input/commands/workstream-runtime.ts +338 -0
- package/src/input/commands.ts +12 -0
- package/src/input/config-modal-types.ts +161 -0
- package/src/input/config-modal.ts +377 -0
- package/src/input/feed-context-factory.ts +7 -8
- package/src/input/handler-command-route.ts +27 -17
- package/src/input/handler-feed-routes.ts +61 -23
- package/src/input/handler-feed.ts +47 -23
- package/src/input/handler-interactions.ts +1 -3
- package/src/input/handler-modal-routes.ts +65 -0
- package/src/input/handler-modal-stack.ts +3 -5
- package/src/input/handler-modal-token-routes.ts +16 -49
- package/src/input/handler-picker-routes.ts +11 -94
- package/src/input/handler-shortcuts.ts +24 -14
- package/src/input/handler-types.ts +2 -6
- package/src/input/handler-ui-state.ts +10 -22
- package/src/input/handler.ts +6 -28
- package/src/input/keybindings.ts +22 -8
- package/src/input/model-picker-types.ts +24 -6
- package/src/input/model-picker.ts +58 -0
- package/src/input/panel-integration-actions.ts +9 -170
- package/src/input/settings-modal-data.ts +95 -0
- package/src/input/settings-modal-mutations.ts +6 -4
- package/src/main.ts +24 -27
- package/src/panels/agent-inspector-shared.ts +2 -1
- package/src/panels/base-panel.ts +22 -1
- package/src/panels/builtin/agent.ts +21 -107
- package/src/panels/builtin/development.ts +15 -61
- package/src/panels/builtin/knowledge.ts +8 -32
- package/src/panels/builtin/operations.ts +84 -428
- package/src/panels/builtin/session.ts +21 -112
- package/src/panels/builtin/shared.ts +9 -43
- package/src/panels/builtin-modals.ts +218 -0
- package/src/panels/builtin-panels.ts +5 -0
- package/src/panels/cost-tracker-panel.ts +36 -10
- package/src/panels/diff-panel.ts +12 -43
- package/src/panels/eval-registry.ts +60 -0
- package/src/panels/fleet-panel.ts +800 -0
- package/src/panels/fleet-read-model.ts +477 -0
- package/src/panels/fleet-steer.ts +92 -0
- package/src/panels/fleet-stop.ts +125 -0
- package/src/panels/fleet-tabs.ts +230 -0
- package/src/panels/fleet-transcript.ts +356 -0
- package/src/panels/index.ts +7 -31
- package/src/panels/modals/hooks-modal.ts +187 -0
- package/src/panels/modals/keybindings-modal.ts +151 -0
- package/src/panels/modals/knowledge-modal.ts +158 -0
- package/src/panels/modals/local-auth-modal.ts +132 -0
- package/src/panels/modals/marketplace-modal.ts +218 -0
- package/src/panels/modals/memory-modal.ts +180 -0
- package/src/panels/modals/modal-surface-helpers.ts +54 -0
- package/src/panels/modals/modal-theme.ts +36 -0
- package/src/panels/modals/pairing-modal.ts +144 -0
- package/src/panels/modals/planning-modal.ts +282 -0
- package/src/panels/modals/plugins-modal.ts +174 -0
- package/src/panels/modals/policy-modal.ts +256 -0
- package/src/panels/modals/provider-health-modal.ts +136 -0
- package/src/panels/modals/remote-modal.ts +115 -0
- package/src/panels/modals/sandbox-modal.ts +150 -0
- package/src/panels/modals/security-modal.ts +217 -0
- package/src/panels/modals/services-modal.ts +179 -0
- package/src/panels/modals/settings-sync-modal.ts +142 -0
- package/src/panels/modals/skills-modal.ts +189 -0
- package/src/panels/modals/subscription-modal.ts +172 -0
- package/src/panels/modals/work-plan-modal.ts +149 -0
- package/src/panels/panel-confirm-overlay.ts +72 -0
- package/src/panels/panel-manager.ts +108 -5
- package/src/panels/project-planning-answer-actions.ts +4 -2
- package/src/panels/skills-panel.ts +7 -51
- package/src/panels/types.ts +13 -0
- package/src/permissions/hunk-selection.ts +179 -0
- package/src/permissions/prompt.ts +60 -4
- package/src/renderer/compaction-history-modal.ts +19 -3
- package/src/renderer/compaction-preview.ts +13 -2
- package/src/renderer/compaction-quality.ts +100 -0
- package/src/renderer/config-modal.ts +81 -0
- package/src/renderer/conversation-overlays.ts +10 -17
- package/src/renderer/fleet-tab-strip.ts +56 -0
- package/src/renderer/footer-tips.ts +10 -2
- package/src/renderer/git-status.ts +40 -0
- package/src/renderer/help-overlay.ts +2 -2
- package/src/renderer/model-workspace.ts +44 -1
- package/src/renderer/panel-workspace-bar.ts +8 -2
- package/src/renderer/turn-injection.ts +114 -0
- package/src/runtime/bootstrap-command-context.ts +21 -1
- package/src/runtime/bootstrap-command-parts.ts +34 -7
- package/src/runtime/bootstrap-core.ts +14 -4
- package/src/runtime/bootstrap-shell.ts +29 -16
- package/src/runtime/bootstrap.ts +11 -17
- package/src/runtime/code-index-services.ts +112 -0
- package/src/runtime/orchestrator-core-services.ts +49 -0
- package/src/runtime/process-lifecycle.ts +3 -1
- package/src/runtime/services.ts +91 -43
- package/src/runtime/ui-services.ts +8 -0
- package/src/runtime/workstream-services.ts +195 -0
- package/src/shell/blocking-input.ts +22 -1
- package/src/shell/ui-openers.ts +129 -17
- package/src/utils/format-duration.ts +8 -18
- package/src/utils/splash-lines.ts +1 -1
- package/src/version.ts +1 -1
- package/src/panels/agent-inspector-panel.ts +0 -786
- package/src/panels/approval-panel.ts +0 -252
- package/src/panels/automation-control-panel.ts +0 -479
- package/src/panels/cockpit-panel.ts +0 -481
- package/src/panels/cockpit-read-model.ts +0 -238
- package/src/panels/communication-panel.ts +0 -256
- package/src/panels/control-plane-panel.ts +0 -470
- package/src/panels/debug-panel.ts +0 -615
- package/src/panels/docs-panel.ts +0 -384
- package/src/panels/eval-panel.ts +0 -627
- package/src/panels/file-explorer-panel.ts +0 -673
- package/src/panels/file-preview-panel.ts +0 -517
- package/src/panels/hooks-panel.ts +0 -401
- package/src/panels/incident-review-panel.ts +0 -406
- package/src/panels/intelligence-panel.ts +0 -383
- package/src/panels/knowledge-graph-panel.ts +0 -515
- package/src/panels/marketplace-panel.ts +0 -399
- package/src/panels/memory-panel.ts +0 -558
- package/src/panels/ops-control-panel.ts +0 -329
- package/src/panels/ops-strategy-panel.ts +0 -321
- package/src/panels/orchestration-panel.ts +0 -465
- package/src/panels/panel-list-panel.ts +0 -566
- package/src/panels/plan-dashboard-panel.ts +0 -707
- package/src/panels/policy-panel.ts +0 -517
- package/src/panels/project-planning-panel.ts +0 -731
- package/src/panels/provider-health-panel.ts +0 -678
- package/src/panels/provider-health-tracker.ts +0 -310
- package/src/panels/provider-health-views.ts +0 -567
- package/src/panels/qr-panel.ts +0 -280
- package/src/panels/remote-panel.ts +0 -534
- package/src/panels/routes-panel.ts +0 -241
- package/src/panels/sandbox-panel.ts +0 -456
- package/src/panels/security-panel.ts +0 -447
- package/src/panels/services-panel.ts +0 -329
- package/src/panels/session-browser-panel.ts +0 -496
- package/src/panels/settings-sync-panel.ts +0 -398
- package/src/panels/subscription-panel.ts +0 -342
- package/src/panels/symbol-outline-panel.ts +0 -619
- package/src/panels/system-messages-panel.ts +0 -364
- package/src/panels/tasks-panel.ts +0 -608
- package/src/panels/thinking-panel.ts +0 -333
- package/src/panels/tool-inspector-panel.ts +0 -537
- package/src/panels/work-plan-panel.ts +0 -540
- package/src/panels/worktree-panel.ts +0 -360
- package/src/panels/wrfc-panel.ts +0 -790
- package/src/renderer/agent-detail-modal.ts +0 -466
- package/src/renderer/live-tail-modal.ts +0 -156
- package/src/renderer/process-modal.ts +0 -671
- 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
|
+
}
|