stagent 0.9.6 → 0.11.0

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 (396) hide show
  1. package/README.md +20 -44
  2. package/dist/cli.js +66 -18
  3. package/docs/.coverage-gaps.json +144 -56
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/agent-intelligence.md +12 -2
  6. package/docs/features/chat.md +40 -5
  7. package/docs/features/cost-usage.md +1 -1
  8. package/docs/features/documents.md +5 -2
  9. package/docs/features/inbox-notifications.md +10 -2
  10. package/docs/features/keyboard-navigation.md +12 -3
  11. package/docs/features/provider-runtimes.md +20 -2
  12. package/docs/features/schedules.md +32 -4
  13. package/docs/features/settings.md +28 -5
  14. package/docs/features/shared-components.md +7 -3
  15. package/docs/features/tables.md +11 -2
  16. package/docs/features/tool-permissions.md +6 -2
  17. package/docs/features/workflows.md +14 -4
  18. package/docs/index.md +1 -1
  19. package/docs/journeys/developer.md +39 -2
  20. package/docs/journeys/personal-use.md +32 -8
  21. package/docs/journeys/power-user.md +45 -14
  22. package/docs/journeys/work-use.md +17 -8
  23. package/docs/manifest.json +15 -15
  24. package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
  25. package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
  26. package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
  27. package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
  28. package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
  29. package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
  30. package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
  31. package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
  32. package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
  33. package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
  34. package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
  35. package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
  36. package/next.config.mjs +1 -0
  37. package/package.json +3 -2
  38. package/src/__tests__/instrumentation-smoke.test.ts +15 -0
  39. package/src/app/analytics/page.tsx +1 -21
  40. package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
  41. package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
  42. package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
  43. package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
  44. package/src/app/api/chat/export/route.ts +52 -0
  45. package/src/app/api/chat/files/search/route.ts +50 -0
  46. package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
  47. package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
  48. package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
  49. package/src/app/api/environment/skills/route.ts +13 -0
  50. package/src/app/api/instance/config/route.ts +41 -0
  51. package/src/app/api/instance/init/route.ts +34 -0
  52. package/src/app/api/instance/upgrade/check/route.ts +26 -0
  53. package/src/app/api/instance/upgrade/route.ts +96 -0
  54. package/src/app/api/instance/upgrade/status/route.ts +35 -0
  55. package/src/app/api/memory/route.ts +0 -11
  56. package/src/app/api/notifications/route.ts +4 -2
  57. package/src/app/api/projects/[id]/route.ts +5 -155
  58. package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
  59. package/src/app/api/schedules/[id]/execute/route.ts +111 -0
  60. package/src/app/api/schedules/[id]/route.ts +9 -1
  61. package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
  62. package/src/app/api/schedules/route.ts +3 -12
  63. package/src/app/api/settings/chat/pins/route.ts +94 -0
  64. package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
  65. package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
  66. package/src/app/api/settings/environment/route.ts +26 -0
  67. package/src/app/api/settings/openai/login/route.ts +22 -0
  68. package/src/app/api/settings/openai/logout/route.ts +7 -0
  69. package/src/app/api/settings/openai/route.ts +21 -1
  70. package/src/app/api/settings/providers/route.ts +35 -8
  71. package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
  72. package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
  73. package/src/app/api/tables/[id]/enrich/route.ts +147 -0
  74. package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
  75. package/src/app/api/tasks/[id]/execute/route.ts +52 -33
  76. package/src/app/api/tasks/[id]/respond/route.ts +31 -15
  77. package/src/app/api/tasks/[id]/resume/route.ts +24 -3
  78. package/src/app/api/workflows/[id]/resume/route.ts +59 -0
  79. package/src/app/api/workflows/[id]/status/route.ts +22 -8
  80. package/src/app/api/workspace/context/route.ts +2 -0
  81. package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
  82. package/src/app/chat/page.tsx +11 -0
  83. package/src/app/documents/page.tsx +4 -1
  84. package/src/app/inbox/page.tsx +12 -5
  85. package/src/app/layout.tsx +42 -21
  86. package/src/app/page.tsx +0 -2
  87. package/src/app/settings/page.tsx +8 -9
  88. package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
  89. package/src/components/chat/__tests__/chat-session-provider.test.tsx +573 -0
  90. package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
  91. package/src/components/chat/capability-banner.tsx +68 -0
  92. package/src/components/chat/chat-command-popover.tsx +670 -49
  93. package/src/components/chat/chat-input.tsx +104 -10
  94. package/src/components/chat/chat-message.tsx +12 -3
  95. package/src/components/chat/chat-session-provider.tsx +790 -0
  96. package/src/components/chat/chat-shell.tsx +151 -401
  97. package/src/components/chat/command-tab-bar.tsx +68 -0
  98. package/src/components/chat/conversation-template-picker.tsx +421 -0
  99. package/src/components/chat/help-dialog.tsx +39 -0
  100. package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
  101. package/src/components/chat/skill-row.tsx +147 -0
  102. package/src/components/documents/document-browser.tsx +37 -19
  103. package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
  104. package/src/components/instance/instance-section.tsx +382 -0
  105. package/src/components/instance/upgrade-badge.tsx +219 -0
  106. package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
  107. package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
  108. package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
  109. package/src/components/notifications/batch-proposal-review.tsx +20 -5
  110. package/src/components/notifications/inbox-list.tsx +11 -2
  111. package/src/components/notifications/notification-item.tsx +56 -2
  112. package/src/components/notifications/pending-approval-host.tsx +56 -37
  113. package/src/components/notifications/permission-response-actions.tsx +155 -1
  114. package/src/components/schedules/schedule-create-sheet.tsx +19 -1
  115. package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
  116. package/src/components/schedules/schedule-form.tsx +31 -0
  117. package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
  118. package/src/components/settings/auth-method-selector.tsx +19 -4
  119. package/src/components/settings/auth-status-badge.tsx +28 -3
  120. package/src/components/settings/environment-section.tsx +102 -0
  121. package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
  122. package/src/components/settings/openai-runtime-section.tsx +7 -1
  123. package/src/components/settings/providers-runtimes-section.tsx +138 -19
  124. package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
  125. package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
  126. package/src/components/shared/app-sidebar.tsx +4 -3
  127. package/src/components/shared/command-palette.tsx +266 -7
  128. package/src/components/shared/filter-hint.tsx +70 -0
  129. package/src/components/shared/filter-input.tsx +59 -0
  130. package/src/components/shared/saved-searches-manager.tsx +199 -0
  131. package/src/components/shared/theme-toggle.tsx +5 -24
  132. package/src/components/shared/workspace-indicator.tsx +61 -2
  133. package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
  134. package/src/components/tables/table-create-sheet.tsx +4 -0
  135. package/src/components/tables/table-enrichment-runs.tsx +103 -0
  136. package/src/components/tables/table-enrichment-sheet.tsx +538 -0
  137. package/src/components/tables/table-spreadsheet.tsx +29 -5
  138. package/src/components/tables/table-toolbar.tsx +10 -1
  139. package/src/components/tasks/kanban-board.tsx +1 -0
  140. package/src/components/tasks/kanban-column.tsx +53 -14
  141. package/src/components/tasks/task-bento-grid.tsx +31 -2
  142. package/src/components/tasks/task-card.tsx +29 -3
  143. package/src/components/tasks/task-chip-bar.tsx +54 -1
  144. package/src/components/tasks/task-result-renderer.tsx +1 -1
  145. package/src/components/workflows/delay-step-body.tsx +109 -0
  146. package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
  147. package/src/components/workflows/loop-status-view.tsx +1 -1
  148. package/src/components/workflows/shared/step-result.tsx +78 -0
  149. package/src/components/workflows/shared/workflow-header.tsx +141 -0
  150. package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
  151. package/src/components/workflows/swarm-dashboard.tsx +2 -15
  152. package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
  153. package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
  154. package/src/components/workflows/workflow-form-view.tsx +133 -16
  155. package/src/components/workflows/workflow-status-view.tsx +30 -740
  156. package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
  157. package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
  158. package/src/hooks/use-active-skills.ts +110 -0
  159. package/src/hooks/use-chat-autocomplete.ts +120 -7
  160. package/src/hooks/use-enriched-skills.ts +19 -0
  161. package/src/hooks/use-pinned-entries.ts +104 -0
  162. package/src/hooks/use-recent-user-messages.ts +19 -0
  163. package/src/hooks/use-saved-searches.ts +142 -0
  164. package/src/instrumentation-node.ts +94 -0
  165. package/src/instrumentation.ts +4 -48
  166. package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
  167. package/src/lib/agents/__tests__/claude-agent.test.ts +212 -0
  168. package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
  169. package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
  170. package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
  171. package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
  172. package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
  173. package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
  174. package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
  175. package/src/lib/agents/claude-agent.ts +217 -21
  176. package/src/lib/agents/execution-manager.ts +0 -35
  177. package/src/lib/agents/handoff/bus.ts +2 -2
  178. package/src/lib/agents/learned-context.ts +0 -12
  179. package/src/lib/agents/learning-session.ts +18 -5
  180. package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
  181. package/src/lib/agents/profiles/__tests__/registry.test.ts +53 -4
  182. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +97 -0
  183. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +36 -0
  184. package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
  185. package/src/lib/agents/profiles/registry.ts +18 -0
  186. package/src/lib/agents/profiles/types.ts +7 -1
  187. package/src/lib/agents/router.ts +3 -6
  188. package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
  189. package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
  190. package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
  191. package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
  192. package/src/lib/agents/runtime/catalog.ts +121 -0
  193. package/src/lib/agents/runtime/claude-sdk.ts +32 -0
  194. package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
  195. package/src/lib/agents/runtime/execution-target.ts +456 -0
  196. package/src/lib/agents/runtime/index.ts +4 -0
  197. package/src/lib/agents/runtime/launch-failure.ts +101 -0
  198. package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
  199. package/src/lib/agents/runtime/openai-codex.ts +64 -60
  200. package/src/lib/agents/runtime/openai-direct.ts +8 -0
  201. package/src/lib/agents/runtime/types.ts +8 -0
  202. package/src/lib/agents/task-dispatch.ts +220 -0
  203. package/src/lib/agents/tool-permissions.ts +16 -1
  204. package/src/lib/book/chapter-mapping.ts +11 -0
  205. package/src/lib/book/content.ts +10 -0
  206. package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
  207. package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
  208. package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
  209. package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
  210. package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
  211. package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
  212. package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
  213. package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
  214. package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
  215. package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
  216. package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
  217. package/src/lib/chat/__tests__/types.test.ts +28 -0
  218. package/src/lib/chat/active-skills.ts +31 -0
  219. package/src/lib/chat/active-streams.ts +27 -0
  220. package/src/lib/chat/clean-filter-input.ts +30 -0
  221. package/src/lib/chat/codex-engine.ts +46 -24
  222. package/src/lib/chat/command-tabs.ts +61 -0
  223. package/src/lib/chat/context-builder.ts +146 -4
  224. package/src/lib/chat/dismissals.ts +73 -0
  225. package/src/lib/chat/engine.ts +159 -18
  226. package/src/lib/chat/files/__tests__/search.test.ts +135 -0
  227. package/src/lib/chat/files/expand-mention.ts +76 -0
  228. package/src/lib/chat/files/search.ts +99 -0
  229. package/src/lib/chat/reconcile.ts +117 -0
  230. package/src/lib/chat/skill-composition.ts +210 -0
  231. package/src/lib/chat/skill-conflict.ts +105 -0
  232. package/src/lib/chat/stagent-tools.ts +7 -19
  233. package/src/lib/chat/stream-telemetry.ts +137 -0
  234. package/src/lib/chat/suggested-prompts.ts +28 -1
  235. package/src/lib/chat/system-prompt.ts +48 -1
  236. package/src/lib/chat/tool-catalog.ts +35 -4
  237. package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
  238. package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
  239. package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
  240. package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
  241. package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
  242. package/src/lib/chat/tools/__tests__/task-tools.test.ts +399 -0
  243. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +351 -0
  244. package/src/lib/chat/tools/blueprint-tools.ts +190 -0
  245. package/src/lib/chat/tools/document-tools.ts +29 -13
  246. package/src/lib/chat/tools/helpers.ts +41 -0
  247. package/src/lib/chat/tools/notification-tools.ts +9 -5
  248. package/src/lib/chat/tools/profile-tools.ts +120 -23
  249. package/src/lib/chat/tools/project-tools.ts +33 -0
  250. package/src/lib/chat/tools/schedule-tools.ts +44 -11
  251. package/src/lib/chat/tools/skill-tools.ts +183 -0
  252. package/src/lib/chat/tools/table-tools.ts +71 -0
  253. package/src/lib/chat/tools/task-tools.ts +89 -21
  254. package/src/lib/chat/tools/workflow-tools.ts +275 -32
  255. package/src/lib/chat/types.ts +15 -0
  256. package/src/lib/constants/settings.ts +10 -18
  257. package/src/lib/data/__tests__/clear.test.ts +56 -2
  258. package/src/lib/data/clear.ts +17 -16
  259. package/src/lib/data/delete-project.ts +171 -0
  260. package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
  261. package/src/lib/db/bootstrap.ts +62 -16
  262. package/src/lib/db/index.ts +5 -0
  263. package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
  264. package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
  265. package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
  266. package/src/lib/db/migrations/0026_drop_license.sql +3 -0
  267. package/src/lib/db/migrations/meta/_journal.json +21 -0
  268. package/src/lib/db/schema.ts +94 -23
  269. package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
  270. package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
  271. package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
  272. package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
  273. package/src/lib/environment/data.ts +9 -0
  274. package/src/lib/environment/list-skills.ts +176 -0
  275. package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
  276. package/src/lib/environment/parsers/skill.ts +26 -5
  277. package/src/lib/environment/profile-generator.ts +54 -0
  278. package/src/lib/environment/skill-enrichment.ts +106 -0
  279. package/src/lib/environment/skill-recommendations.ts +66 -0
  280. package/src/lib/environment/workspace-context.ts +13 -1
  281. package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
  282. package/src/lib/filters/__tests__/parse.test.ts +135 -0
  283. package/src/lib/filters/parse.ts +86 -0
  284. package/src/lib/import/dedup.ts +4 -54
  285. package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
  286. package/src/lib/instance/__tests__/detect.test.ts +115 -0
  287. package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
  288. package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
  289. package/src/lib/instance/__tests__/settings.test.ts +83 -0
  290. package/src/lib/instance/__tests__/upgrade-poller.test.ts +181 -0
  291. package/src/lib/instance/bootstrap.ts +270 -0
  292. package/src/lib/instance/detect.ts +49 -0
  293. package/src/lib/instance/fingerprint.ts +76 -0
  294. package/src/lib/instance/git-ops.ts +95 -0
  295. package/src/lib/instance/settings.ts +61 -0
  296. package/src/lib/instance/types.ts +77 -0
  297. package/src/lib/instance/upgrade-poller.ts +205 -0
  298. package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
  299. package/src/lib/notifications/visibility.ts +33 -0
  300. package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
  301. package/src/lib/schedules/__tests__/config.test.ts +62 -0
  302. package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
  303. package/src/lib/schedules/__tests__/integration.test.ts +82 -0
  304. package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
  305. package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
  306. package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
  307. package/src/lib/schedules/collision-check.ts +105 -0
  308. package/src/lib/schedules/config.ts +53 -0
  309. package/src/lib/schedules/scheduler.ts +236 -17
  310. package/src/lib/schedules/slot-claim.ts +105 -0
  311. package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
  312. package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
  313. package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
  314. package/src/lib/settings/openai-auth.ts +105 -10
  315. package/src/lib/settings/openai-login-manager.ts +260 -0
  316. package/src/lib/settings/runtime-setup.ts +14 -4
  317. package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
  318. package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
  319. package/src/lib/tables/enrichment-planner.ts +454 -0
  320. package/src/lib/tables/enrichment.ts +328 -0
  321. package/src/lib/tables/query-builder.ts +5 -2
  322. package/src/lib/tables/trigger-evaluator.ts +3 -2
  323. package/src/lib/theme.ts +71 -0
  324. package/src/lib/usage/ledger.ts +2 -18
  325. package/src/lib/util/__tests__/similarity.test.ts +106 -0
  326. package/src/lib/util/similarity.ts +77 -0
  327. package/src/lib/utils/format-timestamp.ts +24 -0
  328. package/src/lib/utils/stagent-paths.ts +12 -0
  329. package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
  330. package/src/lib/validators/__tests__/settings.test.ts +10 -0
  331. package/src/lib/validators/blueprint.ts +70 -9
  332. package/src/lib/validators/profile.ts +2 -2
  333. package/src/lib/validators/settings.ts +3 -1
  334. package/src/lib/workflows/__tests__/delay.test.ts +196 -0
  335. package/src/lib/workflows/__tests__/engine.test.ts +8 -0
  336. package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
  337. package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
  338. package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
  339. package/src/lib/workflows/blueprints/instantiator.ts +22 -1
  340. package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
  341. package/src/lib/workflows/blueprints/types.ts +16 -2
  342. package/src/lib/workflows/delay.ts +106 -0
  343. package/src/lib/workflows/engine.ts +212 -7
  344. package/src/lib/workflows/loop-executor.ts +349 -24
  345. package/src/lib/workflows/post-action.ts +91 -0
  346. package/src/lib/workflows/types.ts +166 -1
  347. package/src/test/setup.ts +10 -0
  348. package/src/app/api/license/checkout/route.ts +0 -28
  349. package/src/app/api/license/portal/route.ts +0 -26
  350. package/src/app/api/license/route.ts +0 -89
  351. package/src/app/api/license/usage/route.ts +0 -63
  352. package/src/app/api/marketplace/browse/route.ts +0 -15
  353. package/src/app/api/marketplace/import/route.ts +0 -28
  354. package/src/app/api/marketplace/publish/route.ts +0 -40
  355. package/src/app/api/onboarding/email/route.ts +0 -53
  356. package/src/app/api/settings/telemetry/route.ts +0 -14
  357. package/src/app/api/sync/export/route.ts +0 -54
  358. package/src/app/api/sync/restore/route.ts +0 -37
  359. package/src/app/api/sync/sessions/route.ts +0 -24
  360. package/src/app/auth/callback/route.ts +0 -73
  361. package/src/app/marketplace/page.tsx +0 -19
  362. package/src/components/analytics/analytics-gate-card.tsx +0 -101
  363. package/src/components/marketplace/blueprint-card.tsx +0 -61
  364. package/src/components/marketplace/marketplace-browser.tsx +0 -131
  365. package/src/components/onboarding/email-capture-card.tsx +0 -104
  366. package/src/components/settings/activation-form.tsx +0 -95
  367. package/src/components/settings/cloud-account-section.tsx +0 -147
  368. package/src/components/settings/cloud-sync-section.tsx +0 -155
  369. package/src/components/settings/subscription-section.tsx +0 -410
  370. package/src/components/settings/telemetry-section.tsx +0 -80
  371. package/src/components/shared/premium-gate-overlay.tsx +0 -50
  372. package/src/components/shared/schedule-gate-dialog.tsx +0 -64
  373. package/src/components/shared/upgrade-banner.tsx +0 -112
  374. package/src/hooks/use-supabase-auth.ts +0 -79
  375. package/src/lib/billing/email.ts +0 -54
  376. package/src/lib/billing/products.ts +0 -80
  377. package/src/lib/billing/stripe.ts +0 -101
  378. package/src/lib/cloud/supabase-browser.ts +0 -32
  379. package/src/lib/cloud/supabase-client.ts +0 -56
  380. package/src/lib/license/__tests__/features.test.ts +0 -56
  381. package/src/lib/license/__tests__/key-format.test.ts +0 -88
  382. package/src/lib/license/__tests__/manager.test.ts +0 -64
  383. package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
  384. package/src/lib/license/cloud-validation.ts +0 -60
  385. package/src/lib/license/features.ts +0 -44
  386. package/src/lib/license/key-format.ts +0 -101
  387. package/src/lib/license/limit-check.ts +0 -111
  388. package/src/lib/license/limit-queries.ts +0 -51
  389. package/src/lib/license/manager.ts +0 -345
  390. package/src/lib/license/notifications.ts +0 -59
  391. package/src/lib/license/tier-limits.ts +0 -71
  392. package/src/lib/marketplace/marketplace-client.ts +0 -107
  393. package/src/lib/sync/cloud-sync.ts +0 -235
  394. package/src/lib/telemetry/conversion-events.ts +0 -71
  395. package/src/lib/telemetry/queue.ts +0 -122
  396. package/src/lib/validators/license.ts +0 -33
@@ -0,0 +1,102 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, useCallback } from "react";
4
+ import { Sparkles, Wand2 } from "lucide-react";
5
+ import { toast } from "sonner";
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardDescription,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from "@/components/ui/card";
13
+ import { Switch } from "@/components/ui/switch";
14
+ import { Label } from "@/components/ui/label";
15
+ import { FormSectionCard } from "@/components/shared/form-section-card";
16
+
17
+ interface EnvironmentState {
18
+ autoPromoteSkills: boolean;
19
+ }
20
+
21
+ const DEFAULT_STATE: EnvironmentState = {
22
+ autoPromoteSkills: false,
23
+ };
24
+
25
+ export function EnvironmentSection() {
26
+ const [state, setState] = useState<EnvironmentState>(DEFAULT_STATE);
27
+ const [saving, setSaving] = useState(false);
28
+
29
+ const fetchSettings = useCallback(async () => {
30
+ try {
31
+ const res = await fetch("/api/settings/environment");
32
+ if (res.ok) {
33
+ const data = await res.json();
34
+ setState(data);
35
+ }
36
+ } catch {
37
+ // Use defaults
38
+ }
39
+ }, []);
40
+
41
+ useEffect(() => {
42
+ fetchSettings();
43
+ }, [fetchSettings]);
44
+
45
+ const handleToggle = async (value: boolean) => {
46
+ setState((prev) => ({ ...prev, autoPromoteSkills: value }));
47
+ setSaving(true);
48
+ try {
49
+ const res = await fetch("/api/settings/environment", {
50
+ method: "POST",
51
+ headers: { "Content-Type": "application/json" },
52
+ body: JSON.stringify({ autoPromoteSkills: value }),
53
+ });
54
+ if (res.ok) {
55
+ const data = await res.json();
56
+ setState(data);
57
+ toast.success(`Auto-promote ${value ? "enabled" : "disabled"}`);
58
+ } else {
59
+ throw new Error("Save failed");
60
+ }
61
+ } catch {
62
+ toast.error("Failed to save setting");
63
+ setState((prev) => ({ ...prev, autoPromoteSkills: !value }));
64
+ } finally {
65
+ setSaving(false);
66
+ }
67
+ };
68
+
69
+ return (
70
+ <Card>
71
+ <CardHeader>
72
+ <CardTitle className="flex items-center gap-2">
73
+ <Sparkles className="h-5 w-5" />
74
+ Environment
75
+ </CardTitle>
76
+ <CardDescription>
77
+ How Stagent discovers and syncs skills from your environment into the
78
+ agent profile registry.
79
+ </CardDescription>
80
+ </CardHeader>
81
+ <CardContent className="space-y-4">
82
+ <FormSectionCard
83
+ icon={Wand2}
84
+ title="Auto-promote discovered skills"
85
+ hint="When enabled, every unlinked skill in ~/.claude/skills/ with a valid SKILL.md is automatically converted into an agent profile on the next environment scan. Leave off to review and promote skills manually from the Environment dashboard."
86
+ >
87
+ <div className="flex items-center justify-between">
88
+ <Label htmlFor="auto-promote-toggle" className="text-sm">
89
+ {state.autoPromoteSkills ? "Enabled" : "Disabled"}
90
+ </Label>
91
+ <Switch
92
+ id="auto-promote-toggle"
93
+ checked={state.autoPromoteSkills}
94
+ disabled={saving}
95
+ onCheckedChange={handleToggle}
96
+ />
97
+ </div>
98
+ </FormSectionCard>
99
+ </CardContent>
100
+ </Card>
101
+ );
102
+ }
@@ -0,0 +1,278 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import {
5
+ CircleSlash,
6
+ ExternalLink,
7
+ Loader2,
8
+ LogOut,
9
+ RefreshCw,
10
+ ShieldCheck,
11
+ XCircle,
12
+ } from "lucide-react";
13
+ import { Button } from "@/components/ui/button";
14
+ import type {
15
+ OpenAIAccountInfo,
16
+ OpenAIRateLimitInfo,
17
+ } from "@/lib/settings/openai-auth";
18
+ import type { OpenAILoginState } from "@/lib/settings/openai-login-manager";
19
+ import type { RuntimeConnectionResult } from "@/lib/agents/runtime/types";
20
+
21
+ interface OpenAIChatGPTAuthControlProps {
22
+ connected: boolean;
23
+ account: OpenAIAccountInfo | null;
24
+ rateLimits: OpenAIRateLimitInfo | null;
25
+ initialLoginState: OpenAILoginState;
26
+ onChanged: () => Promise<void>;
27
+ onLoginStateChange?: (state: OpenAILoginState) => void;
28
+ }
29
+
30
+ function formatResetAt(value: number | null | undefined) {
31
+ if (!value) return null;
32
+ return new Date(value * 1000).toLocaleString(undefined, {
33
+ dateStyle: "medium",
34
+ timeStyle: "short",
35
+ });
36
+ }
37
+
38
+ function formatPlanType(value: string | null | undefined) {
39
+ if (!value) return "Unknown";
40
+
41
+ switch (value.toLowerCase()) {
42
+ case "prolite":
43
+ case "pro":
44
+ return "Pro";
45
+ case "plus":
46
+ return "Plus";
47
+ case "business":
48
+ return "Business";
49
+ case "enterprise":
50
+ return "Enterprise";
51
+ case "edu":
52
+ case "education":
53
+ return "Education";
54
+ default:
55
+ return value;
56
+ }
57
+ }
58
+
59
+ export function OpenAIChatGPTAuthControl({
60
+ connected,
61
+ account,
62
+ rateLimits,
63
+ initialLoginState,
64
+ onChanged,
65
+ onLoginStateChange,
66
+ }: OpenAIChatGPTAuthControlProps) {
67
+ const [loginState, setLoginState] = useState<OpenAILoginState>(initialLoginState);
68
+ const [testResult, setTestResult] = useState<RuntimeConnectionResult | null>(null);
69
+ const [testing, setTesting] = useState(false);
70
+ const [signingOut, setSigningOut] = useState(false);
71
+
72
+ function updateLoginState(next: OpenAILoginState) {
73
+ setLoginState(next);
74
+ onLoginStateChange?.(next);
75
+ }
76
+
77
+ useEffect(() => {
78
+ updateLoginState(initialLoginState);
79
+ }, [initialLoginState]);
80
+
81
+ useEffect(() => {
82
+ if (loginState.phase !== "pending") return;
83
+
84
+ const interval = window.setInterval(async () => {
85
+ const res = await fetch("/api/settings/openai/login");
86
+ if (!res.ok) return;
87
+ const next = (await res.json()) as OpenAILoginState;
88
+ updateLoginState(next);
89
+ if (next.phase !== "pending") {
90
+ window.clearInterval(interval);
91
+ await onChanged();
92
+ }
93
+ }, 1500);
94
+
95
+ return () => window.clearInterval(interval);
96
+ }, [loginState.phase, onChanged]);
97
+
98
+ async function handleStartLogin() {
99
+ setTestResult(null);
100
+ const res = await fetch("/api/settings/openai/login", { method: "POST" });
101
+ const next = (await res.json()) as OpenAILoginState;
102
+ updateLoginState(next);
103
+
104
+ if (next.authUrl) {
105
+ window.open(next.authUrl, "_blank", "noopener,noreferrer");
106
+ }
107
+ }
108
+
109
+ async function handleCancelLogin() {
110
+ const res = await fetch("/api/settings/openai/login", { method: "DELETE" });
111
+ const next = (await res.json()) as OpenAILoginState;
112
+ updateLoginState(next);
113
+ await onChanged();
114
+ }
115
+
116
+ async function handleLogout() {
117
+ setSigningOut(true);
118
+ try {
119
+ await fetch("/api/settings/openai/logout", { method: "POST" });
120
+ updateLoginState({
121
+ phase: "idle",
122
+ loginId: null,
123
+ authUrl: null,
124
+ account: null,
125
+ rateLimits: null,
126
+ error: null,
127
+ startedAt: null,
128
+ updatedAt: new Date().toISOString(),
129
+ });
130
+ setTestResult(null);
131
+ await onChanged();
132
+ } finally {
133
+ setSigningOut(false);
134
+ }
135
+ }
136
+
137
+ async function handleTestConnection() {
138
+ setTesting(true);
139
+ setTestResult(null);
140
+ try {
141
+ const res = await fetch("/api/settings/test", {
142
+ method: "POST",
143
+ headers: { "Content-Type": "application/json" },
144
+ body: JSON.stringify({ runtime: "openai-codex-app-server" }),
145
+ });
146
+ const result = (await res.json()) as RuntimeConnectionResult;
147
+ setTestResult(result);
148
+ if (result.connected) {
149
+ await onChanged();
150
+ }
151
+ } finally {
152
+ setTesting(false);
153
+ }
154
+ }
155
+
156
+ const visibleAccount = account ?? loginState.account;
157
+ const visibleRateLimits = rateLimits ?? loginState.rateLimits;
158
+
159
+ return (
160
+ <div className="space-y-3">
161
+ <p className="text-sm text-muted-foreground">
162
+ ChatGPT mode uses Codex App Server&apos;s browser sign-in flow and keeps the session
163
+ in Stagent&apos;s isolated Codex home so it does not touch your normal `~/.codex` login.
164
+ </p>
165
+
166
+ {visibleAccount?.email && (
167
+ <div className="rounded-xl border border-border/60 bg-background/40 px-3 py-2">
168
+ <div className="flex items-center gap-2 text-sm font-medium">
169
+ <ShieldCheck className="h-4 w-4 text-success" />
170
+ <span>{visibleAccount.email}</span>
171
+ </div>
172
+ <p className="mt-1 text-xs text-muted-foreground">
173
+ Plan: {formatPlanType(visibleAccount.planType)}
174
+ </p>
175
+ </div>
176
+ )}
177
+
178
+ {visibleRateLimits?.primary && (
179
+ <div className="rounded-xl border border-border/60 bg-background/40 px-3 py-2">
180
+ <p className="text-sm font-medium">Codex rate limits</p>
181
+ <p className="mt-1 text-xs text-muted-foreground">
182
+ {visibleRateLimits.primary.usedPercent ?? 0}% used in the current{" "}
183
+ {visibleRateLimits.primary.windowDurationMins ?? "?"}-minute window
184
+ {formatResetAt(visibleRateLimits.primary.resetsAt)
185
+ ? ` • resets ${formatResetAt(visibleRateLimits.primary.resetsAt)}`
186
+ : ""}
187
+ </p>
188
+ </div>
189
+ )}
190
+
191
+ {loginState.phase === "pending" ? (
192
+ <div className="rounded-xl border border-primary/20 bg-primary/5 px-3 py-3">
193
+ <div className="flex items-center gap-2 text-sm font-medium text-foreground">
194
+ <Loader2 className="h-4 w-4 animate-spin text-primary" />
195
+ Waiting for ChatGPT sign-in
196
+ </div>
197
+ <p className="mt-1 text-xs text-muted-foreground">
198
+ Complete the browser flow, then return here. This page will update automatically.
199
+ </p>
200
+ <div className="mt-3 flex flex-wrap items-center gap-2">
201
+ {loginState.authUrl && (
202
+ <Button asChild variant="outline" size="sm">
203
+ <a href={loginState.authUrl} target="_blank" rel="noreferrer">
204
+ Open login page
205
+ <ExternalLink className="ml-1 h-3.5 w-3.5" />
206
+ </a>
207
+ </Button>
208
+ )}
209
+ <Button variant="ghost" size="sm" onClick={handleCancelLogin}>
210
+ Cancel sign-in
211
+ </Button>
212
+ </div>
213
+ </div>
214
+ ) : (
215
+ <div className="flex flex-wrap items-center gap-2">
216
+ {!connected && (
217
+ <Button size="sm" onClick={handleStartLogin}>
218
+ Sign in with ChatGPT
219
+ </Button>
220
+ )}
221
+ <Button
222
+ variant="outline"
223
+ size="sm"
224
+ onClick={handleTestConnection}
225
+ disabled={testing}
226
+ >
227
+ {testing && <Loader2 className="mr-1 h-3 w-3 animate-spin" />}
228
+ Test connection
229
+ </Button>
230
+ {connected && (
231
+ <Button
232
+ variant="ghost"
233
+ size="sm"
234
+ onClick={handleLogout}
235
+ disabled={signingOut}
236
+ >
237
+ {signingOut ? (
238
+ <Loader2 className="mr-1 h-3 w-3 animate-spin" />
239
+ ) : (
240
+ <LogOut className="mr-1 h-3.5 w-3.5" />
241
+ )}
242
+ Sign out
243
+ </Button>
244
+ )}
245
+ </div>
246
+ )}
247
+
248
+ {loginState.phase === "cancelled" && (
249
+ <p className="flex items-center gap-1.5 text-sm text-muted-foreground">
250
+ <CircleSlash className="h-4 w-4" />
251
+ <span>ChatGPT sign-in cancelled.</span>
252
+ </p>
253
+ )}
254
+
255
+ {loginState.phase === "failed" && loginState.error && (
256
+ <p className="flex items-center gap-1.5 text-sm text-status-failed">
257
+ <XCircle className="h-4 w-4" />
258
+ <span>{loginState.error}</span>
259
+ </p>
260
+ )}
261
+
262
+ {testResult && (
263
+ <p
264
+ className={`flex items-center gap-1.5 text-sm ${
265
+ testResult.connected ? "text-success" : "text-status-failed"
266
+ }`}
267
+ >
268
+ {testResult.connected ? (
269
+ <ShieldCheck className="h-4 w-4" />
270
+ ) : (
271
+ <RefreshCw className="h-4 w-4" />
272
+ )}
273
+ <span>{testResult.connected ? "Connected" : testResult.error ?? "Connection failed"}</span>
274
+ </p>
275
+ )}
276
+ </div>
277
+ );
278
+ }
@@ -13,12 +13,15 @@ import { ApiKeyForm } from "./api-key-form";
13
13
  import { AuthStatusBadge } from "./auth-status-badge";
14
14
 
15
15
  interface OpenAISettings {
16
+ method: "api_key" | "oauth";
16
17
  hasKey: boolean;
17
18
  apiKeySource: "db" | "env" | "unknown";
19
+ oauthConnected?: boolean;
18
20
  }
19
21
 
20
22
  export function OpenAIRuntimeSection() {
21
23
  const [settings, setSettings] = useState<OpenAISettings>({
24
+ method: "api_key",
22
25
  hasKey: false,
23
26
  apiKeySource: "unknown",
24
27
  });
@@ -41,7 +44,7 @@ export function OpenAIRuntimeSection() {
41
44
  const res = await fetch("/api/settings/openai", {
42
45
  method: "POST",
43
46
  headers: { "Content-Type": "application/json" },
44
- body: JSON.stringify({ apiKey }),
47
+ body: JSON.stringify({ method: "api_key", apiKey }),
45
48
  });
46
49
  if (res.ok) {
47
50
  const data = (await res.json()) as OpenAISettings;
@@ -80,6 +83,9 @@ export function OpenAIRuntimeSection() {
80
83
  <AuthStatusBadge
81
84
  connected={connected}
82
85
  apiKeySource={settings.apiKeySource}
86
+ authMethod={settings.method}
87
+ oauthConnected={settings.oauthConnected}
88
+ oauthLabel="ChatGPT"
83
89
  />
84
90
  </div>
85
91
  </CardHeader>
@@ -17,8 +17,11 @@ import { AuthMethodSelector } from "./auth-method-selector";
17
17
  import { ApiKeyForm } from "./api-key-form";
18
18
  import { AuthStatusBadge } from "./auth-status-badge";
19
19
  import { ConnectionTestControl } from "./connection-test-control";
20
+ import { OpenAIChatGPTAuthControl } from "./openai-chatgpt-auth-control";
20
21
  import type { AuthMethod, ApiKeySource, RoutingPreference } from "@/lib/constants/settings";
21
22
  import type { RuntimeSetupState } from "@/lib/settings/runtime-setup";
23
+ import type { OpenAIAccountInfo, OpenAIRateLimitInfo } from "@/lib/settings/openai-auth";
24
+ import type { OpenAILoginState } from "@/lib/settings/openai-login-manager";
22
25
 
23
26
  // ── Types ────────────────────────────────────────────────────────────
24
27
 
@@ -27,6 +30,10 @@ interface ProviderState {
27
30
  authMethod?: AuthMethod;
28
31
  hasKey: boolean;
29
32
  apiKeySource: ApiKeySource;
33
+ oauthConnected?: boolean;
34
+ account?: OpenAIAccountInfo | null;
35
+ rateLimits?: OpenAIRateLimitInfo | null;
36
+ login?: OpenAILoginState;
30
37
  dualBilling: boolean;
31
38
  runtimes: RuntimeSetupState[];
32
39
  }
@@ -99,6 +106,7 @@ const BILLING_LABELS: Record<string, string> = {
99
106
 
100
107
  function ProviderRow({
101
108
  name,
109
+ oauthLabel,
102
110
  provider,
103
111
  defaultOpen,
104
112
  open: controlledOpen,
@@ -106,6 +114,7 @@ function ProviderRow({
106
114
  children,
107
115
  }: {
108
116
  name: string;
117
+ oauthLabel?: string;
109
118
  provider: ProviderState;
110
119
  defaultOpen: boolean;
111
120
  open?: boolean;
@@ -125,10 +134,19 @@ function ProviderRow({
125
134
  const activeRuntimes = provider.runtimes.filter((r) => r.configured);
126
135
  const activeCount = activeRuntimes.length;
127
136
  const activeLabels = activeRuntimes.map((r) => r.label).join(", ");
137
+ const openAIOAuthPending = provider.authMethod === "oauth" && provider.oauthConnected === false;
138
+ const openAILoginPending = provider.login?.phase === "pending";
128
139
 
129
140
  let statusLine: string;
130
141
  if (!provider.configured) {
131
- statusLine = "Add an API key to enable runtimes";
142
+ statusLine =
143
+ provider.authMethod === "oauth"
144
+ ? "Sign in with ChatGPT to enable Codex App Server"
145
+ : "Add an API key to enable runtimes";
146
+ } else if (openAIOAuthPending && activeCount > 0) {
147
+ statusLine = openAILoginPending
148
+ ? `Waiting for ${oauthLabel ?? "OAuth"} sign-in. ${activeLabels} remains active.`
149
+ : `Codex App Server needs ${oauthLabel ?? "OAuth"} sign-in. ${activeLabels} remains active.`;
132
150
  } else if (activeCount === 2) {
133
151
  statusLine = `2 runtimes active: ${activeLabels}`;
134
152
  } else if (activeCount === 1) {
@@ -157,6 +175,9 @@ function ProviderRow({
157
175
  <AuthStatusBadge
158
176
  connected={provider.configured}
159
177
  apiKeySource={provider.apiKeySource}
178
+ authMethod={provider.authMethod}
179
+ oauthLabel={oauthLabel}
180
+ oauthConnected={provider.oauthConnected}
160
181
  />
161
182
  </div>
162
183
  <p className="text-xs text-muted-foreground mt-0.5">{statusLine}</p>
@@ -178,8 +199,10 @@ function ProviderRow({
178
199
  <div className="rounded-xl border border-primary/20 bg-primary/5 px-3 py-2">
179
200
  <p className="text-xs text-muted-foreground">
180
201
  <span className="font-medium text-foreground">Two billing modes active.</span>{" "}
181
- Claude Code uses your Max/Pro subscription. Anthropic Direct API uses
182
- pay-as-you-go API billing. Budget guardrails track each separately.
202
+ {name === "Anthropic"
203
+ ? "Claude Code uses your Max/Pro subscription. Anthropic Direct API uses pay-as-you-go API billing."
204
+ : "Codex App Server uses your ChatGPT plan. OpenAI Direct uses pay-as-you-go API billing."}{" "}
205
+ Budget guardrails track each separately.
183
206
  </p>
184
207
  </div>
185
208
  )}
@@ -192,6 +215,14 @@ function ProviderRow({
192
215
  <div className="grid gap-2 sm:grid-cols-2">
193
216
  {provider.runtimes.map((runtime) => {
194
217
  const isActive = runtime.configured;
218
+ const inactiveDescription = runtime.runtimeId.includes("direct")
219
+ ? "Requires API key"
220
+ : runtime.runtimeId === "openai-codex-app-server" &&
221
+ provider.authMethod === "oauth"
222
+ ? provider.login?.phase === "pending"
223
+ ? "Waiting for ChatGPT sign-in"
224
+ : "Sign in with ChatGPT"
225
+ : "Requires CLI or API key";
195
226
  return (
196
227
  <div
197
228
  key={runtime.runtimeId}
@@ -212,9 +243,7 @@ function ProviderRow({
212
243
  <p className="text-xs text-muted-foreground mt-0.5">
213
244
  {isActive
214
245
  ? (RUNTIME_DESCRIPTIONS[runtime.runtimeId] ?? "Active")
215
- : runtime.runtimeId.includes("direct")
216
- ? "Requires API key"
217
- : "Requires CLI or API key"}
246
+ : inactiveDescription}
218
247
  </p>
219
248
  </div>
220
249
  );
@@ -233,6 +262,7 @@ export function ProvidersAndRuntimesSection() {
233
262
  const [data, setData] = useState<ProvidersPayload | null>(null);
234
263
  const [loading, setLoading] = useState(true);
235
264
  const [anthropicOpen, setAnthropicOpen] = useState(false);
265
+ const [openAILoginState, setOpenAILoginState] = useState<OpenAILoginState | null>(null);
236
266
 
237
267
  const fetchData = useCallback(async () => {
238
268
  try {
@@ -256,6 +286,7 @@ export function ProvidersAndRuntimesSection() {
256
286
  if (none || !data.providers.anthropic.configured) {
257
287
  setAnthropicOpen(true);
258
288
  }
289
+ setOpenAILoginState(data.providers.openai.login ?? null);
259
290
  }
260
291
  }, [data?.configuredProviderCount, data?.providers.anthropic.configured]);
261
292
 
@@ -292,7 +323,16 @@ export function ProvidersAndRuntimesSection() {
292
323
  const res = await fetch("/api/settings/openai", {
293
324
  method: "POST",
294
325
  headers: { "Content-Type": "application/json" },
295
- body: JSON.stringify({ apiKey }),
326
+ body: JSON.stringify({ method: "api_key", apiKey }),
327
+ });
328
+ if (res.ok) fetchData();
329
+ }
330
+
331
+ async function handleOpenAIMethodChange(method: AuthMethod) {
332
+ const res = await fetch("/api/settings/openai", {
333
+ method: "POST",
334
+ headers: { "Content-Type": "application/json" },
335
+ body: JSON.stringify({ method }),
296
336
  });
297
337
  if (res.ok) fetchData();
298
338
  }
@@ -308,6 +348,17 @@ export function ProvidersAndRuntimesSection() {
308
348
  return result;
309
349
  }
310
350
 
351
+ async function handleOpenAIDirectTest() {
352
+ const res = await fetch("/api/settings/test", {
353
+ method: "POST",
354
+ headers: { "Content-Type": "application/json" },
355
+ body: JSON.stringify({ runtime: "openai-direct" }),
356
+ });
357
+ const result = await res.json();
358
+ fetchData();
359
+ return result;
360
+ }
361
+
311
362
  // ── Routing preference handler ───────────────────────────────────
312
363
 
313
364
  async function handleRoutingChange(value: RoutingPreference) {
@@ -349,6 +400,10 @@ export function ProvidersAndRuntimesSection() {
349
400
  }
350
401
 
351
402
  const { providers, routingPreference, configuredProviderCount } = data;
403
+ const openAIProvider: ProviderState = {
404
+ ...providers.openai,
405
+ login: openAILoginState ?? providers.openai.login,
406
+ };
352
407
  const noneConfigured = configuredProviderCount === 0;
353
408
  const recommendedAuth = recommendedAuthForRouting(routingPreference);
354
409
 
@@ -430,7 +485,7 @@ export function ProvidersAndRuntimesSection() {
430
485
  <p className="text-xs text-primary/70">
431
486
  {recommendedAuth === "api_key"
432
487
  ? "This preference works best with an API key configured below."
433
- : "This preference works well with OAuth (Claude Max/Pro) configured below."}
488
+ : "This preference works well with subscription-backed auth configured below."}
434
489
  </p>
435
490
  )}
436
491
  </div>
@@ -440,6 +495,7 @@ export function ProvidersAndRuntimesSection() {
440
495
  {/* Anthropic provider — controlled open state */}
441
496
  <ProviderRow
442
497
  name="Anthropic"
498
+ oauthLabel="Claude Max/Pro"
443
499
  provider={providers.anthropic}
444
500
  defaultOpen={false}
445
501
  open={anthropicOpen}
@@ -479,19 +535,82 @@ export function ProvidersAndRuntimesSection() {
479
535
  {/* OpenAI provider — uncontrolled */}
480
536
  <ProviderRow
481
537
  name="OpenAI"
482
- provider={providers.openai}
483
- defaultOpen={noneConfigured || !providers.openai.configured}
538
+ oauthLabel="ChatGPT"
539
+ provider={openAIProvider}
540
+ defaultOpen={
541
+ noneConfigured ||
542
+ !openAIProvider.configured ||
543
+ ((openAIProvider.authMethod ?? "api_key") === "oauth" &&
544
+ !(openAIProvider.oauthConnected ?? false))
545
+ }
484
546
  >
485
- <ApiKeyForm
486
- hasKey={providers.openai.hasKey}
487
- onSave={handleOpenAISaveKey}
488
- onTest={handleOpenAITest}
489
- keyPrefix="sk-"
490
- placeholder="sk-..."
491
- maskedPrefix="sk-••••••"
492
- envVarName="OPENAI_API_KEY"
493
- testButtonLabel="Test OpenAI Connection"
547
+ <AuthMethodSelector
548
+ value={openAIProvider.authMethod ?? "api_key"}
549
+ onChange={handleOpenAIMethodChange}
550
+ recommendedMethod={recommendedAuth}
551
+ label="Codex App Server Authentication"
552
+ options={[
553
+ {
554
+ id: "api_key",
555
+ icon: Zap,
556
+ title: "API Key",
557
+ description: "Use an OpenAI API key for Codex App Server",
558
+ },
559
+ {
560
+ id: "oauth",
561
+ icon: Crown,
562
+ title: "ChatGPT",
563
+ description: "Use your ChatGPT plan with browser sign-in",
564
+ },
565
+ ]}
494
566
  />
567
+
568
+ {(openAIProvider.authMethod ?? "api_key") === "oauth" ? (
569
+ <OpenAIChatGPTAuthControl
570
+ connected={openAIProvider.oauthConnected ?? false}
571
+ account={openAIProvider.account ?? null}
572
+ rateLimits={openAIProvider.rateLimits ?? null}
573
+ initialLoginState={
574
+ openAIProvider.login ?? {
575
+ phase: "idle",
576
+ loginId: null,
577
+ authUrl: null,
578
+ account: null,
579
+ rateLimits: null,
580
+ error: null,
581
+ startedAt: null,
582
+ updatedAt: new Date().toISOString(),
583
+ }
584
+ }
585
+ onChanged={fetchData}
586
+ onLoginStateChange={setOpenAILoginState}
587
+ />
588
+ ) : (
589
+ <p className="text-sm text-muted-foreground">
590
+ API key mode authenticates Codex App Server directly and also powers OpenAI Direct.
591
+ </p>
592
+ )}
593
+
594
+ <Separator />
595
+
596
+ <div className="space-y-4">
597
+ <div>
598
+ <p className="text-sm font-medium">OpenAI Direct API Key</p>
599
+ <p className="mt-1 text-xs text-muted-foreground">
600
+ Used by the OpenAI Direct runtime. If Codex App Server is in API key mode, it shares this key.
601
+ </p>
602
+ </div>
603
+ <ApiKeyForm
604
+ hasKey={providers.openai.hasKey}
605
+ onSave={handleOpenAISaveKey}
606
+ onTest={handleOpenAIDirectTest}
607
+ keyPrefix="sk-"
608
+ placeholder="sk-..."
609
+ maskedPrefix="sk-••••••"
610
+ envVarName="OPENAI_API_KEY"
611
+ testButtonLabel="Test OpenAI Direct"
612
+ />
613
+ </div>
495
614
  </ProviderRow>
496
615
  </CardContent>
497
616
  </Card>