@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,789 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { Wand2, Check, X, ChevronDown, ChevronRight, AlertTriangle } from 'lucide-react';
3
+ import { getModels, getSettings, refineWorkItem, applyWorkItemChanges } from '../../lib/api';
4
+
5
+ function normalizeProvider(p = '') {
6
+ if (p === 'claude') return 'anthropic';
7
+ return p;
8
+ }
9
+
10
+ function ModelSelect({ label, value, onChange, models, disabled }) {
11
+ const providers = [...new Set(models.map((m) => m.provider))];
12
+ const chosen = models.find((m) => m.modelId === value);
13
+ return (
14
+ <div>
15
+ <label className="block text-xs font-medium text-slate-500 mb-1">{label}</label>
16
+ <select
17
+ value={value}
18
+ onChange={(e) => onChange(e.target.value)}
19
+ disabled={disabled || models.length === 0}
20
+ className="w-full rounded-md border border-slate-300 px-2 py-1.5 text-xs text-slate-900 focus:outline-none focus:ring-2 focus:ring-violet-500 disabled:opacity-60 bg-white"
21
+ >
22
+ {models.length === 0 && <option value="">Loading…</option>}
23
+ {providers.map((p) => (
24
+ <optgroup key={p} label={p.charAt(0).toUpperCase() + p.slice(1)}>
25
+ {models.filter((m) => m.provider === p).map((m) => (
26
+ <option key={m.modelId} value={m.modelId}>
27
+ {m.displayName}{!m.hasApiKey ? ' (no key)' : ''}
28
+ </option>
29
+ ))}
30
+ </optgroup>
31
+ ))}
32
+ </select>
33
+ {chosen && !chosen.hasApiKey && (
34
+ <p className="text-xs text-amber-600 mt-0.5">
35
+ ⚠ No API key — add to <code>.env</code>
36
+ </p>
37
+ )}
38
+ </div>
39
+ );
40
+ }
41
+
42
+ function FieldDiff({ label, before, after }) {
43
+ const beforeStr = Array.isArray(before) ? before.join('\n') : (before ?? '');
44
+ const afterStr = Array.isArray(after) ? after.join('\n') : (after ?? '');
45
+ if (!beforeStr && !afterStr) return null;
46
+ if (beforeStr === afterStr) return null;
47
+ return (
48
+ <div className="mb-3">
49
+ <p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-1">{label}</p>
50
+ {beforeStr && (
51
+ <div className="mb-1 px-2.5 py-2 bg-red-50 border border-red-100 rounded text-xs text-red-700 leading-relaxed line-through">
52
+ {beforeStr}
53
+ </div>
54
+ )}
55
+ {afterStr && (
56
+ <div className="px-2.5 py-2 bg-green-50 border border-green-100 rounded text-xs text-green-700 leading-relaxed">
57
+ {afterStr}
58
+ </div>
59
+ )}
60
+ </div>
61
+ );
62
+ }
63
+
64
+ function StoryUpdateCard({ impact, checked, onToggle }) {
65
+ const [expanded, setExpanded] = useState(false);
66
+ return (
67
+ <div className={`border rounded-lg overflow-hidden transition-colors ${
68
+ checked ? 'border-violet-300 bg-violet-50' : 'border-slate-200 bg-white'
69
+ }`}>
70
+ <div className="flex items-center gap-2.5 px-3 py-2.5">
71
+ <input
72
+ type="checkbox"
73
+ checked={checked}
74
+ onChange={onToggle}
75
+ className="flex-shrink-0 accent-violet-600"
76
+ />
77
+ <div className="flex-1 min-w-0">
78
+ <p className="text-xs font-medium text-slate-800 truncate">
79
+ {impact.proposedStory?.name ?? impact.storyId}
80
+ </p>
81
+ <p className="text-xs text-slate-400 truncate">{impact.storyId}</p>
82
+ </div>
83
+ {impact.changesNeeded && (
84
+ <button
85
+ type="button"
86
+ onClick={() => setExpanded((v) => !v)}
87
+ className="flex-shrink-0 text-slate-400 hover:text-slate-600"
88
+ >
89
+ {expanded
90
+ ? <ChevronDown className="w-3.5 h-3.5" />
91
+ : <ChevronRight className="w-3.5 h-3.5" />}
92
+ </button>
93
+ )}
94
+ </div>
95
+ {expanded && impact.changesNeeded && (
96
+ <div className="px-3 pb-3 pt-1 border-t border-slate-100">
97
+ <p className="text-xs text-slate-600 leading-relaxed">{impact.changesNeeded}</p>
98
+ </div>
99
+ )}
100
+ </div>
101
+ );
102
+ }
103
+
104
+ function NewStoryCard({ story, checked, onToggle }) {
105
+ const [expanded, setExpanded] = useState(false);
106
+ const acs = story?.acceptance ?? story?.acceptanceCriteria ?? [];
107
+ return (
108
+ <div className={`border rounded-lg overflow-hidden transition-colors ${
109
+ checked ? 'border-emerald-300 bg-emerald-50' : 'border-slate-200 bg-white'
110
+ }`}>
111
+ <div className="flex items-center gap-2.5 px-3 py-2.5">
112
+ <input
113
+ type="checkbox"
114
+ checked={checked}
115
+ onChange={onToggle}
116
+ className="flex-shrink-0 accent-emerald-600"
117
+ />
118
+ <div className="flex-1 min-w-0">
119
+ <div className="flex items-center gap-1.5 mb-0.5">
120
+ <span className="flex-shrink-0 text-xs font-bold px-1.5 py-0.5 rounded bg-emerald-100 text-emerald-700">
121
+ NEW
122
+ </span>
123
+ <p className="text-xs font-medium text-slate-800 truncate">{story?.name}</p>
124
+ </div>
125
+ {story?.description && (
126
+ <p className="text-xs text-slate-500 truncate">{story.description}</p>
127
+ )}
128
+ </div>
129
+ <button
130
+ type="button"
131
+ onClick={() => setExpanded((v) => !v)}
132
+ className="flex-shrink-0 text-slate-400 hover:text-slate-600"
133
+ >
134
+ {expanded
135
+ ? <ChevronDown className="w-3.5 h-3.5" />
136
+ : <ChevronRight className="w-3.5 h-3.5" />}
137
+ </button>
138
+ </div>
139
+ {expanded && (
140
+ <div className="px-3 pb-3 pt-1 border-t border-slate-100 space-y-2">
141
+ {story?.description && (
142
+ <div>
143
+ <p className="text-xs font-medium text-slate-500 mb-0.5">Description</p>
144
+ <p className="text-xs text-slate-700 leading-relaxed">{story.description}</p>
145
+ </div>
146
+ )}
147
+ {acs.length > 0 && (
148
+ <div>
149
+ <p className="text-xs font-medium text-slate-500 mb-0.5">Acceptance Criteria</p>
150
+ <ul className="space-y-0.5">
151
+ {acs.map((ac, i) => (
152
+ <li key={i} className="flex items-start gap-1 text-xs text-slate-700">
153
+ <span className="text-slate-400 mt-0.5 flex-shrink-0">•</span>
154
+ <span>{ac}</span>
155
+ </li>
156
+ ))}
157
+ </ul>
158
+ </div>
159
+ )}
160
+ </div>
161
+ )}
162
+ </div>
163
+ );
164
+ }
165
+
166
+ const SEVERITY_COLOR = {
167
+ critical: 'text-red-600',
168
+ major: 'text-orange-600',
169
+ minor: 'text-amber-600',
170
+ };
171
+ const SEVERITY_BG = {
172
+ critical: 'bg-red-50 border-red-100',
173
+ major: 'bg-orange-50 border-orange-100',
174
+ minor: 'bg-amber-50 border-amber-100',
175
+ };
176
+
177
+ function scoreBadgeClass(score) {
178
+ if (score >= 95) return 'bg-green-100 text-green-700';
179
+ if (score >= 80) return 'bg-amber-100 text-amber-700';
180
+ return 'bg-red-100 text-red-700';
181
+ }
182
+
183
+ /**
184
+ * RefineWorkItemPopup
185
+ * Three-phase popup:
186
+ * configure → running (while LLM runs) → results (accept / discard)
187
+ *
188
+ * Props:
189
+ * item - full work item from CardDetailModal (includes metadata.validationResult)
190
+ * refineProgress - { itemId, jobId, message } from WS, or null
191
+ * refineResult - { itemId, jobId, result } from WS, or null
192
+ * refineError - { itemId, jobId, error } from WS, or null
193
+ * onClose() - close popup without accepting
194
+ * onAccepted() - called after successful apply (triggers detail reload)
195
+ */
196
+ export function RefineWorkItemPopup({
197
+ item,
198
+ refineProgress,
199
+ refineResult,
200
+ refineError,
201
+ onClose,
202
+ onAccepted,
203
+ }) {
204
+ // ── Configure state ─────────────────────────────────────────────────────────
205
+ const vr = item?.metadata?.validationResult;
206
+ const allIssues = [
207
+ ...(vr?.criticalIssues || []).map((i) => ({ ...i, severity: 'critical' })),
208
+ ...(vr?.majorIssues || []).map((i) => ({ ...i, severity: 'major' })),
209
+ ...(vr?.minorIssues || []).map((i) => ({ ...i, severity: 'minor' })),
210
+ ];
211
+
212
+ // Pre-select all critical issues
213
+ const [selectedIssueIndices, setSelectedIssueIndices] = useState(
214
+ () => new Set(
215
+ allIssues
216
+ .map((issue, i) => (issue.severity === 'critical' ? i : null))
217
+ .filter((i) => i !== null)
218
+ )
219
+ );
220
+ const [refinementRequest, setRefinementRequest] = useState('');
221
+ const [models, setModels] = useState([]);
222
+ const [selectedModelId, setSelectedModelId] = useState('');
223
+ const [selectedValidatorModelId, setSelectedValidatorModelId] = useState('');
224
+ const [configError, setConfigError] = useState('');
225
+
226
+ // Local "refine started, waiting for first WS progress" state
227
+ const [refining, setRefining] = useState(false);
228
+
229
+ // ── Running state ───────────────────────────────────────────────────────────
230
+ const [progressLog, setProgressLog] = useState([]);
231
+ const progressEndRef = useRef(null);
232
+
233
+ // ── Results state ───────────────────────────────────────────────────────────
234
+ const [storyCheckboxes, setStoryCheckboxes] = useState(null); // { [storyImpactIndex]: boolean }
235
+ const [accepting, setAccepting] = useState(false);
236
+ const [acceptError, setAcceptError] = useState('');
237
+
238
+ // ── Phase derivation ────────────────────────────────────────────────────────
239
+ const phase = refineResult
240
+ ? 'results'
241
+ : refining || refineProgress
242
+ ? 'running'
243
+ : 'configure';
244
+
245
+ // ── Effects ─────────────────────────────────────────────────────────────────
246
+
247
+ // Load models on mount
248
+ useEffect(() => {
249
+ Promise.all([getModels(), getSettings()])
250
+ .then(([data, settings]) => {
251
+ setModels(data);
252
+ const firstReady = data.find(
253
+ (m) => settings.apiKeys?.[normalizeProvider(m.provider)]?.isSet
254
+ );
255
+ const genId = firstReady
256
+ ? firstReady.modelId
257
+ : data.length > 0
258
+ ? data[0].modelId
259
+ : '';
260
+ setSelectedModelId(genId);
261
+ const otherReady = data.find(
262
+ (m) =>
263
+ settings.apiKeys?.[normalizeProvider(m.provider)]?.isSet &&
264
+ m.modelId !== genId
265
+ );
266
+ setSelectedValidatorModelId(otherReady ? otherReady.modelId : genId);
267
+ })
268
+ .catch(() => setConfigError('Failed to load models.'));
269
+ }, []);
270
+
271
+ // Accumulate progress messages
272
+ useEffect(() => {
273
+ if (refineProgress?.message) {
274
+ setProgressLog((prev) => [...prev, refineProgress.message]);
275
+ }
276
+ }, [refineProgress]);
277
+
278
+ // Auto-scroll progress log
279
+ useEffect(() => {
280
+ progressEndRef.current?.scrollIntoView({ behavior: 'smooth' });
281
+ }, [progressLog]);
282
+
283
+ // When result arrives, reset refining and initialise story checkboxes
284
+ useEffect(() => {
285
+ if (!refineResult) return;
286
+ setRefining(false);
287
+ const impacts = refineResult.result?.storyImpacts || [];
288
+ const init = {};
289
+ impacts.forEach((impact, i) => {
290
+ // Pre-check: all impacted updates and all new stories
291
+ init[i] = impact.type === 'new' || impact.impacted === true;
292
+ });
293
+ setStoryCheckboxes(init);
294
+ }, [refineResult]);
295
+
296
+ // When error arrives, go back to configure with the error message shown
297
+ useEffect(() => {
298
+ if (!refineError) return;
299
+ setRefining(false);
300
+ setConfigError(
301
+ typeof refineError.error === 'string'
302
+ ? refineError.error
303
+ : 'Refinement failed — please try again.'
304
+ );
305
+ }, [refineError]);
306
+
307
+ // ── Handlers ─────────────────────────────────────────────────────────────────
308
+
309
+ function toggleIssue(i) {
310
+ setSelectedIssueIndices((prev) => {
311
+ const next = new Set(prev);
312
+ if (next.has(i)) next.delete(i);
313
+ else next.add(i);
314
+ return next;
315
+ });
316
+ }
317
+
318
+ async function handleRefine() {
319
+ const selectedModel = models.find((m) => m.modelId === selectedModelId);
320
+ const selectedValidatorModel = models.find(
321
+ (m) => m.modelId === selectedValidatorModelId
322
+ );
323
+ if (!selectedModel || !selectedValidatorModel) return;
324
+
325
+ const selectedIssues = allIssues.filter((_, i) => selectedIssueIndices.has(i));
326
+
327
+ setRefining(true);
328
+ setConfigError('');
329
+ setProgressLog([]);
330
+
331
+ try {
332
+ await refineWorkItem(item.id, {
333
+ refinementRequest,
334
+ selectedIssues,
335
+ modelId: selectedModelId,
336
+ provider: selectedModel.provider,
337
+ validatorModelId: selectedValidatorModelId,
338
+ validatorProvider: selectedValidatorModel.provider,
339
+ });
340
+ // Job started — WebSocket broadcasts refine:progress / refine:complete / refine:error
341
+ } catch (err) {
342
+ setRefining(false);
343
+ setConfigError(err.message || 'Failed to start refinement.');
344
+ }
345
+ }
346
+
347
+ async function handleAccept() {
348
+ if (!refineResult) return;
349
+ const { proposedItem, storyImpacts = [] } = refineResult.result;
350
+
351
+ const acceptedStoryChanges = storyImpacts
352
+ .filter((_, i) => storyCheckboxes?.[i])
353
+ .map((impact) => ({
354
+ type: impact.type,
355
+ storyId: impact.storyId ?? null,
356
+ proposedStory: impact.proposedStory,
357
+ }));
358
+
359
+ setAccepting(true);
360
+ setAcceptError('');
361
+ try {
362
+ await applyWorkItemChanges(item.id, proposedItem, acceptedStoryChanges);
363
+ onAccepted?.();
364
+ } catch (err) {
365
+ setAcceptError(err.message || 'Failed to apply changes.');
366
+ setAccepting(false);
367
+ }
368
+ }
369
+
370
+ // ── Derived values ──────────────────────────────────────────────────────────
371
+
372
+ const selectedModel = models.find((m) => m.modelId === selectedModelId);
373
+ const selectedValidatorModel = models.find(
374
+ (m) => m.modelId === selectedValidatorModelId
375
+ );
376
+ const canRefine =
377
+ !refining &&
378
+ !!selectedModelId &&
379
+ !!selectedValidatorModelId &&
380
+ !!selectedModel &&
381
+ !!selectedValidatorModel;
382
+ const selectedIssueCount = selectedIssueIndices.size;
383
+
384
+ // Results phase derived values
385
+ const resultData = refineResult?.result;
386
+ const storyImpacts = resultData?.storyImpacts || [];
387
+ const updateImpacts = storyImpacts
388
+ .map((impact, i) => ({ impact, i }))
389
+ .filter(({ impact }) => impact.type === 'update' && impact.impacted);
390
+ const newImpacts = storyImpacts
391
+ .map((impact, i) => ({ impact, i }))
392
+ .filter(({ impact }) => impact.type === 'new');
393
+
394
+ const checkedStoryCount = Object.values(storyCheckboxes || {}).filter(Boolean).length;
395
+ const isEpic = item.type === 'epic';
396
+
397
+ // ── Render ──────────────────────────────────────────────────────────────────
398
+
399
+ return (
400
+ <div
401
+ className="fixed inset-0 z-[80] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
402
+ onClick={(e) => {
403
+ if (e.target === e.currentTarget && phase !== 'running') onClose();
404
+ }}
405
+ >
406
+ <div
407
+ className={`bg-white rounded-2xl shadow-2xl flex flex-col overflow-hidden w-full transition-all duration-200 ${
408
+ phase === 'results' ? 'max-w-2xl' : 'max-w-xl'
409
+ }`}
410
+ style={{ maxHeight: '88vh' }}
411
+ >
412
+ {/* ── Header ─────────────────────────────────────────────────────────── */}
413
+ <div className="flex items-center justify-between px-5 py-3 border-b border-slate-100 flex-shrink-0">
414
+ <div>
415
+ <h3 className="text-sm font-semibold text-slate-900 flex items-center gap-1.5">
416
+ <Wand2 className="w-3.5 h-3.5 text-violet-600" />
417
+ Refine with AI
418
+ {phase !== 'configure' && (
419
+ <span
420
+ className={`ml-1 text-xs font-medium px-1.5 py-0.5 rounded ${
421
+ phase === 'running'
422
+ ? 'bg-blue-100 text-blue-700'
423
+ : 'bg-violet-100 text-violet-700'
424
+ }`}
425
+ >
426
+ {phase === 'running' ? 'Running…' : 'Results'}
427
+ </span>
428
+ )}
429
+ </h3>
430
+ <p className="text-xs text-slate-400 mt-0.5 truncate max-w-xs">{item.name}</p>
431
+ </div>
432
+ {phase !== 'running' && (
433
+ <button
434
+ type="button"
435
+ onClick={onClose}
436
+ className="text-slate-400 hover:text-slate-600 transition-colors ml-4 flex-shrink-0"
437
+ aria-label="Close"
438
+ >
439
+ <X className="w-4 h-4" />
440
+ </button>
441
+ )}
442
+ </div>
443
+
444
+ {/* ── CONFIGURE PHASE ────────────────────────────────────────────────── */}
445
+ {phase === 'configure' && (
446
+ <div className="flex-1 overflow-y-auto px-5 py-4 space-y-4 min-h-0">
447
+ {/* Issues checklist */}
448
+ {allIssues.length > 0 ? (
449
+ <div>
450
+ <div className="flex items-center justify-between mb-2">
451
+ <p className="text-xs font-semibold text-slate-600 uppercase tracking-wide">
452
+ Validation Issues ({allIssues.length})
453
+ </p>
454
+ <div className="flex items-center gap-2 text-xs">
455
+ <button
456
+ type="button"
457
+ onClick={() =>
458
+ setSelectedIssueIndices(
459
+ new Set(allIssues.map((_, i) => i))
460
+ )
461
+ }
462
+ className="text-violet-600 hover:text-violet-800 transition-colors"
463
+ >
464
+ All
465
+ </button>
466
+ <span className="text-slate-300">|</span>
467
+ <button
468
+ type="button"
469
+ onClick={() => setSelectedIssueIndices(new Set())}
470
+ className="text-slate-400 hover:text-slate-600 transition-colors"
471
+ >
472
+ None
473
+ </button>
474
+ </div>
475
+ </div>
476
+ <ul className="space-y-1.5 max-h-52 overflow-y-auto pr-0.5">
477
+ {allIssues.map((issue, i) => (
478
+ <li
479
+ key={i}
480
+ onClick={() => toggleIssue(i)}
481
+ className={`rounded-lg border px-3 py-2 cursor-pointer flex items-start gap-2.5 transition-shadow ${
482
+ selectedIssueIndices.has(i) ? 'ring-2 ring-violet-400' : ''
483
+ } ${SEVERITY_BG[issue.severity] ?? 'bg-slate-50 border-slate-100'}`}
484
+ >
485
+ <input
486
+ type="checkbox"
487
+ checked={selectedIssueIndices.has(i)}
488
+ onChange={() => toggleIssue(i)}
489
+ onClick={(e) => e.stopPropagation()}
490
+ className="mt-0.5 flex-shrink-0 accent-violet-600"
491
+ />
492
+ <div className="flex-1 min-w-0">
493
+ <span
494
+ className={`text-xs font-semibold uppercase ${
495
+ SEVERITY_COLOR[issue.severity] ?? 'text-slate-500'
496
+ }`}
497
+ >
498
+ {issue.severity}
499
+ </span>
500
+ <p className="text-xs text-slate-800 mt-0.5 leading-snug">
501
+ {issue.description}
502
+ </p>
503
+ {issue.suggestion && (
504
+ <p className="text-xs text-slate-500 mt-0.5 leading-snug">
505
+ <span className="font-medium">Suggestion:</span>{' '}
506
+ {issue.suggestion}
507
+ </p>
508
+ )}
509
+ </div>
510
+ </li>
511
+ ))}
512
+ </ul>
513
+ </div>
514
+ ) : (
515
+ <div className="flex items-center gap-2 px-3 py-2.5 bg-green-50 border border-green-100 rounded-lg">
516
+ <Check className="w-3.5 h-3.5 text-green-600 flex-shrink-0" />
517
+ <p className="text-xs text-green-700">
518
+ No validation issues found — you can still refine with a custom request.
519
+ </p>
520
+ </div>
521
+ )}
522
+
523
+ {/* Free-text request */}
524
+ <div>
525
+ <label className="block text-xs font-medium text-slate-500 mb-1">
526
+ Refinement request{' '}
527
+ <span className="text-slate-400">(optional)</span>
528
+ </label>
529
+ <textarea
530
+ value={refinementRequest}
531
+ onChange={(e) => setRefinementRequest(e.target.value)}
532
+ rows={3}
533
+ placeholder={
534
+ isEpic
535
+ ? 'E.g. Sharpen the scope, add examples, expand the features list…'
536
+ : 'E.g. Make acceptance criteria more testable, add edge cases…'
537
+ }
538
+ className="w-full rounded-md border border-slate-300 px-2 py-1.5 text-xs text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-500 resize-none"
539
+ />
540
+ </div>
541
+
542
+ {/* Model selectors */}
543
+ <div className="grid grid-cols-2 gap-3">
544
+ <ModelSelect
545
+ label="Generator Model"
546
+ value={selectedModelId}
547
+ onChange={setSelectedModelId}
548
+ models={models}
549
+ disabled={false}
550
+ />
551
+ <ModelSelect
552
+ label="Validator Model"
553
+ value={selectedValidatorModelId}
554
+ onChange={setSelectedValidatorModelId}
555
+ models={models}
556
+ disabled={false}
557
+ />
558
+ </div>
559
+
560
+ {configError && (
561
+ <div className="flex items-start gap-2 px-3 py-2 bg-red-50 border border-red-100 rounded-lg">
562
+ <AlertTriangle className="w-3.5 h-3.5 text-red-500 flex-shrink-0 mt-0.5" />
563
+ <p className="text-xs text-red-700">{configError}</p>
564
+ </div>
565
+ )}
566
+ </div>
567
+ )}
568
+
569
+ {/* ── RUNNING PHASE ──────────────────────────────────────────────────── */}
570
+ {phase === 'running' && (
571
+ <div className="flex-1 flex flex-col overflow-hidden px-5 py-4 min-h-0">
572
+ <div className="flex flex-col items-center gap-3 mb-4 flex-shrink-0">
573
+ <span
574
+ className="w-8 h-8 border-2 border-violet-200 border-t-violet-600 rounded-full animate-spin"
575
+ />
576
+ <p className="text-sm font-medium text-slate-700">
577
+ Refining {item.type}…
578
+ </p>
579
+ </div>
580
+ <div className="flex-1 overflow-y-auto bg-slate-50 rounded-lg px-3 py-2.5 space-y-1 min-h-0">
581
+ {progressLog.length === 0 ? (
582
+ <p className="text-xs text-slate-400 italic">Starting…</p>
583
+ ) : (
584
+ progressLog.map((msg, i) => (
585
+ <p key={i} className="text-xs text-slate-600 leading-snug">
586
+ {msg}
587
+ </p>
588
+ ))
589
+ )}
590
+ <div ref={progressEndRef} />
591
+ </div>
592
+ </div>
593
+ )}
594
+
595
+ {/* ── RESULTS PHASE ──────────────────────────────────────────────────── */}
596
+ {phase === 'results' && resultData && (
597
+ <div className="flex-1 overflow-y-auto px-5 py-4 space-y-5 min-h-0">
598
+ {/* Score comparison */}
599
+ {(() => {
600
+ const oldScore = item?.metadata?.validationResult?.averageScore;
601
+ const newScore =
602
+ resultData.validationResult?.averageScore ??
603
+ resultData.proposedItem?.metadata?.validationResult?.averageScore;
604
+ if (newScore == null) return null;
605
+ return (
606
+ <div className="flex items-center gap-3">
607
+ <p className="text-xs font-semibold text-slate-500 uppercase tracking-wide">
608
+ Validation Score
609
+ </p>
610
+ <div className="flex items-center gap-2">
611
+ {oldScore != null && (
612
+ <>
613
+ <span
614
+ className={`text-xs font-bold px-2 py-0.5 rounded-full ${scoreBadgeClass(oldScore)}`}
615
+ >
616
+ {oldScore}/100
617
+ </span>
618
+ <span className="text-slate-400 text-xs">→</span>
619
+ </>
620
+ )}
621
+ <span
622
+ className={`text-xs font-bold px-2 py-0.5 rounded-full ${scoreBadgeClass(newScore)}`}
623
+ >
624
+ {newScore}/100
625
+ </span>
626
+ </div>
627
+ </div>
628
+ );
629
+ })()}
630
+
631
+ {/* Field diffs */}
632
+ <div>
633
+ <p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">
634
+ Proposed Changes
635
+ </p>
636
+ <FieldDiff
637
+ label="Description"
638
+ before={resultData.originalItem?.description}
639
+ after={resultData.proposedItem?.description}
640
+ />
641
+ {isEpic && (
642
+ <FieldDiff
643
+ label="Features"
644
+ before={resultData.originalItem?.features}
645
+ after={resultData.proposedItem?.features}
646
+ />
647
+ )}
648
+ {!isEpic && (
649
+ <FieldDiff
650
+ label="Acceptance Criteria"
651
+ before={resultData.originalItem?.acceptance ?? resultData.originalItem?.acceptanceCriteria}
652
+ after={resultData.proposedItem?.acceptance ?? resultData.proposedItem?.acceptanceCriteria}
653
+ />
654
+ )}
655
+ {resultData.originalItem?.description ===
656
+ resultData.proposedItem?.description &&
657
+ !resultData.originalItem?.features &&
658
+ !resultData.originalItem?.acceptanceCriteria && (
659
+ <p className="text-xs text-slate-400 italic">
660
+ No textual changes detected — check dependencies or metadata.
661
+ </p>
662
+ )}
663
+ </div>
664
+
665
+ {/* Existing stories to update */}
666
+ {updateImpacts.length > 0 && (
667
+ <div>
668
+ <p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">
669
+ Existing Stories to Update ({updateImpacts.length})
670
+ </p>
671
+ <div className="space-y-2">
672
+ {updateImpacts.map(({ impact, i }) => (
673
+ <StoryUpdateCard
674
+ key={i}
675
+ impact={impact}
676
+ checked={storyCheckboxes?.[i] ?? true}
677
+ onToggle={() =>
678
+ setStoryCheckboxes((prev) => ({
679
+ ...prev,
680
+ [i]: !prev?.[i],
681
+ }))
682
+ }
683
+ />
684
+ ))}
685
+ </div>
686
+ </div>
687
+ )}
688
+
689
+ {/* New stories to add */}
690
+ {newImpacts.length > 0 && (
691
+ <div>
692
+ <p className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2">
693
+ New Stories to Add ({newImpacts.length})
694
+ </p>
695
+ <div className="space-y-2">
696
+ {newImpacts.map(({ impact, i }) => (
697
+ <NewStoryCard
698
+ key={i}
699
+ story={impact.proposedStory}
700
+ checked={storyCheckboxes?.[i] ?? true}
701
+ onToggle={() =>
702
+ setStoryCheckboxes((prev) => ({
703
+ ...prev,
704
+ [i]: !prev?.[i],
705
+ }))
706
+ }
707
+ />
708
+ ))}
709
+ </div>
710
+ </div>
711
+ )}
712
+
713
+ {acceptError && (
714
+ <div className="flex items-start gap-2 px-3 py-2 bg-red-50 border border-red-100 rounded-lg">
715
+ <AlertTriangle className="w-3.5 h-3.5 text-red-500 flex-shrink-0 mt-0.5" />
716
+ <p className="text-xs text-red-700">{acceptError}</p>
717
+ </div>
718
+ )}
719
+ </div>
720
+ )}
721
+
722
+ {/* ── Footer ─────────────────────────────────────────────────────────── */}
723
+ <div className="px-5 py-3 border-t border-slate-100 flex-shrink-0 flex items-center justify-between gap-2">
724
+ {phase === 'configure' && (
725
+ <>
726
+ <button
727
+ type="button"
728
+ onClick={onClose}
729
+ className="px-4 py-1.5 text-xs text-slate-600 border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors"
730
+ >
731
+ Cancel
732
+ </button>
733
+ <button
734
+ type="button"
735
+ onClick={handleRefine}
736
+ disabled={!canRefine}
737
+ className="flex items-center gap-1.5 px-4 py-1.5 text-xs font-medium text-white bg-violet-600 rounded-lg hover:bg-violet-700 transition-colors disabled:opacity-40"
738
+ >
739
+ <Wand2 className="w-3.5 h-3.5" />
740
+ {selectedIssueCount > 0
741
+ ? `Refine (${selectedIssueCount} issue${selectedIssueCount !== 1 ? 's' : ''})`
742
+ : 'Refine'}
743
+ </button>
744
+ </>
745
+ )}
746
+
747
+ {phase === 'running' && (
748
+ <p className="w-full text-center text-xs text-slate-400 italic">
749
+ Waiting for LLM response — please keep this window open.
750
+ </p>
751
+ )}
752
+
753
+ {phase === 'results' && (
754
+ <>
755
+ <button
756
+ type="button"
757
+ onClick={onClose}
758
+ disabled={accepting}
759
+ className="px-4 py-1.5 text-xs text-slate-600 border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors disabled:opacity-40"
760
+ >
761
+ Discard
762
+ </button>
763
+ <button
764
+ type="button"
765
+ onClick={handleAccept}
766
+ disabled={accepting}
767
+ className="flex items-center gap-1.5 px-4 py-1.5 text-xs font-medium text-white bg-violet-600 rounded-lg hover:bg-violet-700 transition-colors disabled:opacity-40"
768
+ >
769
+ {accepting ? (
770
+ <>
771
+ <span className="w-3 h-3 border border-white/40 border-t-white rounded-full animate-spin" />
772
+ Applying…
773
+ </>
774
+ ) : (
775
+ <>
776
+ <Check className="w-3.5 h-3.5" />
777
+ {checkedStoryCount > 0
778
+ ? `Accept + ${checkedStoryCount} story change${checkedStoryCount !== 1 ? 's' : ''}`
779
+ : 'Accept Changes'}
780
+ </>
781
+ )}
782
+ </button>
783
+ </>
784
+ )}
785
+ </div>
786
+ </div>
787
+ </div>
788
+ );
789
+ }