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,125 @@
1
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ import { InstanceSection } from "@/components/instance/instance-section";
5
+
6
+ const { push } = vi.hoisted(() => ({
7
+ push: vi.fn(),
8
+ }));
9
+
10
+ vi.mock("next/navigation", () => ({
11
+ useRouter: () => ({ push }),
12
+ }));
13
+
14
+ describe("instance section", () => {
15
+ beforeEach(() => {
16
+ vi.clearAllMocks();
17
+ });
18
+
19
+ afterEach(() => {
20
+ vi.unstubAllGlobals();
21
+ });
22
+
23
+ it("renders a single combined instance card for initialized instances", async () => {
24
+ vi.stubGlobal(
25
+ "fetch",
26
+ vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
27
+ const url = String(input);
28
+ const method = init?.method ?? "GET";
29
+
30
+ if (url === "/api/instance/config" && method === "GET") {
31
+ return {
32
+ ok: true,
33
+ json: async () => ({
34
+ devMode: false,
35
+ config: {
36
+ instanceId: "instance_123456789",
37
+ branchName: "instance/demo",
38
+ isPrivateInstance: true,
39
+ createdAt: 1712700000,
40
+ },
41
+ guardrails: {
42
+ prePushHookInstalled: true,
43
+ prePushHookVersion: "1",
44
+ pushRemoteBlocked: ["main"],
45
+ consentStatus: "enabled",
46
+ firstBootCompletedAt: 1712700000,
47
+ },
48
+ upgrade: {
49
+ lastPolledAt: 1712700000,
50
+ upgradeAvailable: true,
51
+ commitsBehind: 3,
52
+ lastSuccessfulUpgradeAt: 1712600000,
53
+ pollFailureCount: 0,
54
+ lastPollError: null,
55
+ },
56
+ }),
57
+ };
58
+ }
59
+
60
+ if (url === "/api/instance/upgrade/check" && method === "POST") {
61
+ return {
62
+ ok: true,
63
+ json: async () => ({ ok: true }),
64
+ };
65
+ }
66
+
67
+ throw new Error(`Unexpected fetch: ${method} ${url}`);
68
+ })
69
+ );
70
+
71
+ render(<InstanceSection />);
72
+
73
+ expect(await screen.findByRole("button", { name: "Check" })).toBeInTheDocument();
74
+ expect(screen.getByRole("button", { name: "Upgrade (3)" })).toBeInTheDocument();
75
+ expect(screen.getByRole("button", { name: "Repair setup" })).toBeInTheDocument();
76
+ expect(screen.queryByText("Upgrade instance")).not.toBeInTheDocument();
77
+ expect(screen.queryByText("Advanced: re-run instance setup")).not.toBeInTheDocument();
78
+ expect(screen.queryByText("Blocked branches")).not.toBeInTheDocument();
79
+ expect(screen.queryByText("Pre-push hook")).not.toBeInTheDocument();
80
+ });
81
+
82
+ it("uses the shorter setup CTA when the instance is not initialized", async () => {
83
+ vi.stubGlobal(
84
+ "fetch",
85
+ vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
86
+ const url = String(input);
87
+ const method = init?.method ?? "GET";
88
+
89
+ if (url === "/api/instance/config" && method === "GET") {
90
+ return {
91
+ ok: true,
92
+ json: async () => ({
93
+ devMode: false,
94
+ config: null,
95
+ guardrails: null,
96
+ upgrade: null,
97
+ }),
98
+ };
99
+ }
100
+
101
+ if (url === "/api/instance/init" && method === "POST") {
102
+ return {
103
+ ok: true,
104
+ json: async () => ({ ok: true }),
105
+ };
106
+ }
107
+
108
+ throw new Error(`Unexpected fetch: ${method} ${url}`);
109
+ })
110
+ );
111
+
112
+ render(<InstanceSection />);
113
+
114
+ expect(await screen.findByRole("button", { name: "Run setup" })).toBeInTheDocument();
115
+ expect(
116
+ screen.getByText("Instance setup incomplete. Run setup to initialize this workspace.")
117
+ ).toBeInTheDocument();
118
+
119
+ fireEvent.click(screen.getByRole("button", { name: "Run setup" }));
120
+
121
+ await waitFor(() => {
122
+ expect(fetch).toHaveBeenCalledWith("/api/instance/init", { method: "POST" });
123
+ });
124
+ });
125
+ });
@@ -0,0 +1,382 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, type ReactNode } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Badge } from "@/components/ui/badge";
7
+
8
+ interface InstanceConfig {
9
+ instanceId: string;
10
+ branchName: string;
11
+ isPrivateInstance: boolean;
12
+ createdAt: number;
13
+ }
14
+
15
+ interface Guardrails {
16
+ prePushHookInstalled: boolean;
17
+ prePushHookVersion: string;
18
+ pushRemoteBlocked: string[];
19
+ consentStatus: "not_yet" | "enabled" | "declined_permanently";
20
+ firstBootCompletedAt: number | null;
21
+ }
22
+
23
+ interface UpgradeState {
24
+ lastPolledAt: number | null;
25
+ upgradeAvailable: boolean;
26
+ commitsBehind: number;
27
+ lastSuccessfulUpgradeAt: number | null;
28
+ pollFailureCount: number;
29
+ lastPollError: string | null;
30
+ }
31
+
32
+ interface ConfigResponse {
33
+ devMode: boolean;
34
+ config: InstanceConfig | null;
35
+ guardrails: Guardrails | null;
36
+ upgrade: UpgradeState | null;
37
+ }
38
+
39
+ /**
40
+ * Settings → Instance section. Compact horizontal strip with title + actions
41
+ * in a top bar and metadata in a 4-column grid below. On the canonical dev
42
+ * repo (devMode=true) collapses to a single-row notice to avoid pretending
43
+ * the main branch is an instance.
44
+ */
45
+ const STALE_THRESHOLD_MS = 5 * 60 * 1000;
46
+
47
+ export function InstanceSection() {
48
+ const router = useRouter();
49
+ const [state, setState] = useState<ConfigResponse | null>(null);
50
+ const [loading, setLoading] = useState(true);
51
+ const [busy, setBusy] = useState<"check" | "init" | "upgrade" | null>(null);
52
+ const [message, setMessage] = useState<string | null>(null);
53
+
54
+ async function loadConfig() {
55
+ setLoading(true);
56
+ try {
57
+ const res = await fetch("/api/instance/config", { cache: "no-store" });
58
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
59
+ const data = (await res.json()) as ConfigResponse;
60
+ setState(data);
61
+ return data;
62
+ } catch (err) {
63
+ setMessage(err instanceof Error ? err.message : String(err));
64
+ return null;
65
+ } finally {
66
+ setLoading(false);
67
+ }
68
+ }
69
+
70
+ // Silent background refresh — used after auto-check on mount so we don't
71
+ // flicker the whole card back to its loading state.
72
+ async function refreshConfigSilent() {
73
+ try {
74
+ const res = await fetch("/api/instance/config", { cache: "no-store" });
75
+ if (!res.ok) return;
76
+ const data = (await res.json()) as ConfigResponse;
77
+ setState(data);
78
+ } catch {
79
+ // Swallow — this is a best-effort refresh after auto-check.
80
+ }
81
+ }
82
+
83
+ useEffect(() => {
84
+ let cancelled = false;
85
+ (async () => {
86
+ const data = await loadConfig();
87
+ if (cancelled || !data || data.devMode || !data.config) return;
88
+ // If the cached upgrade state is older than 5 minutes, silently force
89
+ // a fresh check. This self-heals after manual `git pull` + merge in
90
+ // the terminal, so users don't see a stale "N updates pending" count.
91
+ const lastPolled = data.upgrade?.lastPolledAt ?? 0;
92
+ const ageMs = Date.now() - lastPolled * 1000;
93
+ if (ageMs > STALE_THRESHOLD_MS) {
94
+ try {
95
+ const res = await fetch("/api/instance/upgrade/check", {
96
+ method: "POST",
97
+ });
98
+ if (res.ok && !cancelled) {
99
+ await refreshConfigSilent();
100
+ }
101
+ } catch {
102
+ // Silent — manual "Check for upgrades" button remains as fallback.
103
+ }
104
+ }
105
+ })();
106
+ return () => {
107
+ cancelled = true;
108
+ };
109
+ }, []);
110
+
111
+ async function checkNow() {
112
+ setBusy("check");
113
+ setMessage(null);
114
+ try {
115
+ const res = await fetch("/api/instance/upgrade/check", { method: "POST" });
116
+ if (res.status === 202) {
117
+ const body = await res.json();
118
+ setMessage(`Check skipped: ${body.skipped ?? body.error ?? "unknown"}`);
119
+ } else if (res.ok) {
120
+ setMessage("Check complete");
121
+ await loadConfig();
122
+ } else {
123
+ throw new Error(`HTTP ${res.status}`);
124
+ }
125
+ } catch (err) {
126
+ setMessage(err instanceof Error ? err.message : String(err));
127
+ } finally {
128
+ setBusy(null);
129
+ }
130
+ }
131
+
132
+ async function startUpgrade() {
133
+ setBusy("upgrade");
134
+ setMessage(null);
135
+ try {
136
+ const res = await fetch("/api/instance/upgrade", { method: "POST" });
137
+ if (!res.ok) {
138
+ const body = await res.json().catch(() => ({}));
139
+ throw new Error(body.error ?? `HTTP ${res.status}`);
140
+ }
141
+ const data = (await res.json()) as { taskId: string };
142
+ router.push(`/tasks/${data.taskId}`);
143
+ } catch (err) {
144
+ setMessage(err instanceof Error ? err.message : String(err));
145
+ setBusy(null);
146
+ }
147
+ }
148
+
149
+ async function reinit() {
150
+ setBusy("init");
151
+ setMessage(null);
152
+ try {
153
+ const res = await fetch("/api/instance/init", { method: "POST" });
154
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
155
+ await loadConfig();
156
+ setMessage("Instance setup re-run complete");
157
+ } catch (err) {
158
+ setMessage(err instanceof Error ? err.message : String(err));
159
+ } finally {
160
+ setBusy(null);
161
+ }
162
+ }
163
+
164
+ if (loading) {
165
+ return (
166
+ <section className="rounded-xl border bg-card px-5 py-4">
167
+ <h2 className="text-base font-semibold">Instance</h2>
168
+ <p className="mt-1 text-sm text-muted-foreground">Loading…</p>
169
+ </section>
170
+ );
171
+ }
172
+
173
+ // Dev mode: main dev repo. Instance bootstrap is gated off. Show a slim
174
+ // single-row notice so the Settings page layout stays stable without
175
+ // misrepresenting the dev repo as an instance.
176
+ if (state?.devMode) {
177
+ return (
178
+ <section className="rounded-xl border bg-card px-5 py-3 flex items-center justify-between gap-4 flex-wrap">
179
+ <div className="flex items-center gap-3">
180
+ <h2 className="text-base font-semibold">Instance</h2>
181
+ <Badge variant="outline" className="text-xs font-normal">
182
+ Dev mode
183
+ </Badge>
184
+ </div>
185
+ <p className="text-xs text-muted-foreground">
186
+ Running on the main dev repo. Instance upgrade features are disabled.
187
+ Set{" "}
188
+ <code className="font-mono text-[11px] px-1 py-0.5 rounded bg-muted">
189
+ STAGENT_INSTANCE_MODE=true
190
+ </code>{" "}
191
+ to test.
192
+ </p>
193
+ </section>
194
+ );
195
+ }
196
+
197
+ const config = state?.config ?? null;
198
+ const guardrails = state?.guardrails ?? null;
199
+ const upgrade = state?.upgrade ?? null;
200
+ const hasConfig = config !== null;
201
+
202
+ // Not-initialized state
203
+ if (!hasConfig) {
204
+ return (
205
+ <section className="rounded-xl border bg-card px-5 py-4 space-y-3">
206
+ <div className="flex items-center justify-between gap-4 flex-wrap">
207
+ <h2 className="text-base font-semibold">Instance</h2>
208
+ <Button
209
+ variant="default"
210
+ size="sm"
211
+ onClick={reinit}
212
+ disabled={busy !== null}
213
+ >
214
+ {busy === "init" ? "Running…" : "Run setup"}
215
+ </Button>
216
+ </div>
217
+ <div className="rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs">
218
+ Instance setup incomplete. Run setup to initialize this workspace.
219
+ </div>
220
+ {message && (
221
+ <div className="text-xs text-muted-foreground">{message}</div>
222
+ )}
223
+ </section>
224
+ );
225
+ }
226
+
227
+ const shortId = config!.instanceId.slice(0, 8) + "…";
228
+ const consentLabel = guardrails?.consentStatus ?? "unknown";
229
+ const hookLabel = guardrails?.prePushHookInstalled
230
+ ? `v${guardrails.prePushHookVersion}`
231
+ : "not installed";
232
+ const blockedLabel = guardrails?.pushRemoteBlocked.length
233
+ ? guardrails.pushRemoteBlocked.join(", ")
234
+ : "none";
235
+ const lastCheck = upgrade?.lastPolledAt
236
+ ? new Date(upgrade.lastPolledAt * 1000).toLocaleString()
237
+ : "never";
238
+ const lastUpgrade = upgrade?.lastSuccessfulUpgradeAt
239
+ ? new Date(upgrade.lastSuccessfulUpgradeAt * 1000).toLocaleString()
240
+ : "never";
241
+ const pollFailing = (upgrade?.pollFailureCount ?? 0) > 0;
242
+
243
+ const upgradeAvailable = upgrade?.upgradeAvailable ?? false;
244
+ const upgradeCount = upgrade?.commitsBehind ?? 0;
245
+ const startUpgradeDisabled = busy !== null || !upgradeAvailable;
246
+ const startUpgradeTitle = upgradeAvailable
247
+ ? `Merge ${upgradeCount} upstream commit${upgradeCount === 1 ? "" : "s"} into ${config!.branchName}`
248
+ : "No upgrades available — click 'Check for upgrades' to refresh";
249
+ const statusMessage = pollFailing && upgrade?.lastPollError
250
+ ? upgrade.lastPollError
251
+ : message;
252
+ const statusToneClass = pollFailing
253
+ ? "text-amber-700 dark:text-amber-400"
254
+ : "text-muted-foreground";
255
+
256
+ return (
257
+ <section className="rounded-xl border bg-card">
258
+ <header className="flex items-start justify-between gap-4 px-5 py-3 border-b flex-wrap">
259
+ <div className="min-w-0 space-y-2">
260
+ <div className="flex items-center gap-3 min-w-0 flex-wrap">
261
+ <h2 className="text-base font-semibold">Instance</h2>
262
+ {upgradeAvailable && (
263
+ <Badge
264
+ variant="outline"
265
+ className="text-xs font-normal border-blue-500/40 bg-blue-500/10 text-blue-700 dark:text-blue-400"
266
+ >
267
+ {upgradeCount} update{upgradeCount === 1 ? "" : "s"} available
268
+ </Badge>
269
+ )}
270
+ {pollFailing && (
271
+ <Badge variant="destructive" className="text-xs font-normal">
272
+ Poll failing ({upgrade?.pollFailureCount})
273
+ </Badge>
274
+ )}
275
+ </div>
276
+ <p className="text-xs text-muted-foreground leading-relaxed max-w-prose">
277
+ Pull latest changes from{" "}
278
+ <code className="font-mono text-[11px] px-1 py-0.5 rounded bg-muted">
279
+ main
280
+ </code>{" "}
281
+ into{" "}
282
+ <code className="font-mono text-[11px] px-1 py-0.5 rounded bg-muted">
283
+ {config!.branchName}
284
+ </code>
285
+ . Nothing is pushed automatically.
286
+ </p>
287
+ </div>
288
+ <div className="flex items-center gap-2 shrink-0 flex-wrap">
289
+ <Button
290
+ variant="outline"
291
+ size="sm"
292
+ onClick={checkNow}
293
+ disabled={busy !== null}
294
+ >
295
+ {busy === "check" ? "Checking…" : "Check"}
296
+ </Button>
297
+ <Button
298
+ variant="default"
299
+ size="sm"
300
+ onClick={startUpgrade}
301
+ disabled={startUpgradeDisabled}
302
+ title={startUpgradeTitle}
303
+ >
304
+ {busy === "upgrade"
305
+ ? "Starting…"
306
+ : upgradeAvailable
307
+ ? `Upgrade (${upgradeCount})`
308
+ : "Upgrade"}
309
+ </Button>
310
+ <Button
311
+ variant="ghost"
312
+ size="sm"
313
+ onClick={reinit}
314
+ disabled={busy !== null}
315
+ >
316
+ {busy === "init" ? "Running…" : "Repair setup"}
317
+ </Button>
318
+ </div>
319
+ </header>
320
+
321
+ <dl className="grid grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-2 px-5 py-3 text-sm">
322
+ <Field label="Branch" mono>
323
+ {config!.branchName}
324
+ </Field>
325
+ <Field
326
+ label="Instance ID"
327
+ mono
328
+ title={config!.instanceId}
329
+ >
330
+ {shortId}
331
+ </Field>
332
+ <Field label="Last check">{lastCheck}</Field>
333
+ <Field label="Last upgrade">{lastUpgrade}</Field>
334
+ </dl>
335
+
336
+ <div className="flex items-start justify-between gap-3 border-t px-5 py-2.5 text-[11px]">
337
+ <p className={`leading-relaxed ${statusToneClass}`}>
338
+ {statusMessage ?? (
339
+ upgradeAvailable
340
+ ? `Ready to merge ${upgradeCount} upstream update${upgradeCount === 1 ? "" : "s"}.`
341
+ : `Up to date. Last checked: ${lastCheck}.`
342
+ )}
343
+ </p>
344
+ <p className="shrink-0 text-muted-foreground">
345
+ Repairs local setup without changing data or commits.
346
+ </p>
347
+ </div>
348
+ </section>
349
+ );
350
+ }
351
+
352
+ function Field({
353
+ label,
354
+ children,
355
+ mono,
356
+ truncate,
357
+ title,
358
+ }: {
359
+ label: string;
360
+ children: ReactNode;
361
+ mono?: boolean;
362
+ truncate?: boolean;
363
+ title?: string;
364
+ }) {
365
+ return (
366
+ <div className="min-w-0">
367
+ <dt className="text-[11px] uppercase tracking-wide text-muted-foreground">
368
+ {label}
369
+ </dt>
370
+ <dd
371
+ title={title}
372
+ className={
373
+ "mt-0.5 " +
374
+ (mono ? "font-mono text-xs " : "") +
375
+ (truncate ? "truncate" : "")
376
+ }
377
+ >
378
+ {children}
379
+ </dd>
380
+ </div>
381
+ );
382
+ }