diff --git a/.mcp.json b/.mcp.json
index a36a88a..22f1f26 100644
--- a/.mcp.json
+++ b/.mcp.json
@@ -2,7 +2,7 @@
"mcpServers": {
"story-kit": {
"type": "http",
- "url": "http://localhost:3001/mcp"
+ "url": "http://localhost:3010/mcp"
}
}
}
diff --git a/frontend/src/components/Chat.test.tsx b/frontend/src/components/Chat.test.tsx
index 29bbf5f..39208c0 100644
--- a/frontend/src/components/Chat.test.tsx
+++ b/frontend/src/components/Chat.test.tsx
@@ -1275,3 +1275,114 @@ describe("Remove bubble styling from streaming messages (Story 163)", () => {
expect(styleAttr).not.toContain("background: transparent");
});
});
+
+describe("Bug 264: Claude Code session ID persisted across browser refresh", () => {
+ const PROJECT_PATH = "/tmp/project";
+ const SESSION_KEY = `storykit-claude-session-id:${PROJECT_PATH}`;
+ const STORAGE_KEY = `storykit-chat-history:${PROJECT_PATH}`;
+
+ beforeEach(() => {
+ capturedWsHandlers = null;
+ lastSendChatArgs = null;
+ localStorage.clear();
+ setupMocks();
+ });
+
+ afterEach(() => {
+ localStorage.clear();
+ });
+
+ it("AC1: session_id is persisted to localStorage when onSessionId fires", async () => {
+ render();
+
+ await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
+
+ act(() => {
+ capturedWsHandlers?.onSessionId("test-session-abc");
+ });
+
+ await waitFor(() => {
+ expect(localStorage.getItem(SESSION_KEY)).toBe("test-session-abc");
+ });
+ });
+
+ it("AC2: after remount, next sendChat includes session_id from localStorage", async () => {
+ // Step 1: Render, receive a session ID, then unmount (simulate refresh)
+ localStorage.setItem(SESSION_KEY, "persisted-session-xyz");
+ localStorage.setItem(
+ STORAGE_KEY,
+ JSON.stringify([
+ { role: "user", content: "Prior message" },
+ { role: "assistant", content: "Prior reply" },
+ ]),
+ );
+
+ const { unmount } = render(
+ ,
+ );
+ await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
+ unmount();
+
+ // Step 2: Remount (simulates page reload)
+ capturedWsHandlers = null;
+ lastSendChatArgs = null;
+ render();
+ await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
+
+ // Prior messages should be visible
+ expect(await screen.findByText("Prior message")).toBeInTheDocument();
+
+ // Step 3: Send a new message — config should include session_id
+ const input = screen.getByPlaceholderText("Send a message...");
+ await act(async () => {
+ fireEvent.change(input, { target: { value: "Continue" } });
+ });
+ await act(async () => {
+ fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
+ });
+
+ expect(lastSendChatArgs).not.toBeNull();
+ expect(
+ (lastSendChatArgs?.config as Record).session_id,
+ ).toBe("persisted-session-xyz");
+ });
+
+ it("AC3: clearing the session also clears the persisted session_id", async () => {
+ localStorage.setItem(SESSION_KEY, "session-to-clear");
+
+ const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true);
+
+ render();
+
+ await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
+
+ const newSessionBtn = screen.getByText(/New Session/);
+ await act(async () => {
+ fireEvent.click(newSessionBtn);
+ });
+
+ expect(localStorage.getItem(SESSION_KEY)).toBeNull();
+
+ confirmSpy.mockRestore();
+ });
+
+ it("AC1: storage key is scoped to project path", async () => {
+ const otherPath = "/other/project";
+ const otherKey = `storykit-claude-session-id:${otherPath}`;
+ localStorage.setItem(otherKey, "other-session");
+
+ render();
+ await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
+
+ act(() => {
+ capturedWsHandlers?.onSessionId("my-session");
+ });
+
+ await waitFor(() => {
+ expect(localStorage.getItem(SESSION_KEY)).toBe("my-session");
+ });
+
+ // Other project's session should be untouched
+ expect(localStorage.getItem(otherKey)).toBe("other-session");
+ });
+});
diff --git a/frontend/src/components/Chat.tsx b/frontend/src/components/Chat.tsx
index 8b9e605..b656594 100644
--- a/frontend/src/components/Chat.tsx
+++ b/frontend/src/components/Chat.tsx
@@ -171,7 +171,16 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
merge: [],
done: [],
});
- const [claudeSessionId, setClaudeSessionId] = useState(null);
+ const [claudeSessionId, setClaudeSessionId] = useState(() => {
+ try {
+ return (
+ localStorage.getItem(`storykit-claude-session-id:${projectPath}`) ??
+ null
+ );
+ } catch {
+ return null;
+ }
+ });
const [activityStatus, setActivityStatus] = useState(null);
const [permissionQueue, setPermissionQueue] = useState<
{
@@ -247,6 +256,21 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
};
}, [messages, streamingContent, model]);
+ useEffect(() => {
+ try {
+ if (claudeSessionId !== null) {
+ localStorage.setItem(
+ `storykit-claude-session-id:${projectPath}`,
+ claudeSessionId,
+ );
+ } else {
+ localStorage.removeItem(`storykit-claude-session-id:${projectPath}`);
+ }
+ } catch {
+ // Ignore — quota or security errors.
+ }
+ }, [claudeSessionId, projectPath]);
+
useEffect(() => {
api
.getOllamaModels()
@@ -664,6 +688,11 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
setLoading(false);
setActivityStatus(null);
setClaudeSessionId(null);
+ try {
+ localStorage.removeItem(`storykit-claude-session-id:${projectPath}`);
+ } catch {
+ // Ignore — quota or security errors.
+ }
}
};