@agile-vibe-coding/avc 0.2.3 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/README.md +475 -3
  2. package/cli/agents/agent-selector.md +23 -0
  3. package/cli/agents/code-implementer.md +117 -0
  4. package/cli/agents/code-validator.md +80 -0
  5. package/cli/agents/context-reviewer-epic.md +101 -0
  6. package/cli/agents/context-reviewer-story.md +92 -0
  7. package/cli/agents/context-writer-epic.md +145 -0
  8. package/cli/agents/context-writer-story.md +111 -0
  9. package/cli/agents/doc-writer-epic.md +42 -0
  10. package/cli/agents/doc-writer-story.md +43 -0
  11. package/cli/agents/duplicate-detector.md +110 -0
  12. package/cli/agents/epic-story-decomposer.md +318 -39
  13. package/cli/agents/mission-scope-generator.md +68 -4
  14. package/cli/agents/mission-scope-validator.md +40 -6
  15. package/cli/agents/project-context-extractor.md +21 -6
  16. package/cli/agents/scaffolding-generator.md +99 -0
  17. package/cli/agents/seed-validator.md +71 -0
  18. package/cli/agents/story-scope-reviewer.md +147 -0
  19. package/cli/agents/story-splitter.md +83 -0
  20. package/cli/agents/validator-documentation.json +31 -0
  21. package/cli/agents/validator-documentation.md +3 -1
  22. package/cli/api-reference-tool.js +368 -0
  23. package/cli/checks/catalog.json +76 -0
  24. package/cli/checks/code/quality.json +26 -0
  25. package/cli/checks/code/testing.json +14 -0
  26. package/cli/checks/code/traceability.json +26 -0
  27. package/cli/checks/cross-refs/epic.json +171 -0
  28. package/cli/checks/cross-refs/story.json +149 -0
  29. package/cli/checks/epic/api.json +114 -0
  30. package/cli/checks/epic/backend.json +126 -0
  31. package/cli/checks/epic/cloud.json +126 -0
  32. package/cli/checks/epic/data.json +102 -0
  33. package/cli/checks/epic/database.json +114 -0
  34. package/cli/checks/epic/developer.json +182 -0
  35. package/cli/checks/epic/devops.json +174 -0
  36. package/cli/checks/epic/frontend.json +162 -0
  37. package/cli/checks/epic/mobile.json +102 -0
  38. package/cli/checks/epic/qa.json +90 -0
  39. package/cli/checks/epic/security.json +184 -0
  40. package/cli/checks/epic/solution-architect.json +192 -0
  41. package/cli/checks/epic/test-architect.json +90 -0
  42. package/cli/checks/epic/ui.json +102 -0
  43. package/cli/checks/epic/ux.json +90 -0
  44. package/cli/checks/fixes/epic-fix-template.md +10 -0
  45. package/cli/checks/fixes/story-fix-template.md +10 -0
  46. package/cli/checks/story/api.json +186 -0
  47. package/cli/checks/story/backend.json +102 -0
  48. package/cli/checks/story/cloud.json +102 -0
  49. package/cli/checks/story/data.json +210 -0
  50. package/cli/checks/story/database.json +102 -0
  51. package/cli/checks/story/developer.json +168 -0
  52. package/cli/checks/story/devops.json +102 -0
  53. package/cli/checks/story/frontend.json +174 -0
  54. package/cli/checks/story/mobile.json +102 -0
  55. package/cli/checks/story/qa.json +210 -0
  56. package/cli/checks/story/security.json +198 -0
  57. package/cli/checks/story/solution-architect.json +230 -0
  58. package/cli/checks/story/test-architect.json +210 -0
  59. package/cli/checks/story/ui.json +102 -0
  60. package/cli/checks/story/ux.json +102 -0
  61. package/cli/coding-order.js +401 -0
  62. package/cli/dependency-checker.js +72 -0
  63. package/cli/epic-story-validator.js +284 -799
  64. package/cli/index.js +0 -0
  65. package/cli/init-model-config.js +17 -10
  66. package/cli/init.js +514 -92
  67. package/cli/kanban-server-manager.js +1 -2
  68. package/cli/llm-claude.js +98 -31
  69. package/cli/llm-gemini.js +29 -5
  70. package/cli/llm-local.js +493 -0
  71. package/cli/llm-openai.js +262 -41
  72. package/cli/llm-provider.js +147 -8
  73. package/cli/llm-token-limits.js +113 -4
  74. package/cli/llm-verifier.js +209 -1
  75. package/cli/llm-xiaomi.js +143 -0
  76. package/cli/message-constants.js +3 -12
  77. package/cli/messaging-api.js +6 -12
  78. package/cli/micro-check-fixer.js +335 -0
  79. package/cli/micro-check-runner.js +449 -0
  80. package/cli/micro-check-scorer.js +148 -0
  81. package/cli/micro-check-validator.js +538 -0
  82. package/cli/model-pricing.js +23 -0
  83. package/cli/model-selector.js +3 -2
  84. package/cli/prompt-logger.js +57 -0
  85. package/cli/repl-ink.js +106 -346
  86. package/cli/repl-old.js +1 -2
  87. package/cli/seed-processor.js +194 -24
  88. package/cli/sprint-planning-processor.js +2638 -289
  89. package/cli/template-processor.js +50 -3
  90. package/cli/token-tracker.js +50 -23
  91. package/cli/tools/generate-story-validators.js +1 -1
  92. package/cli/validation-router.js +70 -8
  93. package/cli/worktree-runner.js +654 -0
  94. package/kanban/client/dist/assets/index-D_KC5EQT.css +1 -0
  95. package/kanban/client/dist/assets/index-DjY5zqW7.js +351 -0
  96. package/kanban/client/dist/index.html +2 -2
  97. package/kanban/client/src/App.jsx +43 -14
  98. package/kanban/client/src/components/ceremony/AskArchPopup.jsx +7 -3
  99. package/kanban/client/src/components/ceremony/AskModelPopup.jsx +23 -10
  100. package/kanban/client/src/components/ceremony/CeremonyWorkflowModal.jsx +320 -133
  101. package/kanban/client/src/components/ceremony/ProviderSwitcherButton.jsx +290 -0
  102. package/kanban/client/src/components/ceremony/SponsorCallModal.jsx +80 -13
  103. package/kanban/client/src/components/ceremony/SprintPlanningModal.jsx +156 -22
  104. package/kanban/client/src/components/ceremony/steps/ArchitectureStep.jsx +11 -11
  105. package/kanban/client/src/components/ceremony/steps/CompleteStep.jsx +3 -21
  106. package/kanban/client/src/components/ceremony/steps/ReviewAnswersStep.jsx +214 -10
  107. package/kanban/client/src/components/ceremony/steps/RunningStep.jsx +23 -2
  108. package/kanban/client/src/components/kanban/CardDetailModal.jsx +97 -10
  109. package/kanban/client/src/components/kanban/GroupingSelector.jsx +7 -1
  110. package/kanban/client/src/components/kanban/KanbanCard.jsx +23 -14
  111. package/kanban/client/src/components/kanban/RefineWorkItemPopup.jsx +9 -14
  112. package/kanban/client/src/components/kanban/RunButton.jsx +162 -0
  113. package/kanban/client/src/components/kanban/SeedButton.jsx +176 -0
  114. package/kanban/client/src/components/settings/AgentsTab.jsx +103 -75
  115. package/kanban/client/src/components/settings/ApiKeysTab.jsx +31 -2
  116. package/kanban/client/src/components/settings/CeremonyModelsTab.jsx +9 -2
  117. package/kanban/client/src/components/settings/CheckEditorPopup.jsx +507 -0
  118. package/kanban/client/src/components/settings/CostThresholdsTab.jsx +3 -2
  119. package/kanban/client/src/components/settings/ModelPricingTab.jsx +72 -7
  120. package/kanban/client/src/components/settings/OpenAIAuthSection.jsx +412 -0
  121. package/kanban/client/src/components/settings/SettingsModal.jsx +4 -4
  122. package/kanban/client/src/components/stats/CostModal.jsx +34 -3
  123. package/kanban/client/src/hooks/useGrouping.js +59 -0
  124. package/kanban/client/src/lib/api.js +118 -4
  125. package/kanban/client/src/lib/status-grouping.js +10 -0
  126. package/kanban/client/src/store/kanbanStore.js +8 -0
  127. package/kanban/server/index.js +23 -2
  128. package/kanban/server/routes/ceremony.js +153 -4
  129. package/kanban/server/routes/costs.js +9 -3
  130. package/kanban/server/routes/openai-oauth.js +366 -0
  131. package/kanban/server/routes/settings.js +447 -14
  132. package/kanban/server/routes/websocket.js +7 -2
  133. package/kanban/server/routes/work-items.js +141 -1
  134. package/kanban/server/services/CeremonyService.js +275 -24
  135. package/kanban/server/services/TaskRunnerService.js +261 -0
  136. package/kanban/server/workers/run-task-worker.js +121 -0
  137. package/kanban/server/workers/seed-worker.js +94 -0
  138. package/kanban/server/workers/sponsor-call-worker.js +14 -6
  139. package/kanban/server/workers/sprint-planning-worker.js +94 -12
  140. package/package.json +2 -3
  141. package/cli/agents/solver-epic-api.json +0 -15
  142. package/cli/agents/solver-epic-api.md +0 -39
  143. package/cli/agents/solver-epic-backend.json +0 -15
  144. package/cli/agents/solver-epic-backend.md +0 -39
  145. package/cli/agents/solver-epic-cloud.json +0 -15
  146. package/cli/agents/solver-epic-cloud.md +0 -39
  147. package/cli/agents/solver-epic-data.json +0 -15
  148. package/cli/agents/solver-epic-data.md +0 -39
  149. package/cli/agents/solver-epic-database.json +0 -15
  150. package/cli/agents/solver-epic-database.md +0 -39
  151. package/cli/agents/solver-epic-developer.json +0 -15
  152. package/cli/agents/solver-epic-developer.md +0 -39
  153. package/cli/agents/solver-epic-devops.json +0 -15
  154. package/cli/agents/solver-epic-devops.md +0 -39
  155. package/cli/agents/solver-epic-frontend.json +0 -15
  156. package/cli/agents/solver-epic-frontend.md +0 -39
  157. package/cli/agents/solver-epic-mobile.json +0 -15
  158. package/cli/agents/solver-epic-mobile.md +0 -39
  159. package/cli/agents/solver-epic-qa.json +0 -15
  160. package/cli/agents/solver-epic-qa.md +0 -39
  161. package/cli/agents/solver-epic-security.json +0 -15
  162. package/cli/agents/solver-epic-security.md +0 -39
  163. package/cli/agents/solver-epic-solution-architect.json +0 -15
  164. package/cli/agents/solver-epic-solution-architect.md +0 -39
  165. package/cli/agents/solver-epic-test-architect.json +0 -15
  166. package/cli/agents/solver-epic-test-architect.md +0 -39
  167. package/cli/agents/solver-epic-ui.json +0 -15
  168. package/cli/agents/solver-epic-ui.md +0 -39
  169. package/cli/agents/solver-epic-ux.json +0 -15
  170. package/cli/agents/solver-epic-ux.md +0 -39
  171. package/cli/agents/solver-story-api.json +0 -15
  172. package/cli/agents/solver-story-api.md +0 -39
  173. package/cli/agents/solver-story-backend.json +0 -15
  174. package/cli/agents/solver-story-backend.md +0 -39
  175. package/cli/agents/solver-story-cloud.json +0 -15
  176. package/cli/agents/solver-story-cloud.md +0 -39
  177. package/cli/agents/solver-story-data.json +0 -15
  178. package/cli/agents/solver-story-data.md +0 -39
  179. package/cli/agents/solver-story-database.json +0 -15
  180. package/cli/agents/solver-story-database.md +0 -39
  181. package/cli/agents/solver-story-developer.json +0 -15
  182. package/cli/agents/solver-story-developer.md +0 -39
  183. package/cli/agents/solver-story-devops.json +0 -15
  184. package/cli/agents/solver-story-devops.md +0 -39
  185. package/cli/agents/solver-story-frontend.json +0 -15
  186. package/cli/agents/solver-story-frontend.md +0 -39
  187. package/cli/agents/solver-story-mobile.json +0 -15
  188. package/cli/agents/solver-story-mobile.md +0 -39
  189. package/cli/agents/solver-story-qa.json +0 -15
  190. package/cli/agents/solver-story-qa.md +0 -39
  191. package/cli/agents/solver-story-security.json +0 -15
  192. package/cli/agents/solver-story-security.md +0 -39
  193. package/cli/agents/solver-story-solution-architect.json +0 -15
  194. package/cli/agents/solver-story-solution-architect.md +0 -39
  195. package/cli/agents/solver-story-test-architect.json +0 -15
  196. package/cli/agents/solver-story-test-architect.md +0 -39
  197. package/cli/agents/solver-story-ui.json +0 -15
  198. package/cli/agents/solver-story-ui.md +0 -39
  199. package/cli/agents/solver-story-ux.json +0 -15
  200. package/cli/agents/solver-story-ux.md +0 -39
  201. package/cli/agents/validator-epic-api.json +0 -93
  202. package/cli/agents/validator-epic-api.md +0 -137
  203. package/cli/agents/validator-epic-backend.json +0 -93
  204. package/cli/agents/validator-epic-backend.md +0 -130
  205. package/cli/agents/validator-epic-cloud.json +0 -93
  206. package/cli/agents/validator-epic-cloud.md +0 -137
  207. package/cli/agents/validator-epic-data.json +0 -93
  208. package/cli/agents/validator-epic-data.md +0 -130
  209. package/cli/agents/validator-epic-database.json +0 -93
  210. package/cli/agents/validator-epic-database.md +0 -137
  211. package/cli/agents/validator-epic-developer.json +0 -74
  212. package/cli/agents/validator-epic-developer.md +0 -153
  213. package/cli/agents/validator-epic-devops.json +0 -74
  214. package/cli/agents/validator-epic-devops.md +0 -153
  215. package/cli/agents/validator-epic-frontend.json +0 -74
  216. package/cli/agents/validator-epic-frontend.md +0 -153
  217. package/cli/agents/validator-epic-mobile.json +0 -93
  218. package/cli/agents/validator-epic-mobile.md +0 -130
  219. package/cli/agents/validator-epic-qa.json +0 -93
  220. package/cli/agents/validator-epic-qa.md +0 -130
  221. package/cli/agents/validator-epic-security.json +0 -74
  222. package/cli/agents/validator-epic-security.md +0 -154
  223. package/cli/agents/validator-epic-solution-architect.json +0 -74
  224. package/cli/agents/validator-epic-solution-architect.md +0 -156
  225. package/cli/agents/validator-epic-test-architect.json +0 -93
  226. package/cli/agents/validator-epic-test-architect.md +0 -130
  227. package/cli/agents/validator-epic-ui.json +0 -93
  228. package/cli/agents/validator-epic-ui.md +0 -130
  229. package/cli/agents/validator-epic-ux.json +0 -93
  230. package/cli/agents/validator-epic-ux.md +0 -130
  231. package/cli/agents/validator-story-api.json +0 -104
  232. package/cli/agents/validator-story-api.md +0 -152
  233. package/cli/agents/validator-story-backend.json +0 -104
  234. package/cli/agents/validator-story-backend.md +0 -152
  235. package/cli/agents/validator-story-cloud.json +0 -104
  236. package/cli/agents/validator-story-cloud.md +0 -152
  237. package/cli/agents/validator-story-data.json +0 -104
  238. package/cli/agents/validator-story-data.md +0 -152
  239. package/cli/agents/validator-story-database.json +0 -104
  240. package/cli/agents/validator-story-database.md +0 -152
  241. package/cli/agents/validator-story-developer.json +0 -104
  242. package/cli/agents/validator-story-developer.md +0 -152
  243. package/cli/agents/validator-story-devops.json +0 -104
  244. package/cli/agents/validator-story-devops.md +0 -152
  245. package/cli/agents/validator-story-frontend.json +0 -104
  246. package/cli/agents/validator-story-frontend.md +0 -152
  247. package/cli/agents/validator-story-mobile.json +0 -104
  248. package/cli/agents/validator-story-mobile.md +0 -152
  249. package/cli/agents/validator-story-qa.json +0 -104
  250. package/cli/agents/validator-story-qa.md +0 -152
  251. package/cli/agents/validator-story-security.json +0 -104
  252. package/cli/agents/validator-story-security.md +0 -152
  253. package/cli/agents/validator-story-solution-architect.json +0 -104
  254. package/cli/agents/validator-story-solution-architect.md +0 -152
  255. package/cli/agents/validator-story-test-architect.json +0 -104
  256. package/cli/agents/validator-story-test-architect.md +0 -152
  257. package/cli/agents/validator-story-ui.json +0 -104
  258. package/cli/agents/validator-story-ui.md +0 -152
  259. package/cli/agents/validator-story-ux.json +0 -104
  260. package/cli/agents/validator-story-ux.md +0 -152
  261. package/kanban/client/dist/assets/index-CiD8PS2e.js +0 -306
  262. package/kanban/client/dist/assets/index-nLh0m82Q.css +0 -1
@@ -0,0 +1,412 @@
1
+ import { useState, useEffect, useRef } from 'react';
2
+ import { Eye, EyeOff } from 'lucide-react';
3
+ import { saveApiKeys, connectOpenAIOAuth, disconnectOpenAIOAuth, getOpenAIOAuthStatus, testOpenAIOAuth, setOpenAIOAuthFallback } from '../../lib/api';
4
+
5
+ function formatExpiresIn(seconds) {
6
+ if (seconds <= 0) return 'expired';
7
+ if (seconds < 60) return `${seconds}s`;
8
+ const mins = Math.floor(seconds / 60);
9
+ if (mins < 60) return `${mins} min`;
10
+ return `${Math.floor(mins / 60)}h ${mins % 60}m`;
11
+ }
12
+
13
+ export function OpenAIAuthSection({ apiKeyInfo, onSaved }) {
14
+ const [authMode, setAuthMode] = useState(apiKeyInfo?.authMode || 'api-key');
15
+
16
+ // API-key sub-state
17
+ const [keyValue, setKeyValue] = useState('');
18
+ const [showKey, setShowKey] = useState(false);
19
+ const [saveStatus, setSaveStatus] = useState(null); // null | 'saving' | 'saved' | 'error'
20
+
21
+ // OAuth sub-state
22
+ const [oauthPhase, setOauthPhase] = useState(
23
+ apiKeyInfo?.oauth?.connected ? 'connected' : 'idle'
24
+ );
25
+ const [oauthInfo, setOauthInfo] = useState(apiKeyInfo?.oauth || { connected: false });
26
+ const [authorizeUrl, setAuthorizeUrl] = useState(null);
27
+ const pollRef = useRef(null);
28
+ const pollTimeoutRef = useRef(null);
29
+
30
+ // Test sub-state
31
+ const [testStatus, setTestStatus] = useState(null); // null | 'running' | { ok, response, model, elapsed } | { error }
32
+
33
+ // Fallback sub-state
34
+ const [fallbackEnabled, setFallbackEnabled] = useState(apiKeyInfo?.oauth?.fallback ?? false);
35
+ const [fallbackStatus, setFallbackStatus] = useState(null); // null | 'saving' | { error }
36
+
37
+
38
+ // Sync from parent settings refresh
39
+ useEffect(() => {
40
+ setAuthMode(apiKeyInfo?.authMode || 'api-key');
41
+ setOauthInfo(apiKeyInfo?.oauth || { connected: false });
42
+ setFallbackEnabled(apiKeyInfo?.oauth?.fallback ?? false);
43
+ if (apiKeyInfo?.oauth?.connected) {
44
+ setOauthPhase('connected');
45
+ }
46
+ }, [apiKeyInfo]);
47
+
48
+ // Stop polling on unmount
49
+ useEffect(() => {
50
+ return () => {
51
+ if (pollRef.current) clearInterval(pollRef.current);
52
+ if (pollTimeoutRef.current) clearTimeout(pollTimeoutRef.current);
53
+ };
54
+ }, []);
55
+
56
+ const startPolling = () => {
57
+ pollRef.current = setInterval(async () => {
58
+ try {
59
+ const status = await getOpenAIOAuthStatus();
60
+ if (status.connected) {
61
+ setOauthInfo(status);
62
+ setOauthPhase('connected');
63
+ setAuthorizeUrl(null);
64
+ stopPolling();
65
+ onSaved();
66
+ }
67
+ } catch { /* ignore */ }
68
+ }, 2000);
69
+
70
+ // Auto-stop after 5 minutes
71
+ pollTimeoutRef.current = setTimeout(() => {
72
+ stopPolling();
73
+ setOauthPhase('idle');
74
+ }, 300_000);
75
+ };
76
+
77
+ const stopPolling = () => {
78
+ if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
79
+ if (pollTimeoutRef.current) { clearTimeout(pollTimeoutRef.current); pollTimeoutRef.current = null; }
80
+ };
81
+
82
+ const handleModeToggle = (mode) => {
83
+ if (mode === authMode) return;
84
+ if (authMode === 'oauth' && oauthPhase === 'connecting') {
85
+ // Cancel an in-progress connection attempt when switching away
86
+ stopPolling();
87
+ setOauthPhase('idle');
88
+ setAuthorizeUrl(null);
89
+ }
90
+ if (mode === 'oauth') {
91
+ // Restore the correct phase when switching back to Subscription tab
92
+ setOauthPhase(oauthInfo.connected ? 'connected' : 'idle');
93
+ }
94
+ setAuthMode(mode);
95
+ };
96
+
97
+ const handleConnect = async () => {
98
+ setOauthPhase('connecting');
99
+ setAuthorizeUrl(null);
100
+ try {
101
+ const result = await connectOpenAIOAuth();
102
+ setAuthorizeUrl(result.authorizeUrl || null);
103
+ startPolling();
104
+ } catch (err) {
105
+ setOauthPhase('idle');
106
+ console.error('OAuth connect error:', err);
107
+ }
108
+ };
109
+
110
+ const handleCancel = () => {
111
+ stopPolling();
112
+ setOauthPhase('idle');
113
+ setAuthorizeUrl(null);
114
+ };
115
+
116
+ const handleDisconnect = async () => {
117
+ try { await disconnectOpenAIOAuth(); } catch { /* ignore */ }
118
+ setOauthPhase('idle');
119
+ setOauthInfo({ connected: false });
120
+ setTestStatus(null);
121
+ setAuthMode('api-key');
122
+ onSaved();
123
+ };
124
+
125
+ const handleFallbackToggle = async (enabled) => {
126
+ setFallbackStatus('saving');
127
+ try {
128
+ await setOpenAIOAuthFallback(enabled);
129
+ setFallbackEnabled(enabled);
130
+ setFallbackStatus(null);
131
+ } catch (err) {
132
+ setFallbackStatus({ error: err.message });
133
+ setTimeout(() => setFallbackStatus(null), 4000);
134
+ }
135
+ };
136
+
137
+ const handleTest = async () => {
138
+ setTestStatus('running');
139
+ try {
140
+ const result = await testOpenAIOAuth();
141
+ setTestStatus(result);
142
+ } catch (err) {
143
+ setTestStatus({ error: err.message });
144
+ }
145
+ };
146
+
147
+ const handleSaveKey = async () => {
148
+ setSaveStatus('saving');
149
+ try {
150
+ await saveApiKeys({ openai: keyValue });
151
+ setSaveStatus('saved');
152
+ setKeyValue('');
153
+ onSaved();
154
+ setTimeout(() => setSaveStatus(null), 2000);
155
+ } catch {
156
+ setSaveStatus('error');
157
+ setTimeout(() => setSaveStatus(null), 2000);
158
+ }
159
+ };
160
+
161
+ const handleClearKey = async () => {
162
+ setSaveStatus('clearing');
163
+ try {
164
+ await saveApiKeys({ openai: '' });
165
+ setSaveStatus('saved');
166
+ onSaved();
167
+ setTimeout(() => setSaveStatus(null), 2000);
168
+ } catch {
169
+ setSaveStatus('error');
170
+ setTimeout(() => setSaveStatus(null), 2000);
171
+ }
172
+ };
173
+
174
+ return (
175
+ <div className="py-3 border-b border-slate-100 last:border-0">
176
+ {/* Header row */}
177
+ <div className="flex items-center gap-3 mb-2">
178
+ <div className="w-36 flex-shrink-0">
179
+ <p className="text-sm font-medium text-slate-800">OpenAI</p>
180
+ <p className="text-xs text-slate-400">Auth mode</p>
181
+ </div>
182
+
183
+ {/* Mode toggle */}
184
+ <div className="flex items-center gap-1 bg-slate-100 rounded-lg p-1">
185
+ <button
186
+ type="button"
187
+ onClick={() => handleModeToggle('api-key')}
188
+ className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
189
+ authMode === 'api-key'
190
+ ? 'bg-white text-slate-800 shadow-sm'
191
+ : 'text-slate-500 hover:text-slate-700'
192
+ }`}
193
+ >
194
+ API Key
195
+ </button>
196
+ <button
197
+ type="button"
198
+ onClick={() => handleModeToggle('oauth')}
199
+ className={`px-3 py-1 text-xs font-medium rounded-md transition-colors ${
200
+ authMode === 'oauth'
201
+ ? 'bg-white text-slate-800 shadow-sm'
202
+ : 'text-slate-500 hover:text-slate-700'
203
+ }`}
204
+ >
205
+ Subscription
206
+ </button>
207
+ </div>
208
+ </div>
209
+
210
+ {/* API Key mode */}
211
+ {authMode === 'api-key' && (
212
+ <div className="flex items-center gap-3 pl-0 pt-1">
213
+ <div className="w-36 flex-shrink-0">
214
+ <p className="text-xs text-slate-400">OPENAI_API_KEY</p>
215
+ </div>
216
+
217
+ <div className="w-16 flex-shrink-0">
218
+ {apiKeyInfo?.isSet ? (
219
+ <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">
220
+ ✓ Set
221
+ </span>
222
+ ) : (
223
+ <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">
224
+ Not set
225
+ </span>
226
+ )}
227
+ </div>
228
+
229
+ {apiKeyInfo?.isSet && !keyValue && (
230
+ <p className="text-xs text-slate-400 font-mono flex-shrink-0">{apiKeyInfo.preview}</p>
231
+ )}
232
+
233
+ <div className="flex-1 flex items-center gap-2 min-w-0">
234
+ <div className="relative flex-1">
235
+ <input
236
+ type={showKey ? 'text' : 'password'}
237
+ value={keyValue}
238
+ onChange={(e) => setKeyValue(e.target.value)}
239
+ placeholder={apiKeyInfo?.isSet ? 'Enter new key to update…' : 'sk-…'}
240
+ 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"
241
+ />
242
+ <button
243
+ type="button"
244
+ onClick={() => setShowKey(v => !v)}
245
+ className="absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600"
246
+ tabIndex={-1}
247
+ >
248
+ {showKey ? <EyeOff className="w-3.5 h-3.5" /> : <Eye className="w-3.5 h-3.5" />}
249
+ </button>
250
+ </div>
251
+
252
+ {apiKeyInfo?.isSet && !keyValue && (
253
+ <button
254
+ type="button"
255
+ onClick={handleClearKey}
256
+ disabled={saveStatus === 'clearing'}
257
+ className="px-3 py-1.5 text-xs font-medium border border-red-200 text-red-600 rounded-md hover:bg-red-50 transition-colors disabled:opacity-40 flex-shrink-0"
258
+ >
259
+ {saveStatus === 'clearing' ? '…' : 'Reset'}
260
+ </button>
261
+ )}
262
+
263
+ <button
264
+ type="button"
265
+ onClick={handleSaveKey}
266
+ disabled={!keyValue.trim() || saveStatus === 'saving'}
267
+ 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"
268
+ >
269
+ {saveStatus === 'saving' ? (
270
+ <span className="inline-flex items-center gap-1">
271
+ <span className="w-3 h-3 border border-white/40 border-t-white rounded-full animate-spin" />
272
+ Saving
273
+ </span>
274
+ ) : saveStatus === 'saved' ? '✓ Saved' : saveStatus === 'error' ? '✗ Error' : 'Save'}
275
+ </button>
276
+ </div>
277
+ </div>
278
+ )}
279
+
280
+ {/* OAuth mode */}
281
+ {authMode === 'oauth' && (
282
+ <div className="pl-0 pt-1">
283
+ {/* Idle — not yet connecting */}
284
+ {oauthPhase === 'idle' && (
285
+ <div className="flex flex-col gap-2">
286
+ <p className="text-xs text-slate-500">
287
+ <span className="font-medium text-amber-700">ℹ</span>{' '}
288
+ Requires a <strong>ChatGPT Pro</strong> subscription ($200/mo).
289
+ Only Codex-endpoint models work (<code className="font-mono bg-slate-100 px-1 rounded">gpt-5.2-codex</code>,{' '}
290
+ <code className="font-mono bg-slate-100 px-1 rounded">gpt-5.3-codex</code>).
291
+ This endpoint is unofficial and may change without notice.
292
+ </p>
293
+ <div>
294
+ <button
295
+ type="button"
296
+ onClick={handleConnect}
297
+ className="inline-flex items-center gap-1.5 px-4 py-1.5 text-xs font-medium bg-emerald-600 text-white rounded-md hover:bg-emerald-700 transition-colors"
298
+ >
299
+ Connect with ChatGPT ↗
300
+ </button>
301
+ </div>
302
+ </div>
303
+ )}
304
+
305
+ {/* Connecting — waiting for browser callback */}
306
+ {oauthPhase === 'connecting' && (
307
+ <div className="flex flex-col gap-2">
308
+ <p className="text-xs text-slate-600 flex items-center gap-2">
309
+ <span className="w-3 h-3 border border-slate-400 border-t-slate-700 rounded-full animate-spin inline-block" />
310
+ Waiting for browser login…
311
+ </p>
312
+ {authorizeUrl && (
313
+ <p className="text-xs text-slate-500">
314
+ If your browser did not open,{' '}
315
+ <a
316
+ href={authorizeUrl}
317
+ target="_blank"
318
+ rel="noreferrer"
319
+ className="text-blue-600 underline break-all"
320
+ >
321
+ click here to authenticate
322
+ </a>
323
+ .
324
+ </p>
325
+ )}
326
+ <div>
327
+ <button
328
+ type="button"
329
+ onClick={handleCancel}
330
+ className="px-3 py-1 text-xs font-medium border border-slate-300 rounded-md text-slate-600 hover:bg-slate-50 transition-colors"
331
+ >
332
+ Cancel
333
+ </button>
334
+ </div>
335
+ </div>
336
+ )}
337
+
338
+ {/* Connected */}
339
+ {oauthPhase === 'connected' && (
340
+ <div className="flex flex-col gap-2">
341
+ <div className="flex items-center gap-3">
342
+ <span className="inline-flex items-center gap-1 text-xs font-medium text-emerald-700 bg-emerald-50 border border-emerald-200 rounded-full px-2 py-0.5">
343
+ ✓ Connected
344
+ </span>
345
+ {oauthInfo.accountId && (
346
+ <span className="text-xs text-slate-500 font-mono">{oauthInfo.accountId}</span>
347
+ )}
348
+ {oauthInfo.expiresIn != null && (
349
+ <span className="text-xs text-slate-400">
350
+ · expires in {formatExpiresIn(oauthInfo.expiresIn)}
351
+ </span>
352
+ )}
353
+ <div className="ml-auto flex items-center gap-2">
354
+ <button
355
+ type="button"
356
+ onClick={handleTest}
357
+ disabled={testStatus === 'running'}
358
+ className="w-14 flex items-center justify-center py-1 text-xs font-medium border border-emerald-300 rounded-md text-emerald-700 hover:bg-emerald-50 transition-colors disabled:opacity-40"
359
+ >
360
+ {testStatus === 'running'
361
+ ? <span className="w-3 h-3 border border-emerald-400 border-t-emerald-700 rounded-full animate-spin" />
362
+ : 'Test'}
363
+ </button>
364
+ <button
365
+ type="button"
366
+ onClick={handleDisconnect}
367
+ className="px-3 py-1 text-xs font-medium border border-slate-300 rounded-md text-slate-600 hover:bg-slate-50 transition-colors"
368
+ >
369
+ Disconnect
370
+ </button>
371
+ </div>
372
+ </div>
373
+ {testStatus && testStatus !== 'running' && (
374
+ <div className={`text-xs rounded-md px-3 py-2 font-mono ${testStatus.error ? 'bg-red-50 text-red-700 border border-red-200' : 'bg-slate-50 text-slate-700 border border-slate-200'}`}>
375
+ {testStatus.error
376
+ ? `✗ ${testStatus.error}`
377
+ : `✓ ${testStatus.response || 'Connected'} [${testStatus.model} · ${testStatus.elapsed}ms]`}
378
+ </div>
379
+ )}
380
+ {/* Fallback toggle — only shown when an API key is also configured */}
381
+ {apiKeyInfo?.isSet && (
382
+ <div className="flex items-center gap-3 pt-1 border-t border-slate-100">
383
+ <label className="flex items-center gap-2 cursor-pointer select-none">
384
+ <div className="relative">
385
+ <input
386
+ type="checkbox"
387
+ checked={fallbackEnabled}
388
+ disabled={fallbackStatus === 'saving'}
389
+ onChange={(e) => handleFallbackToggle(e.target.checked)}
390
+ className="sr-only"
391
+ />
392
+ <div
393
+ onClick={() => fallbackStatus !== 'saving' && handleFallbackToggle(!fallbackEnabled)}
394
+ className={`w-8 h-4 rounded-full transition-colors cursor-pointer ${fallbackEnabled ? 'bg-blue-500' : 'bg-slate-300'} ${fallbackStatus === 'saving' ? 'opacity-40' : ''}`}
395
+ >
396
+ <div className={`absolute top-0.5 left-0.5 w-3 h-3 bg-white rounded-full shadow transition-transform ${fallbackEnabled ? 'translate-x-4' : ''}`} />
397
+ </div>
398
+ </div>
399
+ <span className="text-xs text-slate-600">Fallback to API Key on failure</span>
400
+ </label>
401
+ {fallbackStatus?.error && (
402
+ <span className="text-xs text-red-600">{fallbackStatus.error}</span>
403
+ )}
404
+ </div>
405
+ )}
406
+ </div>
407
+ )}
408
+ </div>
409
+ )}
410
+ </div>
411
+ );
412
+ }
@@ -10,14 +10,14 @@ import { CostThresholdsTab } from './CostThresholdsTab';
10
10
  const TABS = [
11
11
  { id: 'api-keys', label: 'API Keys' },
12
12
  { id: 'ceremonies', label: 'Ceremony Models' },
13
+ { id: 'agents', label: 'Agents' },
13
14
  { id: 'pricing', label: 'Model Pricing' },
14
15
  { id: 'cost-thresholds', label: 'Cost Limits' },
15
16
  { id: 'servers', label: 'Servers & Ports' },
16
- { id: 'agents', label: 'Agents' },
17
17
  ];
18
18
 
19
- export function SettingsModal({ settings, models, onClose, onSaved }) {
20
- const [activeTab, setActiveTab] = useState('api-keys');
19
+ export function SettingsModal({ settings, models, onClose, onSaved, initialTab }) {
20
+ const [activeTab, setActiveTab] = useState(initialTab || 'api-keys');
21
21
 
22
22
  return (
23
23
  <div
@@ -25,7 +25,7 @@ export function SettingsModal({ settings, models, onClose, onSaved }) {
25
25
  onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
26
26
  >
27
27
  <div
28
- className="w-full max-w-2xl bg-white rounded-2xl shadow-2xl flex flex-col overflow-hidden"
28
+ className="w-full max-w-4xl bg-white rounded-2xl shadow-2xl flex flex-col overflow-hidden"
29
29
  style={{ height: '85vh' }}
30
30
  >
31
31
  {/* Header */}
@@ -8,7 +8,7 @@ import {
8
8
  Tooltip,
9
9
  ResponsiveContainer,
10
10
  } from 'recharts';
11
- import { getCostHistory } from '../../lib/api';
11
+ import { getCostHistory, getSettings } from '../../lib/api';
12
12
 
13
13
  const RANGE_TABS = [
14
14
  { label: 'Today', value: 'today' },
@@ -60,6 +60,15 @@ export function CostModal({ onClose }) {
60
60
  const [data, setData] = useState(null);
61
61
  const [loading, setLoading] = useState(true);
62
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
+ }, []);
63
72
 
64
73
  // Fetch data when range changes
65
74
  useEffect(() => {
@@ -107,6 +116,8 @@ export function CostModal({ onClose }) {
107
116
  const totalCost = data?.ceremonies.reduce((s, c) => s + c.cost, 0) ?? 0;
108
117
  const totalTokens = data?.ceremonies.reduce((s, c) => s + c.tokens, 0) ?? 0;
109
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;
110
121
  const hasData = data && (data.daily.length > 0 || data.ceremonies.length > 0);
111
122
 
112
123
  return (
@@ -188,6 +199,17 @@ export function CostModal({ onClose }) {
188
199
  </div>
189
200
  </div>
190
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
+
191
213
  {/* Loading */}
192
214
  {loading && (
193
215
  <div className="flex-1 flex items-center justify-center">
@@ -208,7 +230,7 @@ export function CostModal({ onClose }) {
208
230
  {!loading && hasData && (
209
231
  <>
210
232
  {/* Stat chips */}
211
- <div className="grid grid-cols-3 gap-3">
233
+ <div className={`grid gap-3 ${totalSaved > 0 ? 'grid-cols-2' : 'grid-cols-3'}`}>
212
234
  <div className="bg-slate-50 rounded-lg p-3">
213
235
  <p className="text-xs text-slate-500 mb-1">Total Cost</p>
214
236
  <p className="text-xl font-bold text-slate-900">{formatCostLabel(totalCost)}</p>
@@ -217,13 +239,22 @@ export function CostModal({ onClose }) {
217
239
  <div className="bg-slate-50 rounded-lg p-3">
218
240
  <p className="text-xs text-slate-500 mb-1">Total Tokens</p>
219
241
  <p className="text-xl font-bold text-slate-900">{formatTokens(totalTokens)}</p>
220
- <p className="text-xs text-slate-400 mt-0.5">this period</p>
242
+ {totalCached > 0 && (
243
+ <p className="text-xs text-blue-500 mt-0.5">{formatTokens(totalCached)} cached</p>
244
+ )}
221
245
  </div>
222
246
  <div className="bg-slate-50 rounded-lg p-3">
223
247
  <p className="text-xs text-slate-500 mb-1">API Calls</p>
224
248
  <p className="text-xl font-bold text-slate-900">{totalCalls.toLocaleString()}</p>
225
249
  <p className="text-xs text-slate-400 mt-0.5">this period</p>
226
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
+ )}
227
258
  </div>
228
259
 
229
260
  {/* Bar chart */}
@@ -14,6 +14,8 @@ export function useGrouping(workItems, groupBy) {
14
14
  return groupByEpic(workItems);
15
15
  case 'type':
16
16
  return groupByType(workItems);
17
+ case 'phase':
18
+ return groupByPhase(workItems);
17
19
  default:
18
20
  return groupByStatus(workItems);
19
21
  }
@@ -116,3 +118,60 @@ function groupByType(workItems) {
116
118
  groups: groups.filter((group) => group.items.length > 0),
117
119
  };
118
120
  }
121
+
122
+ /**
123
+ * Group by implementation phase (coding order)
124
+ */
125
+ function groupByPhase(workItems) {
126
+ // Collect all phases from metadata
127
+ const phaseMap = new Map(); // phase number → items
128
+ const unphased = [];
129
+
130
+ for (const item of workItems) {
131
+ const phase = item.metadata?.codingPhase;
132
+ if (phase != null) {
133
+ if (!phaseMap.has(phase)) phaseMap.set(phase, []);
134
+ phaseMap.get(phase).push(item);
135
+ } else {
136
+ unphased.push(item);
137
+ }
138
+ }
139
+
140
+ // Sort phases numerically
141
+ const sortedPhases = [...phaseMap.entries()].sort((a, b) => a[0] - b[0]);
142
+
143
+ const groups = sortedPhases.map(([phase, items]) => {
144
+ // Sort items within phase by codingOrder
145
+ items.sort((a, b) => (a.metadata?.codingOrder ?? 0) - (b.metadata?.codingOrder ?? 0));
146
+ const columns = groupItemsByColumn(items);
147
+
148
+ // Find epic names in this phase for the label
149
+ const epicNames = [...new Set(items.filter(i => i.type === 'epic').map(i => i.name))];
150
+ const label = epicNames.length > 0
151
+ ? `Phase ${phase}: ${epicNames.join(', ')}`
152
+ : `Phase ${phase}`;
153
+
154
+ return {
155
+ id: `phase-${phase}`,
156
+ name: label,
157
+ items,
158
+ columns,
159
+ type: 'phase',
160
+ };
161
+ });
162
+
163
+ if (unphased.length > 0) {
164
+ groups.push({
165
+ id: 'unphased',
166
+ name: 'No Phase',
167
+ items: unphased,
168
+ columns: groupItemsByColumn(unphased),
169
+ type: 'ungrouped',
170
+ });
171
+ }
172
+
173
+ return {
174
+ mode: 'sections',
175
+ groups,
176
+ };
177
+ }