Skip to content

refactor(ai): remove legacy GlobalChatContext message state + fix stream reload - #1308

Merged
2witstudios merged 4 commits into
masterfrom
pu/chat-re-rendering
May 12, 2026
Merged

refactor(ai): remove legacy GlobalChatContext message state + fix stream reload#1308
2witstudios merged 4 commits into
masterfrom
pu/chat-re-rendering

Conversation

@2witstudios

@2witstudios 2witstudios commented May 12, 2026

Copy link
Copy Markdown
Owner

Summary

Root cause: GlobalChatContext.onStreamComplete unconditionally called refreshConversation()loadConversation()setIsInitialized(false) + server re-fetch + setInitialMessages, producing a visible loading flash after every AI response.

Fix: remove duplicate message state + signal-based refresh

  • Removed the legacy messages state, setMessages, and refreshConversation from GlobalChatContext — surfaces (GlobalAssistantView, SidebarChatTab) own their message state via useChat directly
  • Added refreshSignal: number counter to GlobalChatConversationContext; all remote events (reconnect, undo, cross-tab edits, stream complete for remote/bootstrapped streams) increment it
  • Surfaces watch refreshSignal and self-fetch when it changes — no context intermediary, no loading flash
  • Removed legacy useGlobalChat() combined hook; consumers now use the tiered useGlobalChatConfig, useGlobalChatStream, useGlobalChatConversation hooks
  • Fixed broken undo path: undo previously called refreshConversation() which updated context messages but not useChat — now refreshSignal causes surfaces to fetch and call useChat.setMessages directly

Test plan

  • Send a message in sidebar global mode → AI responds → no reload/flash after stream completes
  • Send a message in GlobalAssistantView (dashboard) global mode → no reload/flash
  • Open two tabs → send from tab A → message appears in tab B (refreshSignal + surface fetch)
  • Undo AI changes from same tab → messages update correctly
  • Undo AI changes from second tab → first tab messages update via refreshSignal
  • Disconnect network → reconnect → messages refresh without loading spinner flash
  • App goes to background → returns → sidebar re-fetches messages (handleAppResume)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Refactor
    • Improved global chat synchronization and stream-completion handling to make remote message updates, reconnections, and cross-tab edits more reliable and reduce duplicate refreshes.
    • Reduced unnecessary UI updates in global chat mode for smoother performance and more predictable pull-up/undo behavior.
  • Tests
    • Updated tests to validate the new refresh-signal-based refresh flow and stream cleanup behavior.

Replace unconditional refreshConversation() in
GlobalChatContext.onStreamComplete with local synthesis
from usePendingStreamsStore (matching the pattern already
used by useAgentChannelMultiplayer and AiChatView).

Own fresh streams skip entirely — GlobalAssistantView's
sync effect keeps context messages current. Remote and
bootstrapped streams synthesize the final message locally
without triggering setIsInitialized(false) or a server
round-trip, eliminating the visible reload/flash on every
AI response in the sidebar and dashboard global mode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 801c6893-1b8b-4e10-ba0c-ed548ffafb7f

📥 Commits

Reviewing files that changed from the base of the PR and between 7ba2842 and 553c8f5.

📒 Files selected for processing (1)
  • apps/web/src/contexts/__tests__/GlobalChatContext.test.tsx

📝 Walkthrough

Walkthrough

Tier GlobalChatContext into config/stream/conversation hooks, remove provider-owned messages, add numeric refreshSignal, consult usePendingStreamsStore in socket onStreamComplete(messageId) to synthesize or trigger refreshSignal for matching remote streams, and update GlobalAssistantView and SidebarChatTab to use refreshSignal and local message ownership.

Changes

Global chat refactor and UI integration

Layer / File(s) Summary
Conversation provider: types, state, init, and wiring
apps/web/src/contexts/GlobalChatContext.tsx
Expose refreshSignal, remove provider-owned messages, seed useChat from initialMessages, update create/load/init flows and URL replace logic, rebuild chatConfig/transport memoization, and remove the legacy combined provider/hook.
Reconnect and stream-complete -> refreshSignal
apps/web/src/contexts/GlobalChatContext.tsx
Replace reconnect-triggered reloads with refreshSignal increments; change onStreamComplete(messageId) to consult usePendingStreamsStore and increment refreshSignal when the completed stream belongs to the active conversation.
Provider wiring and exported hooks
apps/web/src/contexts/GlobalChatContext.tsx
Stream context now exposes only isStreaming/stopStreaming; legacy combined provider wrapper removed; exported useGlobalChat() removed; doc/comments updated for tiered hooks.
GlobalAssistantView: tiered hooks and local message ownership
apps/web/src/components/layout/middle-content/page-views/dashboard/GlobalAssistantView.tsx
Replace useGlobalChat with useGlobalChatConfig/useGlobalChatStream/useGlobalChatConversation; global-mode message updates use local useChat (setGlobalLocalMessages); adjust undo/pull-up refresh flows and add an effect that listens to refreshSignal.
SidebarChatTab: refreshSignal-driven refetch and local setters
apps/web/src/components/layout/right-sidebar/ai-assistant/SidebarChatTab.tsx
Use refreshSignal to detect remote events and re-fetch messages when sidebar is the refresher; refactor resume handling to fetch messages directly; simplify unifiedSetMessages to only update sidebar state; adjust undo-success handling.
Tests: pending-streams mock and refreshSignal assertions
apps/web/src/contexts/__tests__/GlobalChatContext.test.tsx
Replace legacy fetch-count/message-refresh assertions with refreshSignal-based tests; add in-memory mockStreams backing pending-streams store; update SSE/stream_complete/user_message/reconnect/unmount test flows.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I listen for sockets where small streams hide,
I stitch pending pieces so messages abide,
The refreshSignal ticks, a tiny counting drum,
Dashboard and sidebar wake and quietly come,
Hooray for tidy tiers — no reloads to hum!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: removing legacy message state from GlobalChatContext and fixing the stream reload issue that caused unnecessary full-page refreshes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pu/chat-re-rendering

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6b01d27456

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +309 to +313
setMessages((prev) =>
prev.some((m) => m.id === messageId)
? prev
: [...prev, synthesizeAssistantMessage(messageId, stream.parts)],
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update useChat state when finalizing remote global streams

This callback synthesizes the completed assistant message into GlobalChatContext state only, but the global UIs render from useChat state (GlobalAssistantView and SidebarChatTab use globalLocalMessages/messages, not context.messages). Because useChannelStreamSocket removes the pending stream immediately after this callback, non-origin tabs (and reload-mid-stream bootstrap cases) can lose the assistant reply at completion: the in-flight ghost disappears and no final message is added to the displayed useChat message list.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review! This comment was against the first intermediate commit (6b01d27) — the subsequent refactor commit (739e9b1) changed the approach entirely.

The final onStreamComplete does not synthesize into context state at all. Instead it just increments refreshSignal, which causes surfaces (GlobalAssistantView and SidebarChatTab) to call their own handlePullUpRefresh → fetch from the API → call useChat's setMessages directly. No context messages state involved.

The lookup-in-pending-store is safe: useChannelStreamSocket's fireComplete fires onStreamComplete(messageId) before removeStream(messageId) (which is in the finally block), so the stream is still in the store when we check it. For own fresh streams, handleStreamStart is filtered by the isOwnStream guard so they're never added to the store — those fall through to the 'own fresh stream' comment and do nothing (surface's useChat already has the message from the direct SSE connection).

Replace vestigial context messages/setMessages/refreshConversation
with a refreshSignal counter. Surfaces (GlobalAssistantView,
SidebarChatTab) now own their fetch-and-set cycles and watch
the signal for remote events (reconnect, undo, cross-tab edits).
Removes duplicate useGlobalChat() combined hook; all callers now
use the tiered selective hooks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@2witstudios 2witstudios changed the title fix(ai): stop reload after global chat stream ends refactor(ai): remove legacy GlobalChatContext message state + fix stream reload May 12, 2026
@2witstudios

Copy link
Copy Markdown
Owner Author

Re: Codex P1 comment on commit 6b01d27

Addressed by commit 739e9b1. The synthesis-into-context-state approach from the first commit has been replaced entirely.

In the new approach (refreshSignal):

  • All remote events (stream complete, reconnect, undo, cross-tab edit/delete) increment a counter in GlobalChatConversationContext
  • Surfaces (GlobalAssistantView, SidebarChatTab) watch the signal via useEffect and call their own handlePullUpRefresh(), which calls setGlobalLocalMessages(data.messages) / setGlobalMessages(data.messages) — both are useChat setters
  • useChat is now the single owner of message state; context holds zero message state

For own fresh streams, onStreamComplete returns early (own streams aren't in the pending store). For remote/bootstrapped streams, the signal fires a server fetch that updates useChat state directly. Messages are never permanently lost — there is a brief async fetch gap (~100-300ms before server responds) but the final persisted message always appears.

2witstudios and others added 2 commits May 12, 2026 08:04
Add globalIsInitialized check before fetching messages on app
resume, matching the guard already present in the refreshSignal
effect. Prevents a fetch attempt against a stale conversationId
while the global chat is still initializing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace useGlobalChat with tiered hooks, test refreshSignal
instead of context messages state, wire mockStreams for
onStreamComplete lookups.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@2witstudios
2witstudios merged commit d9b290e into master May 12, 2026
3 checks passed
@2witstudios
2witstudios deleted the pu/chat-re-rendering branch May 12, 2026 19:08
2witstudios added a commit that referenced this pull request May 15, 2026
…eam reload (#1308)

* fix(ai): stop reload after global chat stream ends

Replace unconditional refreshConversation() in
GlobalChatContext.onStreamComplete with local synthesis
from usePendingStreamsStore (matching the pattern already
used by useAgentChannelMultiplayer and AiChatView).

Own fresh streams skip entirely — GlobalAssistantView's
sync effect keeps context messages current. Remote and
bootstrapped streams synthesize the final message locally
without triggering setIsInitialized(false) or a server
round-trip, eliminating the visible reload/flash on every
AI response in the sidebar and dashboard global mode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(ai): remove legacy GlobalChatContext message state

Replace vestigial context messages/setMessages/refreshConversation
with a refreshSignal counter. Surfaces (GlobalAssistantView,
SidebarChatTab) now own their fetch-and-set cycles and watch
the signal for remote events (reconnect, undo, cross-tab edits).
Removes duplicate useGlobalChat() combined hook; all callers now
use the tiered selective hooks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ai): guard handleAppResume against uninitialized global state

Add globalIsInitialized check before fetching messages on app
resume, matching the guard already present in the refreshSignal
effect. Prevents a fetch attempt against a stale conversationId
while the global chat is still initializing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(ai): update GlobalChatContext tests for new API

Replace useGlobalChat with tiered hooks, test refreshSignal
instead of context messages state, wire mockStreams for
onStreamComplete lookups.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
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