@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,838 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { X, Settings, ArrowDownToLine } from 'lucide-react';
|
|
3
|
+
import { useSprintPlanningStore } from '../../store/sprintPlanningStore';
|
|
4
|
+
import { runSprintPlanning, getSprintPlanningResumable, getSettings, getModels, saveCeremonies, pauseCeremony, resumeCeremony, cancelCeremony, resetCeremony } from '../../lib/api';
|
|
5
|
+
import { CeremonyWorkflowModal } from './CeremonyWorkflowModal';
|
|
6
|
+
|
|
7
|
+
// ── Step progress header ─────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
const STEPS = [
|
|
10
|
+
{ id: 1, label: 'Ready' },
|
|
11
|
+
{ id: 2, label: 'Running' },
|
|
12
|
+
{ id: 3, label: 'Select Epics/Stories' },
|
|
13
|
+
{ id: 4, label: 'Complete' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
function StepProgress({ currentStep }) {
|
|
17
|
+
return (
|
|
18
|
+
<div className="flex items-center gap-1 flex-nowrap">
|
|
19
|
+
{STEPS.map((s, idx) => {
|
|
20
|
+
const isDone = currentStep > s.id;
|
|
21
|
+
const isCurrent = currentStep === s.id;
|
|
22
|
+
return (
|
|
23
|
+
<div key={s.id} className="flex items-center gap-1">
|
|
24
|
+
<div
|
|
25
|
+
className={`flex items-center gap-1 text-xs px-2 py-0.5 rounded-full ${
|
|
26
|
+
isCurrent
|
|
27
|
+
? 'bg-blue-600 text-white font-medium'
|
|
28
|
+
: isDone
|
|
29
|
+
? 'bg-green-100 text-green-700'
|
|
30
|
+
: 'bg-slate-100 text-slate-400'
|
|
31
|
+
}`}
|
|
32
|
+
>
|
|
33
|
+
{isDone ? '✓' : s.id} {s.label}
|
|
34
|
+
</div>
|
|
35
|
+
{idx < STEPS.length - 1 && (
|
|
36
|
+
<span className="text-slate-300 text-xs">›</span>
|
|
37
|
+
)}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
})}
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Stat card ────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
function Stat({ label, value }) {
|
|
48
|
+
return (
|
|
49
|
+
<div className="bg-slate-50 rounded-lg border border-slate-200 p-3 text-center">
|
|
50
|
+
<div className="text-lg font-bold text-slate-900">{value}</div>
|
|
51
|
+
<div className="text-xs text-slate-500">{label}</div>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── Step 1: Ready ─────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
function ReadyStep({ onStart, onResume, resumeInfo }) {
|
|
59
|
+
return (
|
|
60
|
+
<div className="space-y-6">
|
|
61
|
+
<div>
|
|
62
|
+
<h2 className="text-xl font-semibold text-slate-900">Ready to Plan</h2>
|
|
63
|
+
<p className="text-sm text-slate-500 mt-1">
|
|
64
|
+
AI will decompose your project documentation into Epics and Stories using multi-agent analysis.
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{resumeInfo && (
|
|
69
|
+
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4 space-y-2">
|
|
70
|
+
<p className="text-sm font-medium text-amber-800">Previous run can be resumed</p>
|
|
71
|
+
<p className="text-xs text-amber-600">
|
|
72
|
+
Stopped at: <span className="font-medium">{resumeInfo.checkpointLabel}</span>
|
|
73
|
+
{resumeInfo.epics > 0 && <> · {resumeInfo.epics} epics on disk</>}
|
|
74
|
+
</p>
|
|
75
|
+
{resumeInfo.timestamp && (
|
|
76
|
+
<p className="text-xs text-amber-500">
|
|
77
|
+
{new Date(resumeInfo.timestamp).toLocaleString()}
|
|
78
|
+
</p>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
<div className="flex items-center justify-end gap-3 pt-2">
|
|
84
|
+
{resumeInfo && (
|
|
85
|
+
<button
|
|
86
|
+
onClick={onResume}
|
|
87
|
+
className="px-5 py-2 text-sm font-medium bg-amber-600 text-white rounded-lg hover:bg-amber-500 transition-colors"
|
|
88
|
+
>
|
|
89
|
+
Resume
|
|
90
|
+
</button>
|
|
91
|
+
)}
|
|
92
|
+
<button
|
|
93
|
+
onClick={onStart}
|
|
94
|
+
className={`px-5 py-2 text-sm font-medium rounded-lg transition-colors ${
|
|
95
|
+
resumeInfo
|
|
96
|
+
? 'border border-slate-200 text-slate-700 hover:bg-slate-50'
|
|
97
|
+
: 'bg-slate-900 text-white hover:bg-slate-700'
|
|
98
|
+
}`}
|
|
99
|
+
>
|
|
100
|
+
{resumeInfo ? 'Start Fresh' : 'Start'}
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Step 2: Running ───────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
function parseStageNumber(message) {
|
|
110
|
+
const m = message?.match(/Stage\s+(\d+(?:\.\d+)?)\/(\d+)/i);
|
|
111
|
+
if (m) return { current: parseFloat(m[1]), total: parseInt(m[2]) };
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseStageTotals(message) {
|
|
116
|
+
const m = message?.match(/\((\d+)\s+epics?,\s*(\d+)\s+stories?\)/i);
|
|
117
|
+
if (m) return { total: parseInt(m[1]) + parseInt(m[2]) };
|
|
118
|
+
const m2 = message?.match(/\((\d+)\s+stories?\)/i);
|
|
119
|
+
if (m2) return { total: parseInt(m2[1]) };
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Group flat progressLog into a 3-level hierarchy:
|
|
124
|
+
// Level 1 — stage headers (type:'progress')
|
|
125
|
+
// Level 2 — substeps (type:'substep') → { text, details[] }
|
|
126
|
+
// Level 3 — details (type:'detail') → appended to last substep's details[]
|
|
127
|
+
// If no substep exists yet, details go into group.orphanDetails[]
|
|
128
|
+
function buildStageGroups(progressLog) {
|
|
129
|
+
const groups = [];
|
|
130
|
+
for (const entry of progressLog) {
|
|
131
|
+
if (entry.type === 'progress') {
|
|
132
|
+
groups.push({ message: entry.message, substeps: [], orphanDetails: [] });
|
|
133
|
+
} else if (entry.type === 'substep' && groups.length > 0) {
|
|
134
|
+
groups[groups.length - 1].substeps.push({ text: entry.substep, details: [] });
|
|
135
|
+
} else if (entry.type === 'detail' && groups.length > 0) {
|
|
136
|
+
const group = groups[groups.length - 1];
|
|
137
|
+
const substeps = group.substeps;
|
|
138
|
+
if (substeps.length > 0) {
|
|
139
|
+
substeps[substeps.length - 1].details.push(entry.detail);
|
|
140
|
+
} else {
|
|
141
|
+
group.orphanDetails.push(entry.detail);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return groups;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function RunningStep({ transitioning, onPause, onResume, onCancel, onBackground }) {
|
|
149
|
+
const { progressLog, status, error, isPaused, setStatus, setStep, setError } = useSprintPlanningStore();
|
|
150
|
+
const logBottomRef = useRef(null);
|
|
151
|
+
|
|
152
|
+
const handleForceReset = async () => {
|
|
153
|
+
try { await resetCeremony(); } catch (_) {}
|
|
154
|
+
setStatus('idle');
|
|
155
|
+
setStep(1);
|
|
156
|
+
setError(null);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
logBottomRef.current?.scrollIntoView({ behavior: 'smooth' });
|
|
161
|
+
}, [progressLog]);
|
|
162
|
+
|
|
163
|
+
const stageGroups = buildStageGroups(progressLog);
|
|
164
|
+
|
|
165
|
+
const STAGE_WEIGHTS = {
|
|
166
|
+
'1/6': [0, 3],
|
|
167
|
+
'2/6': [3, 7],
|
|
168
|
+
'3/6': [7, 12],
|
|
169
|
+
'4/6': [12, 22],
|
|
170
|
+
'4.5/6': [22, 24],
|
|
171
|
+
'5/6': [24, 76],
|
|
172
|
+
'6/7': [76, 90],
|
|
173
|
+
'7/7': [90, 100],
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const currentGroup = [...stageGroups].reverse().find(g => parseStageNumber(g.message));
|
|
177
|
+
|
|
178
|
+
let progressPct = 5;
|
|
179
|
+
if (currentGroup) {
|
|
180
|
+
const parsed = parseStageNumber(currentGroup.message);
|
|
181
|
+
const key = `${parsed.current}/${parsed.total}`;
|
|
182
|
+
const [stageStart, stageEnd] = STAGE_WEIGHTS[key] ?? [0, 90];
|
|
183
|
+
|
|
184
|
+
const totals = parseStageTotals(currentGroup.message);
|
|
185
|
+
if (totals && totals.total > 0) {
|
|
186
|
+
let countedItems = 0;
|
|
187
|
+
if (key === '5/6') {
|
|
188
|
+
countedItems = currentGroup.substeps.filter(s => s.text?.includes('Validating ')).length;
|
|
189
|
+
} else if (key === '6/7') {
|
|
190
|
+
countedItems = currentGroup.substeps.filter(s => s.text?.includes('Distributing documentation')).length;
|
|
191
|
+
} else if (key === '7/7') {
|
|
192
|
+
countedItems = currentGroup.substeps.filter(s => s.text?.includes('Enriching documentation')).length;
|
|
193
|
+
}
|
|
194
|
+
const fraction = Math.min(countedItems, totals.total) / totals.total;
|
|
195
|
+
progressPct = Math.round(stageStart + fraction * (stageEnd - stageStart));
|
|
196
|
+
} else {
|
|
197
|
+
progressPct = stageStart;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const latestProgress = stageGroups[stageGroups.length - 1]?.message || 'Starting…';
|
|
201
|
+
|
|
202
|
+
if (status === 'error') {
|
|
203
|
+
const isAlreadyRunning = error?.includes('already running');
|
|
204
|
+
return (
|
|
205
|
+
<div className="space-y-4">
|
|
206
|
+
<h2 className="text-xl font-semibold text-slate-900">Sprint Planning Failed</h2>
|
|
207
|
+
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
|
208
|
+
<p className="text-sm font-medium text-red-700 mb-1">Error</p>
|
|
209
|
+
<p className="text-sm text-red-600">{error || 'An unknown error occurred.'}</p>
|
|
210
|
+
</div>
|
|
211
|
+
{isAlreadyRunning ? (
|
|
212
|
+
<div className="space-y-3">
|
|
213
|
+
<p className="text-sm text-slate-500">
|
|
214
|
+
A ceremony is already running on the server. You can force-stop it and reset the state — this will discard any in-progress work.
|
|
215
|
+
</p>
|
|
216
|
+
<button
|
|
217
|
+
onClick={handleForceReset}
|
|
218
|
+
className="px-4 py-2 text-sm rounded-lg bg-red-600 text-white hover:bg-red-700 transition-colors"
|
|
219
|
+
>
|
|
220
|
+
Force Stop & Reset
|
|
221
|
+
</button>
|
|
222
|
+
</div>
|
|
223
|
+
) : error === 'Not found' ? (
|
|
224
|
+
<div className="space-y-3">
|
|
225
|
+
<p className="text-sm text-slate-500">
|
|
226
|
+
The server may be running an older version. Run{' '}
|
|
227
|
+
<code className="bg-slate-100 px-1 rounded">/kanban</code> in the AVC terminal to restart it, then try again.
|
|
228
|
+
</p>
|
|
229
|
+
<button
|
|
230
|
+
onClick={handleForceReset}
|
|
231
|
+
className="px-4 py-2 text-sm rounded-lg bg-slate-600 text-white hover:bg-slate-700 transition-colors"
|
|
232
|
+
>
|
|
233
|
+
Cancel & Retry
|
|
234
|
+
</button>
|
|
235
|
+
</div>
|
|
236
|
+
) : (
|
|
237
|
+
<div className="space-y-3">
|
|
238
|
+
<p className="text-sm text-slate-500">
|
|
239
|
+
You can dismiss this error and start a new sprint planning session.
|
|
240
|
+
</p>
|
|
241
|
+
<button
|
|
242
|
+
onClick={handleForceReset}
|
|
243
|
+
className="px-4 py-2 text-sm rounded-lg bg-slate-600 text-white hover:bg-slate-700 transition-colors"
|
|
244
|
+
>
|
|
245
|
+
Cancel & Retry
|
|
246
|
+
</button>
|
|
247
|
+
</div>
|
|
248
|
+
)}
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<div className="space-y-6">
|
|
255
|
+
<div>
|
|
256
|
+
<h2 className="text-xl font-semibold text-slate-900">Running Sprint Planning</h2>
|
|
257
|
+
<p className="text-sm text-slate-500 mt-1">
|
|
258
|
+
The AI is decomposing your project scope and validating each work item. Duration varies with project size and validation iterations — from a few minutes to 30+ minutes for larger projects.
|
|
259
|
+
</p>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<div>
|
|
263
|
+
<div className="flex items-center justify-between mb-1">
|
|
264
|
+
<span className="text-xs font-medium text-slate-600">{latestProgress}</span>
|
|
265
|
+
<span className="text-xs text-slate-400">{progressPct}%</span>
|
|
266
|
+
</div>
|
|
267
|
+
<div className="w-full bg-slate-200 rounded-full h-2">
|
|
268
|
+
<div
|
|
269
|
+
className="bg-blue-500 h-2 rounded-full transition-all duration-500"
|
|
270
|
+
style={{ width: `${progressPct}%` }}
|
|
271
|
+
/>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
<div className="bg-slate-900 rounded-lg p-4 h-64 overflow-y-auto font-mono text-xs">
|
|
276
|
+
{stageGroups.length === 0 ? (
|
|
277
|
+
<p className="text-slate-400 animate-pulse">Initializing…</p>
|
|
278
|
+
) : (
|
|
279
|
+
<div className="space-y-2">
|
|
280
|
+
{stageGroups.map((group, gi) => {
|
|
281
|
+
const isActive = gi === stageGroups.length - 1 && status === 'running';
|
|
282
|
+
return (
|
|
283
|
+
<div key={gi}>
|
|
284
|
+
{/* Stage header */}
|
|
285
|
+
<div className="flex items-center gap-1.5">
|
|
286
|
+
{isActive ? (
|
|
287
|
+
<span className="inline-block w-3 h-3 border border-blue-400 border-t-blue-200 rounded-full animate-spin flex-shrink-0" />
|
|
288
|
+
) : (
|
|
289
|
+
<span className="text-green-500 flex-shrink-0">✓</span>
|
|
290
|
+
)}
|
|
291
|
+
<span className={isActive ? 'text-blue-300 font-medium' : 'text-slate-400'}>
|
|
292
|
+
{group.message}
|
|
293
|
+
</span>
|
|
294
|
+
</div>
|
|
295
|
+
{/* Orphan details — arrived before any substep in this group */}
|
|
296
|
+
{group.orphanDetails?.length > 0 && (
|
|
297
|
+
<div className="ml-5 mt-0.5 space-y-0.5 border-l border-slate-700 pl-2">
|
|
298
|
+
{group.orphanDetails.map((d, di) => (
|
|
299
|
+
<p key={di} className="text-slate-500">{d}</p>
|
|
300
|
+
))}
|
|
301
|
+
</div>
|
|
302
|
+
)}
|
|
303
|
+
{/* Substeps (Level 2) + Details (Level 3) */}
|
|
304
|
+
{group.substeps.length > 0 && (
|
|
305
|
+
<div className="ml-5 mt-0.5 space-y-0.5 border-l border-slate-700 pl-2">
|
|
306
|
+
{group.substeps.map((sub, si) => (
|
|
307
|
+
<div key={si}>
|
|
308
|
+
<p className="text-slate-400">{sub.text}</p>
|
|
309
|
+
{sub.details.length > 0 && (
|
|
310
|
+
<div className="ml-3 mt-0.5 space-y-0.5 border-l border-slate-700 pl-2">
|
|
311
|
+
{sub.details.map((d, di) => (
|
|
312
|
+
<p key={di} className="text-slate-500">{d}</p>
|
|
313
|
+
))}
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
316
|
+
</div>
|
|
317
|
+
))}
|
|
318
|
+
</div>
|
|
319
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
);
|
|
322
|
+
})}
|
|
323
|
+
{transitioning === 'cancelling' && (
|
|
324
|
+
<div className="flex items-center gap-1.5 mt-1">
|
|
325
|
+
<span className="inline-block w-3 h-3 border border-red-400 border-t-red-200 rounded-full animate-spin flex-shrink-0" />
|
|
326
|
+
<span className="text-red-400 font-medium">Cancelling…</span>
|
|
327
|
+
</div>
|
|
328
|
+
)}
|
|
329
|
+
<div ref={logBottomRef} />
|
|
330
|
+
</div>
|
|
331
|
+
)}
|
|
332
|
+
</div>
|
|
333
|
+
|
|
334
|
+
<div className="flex items-center justify-between gap-2 pt-2">
|
|
335
|
+
{/* Left: run in background */}
|
|
336
|
+
{onBackground && !transitioning ? (
|
|
337
|
+
<button
|
|
338
|
+
type="button"
|
|
339
|
+
onClick={onBackground}
|
|
340
|
+
className="flex items-center gap-1.5 text-xs font-medium text-slate-500 hover:text-slate-700 bg-slate-50 hover:bg-slate-100 border border-slate-200 rounded-md px-2.5 py-1.5 transition-colors"
|
|
341
|
+
title="Hide this window — ceremony keeps running in the background"
|
|
342
|
+
>
|
|
343
|
+
<ArrowDownToLine className="w-3.5 h-3.5" />
|
|
344
|
+
Run in Background
|
|
345
|
+
</button>
|
|
346
|
+
) : <span />}
|
|
347
|
+
|
|
348
|
+
{/* Right: pause / resume / cancel */}
|
|
349
|
+
<div className="flex items-center gap-2">
|
|
350
|
+
{transitioning === 'pausing' ? (
|
|
351
|
+
<span className="flex items-center gap-1.5 text-xs text-slate-400"><span className="inline-block w-3 h-3 border border-slate-400 border-t-slate-200 rounded-full animate-spin" />Pausing…</span>
|
|
352
|
+
) : transitioning === 'cancelling' ? (
|
|
353
|
+
<span className="flex items-center gap-1.5 text-xs text-red-400"><span className="inline-block w-3 h-3 border border-red-400 border-t-red-200 rounded-full animate-spin" />Cancelling…</span>
|
|
354
|
+
) : !isPaused ? (
|
|
355
|
+
<button
|
|
356
|
+
onClick={onPause}
|
|
357
|
+
className="px-4 py-2 text-sm rounded-lg border border-slate-200 text-slate-600 hover:bg-slate-50 transition-colors"
|
|
358
|
+
>
|
|
359
|
+
⏸ Pause
|
|
360
|
+
</button>
|
|
361
|
+
) : (
|
|
362
|
+
<button
|
|
363
|
+
onClick={onResume}
|
|
364
|
+
className="px-4 py-2 text-sm rounded-lg border border-amber-300 bg-amber-50 text-amber-700 hover:bg-amber-100 transition-colors"
|
|
365
|
+
>
|
|
366
|
+
▶ Resume
|
|
367
|
+
</button>
|
|
368
|
+
)}
|
|
369
|
+
{!transitioning && (
|
|
370
|
+
<button
|
|
371
|
+
onClick={onCancel}
|
|
372
|
+
className="px-4 py-2 text-sm rounded-lg border border-red-200 text-red-600 hover:bg-red-50 transition-colors"
|
|
373
|
+
>
|
|
374
|
+
✕ Cancel
|
|
375
|
+
</button>
|
|
376
|
+
)}
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ── Step 4: Complete ──────────────────────────────────────────────────────────
|
|
384
|
+
|
|
385
|
+
const EXAMPLE_ISSUES = [
|
|
386
|
+
{ stage: 'Project Documentation', ruleId: 'fix-header-formatting', name: 'Fix Header Spacing', severity: 'major' },
|
|
387
|
+
{ stage: 'Project Documentation', ruleId: 'add-section-spacing', name: 'Add Section Spacing', severity: 'minor' },
|
|
388
|
+
{ stage: 'Project Context', ruleId: 'token-count-too-short', name: 'Expand If Too Short', severity: 'major' },
|
|
389
|
+
{ stage: 'Project Context', ruleId: 'no-redundant-info', name: 'Remove Truly Redundant Information', severity: 'minor' },
|
|
390
|
+
{ stage: 'Context Validation', ruleId: 'fix-unclosed-code-blocks', name: 'Fix Unclosed Code Blocks', severity: 'major' },
|
|
391
|
+
];
|
|
392
|
+
|
|
393
|
+
function IssueTag({ severity }) {
|
|
394
|
+
const cls =
|
|
395
|
+
severity === 'critical' ? 'bg-red-100 text-red-700' :
|
|
396
|
+
severity === 'major' ? 'bg-amber-100 text-amber-700' :
|
|
397
|
+
'bg-slate-100 text-slate-500';
|
|
398
|
+
return (
|
|
399
|
+
<span className={`flex-shrink-0 px-1.5 py-0.5 rounded text-[10px] font-semibold uppercase ${cls}`}>
|
|
400
|
+
{severity}
|
|
401
|
+
</span>
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function CompleteStep({ onClose }) {
|
|
406
|
+
const { result } = useSprintPlanningStore();
|
|
407
|
+
const r = result || {};
|
|
408
|
+
|
|
409
|
+
const tokenInput = r.tokenUsage?.input || 0;
|
|
410
|
+
const tokenOutput = r.tokenUsage?.output || 0;
|
|
411
|
+
|
|
412
|
+
return (
|
|
413
|
+
<div className="space-y-6">
|
|
414
|
+
<div className="text-center">
|
|
415
|
+
<div className="text-5xl mb-3">✅</div>
|
|
416
|
+
<h2 className="text-xl font-semibold text-slate-900">Sprint Planning Complete</h2>
|
|
417
|
+
<p className="text-sm text-slate-500 mt-1">
|
|
418
|
+
Your project has been decomposed into Epics and Stories. The kanban board will refresh.
|
|
419
|
+
</p>
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
<div className="grid grid-cols-2 gap-3 text-center">
|
|
423
|
+
<div className="bg-slate-50 rounded-lg border border-slate-200 p-3">
|
|
424
|
+
<div className="text-2xl font-bold text-slate-900">{r.epicsCreated ?? 0}</div>
|
|
425
|
+
<div className="text-xs text-slate-500">Epics created</div>
|
|
426
|
+
</div>
|
|
427
|
+
<div className="bg-slate-50 rounded-lg border border-slate-200 p-3">
|
|
428
|
+
<div className="text-2xl font-bold text-slate-900">{r.storiesCreated ?? 0}</div>
|
|
429
|
+
<div className="text-xs text-slate-500">Stories created</div>
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
{(r.totalEpics != null || r.totalStories != null) && (
|
|
434
|
+
<p className="text-xs text-center text-slate-400">
|
|
435
|
+
Total in project: {r.totalEpics ?? 0} Epics · {r.totalStories ?? 0} Stories
|
|
436
|
+
</p>
|
|
437
|
+
)}
|
|
438
|
+
|
|
439
|
+
<div className="grid grid-cols-2 gap-3">
|
|
440
|
+
<Stat label="Input tokens" value={tokenInput.toLocaleString()} />
|
|
441
|
+
<Stat label="Output tokens" value={tokenOutput.toLocaleString()} />
|
|
442
|
+
</div>
|
|
443
|
+
|
|
444
|
+
{r.model && (
|
|
445
|
+
<p className="text-xs text-center text-slate-400">
|
|
446
|
+
Model: <span className="font-mono">{r.model}</span>
|
|
447
|
+
</p>
|
|
448
|
+
)}
|
|
449
|
+
|
|
450
|
+
{(() => {
|
|
451
|
+
const isExample = r.validationIssues === undefined;
|
|
452
|
+
const issues = r.validationIssues ?? EXAMPLE_ISSUES;
|
|
453
|
+
return issues.length > 0 ? (
|
|
454
|
+
<div>
|
|
455
|
+
<p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">
|
|
456
|
+
Quality fixes applied
|
|
457
|
+
{isExample && <span className="ml-2 normal-case font-normal text-slate-300">(example preview)</span>}
|
|
458
|
+
</p>
|
|
459
|
+
<div className="space-y-1.5">
|
|
460
|
+
{issues.map((issue, i) => (
|
|
461
|
+
<div key={i} className="flex items-center gap-2 text-xs bg-amber-50 border border-amber-100 rounded-md px-3 py-2">
|
|
462
|
+
<IssueTag severity={issue.severity} />
|
|
463
|
+
<span className="text-slate-400 flex-shrink-0">{issue.stage}</span>
|
|
464
|
+
<span className="text-slate-600">{issue.name}</span>
|
|
465
|
+
</div>
|
|
466
|
+
))}
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
) : null;
|
|
470
|
+
})()}
|
|
471
|
+
|
|
472
|
+
<div className="flex justify-center pt-2">
|
|
473
|
+
<button
|
|
474
|
+
onClick={onClose}
|
|
475
|
+
className="px-6 py-2 bg-slate-900 text-white text-sm font-medium rounded-lg hover:bg-slate-700 transition-colors"
|
|
476
|
+
>
|
|
477
|
+
Close
|
|
478
|
+
</button>
|
|
479
|
+
</div>
|
|
480
|
+
</div>
|
|
481
|
+
);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// ── Quota-limit pause overlay ─────────────────────────────────────────────────
|
|
485
|
+
|
|
486
|
+
function QuotaLimitOverlay({ quotaLimitPending, onContinueAfterQuota, onCancel, onConfigureModels }) {
|
|
487
|
+
const [resuming, setResuming] = useState(false);
|
|
488
|
+
|
|
489
|
+
// Resume reads the current settings so any model change made via Configure Models is picked up.
|
|
490
|
+
async function handleResume() {
|
|
491
|
+
setResuming(true);
|
|
492
|
+
try {
|
|
493
|
+
const s = await getSettings();
|
|
494
|
+
const ceremony = s.ceremonies?.find(c => c.name === 'sprint-planning');
|
|
495
|
+
const newProvider = ceremony?.stages?.validation?.provider || null;
|
|
496
|
+
const newModel = ceremony?.stages?.validation?.model || null;
|
|
497
|
+
await onContinueAfterQuota(newProvider, newModel);
|
|
498
|
+
} finally {
|
|
499
|
+
setResuming(false);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return (
|
|
504
|
+
<div className="absolute inset-0 z-20 flex items-center justify-center bg-white/90 rounded-2xl">
|
|
505
|
+
<div className="bg-white border border-red-200 rounded-xl shadow-lg p-6 max-w-sm mx-4 text-center space-y-4">
|
|
506
|
+
<div className="text-3xl">⚠️</div>
|
|
507
|
+
<p className="text-base font-semibold text-slate-900">API Quota Exceeded</p>
|
|
508
|
+
<p className="text-sm text-slate-600">
|
|
509
|
+
<span className="font-mono font-medium">{quotaLimitPending.provider}</span>
|
|
510
|
+
{' / '}
|
|
511
|
+
<span className="font-mono text-xs">{quotaLimitPending.model}</span>
|
|
512
|
+
{' returned a quota error.'}
|
|
513
|
+
</p>
|
|
514
|
+
{quotaLimitPending.validatorName && (
|
|
515
|
+
<p className="text-xs text-slate-400">
|
|
516
|
+
Validator: {quotaLimitPending.validatorName.replace('validator-story-', '').replace('validator-epic-', '')}
|
|
517
|
+
</p>
|
|
518
|
+
)}
|
|
519
|
+
<p className="text-sm text-slate-500">The ceremony is paused. What would you like to do?</p>
|
|
520
|
+
<div className="flex flex-col gap-2 pt-1">
|
|
521
|
+
<button
|
|
522
|
+
onClick={handleResume}
|
|
523
|
+
disabled={resuming}
|
|
524
|
+
className="px-4 py-2 text-sm rounded-lg bg-slate-900 text-white hover:bg-slate-700 disabled:opacity-50"
|
|
525
|
+
>
|
|
526
|
+
{resuming ? 'Resuming…' : 'Resume'}
|
|
527
|
+
</button>
|
|
528
|
+
<button
|
|
529
|
+
onClick={onConfigureModels}
|
|
530
|
+
className="px-4 py-2 text-sm rounded-lg border border-slate-200 text-slate-700 hover:bg-slate-50"
|
|
531
|
+
>
|
|
532
|
+
Configure Models
|
|
533
|
+
</button>
|
|
534
|
+
<button
|
|
535
|
+
onClick={onCancel}
|
|
536
|
+
className="px-4 py-2 text-sm rounded-lg border border-red-200 text-red-600 hover:bg-red-50"
|
|
537
|
+
>
|
|
538
|
+
Cancel Ceremony
|
|
539
|
+
</button>
|
|
540
|
+
</div>
|
|
541
|
+
<p className="text-xs text-slate-400">
|
|
542
|
+
Use <strong>Configure Models</strong> to switch provider, then <strong>Resume</strong>.
|
|
543
|
+
</p>
|
|
544
|
+
</div>
|
|
545
|
+
</div>
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// ── Main modal ────────────────────────────────────────────────────────────────
|
|
550
|
+
|
|
551
|
+
export function SprintPlanningModal({ onClose, costLimitPending, onContinuePastCostLimit, onCancelFromCostLimit, quotaLimitPending, onContinueAfterQuota, onCancelFromQuota }) {
|
|
552
|
+
const {
|
|
553
|
+
isOpen,
|
|
554
|
+
step,
|
|
555
|
+
status,
|
|
556
|
+
isPaused,
|
|
557
|
+
setStep,
|
|
558
|
+
setStatus,
|
|
559
|
+
setError,
|
|
560
|
+
closeModal,
|
|
561
|
+
setProcessId,
|
|
562
|
+
} = useSprintPlanningStore();
|
|
563
|
+
|
|
564
|
+
const [workflowOpen, setWorkflowOpen] = useState(false);
|
|
565
|
+
const [workflowCeremony, setWorkflowCeremony] = useState(null);
|
|
566
|
+
const [workflowModels, setWorkflowModels] = useState([]);
|
|
567
|
+
const [workflowAllCeremonies, setWorkflowAllCeremonies] = useState([]);
|
|
568
|
+
const [workflowApiKeys, setWorkflowApiKeys] = useState({});
|
|
569
|
+
const [showCancelConfirm, setShowCancelConfirm] = useState(false);
|
|
570
|
+
const [transitioning, setTransitioning] = useState(null); // null | 'pausing' | 'cancelling'
|
|
571
|
+
const [resumeInfo, setResumeInfo] = useState(null);
|
|
572
|
+
|
|
573
|
+
// Fetch resumable state when modal opens at step 1
|
|
574
|
+
useEffect(() => {
|
|
575
|
+
if (isOpen && step === 1 && status !== 'running') {
|
|
576
|
+
getSprintPlanningResumable()
|
|
577
|
+
.then(data => setResumeInfo(data?.resumable ? data : null))
|
|
578
|
+
.catch(() => setResumeInfo(null));
|
|
579
|
+
}
|
|
580
|
+
}, [isOpen, step, status]);
|
|
581
|
+
|
|
582
|
+
if (!isOpen) return null;
|
|
583
|
+
|
|
584
|
+
const isBlocked = status === 'running' || status === 'awaiting-selection';
|
|
585
|
+
|
|
586
|
+
const handleClose = () => {
|
|
587
|
+
if (isBlocked) return;
|
|
588
|
+
closeModal();
|
|
589
|
+
onClose?.();
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const handleStart = async (resumeFromCheckpoint = null) => {
|
|
593
|
+
setStatus('running');
|
|
594
|
+
setStep(2);
|
|
595
|
+
setResumeInfo(null);
|
|
596
|
+
try {
|
|
597
|
+
const result = await runSprintPlanning(resumeFromCheckpoint);
|
|
598
|
+
if (result?.processId) setProcessId(result.processId);
|
|
599
|
+
// Completion is handled via WebSocket in App.jsx
|
|
600
|
+
} catch (err) {
|
|
601
|
+
setStatus('error');
|
|
602
|
+
setError(err.message);
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
const openWorkflow = async () => {
|
|
607
|
+
try {
|
|
608
|
+
const [s, m] = await Promise.all([getSettings(), getModels()]);
|
|
609
|
+
const sc = s.ceremonies?.find((c) => c.name === 'sprint-planning') ?? { name: 'sprint-planning' };
|
|
610
|
+
setWorkflowCeremony(sc);
|
|
611
|
+
setWorkflowModels(m);
|
|
612
|
+
setWorkflowAllCeremonies(s.ceremonies || []);
|
|
613
|
+
setWorkflowApiKeys(s.apiKeys || {});
|
|
614
|
+
setWorkflowOpen(true);
|
|
615
|
+
} catch {}
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const handleWorkflowSave = async (updatedCeremony) => {
|
|
619
|
+
const base = workflowAllCeremonies.length > 0 ? workflowAllCeremonies : [updatedCeremony];
|
|
620
|
+
const next = base.map((c) => c.name === updatedCeremony.name ? updatedCeremony : c);
|
|
621
|
+
await saveCeremonies(next, null);
|
|
622
|
+
setWorkflowCeremony(updatedCeremony);
|
|
623
|
+
setWorkflowAllCeremonies(next);
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
const handlePause = async () => {
|
|
627
|
+
setTransitioning('pausing');
|
|
628
|
+
try { await pauseCeremony(); } catch (_) {}
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
const handleResume = async () => {
|
|
632
|
+
try { await resumeCeremony(); } catch (_) {}
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
const handleConfirmCancel = async (keepItems = false) => {
|
|
636
|
+
setShowCancelConfirm(false);
|
|
637
|
+
setTransitioning('cancelling');
|
|
638
|
+
try { await cancelCeremony(keepItems); } catch (_) {}
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
// Clear transitioning state when WS events arrive (isPaused / status change)
|
|
642
|
+
useEffect(() => {
|
|
643
|
+
if (transitioning === 'pausing' && isPaused) setTransitioning(null);
|
|
644
|
+
}, [isPaused, transitioning]);
|
|
645
|
+
|
|
646
|
+
useEffect(() => {
|
|
647
|
+
if (transitioning === 'cancelling' && status === 'idle') setTransitioning(null);
|
|
648
|
+
}, [status, transitioning]);
|
|
649
|
+
|
|
650
|
+
const renderStep = () => {
|
|
651
|
+
switch (step) {
|
|
652
|
+
case 1: return (
|
|
653
|
+
<ReadyStep
|
|
654
|
+
onStart={() => handleStart(null)}
|
|
655
|
+
onResume={() => handleStart(resumeInfo?.checkpoint)}
|
|
656
|
+
resumeInfo={resumeInfo}
|
|
657
|
+
/>
|
|
658
|
+
);
|
|
659
|
+
case 2: return (
|
|
660
|
+
<RunningStep
|
|
661
|
+
transitioning={transitioning}
|
|
662
|
+
onPause={handlePause}
|
|
663
|
+
onResume={handleResume}
|
|
664
|
+
onCancel={() => setShowCancelConfirm(true)}
|
|
665
|
+
onBackground={closeModal}
|
|
666
|
+
/>
|
|
667
|
+
);
|
|
668
|
+
case 3: return (
|
|
669
|
+
<div className="flex flex-col items-center justify-center py-16 gap-4 text-center">
|
|
670
|
+
<span className="w-2 h-2 rounded-full bg-amber-400" />
|
|
671
|
+
<p className="text-sm text-slate-500">Reviewing decomposed work…</p>
|
|
672
|
+
</div>
|
|
673
|
+
);
|
|
674
|
+
case 4: return <CompleteStep onClose={handleClose} />;
|
|
675
|
+
default: return null;
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
return (
|
|
680
|
+
<div className="fixed inset-0 z-[70] flex items-center justify-center">
|
|
681
|
+
{/* Backdrop */}
|
|
682
|
+
<div
|
|
683
|
+
className="absolute inset-0 bg-black/40"
|
|
684
|
+
onClick={!isBlocked ? handleClose : undefined}
|
|
685
|
+
/>
|
|
686
|
+
|
|
687
|
+
{/* Modal */}
|
|
688
|
+
<div className="relative bg-white rounded-2xl shadow-2xl w-full max-w-2xl mx-4 max-h-[90vh] flex flex-col">
|
|
689
|
+
{/* Cost-limit pause overlay */}
|
|
690
|
+
{costLimitPending && (
|
|
691
|
+
<div className="absolute inset-0 z-20 flex items-center justify-center bg-white/90 rounded-2xl">
|
|
692
|
+
<div className="bg-white border border-amber-200 rounded-xl shadow-lg p-6 max-w-sm mx-4 text-center space-y-4">
|
|
693
|
+
<div className="text-3xl">⚠️</div>
|
|
694
|
+
<p className="text-base font-semibold text-slate-900">Cost Limit Reached</p>
|
|
695
|
+
<p className="text-sm text-slate-600">
|
|
696
|
+
<span className="font-mono font-medium">${costLimitPending.cost.toFixed(4)}</span> spent
|
|
697
|
+
{costLimitPending.threshold != null && (
|
|
698
|
+
<> (limit: <span className="font-mono">${Number(costLimitPending.threshold).toFixed(2)}</span>)</>
|
|
699
|
+
)}
|
|
700
|
+
</p>
|
|
701
|
+
<p className="text-sm text-slate-500">
|
|
702
|
+
The ceremony is paused. What would you like to do?
|
|
703
|
+
</p>
|
|
704
|
+
<div className="flex gap-3 justify-center pt-1">
|
|
705
|
+
<button
|
|
706
|
+
onClick={onContinuePastCostLimit}
|
|
707
|
+
className="px-4 py-2 text-sm rounded-lg bg-slate-900 text-white hover:bg-slate-700"
|
|
708
|
+
>
|
|
709
|
+
Continue Anyway
|
|
710
|
+
</button>
|
|
711
|
+
<button
|
|
712
|
+
onClick={onCancelFromCostLimit}
|
|
713
|
+
className="px-4 py-2 text-sm rounded-lg border border-red-200 text-red-600 hover:bg-red-50"
|
|
714
|
+
>
|
|
715
|
+
Cancel Ceremony
|
|
716
|
+
</button>
|
|
717
|
+
</div>
|
|
718
|
+
<p className="text-xs text-slate-400">
|
|
719
|
+
Continue disables cost checking for the rest of this run.
|
|
720
|
+
</p>
|
|
721
|
+
</div>
|
|
722
|
+
</div>
|
|
723
|
+
)}
|
|
724
|
+
|
|
725
|
+
{/* Quota-limit pause overlay */}
|
|
726
|
+
{quotaLimitPending && (
|
|
727
|
+
<QuotaLimitOverlay
|
|
728
|
+
quotaLimitPending={quotaLimitPending}
|
|
729
|
+
onContinueAfterQuota={onContinueAfterQuota}
|
|
730
|
+
onCancel={onCancelFromQuota}
|
|
731
|
+
onConfigureModels={openWorkflow}
|
|
732
|
+
/>
|
|
733
|
+
)}
|
|
734
|
+
|
|
735
|
+
{/* Cancel confirmation overlay */}
|
|
736
|
+
{showCancelConfirm && (
|
|
737
|
+
<div className="absolute inset-0 z-10 flex items-center justify-center bg-white/90 rounded-2xl">
|
|
738
|
+
<div className="bg-white border border-slate-200 rounded-xl shadow-lg p-6 max-w-sm mx-4 text-center space-y-4">
|
|
739
|
+
<p className="text-base font-semibold text-slate-900">Cancel sprint planning?</p>
|
|
740
|
+
<p className="text-sm text-slate-500">
|
|
741
|
+
What should happen with epics and stories already created in this run?
|
|
742
|
+
</p>
|
|
743
|
+
<div className="flex flex-col gap-2 pt-1">
|
|
744
|
+
<button
|
|
745
|
+
onClick={() => setShowCancelConfirm(false)}
|
|
746
|
+
className="px-4 py-2 text-sm rounded-lg border border-slate-200 text-slate-700 hover:bg-slate-50"
|
|
747
|
+
>
|
|
748
|
+
Keep Running
|
|
749
|
+
</button>
|
|
750
|
+
<button
|
|
751
|
+
onClick={() => handleConfirmCancel(true)}
|
|
752
|
+
className="px-4 py-2 text-sm rounded-lg border border-amber-300 bg-amber-50 text-amber-700 hover:bg-amber-100"
|
|
753
|
+
>
|
|
754
|
+
Cancel & Keep Items
|
|
755
|
+
</button>
|
|
756
|
+
<button
|
|
757
|
+
onClick={() => handleConfirmCancel(false)}
|
|
758
|
+
className="px-4 py-2 text-sm rounded-lg bg-red-600 text-white hover:bg-red-700"
|
|
759
|
+
>
|
|
760
|
+
Cancel & Delete Items
|
|
761
|
+
</button>
|
|
762
|
+
</div>
|
|
763
|
+
</div>
|
|
764
|
+
</div>
|
|
765
|
+
)}
|
|
766
|
+
|
|
767
|
+
{/* Header */}
|
|
768
|
+
<div className="flex items-start justify-between px-6 pt-5 pb-4 border-b border-slate-200 flex-shrink-0">
|
|
769
|
+
<div className="min-w-0 flex-1">
|
|
770
|
+
<h1 className="text-base font-semibold text-slate-900">Sprint Planning Ceremony</h1>
|
|
771
|
+
<div className="mt-2">
|
|
772
|
+
<StepProgress currentStep={step} />
|
|
773
|
+
</div>
|
|
774
|
+
</div>
|
|
775
|
+
<div className="flex items-center gap-3 ml-4 mt-0.5 flex-shrink-0">
|
|
776
|
+
{!isBlocked && (
|
|
777
|
+
<button
|
|
778
|
+
type="button"
|
|
779
|
+
onClick={openWorkflow}
|
|
780
|
+
className="flex items-center gap-1.5 text-xs font-medium text-slate-500 hover:text-blue-600 bg-slate-50 hover:bg-blue-50 border border-slate-200 hover:border-blue-200 rounded-md px-2.5 py-1.5 transition-colors whitespace-nowrap"
|
|
781
|
+
title="Configure ceremony models"
|
|
782
|
+
>
|
|
783
|
+
<Settings className="w-3.5 h-3.5" />
|
|
784
|
+
Select Model(s)
|
|
785
|
+
</button>
|
|
786
|
+
)}
|
|
787
|
+
{!isBlocked && (
|
|
788
|
+
<button
|
|
789
|
+
onClick={handleClose}
|
|
790
|
+
className="text-slate-400 hover:text-slate-600 transition-colors"
|
|
791
|
+
>
|
|
792
|
+
<X className="w-5 h-5" />
|
|
793
|
+
</button>
|
|
794
|
+
)}
|
|
795
|
+
</div>
|
|
796
|
+
</div>
|
|
797
|
+
|
|
798
|
+
{/* Scrollable content */}
|
|
799
|
+
<div className="flex-1 overflow-y-auto px-6 py-5">
|
|
800
|
+
{renderStep()}
|
|
801
|
+
</div>
|
|
802
|
+
|
|
803
|
+
{/* Status bar */}
|
|
804
|
+
<div className="flex-shrink-0 border-t border-slate-100 px-6 h-8 flex items-center gap-2">
|
|
805
|
+
{status === 'running' && (
|
|
806
|
+
<>
|
|
807
|
+
<span className="w-3 h-3 border-2 border-blue-300 border-t-blue-600 rounded-full animate-spin flex-shrink-0" />
|
|
808
|
+
<p className="text-xs text-blue-600 font-medium truncate">Running sprint planning…</p>
|
|
809
|
+
</>
|
|
810
|
+
)}
|
|
811
|
+
{status === 'awaiting-selection' && (
|
|
812
|
+
<>
|
|
813
|
+
<span className="w-2 h-2 rounded-full bg-amber-400 flex-shrink-0" />
|
|
814
|
+
<p className="text-xs text-amber-600 font-medium truncate">Waiting for your selection to continue…</p>
|
|
815
|
+
</>
|
|
816
|
+
)}
|
|
817
|
+
</div>
|
|
818
|
+
</div>
|
|
819
|
+
|
|
820
|
+
{workflowOpen && workflowCeremony && (
|
|
821
|
+
<CeremonyWorkflowModal
|
|
822
|
+
ceremony={workflowCeremony}
|
|
823
|
+
allCeremonies={workflowAllCeremonies}
|
|
824
|
+
apiKeys={workflowApiKeys}
|
|
825
|
+
models={workflowModels}
|
|
826
|
+
readOnly={status === 'running' && !quotaLimitPending}
|
|
827
|
+
onSave={(status !== 'running' || quotaLimitPending) ? handleWorkflowSave : undefined}
|
|
828
|
+
onClose={() => setWorkflowOpen(false)}
|
|
829
|
+
onCeremoniesUpdated={(updated) => {
|
|
830
|
+
setWorkflowAllCeremonies(updated);
|
|
831
|
+
const sc = updated.find((c) => c.name === 'sprint-planning');
|
|
832
|
+
if (sc) setWorkflowCeremony(sc);
|
|
833
|
+
}}
|
|
834
|
+
/>
|
|
835
|
+
)}
|
|
836
|
+
</div>
|
|
837
|
+
);
|
|
838
|
+
}
|