Skip to content

fix(chat): cross-tab isolation, stream/history consistency, and reasoning replay - #82

Merged
Luhaozhu merged 1 commit into
mainfrom
fix/chat-stability-and-reasoning-replay
Aug 12, 2026
Merged

fix(chat): cross-tab isolation, stream/history consistency, and reasoning replay#82
Luhaozhu merged 1 commit into
mainfrom
fix/chat-stability-and-reasoning-replay

Conversation

@Luhaozhu

Copy link
Copy Markdown
Contributor

Summary

This PR lands a batch of stability and correctness fixes for the chat experience, discovered during full end-to-end functional testing, plus first-class support for models that deliver reasoning through the structured reasoning_content channel.

Multi-tab session integrity

  • The chat tree in localStorage is now written with per-chat granularity merge (newest updatedAt wins) instead of whole-tree overwrite, and tabs sync via the storage event. Two tabs chatting in parallel no longer clobber each other's conversations.
  • The current-chat pointer is tab-private (sessionStorage first, localStorage as fallback for fresh tabs), so refreshing one tab can no longer hijack another tab's restore target.
  • Following a background run's SSE stream is guarded by Web Locks keyed on the run id — only one tab replays a given run, eliminating duplicated / half-rendered bubbles and cross-conversation bleed.
  • Restoring a session keeps the previous chat id even when the backend does not know it yet (e.g. the first message is still streaming), so duplicating a tab no longer mints a phantom conversation.

Stream vs. history consistency

  • Each persisted tool call now records a content_offset (the length of the accumulated answer when the call was emitted). History rebuilds interleave text and tool cards in the original streaming order — a refreshed page now shows exactly what the live stream showed.
  • Visible answer text preceding a <think> open tag is no longer dropped when rebuilding history segments.

Structured reasoning channel

  • Thinking deltas are persisted as interleaved <think>…</think> blocks (same storage format as inline-thinking models), so the thought process replays after a refresh.
  • When the first model round finishes without any </think> in its text, the backend emits a structured-reasoning protocol marker; the frontend then reclassifies provisionally-buffered text back into the answer body. This fixes answers being trapped inside the thinking block when a reasoning-capable model produces no reasoning for a trivial prompt.

Other fixes

  • Edit-and-resend during streaming stops the in-flight run and resends, instead of silently ignoring the click; opening/cancelling the editor no longer force-scrolls to the bottom.
  • Wheel-up during streaming immediately detaches follow-to-bottom (no more fighting the auto-scroll).
  • Per-answer duration is persisted (extra_data.duration_ms) and survives refresh; display unit unified.
  • Share-page printing expands the inner scroll container so the full conversation prints (previously only page 1).
  • Renaming with empty input keeps the original title; manual renames set title_manually_set and are never overwritten by auto-summary; renames made mid-stream sync to the backend once the session exists.
  • Conversation search and favorite-card previews operate on the visible answer only — thinking content (including raw </think> fragments) is excluded from matches and snippets.
  • Attachments uploaded while a stream is running survive stream completion.
  • Message loading retries with backoff after transient failures instead of sticking on the skeleton.
  • Knowledge-base document search is now a server-side whole-library search (title/filename ilike) with pagination over the filtered set.

Testing

  • Automated browser (Playwright) end-to-end runs against a local deployment: dual-tab concurrent chats, duplicated-tab refresh, tool-calling conversations compared live vs. refreshed, print-to-PDF page count, injected 503s for the retry path, reasoning-mode prompts with and without thinking output.
  • Backend unit suites covering chat repository search, artifacts, and KB pass; frontend type-check and stream-segment regression script pass.

… replay

Fixes a batch of issues found in end-to-end functional testing:

Multi-tab session integrity
- Persist the chat tree with per-chat granularity merge instead of
  whole-tree overwrite; sync across tabs via the storage event
- Make the current-chat pointer tab-private (sessionStorage first),
  so tabs no longer steal each other's restore target
- Guard SSE run-following with Web Locks so only one tab follows a
  given background run (eliminates duplicated / cross-bleeding bubbles)
- Restoring a session no longer mints a fresh chat id when the previous
  one is not yet known to the backend (duplicated tabs stay on the
  same conversation)

Stream vs. history consistency
- Record content_offset on each persisted tool call and rebuild history
  by interleaving text and tool cards in the original streaming order
- Visible text preceding a <think> open tag is no longer dropped when
  rebuilding history segments

Structured reasoning (reasoning_content channel)
- Persist thinking deltas as interleaved <think> blocks so the thought
  process replays after a refresh
- When the first model round produces no </think>, emit a structured-
  reasoning marker and reclassify provisionally-buffered body text back
  into the answer (fixes answers being trapped inside the thinking
  block for models with an empty reasoning channel)

Other fixes
- Edit-and-resend now stops the in-flight stream instead of silently
  ignoring the click; editing no longer force-scrolls to the bottom
- Wheel-up during streaming immediately detaches follow-to-bottom
- Persist per-answer duration (extra_data.duration_ms) so it survives
  refresh; unify the display unit
- Share-page printing expands the inner scroll container so all pages
  print (previously only the first page)
- Renaming with empty input keeps the original title; manual renames
  are flagged (title_manually_set) and never overwritten by the
  auto-summary; renames made mid-stream sync after the stream ends
- Favorite-card previews and conversation search operate on the visible
  answer text only (thinking content excluded)
- Attachments uploaded while a stream is running are no longer cleared
  when it finishes
- Message loading retries with backoff instead of sticking on the
  skeleton after a transient failure
- Knowledge-base document search now searches the whole library
  server-side (title/filename) and paginates the filtered result
@Luhaozhu
Luhaozhu merged commit 9e9acbf into main Aug 12, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant