stagent 0.10.0 → 0.11.1

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 (176) hide show
  1. package/README.md +44 -31
  2. package/dist/cli.js +24 -0
  3. package/docs/.coverage-gaps.json +154 -24
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/agent-intelligence.md +12 -2
  6. package/docs/features/chat.md +40 -5
  7. package/docs/features/cost-usage.md +1 -1
  8. package/docs/features/documents.md +5 -2
  9. package/docs/features/inbox-notifications.md +10 -2
  10. package/docs/features/keyboard-navigation.md +12 -3
  11. package/docs/features/provider-runtimes.md +16 -2
  12. package/docs/features/settings.md +2 -2
  13. package/docs/features/shared-components.md +7 -3
  14. package/docs/features/tables.md +3 -1
  15. package/docs/features/tool-permissions.md +6 -2
  16. package/docs/features/workflows.md +6 -2
  17. package/docs/getting-started.md +1 -1
  18. package/docs/index.md +1 -1
  19. package/docs/journeys/developer.md +25 -2
  20. package/docs/journeys/personal-use.md +12 -5
  21. package/docs/journeys/power-user.md +45 -14
  22. package/docs/journeys/work-use.md +17 -8
  23. package/docs/manifest.json +15 -15
  24. package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +2 -2
  25. package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
  26. package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
  27. package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
  28. package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
  29. package/next.config.mjs +1 -0
  30. package/package.json +3 -3
  31. package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
  32. package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
  33. package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
  34. package/src/app/api/chat/export/route.ts +52 -0
  35. package/src/app/api/chat/files/search/route.ts +50 -0
  36. package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
  37. package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
  38. package/src/app/api/environment/skills/route.ts +13 -0
  39. package/src/app/api/schedules/[id]/execute/route.ts +2 -2
  40. package/src/app/api/settings/chat/pins/route.ts +94 -0
  41. package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
  42. package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
  43. package/src/app/api/settings/environment/route.ts +26 -0
  44. package/src/app/api/tasks/[id]/execute/route.ts +52 -12
  45. package/src/app/api/tasks/[id]/respond/route.ts +31 -15
  46. package/src/app/api/tasks/[id]/resume/route.ts +24 -3
  47. package/src/app/documents/page.tsx +4 -1
  48. package/src/app/settings/page.tsx +2 -0
  49. package/src/components/book/content-blocks.tsx +1 -1
  50. package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
  51. package/src/components/chat/__tests__/chat-session-provider.test.tsx +166 -1
  52. package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
  53. package/src/components/chat/capability-banner.tsx +68 -0
  54. package/src/components/chat/chat-command-popover.tsx +668 -47
  55. package/src/components/chat/chat-input.tsx +103 -8
  56. package/src/components/chat/chat-message.tsx +12 -3
  57. package/src/components/chat/chat-session-provider.tsx +73 -3
  58. package/src/components/chat/chat-shell.tsx +62 -3
  59. package/src/components/chat/command-tab-bar.tsx +68 -0
  60. package/src/components/chat/conversation-template-picker.tsx +421 -0
  61. package/src/components/chat/help-dialog.tsx +39 -0
  62. package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
  63. package/src/components/chat/skill-row.tsx +147 -0
  64. package/src/components/documents/document-browser.tsx +37 -19
  65. package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
  66. package/src/components/notifications/permission-response-actions.tsx +155 -1
  67. package/src/components/playbook/playbook-detail-view.tsx +1 -1
  68. package/src/components/settings/environment-section.tsx +102 -0
  69. package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
  70. package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
  71. package/src/components/shared/command-palette.tsx +262 -2
  72. package/src/components/shared/filter-hint.tsx +70 -0
  73. package/src/components/shared/filter-input.tsx +59 -0
  74. package/src/components/shared/saved-searches-manager.tsx +199 -0
  75. package/src/components/tasks/task-bento-grid.tsx +12 -2
  76. package/src/components/tasks/task-card.tsx +3 -0
  77. package/src/components/tasks/task-chip-bar.tsx +30 -1
  78. package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
  79. package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
  80. package/src/hooks/use-active-skills.ts +110 -0
  81. package/src/hooks/use-chat-autocomplete.ts +120 -7
  82. package/src/hooks/use-enriched-skills.ts +19 -0
  83. package/src/hooks/use-pinned-entries.ts +104 -0
  84. package/src/hooks/use-recent-user-messages.ts +19 -0
  85. package/src/hooks/use-saved-searches.ts +142 -0
  86. package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
  87. package/src/lib/agents/__tests__/claude-agent.test.ts +17 -4
  88. package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
  89. package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
  90. package/src/lib/agents/claude-agent.ts +105 -46
  91. package/src/lib/agents/handoff/bus.ts +2 -2
  92. package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
  93. package/src/lib/agents/profiles/__tests__/registry.test.ts +47 -0
  94. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +30 -3
  95. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +6 -2
  96. package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
  97. package/src/lib/agents/profiles/registry.ts +97 -22
  98. package/src/lib/agents/profiles/types.ts +7 -1
  99. package/src/lib/agents/router.ts +3 -6
  100. package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
  101. package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
  102. package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
  103. package/src/lib/agents/runtime/catalog.ts +121 -0
  104. package/src/lib/agents/runtime/claude-sdk.ts +32 -0
  105. package/src/lib/agents/runtime/execution-target.ts +456 -0
  106. package/src/lib/agents/runtime/index.ts +4 -0
  107. package/src/lib/agents/runtime/launch-failure.ts +101 -0
  108. package/src/lib/agents/runtime/openai-codex.ts +35 -0
  109. package/src/lib/agents/runtime/openai-direct.ts +8 -0
  110. package/src/lib/agents/task-dispatch.ts +220 -0
  111. package/src/lib/agents/tool-permissions.ts +16 -1
  112. package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
  113. package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
  114. package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
  115. package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
  116. package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
  117. package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
  118. package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
  119. package/src/lib/chat/__tests__/types.test.ts +28 -0
  120. package/src/lib/chat/active-skills.ts +31 -0
  121. package/src/lib/chat/clean-filter-input.ts +30 -0
  122. package/src/lib/chat/codex-engine.ts +30 -7
  123. package/src/lib/chat/command-tabs.ts +61 -0
  124. package/src/lib/chat/context-builder.ts +141 -1
  125. package/src/lib/chat/dismissals.ts +73 -0
  126. package/src/lib/chat/engine.ts +109 -15
  127. package/src/lib/chat/files/__tests__/search.test.ts +135 -0
  128. package/src/lib/chat/files/expand-mention.ts +76 -0
  129. package/src/lib/chat/files/search.ts +99 -0
  130. package/src/lib/chat/skill-composition.ts +210 -0
  131. package/src/lib/chat/skill-conflict.ts +105 -0
  132. package/src/lib/chat/stagent-tools.ts +6 -19
  133. package/src/lib/chat/stream-telemetry.ts +9 -4
  134. package/src/lib/chat/system-prompt.ts +22 -0
  135. package/src/lib/chat/tool-catalog.ts +33 -3
  136. package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
  137. package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
  138. package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
  139. package/src/lib/chat/tools/__tests__/task-tools.test.ts +47 -0
  140. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +134 -0
  141. package/src/lib/chat/tools/blueprint-tools.ts +190 -0
  142. package/src/lib/chat/tools/helpers.ts +2 -0
  143. package/src/lib/chat/tools/profile-tools.ts +120 -23
  144. package/src/lib/chat/tools/skill-tools.ts +183 -0
  145. package/src/lib/chat/tools/task-tools.ts +6 -2
  146. package/src/lib/chat/tools/workflow-tools.ts +61 -20
  147. package/src/lib/chat/types.ts +15 -0
  148. package/src/lib/constants/settings.ts +2 -0
  149. package/src/lib/data/clear.ts +2 -6
  150. package/src/lib/db/bootstrap.ts +17 -0
  151. package/src/lib/db/schema.ts +26 -0
  152. package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
  153. package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
  154. package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
  155. package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
  156. package/src/lib/environment/data.ts +9 -0
  157. package/src/lib/environment/list-skills.ts +176 -0
  158. package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
  159. package/src/lib/environment/parsers/skill.ts +26 -5
  160. package/src/lib/environment/profile-generator.ts +56 -2
  161. package/src/lib/environment/skill-enrichment.ts +106 -0
  162. package/src/lib/environment/skill-recommendations.ts +66 -0
  163. package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
  164. package/src/lib/filters/__tests__/parse.test.ts +135 -0
  165. package/src/lib/filters/parse.ts +86 -0
  166. package/src/lib/instance/__tests__/detect.test.ts +1 -1
  167. package/src/lib/instance/__tests__/upgrade-poller.test.ts +50 -0
  168. package/src/lib/instance/fingerprint.ts +8 -10
  169. package/src/lib/instance/upgrade-poller.ts +53 -1
  170. package/src/lib/schedules/scheduler.ts +4 -4
  171. package/src/lib/utils/stagent-paths.ts +4 -0
  172. package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
  173. package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
  174. package/src/lib/workflows/blueprints/types.ts +6 -0
  175. package/src/lib/workflows/engine.ts +5 -3
  176. package/src/test/setup.ts +10 -0
@@ -0,0 +1,399 @@
1
+ # Chat Session Persistence Provider — Closeout Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Close out the `chat-session-persistence-provider` feature by filling the remaining AC gaps (telemetry code + doc comment + smoke test) and flipping the spec status from `planned` → `completed`.
6
+
7
+ **Architecture:** The provider, layout wiring, `ChatShell` refactor, and unit tests are already shipped. What remains is the `client.stream.view-remount` telemetry code described in spec §5 (a documented reason code plus a useEffect cleanup emitter), followed by a real browser smoke test per the spec's manual repro steps, and finally status + changelog updates.
8
+
9
+ **Tech Stack:** Next.js 16 App Router, React 19 client context, Vitest + @testing-library/react, SSE readers via `fetch().body.getReader()`.
10
+
11
+ ---
12
+
13
+ ## NOT in scope
14
+
15
+ - **SSE resume protocol (`lastEventId` replay).** Spec "Scope Boundaries" explicitly defers this; the provider preserves state across view switches but not across full page reloads. Unchanged.
16
+ - **Web Worker isolation for the SSE reader.** Still deferred per spec.
17
+ - **Multi-tab BroadcastChannel sync.** Out of scope per spec.
18
+ - **Server-side engine / reconcile / route-handler changes.** The provider fix is purely client-architecture; server code stays untouched.
19
+ - **Provider or ChatShell rewrite.** Both are already correct. This plan only *augments* them with the telemetry hook.
20
+ - **New TDR.** Spec notes a TDR is only warranted if the layout-provider pattern gets reused (e.g., workflow execution state). It hasn't been, so no TDR.
21
+
22
+ ## What already exists
23
+
24
+ | Artifact | Location | State |
25
+ |---|---|---|
26
+ | `ChatSessionProvider` with full action surface | `src/components/chat/chat-session-provider.tsx` (720 LOC) | Shipped. Holds `conversations`, `activeId`, `messagesByConversation`, `streamingState` (with `AbortController`), `modelId`, `availableModels`, `hydrated`. |
27
+ | Provider mounted in root layout | `src/app/layout.tsx:101,114` wraps `<main>` | Shipped. |
28
+ | `ChatShell` refactored to thin consumer | `src/components/chat/chat-shell.tsx` | Shipped. Zero chat-domain `useState`; only `mobileListOpen` + `hoverPreview` remain (both view-local). |
29
+ | `setMessages([])` catch-all removed | `chat-session-provider.tsx:198` | Shipped. Only appears in comments documenting the old bug. |
30
+ | Provider unit tests (4/4 green) | `src/components/chat/__tests__/chat-session-provider.test.tsx` (408 LOC) | Shipped. Covers unmount/remount preservation, fetch-failure tolerance, SSE delta accumulation, abort. |
31
+ | Dev diagnostics endpoint | `src/app/api/diagnostics/chat-streams/route.ts` | Shipped. Reads the ring buffer from `stream-telemetry.ts`. |
32
+ | 3 client reason codes documented | `src/lib/chat/stream-telemetry.ts:28-30` | Shipped. `client.stream.done`, `client.stream.user-abort`, `client.stream.reader-error`. |
33
+
34
+ The **only** missing code artifact is the 4th client reason code `client.stream.view-remount` described in spec §5, plus its emission site.
35
+
36
+ ## Error & Rescue Registry
37
+
38
+ | Failure mode | Detection | Recovery |
39
+ |---|---|---|
40
+ | `ChatShell` unmounts mid-stream but provider does not persist state (regression of the provider hoisting). | Browser smoke test shows the assistant message clears on nav-away. | Check that `<ChatSessionProvider>` is in `layout.tsx`, not inside `/chat` route. |
41
+ | Telemetry log prefix drifts from `[chat-stream]`. | Unit test assertion on `console.info` prefix fails. | Keep the literal `[chat-stream]` prefix — it's the grep contract used by the diagnostics endpoint / log scrapers. |
42
+ | View-remount code fires on **initial** mount (false positive). | Unit test fails: cleanup should only fire if `isStreaming` was true at cleanup time. | Read `isStreaming` via ref (not closure) inside cleanup so we capture the value at unmount, not at effect setup. |
43
+
44
+ ---
45
+
46
+ ## Task 1: Document the 4th client reason code
47
+
48
+ **Files:**
49
+ - Modify: `src/lib/chat/stream-telemetry.ts:26-31`
50
+
51
+ - [ ] **Step 1: Extend the docblock**
52
+
53
+ Edit the top docblock so the "Three client-side reason codes" section becomes "Four client-side reason codes" and adds the new bullet. The list today (lines 26-30):
54
+
55
+ ```typescript
56
+ * Three client-side reason codes (logged via console.info with a stable
57
+ * prefix so tests and grep can find them):
58
+ * - client.stream.done — reader.read() returned done: true
59
+ * - client.stream.user-abort — user clicked Stop / AbortController fired
60
+ * - client.stream.reader-error — reader.read() or decode threw
61
+ ```
62
+
63
+ Replace with:
64
+
65
+ ```typescript
66
+ * Four client-side reason codes (logged via console.info with a stable
67
+ * prefix so tests and grep can find them):
68
+ * - client.stream.done — reader.read() returned done: true
69
+ * - client.stream.user-abort — user clicked Stop / AbortController fired
70
+ * - client.stream.reader-error — reader.read() or decode threw
71
+ * - client.stream.view-remount — a chat-consuming component unmounted
72
+ * while a stream was in flight. The stream
73
+ * itself continues in the provider; this
74
+ * code exists so diagnostics can confirm
75
+ * the provider-hoisting fix is holding.
76
+ ```
77
+
78
+ - [ ] **Step 2: Verify no other grep hits need updating**
79
+
80
+ Run: `rg "Three client-side reason codes" src`
81
+ Expected: no matches (the string was only in this file).
82
+
83
+ - [ ] **Step 3: Commit**
84
+
85
+ ```bash
86
+ git add src/lib/chat/stream-telemetry.ts
87
+ git commit -m "docs(chat): document client.stream.view-remount reason code"
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Task 2: Write the failing test for the cleanup emitter
93
+
94
+ **Files:**
95
+ - Modify: `src/components/chat/__tests__/chat-session-provider.test.tsx` (add one new test block)
96
+
97
+ - [ ] **Step 1: Add the test**
98
+
99
+ Append to the existing test file, inside the `describe("ChatSessionProvider", ...)` block:
100
+
101
+ ```typescript
102
+ it("emits client.stream.view-remount when a consumer unmounts while streaming", async () => {
103
+ // Arrange: a consumer component that reads isStreaming from the provider
104
+ // and, on unmount, logs the view-remount telemetry if a stream was active.
105
+ const consoleInfoSpy = vi.spyOn(console, "info").mockImplementation(() => {});
106
+
107
+ function StreamingConsumer() {
108
+ const { isStreaming, sendMessage } = useChatSession();
109
+ const isStreamingRef = useRef(isStreaming);
110
+ useEffect(() => {
111
+ isStreamingRef.current = isStreaming;
112
+ }, [isStreaming]);
113
+ useEffect(() => {
114
+ return () => {
115
+ if (isStreamingRef.current) {
116
+ // eslint-disable-next-line no-console
117
+ console.info("[chat-stream] client.stream.view-remount", {
118
+ conversationId: null,
119
+ });
120
+ }
121
+ };
122
+ }, []);
123
+ return (
124
+ <button onClick={() => void sendMessage("hi")}>send</button>
125
+ );
126
+ }
127
+
128
+ // Use a never-resolving SSE body so isStreaming stays true until unmount.
129
+ const neverResolve = new Promise<Response>(() => {});
130
+ global.fetch = vi.fn((url: string) => {
131
+ if (url.startsWith("/api/chat/conversations") && !url.includes("messages")) {
132
+ return Promise.resolve(new Response(JSON.stringify({ id: "conv-vm" }), { status: 200 }));
133
+ }
134
+ if (url.includes("/stream")) return neverResolve;
135
+ return Promise.resolve(new Response("[]", { status: 200 }));
136
+ }) as typeof fetch;
137
+
138
+ const { unmount, getByText } = render(
139
+ <ChatSessionProvider>
140
+ <StreamingConsumer />
141
+ </ChatSessionProvider>
142
+ );
143
+
144
+ fireEvent.click(getByText("send"));
145
+ // Let sendMessage start the stream (isStreaming flips true)
146
+ await waitFor(() => {
147
+ // Consumer cleanup hasn't fired yet; we just need the streaming flag set.
148
+ });
149
+
150
+ unmount();
151
+
152
+ expect(consoleInfoSpy).toHaveBeenCalledWith(
153
+ "[chat-stream] client.stream.view-remount",
154
+ expect.objectContaining({ conversationId: expect.anything() })
155
+ );
156
+
157
+ consoleInfoSpy.mockRestore();
158
+ });
159
+ ```
160
+
161
+ Import additions at the top of the test file (only if missing):
162
+
163
+ ```typescript
164
+ import { useEffect, useRef } from "react";
165
+ import { fireEvent, render, waitFor } from "@testing-library/react";
166
+ ```
167
+
168
+ - [ ] **Step 2: Run the test to verify it passes (self-contained)**
169
+
170
+ Run: `npx vitest run src/components/chat/__tests__/chat-session-provider.test.tsx -t view-remount`
171
+
172
+ Expected: the test passes, because the `StreamingConsumer` component defined inside the test itself emits the log. This test is the **contract template** — Task 3 moves the emitter into `ChatShell` so real consumers honor the contract.
173
+
174
+ - [ ] **Step 3: Commit**
175
+
176
+ ```bash
177
+ git add src/components/chat/__tests__/chat-session-provider.test.tsx
178
+ git commit -m "test(chat): add view-remount telemetry contract test"
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Task 3: Emit `client.stream.view-remount` from `ChatShell`
184
+
185
+ **Files:**
186
+ - Modify: `src/components/chat/chat-shell.tsx` (add one useEffect + ref at the top of the component)
187
+
188
+ - [ ] **Step 1: Add the ref + cleanup effect**
189
+
190
+ Open `src/components/chat/chat-shell.tsx`. Directly after the `const session = useChatSession(); const { ... } = session;` destructure (around line 54), insert:
191
+
192
+ ```typescript
193
+ // Track streaming state in a ref so the unmount cleanup sees the latest
194
+ // value, not the value at effect-setup time. If ChatShell unmounts while
195
+ // a stream is in flight (user navigated away), log a telemetry breadcrumb.
196
+ // The stream itself continues inside ChatSessionProvider — this log only
197
+ // exists to confirm the provider-hoisting fix is holding. See
198
+ // `src/lib/chat/stream-telemetry.ts` for the full reason code list.
199
+ const isStreamingRef = useRef(isStreaming);
200
+ useEffect(() => {
201
+ isStreamingRef.current = isStreaming;
202
+ }, [isStreaming]);
203
+ useEffect(() => {
204
+ return () => {
205
+ if (isStreamingRef.current) {
206
+ console.info("[chat-stream] client.stream.view-remount", {
207
+ conversationId: activeId,
208
+ });
209
+ }
210
+ };
211
+ // Intentionally empty deps: we want this exactly-once cleanup on unmount.
212
+ // eslint-disable-next-line react-hooks/exhaustive-deps
213
+ }, []);
214
+ ```
215
+
216
+ Add `useRef` to the React import at the top of the file (line 3). Change:
217
+
218
+ ```typescript
219
+ import { useState, useCallback, useEffect, useMemo } from "react";
220
+ ```
221
+
222
+ to:
223
+
224
+ ```typescript
225
+ import { useState, useCallback, useEffect, useMemo, useRef } from "react";
226
+ ```
227
+
228
+ - [ ] **Step 2: Confirm TypeScript is clean**
229
+
230
+ Run: `npx tsc --noEmit`
231
+
232
+ Expected: no errors.
233
+
234
+ - [ ] **Step 3: Run the full provider test file to confirm no regressions**
235
+
236
+ Run: `npx vitest run src/components/chat/__tests__/chat-session-provider.test.tsx`
237
+
238
+ Expected: 5 tests pass (the original 4 plus the new view-remount contract test from Task 2).
239
+
240
+ - [ ] **Step 4: Commit**
241
+
242
+ ```bash
243
+ git add src/components/chat/chat-shell.tsx
244
+ git commit -m "feat(chat): emit client.stream.view-remount on ChatShell unmount"
245
+ ```
246
+
247
+ ---
248
+
249
+ ## Task 4: Manual browser smoke test
250
+
251
+ This verifies the fix against the original bug report, not just the logic. Spec AC requires it explicitly: *"Manual repro: start a 5-10s streaming response, click Dashboard, wait 10s, return to /chat. Assistant message is complete or still streaming live. Prior user turn and assistant content intact."*
252
+
253
+ **No code changes in this task — pure verification.**
254
+
255
+ - [ ] **Step 1: Start a clean dev server**
256
+
257
+ Per `MEMORY.md` "Clean Next.js restart procedure":
258
+
259
+ ```bash
260
+ pkill -f "next dev --turbopack$"; pkill -f "next-server"; sleep 2
261
+ npm run dev
262
+ ```
263
+
264
+ Wait until the console shows `Ready in …`.
265
+
266
+ - [ ] **Step 2: Open `http://localhost:3000/chat` in the browser**
267
+
268
+ Use Claude in Chrome (first choice, per MEMORY.md) or Chrome DevTools MCP. Retry once on failure before falling back to Playwright.
269
+
270
+ - [ ] **Step 3: Trigger a 5–10s streaming response on Claude runtime**
271
+
272
+ Select Claude model. Send a prompt that reliably takes 5–10s, e.g.:
273
+
274
+ ```
275
+ Explain in 3 short paragraphs how SSE backpressure works.
276
+ ```
277
+
278
+ - [ ] **Step 4: Mid-stream, navigate away and back**
279
+
280
+ While the assistant message is still streaming:
281
+ 1. Click "Dashboard" in the sidebar.
282
+ 2. Wait 10 seconds.
283
+ 3. Click "Chat" to return.
284
+
285
+ Expected:
286
+ - Assistant message either completed or still streaming live
287
+ - Prior user turn intact
288
+ - Prior assistant content intact (no blank)
289
+
290
+ - [ ] **Step 5: Repeat 5× rapidly**
291
+
292
+ Click sidebar items in quick succession (Dashboard → Projects → Workflows → Chat) while a stream is in flight. Do this five times.
293
+
294
+ Expected: zero turn loss, zero blank conversations.
295
+
296
+ - [ ] **Step 6: Repeat steps 3–5 on the GPT (Codex) runtime**
297
+
298
+ Switch model to a GPT option. Repeat the test sequence. Expected: same zero-loss behavior.
299
+
300
+ - [ ] **Step 7: Verify the diagnostics endpoint**
301
+
302
+ Open `http://localhost:3000/api/diagnostics/chat-streams` in a new tab.
303
+
304
+ Expected:
305
+ - `stream.abandoned` count is zero for the test window.
306
+ - `client.stream.view-remount` log lines appear in the dev-server console for each nav-away that happened during streaming.
307
+
308
+ - [ ] **Step 8: Record results in the feature spec**
309
+
310
+ Append a "Verification run — 2026-04-14" section to `features/chat-session-persistence-provider.md` with:
311
+ - Runtimes tested (Claude + GPT)
312
+ - Number of nav-away cycles
313
+ - Observed `stream.abandoned` count (expected 0)
314
+ - Observed `client.stream.view-remount` occurrences (expected >0 — proves the telemetry hook works)
315
+ - Any anomaly
316
+
317
+ Commit it:
318
+
319
+ ```bash
320
+ git add features/chat-session-persistence-provider.md
321
+ git commit -m "docs(features): record chat-session-persistence-provider smoke run"
322
+ ```
323
+
324
+ ---
325
+
326
+ ## Task 5: Close out spec status and changelog
327
+
328
+ **Files:**
329
+ - Modify: `features/chat-session-persistence-provider.md` (frontmatter `status:`)
330
+ - Modify: `features/changelog.md` (add entry)
331
+
332
+ - [ ] **Step 1: Flip spec status**
333
+
334
+ Change the frontmatter in `features/chat-session-persistence-provider.md`:
335
+
336
+ ```yaml
337
+ status: planned
338
+ ```
339
+
340
+ to:
341
+
342
+ ```yaml
343
+ status: completed
344
+ ```
345
+
346
+ - [ ] **Step 2: Add a changelog entry**
347
+
348
+ Append to `features/changelog.md` under the latest date section (create a new `## 2026-04-14` heading if needed):
349
+
350
+ ```markdown
351
+ - **chat-session-persistence-provider** — Closed out. Provider + layout + ChatShell refactor already shipped earlier; this pass adds the `client.stream.view-remount` telemetry reason code and emitter to satisfy AC §5, plus a browser smoke-test verification run. No server-side changes. Spec flipped to `completed`.
352
+ ```
353
+
354
+ - [ ] **Step 3: Final verification**
355
+
356
+ Run:
357
+
358
+ ```bash
359
+ npm test -- src/components/chat
360
+ npx tsc --noEmit
361
+ ```
362
+
363
+ Expected: all tests pass, zero TS errors.
364
+
365
+ - [ ] **Step 4: Commit**
366
+
367
+ ```bash
368
+ git add features/chat-session-persistence-provider.md features/changelog.md
369
+ git commit -m "docs(features): mark chat-session-persistence-provider complete"
370
+ ```
371
+
372
+ ---
373
+
374
+ ## Verification summary
375
+
376
+ After all 5 tasks:
377
+
378
+ | Acceptance criterion from spec | Verified by |
379
+ |---|---|
380
+ | `chat-session-provider.tsx` exists with action surface | Pre-existing; confirmed in Task 1 scope check |
381
+ | `layout.tsx` wraps `<main>` with `<ChatSessionProvider>` | Pre-existing; lines 101/114 |
382
+ | `ChatShell` holds zero chat-domain `useState` | Pre-existing; only view-local state remains |
383
+ | No `setMessages([])` catch-all | Pre-existing; only in comments |
384
+ | **Manual repro (view-switch, 5× rapid, both runtimes)** | **Task 4** |
385
+ | **`/api/diagnostics/chat-streams` shows zero `stream.abandoned`** | **Task 4 step 7** |
386
+ | Stop button aborts via AbortController | Pre-existing provider test |
387
+ | Unit tests in provider test file | Pre-existing 4 + new 1 = 5 |
388
+ | **`client.stream.view-remount` reason code added** | **Task 1 + Task 3** |
389
+ | `npm test` passes, `npx tsc --noEmit` clean | **Task 5 step 3** |
390
+
391
+ ## Self-review
392
+
393
+ **Spec coverage:** every AC bullet maps to a pre-existing artifact or a task above.
394
+
395
+ **Placeholder scan:** no TBDs, TODOs, "add appropriate error handling" phrases, or "similar to Task N" shortcuts. Each task contains complete code.
396
+
397
+ **Type consistency:** `isStreamingRef`, `useChatSession()`, and telemetry log prefix `[chat-stream]` are used identically across Tasks 2 and 3.
398
+
399
+ **Smoke-test budget:** this plan does **not** touch any module under `src/lib/agents/runtime/`, `src/lib/workflows/engine.ts`, or anything that statically imports `@/lib/chat/stagent-tools`. The project override's mandatory smoke task is not triggered. Task 4's smoke step is driven by the spec's own AC, not the runtime-registry gate.
package/next.config.mjs CHANGED
@@ -2,6 +2,7 @@
2
2
  const nextConfig = {
3
3
  serverExternalPackages: ["better-sqlite3", "pdf-parse", "pdfjs-dist"],
4
4
  devIndicators: false,
5
+ allowedDevOrigins: ["127.0.0.1"],
5
6
  };
6
7
 
7
8
  export default nextConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stagent",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "AI Business Operating System — run your business with AI agents. Local-first, multi-provider, governed.",
5
5
  "keywords": [
6
6
  "ai",
@@ -38,10 +38,10 @@
38
38
  ],
39
39
  "repository": {
40
40
  "type": "git",
41
- "url": "https://github.com/navam-io/stagent.git"
41
+ "url": "https://github.com/manavsehgal/stagent.git"
42
42
  },
43
43
  "bugs": {
44
- "url": "https://github.com/navam-io/stagent/issues"
44
+ "url": "https://github.com/manavsehgal/stagent/issues"
45
45
  },
46
46
  "homepage": "https://stagent.io",
47
47
  "scripts": {
@@ -0,0 +1,141 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { NextRequest } from "next/server";
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Mocked service module — controls return values for each test.
6
+ // ---------------------------------------------------------------------------
7
+ const mockActivateSkill = vi.fn();
8
+
9
+ vi.mock("@/lib/chat/skill-composition", () => ({
10
+ activateSkill: (...args: unknown[]) => mockActivateSkill(...args),
11
+ }));
12
+
13
+ // Import the route handler AFTER the mock is set up.
14
+ import { POST } from "../activate/route";
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Helpers
18
+ // ---------------------------------------------------------------------------
19
+ function makeRequest(body: unknown, conversationId = "conv-1"): [NextRequest, { params: Promise<{ id: string }> }] {
20
+ const req = new NextRequest("http://localhost/api/chat/conversations/conv-1/skills/activate", {
21
+ method: "POST",
22
+ headers: { "Content-Type": "application/json" },
23
+ body: JSON.stringify(body),
24
+ });
25
+ return [req, { params: Promise.resolve({ id: conversationId }) }];
26
+ }
27
+
28
+ beforeEach(() => {
29
+ vi.resetAllMocks();
30
+ });
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Tests
34
+ // ---------------------------------------------------------------------------
35
+ describe("POST /api/chat/conversations/[id]/skills/activate", () => {
36
+ it("returns 400 for invalid JSON", async () => {
37
+ const req = new NextRequest("http://localhost/...", {
38
+ method: "POST",
39
+ headers: { "Content-Type": "application/json" },
40
+ body: "not-json",
41
+ });
42
+ const res = await POST(req, { params: Promise.resolve({ id: "conv-1" }) });
43
+ expect(res.status).toBe(400);
44
+ const json = await res.json() as Record<string, unknown>;
45
+ expect(typeof json.error).toBe("string");
46
+ });
47
+
48
+ it("returns 400 when skillId is missing", async () => {
49
+ const [req, ctx] = makeRequest({ mode: "replace" });
50
+ const res = await POST(req, ctx);
51
+ expect(res.status).toBe(400);
52
+ });
53
+
54
+ it("returns 200 with activation payload on success (replace mode)", async () => {
55
+ mockActivateSkill.mockResolvedValueOnce({
56
+ kind: "ok",
57
+ activatedSkillId: "my-skill",
58
+ activeSkillIds: ["my-skill"],
59
+ skillName: "My Skill",
60
+ });
61
+ const [req, ctx] = makeRequest({ skillId: "my-skill" });
62
+ const res = await POST(req, ctx);
63
+ expect(res.status).toBe(200);
64
+ const json = await res.json() as Record<string, unknown>;
65
+ expect(json.activatedSkillId).toBe("my-skill");
66
+ expect(json.activeSkillIds).toEqual(["my-skill"]);
67
+ expect(json.skillName).toBe("My Skill");
68
+ expect(mockActivateSkill).toHaveBeenCalledWith({
69
+ conversationId: "conv-1",
70
+ skillId: "my-skill",
71
+ mode: "replace",
72
+ force: false,
73
+ });
74
+ });
75
+
76
+ it("returns 200 with requiresConfirmation when conflicts detected", async () => {
77
+ mockActivateSkill.mockResolvedValueOnce({
78
+ kind: "conflicts",
79
+ activeSkillIds: ["first"],
80
+ conflicts: [
81
+ { skillA: "first", skillB: "second", sharedTopic: "tests", excerptA: "Always …", excerptB: "Never …" },
82
+ ],
83
+ hint: "Re-call with force=true to add anyway",
84
+ });
85
+ const [req, ctx] = makeRequest({ skillId: "second", mode: "add" });
86
+ const res = await POST(req, ctx);
87
+ expect(res.status).toBe(200);
88
+ const json = await res.json() as Record<string, unknown>;
89
+ expect(json.requiresConfirmation).toBe(true);
90
+ expect(Array.isArray(json.conflicts)).toBe(true);
91
+ expect((json.conflicts as unknown[]).length).toBe(1);
92
+ });
93
+
94
+ it("returns 404 when conversation is not found", async () => {
95
+ mockActivateSkill.mockResolvedValueOnce({
96
+ kind: "error",
97
+ message: "Conversation not found: ghost",
98
+ });
99
+ const [req, ctx] = makeRequest({ skillId: "any-skill" }, "ghost");
100
+ const res = await POST(req, ctx);
101
+ expect(res.status).toBe(404);
102
+ const json = await res.json() as Record<string, unknown>;
103
+ expect((json.error as string)).toContain("Conversation not found");
104
+ });
105
+
106
+ it("returns 404 when skill is not found", async () => {
107
+ mockActivateSkill.mockResolvedValueOnce({
108
+ kind: "error",
109
+ message: "Skill not found: no-such-skill",
110
+ });
111
+ const [req, ctx] = makeRequest({ skillId: "no-such-skill" });
112
+ const res = await POST(req, ctx);
113
+ expect(res.status).toBe(404);
114
+ });
115
+
116
+ it("returns 400 for other logic errors (e.g. max skills reached)", async () => {
117
+ mockActivateSkill.mockResolvedValueOnce({
118
+ kind: "error",
119
+ message: "Max active skills (3) reached on 'claude-code' — deactivate one first",
120
+ });
121
+ const [req, ctx] = makeRequest({ skillId: "any", mode: "add", force: true });
122
+ const res = await POST(req, ctx);
123
+ expect(res.status).toBe(400);
124
+ const json = await res.json() as Record<string, unknown>;
125
+ expect((json.error as string)).toMatch(/max active skills/i);
126
+ });
127
+
128
+ it("passes force=true to the service", async () => {
129
+ mockActivateSkill.mockResolvedValueOnce({
130
+ kind: "ok",
131
+ activatedSkillId: "any",
132
+ activeSkillIds: ["first", "any"],
133
+ skillName: "Any",
134
+ });
135
+ const [req, ctx] = makeRequest({ skillId: "any", mode: "add", force: true });
136
+ await POST(req, ctx);
137
+ expect(mockActivateSkill).toHaveBeenCalledWith(
138
+ expect.objectContaining({ force: true, mode: "add" })
139
+ );
140
+ });
141
+ });
@@ -0,0 +1,74 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { activateSkill } from "@/lib/chat/skill-composition";
4
+
5
+ const ActivateBody = z.object({
6
+ skillId: z.string().min(1),
7
+ mode: z.enum(["replace", "add"]).optional().default("replace"),
8
+ force: z.boolean().optional().default(false),
9
+ });
10
+
11
+ /**
12
+ * POST /api/chat/conversations/[id]/skills/activate
13
+ *
14
+ * Thin HTTP wrapper over the activateSkill composition service so the chat UI
15
+ * can reach composition logic without going through MCP.
16
+ *
17
+ * Returns:
18
+ * 200 { activatedSkillId, activeSkillIds, skillName } — success
19
+ * 200 { requiresConfirmation: true, conflicts: [...] } — needs confirm
20
+ * 400 { error: string } — validation / logic error
21
+ * 404 { error: string } — conversation not found
22
+ *
23
+ * See `features/chat-composition-ui-v1.md`.
24
+ */
25
+ export async function POST(
26
+ req: NextRequest,
27
+ { params }: { params: Promise<{ id: string }> }
28
+ ) {
29
+ const { id: conversationId } = await params;
30
+
31
+ let body: unknown;
32
+ try {
33
+ body = await req.json();
34
+ } catch {
35
+ return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
36
+ }
37
+
38
+ const parsed = ActivateBody.safeParse(body);
39
+ if (!parsed.success) {
40
+ return NextResponse.json(
41
+ { error: parsed.error.issues.map((i) => i.message).join("; ") },
42
+ { status: 400 }
43
+ );
44
+ }
45
+
46
+ const { skillId, mode, force } = parsed.data;
47
+ const result = await activateSkill({ conversationId, skillId, mode, force });
48
+
49
+ if (result.kind === "error") {
50
+ const isNotFound =
51
+ result.message.startsWith("Conversation not found") ||
52
+ result.message.startsWith("Skill not found");
53
+ return NextResponse.json(
54
+ { error: result.message },
55
+ { status: isNotFound ? 404 : 400 }
56
+ );
57
+ }
58
+
59
+ if (result.kind === "conflicts") {
60
+ return NextResponse.json({
61
+ requiresConfirmation: true,
62
+ conflicts: result.conflicts,
63
+ hint: result.hint,
64
+ });
65
+ }
66
+
67
+ // kind === "ok"
68
+ return NextResponse.json({
69
+ activatedSkillId: result.activatedSkillId,
70
+ activeSkillIds: result.activeSkillIds,
71
+ skillName: result.skillName,
72
+ ...(result.note ? { note: result.note } : {}),
73
+ });
74
+ }
@@ -0,0 +1,33 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { deactivateSkill } from "@/lib/chat/skill-composition";
3
+
4
+ /**
5
+ * POST /api/chat/conversations/[id]/skills/deactivate
6
+ *
7
+ * Clears both activeSkillId and activeSkillIds on the conversation row.
8
+ * Idempotent — safe to call when no skill is active.
9
+ *
10
+ * Returns:
11
+ * 200 { previousSkillId: string | null } — success
12
+ * 404 { error: string } — conversation not found
13
+ *
14
+ * See `features/chat-composition-ui-v1.md`.
15
+ */
16
+ export async function POST(
17
+ _req: NextRequest,
18
+ { params }: { params: Promise<{ id: string }> }
19
+ ) {
20
+ const { id: conversationId } = await params;
21
+
22
+ const result = await deactivateSkill({ conversationId });
23
+
24
+ if (result.kind === "error") {
25
+ const isNotFound = result.message.startsWith("Conversation not found");
26
+ return NextResponse.json(
27
+ { error: result.message },
28
+ { status: isNotFound ? 404 : 400 }
29
+ );
30
+ }
31
+
32
+ return NextResponse.json({ previousSkillId: result.previousSkillId });
33
+ }