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
@@ -1,20 +1,17 @@
1
1
  "use client";
2
2
 
3
3
  import { useState, useCallback, useEffect, useMemo } from "react";
4
- import { useRouter } from "next/navigation";
5
- import type { ConversationRow, ChatMessageRow } from "@/lib/db/schema";
4
+ import type { ConversationRow } from "@/lib/db/schema";
6
5
  import type { PromptCategory } from "@/lib/chat/types";
7
- import { DEFAULT_CHAT_MODEL, CHAT_MODELS, getRuntimeForModel, type ChatModelOption } from "@/lib/chat/types";
8
- import { usePersistedState } from "@/hooks/use-persisted-state";
6
+ import { useChatSession } from "./chat-session-provider";
9
7
  import { ConversationList } from "./conversation-list";
10
8
  import { ChatMessageList } from "./chat-message-list";
11
9
  import { ChatInput } from "./chat-input";
12
- import type { MentionReference } from "@/hooks/use-chat-autocomplete";
13
10
  import { ChatEmptyState } from "./chat-empty-state";
14
11
  import { ChatActivityIndicator } from "./chat-activity-indicator";
15
12
  import { Button } from "@/components/ui/button";
16
13
  import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
17
- import { MessageCircle, PanelRightOpen } from "lucide-react";
14
+ import { PanelRightOpen } from "lucide-react";
18
15
 
19
16
  interface ChatShellProps {
20
17
  initialConversations: ConversationRow[];
@@ -22,60 +19,60 @@ interface ChatShellProps {
22
19
  initialActiveId?: string | null;
23
20
  }
24
21
 
22
+ /**
23
+ * Thin view component for the /chat route. All chat-domain state lives in
24
+ * `ChatSessionProvider` (rendered from `src/app/layout.tsx`), so unmounting
25
+ * this component — e.g., when the user navigates to another sidebar view —
26
+ * does not touch the in-flight SSE reader loop or clear any messages. On
27
+ * remount, we read the provider's current state and render it directly.
28
+ *
29
+ * See `features/chat-session-persistence-provider.md`.
30
+ */
25
31
  export function ChatShell({
26
32
  initialConversations,
27
33
  promptCategories,
28
34
  initialActiveId,
29
35
  }: ChatShellProps) {
30
- const router = useRouter();
31
- const [conversations, setConversations] =
32
- useState<ConversationRow[]>(initialConversations);
33
- const [activeId, setActiveId] = useState<string | null>(null);
34
- const [messages, setMessages] = useState<ChatMessageRow[]>([]);
35
- const [isStreaming, setIsStreaming] = useState(false);
36
- const [abortController, setAbortController] =
37
- useState<AbortController | null>(null);
36
+ const session = useChatSession();
37
+ const {
38
+ conversations,
39
+ activeId,
40
+ messages,
41
+ isStreaming,
42
+ modelId,
43
+ availableModels,
44
+ hydrated,
45
+ hydrate,
46
+ setActiveConversation,
47
+ sendMessage,
48
+ stopStreaming,
49
+ createConversation,
50
+ deleteConversation,
51
+ renameConversation,
52
+ setMessageStatus,
53
+ setModelId,
54
+ } = session;
55
+
56
+ // View-local state only
38
57
  const [mobileListOpen, setMobileListOpen] = useState(false);
39
58
  const [hoverPreview, setHoverPreview] = useState<string | null>(null);
40
- const [modelId, setModelId] = useState(DEFAULT_CHAT_MODEL);
41
- const [availableModels, setAvailableModels] = useState<ChatModelOption[]>(CHAT_MODELS);
42
59
 
43
- // Persistence via localStorage fallback
44
- const [persistedActiveId, setPersistedActiveId] = usePersistedState<string>("stagent-active-chat", "");
45
-
46
- const activeConversation = conversations.find((c) => c.id === activeId);
47
-
48
- // Restore active conversation on mount
49
- // Read localStorage synchronously to avoid race with usePersistedState's async useEffect
60
+ // Hydrate provider once with the server-rendered conversation list.
61
+ // Subsequent remounts are no-ops — the provider preserves its state.
50
62
  useEffect(() => {
51
- let restoredId = initialActiveId || null;
52
- if (!restoredId) {
53
- try {
54
- restoredId = localStorage.getItem("stagent-active-chat") || null;
55
- } catch { /* localStorage unavailable */ }
56
- }
57
- if (restoredId && conversations.some((c) => c.id === restoredId)) {
58
- setActiveId(restoredId);
59
- setPersistedActiveId(restoredId);
60
- // Fetch messages for restored conversation
61
- fetch(`/api/chat/conversations/${restoredId}/messages`)
62
- .then((r) => r.ok ? r.json() : [])
63
- .then((msgs) => setMessages(msgs))
64
- .catch(() => setMessages([]));
65
- }
63
+ hydrate({
64
+ conversations: initialConversations,
65
+ initialActiveId: initialActiveId ?? null,
66
+ });
67
+ // Intentionally run only on mount: initialConversations is the
68
+ // server-rendered snapshot for this specific page visit.
66
69
  // eslint-disable-next-line react-hooks/exhaustive-deps
67
70
  }, []);
68
71
 
69
- // Sync activeId to URL and localStorage
70
- const updateActiveId = useCallback((id: string | null) => {
71
- setActiveId(id);
72
- setPersistedActiveId(id ?? "");
73
- if (id) {
74
- router.replace(`/chat?c=${id}`, { scroll: false });
75
- } else {
76
- router.replace("/chat", { scroll: false });
77
- }
78
- }, [router, setPersistedActiveId]);
72
+ const activeConversation = useMemo(
73
+ () => conversations.find((c) => c.id === activeId),
74
+ [conversations, activeId]
75
+ );
79
76
 
80
77
  // Extract spawned task IDs from messages (execute_task tool results)
81
78
  const spawnedTaskIds = useMemo(() => {
@@ -83,9 +80,14 @@ export function ChatShell({
83
80
  for (const msg of messages) {
84
81
  if (msg.metadata) {
85
82
  try {
86
- const meta = typeof msg.metadata === "string" ? JSON.parse(msg.metadata) : msg.metadata;
87
- // Check for execute_task tool result in metadata
88
- if (meta.type === "permission_request" && meta.toolName === "mcp__stagent__execute_task") {
83
+ const meta =
84
+ typeof msg.metadata === "string"
85
+ ? JSON.parse(msg.metadata)
86
+ : msg.metadata;
87
+ if (
88
+ meta.type === "permission_request" &&
89
+ meta.toolName === "mcp__stagent__execute_task"
90
+ ) {
89
91
  const input = meta.toolInput;
90
92
  if (input?.taskId) taskIds.push(input.taskId);
91
93
  }
@@ -93,379 +95,63 @@ export function ChatShell({
93
95
  // Ignore parse errors
94
96
  }
95
97
  }
96
- // Also scan assistant message content for task execution confirmations
97
98
  if (msg.role === "assistant" && msg.content) {
98
- const taskIdMatch = msg.content.match(/Execution started.*?taskId["\s:]+([a-f0-9-]{36})/i);
99
+ const taskIdMatch = msg.content.match(
100
+ /Execution started.*?taskId["\s:]+([a-f0-9-]{36})/i
101
+ );
99
102
  if (taskIdMatch) taskIds.push(taskIdMatch[1]);
100
103
  }
101
104
  }
102
105
  return [...new Set(taskIds)];
103
106
  }, [messages]);
104
107
 
105
- // Fetch default model and available models on mount
106
- useEffect(() => {
107
- fetch("/api/settings/chat")
108
- .then((r) => r.ok ? r.json() : null)
109
- .then((data) => {
110
- if (data?.defaultModel) setModelId(data.defaultModel);
111
- })
112
- .catch(() => {});
113
-
114
- fetch("/api/chat/models")
115
- .then((r) => r.ok ? r.json() : null)
116
- .then((models) => {
117
- if (models?.length) setAvailableModels(models);
118
- })
119
- .catch(() => {});
120
- }, []);
121
-
122
- // ── Conversation Management ──────────────────────────────────────────
123
-
108
+ // ── Action wrappers ──────────────────────────────────────────────────
124
109
  const handleNewChat = useCallback(async () => {
125
- try {
126
- const res = await fetch("/api/chat/conversations", {
127
- method: "POST",
128
- headers: { "Content-Type": "application/json" },
129
- body: JSON.stringify({ runtimeId: getRuntimeForModel(modelId), modelId }),
130
- });
131
- if (!res.ok) return;
132
- const conversation = await res.json();
133
- setConversations((prev) => [conversation, ...prev]);
134
- updateActiveId(conversation.id);
135
- setMessages([]);
136
- setMobileListOpen(false);
137
- } catch {
138
- // Handle error silently
139
- }
140
- }, [modelId, updateActiveId]);
141
-
142
- const handleSelectConversation = useCallback(async (id: string) => {
143
- updateActiveId(id);
110
+ await createConversation();
144
111
  setMobileListOpen(false);
145
- try {
146
- const [msgRes, convRes] = await Promise.all([
147
- fetch(`/api/chat/conversations/${id}/messages`),
148
- fetch(`/api/chat/conversations/${id}`),
149
- ]);
150
- if (msgRes.ok) {
151
- const msgs = await msgRes.json();
152
- // Clean up stale "streaming" messages from interrupted sessions
153
- setMessages(
154
- msgs.map((m: ChatMessageRow) =>
155
- m.status === "streaming" ? { ...m, status: "complete" as const } : m
156
- )
157
- );
158
- }
159
- if (convRes.ok) {
160
- const conv = await convRes.json();
161
- if (conv.modelId) setModelId(conv.modelId);
162
- }
163
- } catch {
164
- setMessages([]);
165
- }
166
- }, [updateActiveId]);
112
+ }, [createConversation]);
167
113
 
168
- const handleDeleteConversation = useCallback(
169
- async (id: string) => {
170
- try {
171
- await fetch(`/api/chat/conversations/${id}`, {
172
- method: "DELETE",
173
- });
174
- setConversations((prev) => prev.filter((c) => c.id !== id));
175
- if (activeId === id) {
176
- updateActiveId(null);
177
- setMessages([]);
178
- }
179
- } catch {
180
- // Handle error silently
181
- }
114
+ const handleSelectConversation = useCallback(
115
+ (id: string) => {
116
+ setActiveConversation(id);
117
+ setMobileListOpen(false);
182
118
  },
183
- [activeId, updateActiveId]
119
+ [setActiveConversation]
184
120
  );
185
121
 
186
- const handleRenameConversation = useCallback(
187
- async (id: string, title: string) => {
188
- try {
189
- const res = await fetch(`/api/chat/conversations/${id}`, {
190
- method: "PATCH",
191
- headers: { "Content-Type": "application/json" },
192
- body: JSON.stringify({ title }),
193
- });
194
- if (res.ok) {
195
- const updated = await res.json();
196
- setConversations((prev) =>
197
- prev.map((c) => (c.id === id ? updated : c))
198
- );
199
- }
200
- } catch {
201
- // Handle error silently
202
- }
203
- },
204
- []
122
+ const handleDeleteConversation = useCallback(
123
+ (id: string) => deleteConversation(id),
124
+ [deleteConversation]
205
125
  );
206
126
 
207
- // ── Message Sending ──────────────────────────────────────────────────
208
-
209
- const handleSend = useCallback(
210
- async (content: string, mentions?: MentionReference[]) => {
211
- let conversationId = activeId;
212
-
213
- // Create conversation on first message if none active
214
- if (!conversationId) {
215
- try {
216
- const res = await fetch("/api/chat/conversations", {
217
- method: "POST",
218
- headers: { "Content-Type": "application/json" },
219
- body: JSON.stringify({ runtimeId: getRuntimeForModel(modelId), modelId }),
220
- });
221
- if (!res.ok) return;
222
- const conversation = await res.json();
223
- setConversations((prev) => [conversation, ...prev]);
224
- updateActiveId(conversation.id);
225
- conversationId = conversation.id;
226
- } catch {
227
- return;
228
- }
229
- }
230
-
231
- // Add optimistic user message
232
- const userMsg: ChatMessageRow = {
233
- id: crypto.randomUUID(),
234
- conversationId: conversationId!,
235
- role: "user",
236
- content,
237
- metadata: null,
238
- status: "complete",
239
- createdAt: new Date(),
240
- };
241
- setMessages((prev) => [...prev, userMsg]);
242
-
243
- // Add placeholder assistant message
244
- const assistantMsgId = crypto.randomUUID();
245
- const assistantMsg: ChatMessageRow = {
246
- id: assistantMsgId,
247
- conversationId: conversationId!,
248
- role: "assistant",
249
- content: "",
250
- metadata: null,
251
- status: "streaming",
252
- createdAt: new Date(),
253
- };
254
- setMessages((prev) => [...prev, assistantMsg]);
255
-
256
- setIsStreaming(true);
257
- const controller = new AbortController();
258
- setAbortController(controller);
259
-
260
- try {
261
- const res = await fetch(
262
- `/api/chat/conversations/${conversationId}/messages`,
263
- {
264
- method: "POST",
265
- headers: { "Content-Type": "application/json" },
266
- body: JSON.stringify({ content, mentions }),
267
- signal: controller.signal,
268
- }
269
- );
270
-
271
- if (!res.ok || !res.body) {
272
- throw new Error("Failed to send message");
273
- }
274
-
275
- const reader = res.body.getReader();
276
- const decoder = new TextDecoder();
277
- let buffer = "";
278
-
279
- while (true) {
280
- const { done, value } = await reader.read();
281
- if (done) break;
282
-
283
- buffer += decoder.decode(value, { stream: true });
284
- const lines = buffer.split("\n");
285
- buffer = lines.pop() ?? "";
286
-
287
- for (const line of lines) {
288
- if (!line.startsWith("data: ")) continue;
289
- const json = line.slice(6);
290
- try {
291
- const event = JSON.parse(json);
292
- if (event.type === "status") {
293
- setMessages((prev) =>
294
- prev.map((m) =>
295
- m.id === assistantMsgId
296
- ? { ...m, metadata: JSON.stringify({ statusPhase: event.phase, statusMessage: event.message }) }
297
- : m
298
- )
299
- );
300
- } else if (event.type === "delta") {
301
- setMessages((prev) =>
302
- prev.map((m) =>
303
- m.id === assistantMsgId
304
- ? { ...m, content: m.content + event.content }
305
- : m
306
- )
307
- );
308
- } else if (event.type === "done") {
309
- setMessages((prev) =>
310
- prev.map((m) =>
311
- m.id === assistantMsgId
312
- ? {
313
- ...m,
314
- id: event.messageId,
315
- status: "complete",
316
- metadata: (() => {
317
- const existing = m.metadata
318
- ? (() => { try { return JSON.parse(m.metadata!); } catch { return {}; } })()
319
- : {};
320
- if (event.quickAccess?.length) {
321
- existing.quickAccess = event.quickAccess;
322
- }
323
- return JSON.stringify(existing);
324
- })(),
325
- }
326
- : m
327
- )
328
- );
329
- // Refresh conversation from API to get auto-generated title
330
- fetch(`/api/chat/conversations/${conversationId}`)
331
- .then((r) => r.ok ? r.json() : null)
332
- .then((conv) => {
333
- if (conv) {
334
- setConversations((prev) =>
335
- prev.map((c) =>
336
- c.id === conversationId
337
- ? { ...c, title: conv.title, updatedAt: new Date() }
338
- : c
339
- )
340
- );
341
- }
342
- })
343
- .catch(() => {});
344
- } else if (event.type === "permission_request" || event.type === "question") {
345
- // Insert system message for inline permission/question UI
346
- const systemMsg = {
347
- id: event.messageId,
348
- conversationId: conversationId!,
349
- role: "system" as const,
350
- content: event.type === "permission_request"
351
- ? `Permission required: ${event.toolName}`
352
- : "Agent has a question",
353
- metadata: JSON.stringify(event.type === "permission_request"
354
- ? { type: "permission_request", requestId: event.requestId, toolName: event.toolName, toolInput: event.toolInput }
355
- : { type: "question", requestId: event.requestId, questions: event.questions }
356
- ),
357
- status: "pending" as const,
358
- createdAt: new Date(),
359
- };
360
- setMessages((prev) => [...prev, systemMsg]);
361
- } else if (event.type === "screenshot") {
362
- // Append screenshot attachment to assistant message metadata
363
- setMessages((prev) =>
364
- prev.map((m) => {
365
- if (m.id !== assistantMsgId) return m;
366
- const meta = m.metadata ? (() => { try { return JSON.parse(m.metadata!); } catch { return {}; } })() : {};
367
- const attachments = Array.isArray(meta.attachments) ? meta.attachments : [];
368
- attachments.push({
369
- documentId: event.documentId,
370
- thumbnailUrl: event.thumbnailUrl,
371
- originalUrl: event.originalUrl,
372
- width: event.width,
373
- height: event.height,
374
- });
375
- return { ...m, metadata: JSON.stringify({ ...meta, attachments }) };
376
- })
377
- );
378
- } else if (event.type === "error") {
379
- setMessages((prev) =>
380
- prev.map((m) =>
381
- m.id === assistantMsgId
382
- ? {
383
- ...m,
384
- content: m.content || event.message,
385
- status: "error",
386
- }
387
- : m
388
- )
389
- );
390
- }
391
- } catch {
392
- // Ignore malformed SSE data
393
- }
394
- }
395
- }
396
- } catch (error) {
397
- if ((error as Error).name !== "AbortError") {
398
- setMessages((prev) =>
399
- prev.map((m) =>
400
- m.id === assistantMsgId
401
- ? {
402
- ...m,
403
- content:
404
- m.content || "Failed to get response. Please try again.",
405
- status: "error",
406
- }
407
- : m
408
- )
409
- );
410
- }
411
- } finally {
412
- setIsStreaming(false);
413
- setAbortController(null);
414
- }
415
- },
416
- [activeId, modelId, updateActiveId]
127
+ const handleRenameConversation = useCallback(
128
+ (id: string, title: string) => renameConversation(id, title),
129
+ [renameConversation]
417
130
  );
418
131
 
419
- const handleStop = useCallback(() => {
420
- abortController?.abort();
421
- }, [abortController]);
422
-
423
132
  const handleSuggestionClick = useCallback(
424
133
  (prompt: string) => {
425
- handleSend(prompt);
134
+ void sendMessage(prompt);
426
135
  },
427
- [handleSend]
136
+ [sendMessage]
428
137
  );
429
138
 
430
139
  const handleMessageStatusChange = useCallback(
431
140
  (messageId: string, status: string) => {
432
- setMessages((prev) =>
433
- prev.map((m) =>
434
- m.id === messageId
435
- ? { ...m, status: status as "pending" | "streaming" | "complete" | "error" }
436
- : m
437
- )
141
+ setMessageStatus(
142
+ messageId,
143
+ status as "pending" | "streaming" | "complete" | "error"
438
144
  );
439
145
  },
440
- []
146
+ [setMessageStatus]
441
147
  );
442
148
 
443
- const handleModelChange = useCallback(
444
- async (newModelId: string) => {
445
- setModelId(newModelId);
446
- // If there's an active conversation, update both modelId and runtimeId
447
- if (activeId) {
448
- const newRuntimeId = getRuntimeForModel(newModelId);
449
- await fetch(`/api/chat/conversations/${activeId}`, {
450
- method: "PATCH",
451
- headers: { "Content-Type": "application/json" },
452
- body: JSON.stringify({ modelId: newModelId, runtimeId: newRuntimeId }),
453
- }).catch(() => {});
454
- // Update local state so conversation list reflects the change
455
- setConversations((prev) =>
456
- prev.map((c) =>
457
- c.id === activeId
458
- ? { ...c, modelId: newModelId, runtimeId: newRuntimeId }
459
- : c
460
- )
461
- );
462
- }
463
- },
464
- [activeId]
465
- );
149
+ // Suppress unused warnings from props we still accept but no longer own.
150
+ // `hydrated` tells us whether the provider has data — we can use it to
151
+ // skip the empty-state flash on a remount that finds existing state.
152
+ void hydrated;
466
153
 
467
154
  // ── Render ───────────────────────────────────────────────────────────
468
-
469
155
  const conversationListContent = (
470
156
  <ConversationList
471
157
  conversations={conversations}
@@ -507,13 +193,13 @@ export function ChatShell({
507
193
  onHoverPreview={setHoverPreview}
508
194
  >
509
195
  <ChatInput
510
- onSend={handleSend}
511
- onStop={handleStop}
196
+ onSend={sendMessage}
197
+ onStop={stopStreaming}
512
198
  isStreaming={isStreaming}
513
199
  isHeroMode
514
200
  previewText={hoverPreview}
515
201
  modelId={modelId}
516
- onModelChange={handleModelChange}
202
+ onModelChange={setModelId}
517
203
  availableModels={availableModels}
518
204
  projectId={activeConversation?.projectId}
519
205
  />
@@ -523,7 +209,12 @@ export function ChatShell({
523
209
  <>
524
210
  {/* Messages */}
525
211
  <div className="flex-1 overflow-hidden">
526
- <ChatMessageList messages={messages} isStreaming={isStreaming} conversationId={activeId ?? undefined} onMessageStatusChange={handleMessageStatusChange} />
212
+ <ChatMessageList
213
+ messages={messages}
214
+ isStreaming={isStreaming}
215
+ conversationId={activeId ?? undefined}
216
+ onMessageStatusChange={handleMessageStatusChange}
217
+ />
527
218
  </div>
528
219
 
529
220
  {/* Background activity indicator */}
@@ -533,12 +224,12 @@ export function ChatShell({
533
224
 
534
225
  {/* Docked input */}
535
226
  <ChatInput
536
- onSend={handleSend}
537
- onStop={handleStop}
227
+ onSend={sendMessage}
228
+ onStop={stopStreaming}
538
229
  isStreaming={isStreaming}
539
230
  isHeroMode={false}
540
231
  modelId={modelId}
541
- onModelChange={handleModelChange}
232
+ onModelChange={setModelId}
542
233
  availableModels={availableModels}
543
234
  projectId={activeConversation?.projectId}
544
235
  />