stagent 0.9.5 → 0.10.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 (277) hide show
  1. package/README.md +5 -42
  2. package/dist/cli.js +42 -18
  3. package/docs/.coverage-gaps.json +13 -55
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/provider-runtimes.md +4 -0
  6. package/docs/features/schedules.md +32 -4
  7. package/docs/features/settings.md +28 -5
  8. package/docs/features/tables.md +9 -2
  9. package/docs/features/workflows.md +10 -4
  10. package/docs/journeys/developer.md +15 -1
  11. package/docs/journeys/personal-use.md +21 -4
  12. package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
  13. package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
  14. package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
  15. package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
  16. package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
  17. package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
  18. package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
  19. package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
  20. package/package.json +3 -2
  21. package/src/__tests__/instrumentation-smoke.test.ts +15 -0
  22. package/src/app/analytics/page.tsx +1 -21
  23. package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
  24. package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
  25. package/src/app/api/instance/config/route.ts +41 -0
  26. package/src/app/api/instance/init/route.ts +34 -0
  27. package/src/app/api/instance/upgrade/check/route.ts +26 -0
  28. package/src/app/api/instance/upgrade/route.ts +96 -0
  29. package/src/app/api/instance/upgrade/status/route.ts +35 -0
  30. package/src/app/api/memory/route.ts +0 -11
  31. package/src/app/api/notifications/route.ts +4 -2
  32. package/src/app/api/projects/[id]/route.ts +5 -155
  33. package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
  34. package/src/app/api/schedules/[id]/execute/route.ts +111 -0
  35. package/src/app/api/schedules/[id]/route.ts +9 -1
  36. package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
  37. package/src/app/api/schedules/route.ts +3 -12
  38. package/src/app/api/settings/openai/login/route.ts +22 -0
  39. package/src/app/api/settings/openai/logout/route.ts +7 -0
  40. package/src/app/api/settings/openai/route.ts +21 -1
  41. package/src/app/api/settings/providers/route.ts +35 -8
  42. package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
  43. package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
  44. package/src/app/api/tables/[id]/enrich/route.ts +147 -0
  45. package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
  46. package/src/app/api/tasks/[id]/execute/route.ts +0 -21
  47. package/src/app/api/workflows/[id]/resume/route.ts +59 -0
  48. package/src/app/api/workflows/[id]/status/route.ts +22 -8
  49. package/src/app/api/workspace/context/route.ts +2 -0
  50. package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
  51. package/src/app/chat/page.tsx +11 -0
  52. package/src/app/inbox/page.tsx +12 -5
  53. package/src/app/layout.tsx +42 -21
  54. package/src/app/page.tsx +0 -2
  55. package/src/app/settings/page.tsx +6 -9
  56. package/src/components/chat/__tests__/chat-session-provider.test.tsx +408 -0
  57. package/src/components/chat/chat-command-popover.tsx +2 -2
  58. package/src/components/chat/chat-input.tsx +2 -3
  59. package/src/components/chat/chat-session-provider.tsx +720 -0
  60. package/src/components/chat/chat-shell.tsx +92 -401
  61. package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
  62. package/src/components/instance/instance-section.tsx +382 -0
  63. package/src/components/instance/upgrade-badge.tsx +219 -0
  64. package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
  65. package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
  66. package/src/components/notifications/batch-proposal-review.tsx +20 -5
  67. package/src/components/notifications/inbox-list.tsx +11 -2
  68. package/src/components/notifications/notification-item.tsx +56 -2
  69. package/src/components/notifications/pending-approval-host.tsx +56 -37
  70. package/src/components/schedules/schedule-create-sheet.tsx +19 -1
  71. package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
  72. package/src/components/schedules/schedule-form.tsx +31 -0
  73. package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
  74. package/src/components/settings/auth-method-selector.tsx +19 -4
  75. package/src/components/settings/auth-status-badge.tsx +28 -3
  76. package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
  77. package/src/components/settings/openai-runtime-section.tsx +7 -1
  78. package/src/components/settings/providers-runtimes-section.tsx +138 -19
  79. package/src/components/shared/app-sidebar.tsx +4 -3
  80. package/src/components/shared/command-palette.tsx +4 -5
  81. package/src/components/shared/theme-toggle.tsx +5 -24
  82. package/src/components/shared/workspace-indicator.tsx +61 -2
  83. package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
  84. package/src/components/tables/table-create-sheet.tsx +4 -0
  85. package/src/components/tables/table-enrichment-runs.tsx +103 -0
  86. package/src/components/tables/table-enrichment-sheet.tsx +538 -0
  87. package/src/components/tables/table-spreadsheet.tsx +29 -5
  88. package/src/components/tables/table-toolbar.tsx +10 -1
  89. package/src/components/tasks/kanban-board.tsx +1 -0
  90. package/src/components/tasks/kanban-column.tsx +53 -14
  91. package/src/components/tasks/task-bento-grid.tsx +19 -0
  92. package/src/components/tasks/task-card.tsx +26 -3
  93. package/src/components/tasks/task-chip-bar.tsx +24 -0
  94. package/src/components/tasks/task-result-renderer.tsx +1 -1
  95. package/src/components/workflows/delay-step-body.tsx +109 -0
  96. package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
  97. package/src/components/workflows/loop-status-view.tsx +1 -1
  98. package/src/components/workflows/shared/step-result.tsx +78 -0
  99. package/src/components/workflows/shared/workflow-header.tsx +141 -0
  100. package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
  101. package/src/components/workflows/swarm-dashboard.tsx +2 -15
  102. package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
  103. package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
  104. package/src/components/workflows/workflow-form-view.tsx +133 -16
  105. package/src/components/workflows/workflow-status-view.tsx +30 -740
  106. package/src/instrumentation-node.ts +94 -0
  107. package/src/instrumentation.ts +4 -48
  108. package/src/lib/agents/__tests__/claude-agent.test.ts +199 -0
  109. package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
  110. package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
  111. package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
  112. package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
  113. package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
  114. package/src/lib/agents/claude-agent.ts +155 -18
  115. package/src/lib/agents/execution-manager.ts +0 -35
  116. package/src/lib/agents/learned-context.ts +0 -12
  117. package/src/lib/agents/learning-session.ts +18 -5
  118. package/src/lib/agents/profiles/__tests__/registry.test.ts +6 -4
  119. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +70 -0
  120. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +32 -0
  121. package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
  122. package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
  123. package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
  124. package/src/lib/agents/runtime/openai-codex.ts +29 -60
  125. package/src/lib/agents/runtime/types.ts +8 -0
  126. package/src/lib/book/chapter-mapping.ts +11 -0
  127. package/src/lib/book/content.ts +10 -0
  128. package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
  129. package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
  130. package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
  131. package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
  132. package/src/lib/chat/active-streams.ts +27 -0
  133. package/src/lib/chat/codex-engine.ts +16 -17
  134. package/src/lib/chat/context-builder.ts +5 -3
  135. package/src/lib/chat/engine.ts +50 -3
  136. package/src/lib/chat/reconcile.ts +117 -0
  137. package/src/lib/chat/stagent-tools.ts +1 -0
  138. package/src/lib/chat/stream-telemetry.ts +132 -0
  139. package/src/lib/chat/suggested-prompts.ts +28 -1
  140. package/src/lib/chat/system-prompt.ts +26 -1
  141. package/src/lib/chat/tool-catalog.ts +2 -1
  142. package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
  143. package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
  144. package/src/lib/chat/tools/__tests__/task-tools.test.ts +352 -0
  145. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +217 -0
  146. package/src/lib/chat/tools/document-tools.ts +29 -13
  147. package/src/lib/chat/tools/helpers.ts +39 -0
  148. package/src/lib/chat/tools/notification-tools.ts +9 -5
  149. package/src/lib/chat/tools/project-tools.ts +33 -0
  150. package/src/lib/chat/tools/schedule-tools.ts +44 -11
  151. package/src/lib/chat/tools/table-tools.ts +71 -0
  152. package/src/lib/chat/tools/task-tools.ts +84 -20
  153. package/src/lib/chat/tools/workflow-tools.ts +234 -32
  154. package/src/lib/constants/settings.ts +8 -18
  155. package/src/lib/data/__tests__/clear.test.ts +56 -2
  156. package/src/lib/data/clear.ts +20 -15
  157. package/src/lib/data/delete-project.ts +171 -0
  158. package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
  159. package/src/lib/db/bootstrap.ts +45 -16
  160. package/src/lib/db/index.ts +5 -0
  161. package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
  162. package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
  163. package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
  164. package/src/lib/db/migrations/0026_drop_license.sql +3 -0
  165. package/src/lib/db/migrations/meta/_journal.json +21 -0
  166. package/src/lib/db/schema.ts +68 -23
  167. package/src/lib/environment/workspace-context.ts +13 -1
  168. package/src/lib/import/dedup.ts +4 -54
  169. package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
  170. package/src/lib/instance/__tests__/detect.test.ts +115 -0
  171. package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
  172. package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
  173. package/src/lib/instance/__tests__/settings.test.ts +83 -0
  174. package/src/lib/instance/__tests__/upgrade-poller.test.ts +131 -0
  175. package/src/lib/instance/bootstrap.ts +270 -0
  176. package/src/lib/instance/detect.ts +49 -0
  177. package/src/lib/instance/fingerprint.ts +78 -0
  178. package/src/lib/instance/git-ops.ts +95 -0
  179. package/src/lib/instance/settings.ts +61 -0
  180. package/src/lib/instance/types.ts +77 -0
  181. package/src/lib/instance/upgrade-poller.ts +153 -0
  182. package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
  183. package/src/lib/notifications/visibility.ts +33 -0
  184. package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
  185. package/src/lib/schedules/__tests__/config.test.ts +62 -0
  186. package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
  187. package/src/lib/schedules/__tests__/integration.test.ts +82 -0
  188. package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
  189. package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
  190. package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
  191. package/src/lib/schedules/collision-check.ts +105 -0
  192. package/src/lib/schedules/config.ts +53 -0
  193. package/src/lib/schedules/scheduler.ts +232 -13
  194. package/src/lib/schedules/slot-claim.ts +105 -0
  195. package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
  196. package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
  197. package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
  198. package/src/lib/settings/openai-auth.ts +105 -10
  199. package/src/lib/settings/openai-login-manager.ts +260 -0
  200. package/src/lib/settings/runtime-setup.ts +14 -4
  201. package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
  202. package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
  203. package/src/lib/tables/enrichment-planner.ts +454 -0
  204. package/src/lib/tables/enrichment.ts +328 -0
  205. package/src/lib/tables/query-builder.ts +5 -2
  206. package/src/lib/tables/trigger-evaluator.ts +3 -2
  207. package/src/lib/theme.ts +71 -0
  208. package/src/lib/usage/ledger.ts +2 -18
  209. package/src/lib/util/__tests__/similarity.test.ts +106 -0
  210. package/src/lib/util/similarity.ts +77 -0
  211. package/src/lib/utils/format-timestamp.ts +24 -0
  212. package/src/lib/utils/stagent-paths.ts +12 -0
  213. package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
  214. package/src/lib/validators/__tests__/settings.test.ts +10 -0
  215. package/src/lib/validators/blueprint.ts +70 -9
  216. package/src/lib/validators/profile.ts +2 -2
  217. package/src/lib/validators/settings.ts +3 -1
  218. package/src/lib/workflows/__tests__/delay.test.ts +196 -0
  219. package/src/lib/workflows/__tests__/engine.test.ts +8 -0
  220. package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
  221. package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
  222. package/src/lib/workflows/blueprints/instantiator.ts +22 -1
  223. package/src/lib/workflows/blueprints/types.ts +10 -2
  224. package/src/lib/workflows/delay.ts +106 -0
  225. package/src/lib/workflows/engine.ts +207 -4
  226. package/src/lib/workflows/loop-executor.ts +349 -24
  227. package/src/lib/workflows/post-action.ts +91 -0
  228. package/src/lib/workflows/types.ts +166 -1
  229. package/src/app/api/license/checkout/route.ts +0 -28
  230. package/src/app/api/license/portal/route.ts +0 -26
  231. package/src/app/api/license/route.ts +0 -89
  232. package/src/app/api/license/usage/route.ts +0 -63
  233. package/src/app/api/marketplace/browse/route.ts +0 -15
  234. package/src/app/api/marketplace/import/route.ts +0 -28
  235. package/src/app/api/marketplace/publish/route.ts +0 -40
  236. package/src/app/api/onboarding/email/route.ts +0 -53
  237. package/src/app/api/settings/telemetry/route.ts +0 -14
  238. package/src/app/api/sync/export/route.ts +0 -54
  239. package/src/app/api/sync/restore/route.ts +0 -37
  240. package/src/app/api/sync/sessions/route.ts +0 -24
  241. package/src/app/auth/callback/route.ts +0 -73
  242. package/src/app/marketplace/page.tsx +0 -19
  243. package/src/components/analytics/analytics-gate-card.tsx +0 -101
  244. package/src/components/marketplace/blueprint-card.tsx +0 -61
  245. package/src/components/marketplace/marketplace-browser.tsx +0 -131
  246. package/src/components/onboarding/email-capture-card.tsx +0 -104
  247. package/src/components/settings/activation-form.tsx +0 -95
  248. package/src/components/settings/cloud-account-section.tsx +0 -147
  249. package/src/components/settings/cloud-sync-section.tsx +0 -155
  250. package/src/components/settings/subscription-section.tsx +0 -410
  251. package/src/components/settings/telemetry-section.tsx +0 -80
  252. package/src/components/shared/premium-gate-overlay.tsx +0 -50
  253. package/src/components/shared/schedule-gate-dialog.tsx +0 -64
  254. package/src/components/shared/upgrade-banner.tsx +0 -112
  255. package/src/hooks/use-supabase-auth.ts +0 -79
  256. package/src/lib/billing/email.ts +0 -54
  257. package/src/lib/billing/products.ts +0 -80
  258. package/src/lib/billing/stripe.ts +0 -101
  259. package/src/lib/cloud/supabase-browser.ts +0 -32
  260. package/src/lib/cloud/supabase-client.ts +0 -56
  261. package/src/lib/license/__tests__/features.test.ts +0 -56
  262. package/src/lib/license/__tests__/key-format.test.ts +0 -88
  263. package/src/lib/license/__tests__/manager.test.ts +0 -64
  264. package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
  265. package/src/lib/license/cloud-validation.ts +0 -60
  266. package/src/lib/license/features.ts +0 -44
  267. package/src/lib/license/key-format.ts +0 -101
  268. package/src/lib/license/limit-check.ts +0 -111
  269. package/src/lib/license/limit-queries.ts +0 -51
  270. package/src/lib/license/manager.ts +0 -345
  271. package/src/lib/license/notifications.ts +0 -59
  272. package/src/lib/license/tier-limits.ts +0 -71
  273. package/src/lib/marketplace/marketplace-client.ts +0 -107
  274. package/src/lib/sync/cloud-sync.ts +0 -235
  275. package/src/lib/telemetry/conversion-events.ts +0 -71
  276. package/src/lib/telemetry/queue.ts +0 -122
  277. package/src/lib/validators/license.ts +0 -33
@@ -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>
@@ -21,7 +21,6 @@ import {
21
21
  MessageCircle,
22
22
  Table2,
23
23
  BarChart3,
24
- Store,
25
24
  ChevronDown,
26
25
  } from "lucide-react";
27
26
  import { cn } from "@/lib/utils";
@@ -43,6 +42,7 @@ import {
43
42
  import { ThemeToggle } from "@/components/shared/theme-toggle";
44
43
  import { TrustTierBadge } from "@/components/shared/trust-tier-badge";
45
44
  import { UnreadBadge } from "@/components/notifications/unread-badge";
45
+ import { UpgradeBadge } from "@/components/instance/upgrade-badge";
46
46
  import { AuthStatusDot } from "@/components/settings/auth-status-dot";
47
47
  import { StagentLogo } from "@/components/shared/stagent-logo";
48
48
  import { WorkspaceIndicator } from "@/components/shared/workspace-indicator";
@@ -65,7 +65,6 @@ const workItems: NavItem[] = [
65
65
  { title: "Workflows", href: "/workflows", icon: Workflow },
66
66
  { title: "Documents", href: "/documents", icon: FileText },
67
67
  { title: "Tables", href: "/tables", icon: Table2, alsoMatches: ["/tables/"] },
68
- { title: "Marketplace", href: "/marketplace", icon: Store },
69
68
  ];
70
69
 
71
70
  const manageItems: NavItem[] = [
@@ -199,7 +198,6 @@ function NavGroup({
199
198
 
200
199
  export function AppSidebar() {
201
200
  const pathname = usePathname();
202
-
203
201
  // Determine which group owns the current route
204
202
  const activeGroup = useMemo(() => {
205
203
  for (const group of groupMap) {
@@ -248,6 +246,9 @@ export function AppSidebar() {
248
246
  ))}
249
247
  </SidebarContent>
250
248
  <SidebarFooter className="px-4 py-3">
249
+ <div className="group-data-[collapsible=icon]:hidden mb-2 empty:hidden">
250
+ <UpgradeBadge />
251
+ </div>
251
252
  <div className="group-data-[collapsible=icon]:hidden mb-2">
252
253
  <WorkspaceIndicator variant="sidebar" />
253
254
  </div>
@@ -23,6 +23,7 @@ import {
23
23
  BookOpen,
24
24
  } from "lucide-react";
25
25
  import { navigationItems, createItems } from "@/lib/chat/command-data";
26
+ import { toggleTheme } from "@/lib/theme";
26
27
 
27
28
  interface RecentProject {
28
29
  id: string;
@@ -115,11 +116,9 @@ export function CommandPalette() {
115
116
  [router]
116
117
  );
117
118
 
118
- function toggleTheme() {
119
+ function handleToggleTheme() {
119
120
  setOpen(false);
120
- const isDark = document.documentElement.classList.contains("dark");
121
- document.documentElement.classList.toggle("dark");
122
- localStorage.setItem("stagent-theme", isDark ? "light" : "dark");
121
+ toggleTheme();
123
122
  }
124
123
 
125
124
  async function markAllRead() {
@@ -239,7 +238,7 @@ export function CommandPalette() {
239
238
 
240
239
  {/* Utility */}
241
240
  <CommandGroup heading="Utility">
242
- <CommandItem onSelect={toggleTheme} value="Toggle Theme" keywords={["dark", "light", "mode"]}>
241
+ <CommandItem onSelect={handleToggleTheme} value="Toggle Theme" keywords={["dark", "light", "mode"]}>
243
242
  <Sun className="h-4 w-4 dark:hidden" />
244
243
  <Moon className="h-4 w-4 hidden dark:block" />
245
244
  Toggle Theme