@agile-vibe-coding/avc 0.1.0 → 0.2.3

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 (290) hide show
  1. package/README.md +2 -0
  2. package/cli/agent-loader.js +21 -0
  3. package/cli/agents/agent-selector.md +129 -0
  4. package/cli/agents/architecture-recommender.md +418 -0
  5. package/cli/agents/database-deep-dive.md +470 -0
  6. package/cli/agents/database-recommender.md +634 -0
  7. package/cli/agents/doc-distributor.md +176 -0
  8. package/cli/agents/documentation-updater.md +203 -0
  9. package/cli/agents/epic-story-decomposer.md +280 -0
  10. package/cli/agents/feature-context-generator.md +91 -0
  11. package/cli/agents/gap-checker-epic.md +52 -0
  12. package/cli/agents/impact-checker-story.md +51 -0
  13. package/cli/agents/migration-guide-generator.md +305 -0
  14. package/cli/agents/mission-scope-generator.md +79 -0
  15. package/cli/agents/mission-scope-validator.md +112 -0
  16. package/cli/agents/project-context-extractor.md +107 -0
  17. package/cli/agents/project-documentation-creator.json +226 -0
  18. package/cli/agents/project-documentation-creator.md +595 -0
  19. package/cli/agents/question-prefiller.md +269 -0
  20. package/cli/agents/refiner-epic.md +39 -0
  21. package/cli/agents/refiner-story.md +42 -0
  22. package/cli/agents/solver-epic-api.json +15 -0
  23. package/cli/agents/solver-epic-api.md +39 -0
  24. package/cli/agents/solver-epic-backend.json +15 -0
  25. package/cli/agents/solver-epic-backend.md +39 -0
  26. package/cli/agents/solver-epic-cloud.json +15 -0
  27. package/cli/agents/solver-epic-cloud.md +39 -0
  28. package/cli/agents/solver-epic-data.json +15 -0
  29. package/cli/agents/solver-epic-data.md +39 -0
  30. package/cli/agents/solver-epic-database.json +15 -0
  31. package/cli/agents/solver-epic-database.md +39 -0
  32. package/cli/agents/solver-epic-developer.json +15 -0
  33. package/cli/agents/solver-epic-developer.md +39 -0
  34. package/cli/agents/solver-epic-devops.json +15 -0
  35. package/cli/agents/solver-epic-devops.md +39 -0
  36. package/cli/agents/solver-epic-frontend.json +15 -0
  37. package/cli/agents/solver-epic-frontend.md +39 -0
  38. package/cli/agents/solver-epic-mobile.json +15 -0
  39. package/cli/agents/solver-epic-mobile.md +39 -0
  40. package/cli/agents/solver-epic-qa.json +15 -0
  41. package/cli/agents/solver-epic-qa.md +39 -0
  42. package/cli/agents/solver-epic-security.json +15 -0
  43. package/cli/agents/solver-epic-security.md +39 -0
  44. package/cli/agents/solver-epic-solution-architect.json +15 -0
  45. package/cli/agents/solver-epic-solution-architect.md +39 -0
  46. package/cli/agents/solver-epic-test-architect.json +15 -0
  47. package/cli/agents/solver-epic-test-architect.md +39 -0
  48. package/cli/agents/solver-epic-ui.json +15 -0
  49. package/cli/agents/solver-epic-ui.md +39 -0
  50. package/cli/agents/solver-epic-ux.json +15 -0
  51. package/cli/agents/solver-epic-ux.md +39 -0
  52. package/cli/agents/solver-story-api.json +15 -0
  53. package/cli/agents/solver-story-api.md +39 -0
  54. package/cli/agents/solver-story-backend.json +15 -0
  55. package/cli/agents/solver-story-backend.md +39 -0
  56. package/cli/agents/solver-story-cloud.json +15 -0
  57. package/cli/agents/solver-story-cloud.md +39 -0
  58. package/cli/agents/solver-story-data.json +15 -0
  59. package/cli/agents/solver-story-data.md +39 -0
  60. package/cli/agents/solver-story-database.json +15 -0
  61. package/cli/agents/solver-story-database.md +39 -0
  62. package/cli/agents/solver-story-developer.json +15 -0
  63. package/cli/agents/solver-story-developer.md +39 -0
  64. package/cli/agents/solver-story-devops.json +15 -0
  65. package/cli/agents/solver-story-devops.md +39 -0
  66. package/cli/agents/solver-story-frontend.json +15 -0
  67. package/cli/agents/solver-story-frontend.md +39 -0
  68. package/cli/agents/solver-story-mobile.json +15 -0
  69. package/cli/agents/solver-story-mobile.md +39 -0
  70. package/cli/agents/solver-story-qa.json +15 -0
  71. package/cli/agents/solver-story-qa.md +39 -0
  72. package/cli/agents/solver-story-security.json +15 -0
  73. package/cli/agents/solver-story-security.md +39 -0
  74. package/cli/agents/solver-story-solution-architect.json +15 -0
  75. package/cli/agents/solver-story-solution-architect.md +39 -0
  76. package/cli/agents/solver-story-test-architect.json +15 -0
  77. package/cli/agents/solver-story-test-architect.md +39 -0
  78. package/cli/agents/solver-story-ui.json +15 -0
  79. package/cli/agents/solver-story-ui.md +39 -0
  80. package/cli/agents/solver-story-ux.json +15 -0
  81. package/cli/agents/solver-story-ux.md +39 -0
  82. package/cli/agents/story-doc-enricher.md +133 -0
  83. package/cli/agents/suggestion-business-analyst.md +88 -0
  84. package/cli/agents/suggestion-deployment-architect.md +263 -0
  85. package/cli/agents/suggestion-product-manager.md +129 -0
  86. package/cli/agents/suggestion-security-specialist.md +156 -0
  87. package/cli/agents/suggestion-technical-architect.md +269 -0
  88. package/cli/agents/suggestion-ux-researcher.md +93 -0
  89. package/cli/agents/task-subtask-decomposer.md +188 -0
  90. package/cli/agents/validator-documentation.json +152 -0
  91. package/cli/agents/validator-documentation.md +453 -0
  92. package/cli/agents/validator-epic-api.json +93 -0
  93. package/cli/agents/validator-epic-api.md +137 -0
  94. package/cli/agents/validator-epic-backend.json +93 -0
  95. package/cli/agents/validator-epic-backend.md +130 -0
  96. package/cli/agents/validator-epic-cloud.json +93 -0
  97. package/cli/agents/validator-epic-cloud.md +137 -0
  98. package/cli/agents/validator-epic-data.json +93 -0
  99. package/cli/agents/validator-epic-data.md +130 -0
  100. package/cli/agents/validator-epic-database.json +93 -0
  101. package/cli/agents/validator-epic-database.md +137 -0
  102. package/cli/agents/validator-epic-developer.json +74 -0
  103. package/cli/agents/validator-epic-developer.md +153 -0
  104. package/cli/agents/validator-epic-devops.json +74 -0
  105. package/cli/agents/validator-epic-devops.md +153 -0
  106. package/cli/agents/validator-epic-frontend.json +74 -0
  107. package/cli/agents/validator-epic-frontend.md +153 -0
  108. package/cli/agents/validator-epic-mobile.json +93 -0
  109. package/cli/agents/validator-epic-mobile.md +130 -0
  110. package/cli/agents/validator-epic-qa.json +93 -0
  111. package/cli/agents/validator-epic-qa.md +130 -0
  112. package/cli/agents/validator-epic-security.json +74 -0
  113. package/cli/agents/validator-epic-security.md +154 -0
  114. package/cli/agents/validator-epic-solution-architect.json +74 -0
  115. package/cli/agents/validator-epic-solution-architect.md +156 -0
  116. package/cli/agents/validator-epic-test-architect.json +93 -0
  117. package/cli/agents/validator-epic-test-architect.md +130 -0
  118. package/cli/agents/validator-epic-ui.json +93 -0
  119. package/cli/agents/validator-epic-ui.md +130 -0
  120. package/cli/agents/validator-epic-ux.json +93 -0
  121. package/cli/agents/validator-epic-ux.md +130 -0
  122. package/cli/agents/validator-selector.md +211 -0
  123. package/cli/agents/validator-story-api.json +104 -0
  124. package/cli/agents/validator-story-api.md +152 -0
  125. package/cli/agents/validator-story-backend.json +104 -0
  126. package/cli/agents/validator-story-backend.md +152 -0
  127. package/cli/agents/validator-story-cloud.json +104 -0
  128. package/cli/agents/validator-story-cloud.md +152 -0
  129. package/cli/agents/validator-story-data.json +104 -0
  130. package/cli/agents/validator-story-data.md +152 -0
  131. package/cli/agents/validator-story-database.json +104 -0
  132. package/cli/agents/validator-story-database.md +152 -0
  133. package/cli/agents/validator-story-developer.json +104 -0
  134. package/cli/agents/validator-story-developer.md +152 -0
  135. package/cli/agents/validator-story-devops.json +104 -0
  136. package/cli/agents/validator-story-devops.md +152 -0
  137. package/cli/agents/validator-story-frontend.json +104 -0
  138. package/cli/agents/validator-story-frontend.md +152 -0
  139. package/cli/agents/validator-story-mobile.json +104 -0
  140. package/cli/agents/validator-story-mobile.md +152 -0
  141. package/cli/agents/validator-story-qa.json +104 -0
  142. package/cli/agents/validator-story-qa.md +152 -0
  143. package/cli/agents/validator-story-security.json +104 -0
  144. package/cli/agents/validator-story-security.md +152 -0
  145. package/cli/agents/validator-story-solution-architect.json +104 -0
  146. package/cli/agents/validator-story-solution-architect.md +152 -0
  147. package/cli/agents/validator-story-test-architect.json +104 -0
  148. package/cli/agents/validator-story-test-architect.md +152 -0
  149. package/cli/agents/validator-story-ui.json +104 -0
  150. package/cli/agents/validator-story-ui.md +152 -0
  151. package/cli/agents/validator-story-ux.json +104 -0
  152. package/cli/agents/validator-story-ux.md +152 -0
  153. package/cli/ansi-colors.js +21 -0
  154. package/cli/build-docs.js +298 -0
  155. package/cli/ceremony-history.js +369 -0
  156. package/cli/command-logger.js +245 -0
  157. package/cli/components/static-output.js +63 -0
  158. package/cli/console-output-manager.js +94 -0
  159. package/cli/docs-sync.js +306 -0
  160. package/cli/epic-story-validator.js +1174 -0
  161. package/cli/evaluation-prompts.js +1008 -0
  162. package/cli/execution-context.js +195 -0
  163. package/cli/generate-summary-table.js +340 -0
  164. package/cli/index.js +3 -25
  165. package/cli/init-model-config.js +697 -0
  166. package/cli/init.js +1765 -100
  167. package/cli/kanban-server-manager.js +228 -0
  168. package/cli/llm-claude.js +109 -0
  169. package/cli/llm-gemini.js +115 -0
  170. package/cli/llm-mock.js +233 -0
  171. package/cli/llm-openai.js +233 -0
  172. package/cli/llm-provider.js +300 -0
  173. package/cli/llm-token-limits.js +102 -0
  174. package/cli/llm-verifier.js +454 -0
  175. package/cli/logger.js +32 -5
  176. package/cli/message-constants.js +58 -0
  177. package/cli/message-manager.js +334 -0
  178. package/cli/message-types.js +96 -0
  179. package/cli/messaging-api.js +297 -0
  180. package/cli/model-pricing.js +169 -0
  181. package/cli/model-query-engine.js +468 -0
  182. package/cli/model-recommendation-analyzer.js +495 -0
  183. package/cli/model-selector.js +269 -0
  184. package/cli/output-buffer.js +107 -0
  185. package/cli/process-manager.js +332 -0
  186. package/cli/repl-ink.js +5840 -504
  187. package/cli/repl-old.js +4 -4
  188. package/cli/seed-processor.js +792 -0
  189. package/cli/sprint-planning-processor.js +1813 -0
  190. package/cli/template-processor.js +2306 -108
  191. package/cli/templates/project.md +25 -8
  192. package/cli/templates/vitepress-config.mts.template +34 -0
  193. package/cli/token-tracker.js +520 -0
  194. package/cli/tools/generate-story-validators.js +317 -0
  195. package/cli/tools/generate-validators.js +669 -0
  196. package/cli/update-checker.js +19 -17
  197. package/cli/update-notifier.js +4 -4
  198. package/cli/validation-router.js +605 -0
  199. package/cli/verification-tracker.js +563 -0
  200. package/kanban/README.md +386 -0
  201. package/kanban/client/README.md +205 -0
  202. package/kanban/client/components.json +20 -0
  203. package/kanban/client/dist/assets/index-CiD8PS2e.js +306 -0
  204. package/kanban/client/dist/assets/index-nLh0m82Q.css +1 -0
  205. package/kanban/client/dist/index.html +16 -0
  206. package/kanban/client/dist/vite.svg +1 -0
  207. package/kanban/client/index.html +15 -0
  208. package/kanban/client/package-lock.json +9442 -0
  209. package/kanban/client/package.json +44 -0
  210. package/kanban/client/postcss.config.js +6 -0
  211. package/kanban/client/public/vite.svg +1 -0
  212. package/kanban/client/src/App.jsx +622 -0
  213. package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
  214. package/kanban/client/src/components/ceremony/AskArchPopup.jsx +416 -0
  215. package/kanban/client/src/components/ceremony/AskModelPopup.jsx +616 -0
  216. package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +946 -0
  217. package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
  218. package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +619 -0
  219. package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +704 -0
  220. package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
  221. package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +154 -0
  222. package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
  223. package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
  224. package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
  225. package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +125 -0
  226. package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +228 -0
  227. package/kanban/client/src/components/kanban/CardDetailModal.jsx +559 -0
  228. package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
  229. package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
  230. package/kanban/client/src/components/kanban/GroupingSelector.jsx +57 -0
  231. package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
  232. package/kanban/client/src/components/kanban/KanbanCard.jsx +138 -0
  233. package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
  234. package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +789 -0
  235. package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
  236. package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
  237. package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
  238. package/kanban/client/src/components/settings/AgentsTab.jsx +353 -0
  239. package/kanban/client/src/components/settings/ApiKeysTab.jsx +113 -0
  240. package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +98 -0
  241. package/kanban/client/src/components/settings/CostThresholdsTab.jsx +94 -0
  242. package/kanban/client/src/components/settings/ModelPricingTab.jsx +204 -0
  243. package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
  244. package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
  245. package/kanban/client/src/components/stats/CostModal.jsx +353 -0
  246. package/kanban/client/src/components/ui/badge.jsx +27 -0
  247. package/kanban/client/src/components/ui/dialog.jsx +121 -0
  248. package/kanban/client/src/components/ui/tabs.jsx +85 -0
  249. package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
  250. package/kanban/client/src/hooks/useGrouping.js +118 -0
  251. package/kanban/client/src/hooks/useWebSocket.js +120 -0
  252. package/kanban/client/src/lib/__tests__/api.test.js +196 -0
  253. package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
  254. package/kanban/client/src/lib/api.js +401 -0
  255. package/kanban/client/src/lib/status-grouping.js +144 -0
  256. package/kanban/client/src/lib/utils.js +11 -0
  257. package/kanban/client/src/main.jsx +10 -0
  258. package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
  259. package/kanban/client/src/store/ceremonyStore.js +172 -0
  260. package/kanban/client/src/store/filterStore.js +201 -0
  261. package/kanban/client/src/store/kanbanStore.js +115 -0
  262. package/kanban/client/src/store/processStore.js +65 -0
  263. package/kanban/client/src/store/sprintPlanningStore.js +33 -0
  264. package/kanban/client/src/styles/globals.css +59 -0
  265. package/kanban/client/tailwind.config.js +77 -0
  266. package/kanban/client/vite.config.js +28 -0
  267. package/kanban/client/vitest.config.js +28 -0
  268. package/kanban/dev-start.sh +47 -0
  269. package/kanban/package.json +12 -0
  270. package/kanban/server/index.js +516 -0
  271. package/kanban/server/routes/ceremony.js +305 -0
  272. package/kanban/server/routes/costs.js +157 -0
  273. package/kanban/server/routes/processes.js +50 -0
  274. package/kanban/server/routes/settings.js +303 -0
  275. package/kanban/server/routes/websocket.js +276 -0
  276. package/kanban/server/routes/work-items.js +347 -0
  277. package/kanban/server/services/CeremonyService.js +1190 -0
  278. package/kanban/server/services/FileSystemScanner.js +95 -0
  279. package/kanban/server/services/FileWatcher.js +144 -0
  280. package/kanban/server/services/HierarchyBuilder.js +196 -0
  281. package/kanban/server/services/ProcessRegistry.js +122 -0
  282. package/kanban/server/services/WorkItemReader.js +123 -0
  283. package/kanban/server/services/WorkItemRefineService.js +510 -0
  284. package/kanban/server/start.js +49 -0
  285. package/kanban/server/utils/kanban-logger.js +132 -0
  286. package/kanban/server/utils/markdown.js +91 -0
  287. package/kanban/server/utils/status-grouping.js +107 -0
  288. package/kanban/server/workers/sponsor-call-worker.js +84 -0
  289. package/kanban/server/workers/sprint-planning-worker.js +130 -0
  290. package/package.json +34 -7
@@ -0,0 +1,353 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { ChevronDown, ChevronRight } from 'lucide-react';
3
+ import { getAgentList } from '../../lib/api';
4
+ import { AgentEditorPopup } from './AgentEditorPopup';
5
+
6
+ const CEREMONY_STRUCTURE = [
7
+ {
8
+ ceremony: 'Sponsor Call',
9
+ color: 'blue',
10
+ phases: [
11
+ { phase: 'Mission & Scope', agents: [
12
+ { slug: 'mission-scope-generator', label: 'Mission Scope Generator', note: 'Generates mission & initial scope' },
13
+ { slug: 'mission-scope-validator', label: 'Mission Scope Validator', note: 'Validates mission quality' },
14
+ ]},
15
+ { phase: 'Questionnaire', agents: [
16
+ { slug: 'suggestion-product-manager', label: 'Product Manager', note: 'Fills Initial Scope' },
17
+ { slug: 'suggestion-ux-researcher', label: 'UX Researcher', note: 'Fills Target Users' },
18
+ { slug: 'suggestion-deployment-architect', label: 'Deployment Architect', note: 'Fills Deployment Target' },
19
+ { slug: 'suggestion-technical-architect', label: 'Technical Architect', note: 'Fills Technical Considerations' },
20
+ { slug: 'suggestion-security-specialist', label: 'Security Specialist', note: 'Fills Security & Compliance' },
21
+ { slug: 'architecture-recommender', label: 'Architecture Recommender', note: 'Recommends deployment architectures' },
22
+ { slug: 'database-recommender', label: 'Database Recommender', note: 'Recommends database type' },
23
+ { slug: 'database-deep-dive', label: 'Database Deep Dive', note: 'Detailed database analysis' },
24
+ { slug: 'question-prefiller', label: 'Question Prefiller', note: 'Pre-fills answers from architecture' },
25
+ ]},
26
+ { phase: 'Documentation', agents: [
27
+ { slug: 'project-documentation-creator', label: 'Documentation Creator', note: 'Creates project documentation' },
28
+ { slug: 'validator-documentation', label: 'Documentation Validator', note: 'Validates documentation quality' },
29
+ ]},
30
+ { phase: 'Context', agents: [
31
+ { slug: 'migration-guide-generator', label: 'Migration Guide Generator', note: 'Generates cloud migration guide' },
32
+ ]},
33
+ ],
34
+ },
35
+ {
36
+ ceremony: 'Sprint Planning',
37
+ color: 'purple',
38
+ phases: [
39
+ { phase: 'Decomposition', agents: [
40
+ { slug: 'epic-story-decomposer', label: 'Epic Story Decomposer', note: 'Breaks scope into epics & stories' },
41
+ ]},
42
+ { phase: 'Contextual Selection', agents: [
43
+ { slug: 'project-context-extractor', label: 'Project Context Extractor', note: 'Extracts project traits (once per run) to inform validator selection' },
44
+ { slug: 'agent-selector', label: 'Agent Selector', note: 'Selects relevant validators per Epic/Story based on project context' },
45
+ ]},
46
+ { phase: 'Documentation & Enrichment', agents: [
47
+ { slug: 'doc-distributor', label: 'Doc Distributor', note: 'Moves content from parent doc into child docs (project→epic, epic→story)' },
48
+ { slug: 'story-doc-enricher', label: 'Story Doc Enricher', note: 'Enriches story docs with API contracts, error tables, DB fields, business rules' },
49
+ ]},
50
+ { phase: 'Validation — Epic', agents: [
51
+ { slug: 'validator-selector', label: 'Validator Selector', note: 'Selects appropriate domain validators' },
52
+ { slug: 'validator-epic-solution-architect', label: 'Solution Architect' },
53
+ { slug: 'validator-epic-developer', label: 'Developer' },
54
+ { slug: 'validator-epic-security', label: 'Security' },
55
+ { slug: 'validator-epic-devops', label: 'DevOps' },
56
+ { slug: 'validator-epic-cloud', label: 'Cloud' },
57
+ { slug: 'validator-epic-backend', label: 'Backend' },
58
+ { slug: 'validator-epic-database', label: 'Database' },
59
+ { slug: 'validator-epic-api', label: 'API' },
60
+ { slug: 'validator-epic-frontend', label: 'Frontend' },
61
+ { slug: 'validator-epic-ui', label: 'UI' },
62
+ { slug: 'validator-epic-ux', label: 'UX' },
63
+ { slug: 'validator-epic-mobile', label: 'Mobile' },
64
+ { slug: 'validator-epic-data', label: 'Data' },
65
+ { slug: 'validator-epic-qa', label: 'QA' },
66
+ { slug: 'validator-epic-test-architect', label: 'Test Architect' },
67
+ ]},
68
+ { phase: 'Solving — Epic', agents: [
69
+ { slug: 'solver-epic-solution-architect', label: 'Solution Architect' },
70
+ { slug: 'solver-epic-developer', label: 'Developer' },
71
+ { slug: 'solver-epic-security', label: 'Security' },
72
+ { slug: 'solver-epic-devops', label: 'DevOps' },
73
+ { slug: 'solver-epic-cloud', label: 'Cloud' },
74
+ { slug: 'solver-epic-backend', label: 'Backend' },
75
+ { slug: 'solver-epic-database', label: 'Database' },
76
+ { slug: 'solver-epic-api', label: 'API' },
77
+ { slug: 'solver-epic-frontend', label: 'Frontend' },
78
+ { slug: 'solver-epic-ui', label: 'UI' },
79
+ { slug: 'solver-epic-ux', label: 'UX' },
80
+ { slug: 'solver-epic-mobile', label: 'Mobile' },
81
+ { slug: 'solver-epic-data', label: 'Data' },
82
+ { slug: 'solver-epic-qa', label: 'QA' },
83
+ { slug: 'solver-epic-test-architect', label: 'Test Architect' },
84
+ ]},
85
+ { phase: 'Validation — Story', agents: [
86
+ { slug: 'validator-story-solution-architect', label: 'Solution Architect' },
87
+ { slug: 'validator-story-developer', label: 'Developer' },
88
+ { slug: 'validator-story-security', label: 'Security' },
89
+ { slug: 'validator-story-devops', label: 'DevOps' },
90
+ { slug: 'validator-story-cloud', label: 'Cloud' },
91
+ { slug: 'validator-story-backend', label: 'Backend' },
92
+ { slug: 'validator-story-database', label: 'Database' },
93
+ { slug: 'validator-story-api', label: 'API' },
94
+ { slug: 'validator-story-frontend', label: 'Frontend' },
95
+ { slug: 'validator-story-ui', label: 'UI' },
96
+ { slug: 'validator-story-ux', label: 'UX' },
97
+ { slug: 'validator-story-mobile', label: 'Mobile' },
98
+ { slug: 'validator-story-data', label: 'Data' },
99
+ { slug: 'validator-story-qa', label: 'QA' },
100
+ { slug: 'validator-story-test-architect', label: 'Test Architect' },
101
+ ]},
102
+ { phase: 'Solving — Story', agents: [
103
+ { slug: 'solver-story-solution-architect', label: 'Solution Architect' },
104
+ { slug: 'solver-story-developer', label: 'Developer' },
105
+ { slug: 'solver-story-security', label: 'Security' },
106
+ { slug: 'solver-story-devops', label: 'DevOps' },
107
+ { slug: 'solver-story-cloud', label: 'Cloud' },
108
+ { slug: 'solver-story-backend', label: 'Backend' },
109
+ { slug: 'solver-story-database', label: 'Database' },
110
+ { slug: 'solver-story-api', label: 'API' },
111
+ { slug: 'solver-story-frontend', label: 'Frontend' },
112
+ { slug: 'solver-story-ui', label: 'UI' },
113
+ { slug: 'solver-story-ux', label: 'UX' },
114
+ { slug: 'solver-story-mobile', label: 'Mobile' },
115
+ { slug: 'solver-story-data', label: 'Data' },
116
+ { slug: 'solver-story-qa', label: 'QA' },
117
+ { slug: 'solver-story-test-architect', label: 'Test Architect' },
118
+ ]},
119
+ ],
120
+ },
121
+ {
122
+ ceremony: 'Seed',
123
+ color: 'amber',
124
+ phases: [
125
+ { phase: 'Decomposition', agents: [
126
+ { slug: 'task-subtask-decomposer', label: 'Task Decomposer', note: 'Breaks stories into tasks & subtasks' },
127
+ ]},
128
+ { phase: 'Documentation', agents: [
129
+ { slug: 'doc-distributor', label: 'Doc Distributor', note: 'Moves content from story doc into task/subtask docs' },
130
+ { slug: 'feature-context-generator', label: 'Feature Context Generator', note: 'Generates implementation context.md for each task/subtask' },
131
+ ]},
132
+ ],
133
+ },
134
+ ];
135
+
136
+ const CEREMONY_COLORS = {
137
+ blue: { border: 'border-blue-200', header: 'bg-blue-50', accent: 'border-l-blue-400', text: 'text-blue-800' },
138
+ purple: { border: 'border-purple-200', header: 'bg-purple-50', accent: 'border-l-purple-400', text: 'text-purple-800' },
139
+ amber: { border: 'border-amber-200', header: 'bg-amber-50', accent: 'border-l-amber-400', text: 'text-amber-800' },
140
+ green: { border: 'border-green-200', header: 'bg-green-50', accent: 'border-l-green-400', text: 'text-green-800' },
141
+ slate: { border: 'border-slate-200', header: 'bg-slate-50', accent: 'border-l-slate-400', text: 'text-slate-800' },
142
+ };
143
+
144
+ // Flat set of all known slugs for computing "Other" group
145
+ const KNOWN_SLUGS = new Set(
146
+ CEREMONY_STRUCTURE.flatMap(c => c.phases.flatMap(p => p.agents.map(a => a.slug)))
147
+ );
148
+
149
+ export function AgentsTab() {
150
+ const [agentStatus, setAgentStatus] = useState({}); // { slug: isCustomized }
151
+ const [openAgent, setOpenAgent] = useState(null); // slug | null
152
+ const [search, setSearch] = useState('');
153
+ const [error, setError] = useState(null);
154
+ // collapsed state: absence = collapsed (default), true = open
155
+ const [collapsed, setCollapsed] = useState({});
156
+
157
+ useEffect(() => {
158
+ getAgentList()
159
+ .then(r => {
160
+ const status = {};
161
+ r.agents.forEach(a => {
162
+ const slug = a.name.replace(/\.md$/, '');
163
+ status[slug] = a.isCustomized;
164
+ });
165
+ setAgentStatus(status);
166
+ })
167
+ .catch(err => setError(err.message));
168
+ }, []);
169
+
170
+ const toggle = (key) => setCollapsed(prev => ({ ...prev, [key]: !prev[key] }));
171
+ const isOpen = (key) => collapsed[key] === true; // default collapsed when key absent
172
+
173
+ // Build "Other" group from agents returned by the API that aren't in any ceremony structure
174
+ const otherAgents = Object.keys(agentStatus)
175
+ .filter(slug => !KNOWN_SLUGS.has(slug))
176
+ .map(slug => ({ slug, label: slug, note: null }));
177
+
178
+ const allCeremonies = [
179
+ ...CEREMONY_STRUCTURE,
180
+ ...(otherAgents.length > 0
181
+ ? [{ ceremony: 'Other', color: 'slate', phases: [{ phase: 'Other', agents: otherAgents }] }]
182
+ : []),
183
+ ];
184
+
185
+ // Filter by search query — when searching, force everything open
186
+ const q = search.toLowerCase();
187
+ const filteredCeremonies = q
188
+ ? allCeremonies
189
+ .map(c => ({
190
+ ...c,
191
+ phases: c.phases
192
+ .map(p => ({
193
+ ...p,
194
+ agents: p.agents.filter(a =>
195
+ a.label.toLowerCase().includes(q) ||
196
+ a.slug.toLowerCase().includes(q) ||
197
+ c.ceremony.toLowerCase().includes(q) ||
198
+ p.phase.toLowerCase().includes(q)
199
+ ),
200
+ }))
201
+ .filter(p => p.agents.length > 0),
202
+ }))
203
+ .filter(c => c.phases.length > 0)
204
+ : allCeremonies;
205
+
206
+ const hasAgentsLoaded = Object.keys(agentStatus).length > 0;
207
+ const forceOpen = q.length > 0;
208
+
209
+ // Count customized agents per ceremony for the badge
210
+ const countCustomized = (ceremony) =>
211
+ ceremony.phases
212
+ .flatMap(p => p.agents)
213
+ .filter(a => agentStatus[a.slug])
214
+ .length;
215
+
216
+ return (
217
+ <div>
218
+ {/* Sticky search bar */}
219
+ <div className="sticky top-0 bg-white z-10 px-4 py-2.5 border-b border-slate-100 flex items-center justify-between gap-3">
220
+ <input
221
+ type="search"
222
+ placeholder="Search agents…"
223
+ value={search}
224
+ onChange={e => setSearch(e.target.value)}
225
+ className="w-48 rounded border border-slate-200 px-3 py-1.5 text-xs text-slate-700 focus:outline-none focus:ring-1 focus:ring-blue-500"
226
+ />
227
+ <span className="text-xs text-slate-400 italic flex-shrink-0">Click any agent to edit its prompt</span>
228
+ </div>
229
+
230
+ {/* Agent hierarchy list */}
231
+ <div className="px-4 py-4 flex flex-col gap-3">
232
+ {error && (
233
+ <p className="text-xs text-red-500">{error}</p>
234
+ )}
235
+ {filteredCeremonies.length === 0 && (
236
+ <p className="text-sm text-slate-400 py-4">No agents match your search.</p>
237
+ )}
238
+
239
+ {filteredCeremonies.map(ceremony => {
240
+ const colors = CEREMONY_COLORS[ceremony.color] || CEREMONY_COLORS.slate;
241
+ const ceremonyOpen = forceOpen || isOpen(ceremony.ceremony);
242
+ const customCount = countCustomized(ceremony);
243
+
244
+ return (
245
+ <div
246
+ key={ceremony.ceremony}
247
+ className={`rounded-xl border ${colors.border} overflow-hidden`}
248
+ >
249
+ {/* Ceremony header — clickable to collapse/expand */}
250
+ <button
251
+ type="button"
252
+ onClick={() => !forceOpen && toggle(ceremony.ceremony)}
253
+ className={`w-full flex items-center gap-2 px-4 py-2.5 border-l-4 ${colors.header} ${colors.accent} text-left`}
254
+ >
255
+ {ceremonyOpen
256
+ ? <ChevronDown className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />
257
+ : <ChevronRight className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />
258
+ }
259
+ <span className={`text-sm font-semibold flex-1 ${colors.text}`}>
260
+ {ceremony.ceremony}
261
+ </span>
262
+ {customCount > 0 && (
263
+ <span className="text-[10px] font-medium px-1.5 py-0.5 rounded bg-amber-100 text-amber-700">
264
+ {customCount} custom
265
+ </span>
266
+ )}
267
+ </button>
268
+
269
+ {/* Ceremony body — collapsible */}
270
+ {ceremonyOpen && (
271
+ <div className="divide-y divide-slate-100">
272
+ {ceremony.phases.map(phase => {
273
+ const phaseKey = `${ceremony.ceremony}::${phase.phase}`;
274
+ const phaseOpen = forceOpen || isOpen(phaseKey);
275
+ const visibleAgents = phase.agents.filter(
276
+ a => !hasAgentsLoaded || a.slug in agentStatus
277
+ );
278
+ if (visibleAgents.length === 0) return null;
279
+
280
+ return (
281
+ <div key={phase.phase}>
282
+ {/* Phase sub-header — indented, highlighted as non-leaf node */}
283
+ <button
284
+ type="button"
285
+ onClick={() => !forceOpen && toggle(phaseKey)}
286
+ className="w-full flex items-center gap-2 pl-6 pr-4 py-2 bg-slate-50 hover:bg-slate-100 transition-colors text-left"
287
+ >
288
+ {phaseOpen
289
+ ? <ChevronDown className="w-3 h-3 text-slate-400 flex-shrink-0" />
290
+ : <ChevronRight className="w-3 h-3 text-slate-400 flex-shrink-0" />
291
+ }
292
+ <span className="text-xs font-semibold text-slate-600 uppercase tracking-wide">
293
+ {phase.phase}
294
+ </span>
295
+ <span className="text-xs text-slate-400 ml-1">
296
+ {visibleAgents.length}
297
+ </span>
298
+ </button>
299
+
300
+ {/* Agent rows — further indented */}
301
+ {phaseOpen && (
302
+ <div className="pb-1">
303
+ {visibleAgents.map(agent => {
304
+ const isCustomized = agentStatus[agent.slug] ?? false;
305
+ return (
306
+ <button
307
+ key={`${ceremony.ceremony}-${agent.slug}`}
308
+ type="button"
309
+ onClick={() => setOpenAgent(agent.slug)}
310
+ className="w-full text-left pl-10 pr-4 py-1.5 hover:bg-slate-50 transition-colors flex items-center gap-3 group"
311
+ >
312
+ <div className="flex-1 min-w-0">
313
+ <span className="text-sm text-slate-700 group-hover:text-slate-900">
314
+ {agent.label}
315
+ </span>
316
+ {agent.note && (
317
+ <span className="text-xs text-slate-400 italic ml-2">
318
+ {agent.note}
319
+ </span>
320
+ )}
321
+ </div>
322
+ {isCustomized && (
323
+ <span className="flex-shrink-0 text-[10px] font-medium px-1.5 py-0.5 rounded bg-amber-100 text-amber-700">
324
+ Custom
325
+ </span>
326
+ )}
327
+ </button>
328
+ );
329
+ })}
330
+ </div>
331
+ )}
332
+ </div>
333
+ );
334
+ })}
335
+ </div>
336
+ )}
337
+ </div>
338
+ );
339
+ })}
340
+ </div>
341
+
342
+ {/* Agent editor popup */}
343
+ {openAgent && (
344
+ <AgentEditorPopup
345
+ agentName={`${openAgent}.md`}
346
+ onClose={() => setOpenAgent(null)}
347
+ onSaved={() => setAgentStatus(prev => ({ ...prev, [openAgent]: true }))}
348
+ onReset={() => setAgentStatus(prev => ({ ...prev, [openAgent]: false }))}
349
+ />
350
+ )}
351
+ </div>
352
+ );
353
+ }
@@ -0,0 +1,113 @@
1
+ import { useState } from 'react';
2
+ import { Eye, EyeOff } from 'lucide-react';
3
+ import { saveApiKeys } from '../../lib/api';
4
+
5
+ const PROVIDERS = [
6
+ { key: 'anthropic', label: 'Anthropic', envKey: 'ANTHROPIC_API_KEY', placeholder: 'sk-ant-…' },
7
+ { key: 'gemini', label: 'Google (Gemini)', envKey: 'GEMINI_API_KEY', placeholder: 'AIza…' },
8
+ { key: 'openai', label: 'OpenAI', envKey: 'OPENAI_API_KEY', placeholder: 'sk-…' },
9
+ ];
10
+
11
+ function ApiKeyRow({ provider, apiKeyInfo, onSaved }) {
12
+ const [value, setValue] = useState('');
13
+ const [showKey, setShowKey] = useState(false);
14
+ const [status, setStatus] = useState(null); // null | 'saving' | 'saved' | 'error'
15
+
16
+ const handleSave = async () => {
17
+ setStatus('saving');
18
+ try {
19
+ await saveApiKeys({ [provider.key]: value });
20
+ setStatus('saved');
21
+ setValue('');
22
+ onSaved();
23
+ setTimeout(() => setStatus(null), 2000);
24
+ } catch {
25
+ setStatus('error');
26
+ setTimeout(() => setStatus(null), 2000);
27
+ }
28
+ };
29
+
30
+ return (
31
+ <div className="flex items-center gap-3 py-3 border-b border-slate-100 last:border-0">
32
+ {/* Provider name */}
33
+ <div className="w-36 flex-shrink-0">
34
+ <p className="text-sm font-medium text-slate-800">{provider.label}</p>
35
+ <p className="text-xs text-slate-400">{provider.envKey}</p>
36
+ </div>
37
+
38
+ {/* Status badge */}
39
+ <div className="w-16 flex-shrink-0">
40
+ {apiKeyInfo.isSet ? (
41
+ <span className="inline-flex items-center gap-1 text-xs font-medium text-green-700 bg-green-50 border border-green-200 rounded-full px-2 py-0.5">
42
+ ✓ Set
43
+ </span>
44
+ ) : (
45
+ <span className="inline-flex items-center text-xs font-medium text-slate-400 bg-slate-50 border border-slate-200 rounded-full px-2 py-0.5">
46
+ Not set
47
+ </span>
48
+ )}
49
+ </div>
50
+
51
+ {/* Preview */}
52
+ {apiKeyInfo.isSet && !value && (
53
+ <p className="text-xs text-slate-400 font-mono flex-shrink-0">{apiKeyInfo.preview}</p>
54
+ )}
55
+
56
+ {/* Key input */}
57
+ <div className="flex-1 flex items-center gap-2 min-w-0">
58
+ <div className="relative flex-1">
59
+ <input
60
+ type={showKey ? 'text' : 'password'}
61
+ value={value}
62
+ onChange={(e) => setValue(e.target.value)}
63
+ placeholder={apiKeyInfo.isSet ? 'Enter new key to update…' : provider.placeholder}
64
+ className="w-full rounded-md border border-slate-300 px-2 py-1.5 pr-8 text-xs text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono"
65
+ />
66
+ <button
67
+ type="button"
68
+ onClick={() => setShowKey((v) => !v)}
69
+ className="absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600"
70
+ tabIndex={-1}
71
+ >
72
+ {showKey ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
73
+ </button>
74
+ </div>
75
+
76
+ <button
77
+ type="button"
78
+ onClick={handleSave}
79
+ disabled={!value.trim() || status === 'saving'}
80
+ className="px-3 py-1.5 text-xs font-medium bg-slate-900 text-white rounded-md hover:bg-slate-700 transition-colors disabled:opacity-40 flex-shrink-0"
81
+ >
82
+ {status === 'saving' ? (
83
+ <span className="inline-flex items-center gap-1">
84
+ <span className="w-3 h-3 border border-white/40 border-t-white rounded-full animate-spin" />
85
+ Saving
86
+ </span>
87
+ ) : status === 'saved' ? '✓ Saved' : status === 'error' ? '✗ Error' : 'Save'}
88
+ </button>
89
+ </div>
90
+ </div>
91
+ );
92
+ }
93
+
94
+ export function ApiKeysTab({ settings, onSaved }) {
95
+ return (
96
+ <div className="px-5 py-4">
97
+ <p className="text-xs text-slate-500 mb-4">
98
+ API keys are stored in your project's <code className="font-mono bg-slate-100 px-1 rounded">.env</code> file.
99
+ Enter a new key and click Save to update. Clear the field and save to remove a key.
100
+ </p>
101
+ <div>
102
+ {PROVIDERS.map((provider) => (
103
+ <ApiKeyRow
104
+ key={provider.key}
105
+ provider={provider}
106
+ apiKeyInfo={settings.apiKeys[provider.key]}
107
+ onSaved={onSaved}
108
+ />
109
+ ))}
110
+ </div>
111
+ </div>
112
+ );
113
+ }
@@ -0,0 +1,98 @@
1
+ import { useState } from 'react';
2
+ import { Workflow } from 'lucide-react';
3
+ import { saveCeremonies } from '../../lib/api';
4
+ import { CeremonyWorkflowModal } from '../ceremony/CeremonyWorkflowModal';
5
+
6
+ function humanize(str) {
7
+ return str
8
+ .replace(/-/g, ' ')
9
+ .replace(/\b\w/g, (c) => c.toUpperCase());
10
+ }
11
+
12
+ const CEREMONY_DESCRIPTIONS = {
13
+ 'sponsor-call': 'Generates mission statement and project documentation through a structured AI-guided interview.',
14
+ 'sprint-planning': 'Plans and assigns work items for the upcoming sprint based on team capacity and priorities.',
15
+ 'seed': 'Seeds the initial project work item structure from the project documentation.',
16
+ };
17
+
18
+ export function CeremonyModelsTab({ settings, models, onSaved }) {
19
+ const [ceremonies, setCeremonies] = useState(
20
+ () => JSON.parse(JSON.stringify(settings.ceremonies || []))
21
+ );
22
+ const [missionGenValidation, setMissionGenValidation] = useState(
23
+ () => JSON.parse(JSON.stringify(
24
+ settings.missionGenerator?.validation || { maxIterations: 3, acceptanceThreshold: 95 }
25
+ ))
26
+ );
27
+ const [activeWorkflow, setActiveWorkflow] = useState(null);
28
+
29
+ const handleCeremonySave = async (updatedCeremony, updatedMG) => {
30
+ const next = ceremonies.map((c) =>
31
+ c.name === updatedCeremony.name ? updatedCeremony : c
32
+ );
33
+ // Always pass missionGen params; use updated value for sponsor-call, current for others
34
+ const missionGenArg = updatedCeremony.name === 'sponsor-call'
35
+ ? { validation: updatedMG || missionGenValidation }
36
+ : { validation: missionGenValidation };
37
+ await saveCeremonies(next, missionGenArg);
38
+ setCeremonies(next);
39
+ if (updatedCeremony.name === 'sponsor-call' && updatedMG) {
40
+ setMissionGenValidation(updatedMG);
41
+ }
42
+ onSaved();
43
+ };
44
+
45
+ if (!ceremonies.length) {
46
+ return (
47
+ <div className="px-5 py-8 text-center">
48
+ <p className="text-sm text-slate-500">
49
+ No ceremony configurations found yet. Run your first ceremony from the kanban board
50
+ to populate settings here.
51
+ </p>
52
+ </div>
53
+ );
54
+ }
55
+
56
+ const activeWorkflowCeremony = ceremonies.find((c) => c.name === activeWorkflow);
57
+
58
+ return (
59
+ <div className="px-5 py-4 flex flex-col gap-3">
60
+ {ceremonies.map((ceremony) => {
61
+ const description = CEREMONY_DESCRIPTIONS[ceremony.name];
62
+ return (
63
+ <div
64
+ key={ceremony.name}
65
+ className="border border-slate-200 rounded-lg px-4 py-3 flex items-center justify-between gap-4"
66
+ >
67
+ <div className="min-w-0">
68
+ <p className="text-sm font-semibold text-slate-800">
69
+ {ceremony.displayName || humanize(ceremony.name || '')}
70
+ </p>
71
+ {description && (
72
+ <p className="text-xs text-slate-500 mt-0.5 leading-relaxed">{description}</p>
73
+ )}
74
+ </div>
75
+ <button
76
+ type="button"
77
+ onClick={() => setActiveWorkflow(ceremony.name)}
78
+ className="flex items-center gap-1.5 text-xs font-medium text-blue-600 hover:text-blue-700 bg-blue-50 hover:bg-blue-100 border border-blue-200 rounded-md px-3 py-1.5 transition-colors flex-shrink-0"
79
+ >
80
+ <Workflow className="w-3.5 h-3.5" />
81
+ Configure Models
82
+ </button>
83
+ </div>
84
+ );
85
+ })}
86
+
87
+ {activeWorkflow && activeWorkflowCeremony && (
88
+ <CeremonyWorkflowModal
89
+ ceremony={activeWorkflowCeremony}
90
+ models={models}
91
+ missionGenValidation={activeWorkflow === 'sponsor-call' ? missionGenValidation : null}
92
+ onClose={() => setActiveWorkflow(null)}
93
+ onSave={handleCeremonySave}
94
+ />
95
+ )}
96
+ </div>
97
+ );
98
+ }
@@ -0,0 +1,94 @@
1
+ import { useState } from 'react';
2
+ import { saveCostThresholds } from '../../lib/api';
3
+
4
+ const CEREMONIES = [
5
+ { key: 'sponsor-call', label: 'Sponsor Call', desc: 'Wizard to define project mission, scope, and architecture' },
6
+ { key: 'sprint-planning', label: 'Sprint Planning', desc: 'Generates epics, stories, and feature contexts' },
7
+ { key: 'seed', label: 'Seed', desc: 'Populates initial epics and stories from a seed document' },
8
+ ];
9
+
10
+ function initState(costThresholds) {
11
+ const state = {};
12
+ for (const { key } of CEREMONIES) {
13
+ const val = costThresholds?.[key];
14
+ state[key] = val != null ? String(val) : '';
15
+ }
16
+ return state;
17
+ }
18
+
19
+ export function CostThresholdsTab({ settings, onSaved }) {
20
+ const [values, setValues] = useState(() => initState(settings.costThresholds));
21
+ const [status, setStatus] = useState(null); // null | 'saving' | 'saved' | 'error'
22
+
23
+ const update = (key, val) => {
24
+ setValues((prev) => ({ ...prev, [key]: val }));
25
+ };
26
+
27
+ const handleSave = async () => {
28
+ setStatus('saving');
29
+ try {
30
+ const payload = {};
31
+ for (const { key } of CEREMONIES) {
32
+ const raw = values[key].trim();
33
+ payload[key] = raw === '' ? null : parseFloat(raw) || null;
34
+ }
35
+ await saveCostThresholds(payload);
36
+ setStatus('saved');
37
+ onSaved();
38
+ setTimeout(() => setStatus(null), 2000);
39
+ } catch {
40
+ setStatus('error');
41
+ setTimeout(() => setStatus(null), 2000);
42
+ }
43
+ };
44
+
45
+ return (
46
+ <div className="px-5 py-4 flex flex-col gap-4">
47
+ <p className="text-xs text-slate-500">
48
+ Set a maximum spend (in <strong>USD</strong>) per ceremony run. Leave empty for unlimited.
49
+ When the running cost exceeds the limit, the ceremony stops automatically.
50
+ </p>
51
+
52
+ <div className="flex flex-col gap-2">
53
+ {CEREMONIES.map(({ key, label, desc }) => (
54
+ <div key={key} className="border border-slate-200 rounded-lg p-4">
55
+ <div className="flex items-start justify-between gap-4">
56
+ <div className="flex-1 min-w-0">
57
+ <div className="text-sm font-semibold text-slate-800">{label}</div>
58
+ <div className="text-xs text-slate-500 mt-0.5">{desc}</div>
59
+ </div>
60
+ <div className="flex items-center gap-1.5 shrink-0">
61
+ <span className="text-sm text-slate-500">$</span>
62
+ <input
63
+ type="number"
64
+ min="0"
65
+ step="0.01"
66
+ value={values[key]}
67
+ onChange={(e) => update(key, e.target.value)}
68
+ placeholder="Unlimited"
69
+ className="rounded-md border border-slate-300 px-2 py-1.5 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-blue-500 w-28 placeholder:text-slate-400"
70
+ />
71
+ </div>
72
+ </div>
73
+ </div>
74
+ ))}
75
+ </div>
76
+
77
+ <div className="flex justify-end pt-1">
78
+ <button
79
+ type="button"
80
+ onClick={handleSave}
81
+ disabled={status === 'saving'}
82
+ className="px-4 py-2 text-sm font-medium bg-slate-900 text-white rounded-md hover:bg-slate-700 transition-colors disabled:opacity-40"
83
+ >
84
+ {status === 'saving' ? (
85
+ <span className="inline-flex items-center gap-2">
86
+ <span className="w-3.5 h-3.5 border border-white/40 border-t-white rounded-full animate-spin" />
87
+ Saving…
88
+ </span>
89
+ ) : status === 'saved' ? '✓ Saved' : status === 'error' ? '✗ Error' : 'Save Cost Limits'}
90
+ </button>
91
+ </div>
92
+ </div>
93
+ );
94
+ }