fix(ai-chat): keep the live thread on new-chat adoption; log stream errors

A brand-new chat's first turn streamed and finished successfully, but the
whole assistant response vanished from the UI. On finish the window adopts
the server-created chat id, which changed the <ChatThread> key and remounted
it — discarding the live useChat store (the full answer) and re-seeding from
not-yet-persisted history, so only the user message remained.

- chat-thread: pin the useChat store id to a per-mount value so adopting the
  chatId prop no longer recreates the store and wipes the live turn.
- ai-chat-window: derive the thread mount key via setState-during-render and
  move the live-thread marker in lockstep with the adopted id, so in-place
  adoption keeps the same mounted thread while real chat switches still
  remount and re-seed; gate the history loader to a freshly opened chat.
- cancel a pending adoption on New chat / explicit chat selection.
- log the raw stream error to the browser console for debugging.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude_code
2026-06-21 22:14:32 +03:00
parent 69f385ccb7
commit eb1e233d46
2 changed files with 59 additions and 7 deletions

View File

@@ -129,7 +129,13 @@ export default function ChatThread({
// The id only needs to be stable per mount — the parent remounts this via
// `key` on chat switch, which re-seeds cleanly.
const stableIdRef = useRef<string>(chatId ?? `new-${generateId()}`);
const chatStoreId = chatId ?? stableIdRef.current;
// Stable for the LIFETIME of this mount. When a brand-new chat adopts its
// server id, the parent now updates the `chatId` prop WITHOUT remounting this
// thread, so the store id must NOT follow `chatId`: recreating the useChat
// store would wipe the live (just-finished) turn. The server still resolves
// the real chat from `chatId` in the request body (see chatIdRef /
// prepareSendMessagesRequest), so this purely-client store key can stay fixed.
const chatStoreId = stableIdRef.current;
const transport = useMemo(
() =>
@@ -170,7 +176,12 @@ export default function ChatThread({
// saves the error message). Run the same post-turn path on error so the
// failed chat appears in history immediately instead of after a manual
// refresh. The error itself is still surfaced via `error` below.
onError: () => onTurnFinished(),
onError: (streamError) => {
// Surface the raw failure in the browser console (devtools) for debugging;
// the UI separately shows a friendly classified banner (see errorView).
console.error("AI chat stream error:", streamError);
onTurnFinished();
},
});
const isStreaming = status === "submitted" || status === "streaming";