@agile-vibe-coding/avc 0.1.1 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. package/cli/agent-loader.js +21 -0
  2. package/cli/agents/agent-selector.md +152 -0
  3. package/cli/agents/architecture-recommender.md +418 -0
  4. package/cli/agents/code-implementer.md +117 -0
  5. package/cli/agents/code-validator.md +80 -0
  6. package/cli/agents/context-reviewer-epic.md +101 -0
  7. package/cli/agents/context-reviewer-story.md +92 -0
  8. package/cli/agents/context-writer-epic.md +145 -0
  9. package/cli/agents/context-writer-story.md +111 -0
  10. package/cli/agents/database-deep-dive.md +470 -0
  11. package/cli/agents/database-recommender.md +634 -0
  12. package/cli/agents/doc-distributor.md +176 -0
  13. package/cli/agents/doc-writer-epic.md +42 -0
  14. package/cli/agents/doc-writer-story.md +43 -0
  15. package/cli/agents/documentation-updater.md +203 -0
  16. package/cli/agents/duplicate-detector.md +110 -0
  17. package/cli/agents/epic-story-decomposer.md +559 -0
  18. package/cli/agents/feature-context-generator.md +91 -0
  19. package/cli/agents/gap-checker-epic.md +52 -0
  20. package/cli/agents/impact-checker-story.md +51 -0
  21. package/cli/agents/migration-guide-generator.md +305 -0
  22. package/cli/agents/mission-scope-generator.md +143 -0
  23. package/cli/agents/mission-scope-validator.md +146 -0
  24. package/cli/agents/project-context-extractor.md +122 -0
  25. package/cli/agents/project-documentation-creator.json +226 -0
  26. package/cli/agents/project-documentation-creator.md +595 -0
  27. package/cli/agents/question-prefiller.md +269 -0
  28. package/cli/agents/refiner-epic.md +39 -0
  29. package/cli/agents/refiner-story.md +42 -0
  30. package/cli/agents/scaffolding-generator.md +99 -0
  31. package/cli/agents/seed-validator.md +71 -0
  32. package/cli/agents/story-doc-enricher.md +133 -0
  33. package/cli/agents/story-scope-reviewer.md +147 -0
  34. package/cli/agents/story-splitter.md +83 -0
  35. package/cli/agents/suggestion-business-analyst.md +88 -0
  36. package/cli/agents/suggestion-deployment-architect.md +263 -0
  37. package/cli/agents/suggestion-product-manager.md +129 -0
  38. package/cli/agents/suggestion-security-specialist.md +156 -0
  39. package/cli/agents/suggestion-technical-architect.md +269 -0
  40. package/cli/agents/suggestion-ux-researcher.md +93 -0
  41. package/cli/agents/task-subtask-decomposer.md +188 -0
  42. package/cli/agents/validator-documentation.json +183 -0
  43. package/cli/agents/validator-documentation.md +455 -0
  44. package/cli/agents/validator-selector.md +211 -0
  45. package/cli/ansi-colors.js +21 -0
  46. package/cli/api-reference-tool.js +368 -0
  47. package/cli/build-docs.js +29 -8
  48. package/cli/ceremony-history.js +369 -0
  49. package/cli/checks/catalog.json +76 -0
  50. package/cli/checks/code/quality.json +26 -0
  51. package/cli/checks/code/testing.json +14 -0
  52. package/cli/checks/code/traceability.json +26 -0
  53. package/cli/checks/cross-refs/epic.json +171 -0
  54. package/cli/checks/cross-refs/story.json +149 -0
  55. package/cli/checks/epic/api.json +114 -0
  56. package/cli/checks/epic/backend.json +126 -0
  57. package/cli/checks/epic/cloud.json +126 -0
  58. package/cli/checks/epic/data.json +102 -0
  59. package/cli/checks/epic/database.json +114 -0
  60. package/cli/checks/epic/developer.json +182 -0
  61. package/cli/checks/epic/devops.json +174 -0
  62. package/cli/checks/epic/frontend.json +162 -0
  63. package/cli/checks/epic/mobile.json +102 -0
  64. package/cli/checks/epic/qa.json +90 -0
  65. package/cli/checks/epic/security.json +184 -0
  66. package/cli/checks/epic/solution-architect.json +192 -0
  67. package/cli/checks/epic/test-architect.json +90 -0
  68. package/cli/checks/epic/ui.json +102 -0
  69. package/cli/checks/epic/ux.json +90 -0
  70. package/cli/checks/fixes/epic-fix-template.md +10 -0
  71. package/cli/checks/fixes/story-fix-template.md +10 -0
  72. package/cli/checks/story/api.json +186 -0
  73. package/cli/checks/story/backend.json +102 -0
  74. package/cli/checks/story/cloud.json +102 -0
  75. package/cli/checks/story/data.json +210 -0
  76. package/cli/checks/story/database.json +102 -0
  77. package/cli/checks/story/developer.json +168 -0
  78. package/cli/checks/story/devops.json +102 -0
  79. package/cli/checks/story/frontend.json +174 -0
  80. package/cli/checks/story/mobile.json +102 -0
  81. package/cli/checks/story/qa.json +210 -0
  82. package/cli/checks/story/security.json +198 -0
  83. package/cli/checks/story/solution-architect.json +230 -0
  84. package/cli/checks/story/test-architect.json +210 -0
  85. package/cli/checks/story/ui.json +102 -0
  86. package/cli/checks/story/ux.json +102 -0
  87. package/cli/coding-order.js +401 -0
  88. package/cli/command-logger.js +49 -12
  89. package/cli/components/static-output.js +63 -0
  90. package/cli/console-output-manager.js +94 -0
  91. package/cli/dependency-checker.js +72 -0
  92. package/cli/docs-sync.js +306 -0
  93. package/cli/epic-story-validator.js +659 -0
  94. package/cli/evaluation-prompts.js +1008 -0
  95. package/cli/execution-context.js +195 -0
  96. package/cli/generate-summary-table.js +340 -0
  97. package/cli/init-model-config.js +704 -0
  98. package/cli/init.js +1737 -278
  99. package/cli/kanban-server-manager.js +227 -0
  100. package/cli/llm-claude.js +150 -1
  101. package/cli/llm-gemini.js +109 -0
  102. package/cli/llm-local.js +493 -0
  103. package/cli/llm-mock.js +233 -0
  104. package/cli/llm-openai.js +454 -0
  105. package/cli/llm-provider.js +379 -3
  106. package/cli/llm-token-limits.js +211 -0
  107. package/cli/llm-verifier.js +662 -0
  108. package/cli/llm-xiaomi.js +143 -0
  109. package/cli/message-constants.js +49 -0
  110. package/cli/message-manager.js +334 -0
  111. package/cli/message-types.js +96 -0
  112. package/cli/messaging-api.js +291 -0
  113. package/cli/micro-check-fixer.js +335 -0
  114. package/cli/micro-check-runner.js +449 -0
  115. package/cli/micro-check-scorer.js +148 -0
  116. package/cli/micro-check-validator.js +538 -0
  117. package/cli/model-pricing.js +192 -0
  118. package/cli/model-query-engine.js +468 -0
  119. package/cli/model-recommendation-analyzer.js +495 -0
  120. package/cli/model-selector.js +270 -0
  121. package/cli/output-buffer.js +107 -0
  122. package/cli/process-manager.js +73 -2
  123. package/cli/prompt-logger.js +57 -0
  124. package/cli/repl-ink.js +4625 -1094
  125. package/cli/repl-old.js +3 -4
  126. package/cli/seed-processor.js +962 -0
  127. package/cli/sprint-planning-processor.js +4162 -0
  128. package/cli/template-processor.js +2149 -105
  129. package/cli/templates/project.md +25 -8
  130. package/cli/templates/vitepress-config.mts.template +5 -4
  131. package/cli/token-tracker.js +547 -0
  132. package/cli/tools/generate-story-validators.js +317 -0
  133. package/cli/tools/generate-validators.js +669 -0
  134. package/cli/update-checker.js +19 -17
  135. package/cli/update-notifier.js +4 -4
  136. package/cli/validation-router.js +667 -0
  137. package/cli/verification-tracker.js +563 -0
  138. package/cli/worktree-runner.js +654 -0
  139. package/kanban/README.md +386 -0
  140. package/kanban/client/README.md +205 -0
  141. package/kanban/client/components.json +20 -0
  142. package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
  143. package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
  144. package/kanban/client/dist/index.html +16 -0
  145. package/kanban/client/dist/vite.svg +1 -0
  146. package/kanban/client/index.html +15 -0
  147. package/kanban/client/package-lock.json +9442 -0
  148. package/kanban/client/package.json +44 -0
  149. package/kanban/client/postcss.config.js +6 -0
  150. package/kanban/client/public/vite.svg +1 -0
  151. package/kanban/client/src/App.jsx +651 -0
  152. package/kanban/client/src/components/ProjectFileEditorPopup.jsx +117 -0
  153. package/kanban/client/src/components/ceremony/AskArchPopup.jsx +420 -0
  154. package/kanban/client/src/components/ceremony/AskModelPopup.jsx +629 -0
  155. package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +1133 -0
  156. package/kanban/client/src/components/ceremony/EpicStorySelectionModal.jsx +254 -0
  157. package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
  158. package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +686 -0
  159. package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +838 -0
  160. package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +150 -0
  161. package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +136 -0
  162. package/kanban/client/src/components/ceremony/steps/DatabaseStep.jsx +202 -0
  163. package/kanban/client/src/components/ceremony/steps/DeploymentStep.jsx +123 -0
  164. package/kanban/client/src/components/ceremony/steps/MissionStep.jsx +106 -0
  165. package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +329 -0
  166. package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +249 -0
  167. package/kanban/client/src/components/kanban/CardDetailModal.jsx +646 -0
  168. package/kanban/client/src/components/kanban/EpicSection.jsx +146 -0
  169. package/kanban/client/src/components/kanban/FilterToolbar.jsx +222 -0
  170. package/kanban/client/src/components/kanban/GroupingSelector.jsx +63 -0
  171. package/kanban/client/src/components/kanban/KanbanBoard.jsx +211 -0
  172. package/kanban/client/src/components/kanban/KanbanCard.jsx +147 -0
  173. package/kanban/client/src/components/kanban/KanbanColumn.jsx +90 -0
  174. package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +784 -0
  175. package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
  176. package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
  177. package/kanban/client/src/components/layout/LoadingScreen.jsx +82 -0
  178. package/kanban/client/src/components/process/ProcessMonitorBar.jsx +80 -0
  179. package/kanban/client/src/components/settings/AgentEditorPopup.jsx +171 -0
  180. package/kanban/client/src/components/settings/AgentsTab.jsx +381 -0
  181. package/kanban/client/src/components/settings/ApiKeysTab.jsx +142 -0
  182. package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +105 -0
  183. package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
  184. package/kanban/client/src/components/settings/CostThresholdsTab.jsx +95 -0
  185. package/kanban/client/src/components/settings/ModelPricingTab.jsx +269 -0
  186. package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
  187. package/kanban/client/src/components/settings/ServersTab.jsx +121 -0
  188. package/kanban/client/src/components/settings/SettingsModal.jsx +84 -0
  189. package/kanban/client/src/components/stats/CostModal.jsx +384 -0
  190. package/kanban/client/src/components/ui/badge.jsx +27 -0
  191. package/kanban/client/src/components/ui/dialog.jsx +121 -0
  192. package/kanban/client/src/components/ui/tabs.jsx +85 -0
  193. package/kanban/client/src/hooks/__tests__/useGrouping.test.js +232 -0
  194. package/kanban/client/src/hooks/useGrouping.js +177 -0
  195. package/kanban/client/src/hooks/useWebSocket.js +120 -0
  196. package/kanban/client/src/lib/__tests__/api.test.js +196 -0
  197. package/kanban/client/src/lib/__tests__/status-grouping.test.js +94 -0
  198. package/kanban/client/src/lib/api.js +515 -0
  199. package/kanban/client/src/lib/status-grouping.js +154 -0
  200. package/kanban/client/src/lib/utils.js +11 -0
  201. package/kanban/client/src/main.jsx +10 -0
  202. package/kanban/client/src/store/__tests__/kanbanStore.test.js +164 -0
  203. package/kanban/client/src/store/ceremonyStore.js +172 -0
  204. package/kanban/client/src/store/filterStore.js +201 -0
  205. package/kanban/client/src/store/kanbanStore.js +123 -0
  206. package/kanban/client/src/store/processStore.js +65 -0
  207. package/kanban/client/src/store/sprintPlanningStore.js +33 -0
  208. package/kanban/client/src/styles/globals.css +59 -0
  209. package/kanban/client/tailwind.config.js +77 -0
  210. package/kanban/client/vite.config.js +28 -0
  211. package/kanban/client/vitest.config.js +28 -0
  212. package/kanban/dev-start.sh +47 -0
  213. package/kanban/package.json +12 -0
  214. package/kanban/server/index.js +537 -0
  215. package/kanban/server/routes/ceremony.js +454 -0
  216. package/kanban/server/routes/costs.js +163 -0
  217. package/kanban/server/routes/openai-oauth.js +366 -0
  218. package/kanban/server/routes/processes.js +50 -0
  219. package/kanban/server/routes/settings.js +736 -0
  220. package/kanban/server/routes/websocket.js +281 -0
  221. package/kanban/server/routes/work-items.js +487 -0
  222. package/kanban/server/services/CeremonyService.js +1441 -0
  223. package/kanban/server/services/FileSystemScanner.js +95 -0
  224. package/kanban/server/services/FileWatcher.js +144 -0
  225. package/kanban/server/services/HierarchyBuilder.js +196 -0
  226. package/kanban/server/services/ProcessRegistry.js +122 -0
  227. package/kanban/server/services/TaskRunnerService.js +261 -0
  228. package/kanban/server/services/WorkItemReader.js +123 -0
  229. package/kanban/server/services/WorkItemRefineService.js +510 -0
  230. package/kanban/server/start.js +49 -0
  231. package/kanban/server/utils/kanban-logger.js +132 -0
  232. package/kanban/server/utils/markdown.js +91 -0
  233. package/kanban/server/utils/status-grouping.js +107 -0
  234. package/kanban/server/workers/run-task-worker.js +121 -0
  235. package/kanban/server/workers/seed-worker.js +94 -0
  236. package/kanban/server/workers/sponsor-call-worker.js +92 -0
  237. package/kanban/server/workers/sprint-planning-worker.js +212 -0
  238. package/package.json +19 -7
  239. package/cli/agents/documentation.md +0 -302
@@ -0,0 +1,121 @@
1
+ import { useState } from 'react';
2
+ import { saveGeneralSettings } from '../../lib/api';
3
+
4
+ export function ServersTab({ settings, onSaved }) {
5
+ const [kanbanPort, setKanbanPort] = useState(String(settings.kanbanPort || 4174));
6
+ const [docsPort, setDocsPort] = useState(String(settings.docsPort || 4173));
7
+ const [boardTitle, setBoardTitle] = useState(settings.boardTitle || 'AVC Kanban Board');
8
+ const [portsStatus, setPortsStatus] = useState(null); // null | 'saving' | 'saved' | 'error'
9
+ const [titleStatus, setTitleStatus] = useState(null);
10
+
11
+ const handleSavePorts = async () => {
12
+ setPortsStatus('saving');
13
+ try {
14
+ await saveGeneralSettings({
15
+ kanbanPort: Number(kanbanPort),
16
+ docsPort: Number(docsPort),
17
+ });
18
+ setPortsStatus('saved');
19
+ onSaved();
20
+ setTimeout(() => setPortsStatus(null), 2000);
21
+ } catch {
22
+ setPortsStatus('error');
23
+ setTimeout(() => setPortsStatus(null), 2000);
24
+ }
25
+ };
26
+
27
+ const handleSaveTitle = async () => {
28
+ const trimmed = boardTitle.trim();
29
+ if (!trimmed) return;
30
+ setTitleStatus('saving');
31
+ try {
32
+ await saveGeneralSettings({ boardTitle: trimmed });
33
+ setTitleStatus('saved');
34
+ onSaved();
35
+ setTimeout(() => setTitleStatus(null), 2000);
36
+ } catch {
37
+ setTitleStatus('error');
38
+ setTimeout(() => setTitleStatus(null), 2000);
39
+ }
40
+ };
41
+
42
+ return (
43
+ <div className="px-5 py-4 flex flex-col gap-6">
44
+ {/* General section */}
45
+ <div>
46
+ <h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-3">General</h3>
47
+ <div className="flex items-center gap-3">
48
+ <label className="text-sm text-slate-700 w-28 flex-shrink-0">Board title</label>
49
+ <input
50
+ type="text"
51
+ value={boardTitle}
52
+ onChange={(e) => setBoardTitle(e.target.value)}
53
+ onKeyDown={(e) => { if (e.key === 'Enter') handleSaveTitle(); }}
54
+ className="flex-1 max-w-xs 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"
55
+ />
56
+ <button
57
+ type="button"
58
+ onClick={handleSaveTitle}
59
+ disabled={!boardTitle.trim() || titleStatus === 'saving'}
60
+ 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"
61
+ >
62
+ {titleStatus === 'saving' ? (
63
+ <span className="inline-flex items-center gap-1">
64
+ <span className="w-3 h-3 border border-white/40 border-t-white rounded-full animate-spin" />
65
+ Saving
66
+ </span>
67
+ ) : titleStatus === 'saved' ? '✓ Saved' : titleStatus === 'error' ? '✗ Error' : 'Save'}
68
+ </button>
69
+ </div>
70
+ </div>
71
+
72
+ {/* Ports section */}
73
+ <div>
74
+ <h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-3">Server Ports</h3>
75
+ <div className="flex flex-col gap-3">
76
+ <div className="flex items-center gap-3">
77
+ <label className="text-sm text-slate-700 w-28 flex-shrink-0">Kanban board</label>
78
+ <input
79
+ type="number"
80
+ min="1024"
81
+ max="65535"
82
+ value={kanbanPort}
83
+ onChange={(e) => setKanbanPort(e.target.value)}
84
+ className="w-24 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"
85
+ />
86
+ </div>
87
+ <div className="flex items-center gap-3">
88
+ <label className="text-sm text-slate-700 w-28 flex-shrink-0">Documentation</label>
89
+ <input
90
+ type="number"
91
+ min="1024"
92
+ max="65535"
93
+ value={docsPort}
94
+ onChange={(e) => setDocsPort(e.target.value)}
95
+ className="w-24 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"
96
+ />
97
+ </div>
98
+ <div className="flex items-center gap-3 pt-1">
99
+ <div className="w-28 flex-shrink-0" />
100
+ <button
101
+ type="button"
102
+ onClick={handleSavePorts}
103
+ disabled={portsStatus === 'saving'}
104
+ 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"
105
+ >
106
+ {portsStatus === 'saving' ? (
107
+ <span className="inline-flex items-center gap-1">
108
+ <span className="w-3 h-3 border border-white/40 border-t-white rounded-full animate-spin" />
109
+ Saving
110
+ </span>
111
+ ) : portsStatus === 'saved' ? '✓ Saved' : portsStatus === 'error' ? '✗ Error' : 'Save'}
112
+ </button>
113
+ </div>
114
+ </div>
115
+ <p className="text-xs text-slate-400 mt-3">
116
+ Changing ports requires restarting the servers (run <code className="font-mono bg-slate-100 px-1 rounded">/kanban</code> and <code className="font-mono bg-slate-100 px-1 rounded">/documentation</code>).
117
+ </p>
118
+ </div>
119
+ </div>
120
+ );
121
+ }
@@ -0,0 +1,84 @@
1
+ import { useState } from 'react';
2
+ import { X } from 'lucide-react';
3
+ import { ApiKeysTab } from './ApiKeysTab';
4
+ import { CeremonyModelsTab } from './CeremonyModelsTab';
5
+ import { ServersTab } from './ServersTab';
6
+ import { ModelPricingTab } from './ModelPricingTab';
7
+ import { AgentsTab } from './AgentsTab';
8
+ import { CostThresholdsTab } from './CostThresholdsTab';
9
+
10
+ const TABS = [
11
+ { id: 'api-keys', label: 'API Keys' },
12
+ { id: 'ceremonies', label: 'Ceremony Models' },
13
+ { id: 'agents', label: 'Agents' },
14
+ { id: 'pricing', label: 'Model Pricing' },
15
+ { id: 'cost-thresholds', label: 'Cost Limits' },
16
+ { id: 'servers', label: 'Servers & Ports' },
17
+ ];
18
+
19
+ export function SettingsModal({ settings, models, onClose, onSaved, initialTab }) {
20
+ const [activeTab, setActiveTab] = useState(initialTab || 'api-keys');
21
+
22
+ return (
23
+ <div
24
+ className="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
25
+ onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
26
+ >
27
+ <div
28
+ className="w-full max-w-4xl bg-white rounded-2xl shadow-2xl flex flex-col overflow-hidden"
29
+ style={{ height: '85vh' }}
30
+ >
31
+ {/* Header */}
32
+ <div className="flex items-center justify-between px-5 py-4 border-b border-slate-100 flex-shrink-0">
33
+ <h2 className="text-base font-semibold text-slate-900">⚙ Project Settings</h2>
34
+ <button
35
+ type="button"
36
+ onClick={onClose}
37
+ className="text-slate-400 hover:text-slate-600 transition-colors ml-4"
38
+ aria-label="Close"
39
+ >
40
+ <X className="w-5 h-5" />
41
+ </button>
42
+ </div>
43
+
44
+ {/* Tab bar */}
45
+ <div className="flex border-b border-slate-100 flex-shrink-0 px-5">
46
+ {TABS.map((tab) => (
47
+ <button
48
+ key={tab.id}
49
+ type="button"
50
+ onClick={() => setActiveTab(tab.id)}
51
+ className={`px-3 py-3 text-sm font-medium transition-colors border-b-2 -mb-px ${
52
+ activeTab === tab.id
53
+ ? 'border-slate-900 text-slate-900'
54
+ : 'border-transparent text-slate-500 hover:text-slate-700'
55
+ }`}
56
+ >
57
+ {tab.label}
58
+ </button>
59
+ ))}
60
+ </div>
61
+
62
+ {/* Tab content — scrollable */}
63
+ <div className="flex-1 overflow-y-auto">
64
+ {activeTab === 'api-keys' && (
65
+ <ApiKeysTab settings={settings} onSaved={onSaved} />
66
+ )}
67
+ {activeTab === 'ceremonies' && (
68
+ <CeremonyModelsTab settings={settings} models={models} onSaved={onSaved} />
69
+ )}
70
+ {activeTab === 'pricing' && (
71
+ <ModelPricingTab settings={settings} onSaved={onSaved} />
72
+ )}
73
+ {activeTab === 'cost-thresholds' && (
74
+ <CostThresholdsTab settings={settings} onSaved={onSaved} />
75
+ )}
76
+ {activeTab === 'servers' && (
77
+ <ServersTab settings={settings} onSaved={onSaved} />
78
+ )}
79
+ {activeTab === 'agents' && <AgentsTab />}
80
+ </div>
81
+ </div>
82
+ </div>
83
+ );
84
+ }
@@ -0,0 +1,384 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { X, BarChart2, DollarSign, ChevronDown, ChevronRight } from 'lucide-react';
3
+ import {
4
+ BarChart,
5
+ Bar,
6
+ XAxis,
7
+ YAxis,
8
+ Tooltip,
9
+ ResponsiveContainer,
10
+ } from 'recharts';
11
+ import { getCostHistory, getSettings } from '../../lib/api';
12
+
13
+ const RANGE_TABS = [
14
+ { label: 'Today', value: 'today' },
15
+ { label: '7 days', value: 7 },
16
+ { label: '30 days', value: 30 },
17
+ { label: '90 days', value: 90 },
18
+ ];
19
+
20
+ function formatCostLabel(cost) {
21
+ if (cost === 0) return '$0.00';
22
+ if (cost < 0.01) return '< $0.01';
23
+ return `$${cost.toFixed(2)}`;
24
+ }
25
+
26
+ function formatCostDetail(cost) {
27
+ if (cost === 0) return '$0.0000';
28
+ return `$${cost.toFixed(4)}`;
29
+ }
30
+
31
+ function formatTokens(n) {
32
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
33
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
34
+ return String(n);
35
+ }
36
+
37
+ function formatDateLabel(dateStr) {
38
+ const d = new Date(dateStr + 'T00:00:00');
39
+ return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
40
+ }
41
+
42
+ function formatCeremonyName(name) {
43
+ return name
44
+ .replace(/-/g, ' ')
45
+ .replace(/\b\w/g, (c) => c.toUpperCase());
46
+ }
47
+
48
+ /** Strip parent prefix from stage name, e.g. "sponsor-call-validation" → "Validation" */
49
+ function formatStageName(name, parentName) {
50
+ if (name === parentName) return 'Documentation';
51
+ const prefix = `${parentName}-`;
52
+ if (name.startsWith(prefix)) return formatCeremonyName(name.slice(prefix.length));
53
+ return formatCeremonyName(name);
54
+ }
55
+
56
+ export function CostModal({ onClose }) {
57
+ const [rangeMode, setRangeMode] = useState('today');
58
+ const [customFrom, setCustomFrom] = useState('');
59
+ const [customTo, setCustomTo] = useState('');
60
+ const [data, setData] = useState(null);
61
+ const [loading, setLoading] = useState(true);
62
+ const [expanded, setExpanded] = useState({});
63
+ const [oauthActive, setOauthActive] = useState(false);
64
+
65
+ // Check if OpenAI OAuth is active — if so, costs are not tracked for OpenAI calls
66
+ useEffect(() => {
67
+ getSettings().then((s) => {
68
+ const openai = s?.apiKeys?.openai;
69
+ setOauthActive(openai?.authMode === 'oauth' && openai?.oauth?.connected === true);
70
+ }).catch(() => {});
71
+ }, []);
72
+
73
+ // Fetch data when range changes
74
+ useEffect(() => {
75
+ setLoading(true);
76
+ setData(null);
77
+
78
+ let rangeArg;
79
+ if (rangeMode === 'today') {
80
+ const today = new Date().toISOString().split('T')[0];
81
+ rangeArg = { from: today, to: today };
82
+ } else if (rangeMode === 'custom') {
83
+ if (!customFrom || !customTo) {
84
+ setLoading(false);
85
+ return;
86
+ }
87
+ rangeArg = { from: customFrom, to: customTo };
88
+ } else {
89
+ rangeArg = parseInt(rangeMode, 10);
90
+ }
91
+
92
+ getCostHistory(rangeArg)
93
+ .then((d) => {
94
+ setData(d);
95
+ setLoading(false);
96
+ // Auto-expand parents that have stages
97
+ const init = {};
98
+ (d.ceremonies || []).forEach((c) => {
99
+ if (c.stages && c.stages.length > 0) init[c.name] = true;
100
+ });
101
+ setExpanded(init);
102
+ })
103
+ .catch(() => { setData({ daily: [], ceremonies: [] }); setLoading(false); });
104
+ }, [rangeMode, customFrom, customTo]);
105
+
106
+ // Close on Escape
107
+ useEffect(() => {
108
+ const handler = (e) => { if (e.key === 'Escape') onClose(); };
109
+ document.addEventListener('keydown', handler);
110
+ return () => document.removeEventListener('keydown', handler);
111
+ }, [onClose]);
112
+
113
+ const toggleExpanded = (name) => setExpanded((prev) => ({ ...prev, [name]: !prev[name] }));
114
+
115
+ // Totals come from parent nodes only — stages are already rolled up into them
116
+ const totalCost = data?.ceremonies.reduce((s, c) => s + c.cost, 0) ?? 0;
117
+ const totalTokens = data?.ceremonies.reduce((s, c) => s + c.tokens, 0) ?? 0;
118
+ const totalCalls = data?.ceremonies.reduce((s, c) => s + c.calls, 0) ?? 0;
119
+ const totalSaved = data?.ceremonies.reduce((s, c) => s + (c.saved ?? 0), 0) ?? 0;
120
+ const totalCached = data?.ceremonies.reduce((s, c) => s + (c.cached ?? 0), 0) ?? 0;
121
+ const hasData = data && (data.daily.length > 0 || data.ceremonies.length > 0);
122
+
123
+ return (
124
+ <div className="fixed inset-0 z-[65] flex items-center justify-center p-4">
125
+ {/* Backdrop */}
126
+ <div
127
+ className="absolute inset-0 bg-black/40"
128
+ onClick={onClose}
129
+ aria-hidden="true"
130
+ />
131
+
132
+ {/* Panel */}
133
+ <div
134
+ className="relative bg-white rounded-xl shadow-2xl w-full max-w-2xl flex flex-col"
135
+ style={{ height: '90vh', maxHeight: '900px' }}
136
+ onClick={(e) => e.stopPropagation()}
137
+ >
138
+ {/* Header */}
139
+ <div className="flex items-center justify-between px-6 py-4 border-b border-slate-200 flex-shrink-0">
140
+ <div className="flex items-center gap-2">
141
+ <DollarSign className="w-5 h-5 text-slate-500" />
142
+ <h2 className="text-lg font-semibold text-slate-900">LLM Cost Tracker</h2>
143
+ </div>
144
+ <button
145
+ onClick={onClose}
146
+ className="text-slate-400 hover:text-slate-600 transition-colors"
147
+ aria-label="Close"
148
+ >
149
+ <X className="w-5 h-5" />
150
+ </button>
151
+ </div>
152
+
153
+ {/* Scrollable body */}
154
+ <div className="overflow-y-auto flex-1 px-6 py-4 flex flex-col gap-5">
155
+ {/* Time range tabs — always 2 rows */}
156
+ <div className="flex flex-col gap-1.5">
157
+ {/* Row 1: preset buttons + Custom */}
158
+ <div className="flex items-center gap-2">
159
+ {RANGE_TABS.map((tab) => (
160
+ <button
161
+ key={tab.value}
162
+ onClick={() => setRangeMode(String(tab.value))}
163
+ className={`px-3 py-1 rounded text-sm font-medium transition-colors ${
164
+ rangeMode === String(tab.value)
165
+ ? 'bg-blue-600 text-white'
166
+ : 'bg-slate-100 text-slate-600 hover:bg-slate-200'
167
+ }`}
168
+ >
169
+ {tab.label}
170
+ </button>
171
+ ))}
172
+ <button
173
+ onClick={() => setRangeMode('custom')}
174
+ className={`px-3 py-1 rounded text-sm font-medium transition-colors ${
175
+ rangeMode === 'custom'
176
+ ? 'bg-blue-600 text-white'
177
+ : 'bg-slate-100 text-slate-600 hover:bg-slate-200'
178
+ }`}
179
+ >
180
+ Custom
181
+ </button>
182
+ </div>
183
+
184
+ {/* Row 2: date inputs — always rendered to keep height constant */}
185
+ <div className={`flex items-center gap-2 ${rangeMode !== 'custom' ? 'invisible' : ''}`}>
186
+ <input
187
+ type="date"
188
+ value={customFrom}
189
+ onChange={(e) => setCustomFrom(e.target.value)}
190
+ className="text-sm border border-slate-300 rounded px-2 py-0.5 text-slate-700"
191
+ />
192
+ <span className="text-slate-400 text-sm">to</span>
193
+ <input
194
+ type="date"
195
+ value={customTo}
196
+ onChange={(e) => setCustomTo(e.target.value)}
197
+ className="text-sm border border-slate-300 rounded px-2 py-0.5 text-slate-700"
198
+ />
199
+ </div>
200
+ </div>
201
+
202
+ {/* OAuth notice */}
203
+ {oauthActive && (
204
+ <div className="bg-blue-50 border border-blue-200 rounded-lg px-4 py-3 text-xs text-blue-700 flex items-start gap-2">
205
+ <span className="flex-shrink-0 mt-0.5">ℹ️</span>
206
+ <span>
207
+ <strong>OpenAI OAuth active</strong> — API calls made via OAuth (ChatGPT subscription) are not billed per token.
208
+ No cost is recorded for OpenAI usage in this mode; token counts are still tracked for informational purposes.
209
+ </span>
210
+ </div>
211
+ )}
212
+
213
+ {/* Loading */}
214
+ {loading && (
215
+ <div className="flex-1 flex items-center justify-center">
216
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600" />
217
+ </div>
218
+ )}
219
+
220
+ {/* No data */}
221
+ {!loading && !hasData && (
222
+ <div className="flex-1 flex flex-col items-center justify-center text-slate-400 gap-3">
223
+ <BarChart2 className="w-10 h-10" />
224
+ <p className="text-sm">No usage data for this period.</p>
225
+ <p className="text-xs text-slate-300">Run a ceremony to start tracking costs.</p>
226
+ </div>
227
+ )}
228
+
229
+ {/* Content */}
230
+ {!loading && hasData && (
231
+ <>
232
+ {/* Stat chips */}
233
+ <div className={`grid gap-3 ${totalSaved > 0 ? 'grid-cols-2' : 'grid-cols-3'}`}>
234
+ <div className="bg-slate-50 rounded-lg p-3">
235
+ <p className="text-xs text-slate-500 mb-1">Total Cost</p>
236
+ <p className="text-xl font-bold text-slate-900">{formatCostLabel(totalCost)}</p>
237
+ <p className="text-xs text-slate-400 mt-0.5">this period</p>
238
+ </div>
239
+ <div className="bg-slate-50 rounded-lg p-3">
240
+ <p className="text-xs text-slate-500 mb-1">Total Tokens</p>
241
+ <p className="text-xl font-bold text-slate-900">{formatTokens(totalTokens)}</p>
242
+ {totalCached > 0 && (
243
+ <p className="text-xs text-blue-500 mt-0.5">{formatTokens(totalCached)} cached</p>
244
+ )}
245
+ </div>
246
+ <div className="bg-slate-50 rounded-lg p-3">
247
+ <p className="text-xs text-slate-500 mb-1">API Calls</p>
248
+ <p className="text-xl font-bold text-slate-900">{totalCalls.toLocaleString()}</p>
249
+ <p className="text-xs text-slate-400 mt-0.5">this period</p>
250
+ </div>
251
+ {totalSaved > 0 && (
252
+ <div className="bg-green-50 rounded-lg p-3">
253
+ <p className="text-xs text-green-600 mb-1">Cache Saved</p>
254
+ <p className="text-xl font-bold text-green-700">{formatCostLabel(totalSaved)}</p>
255
+ <p className="text-xs text-green-500 mt-0.5">vs. no cache</p>
256
+ </div>
257
+ )}
258
+ </div>
259
+
260
+ {/* Bar chart */}
261
+ {data.daily.length > 0 && (
262
+ <div>
263
+ <p className="text-xs font-medium text-slate-500 mb-2 uppercase tracking-wide">Daily Cost</p>
264
+ <ResponsiveContainer width="100%" height={180}>
265
+ <BarChart data={data.daily} margin={{ top: 0, right: 0, bottom: 0, left: 0 }}>
266
+ <XAxis
267
+ dataKey="date"
268
+ tickFormatter={formatDateLabel}
269
+ tick={{ fontSize: 11, fill: '#94a3b8' }}
270
+ axisLine={false}
271
+ tickLine={false}
272
+ />
273
+ <YAxis
274
+ tickFormatter={(v) => `$${v.toFixed(2)}`}
275
+ tick={{ fontSize: 11, fill: '#94a3b8' }}
276
+ width={56}
277
+ axisLine={false}
278
+ tickLine={false}
279
+ />
280
+ <Tooltip
281
+ formatter={(v) => [`$${v.toFixed(4)}`, 'Cost']}
282
+ labelFormatter={(label) => formatDateLabel(label)}
283
+ contentStyle={{ fontSize: 12 }}
284
+ />
285
+ <Bar dataKey="cost" fill="#3b82f6" radius={[3, 3, 0, 0]} />
286
+ </BarChart>
287
+ </ResponsiveContainer>
288
+ </div>
289
+ )}
290
+
291
+ {/* Ceremony breakdown — hierarchical */}
292
+ {data.ceremonies.length > 0 && (
293
+ <div>
294
+ <p className="text-xs font-medium text-slate-500 mb-2 uppercase tracking-wide">By Ceremony</p>
295
+ <div className="overflow-x-auto">
296
+ <table className="w-full text-sm">
297
+ <thead>
298
+ <tr className="text-left text-xs text-slate-400 border-b border-slate-100">
299
+ <th className="pb-2 font-medium">Ceremony / Stage</th>
300
+ <th className="pb-2 font-medium text-right">Calls</th>
301
+ <th className="pb-2 font-medium text-right">Tokens</th>
302
+ <th className="pb-2 font-medium text-right">Cost</th>
303
+ <th className="pb-2 font-medium pl-4">Share</th>
304
+ </tr>
305
+ </thead>
306
+ <tbody>
307
+ {data.ceremonies.map((c) => {
308
+ const pct = totalCost > 0 ? (c.cost / totalCost) * 100 : 0;
309
+ const hasStages = c.stages && c.stages.length > 0;
310
+ const isOpen = expanded[c.name];
311
+
312
+ return [
313
+ /* Parent row */
314
+ <tr
315
+ key={c.name}
316
+ className={`border-b border-slate-100 ${hasStages ? 'cursor-pointer hover:bg-slate-50' : ''}`}
317
+ onClick={hasStages ? () => toggleExpanded(c.name) : undefined}
318
+ >
319
+ <td className="py-2 text-slate-800 font-semibold">
320
+ <div className="flex items-center gap-1.5">
321
+ {hasStages
322
+ ? (isOpen
323
+ ? <ChevronDown className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />
324
+ : <ChevronRight className="w-3.5 h-3.5 text-slate-400 flex-shrink-0" />)
325
+ : <span className="w-3.5 flex-shrink-0" />
326
+ }
327
+ {formatCeremonyName(c.name)}
328
+ </div>
329
+ </td>
330
+ <td className="py-2 text-right text-slate-500">{c.calls}</td>
331
+ <td className="py-2 text-right text-slate-500">{formatTokens(c.tokens)}</td>
332
+ <td className="py-2 text-right text-slate-800 font-semibold">{formatCostDetail(c.cost)}</td>
333
+ <td className="py-2 pl-4">
334
+ <div className="flex items-center gap-2">
335
+ <div className="w-20 bg-slate-100 rounded-full h-1.5 flex-shrink-0">
336
+ <div
337
+ className="bg-blue-500 h-1.5 rounded-full"
338
+ style={{ width: `${pct}%` }}
339
+ />
340
+ </div>
341
+ <span className="text-xs text-slate-400">{Math.round(pct)}%</span>
342
+ </div>
343
+ </td>
344
+ </tr>,
345
+
346
+ /* Stage rows — shown when expanded */
347
+ ...(isOpen && hasStages ? c.stages.map((s) => {
348
+ const stagePct = c.cost > 0 ? (s.cost / c.cost) * 100 : 0;
349
+ return (
350
+ <tr key={`${c.name}/${s.name}`} className="border-b border-slate-50 bg-slate-50/50">
351
+ <td className="py-1.5 text-slate-500 pl-7">
352
+ {formatStageName(s.name, c.name)}
353
+ </td>
354
+ <td className="py-1.5 text-right text-slate-400 text-xs">{s.calls}</td>
355
+ <td className="py-1.5 text-right text-slate-400 text-xs">{formatTokens(s.tokens)}</td>
356
+ <td className="py-1.5 text-right text-slate-500 text-xs">{formatCostDetail(s.cost)}</td>
357
+ <td className="py-1.5 pl-4">
358
+ <div className="flex items-center gap-2">
359
+ <div className="w-20 bg-slate-100 rounded-full h-1 flex-shrink-0">
360
+ <div
361
+ className="bg-blue-300 h-1 rounded-full"
362
+ style={{ width: `${stagePct}%` }}
363
+ />
364
+ </div>
365
+ <span className="text-[10px] text-slate-300">{Math.round(stagePct)}%</span>
366
+ </div>
367
+ </td>
368
+ </tr>
369
+ );
370
+ }) : []),
371
+ ];
372
+ })}
373
+ </tbody>
374
+ </table>
375
+ </div>
376
+ </div>
377
+ )}
378
+ </>
379
+ )}
380
+ </div>
381
+ </div>
382
+ </div>
383
+ );
384
+ }
@@ -0,0 +1,27 @@
1
+ import { cn } from '../../lib/utils';
2
+
3
+ /**
4
+ * Badge Component (shadcn/ui style)
5
+ */
6
+ export function Badge({ variant = 'default', className, children }) {
7
+ return (
8
+ <div
9
+ className={cn(
10
+ 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold',
11
+ 'transition-colors focus:outline-none focus:ring-2 focus:ring-slate-950 focus:ring-offset-2',
12
+ {
13
+ 'border-transparent bg-slate-900 text-slate-50 hover:bg-slate-900/80':
14
+ variant === 'default',
15
+ 'border-transparent bg-slate-100 text-slate-900 hover:bg-slate-100/80':
16
+ variant === 'secondary',
17
+ 'border-transparent bg-red-100 text-red-900 hover:bg-red-100/80':
18
+ variant === 'destructive',
19
+ 'border-slate-200 text-slate-900': variant === 'outline',
20
+ },
21
+ className
22
+ )}
23
+ >
24
+ {children}
25
+ </div>
26
+ );
27
+ }