Skip to content

Fix frontend stream-state, save/continue, chart redraw, and tooltip bugs - #181

Merged
anth-volk merged 3 commits into
mainfrom
fix/frontend-stream-bugs
Jul 6, 2026
Merged

Fix frontend stream-state, save/continue, chart redraw, and tooltip bugs#181
anth-volk merged 3 commits into
mainfrom
fix/frontend-stream-bugs

Conversation

@anth-volk

Copy link
Copy Markdown
Contributor

Continues #164, which was auto-closed by renaming its head branch (GitHub closes a PR when its head branch is renamed; the shorter name keeps the Modal preview hostname under the 63-char DNS label limit — see #180 discussion). Same commits.

Fixes #155
Fixes #180

Single PR fixing the batch of verified frontend bugs in frontend/. Numbering matches the issue.

Changes

High

  1. Stream-state corruption when switching/clearing/deleting chats mid-stream (ChatPage.tsx): added a streamGeneration ref. sendMessage and continueMessage capture the generation at start and every state write (updateMessage/flushTarget, the drain interval, 402/429 notices, done handling incl. sessionId.current, saveConversation calls, abort/error handling, and the finally isStreaming/isWaiting resets) bails when stale. startNewChat, loadConversation, and deleteConversation (of the active conversation) call a shared invalidateStream() that bumps the generation, aborts the in-flight request, and resets streaming UI state. saveConversation also gen-guards its late setActiveConversationId so a save completing after a switch can't steal the active highlight. finally blocks only clear abortRef when it still points at their own controller, so a newer stream's Stop button keeps working.

Medium

  1. Proxy header drops (route.ts): forwards X-User-Id (with Content-Type) to the backend, and propagates Retry-After on error responses so the frontend's 429 wait message is accurate through the proxy. Superseded by Frontend: forward X-User-Id through proxy, pass through backend errors, fix Continue #146, which also forwards Authorization and passes the backend's real error body through; dropped from this PR during rebase.
  2. Floating pipeTo promise (route.ts): .catch(() => {}) on the SSE pipe so client aborts (Stop button) no longer produce unhandled rejections. Superseded by Frontend: forward X-User-Id through proxy, pass through backend errors, fix Continue #146; dropped from this PR during rebase.
  3. continueMessage stuck-incomplete on early returns (ChatPage.tsx): snapshots isComplete/stop_reason/stopped before the optimistic clear and restores them on the 402 and 429 branches and on fetch failure, so Copy/cost/Continue survive a failed continuation attempt.
  4. sendMessage save omitted turn metadata (ChatPage.tsx): cost_gbp/stop_reason are hoisted out of the done branch and persisted in both the done save and the later suggestions save, matching what continueMessage already saved.
  5. Double title generation per turn (ChatPage.tsx): a conversationTitleRef (known title) plus a memoized in-flight generation promise mean chat/title is POSTed at most once per conversation; the suggestions-arrival save reuses the same title. Loaded conversations seed the ref from their existing title, so continuing an old conversation never regenerates it.
  6. Auto-scroll no-op (ChatPage.tsx): the old scrollRef container was not scrollable (the document scrolls), so scrollTo did nothing. Replaced with a bottom-sentinel scrollIntoView, gated on already being within 200px of the document bottom so it never fights a user who scrolled up mid-stream; the on-done smooth-scroll trigger is preserved with the same gate.
  7. Chart redraw churn (LineChart.tsx, BarChart.tsx, ScatterChart.tsx): scales, domains, margins, categories, and stacked data are now useMemoized, so the svg.selectAll("*").remove() draw effects only re-run when the spec/dimensions actually change instead of on every tooltip-mousemove-driven render.

Low

  1. Retry-After NaN (ChatPage.tsx): shared parseRetryAfterSeconds helper guards with Number.isFinite (HTTP-date headers fall back to 60s); used by both send and continue paths.

  2. Stop before first token (ChatPage.tsx): the abort path now removes the empty assistant bubble (no text, no tools) instead of marking it stopped with a dead Continue button.

  3. Deleting the active conversation (ChatPage.tsx): also aborts any stream, clears messages, sessionId.current, collapsed-working state, and the title refs, and drops the cache entry, so the next send starts fresh instead of resurrecting the deleted conversation.

  4. Shared page [CHART_LOADING] (s/[token]/page.tsx): the segment regex now matches [CHART_LOADING] like ChatPage's, so the literal marker no longer renders. Since a saved share can never finish generating the chart, it renders a static "Chart unavailable" placeholder box rather than ChatPage's live spinner.

  5. Tooltip dead space below the composer (ChatPage.tsx, fixes Invisible tooltip pseudo-elements add dead scroll space below the composer #180): the data-tip tooltips were hidden with opacity: 0, so their boxes were always laid out and the ones hanging below the composer’s bottom button row stretched the document’s scroll height once the feed exceeded 100dvh. Tooltips now generate no box until :hover (content: nonecontent: attr(...), fade via keyframe animation), and the attach/Charts tooltips open to the button’s left/right instead of below.

Self-review

Reviewed the full git diff main once after implementing; findings:

  • Medium (fixed): loadConversation had no staleness check after its own await, so two rapid sidebar clicks on uncached conversations could apply the older response after the newer one (and the error path could inject an error bubble into the wrong view). Fixed by capturing the generation after invalidateStream() and bailing (success and error paths) if it moved.
  • Low (recorded, not addressed — pre-existing or intentional):
    • 402 responses routed through the proxy display the wrapped "Backend error: 402" text rather than the backend's friendly copy (pre-existing: the proxy re-wraps error bodies). Fixed on main by Frontend: forward X-User-Id through proxy, pass through backend errors, fix Continue #146, which passes the backend's error body through unwrapped.
    • continueMessage does not re-save after a late suggestions event, unlike sendMessage (pre-existing asymmetry; out of scope per the no-refactor constraint).
    • The continuation-failure error text is appended to content, which is invisible when the message has events (renderer prefers events; pre-existing). The restored flags make the failure recoverable regardless.
    • Behavior change: loading a conversation now scrolls to its latest message (previously nothing scrolled because auto-scroll was a no-op). This matches typical chat UX.
    • Edge: a user who focuses the textarea, scrolls far up, and presses Enter won't be auto-scrolled to their new message (near-bottom gate). Accepted per the "don't be aggressive" constraint.
    • Streaming auto-follow uses instant scrollIntoView rather than smooth to avoid smooth-scroll churn at the 20ms drain cadence; the on-done scroll stays smooth.

Verification

There is no frontend unit-test suite; verification was npm run build (passes: compile + lint + type-check) plus end-to-end re-reads of each changed flow (send, continue, stop, switch/new/delete mid-stream, save/title, proxy error paths, chart render/tooltip cycles).

🤖 Generated with Claude Code

anth-volk and others added 3 commits July 6, 2026 15:54
Fixes #155.

- Add a stream-generation guard (streamGeneration ref) captured by
  sendMessage/continueMessage; every state write bails when stale, and
  startNewChat/loadConversation/deleteConversation(active) abort the
  in-flight request, so switching or clearing chats mid-stream no longer
  bleeds streamed text, session ids, or saves into the new conversation.
- Guard loadConversation against out-of-order completion when the user
  switches again while an uncached conversation fetch is in flight.
- Proxy: forward X-User-Id to the backend and propagate Retry-After on
  error responses; handle the floating pipeTo promise so aborted SSE
  streams no longer emit unhandled rejections.
- continueMessage: snapshot and restore isComplete/stop_reason/stopped
  on 402/429/fetch-failure so a failed continuation no longer leaves the
  message stuck incomplete without its Continue affordance.
- sendMessage: persist cost_gbp/stop_reason in both the done and
  suggestions saves so truncated turns keep Continue and cost on reload.
- Generate the conversation title once per conversation (title ref plus
  memoized in-flight generation shared by the done/suggestions saves).
- Replace the no-op scrollRef auto-scroll with a bottom-sentinel
  scrollIntoView gated on being near the bottom of the document.
- Memoize chart scales/domains/margins/stacks so the D3 draw effects
  stop tearing down and redrawing on every tooltip mousemove render.
- Parse Retry-After defensively (HTTP-date -> fallback 60s).
- Remove the empty assistant bubble when a stream is stopped before the
  first token instead of leaving a dead Continue button.
- Deleting the active conversation now clears messages/session state so
  the next send cannot resurrect it.
- Shared page: treat [CHART_LOADING] as a placeholder instead of
  rendering the literal marker.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The data-tip tooltips were hidden with opacity:0, so their boxes were
always laid out; the ones hanging below the composer's bottom button
row extended the document's scrollable height once the feed grew past
100dvh, leaving dead space below the page content.

- Generate no tooltip box until :hover (content:none -> content:attr),
  with the 60ms fade as a keyframe animation since transitions cannot
  animate a freshly created box.
- Move the attach tooltip to the button's left (new data-tip-left) and
  the Charts toggle tooltip to the right, so nothing hangs below the
  bottom row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
policyengine-uk-chat Ready Ready Preview, Comment Jul 6, 2026 4:02pm

Request Review

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Beta preview has been cleaned up because this PR was closed.

@anth-volk
anth-volk marked this pull request as ready for review July 6, 2026 16:36
@anth-volk
anth-volk merged commit 85aebd5 into main Jul 6, 2026
6 of 7 checks passed
@anth-volk
anth-volk deleted the fix/frontend-stream-bugs branch July 6, 2026 16:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant