@agile-vibe-coding/avc 0.2.3 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/README.md +475 -3
  2. package/cli/agents/agent-selector.md +23 -0
  3. package/cli/agents/code-implementer.md +117 -0
  4. package/cli/agents/code-validator.md +80 -0
  5. package/cli/agents/context-reviewer-epic.md +101 -0
  6. package/cli/agents/context-reviewer-story.md +92 -0
  7. package/cli/agents/context-writer-epic.md +145 -0
  8. package/cli/agents/context-writer-story.md +111 -0
  9. package/cli/agents/doc-writer-epic.md +42 -0
  10. package/cli/agents/doc-writer-story.md +43 -0
  11. package/cli/agents/duplicate-detector.md +110 -0
  12. package/cli/agents/epic-story-decomposer.md +318 -39
  13. package/cli/agents/mission-scope-generator.md +68 -4
  14. package/cli/agents/mission-scope-validator.md +40 -6
  15. package/cli/agents/project-context-extractor.md +21 -6
  16. package/cli/agents/scaffolding-generator.md +99 -0
  17. package/cli/agents/seed-validator.md +71 -0
  18. package/cli/agents/story-scope-reviewer.md +147 -0
  19. package/cli/agents/story-splitter.md +83 -0
  20. package/cli/agents/validator-documentation.json +31 -0
  21. package/cli/agents/validator-documentation.md +3 -1
  22. package/cli/api-reference-tool.js +368 -0
  23. package/cli/checks/catalog.json +76 -0
  24. package/cli/checks/code/quality.json +26 -0
  25. package/cli/checks/code/testing.json +14 -0
  26. package/cli/checks/code/traceability.json +26 -0
  27. package/cli/checks/cross-refs/epic.json +171 -0
  28. package/cli/checks/cross-refs/story.json +149 -0
  29. package/cli/checks/epic/api.json +114 -0
  30. package/cli/checks/epic/backend.json +126 -0
  31. package/cli/checks/epic/cloud.json +126 -0
  32. package/cli/checks/epic/data.json +102 -0
  33. package/cli/checks/epic/database.json +114 -0
  34. package/cli/checks/epic/developer.json +182 -0
  35. package/cli/checks/epic/devops.json +174 -0
  36. package/cli/checks/epic/frontend.json +162 -0
  37. package/cli/checks/epic/mobile.json +102 -0
  38. package/cli/checks/epic/qa.json +90 -0
  39. package/cli/checks/epic/security.json +184 -0
  40. package/cli/checks/epic/solution-architect.json +192 -0
  41. package/cli/checks/epic/test-architect.json +90 -0
  42. package/cli/checks/epic/ui.json +102 -0
  43. package/cli/checks/epic/ux.json +90 -0
  44. package/cli/checks/fixes/epic-fix-template.md +10 -0
  45. package/cli/checks/fixes/story-fix-template.md +10 -0
  46. package/cli/checks/story/api.json +186 -0
  47. package/cli/checks/story/backend.json +102 -0
  48. package/cli/checks/story/cloud.json +102 -0
  49. package/cli/checks/story/data.json +210 -0
  50. package/cli/checks/story/database.json +102 -0
  51. package/cli/checks/story/developer.json +168 -0
  52. package/cli/checks/story/devops.json +102 -0
  53. package/cli/checks/story/frontend.json +174 -0
  54. package/cli/checks/story/mobile.json +102 -0
  55. package/cli/checks/story/qa.json +210 -0
  56. package/cli/checks/story/security.json +198 -0
  57. package/cli/checks/story/solution-architect.json +230 -0
  58. package/cli/checks/story/test-architect.json +210 -0
  59. package/cli/checks/story/ui.json +102 -0
  60. package/cli/checks/story/ux.json +102 -0
  61. package/cli/coding-order.js +401 -0
  62. package/cli/dependency-checker.js +72 -0
  63. package/cli/epic-story-validator.js +284 -799
  64. package/cli/index.js +0 -0
  65. package/cli/init-model-config.js +17 -10
  66. package/cli/init.js +514 -92
  67. package/cli/kanban-server-manager.js +1 -2
  68. package/cli/llm-claude.js +98 -31
  69. package/cli/llm-gemini.js +29 -5
  70. package/cli/llm-local.js +493 -0
  71. package/cli/llm-openai.js +262 -41
  72. package/cli/llm-provider.js +147 -8
  73. package/cli/llm-token-limits.js +113 -4
  74. package/cli/llm-verifier.js +209 -1
  75. package/cli/llm-xiaomi.js +143 -0
  76. package/cli/message-constants.js +3 -12
  77. package/cli/messaging-api.js +6 -12
  78. package/cli/micro-check-fixer.js +335 -0
  79. package/cli/micro-check-runner.js +449 -0
  80. package/cli/micro-check-scorer.js +148 -0
  81. package/cli/micro-check-validator.js +538 -0
  82. package/cli/model-pricing.js +23 -0
  83. package/cli/model-selector.js +3 -2
  84. package/cli/prompt-logger.js +57 -0
  85. package/cli/repl-ink.js +106 -346
  86. package/cli/repl-old.js +1 -2
  87. package/cli/seed-processor.js +194 -24
  88. package/cli/sprint-planning-processor.js +2638 -289
  89. package/cli/template-processor.js +50 -3
  90. package/cli/token-tracker.js +50 -23
  91. package/cli/tools/generate-story-validators.js +1 -1
  92. package/cli/validation-router.js +70 -8
  93. package/cli/worktree-runner.js +654 -0
  94. package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
  95. package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
  96. package/kanban/client/dist/index.html +2 -2
  97. package/kanban/client/src/App.jsx +43 -14
  98. package/kanban/client/src/components/ceremony/AskArchPopup.jsx +7 -3
  99. package/kanban/client/src/components/ceremony/AskModelPopup.jsx +23 -10
  100. package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +320 -133
  101. package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
  102. package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +80 -13
  103. package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +156 -22
  104. package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +11 -11
  105. package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +3 -21
  106. package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +214 -10
  107. package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +23 -2
  108. package/kanban/client/src/components/kanban/CardDetailModal.jsx +97 -10
  109. package/kanban/client/src/components/kanban/GroupingSelector.jsx +7 -1
  110. package/kanban/client/src/components/kanban/KanbanCard.jsx +23 -14
  111. package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +9 -14
  112. package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
  113. package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
  114. package/kanban/client/src/components/settings/AgentsTab.jsx +103 -75
  115. package/kanban/client/src/components/settings/ApiKeysTab.jsx +31 -2
  116. package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +9 -2
  117. package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
  118. package/kanban/client/src/components/settings/CostThresholdsTab.jsx +3 -2
  119. package/kanban/client/src/components/settings/ModelPricingTab.jsx +72 -7
  120. package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
  121. package/kanban/client/src/components/settings/SettingsModal.jsx +4 -4
  122. package/kanban/client/src/components/stats/CostModal.jsx +34 -3
  123. package/kanban/client/src/hooks/useGrouping.js +59 -0
  124. package/kanban/client/src/lib/api.js +118 -4
  125. package/kanban/client/src/lib/status-grouping.js +10 -0
  126. package/kanban/client/src/store/kanbanStore.js +8 -0
  127. package/kanban/server/index.js +23 -2
  128. package/kanban/server/routes/ceremony.js +153 -4
  129. package/kanban/server/routes/costs.js +9 -3
  130. package/kanban/server/routes/openai-oauth.js +366 -0
  131. package/kanban/server/routes/settings.js +447 -14
  132. package/kanban/server/routes/websocket.js +7 -2
  133. package/kanban/server/routes/work-items.js +141 -1
  134. package/kanban/server/services/CeremonyService.js +275 -24
  135. package/kanban/server/services/TaskRunnerService.js +261 -0
  136. package/kanban/server/workers/run-task-worker.js +121 -0
  137. package/kanban/server/workers/seed-worker.js +94 -0
  138. package/kanban/server/workers/sponsor-call-worker.js +14 -6
  139. package/kanban/server/workers/sprint-planning-worker.js +94 -12
  140. package/package.json +2 -3
  141. package/cli/agents/solver-epic-api.json +0 -15
  142. package/cli/agents/solver-epic-api.md +0 -39
  143. package/cli/agents/solver-epic-backend.json +0 -15
  144. package/cli/agents/solver-epic-backend.md +0 -39
  145. package/cli/agents/solver-epic-cloud.json +0 -15
  146. package/cli/agents/solver-epic-cloud.md +0 -39
  147. package/cli/agents/solver-epic-data.json +0 -15
  148. package/cli/agents/solver-epic-data.md +0 -39
  149. package/cli/agents/solver-epic-database.json +0 -15
  150. package/cli/agents/solver-epic-database.md +0 -39
  151. package/cli/agents/solver-epic-developer.json +0 -15
  152. package/cli/agents/solver-epic-developer.md +0 -39
  153. package/cli/agents/solver-epic-devops.json +0 -15
  154. package/cli/agents/solver-epic-devops.md +0 -39
  155. package/cli/agents/solver-epic-frontend.json +0 -15
  156. package/cli/agents/solver-epic-frontend.md +0 -39
  157. package/cli/agents/solver-epic-mobile.json +0 -15
  158. package/cli/agents/solver-epic-mobile.md +0 -39
  159. package/cli/agents/solver-epic-qa.json +0 -15
  160. package/cli/agents/solver-epic-qa.md +0 -39
  161. package/cli/agents/solver-epic-security.json +0 -15
  162. package/cli/agents/solver-epic-security.md +0 -39
  163. package/cli/agents/solver-epic-solution-architect.json +0 -15
  164. package/cli/agents/solver-epic-solution-architect.md +0 -39
  165. package/cli/agents/solver-epic-test-architect.json +0 -15
  166. package/cli/agents/solver-epic-test-architect.md +0 -39
  167. package/cli/agents/solver-epic-ui.json +0 -15
  168. package/cli/agents/solver-epic-ui.md +0 -39
  169. package/cli/agents/solver-epic-ux.json +0 -15
  170. package/cli/agents/solver-epic-ux.md +0 -39
  171. package/cli/agents/solver-story-api.json +0 -15
  172. package/cli/agents/solver-story-api.md +0 -39
  173. package/cli/agents/solver-story-backend.json +0 -15
  174. package/cli/agents/solver-story-backend.md +0 -39
  175. package/cli/agents/solver-story-cloud.json +0 -15
  176. package/cli/agents/solver-story-cloud.md +0 -39
  177. package/cli/agents/solver-story-data.json +0 -15
  178. package/cli/agents/solver-story-data.md +0 -39
  179. package/cli/agents/solver-story-database.json +0 -15
  180. package/cli/agents/solver-story-database.md +0 -39
  181. package/cli/agents/solver-story-developer.json +0 -15
  182. package/cli/agents/solver-story-developer.md +0 -39
  183. package/cli/agents/solver-story-devops.json +0 -15
  184. package/cli/agents/solver-story-devops.md +0 -39
  185. package/cli/agents/solver-story-frontend.json +0 -15
  186. package/cli/agents/solver-story-frontend.md +0 -39
  187. package/cli/agents/solver-story-mobile.json +0 -15
  188. package/cli/agents/solver-story-mobile.md +0 -39
  189. package/cli/agents/solver-story-qa.json +0 -15
  190. package/cli/agents/solver-story-qa.md +0 -39
  191. package/cli/agents/solver-story-security.json +0 -15
  192. package/cli/agents/solver-story-security.md +0 -39
  193. package/cli/agents/solver-story-solution-architect.json +0 -15
  194. package/cli/agents/solver-story-solution-architect.md +0 -39
  195. package/cli/agents/solver-story-test-architect.json +0 -15
  196. package/cli/agents/solver-story-test-architect.md +0 -39
  197. package/cli/agents/solver-story-ui.json +0 -15
  198. package/cli/agents/solver-story-ui.md +0 -39
  199. package/cli/agents/solver-story-ux.json +0 -15
  200. package/cli/agents/solver-story-ux.md +0 -39
  201. package/cli/agents/validator-epic-api.json +0 -93
  202. package/cli/agents/validator-epic-api.md +0 -137
  203. package/cli/agents/validator-epic-backend.json +0 -93
  204. package/cli/agents/validator-epic-backend.md +0 -130
  205. package/cli/agents/validator-epic-cloud.json +0 -93
  206. package/cli/agents/validator-epic-cloud.md +0 -137
  207. package/cli/agents/validator-epic-data.json +0 -93
  208. package/cli/agents/validator-epic-data.md +0 -130
  209. package/cli/agents/validator-epic-database.json +0 -93
  210. package/cli/agents/validator-epic-database.md +0 -137
  211. package/cli/agents/validator-epic-developer.json +0 -74
  212. package/cli/agents/validator-epic-developer.md +0 -153
  213. package/cli/agents/validator-epic-devops.json +0 -74
  214. package/cli/agents/validator-epic-devops.md +0 -153
  215. package/cli/agents/validator-epic-frontend.json +0 -74
  216. package/cli/agents/validator-epic-frontend.md +0 -153
  217. package/cli/agents/validator-epic-mobile.json +0 -93
  218. package/cli/agents/validator-epic-mobile.md +0 -130
  219. package/cli/agents/validator-epic-qa.json +0 -93
  220. package/cli/agents/validator-epic-qa.md +0 -130
  221. package/cli/agents/validator-epic-security.json +0 -74
  222. package/cli/agents/validator-epic-security.md +0 -154
  223. package/cli/agents/validator-epic-solution-architect.json +0 -74
  224. package/cli/agents/validator-epic-solution-architect.md +0 -156
  225. package/cli/agents/validator-epic-test-architect.json +0 -93
  226. package/cli/agents/validator-epic-test-architect.md +0 -130
  227. package/cli/agents/validator-epic-ui.json +0 -93
  228. package/cli/agents/validator-epic-ui.md +0 -130
  229. package/cli/agents/validator-epic-ux.json +0 -93
  230. package/cli/agents/validator-epic-ux.md +0 -130
  231. package/cli/agents/validator-story-api.json +0 -104
  232. package/cli/agents/validator-story-api.md +0 -152
  233. package/cli/agents/validator-story-backend.json +0 -104
  234. package/cli/agents/validator-story-backend.md +0 -152
  235. package/cli/agents/validator-story-cloud.json +0 -104
  236. package/cli/agents/validator-story-cloud.md +0 -152
  237. package/cli/agents/validator-story-data.json +0 -104
  238. package/cli/agents/validator-story-data.md +0 -152
  239. package/cli/agents/validator-story-database.json +0 -104
  240. package/cli/agents/validator-story-database.md +0 -152
  241. package/cli/agents/validator-story-developer.json +0 -104
  242. package/cli/agents/validator-story-developer.md +0 -152
  243. package/cli/agents/validator-story-devops.json +0 -104
  244. package/cli/agents/validator-story-devops.md +0 -152
  245. package/cli/agents/validator-story-frontend.json +0 -104
  246. package/cli/agents/validator-story-frontend.md +0 -152
  247. package/cli/agents/validator-story-mobile.json +0 -104
  248. package/cli/agents/validator-story-mobile.md +0 -152
  249. package/cli/agents/validator-story-qa.json +0 -104
  250. package/cli/agents/validator-story-qa.md +0 -152
  251. package/cli/agents/validator-story-security.json +0 -104
  252. package/cli/agents/validator-story-security.md +0 -152
  253. package/cli/agents/validator-story-solution-architect.json +0 -104
  254. package/cli/agents/validator-story-solution-architect.md +0 -152
  255. package/cli/agents/validator-story-test-architect.json +0 -104
  256. package/cli/agents/validator-story-test-architect.md +0 -152
  257. package/cli/agents/validator-story-ui.json +0 -104
  258. package/cli/agents/validator-story-ui.md +0 -152
  259. package/cli/agents/validator-story-ux.json +0 -104
  260. package/cli/agents/validator-story-ux.md +0 -152
  261. package/kanban/client/dist/assets/index-CiD8PS2e.js +0 -306
  262. package/kanban/client/dist/assets/index-nLh0m82Q.css +0 -1
@@ -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
+ }
@@ -1,5 +1,5 @@
1
- import { useState, useEffect } from 'react';
2
- import { X, Info, AlertTriangle, Settings as SettingsIcon } from 'lucide-react';
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { X, AlertTriangle, Settings as SettingsIcon } from 'lucide-react';
3
3
  import { useCeremonyStore } from '../../store/ceremonyStore';
4
4
  import {
5
5
  analyzeDatabase,
@@ -30,6 +30,7 @@ const KEY_LABELS = {
30
30
  anthropic: 'Anthropic API Key (ANTHROPIC_API_KEY)',
31
31
  gemini: 'Google Gemini API Key (GEMINI_API_KEY)',
32
32
  openai: 'OpenAI API Key (OPENAI_API_KEY)',
33
+ xiaomi: 'Xiaomi MiMo API Key (XIAOMI_API_KEY)',
33
34
  };
34
35
 
35
36
  function normalizeProvider(provider = '') {
@@ -60,7 +61,7 @@ function computeMissingProviders(settings) {
60
61
  }
61
62
 
62
63
  const apiKeys = settings.apiKeys ?? {};
63
- return [...needed].filter((p) => !apiKeys[p]?.isSet);
64
+ return [...needed].filter((p) => p !== 'local' && !apiKeys[p]?.isSet);
64
65
  }
65
66
 
66
67
  // Step definitions for the progress header (shown steps vary based on hasDb)
@@ -151,6 +152,8 @@ export function SponsorCallModal({ onClose, onOpenSettings, costLimitPending, on
151
152
  } = useCeremonyStore();
152
153
 
153
154
  const [analyzingMessage, setAnalyzingMessage] = useState('');
155
+ const [analysisError, setAnalysisError] = useState(null); // { message, retry? }
156
+ const setError = (message, retryFn = null) => setAnalysisError(message ? { message, retry: retryFn } : null);
154
157
  const [showCancelConfirm, setShowCancelConfirm] = useState(false);
155
158
  const [transitioning, setTransitioning] = useState(null); // null | 'pausing' | 'cancelling'
156
159
  const [workflowOpen, setWorkflowOpen] = useState(false);
@@ -158,16 +161,21 @@ export function SponsorCallModal({ onClose, onOpenSettings, costLimitPending, on
158
161
  const [workflowModels, setWorkflowModels] = useState([]);
159
162
  const [workflowMissionGenValidation, setWorkflowMissionGenValidation] = useState(null);
160
163
  const [workflowAllCeremonies, setWorkflowAllCeremonies] = useState([]);
164
+ const [workflowApiKeys, setWorkflowApiKeys] = useState({});
161
165
  const [apiKeyCheck, setApiKeyCheck] = useState({ loading: true, missing: [] });
162
166
  const [showResumePrompt, setShowResumePrompt] = useState(false);
163
167
  const [draftData, setDraftData] = useState(null);
168
+ const [settings, setSettings] = useState({ ceremonies: [], apiKeys: {} });
164
169
 
165
170
  // Check required API keys when the modal opens
166
171
  useEffect(() => {
167
172
  let cancelled = false;
168
173
  getSettings()
169
174
  .then((s) => {
170
- if (!cancelled) setApiKeyCheck({ loading: false, missing: computeMissingProviders(s) });
175
+ if (!cancelled) {
176
+ setApiKeyCheck({ loading: false, missing: computeMissingProviders(s) });
177
+ setSettings(s);
178
+ }
171
179
  })
172
180
  .catch(() => {
173
181
  if (!cancelled) setApiKeyCheck({ loading: false, missing: [] }); // fail open
@@ -189,12 +197,18 @@ export function SponsorCallModal({ onClose, onOpenSettings, costLimitPending, on
189
197
  return () => { cancelled = true; };
190
198
  }, [isOpen]); // eslint-disable-line react-hooks/exhaustive-deps
191
199
 
192
- const recheckKeys = () => {
200
+ const recheckKeys = useCallback(() => {
193
201
  setApiKeyCheck({ loading: true, missing: [] });
194
202
  getSettings()
195
203
  .then((s) => setApiKeyCheck({ loading: false, missing: computeMissingProviders(s) }))
196
204
  .catch(() => setApiKeyCheck({ loading: false, missing: [] }));
197
- };
205
+ }, []);
206
+
207
+ // Auto-recheck whenever settings are saved anywhere in the app (API keys or ceremony models)
208
+ useEffect(() => {
209
+ document.addEventListener('avc:settings-saved', recheckKeys);
210
+ return () => document.removeEventListener('avc:settings-saved', recheckKeys);
211
+ }, [recheckKeys]);
198
212
 
199
213
  // Snapshot current wizard state and persist to server
200
214
  const saveDraft = (overrides = {}) => {
@@ -290,6 +304,7 @@ export function SponsorCallModal({ onClose, onOpenSettings, costLimitPending, on
290
304
 
291
305
  // Step 2 → (DB analysis) → Step 3 or 4
292
306
  const handleMissionNext = async () => {
307
+ setError(null);
293
308
  setAnalyzing(true);
294
309
  setAnalyzingMessage('Checking database needs…');
295
310
  try {
@@ -309,7 +324,7 @@ export function SponsorCallModal({ onClose, onOpenSettings, costLimitPending, on
309
324
  }
310
325
  } catch (err) {
311
326
  console.error('Mission analysis error:', err);
312
- alert(`Analysis failed: ${err.message}`);
327
+ setError(err.message, handleMissionNext);
313
328
  } finally {
314
329
  setAnalyzing(false);
315
330
  setAnalyzingMessage('');
@@ -318,6 +333,7 @@ export function SponsorCallModal({ onClose, onOpenSettings, costLimitPending, on
318
333
 
319
334
  // Step 3 → Architecture analysis → Step 4
320
335
  const handleDatabaseNext = async () => {
336
+ setError(null);
321
337
  setAnalyzing(true);
322
338
  setAnalyzingMessage('Analysing architecture options…');
323
339
  try {
@@ -328,7 +344,7 @@ export function SponsorCallModal({ onClose, onOpenSettings, costLimitPending, on
328
344
  saveDraft({ wizardStep: 4, archOptions: archData });
329
345
  } catch (err) {
330
346
  console.error('Architecture analysis error:', err);
331
- alert(`Analysis failed: ${err.message}`);
347
+ setError(err.message, handleDatabaseNext);
332
348
  } finally {
333
349
  setAnalyzing(false);
334
350
  setAnalyzingMessage('');
@@ -337,6 +353,7 @@ export function SponsorCallModal({ onClose, onOpenSettings, costLimitPending, on
337
353
 
338
354
  // Step 4 → Prefill → Step 5
339
355
  const handleArchitectureNext = async () => {
356
+ setError(null);
340
357
  setAnalyzing(true);
341
358
  setAnalyzingMessage('Pre-filling requirements from your selections…');
342
359
  try {
@@ -348,7 +365,7 @@ export function SponsorCallModal({ onClose, onOpenSettings, costLimitPending, on
348
365
  setTimeout(() => saveDraft({ wizardStep: 5, prefillResult: prefill }), 0);
349
366
  } catch (err) {
350
367
  console.error('Prefill error:', err);
351
- alert(`Prefill failed: ${err.message}`);
368
+ setError(err.message, handleArchitectureNext);
352
369
  } finally {
353
370
  setAnalyzing(false);
354
371
  setAnalyzingMessage('');
@@ -526,14 +543,15 @@ export function SponsorCallModal({ onClose, onOpenSettings, costLimitPending, on
526
543
  setWorkflowModels(m);
527
544
  setWorkflowMissionGenValidation(s.missionGenerator?.validation ?? null);
528
545
  setWorkflowAllCeremonies(s.ceremonies || []);
546
+ setWorkflowApiKeys(s.apiKeys || {});
529
547
  setWorkflowOpen(true);
530
548
  } catch {}
531
549
  }}
532
- className="flex items-center gap-1 text-xs text-slate-400 hover:text-blue-500 transition-colors whitespace-nowrap"
533
- title="View ceremony workflow"
550
+ 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"
551
+ title="Configure ceremony models"
534
552
  >
535
- <Info className="w-3.5 h-3.5" />
536
- How it works
553
+ <SettingsIcon className="w-3.5 h-3.5" />
554
+ Select Model(s)
537
555
  </button>
538
556
  )}
539
557
  {ceremonyStatus !== 'running' && (
@@ -593,6 +611,48 @@ export function SponsorCallModal({ onClose, onOpenSettings, costLimitPending, on
593
611
  )}
594
612
  </div>
595
613
 
614
+ {/* Error banner — shown above status bar when an analysis step fails */}
615
+ {analysisError && (
616
+ <div className="flex-shrink-0 border-t border-red-200 bg-red-50 px-6 py-3">
617
+ <div className="flex items-start gap-2">
618
+ <span className="text-red-500 flex-shrink-0 mt-0.5 text-sm">✕</span>
619
+ <div className="flex-1 min-w-0">
620
+ {analysisError.message?.includes('n_keep') || analysisError.message?.includes('n_ctx') || analysisError.message?.includes('context length') || analysisError.message?.includes('context_length') ? (
621
+ <>
622
+ <p className="text-xs font-medium text-red-700">Context length exceeded</p>
623
+ <p className="text-xs text-red-600 mt-0.5">
624
+ The prompt is too large for this model's context window. Increase the context length in your local server (e.g. LM Studio → Model Settings → Context Length), then click Retry.
625
+ </p>
626
+ </>
627
+ ) : (
628
+ <>
629
+ <p className="text-xs font-medium text-red-700">Analysis failed</p>
630
+ <p className="text-xs text-red-600 mt-0.5 break-words">{analysisError.message}</p>
631
+ </>
632
+ )}
633
+ <div className="flex items-center gap-3 mt-2">
634
+ {analysisError.retry && (
635
+ <button
636
+ type="button"
637
+ onClick={() => { const fn = analysisError.retry; setError(null); fn(); }}
638
+ className="text-xs font-medium text-white bg-red-600 hover:bg-red-700 px-3 py-1 rounded transition-colors"
639
+ >
640
+ Retry
641
+ </button>
642
+ )}
643
+ <button
644
+ type="button"
645
+ onClick={() => setError(null)}
646
+ className="text-xs font-medium text-red-700 hover:text-red-900 underline underline-offset-2"
647
+ >
648
+ Dismiss
649
+ </button>
650
+ </div>
651
+ </div>
652
+ </div>
653
+ </div>
654
+ )}
655
+
596
656
  {/* Status bar — always rendered to prevent height flicker */}
597
657
  <div className="flex-shrink-0 border-t border-slate-100 px-6 h-8 flex items-center gap-2">
598
658
  {analyzingMessage && (
@@ -607,11 +667,18 @@ export function SponsorCallModal({ onClose, onOpenSettings, costLimitPending, on
607
667
  {workflowOpen && workflowCeremony && (
608
668
  <CeremonyWorkflowModal
609
669
  ceremony={workflowCeremony}
670
+ allCeremonies={workflowAllCeremonies}
671
+ apiKeys={workflowApiKeys}
610
672
  models={workflowModels}
611
673
  missionGenValidation={workflowMissionGenValidation}
612
674
  readOnly={ceremonyStatus === 'running'}
613
675
  onSave={ceremonyStatus !== 'running' ? handleWorkflowSave : undefined}
614
676
  onClose={handleWorkflowClose}
677
+ onCeremoniesUpdated={(updated) => {
678
+ setWorkflowAllCeremonies(updated);
679
+ const sc = updated.find((c) => c.name === 'sponsor-call');
680
+ if (sc) setWorkflowCeremony(sc);
681
+ }}
615
682
  />
616
683
  )}
617
684
  </div>