Skip to content

fix(web): Improve WebSocket reconnection experience in Web UI#1669

Open
zgat wants to merge 2 commits intoMoonshotAI:mainfrom
zgat:main
Open

fix(web): Improve WebSocket reconnection experience in Web UI#1669
zgat wants to merge 2 commits intoMoonshotAI:mainfrom
zgat:main

Conversation

@zgat
Copy link
Copy Markdown

@zgat zgat commented Mar 31, 2026

Description

Optimize Web UI WebSocket reconnection experience to prevent message list flickering/jumping. Ensures no input content loss or scroll position reset after WebSocket reconnection.

Changes

• Preserve messages on WebSocket reconnection: No longer clears the message list when reconnecting, preventing users from seeing a blank page
• Optimize scroll behavior: Keep isReplayingHistory state unchanged during reconnection to avoid triggering automatic message list scrolling
• history_complete handling: Properly set isReplayingHistory(false) when receiving history_complete to prevent unexpected scrolling from subsequent state changes
• VirtualizedMessageList optimization: Use useMemo to cache initialTopMostItemIndex to avoid unnecessary recalculation
• Build configuration: Add [tool.hatch.build.targets.wheel] configuration to ensure static files are properly packaged
Technical Details

Technical Details

Core changes in useSessionStream.ts:
• resetState() adds preserveMessages parameter to preserve existing messages during reconnection
• connect() supports isReconnect option to distinguish between initial connection and reconnection scenarios
• Do not reset isReplayingHistory during reconnection to avoid triggering VirtualizedMessageList scroll logic

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked the related issue, if any. (None)
  • I have run make gen-changelog to update the changelog.
  • I have run make gen-docs to update the user documentation. (Not needed, internal optimization)

Open with Devin

Copy link
Copy Markdown

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

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: 1e12de063e

ℹ️ 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 +2418 to +2421
if (isReconnect) {
// Reconnect: preserve messages and slash commands
resetState(true, true);
} else {
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 Clear or dedupe messages before reconnect history replay

This reconnect path preserves messages (resetState(true, true)), but the backend always replays persisted wire history on every WebSocket attach (src/kimi_cli/web/api/sessions.py calls replay_history when history exists). Because processEvent rebuilds chat entries with fresh client-side IDs, replayed turns are appended again instead of merged, so any reconnect (watchdog/network blip/manual reconnect) can duplicate the entire transcript and tool outputs in the UI.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines +2418 to +2424
if (isReconnect) {
// Reconnect: preserve messages and slash commands
resetState(true, true);
} else {
// First connect: reset everything
resetState(true);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 WebSocket reconnect duplicates all messages because history replay generates new IDs against preserved messages

On reconnect, connect({ isReconnect: true }) at web/src/hooks/useSessionStream.ts:2774 calls resetState(true, true) which preserves existing messages and does NOT reset isReplayingRef.current to true (lines 880-888). When the new WebSocket connects, the server replays all history events. Since isReplayingRef.current remains false, processEvent at line 2166 passes isReplay=false for every replayed event. Each event handler (TurnBegin at line 1083, ContentPart at line 1122, ToolCall at line 1222) calls getNextMessageId() which generates a fresh UUID via createMessageId (web/src/hooks/utils.ts:42). The upsertMessage function (web/src/hooks/useSessionStream.ts:808-816) searches by ID, finds no match against the preserved messages, and appends each replayed event as a brand-new message. The result is every message appearing twice in the UI after any WebSocket reconnection (triggered by network issues, watchdog timeout at line 2468, history_complete timeout at line 2097, etc.).

Before vs after this PR

The old code correctly handled reconnect by clearing messages:

resetState(true);  // old: always reset isReplayingRef to true
setMessages([]);   // old: clear messages before replay

The new code preserves messages but doesn't account for the server replaying history:

resetState(true, true); // preserveMessages=true: skips setMessages([]) AND skips isReplayingRef=true
Prompt for agents
In web/src/hooks/useSessionStream.ts, the reconnect path at lines 2418-2424 calls resetState(true, true) which preserves messages but does NOT set isReplayingRef.current = true. When the server replays history on the new WebSocket, all events generate new client-side message IDs (via createMessageId/uuidV4) that don't match preserved messages, causing upsertMessage to append duplicates.

To fix this, you need one of these approaches:

1. (Simplest) In the resetState function around lines 880-888, when preserveMessages=true, still set isReplayingRef.current = true so that the server's history replay is processed with isReplay=true. Then after history_complete arrives, clear the old preserved messages and replace them with the freshly replayed ones. This requires tracking which messages came from the new replay vs the old preserved set.

2. (Alternative) Keep clearing messages on reconnect (revert to old behavior) and instead fix the scroll jump issue in VirtualizedMessageList separately — for example, by saving and restoring the scroll position after replay completes rather than trying to preserve the message array.

3. (Alternative) On reconnect, set isReplayingRef.current = true AND clear messages in resetState (like the old code), but immediately re-set isReplayingHistory to false after clearing so the VirtualizedMessageList doesn't trigger a scroll reset. The key={conversationKey} on Virtuoso won't change (same session), so scroll position may be naturally preserved.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

zgat added 2 commits March 31, 2026 15:25
- 重连时保留现有消息,不清空消息列表
- 重连时保持 isReplayingHistory 状态不变,避免触发滚动
- history_complete 时设置 isReplayingHistory(false)
- 优化 VirtualizedMessageList 的 initialTopMostItemIndex 计算
- 添加静态文件到构建配置
Copy link
Copy Markdown

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

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: 6ed2c2c8b3

ℹ️ 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 +883 to +885
// Only reset replay state when clearing messages
isReplayingRef.current = true;
setIsReplayingHistory(true);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reset replay flag when preserving messages on reconnect

When reconnect calls resetState(true, true), this branch skips isReplayingRef.current = true, even though resetState still clears first-turn tracking refs. That makes replayed history events go through processEvent as non-replay events, so TurnBegin sets hasTurnStartedRef and a later idle session_status can fire onFirstTurnComplete again. In this repo, chat-workspace-container wires that callback to generateTitle, so a reconnect on any non-empty session can trigger extra title-generation calls and other non-replay side effects.

Useful? React with 👍 / 👎.

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