@agile-vibe-coding/avc 0.1.1 → 0.3.1
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/cli/agent-loader.js +21 -0
- package/cli/agents/agent-selector.md +152 -0
- package/cli/agents/architecture-recommender.md +418 -0
- package/cli/agents/code-implementer.md +117 -0
- package/cli/agents/code-validator.md +80 -0
- package/cli/agents/context-reviewer-epic.md +101 -0
- package/cli/agents/context-reviewer-story.md +92 -0
- package/cli/agents/context-writer-epic.md +145 -0
- package/cli/agents/context-writer-story.md +111 -0
- package/cli/agents/database-deep-dive.md +470 -0
- package/cli/agents/database-recommender.md +634 -0
- package/cli/agents/doc-distributor.md +176 -0
- package/cli/agents/doc-writer-epic.md +42 -0
- package/cli/agents/doc-writer-story.md +43 -0
- package/cli/agents/documentation-updater.md +203 -0
- package/cli/agents/duplicate-detector.md +110 -0
- package/cli/agents/epic-story-decomposer.md +559 -0
- package/cli/agents/feature-context-generator.md +91 -0
- package/cli/agents/gap-checker-epic.md +52 -0
- package/cli/agents/impact-checker-story.md +51 -0
- package/cli/agents/migration-guide-generator.md +305 -0
- package/cli/agents/mission-scope-generator.md +143 -0
- package/cli/agents/mission-scope-validator.md +146 -0
- package/cli/agents/project-context-extractor.md +122 -0
- package/cli/agents/project-documentation-creator.json +226 -0
- package/cli/agents/project-documentation-creator.md +595 -0
- package/cli/agents/question-prefiller.md +269 -0
- package/cli/agents/refiner-epic.md +39 -0
- package/cli/agents/refiner-story.md +42 -0
- package/cli/agents/scaffolding-generator.md +99 -0
- package/cli/agents/seed-validator.md +71 -0
- package/cli/agents/story-doc-enricher.md +133 -0
- package/cli/agents/story-scope-reviewer.md +147 -0
- package/cli/agents/story-splitter.md +83 -0
- package/cli/agents/suggestion-business-analyst.md +88 -0
- package/cli/agents/suggestion-deployment-architect.md +263 -0
- package/cli/agents/suggestion-product-manager.md +129 -0
- package/cli/agents/suggestion-security-specialist.md +156 -0
- package/cli/agents/suggestion-technical-architect.md +269 -0
- package/cli/agents/suggestion-ux-researcher.md +93 -0
- package/cli/agents/task-subtask-decomposer.md +188 -0
- package/cli/agents/validator-documentation.json +183 -0
- package/cli/agents/validator-documentation.md +455 -0
- package/cli/agents/validator-selector.md +211 -0
- package/cli/ansi-colors.js +21 -0
- package/cli/api-reference-tool.js +368 -0
- package/cli/build-docs.js +29 -8
- package/cli/ceremony-history.js +369 -0
- package/cli/checks/catalog.json +76 -0
- package/cli/checks/code/quality.json +26 -0
- package/cli/checks/code/testing.json +14 -0
- package/cli/checks/code/traceability.json +26 -0
- package/cli/checks/cross-refs/epic.json +171 -0
- package/cli/checks/cross-refs/story.json +149 -0
- package/cli/checks/epic/api.json +114 -0
- package/cli/checks/epic/backend.json +126 -0
- package/cli/checks/epic/cloud.json +126 -0
- package/cli/checks/epic/data.json +102 -0
- package/cli/checks/epic/database.json +114 -0
- package/cli/checks/epic/developer.json +182 -0
- package/cli/checks/epic/devops.json +174 -0
- package/cli/checks/epic/frontend.json +162 -0
- package/cli/checks/epic/mobile.json +102 -0
- package/cli/checks/epic/qa.json +90 -0
- package/cli/checks/epic/security.json +184 -0
- package/cli/checks/epic/solution-architect.json +192 -0
- package/cli/checks/epic/test-architect.json +90 -0
- package/cli/checks/epic/ui.json +102 -0
- package/cli/checks/epic/ux.json +90 -0
- package/cli/checks/fixes/epic-fix-template.md +10 -0
- package/cli/checks/fixes/story-fix-template.md +10 -0
- package/cli/checks/story/api.json +186 -0
- package/cli/checks/story/backend.json +102 -0
- package/cli/checks/story/cloud.json +102 -0
- package/cli/checks/story/data.json +210 -0
- package/cli/checks/story/database.json +102 -0
- package/cli/checks/story/developer.json +168 -0
- package/cli/checks/story/devops.json +102 -0
- package/cli/checks/story/frontend.json +174 -0
- package/cli/checks/story/mobile.json +102 -0
- package/cli/checks/story/qa.json +210 -0
- package/cli/checks/story/security.json +198 -0
- package/cli/checks/story/solution-architect.json +230 -0
- package/cli/checks/story/test-architect.json +210 -0
- package/cli/checks/story/ui.json +102 -0
- package/cli/checks/story/ux.json +102 -0
- package/cli/coding-order.js +401 -0
- package/cli/command-logger.js +49 -12
- package/cli/components/static-output.js +63 -0
- package/cli/console-output-manager.js +94 -0
- package/cli/dependency-checker.js +72 -0
- package/cli/docs-sync.js +306 -0
- package/cli/epic-story-validator.js +659 -0
- package/cli/evaluation-prompts.js +1008 -0
- package/cli/execution-context.js +195 -0
- package/cli/generate-summary-table.js +340 -0
- package/cli/init-model-config.js +704 -0
- package/cli/init.js +1737 -278
- package/cli/kanban-server-manager.js +227 -0
- package/cli/llm-claude.js +150 -1
- package/cli/llm-gemini.js +109 -0
- package/cli/llm-local.js +493 -0
- package/cli/llm-mock.js +233 -0
- package/cli/llm-openai.js +454 -0
- package/cli/llm-provider.js +379 -3
- package/cli/llm-token-limits.js +211 -0
- package/cli/llm-verifier.js +662 -0
- package/cli/llm-xiaomi.js +143 -0
- package/cli/message-constants.js +49 -0
- package/cli/message-manager.js +334 -0
- package/cli/message-types.js +96 -0
- package/cli/messaging-api.js +291 -0
- package/cli/micro-check-fixer.js +335 -0
- package/cli/micro-check-runner.js +449 -0
- package/cli/micro-check-scorer.js +148 -0
- package/cli/micro-check-validator.js +538 -0
- package/cli/model-pricing.js +192 -0
- package/cli/model-query-engine.js +468 -0
- package/cli/model-recommendation-analyzer.js +495 -0
- package/cli/model-selector.js +270 -0
- package/cli/output-buffer.js +107 -0
- package/cli/process-manager.js +73 -2
- package/cli/prompt-logger.js +57 -0
- package/cli/repl-ink.js +4625 -1094
- package/cli/repl-old.js +3 -4
- package/cli/seed-processor.js +962 -0
- package/cli/sprint-planning-processor.js +4162 -0
- package/cli/template-processor.js +2149 -105
- package/cli/templates/project.md +25 -8
- package/cli/templates/vitepress-config.mts.template +5 -4
- package/cli/token-tracker.js +547 -0
- package/cli/tools/generate-story-validators.js +317 -0
- package/cli/tools/generate-validators.js +669 -0
- package/cli/update-checker.js +19 -17
- package/cli/update-notifier.js +4 -4
- package/cli/validation-router.js +667 -0
- package/cli/verification-tracker.js +563 -0
- package/cli/worktree-runner.js +654 -0
- package/kanban/README.md +386 -0
- package/kanban/client/README.md +205 -0
- package/kanban/client/components.json +20 -0
- package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
- package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
- package/kanban/client/dist/index.html +16 -0
- package/kanban/client/dist/vite.svg +1 -0
- package/kanban/client/index.html +15 -0
- package/kanban/client/package-lock.json +9442 -0
- package/kanban/client/package.json +44 -0
- package/kanban/client/postcss.config.js +6 -0
- package/kanban/client/public/vite.svg +1 -0
- package/kanban/client/src/App.jsx +651 -0
- package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
- package/kanban/client/src/components/ceremony/AskArchPopup.jsx +420 -0
- package/kanban/client/src/components/ceremony/AskModelPopup.jsx +629 -0
- package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +1133 -0
- package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
- package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
- package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +686 -0
- package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +838 -0
- package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
- package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +136 -0
- package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
- package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
- package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
- package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +329 -0
- package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +249 -0
- package/kanban/client/src/components/kanban/CardDetailModal.jsx +646 -0
- package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
- package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
- package/kanban/client/src/components/kanban/GroupingSelector.jsx +63 -0
- package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
- package/kanban/client/src/components/kanban/KanbanCard.jsx +147 -0
- package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
- package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +784 -0
- package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
- package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
- package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
- package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
- package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
- package/kanban/client/src/components/settings/AgentsTab.jsx +381 -0
- package/kanban/client/src/components/settings/ApiKeysTab.jsx +142 -0
- package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +105 -0
- package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
- package/kanban/client/src/components/settings/CostThresholdsTab.jsx +95 -0
- package/kanban/client/src/components/settings/ModelPricingTab.jsx +269 -0
- package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
- package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
- package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
- package/kanban/client/src/components/stats/CostModal.jsx +384 -0
- package/kanban/client/src/components/ui/badge.jsx +27 -0
- package/kanban/client/src/components/ui/dialog.jsx +121 -0
- package/kanban/client/src/components/ui/tabs.jsx +85 -0
- package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
- package/kanban/client/src/hooks/useGrouping.js +177 -0
- package/kanban/client/src/hooks/useWebSocket.js +120 -0
- package/kanban/client/src/lib/__tests__/api.test.js +196 -0
- package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
- package/kanban/client/src/lib/api.js +515 -0
- package/kanban/client/src/lib/status-grouping.js +154 -0
- package/kanban/client/src/lib/utils.js +11 -0
- package/kanban/client/src/main.jsx +10 -0
- package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
- package/kanban/client/src/store/ceremonyStore.js +172 -0
- package/kanban/client/src/store/filterStore.js +201 -0
- package/kanban/client/src/store/kanbanStore.js +123 -0
- package/kanban/client/src/store/processStore.js +65 -0
- package/kanban/client/src/store/sprintPlanningStore.js +33 -0
- package/kanban/client/src/styles/globals.css +59 -0
- package/kanban/client/tailwind.config.js +77 -0
- package/kanban/client/vite.config.js +28 -0
- package/kanban/client/vitest.config.js +28 -0
- package/kanban/dev-start.sh +47 -0
- package/kanban/package.json +12 -0
- package/kanban/server/index.js +537 -0
- package/kanban/server/routes/ceremony.js +454 -0
- package/kanban/server/routes/costs.js +163 -0
- package/kanban/server/routes/openai-oauth.js +366 -0
- package/kanban/server/routes/processes.js +50 -0
- package/kanban/server/routes/settings.js +736 -0
- package/kanban/server/routes/websocket.js +281 -0
- package/kanban/server/routes/work-items.js +487 -0
- package/kanban/server/services/CeremonyService.js +1441 -0
- package/kanban/server/services/FileSystemScanner.js +95 -0
- package/kanban/server/services/FileWatcher.js +144 -0
- package/kanban/server/services/HierarchyBuilder.js +196 -0
- package/kanban/server/services/ProcessRegistry.js +122 -0
- package/kanban/server/services/TaskRunnerService.js +261 -0
- package/kanban/server/services/WorkItemReader.js +123 -0
- package/kanban/server/services/WorkItemRefineService.js +510 -0
- package/kanban/server/start.js +49 -0
- package/kanban/server/utils/kanban-logger.js +132 -0
- package/kanban/server/utils/markdown.js +91 -0
- package/kanban/server/utils/status-grouping.js +107 -0
- package/kanban/server/workers/run-task-worker.js +121 -0
- package/kanban/server/workers/seed-worker.js +94 -0
- package/kanban/server/workers/sponsor-call-worker.js +92 -0
- package/kanban/server/workers/sprint-planning-worker.js +212 -0
- package/package.json +19 -7
- package/cli/agents/documentation.md +0 -302
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useSprintPlanningStore } from '../../store/sprintPlanningStore';
|
|
3
|
+
import { confirmSprintPlanningSelection, cancelCeremony } from '../../lib/api';
|
|
4
|
+
|
|
5
|
+
// ── Inner component: only mounted when hierarchy is non-null ──────────────────
|
|
6
|
+
// Using a wrapper+inner pattern so useState can be lazily initialised from
|
|
7
|
+
// a non-null hierarchy prop, avoiding "hooks before conditional return" issues.
|
|
8
|
+
|
|
9
|
+
function SelectionContent({ hierarchy, onConfirm, onCancel }) {
|
|
10
|
+
const [epicSel, setEpicSel] = useState(() => {
|
|
11
|
+
const m = {};
|
|
12
|
+
for (const e of hierarchy.epics) m[e.id] = true;
|
|
13
|
+
return m;
|
|
14
|
+
});
|
|
15
|
+
const [storySel, setStorySel] = useState(() => {
|
|
16
|
+
const m = {};
|
|
17
|
+
for (const e of hierarchy.epics)
|
|
18
|
+
for (const s of (e.stories || []))
|
|
19
|
+
m[s.id] = true;
|
|
20
|
+
return m;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const selectedEpicCount = Object.values(epicSel).filter(Boolean).length;
|
|
24
|
+
const totalEpicCount = hierarchy.epics.length;
|
|
25
|
+
const selectedStoryCount = Object.values(storySel).filter(Boolean).length;
|
|
26
|
+
const totalStoryCount = hierarchy.epics.reduce((n, e) => n + (e.stories?.length || 0), 0);
|
|
27
|
+
const canConfirm = selectedEpicCount > 0;
|
|
28
|
+
|
|
29
|
+
const toggleEpic = (epicId, checked) => {
|
|
30
|
+
setEpicSel(prev => ({ ...prev, [epicId]: checked }));
|
|
31
|
+
const epic = hierarchy.epics.find(e => e.id === epicId);
|
|
32
|
+
if (epic) {
|
|
33
|
+
const update = {};
|
|
34
|
+
for (const s of (epic.stories || [])) update[s.id] = checked;
|
|
35
|
+
setStorySel(prev => ({ ...prev, ...update }));
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const toggleStory = (epicId, storyId, checked) => {
|
|
40
|
+
const next = { ...storySel, [storyId]: checked };
|
|
41
|
+
setStorySel(next);
|
|
42
|
+
const epic = hierarchy.epics.find(e => e.id === epicId);
|
|
43
|
+
if (epic) {
|
|
44
|
+
const anyChecked = (epic.stories || []).some(s => next[s.id]);
|
|
45
|
+
setEpicSel(prev => ({ ...prev, [epicId]: anyChecked }));
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const selectAll = () => {
|
|
50
|
+
const em = {}, sm = {};
|
|
51
|
+
for (const e of hierarchy.epics) {
|
|
52
|
+
em[e.id] = true;
|
|
53
|
+
for (const s of (e.stories || [])) sm[s.id] = true;
|
|
54
|
+
}
|
|
55
|
+
setEpicSel(em); setStorySel(sm);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const deselectAll = () => {
|
|
59
|
+
const em = {}, sm = {};
|
|
60
|
+
for (const e of hierarchy.epics) {
|
|
61
|
+
em[e.id] = false;
|
|
62
|
+
for (const s of (e.stories || [])) sm[s.id] = false;
|
|
63
|
+
}
|
|
64
|
+
setEpicSel(em); setStorySel(sm);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const handleConfirm = () => {
|
|
68
|
+
const selectedEpicIds = Object.entries(epicSel).filter(([, v]) => v).map(([k]) => k);
|
|
69
|
+
const selectedStoryIds = Object.entries(storySel).filter(([, v]) => v).map(([k]) => k);
|
|
70
|
+
onConfirm(selectedEpicIds, selectedStoryIds);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<>
|
|
75
|
+
{/* Header */}
|
|
76
|
+
<div className="px-6 pt-5 pb-4 border-b border-slate-200 flex-shrink-0">
|
|
77
|
+
<h2 className="text-base font-semibold text-slate-900">Review Decomposed Work</h2>
|
|
78
|
+
<p className="text-sm text-slate-500 mt-1">
|
|
79
|
+
The AI has decomposed your scope into epics and stories. Select which items to carry
|
|
80
|
+
forward into validation — deselected items will be skipped entirely.
|
|
81
|
+
</p>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Scrollable checklist */}
|
|
85
|
+
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-4 min-h-0">
|
|
86
|
+
{/* Counts + select-all controls */}
|
|
87
|
+
<div className="flex items-center justify-between">
|
|
88
|
+
<p className="text-xs text-slate-500">
|
|
89
|
+
<span className="font-medium text-slate-700">{selectedEpicCount}/{totalEpicCount}</span> epics ·{' '}
|
|
90
|
+
<span className="font-medium text-slate-700">{selectedStoryCount}/{totalStoryCount}</span> stories selected
|
|
91
|
+
</p>
|
|
92
|
+
<div className="flex items-center gap-1.5">
|
|
93
|
+
<button onClick={selectAll} className="text-xs px-2 py-0.5 rounded border border-slate-200 text-slate-600 hover:bg-slate-50 transition-colors">
|
|
94
|
+
Select all
|
|
95
|
+
</button>
|
|
96
|
+
<button onClick={deselectAll} className="text-xs px-2 py-0.5 rounded border border-slate-200 text-slate-600 hover:bg-slate-50 transition-colors">
|
|
97
|
+
Deselect all
|
|
98
|
+
</button>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
{/* Two-level checklist */}
|
|
103
|
+
<div className="space-y-2">
|
|
104
|
+
{hierarchy.epics.map(epic => {
|
|
105
|
+
const epicChecked = epicSel[epic.id] ?? false;
|
|
106
|
+
const stories = epic.stories || [];
|
|
107
|
+
return (
|
|
108
|
+
<div key={epic.id} className="rounded-lg border border-slate-200 overflow-hidden">
|
|
109
|
+
{/* Epic row */}
|
|
110
|
+
<label className="flex items-start gap-3 px-4 py-3 bg-slate-50 cursor-pointer hover:bg-slate-100 select-none">
|
|
111
|
+
<input
|
|
112
|
+
type="checkbox"
|
|
113
|
+
checked={epicChecked}
|
|
114
|
+
onChange={e => toggleEpic(epic.id, e.target.checked)}
|
|
115
|
+
className="mt-0.5 flex-shrink-0 accent-slate-900"
|
|
116
|
+
/>
|
|
117
|
+
<div className="min-w-0 flex-1">
|
|
118
|
+
<p className="text-sm font-semibold text-slate-900 leading-snug">{epic.name}</p>
|
|
119
|
+
{epic.description && (
|
|
120
|
+
<p className="text-xs text-slate-500 mt-0.5 line-clamp-2">{epic.description}</p>
|
|
121
|
+
)}
|
|
122
|
+
<p className="text-xs text-slate-400 mt-0.5">
|
|
123
|
+
{stories.length} {stories.length === 1 ? 'story' : 'stories'}
|
|
124
|
+
{epic.domain ? <span className="ml-1 text-slate-300">· {epic.domain}</span> : null}
|
|
125
|
+
</p>
|
|
126
|
+
</div>
|
|
127
|
+
</label>
|
|
128
|
+
|
|
129
|
+
{/* Story rows */}
|
|
130
|
+
{stories.length > 0 && (
|
|
131
|
+
<div className="divide-y divide-slate-100">
|
|
132
|
+
{stories.map(story => (
|
|
133
|
+
<label
|
|
134
|
+
key={story.id}
|
|
135
|
+
className="flex items-start gap-3 px-4 py-2 pl-10 cursor-pointer hover:bg-slate-50 select-none"
|
|
136
|
+
>
|
|
137
|
+
<input
|
|
138
|
+
type="checkbox"
|
|
139
|
+
checked={storySel[story.id] ?? false}
|
|
140
|
+
onChange={e => toggleStory(epic.id, story.id, e.target.checked)}
|
|
141
|
+
className="mt-0.5 flex-shrink-0 accent-slate-600"
|
|
142
|
+
/>
|
|
143
|
+
<div className="min-w-0">
|
|
144
|
+
<p className="text-xs font-medium text-slate-800">{story.name}</p>
|
|
145
|
+
{story.userType && (
|
|
146
|
+
<p className="text-xs text-slate-400 mt-0.5">{story.userType}</p>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
</label>
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
})}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
{/* Footer */}
|
|
160
|
+
<div className="flex items-center justify-between px-6 py-4 border-t border-slate-200 flex-shrink-0">
|
|
161
|
+
<button
|
|
162
|
+
onClick={onCancel}
|
|
163
|
+
className="px-4 py-2 text-sm rounded-lg border border-red-200 text-red-600 hover:bg-red-50 transition-colors"
|
|
164
|
+
>
|
|
165
|
+
✕ Cancel Run
|
|
166
|
+
</button>
|
|
167
|
+
<button
|
|
168
|
+
onClick={handleConfirm}
|
|
169
|
+
disabled={!canConfirm}
|
|
170
|
+
className="px-6 py-2 text-sm font-medium bg-slate-900 text-white rounded-lg hover:bg-slate-700 disabled:opacity-40 disabled:cursor-not-allowed transition-colors"
|
|
171
|
+
>
|
|
172
|
+
Confirm Selection →
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
</>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Outer wrapper: guards on status + hierarchy ───────────────────────────────
|
|
180
|
+
|
|
181
|
+
export function EpicStorySelectionModal() {
|
|
182
|
+
const {
|
|
183
|
+
status,
|
|
184
|
+
decomposedHierarchy,
|
|
185
|
+
setStatus,
|
|
186
|
+
setStep,
|
|
187
|
+
setDecomposedHierarchy,
|
|
188
|
+
setError,
|
|
189
|
+
} = useSprintPlanningStore();
|
|
190
|
+
|
|
191
|
+
const [showCancelConfirm, setShowCancelConfirm] = useState(false);
|
|
192
|
+
|
|
193
|
+
// Only render when the worker is waiting for selection
|
|
194
|
+
if (status !== 'awaiting-selection' || !decomposedHierarchy) return null;
|
|
195
|
+
|
|
196
|
+
const handleConfirm = async (selectedEpicIds, selectedStoryIds) => {
|
|
197
|
+
setDecomposedHierarchy(null);
|
|
198
|
+
setStatus('running');
|
|
199
|
+
setStep(2);
|
|
200
|
+
try {
|
|
201
|
+
await confirmSprintPlanningSelection(selectedEpicIds, selectedStoryIds);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
setStatus('error');
|
|
204
|
+
setError(err.message);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const handleConfirmCancel = async () => {
|
|
209
|
+
setShowCancelConfirm(false);
|
|
210
|
+
try { await cancelCeremony(); } catch (_) {}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<div className="fixed inset-0 z-[80] flex items-center justify-center">
|
|
215
|
+
{/* Non-dismissible backdrop — user must make a selection or cancel */}
|
|
216
|
+
<div className="absolute inset-0 bg-black/60" />
|
|
217
|
+
|
|
218
|
+
{/* Modal */}
|
|
219
|
+
<div className="relative bg-white rounded-2xl shadow-2xl w-full max-w-2xl mx-4 max-h-[90vh] flex flex-col">
|
|
220
|
+
{/* Cancel confirmation overlay */}
|
|
221
|
+
{showCancelConfirm && (
|
|
222
|
+
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/90 rounded-2xl">
|
|
223
|
+
<div className="bg-white border border-slate-200 rounded-xl shadow-lg p-6 max-w-sm mx-4 text-center space-y-4">
|
|
224
|
+
<p className="text-base font-semibold text-slate-900">Cancel sprint planning?</p>
|
|
225
|
+
<p className="text-sm text-slate-500">
|
|
226
|
+
Any epics and stories created in this run will be permanently deleted.
|
|
227
|
+
</p>
|
|
228
|
+
<div className="flex gap-3 justify-center pt-1">
|
|
229
|
+
<button
|
|
230
|
+
onClick={() => setShowCancelConfirm(false)}
|
|
231
|
+
className="px-4 py-2 text-sm rounded-lg border border-slate-200 text-slate-700 hover:bg-slate-50"
|
|
232
|
+
>
|
|
233
|
+
Go Back
|
|
234
|
+
</button>
|
|
235
|
+
<button
|
|
236
|
+
onClick={handleConfirmCancel}
|
|
237
|
+
className="px-4 py-2 text-sm rounded-lg bg-red-600 text-white hover:bg-red-700"
|
|
238
|
+
>
|
|
239
|
+
Cancel Run
|
|
240
|
+
</button>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
|
|
246
|
+
<SelectionContent
|
|
247
|
+
hierarchy={decomposedHierarchy}
|
|
248
|
+
onConfirm={handleConfirm}
|
|
249
|
+
onCancel={() => setShowCancelConfirm(true)}
|
|
250
|
+
/>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { ChevronDown, Check } from 'lucide-react';
|
|
3
|
+
import { saveCeremonies, getLocalModels } from '../../lib/api';
|
|
4
|
+
|
|
5
|
+
// Map ceremony provider name → apiKeys property name
|
|
6
|
+
const PROVIDER_TO_KEY = { claude: 'anthropic', gemini: 'gemini', openai: 'openai', xiaomi: 'xiaomi', local: 'local' };
|
|
7
|
+
// Display labels (extendable as new providers are added)
|
|
8
|
+
const PROVIDER_LABELS = { claude: 'Claude', gemini: 'Gemini', openai: 'OpenAI', xiaomi: 'Xiaomi MiMo', local: 'Local LLM', custom: 'Custom' };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Inspect all stage and validation provider fields to determine whether a
|
|
12
|
+
* ceremony is using a single provider or a mix.
|
|
13
|
+
* Returns the provider name when uniform, or 'custom' when mixed.
|
|
14
|
+
*/
|
|
15
|
+
function detectProvider(ceremony) {
|
|
16
|
+
const providers = new Set();
|
|
17
|
+
for (const stage of Object.values(ceremony?.stages || {})) {
|
|
18
|
+
if (stage?.provider) providers.add(stage.provider);
|
|
19
|
+
}
|
|
20
|
+
// Sponsor-call validation sub-models
|
|
21
|
+
if (ceremony?.validation?.provider) providers.add(ceremony.validation.provider);
|
|
22
|
+
if (ceremony?.validation?.documentation?.provider) providers.add(ceremony.validation.documentation.provider);
|
|
23
|
+
if (ceremony?.validation?.refinement?.provider) providers.add(ceremony.validation.refinement.provider);
|
|
24
|
+
|
|
25
|
+
if (providers.size === 0) return ceremony?.provider || null;
|
|
26
|
+
if (providers.size === 1) return [...providers][0];
|
|
27
|
+
return 'custom';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Apply a provider preset to a ceremony object (immutable).
|
|
32
|
+
* Merges preset provider/model/stages while preserving non-model keys
|
|
33
|
+
* (useContextualSelection, maxIterations, etc.) from the existing config.
|
|
34
|
+
*/
|
|
35
|
+
function applyProviderPreset(ceremony, providerKey) {
|
|
36
|
+
const preset = ceremony.providerPresets?.[providerKey];
|
|
37
|
+
if (!preset) return ceremony;
|
|
38
|
+
|
|
39
|
+
const updated = { ...ceremony, provider: preset.provider, defaultModel: preset.defaultModel };
|
|
40
|
+
|
|
41
|
+
// Merge stages: start from current stages to preserve extra keys, overlay preset values
|
|
42
|
+
const newStages = {};
|
|
43
|
+
const allStageNames = new Set([
|
|
44
|
+
...Object.keys(updated.stages || {}),
|
|
45
|
+
...Object.keys(preset.stages || {}),
|
|
46
|
+
]);
|
|
47
|
+
for (const stageName of allStageNames) {
|
|
48
|
+
const existing = updated.stages?.[stageName] ?? {};
|
|
49
|
+
const presetStage = preset.stages?.[stageName];
|
|
50
|
+
if (presetStage) {
|
|
51
|
+
newStages[stageName] = { ...existing, provider: presetStage.provider, model: presetStage.model };
|
|
52
|
+
} else {
|
|
53
|
+
newStages[stageName] = existing;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
updated.stages = newStages;
|
|
57
|
+
|
|
58
|
+
// Handle validation (sponsor-call specific)
|
|
59
|
+
if (preset.validation && updated.validation) {
|
|
60
|
+
updated.validation = {
|
|
61
|
+
...updated.validation,
|
|
62
|
+
provider: preset.validation.provider,
|
|
63
|
+
model: preset.validation.model,
|
|
64
|
+
};
|
|
65
|
+
if (preset.validation.refinement && updated.validation.refinement) {
|
|
66
|
+
updated.validation.refinement = {
|
|
67
|
+
...updated.validation.refinement,
|
|
68
|
+
provider: preset.validation.refinement.provider,
|
|
69
|
+
model: preset.validation.refinement.model,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (preset.validation.documentation && updated.validation.documentation) {
|
|
73
|
+
updated.validation.documentation = {
|
|
74
|
+
...updated.validation.documentation,
|
|
75
|
+
provider: preset.validation.documentation.provider,
|
|
76
|
+
model: preset.validation.documentation.model,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return updated;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Apply a local model to all stages of a ceremony (immutable).
|
|
86
|
+
* Sets every stage + validation area to { provider: 'local', model: modelId }.
|
|
87
|
+
*/
|
|
88
|
+
function applyLocalModel(ceremony, modelId) {
|
|
89
|
+
const updated = { ...ceremony, provider: 'local', defaultModel: modelId };
|
|
90
|
+
|
|
91
|
+
// Set all stages to local
|
|
92
|
+
const newStages = {};
|
|
93
|
+
for (const [stageName, existing] of Object.entries(updated.stages || {})) {
|
|
94
|
+
newStages[stageName] = { ...existing, provider: 'local', model: modelId };
|
|
95
|
+
}
|
|
96
|
+
updated.stages = newStages;
|
|
97
|
+
|
|
98
|
+
// Set validation (sponsor-call specific)
|
|
99
|
+
if (updated.validation) {
|
|
100
|
+
updated.validation = { ...updated.validation, provider: 'local', model: modelId };
|
|
101
|
+
if (updated.validation.documentation) {
|
|
102
|
+
updated.validation.documentation = { ...updated.validation.documentation, provider: 'local', model: modelId };
|
|
103
|
+
}
|
|
104
|
+
if (updated.validation.refinement) {
|
|
105
|
+
updated.validation.refinement = { ...updated.validation.refinement, provider: 'local', model: modelId };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return updated;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function ProviderSwitcherButton({ ceremonyName, ceremonies, apiKeys, onApplied }) {
|
|
113
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
114
|
+
const [saving, setSaving] = useState(false);
|
|
115
|
+
const [localServers, setLocalServers] = useState(null); // null = not yet probed
|
|
116
|
+
const [localExpanded, setLocalExpanded] = useState(false);
|
|
117
|
+
const dropdownRef = useRef(null);
|
|
118
|
+
|
|
119
|
+
const ceremony = ceremonies?.find((c) => c.name === ceremonyName);
|
|
120
|
+
const currentProvider = detectProvider(ceremony);
|
|
121
|
+
const presets = ceremony?.providerPresets;
|
|
122
|
+
|
|
123
|
+
// Close dropdown on outside click
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (!isOpen) return;
|
|
126
|
+
const handler = (e) => {
|
|
127
|
+
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
|
|
128
|
+
setIsOpen(false);
|
|
129
|
+
setLocalExpanded(false);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
document.addEventListener('mousedown', handler);
|
|
133
|
+
return () => document.removeEventListener('mousedown', handler);
|
|
134
|
+
}, [isOpen]);
|
|
135
|
+
|
|
136
|
+
// Probe local servers when dropdown opens
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (!isOpen || localServers !== null) return;
|
|
139
|
+
getLocalModels()
|
|
140
|
+
.then((data) => setLocalServers(data.servers || []))
|
|
141
|
+
.catch(() => setLocalServers([]));
|
|
142
|
+
}, [isOpen, localServers]);
|
|
143
|
+
|
|
144
|
+
if (!presets || Object.keys(presets).length === 0) return null;
|
|
145
|
+
|
|
146
|
+
const providerKeys = Object.keys(presets);
|
|
147
|
+
const currentLabel = PROVIDER_LABELS[currentProvider] || currentProvider || '—';
|
|
148
|
+
|
|
149
|
+
const handleSelect = async (providerKey) => {
|
|
150
|
+
if (!ceremony || providerKey === currentProvider) { setIsOpen(false); return; }
|
|
151
|
+
setIsOpen(false);
|
|
152
|
+
setLocalExpanded(false);
|
|
153
|
+
setSaving(true);
|
|
154
|
+
try {
|
|
155
|
+
const updated = applyProviderPreset(ceremony, providerKey);
|
|
156
|
+
const updatedCeremonies = ceremonies.map((c) => c.name === ceremonyName ? updated : c);
|
|
157
|
+
await saveCeremonies(updatedCeremonies, null);
|
|
158
|
+
onApplied(updatedCeremonies);
|
|
159
|
+
} finally {
|
|
160
|
+
setSaving(false);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const handleSelectLocalModel = async (modelId) => {
|
|
165
|
+
if (!ceremony) return;
|
|
166
|
+
setIsOpen(false);
|
|
167
|
+
setLocalExpanded(false);
|
|
168
|
+
setSaving(true);
|
|
169
|
+
try {
|
|
170
|
+
const updated = applyLocalModel(ceremony, modelId);
|
|
171
|
+
const updatedCeremonies = ceremonies.map((c) => c.name === ceremonyName ? updated : c);
|
|
172
|
+
await saveCeremonies(updatedCeremonies, null);
|
|
173
|
+
onApplied(updatedCeremonies);
|
|
174
|
+
} finally {
|
|
175
|
+
setSaving(false);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const allLocalModels = (localServers || []).flatMap((srv) =>
|
|
180
|
+
srv.models.map((m) => ({ id: m.id, server: srv.label }))
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<div className="relative" ref={dropdownRef}>
|
|
185
|
+
<button
|
|
186
|
+
type="button"
|
|
187
|
+
disabled={saving}
|
|
188
|
+
onClick={() => { setIsOpen((o) => !o); setLocalExpanded(false); }}
|
|
189
|
+
className="flex items-center gap-1 text-xs px-2 py-1 rounded-md bg-slate-100 hover:bg-slate-200 text-slate-600 hover:text-slate-800 transition-colors disabled:opacity-60"
|
|
190
|
+
title="Switch AI provider preset"
|
|
191
|
+
>
|
|
192
|
+
{saving ? (
|
|
193
|
+
<span className="w-3 h-3 border border-slate-400 border-t-slate-700 rounded-full animate-spin" />
|
|
194
|
+
) : (
|
|
195
|
+
<span className="font-medium">⚡ {currentLabel}</span>
|
|
196
|
+
)}
|
|
197
|
+
{!saving && <ChevronDown className="w-3 h-3" />}
|
|
198
|
+
</button>
|
|
199
|
+
{isOpen && (
|
|
200
|
+
<div className="absolute right-0 mt-1 w-56 bg-white border border-slate-200 rounded-lg shadow-lg z-50 py-1">
|
|
201
|
+
{/* Cloud provider presets (local handled separately below) */}
|
|
202
|
+
{providerKeys.filter((k) => k !== 'local').map((key) => {
|
|
203
|
+
const label = PROVIDER_LABELS[key] || key;
|
|
204
|
+
const apiKeyProp = PROVIDER_TO_KEY[key];
|
|
205
|
+
const hasKey = apiKeys?.[apiKeyProp]?.isSet ?? false;
|
|
206
|
+
const isCurrent = key === currentProvider;
|
|
207
|
+
return (
|
|
208
|
+
<button
|
|
209
|
+
key={key}
|
|
210
|
+
type="button"
|
|
211
|
+
onClick={() => handleSelect(key)}
|
|
212
|
+
className="w-full flex items-center justify-between px-3 py-2 text-sm text-left hover:bg-slate-50 transition-colors"
|
|
213
|
+
>
|
|
214
|
+
<span className={isCurrent ? 'font-medium text-slate-900' : 'text-slate-700'}>
|
|
215
|
+
{label}
|
|
216
|
+
</span>
|
|
217
|
+
<div className="flex items-center gap-1.5">
|
|
218
|
+
{isCurrent && <Check className="w-3.5 h-3.5 text-blue-500" />}
|
|
219
|
+
<span
|
|
220
|
+
className={`text-xs px-1.5 py-0.5 rounded-full ${
|
|
221
|
+
hasKey ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-400'
|
|
222
|
+
}`}
|
|
223
|
+
>
|
|
224
|
+
{hasKey ? 'key set' : 'no key'}
|
|
225
|
+
</span>
|
|
226
|
+
</div>
|
|
227
|
+
</button>
|
|
228
|
+
);
|
|
229
|
+
})}
|
|
230
|
+
|
|
231
|
+
{/* Divider */}
|
|
232
|
+
<div className="border-t border-slate-100 my-1" />
|
|
233
|
+
|
|
234
|
+
{/* Local LLM option */}
|
|
235
|
+
<button
|
|
236
|
+
type="button"
|
|
237
|
+
onClick={() => {
|
|
238
|
+
if (allLocalModels.length === 1) {
|
|
239
|
+
handleSelectLocalModel(allLocalModels[0].id);
|
|
240
|
+
} else {
|
|
241
|
+
setLocalExpanded((v) => !v);
|
|
242
|
+
}
|
|
243
|
+
}}
|
|
244
|
+
className="w-full flex items-center justify-between px-3 py-2 text-sm text-left hover:bg-slate-50 transition-colors"
|
|
245
|
+
>
|
|
246
|
+
<span className={currentProvider === 'local' ? 'font-medium text-slate-900' : 'text-slate-700'}>
|
|
247
|
+
🖥 Local LLM
|
|
248
|
+
</span>
|
|
249
|
+
<div className="flex items-center gap-1.5">
|
|
250
|
+
{currentProvider === 'local' && <Check className="w-3.5 h-3.5 text-blue-500" />}
|
|
251
|
+
{localServers === null ? (
|
|
252
|
+
<span className="w-3 h-3 border border-slate-300 border-t-slate-500 rounded-full animate-spin" />
|
|
253
|
+
) : allLocalModels.length > 0 ? (
|
|
254
|
+
<span className="text-xs px-1.5 py-0.5 rounded-full bg-green-100 text-green-700">
|
|
255
|
+
{allLocalModels.length} model{allLocalModels.length !== 1 ? 's' : ''}
|
|
256
|
+
</span>
|
|
257
|
+
) : (
|
|
258
|
+
<span className="text-xs px-1.5 py-0.5 rounded-full bg-slate-100 text-slate-400">
|
|
259
|
+
offline
|
|
260
|
+
</span>
|
|
261
|
+
)}
|
|
262
|
+
</div>
|
|
263
|
+
</button>
|
|
264
|
+
|
|
265
|
+
{/* Expanded local model list */}
|
|
266
|
+
{localExpanded && allLocalModels.length > 0 && (
|
|
267
|
+
<div className="border-t border-slate-100 bg-slate-50 py-1 max-h-48 overflow-y-auto">
|
|
268
|
+
{allLocalModels.map((lm) => (
|
|
269
|
+
<button
|
|
270
|
+
key={`${lm.server}-${lm.id}`}
|
|
271
|
+
type="button"
|
|
272
|
+
onClick={() => handleSelectLocalModel(lm.id)}
|
|
273
|
+
className="w-full px-4 py-1.5 text-xs text-left hover:bg-slate-100 transition-colors flex items-center justify-between gap-2"
|
|
274
|
+
>
|
|
275
|
+
<span className="text-slate-700 truncate">{lm.id}</span>
|
|
276
|
+
<span className="text-[10px] text-slate-400 flex-shrink-0">{lm.server}</span>
|
|
277
|
+
</button>
|
|
278
|
+
))}
|
|
279
|
+
</div>
|
|
280
|
+
)}
|
|
281
|
+
{localExpanded && allLocalModels.length === 0 && (
|
|
282
|
+
<div className="px-4 py-2 text-xs text-slate-400">
|
|
283
|
+
No local server detected. Start LM Studio, Ollama, or similar.
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
</div>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
}
|