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.
- package/README.md +44 -31
- package/dist/cli.js +24 -0
- package/docs/.coverage-gaps.json +154 -24
- package/docs/.last-generated +1 -1
- package/docs/features/agent-intelligence.md +12 -2
- package/docs/features/chat.md +40 -5
- package/docs/features/cost-usage.md +1 -1
- package/docs/features/documents.md +5 -2
- package/docs/features/inbox-notifications.md +10 -2
- package/docs/features/keyboard-navigation.md +12 -3
- package/docs/features/provider-runtimes.md +16 -2
- package/docs/features/settings.md +2 -2
- package/docs/features/shared-components.md +7 -3
- package/docs/features/tables.md +3 -1
- package/docs/features/tool-permissions.md +6 -2
- package/docs/features/workflows.md +6 -2
- package/docs/getting-started.md +1 -1
- package/docs/index.md +1 -1
- package/docs/journeys/developer.md +25 -2
- package/docs/journeys/personal-use.md +12 -5
- package/docs/journeys/power-user.md +45 -14
- package/docs/journeys/work-use.md +17 -8
- package/docs/manifest.json +15 -15
- package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +2 -2
- package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
- package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
- package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
- package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
- package/next.config.mjs +1 -0
- package/package.json +3 -3
- package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
- package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
- package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
- package/src/app/api/chat/export/route.ts +52 -0
- package/src/app/api/chat/files/search/route.ts +50 -0
- package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
- package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
- package/src/app/api/environment/skills/route.ts +13 -0
- package/src/app/api/schedules/[id]/execute/route.ts +2 -2
- package/src/app/api/settings/chat/pins/route.ts +94 -0
- package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
- package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
- package/src/app/api/settings/environment/route.ts +26 -0
- package/src/app/api/tasks/[id]/execute/route.ts +52 -12
- package/src/app/api/tasks/[id]/respond/route.ts +31 -15
- package/src/app/api/tasks/[id]/resume/route.ts +24 -3
- package/src/app/documents/page.tsx +4 -1
- package/src/app/settings/page.tsx +2 -0
- package/src/components/book/content-blocks.tsx +1 -1
- package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
- package/src/components/chat/__tests__/chat-session-provider.test.tsx +166 -1
- package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
- package/src/components/chat/capability-banner.tsx +68 -0
- package/src/components/chat/chat-command-popover.tsx +668 -47
- package/src/components/chat/chat-input.tsx +103 -8
- package/src/components/chat/chat-message.tsx +12 -3
- package/src/components/chat/chat-session-provider.tsx +73 -3
- package/src/components/chat/chat-shell.tsx +62 -3
- package/src/components/chat/command-tab-bar.tsx +68 -0
- package/src/components/chat/conversation-template-picker.tsx +421 -0
- package/src/components/chat/help-dialog.tsx +39 -0
- package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
- package/src/components/chat/skill-row.tsx +147 -0
- package/src/components/documents/document-browser.tsx +37 -19
- package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
- package/src/components/notifications/permission-response-actions.tsx +155 -1
- package/src/components/playbook/playbook-detail-view.tsx +1 -1
- package/src/components/settings/environment-section.tsx +102 -0
- package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
- package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
- package/src/components/shared/command-palette.tsx +262 -2
- package/src/components/shared/filter-hint.tsx +70 -0
- package/src/components/shared/filter-input.tsx +59 -0
- package/src/components/shared/saved-searches-manager.tsx +199 -0
- package/src/components/tasks/task-bento-grid.tsx +12 -2
- package/src/components/tasks/task-card.tsx +3 -0
- package/src/components/tasks/task-chip-bar.tsx +30 -1
- package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
- package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
- package/src/hooks/use-active-skills.ts +110 -0
- package/src/hooks/use-chat-autocomplete.ts +120 -7
- package/src/hooks/use-enriched-skills.ts +19 -0
- package/src/hooks/use-pinned-entries.ts +104 -0
- package/src/hooks/use-recent-user-messages.ts +19 -0
- package/src/hooks/use-saved-searches.ts +142 -0
- package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
- package/src/lib/agents/__tests__/claude-agent.test.ts +17 -4
- package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
- package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
- package/src/lib/agents/claude-agent.ts +105 -46
- package/src/lib/agents/handoff/bus.ts +2 -2
- package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
- package/src/lib/agents/profiles/__tests__/registry.test.ts +47 -0
- package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +30 -3
- package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +6 -2
- package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
- package/src/lib/agents/profiles/registry.ts +97 -22
- package/src/lib/agents/profiles/types.ts +7 -1
- package/src/lib/agents/router.ts +3 -6
- package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
- package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
- package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
- package/src/lib/agents/runtime/catalog.ts +121 -0
- package/src/lib/agents/runtime/claude-sdk.ts +32 -0
- package/src/lib/agents/runtime/execution-target.ts +456 -0
- package/src/lib/agents/runtime/index.ts +4 -0
- package/src/lib/agents/runtime/launch-failure.ts +101 -0
- package/src/lib/agents/runtime/openai-codex.ts +35 -0
- package/src/lib/agents/runtime/openai-direct.ts +8 -0
- package/src/lib/agents/task-dispatch.ts +220 -0
- package/src/lib/agents/tool-permissions.ts +16 -1
- package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
- package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
- package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
- package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
- package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
- package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
- package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
- package/src/lib/chat/__tests__/types.test.ts +28 -0
- package/src/lib/chat/active-skills.ts +31 -0
- package/src/lib/chat/clean-filter-input.ts +30 -0
- package/src/lib/chat/codex-engine.ts +30 -7
- package/src/lib/chat/command-tabs.ts +61 -0
- package/src/lib/chat/context-builder.ts +141 -1
- package/src/lib/chat/dismissals.ts +73 -0
- package/src/lib/chat/engine.ts +109 -15
- package/src/lib/chat/files/__tests__/search.test.ts +135 -0
- package/src/lib/chat/files/expand-mention.ts +76 -0
- package/src/lib/chat/files/search.ts +99 -0
- package/src/lib/chat/skill-composition.ts +210 -0
- package/src/lib/chat/skill-conflict.ts +105 -0
- package/src/lib/chat/stagent-tools.ts +6 -19
- package/src/lib/chat/stream-telemetry.ts +9 -4
- package/src/lib/chat/system-prompt.ts +22 -0
- package/src/lib/chat/tool-catalog.ts +33 -3
- package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
- package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
- package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
- package/src/lib/chat/tools/__tests__/task-tools.test.ts +47 -0
- package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +134 -0
- package/src/lib/chat/tools/blueprint-tools.ts +190 -0
- package/src/lib/chat/tools/helpers.ts +2 -0
- package/src/lib/chat/tools/profile-tools.ts +120 -23
- package/src/lib/chat/tools/skill-tools.ts +183 -0
- package/src/lib/chat/tools/task-tools.ts +6 -2
- package/src/lib/chat/tools/workflow-tools.ts +61 -20
- package/src/lib/chat/types.ts +15 -0
- package/src/lib/constants/settings.ts +2 -0
- package/src/lib/data/clear.ts +2 -6
- package/src/lib/db/bootstrap.ts +17 -0
- package/src/lib/db/schema.ts +26 -0
- package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
- package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
- package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
- package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
- package/src/lib/environment/data.ts +9 -0
- package/src/lib/environment/list-skills.ts +176 -0
- package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
- package/src/lib/environment/parsers/skill.ts +26 -5
- package/src/lib/environment/profile-generator.ts +56 -2
- package/src/lib/environment/skill-enrichment.ts +106 -0
- package/src/lib/environment/skill-recommendations.ts +66 -0
- package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
- package/src/lib/filters/__tests__/parse.test.ts +135 -0
- package/src/lib/filters/parse.ts +86 -0
- package/src/lib/instance/__tests__/detect.test.ts +1 -1
- package/src/lib/instance/__tests__/upgrade-poller.test.ts +50 -0
- package/src/lib/instance/fingerprint.ts +8 -10
- package/src/lib/instance/upgrade-poller.ts +53 -1
- package/src/lib/schedules/scheduler.ts +4 -4
- package/src/lib/utils/stagent-paths.ts +4 -0
- package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
- package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
- package/src/lib/workflows/blueprints/types.ts +6 -0
- package/src/lib/workflows/engine.ts +5 -3
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stagent",
|
|
3
|
-
"version": "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/
|
|
41
|
+
"url": "https://github.com/manavsehgal/stagent.git"
|
|
42
42
|
},
|
|
43
43
|
"bugs": {
|
|
44
|
-
"url": "https://github.com/
|
|
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
|
+
}
|