Skip to content

refactor: ux performance optimization#1912

Merged
zerob13 merged 3 commits into
devfrom
fix/chat-input-scroll-performance
Jul 10, 2026
Merged

refactor: ux performance optimization#1912
zerob13 merged 3 commits into
devfrom
fix/chat-input-scroll-performance

Conversation

@zhangmo8

@zhangmo8 zhangmo8 commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Optimize chat submit/input state handling so attachment and skill-chip sync do not leave stale drafts or inconsistent composer state.
  • Optimize ChatPage scrolling by simplifying custom scroll handling and keeping only the intended behaviors: CSS-driven smooth scrolling, scroll-to-bottom on session open, and setting-controlled auto-scroll while generating.
  • Optimize assistant message activity rendering by making tool-call block keys stable and unique across multi-stage tool updates.
  • Polish chat interaction performance and visual hierarchy with shared motion/blur/z-index tokens across chat chrome, spotlight, side panels, overlays, and message controls.
  • Add UX interaction audit notes for the current optimization pass.

UI layout

BEFORE:

Chat route transition
└─ fades entire ChatPage message tree during session switches

Permission/question active
└─ ChatInputBox unmounted

Active scroll
└─ full blur + frequent row measurements

AFTER:

Chat route shell
└─ stable page wrapper without full message-tree fade

Permission/question active
└─ ChatInputBox kept mounted via v-show/inert

Active user scroll
└─ simpler native scrolling + CSS smooth behavior

Tests

  • pnpm run format
  • pnpm run i18n
  • pnpm run lint
  • pnpm run typecheck:web
  • pnpm exec vitest --config vitest.config.renderer.ts test/renderer/components/ChatInputBox.test.ts test/renderer/components/ChatPage.test.ts test/renderer/composables/useMessageWindow.test.ts
  • pnpm exec vitest --config vitest.config.renderer.ts test/renderer/components/message/messageActivityGroups.test.ts test/renderer/components/message/MessageItemAssistant.test.ts --run
  • git diff --check

Review

  • Ran targeted agent review loops; addressed reported scroll-measurement, programmatic-scroll, and duplicate tool-call key issues before updating this PR.

Summary by CodeRabbit

  • Bug Fixes
    • Prevented stale chat input updates during internal editor synchronization and file-chip syncing.
    • Reduced message-list layout jumps with more reliable height measurement and improved measurement batching.
    • Improved cached model-icon loading state handling to minimize flicker and shifts.
  • User Experience
    • Refined scrolling/overscroll behavior, gesture handling, and overlay/panel layering for chat and workspace.
    • Improved transition and blur rendering consistency (including macOS-specific behavior).
  • Documentation
    • Added a Chinese-language UX interaction audit document.
  • Tests
    • Updated/added unit tests covering editor syncing, measurement thresholds, and icon/loading and key uniqueness.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The PR adds a Chinese static UX audit and updates renderer interaction behavior. Changes include shared blur and z-index tokens, overscroll containment, platform detection, editor synchronization safeguards, icon loading states, overlay transitions, chat virtualization and scroll measurement coordination, route shell sizing, activity-key handling, and corresponding tests.

UX and renderer updates

Layer / File(s) Summary
Static UX audit
docs/ux-interaction-audit-2026-07-09.md
Adds audit methodology, comparison findings, supplemental issues, priorities, evaluation, and a static-output disclaimer.
Visual tokens and scroll containment
src/renderer/src/App.vue, src/renderer/src/assets/style.css, src/renderer/src/components/chat/..., src/renderer/src/components/message/..., src/renderer/src/components/sidepanel/...
Adds shared blur/z-index tokens, platform dataset detection, scrollbar updates, and overscroll containment across renderer surfaces.
Editor, icons, and overlays
src/renderer/src/components/chat-input/..., src/renderer/src/components/chat/..., src/renderer/src/components/icons/..., src/renderer/src/components/spotlight/..., src/renderer/src/components/message/...
Guards internal TipTap synchronization, adds icon loading placeholders, updates overlay transitions and positioning, and standardizes popup and action-button stacking.
Chat measurement and session coordination
src/renderer/src/composables/message/useMessageWindow.ts, src/renderer/src/components/chat/MessageListRow.vue, src/renderer/src/pages/ChatPage.vue, src/renderer/src/components/message/MessageBlockActivityGroup.vue, src/renderer/src/components/message/messageActivityGroups.ts
Coalesces height measurements, defers updates during scrolling, refactors session and viewport coordination, adjusts cleanup, and makes repeated activity keys distinct.
Route shells and behavior validation
src/renderer/src/views/ChatTabView.vue, test/renderer/components/*, test/renderer/composables/*
Adds keyed route shells and scoped sizing rules, and updates tests for editor transactions, mounted pending input, measurement deltas, estimated heights, and activity keys.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested reviewers: zerob13, deepinfect

Sequence Diagram(s)

sequenceDiagram
  participant ChatPage
  participant MessageListRow
  participant useMessageWindow
  participant Browser
  ChatPage->>Browser: receive scroll and pointer events
  Browser-->>ChatPage: report scrolling intent
  ChatPage->>MessageListRow: defer row measurements during gestures
  MessageListRow->>useMessageWindow: submit measured heights
  useMessageWindow-->>ChatPage: flush coalesced layout updates
  ChatPage->>Browser: restore viewport anchor when idle
Loading
🚥 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 is concise and broadly matches the PR’s UX and performance-focused refactor.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/chat-input-scroll-performance

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.

@zhangmo8 zhangmo8 changed the title fix(chat): clear drafts and smooth scrolling refractor: ux performance optimization Jul 10, 2026
@zhangmo8 zhangmo8 marked this pull request as draft July 10, 2026 01:32

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 6

🧹 Nitpick comments (2)
src/renderer/src/components/chat/ChatTopBar.vue (1)

4-9: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use shared blur and z-index utilities instead of static inline styling.

dc-blur-panel and z-[var(--dc-z-sticky)] already represent these values. This also preserves the shared -webkit-backdrop-filter behavior.

As per coding guidelines, **/*.{ts,tsx,vue} must use Tailwind for styling.

Proposed fix
-    class="sticky top-0 flex h-12 items-center justify-between bg-background/60 px-4 window-drag-region transition-[padding] duration-[var(--dc-motion-default)] ease-[var(--dc-ease-out-express)]"
-    style="
-      z-index: var(--dc-z-sticky);
-      backdrop-filter: blur(var(--dc-blur-panel));
-      -webkit-backdrop-filter: blur(var(--dc-blur-panel));
-    "
+    class="sticky top-0 z-[var(--dc-z-sticky)] dc-blur-panel flex h-12 items-center justify-between bg-background/60 px-4 window-drag-region transition-[padding] duration-[var(--dc-motion-default)] ease-[var(--dc-ease-out-express)]"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/chat/ChatTopBar.vue` around lines 4 - 9, Replace
the inline z-index and backdrop-filter styles on the ChatTopBar root element
with the shared Tailwind utilities `z-[var(--dc-z-sticky)]` and `dc-blur-panel`,
preserving the existing layout and transition classes and removing the entire
style attribute.

Source: Coding guidelines

src/renderer/src/components/chat/ChatSearchBar.vue (1)

3-7: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse dc-blur-overlay instead of duplicating the blur inline.

The shared utility already defines both backdrop-filter and -webkit-backdrop-filter. Reusing it keeps this component aligned with the visual token layer.

As per coding guidelines, **/*.{ts,tsx,vue} must use Tailwind for styling.

Proposed fix
-    class="chat-search-bar flex w-full max-w-[24rem] items-center gap-2 rounded-2xl border bg-background/90 px-2.5 py-2 shadow-lg"
-    style="
-      backdrop-filter: blur(var(--dc-blur-overlay));
-      -webkit-backdrop-filter: blur(var(--dc-blur-overlay));
-    "
+    class="chat-search-bar dc-blur-overlay flex w-full max-w-[24rem] items-center gap-2 rounded-2xl border bg-background/90 px-2.5 py-2 shadow-lg"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/components/chat/ChatSearchBar.vue` around lines 3 - 7,
Replace the inline backdrop-filter declarations on the ChatSearchBar root
element with the shared dc-blur-overlay utility class, and remove the
accompanying style attribute. Keep the existing layout classes unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/ux-interaction-audit-2026-07-09.md`:
- Around line 93-110: Update section 2.3 to reference the semantic blur tokens
actually used by each component, including --dc-blur-panel in ChatInputBox.vue
and --dc-blur-overlay in AgentProgressFloat.vue, and reflect the separate soft,
panel, and overlay definitions in style.css with their resolved values. Remove
the outdated backdrop-blur utility-class entries and revise the recommendation
to assess consolidation without assuming a single global token or removing
intentional layer distinctions.
- Around line 20-39: The ChatPage.vue finding about ChatInputBox and
ChatStatusBar unmounting is outdated because the current implementation
preserves ChatInputBox with v-show and inert during permission/question states.
Update the referenced finding and P0 recommendation to describe the current
mounted-but-hidden behavior, or explicitly label the unmounting and missing
draft recovery claims as historical/pre-fix evidence.

In `@src/renderer/src/assets/style.css`:
- Around line 1110-1117: Update the dark-mode scrollbar rules near the existing
`.dark *::-webkit-scrollbar-thumb` selectors to also target the root element
itself, including `html.dark` and the corresponding root thumb pseudo-elements.
Add a dark-mode `scrollbar-color` override on the dark root selector for
Firefox, while preserving the existing descendant thumb and hover styling.

In `@src/renderer/src/components/icons/ModelIcon.vue`:
- Around line 124-129: Make the opacity classes mutually exclusive in the
ModelIcon template’s class binding: ensure the inverted state applies
opacity-50, while the non-inverted loaded state applies opacity-100, and retain
opacity-0 for unloaded icons. Update the binding so iconLoaded and invert cannot
add conflicting opacity classes.

In `@src/renderer/src/pages/ChatPage.vue`:
- Around line 9-12: Extend gesture gating in ChatPage’s touch/pointer handling
so it remains active for the entire touch or drag rather than expiring after the
140ms timer. Update markListScrolling and the relevant `@touchstart/`@pointerdown
bindings to refresh or maintain the gate during movement, and clear it only on
the corresponding touchend/touchcancel or pointerup/pointercancel events; apply
the same change to the related handlers around the alternate referenced section.
- Around line 619-684: Recheck scroll ownership inside the requestAnimationFrame
callback in scheduleViewportAnchorRestore before any scrollTop mutation. If
isProgrammaticScrollActive() or hasRecentUserScrollAwayIntent() is true, skip
restoration and avoid applying the pending anchor or above-viewport delta.

---

Nitpick comments:
In `@src/renderer/src/components/chat/ChatSearchBar.vue`:
- Around line 3-7: Replace the inline backdrop-filter declarations on the
ChatSearchBar root element with the shared dc-blur-overlay utility class, and
remove the accompanying style attribute. Keep the existing layout classes
unchanged.

In `@src/renderer/src/components/chat/ChatTopBar.vue`:
- Around line 4-9: Replace the inline z-index and backdrop-filter styles on the
ChatTopBar root element with the shared Tailwind utilities
`z-[var(--dc-z-sticky)]` and `dc-blur-panel`, preserving the existing layout and
transition classes and removing the entire style attribute.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 01fcf7b9-e3c5-450a-ac8b-ab54c89d6730

📥 Commits

Reviewing files that changed from the base of the PR and between 95c3af7 and cdcb8c1.

📒 Files selected for processing (35)
  • docs/ux-interaction-audit-2026-07-09.md
  • src/renderer/src/App.vue
  • src/renderer/src/assets/style.css
  • src/renderer/src/components/chat-input/McpIndicator.vue
  • src/renderer/src/components/chat/AgentProgressFloat.vue
  • src/renderer/src/components/chat/ChatInputBox.vue
  • src/renderer/src/components/chat/ChatSearchBar.vue
  • src/renderer/src/components/chat/ChatStatusBar.vue
  • src/renderer/src/components/chat/ChatToolInteractionOverlay.vue
  • src/renderer/src/components/chat/ChatTopBar.vue
  • src/renderer/src/components/chat/MemoryTurnDialog.vue
  • src/renderer/src/components/chat/MemoryUpdateChip.vue
  • src/renderer/src/components/chat/MessageListRow.vue
  • src/renderer/src/components/chat/PendingInputLane.vue
  • src/renderer/src/components/chat/mentions/SuggestionList.vue
  • src/renderer/src/components/editor/mention/PromptParamsDialog.vue
  • src/renderer/src/components/icons/ModelIcon.vue
  • src/renderer/src/components/message/MessageActionButtons.vue
  • src/renderer/src/components/message/MessageBlockActivityGroup.vue
  • src/renderer/src/components/message/MessageBlockToolCall.vue
  • src/renderer/src/components/message/MessageToolbar.vue
  • src/renderer/src/components/message/ReferencePreview.vue
  • src/renderer/src/components/popup/TranslatePopup.vue
  • src/renderer/src/components/sidepanel/ChatSidePanel.vue
  • src/renderer/src/components/sidepanel/WorkspacePanel.vue
  • src/renderer/src/components/sidepanel/WorkspaceViewer.vue
  • src/renderer/src/components/sidepanel/viewer/WorkspaceInfoPane.vue
  • src/renderer/src/components/sidepanel/viewer/WorkspacePreviewPane.vue
  • src/renderer/src/components/spotlight/SpotlightOverlay.vue
  • src/renderer/src/composables/message/useMessageWindow.ts
  • src/renderer/src/pages/ChatPage.vue
  • src/renderer/src/views/ChatTabView.vue
  • test/renderer/components/ChatInputBox.test.ts
  • test/renderer/components/ChatPage.test.ts
  • test/renderer/composables/useMessageWindow.test.ts

Comment thread docs/ux-interaction-audit-2026-07-09.md
Comment thread docs/ux-interaction-audit-2026-07-09.md
Comment thread src/renderer/src/assets/style.css
Comment thread src/renderer/src/components/icons/ModelIcon.vue
Comment thread src/renderer/src/pages/ChatPage.vue Outdated
Comment thread src/renderer/src/pages/ChatPage.vue
@zhangmo8 zhangmo8 marked this pull request as ready for review July 10, 2026 02:49
@zhangmo8 zhangmo8 changed the title refractor: ux performance optimization refactor: ux performance optimization Jul 10, 2026

@zerob13 zerob13 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed the current head (9c4e6717c). The simplified scroll implementation removes several still-required behaviors, and the targeted ChatPage suite does not pass on this head. Inline comments cover the reproducible regressions.

@@ -5,8 +5,6 @@
data-testid="chat-page"
:data-generating="String(isGenerating)"
class="message-list-container h-full w-full min-w-0 overflow-y-auto"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P1 — Restore a trigger for loading older history. Removing the scroll binding also removes the only path that called messageStore.loadOlderMessages() (onScrollloadOlderMessagesAtTop, both deleted below). Session restore still requests only INITIAL_MESSAGE_RESTORE_COUNT (40), and there is no sentinel/observer or explicit load-more action elsewhere. As a result, conversations longer than 40 messages can never expose their older history.

Comment thread src/renderer/src/pages/ChatPage.vue Outdated
<MessageList
ref="messageListRef"
:messages="visibleDisplayMessages"
:messages="displayMessages"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P2 — This removes the performance bound the PR is meant to improve. displayMessages mounts the entire loaded history instead of the previous window. The existing bounds mounted message rows for long loaded histories test now receives 300 rows instead of at most 90. Every row also still installs a ResizeObserver in MessageListRow, even though @measure was removed here, so long conversations pay that observer/rAF cost for no consumer. Please either retain windowing or remove the unused measurement machinery and explicitly re-evaluate the long-history performance contract.

Comment thread src/renderer/src/pages/ChatPage.vue Outdated
}

applyRestoredSessionSummary(restoredSession)
scheduleScrollToBottom()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P1 — Preserve Spotlight message jumps during restore. The Spotlight store still sets pendingMessageJump before selecting a message result's session, but this PR removes useSpotlightStore and focusPendingSpotlightMessageJump. Restore now always schedules a bottom scroll here, so selecting a message result lands at the conversation bottom, never highlights the target, and never clears the pending jump. The existing consumes pending spotlight message jumps... test fails on this head.

Comment thread src/renderer/src/pages/ChatPage.vue Outdated

remainingFrames -= 1
scheduleNextFrame()
el.scrollTo({ top: el.scrollHeight })

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P1 — The documented targeted test command fails on this call. pnpm exec vitest --config vitest.config.renderer.ts test/renderer/components/ChatPage.test.ts --run produces TypeError: el.scrollTo is not a function from this line (46 uncaught exceptions in the current jsdom environment). After locally supplying only a scrollTo polyfill, five behavioral assertions still fail. Using the previous el.scrollTop = Math.max(el.scrollHeight - el.clientHeight, 0) path would keep the production behavior deterministic and avoid requiring a new test-environment API shim.

Comment thread src/renderer/src/pages/ChatPage.vue Outdated
} else {
scheduleScrollMetricsRead()
if (isGenerating.value && uiSettingsStore.autoScrollEnabled) {
scheduleScrollToBottom()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P1 — Do not override the user's reading position during generation. Every stream revision now schedules a bottom scroll whenever the setting is enabled. The removed implementation tracked isNearBottom/shouldAutoFollow and stopped following after an upward wheel/scroll gesture; with that state gone, a user who scrolls up to read earlier content is pulled back to the bottom on the next token update. Keep a minimal near-bottom/follow-state guard so auto-scroll can be disengaged by user intent.

Comment thread src/renderer/src/pages/ChatPage.vue Outdated
await nextTick()
applyChatSearchHighlights(root, chatSearchQuery.value)
setActiveChatSearchResult(root, result, { behavior })
setActiveChatSearchResult(root, chatSearchResults.value[nextIndex], { scroll: false })

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P1 — Search navigation no longer reveals off-screen matches. setActiveChatSearchResult calls scrollIntoView unless options.scroll === false; this argument explicitly disables it. Since revealChatSearchResult and its fallback were also removed, next/previous navigation only changes an off-screen highlight and gives the user no visible result. Removing { scroll: false } restores the helper's built-in scrolling now that all rows are mounted.

Comment thread src/renderer/src/pages/ChatPage.vue Outdated
overscroll-behavior: contain;
overflow-anchor: none;
scroll-behavior: auto;
scroll-behavior: smooth;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P2 — Avoid smooth behavior for high-frequency auto-follow. This CSS also governs the scrollTo issued for every streaming revision. Each update starts/restarts a smooth animation while scrollHeight keeps growing, so the viewport can visibly lag or crawl behind the generated content. Keep the container at scroll-behavior: auto and request smooth behavior only for one-shot, user-initiated navigation.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/renderer/src/pages/ChatPage.vue (1)

101-110: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reserve space for question-only and combined interaction overlays.

The reservation is zero without a plan and otherwise measures only the plan trigger. Question overlays can therefore obscure the final messages beneath the absolute layer.

Proposed fix
 function syncPlanFloatReservedHeight() {
   const layer = planFloatLayer.value
-  if (!latestPlanSnapshot.value || !layer) {
+  if ((!latestPlanSnapshot.value && !activePendingInteraction.value) || !layer) {
     planFloatReservedHeight.value = 0
     return
   }

   const trigger = layer.querySelector<HTMLElement>('[data-testid="agent-progress-float-trigger"]')
   const triggerHeight = trigger?.offsetHeight ?? layer.offsetHeight
-  const next = triggerHeight + PLAN_FLOAT_SAFE_GAP
+  const contentHeight = activePendingInteraction.value ? layer.offsetHeight : triggerHeight
+  const next = contentHeight + PLAN_FLOAT_SAFE_GAP
-  if (!latestPlanSnapshot.value || !layer) {
+  if ((!latestPlanSnapshot.value && !activePendingInteraction.value) || !layer) {
 watch(
-  [latestPlanSnapshot, isPlanFloatCollapsed],
+  [latestPlanSnapshot, isPlanFloatCollapsed, activePendingInteraction],

Also applies to: 528-575, 2237-2250

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/pages/ChatPage.vue` around lines 101 - 110, Update the
layout reservation associated with the planFloatLayer and its related sections
so it accounts for question-only as well as combined plan-and-question overlays,
not just latestPlanSnapshot. Use activePendingInteraction when determining the
reserved space, and ensure the corresponding sizing logic near the other
referenced interaction-overlay sections stays consistent so final messages are
not obscured.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/ux-interaction-audit-2026-07-09.md`:
- Line 246: The audit entry incorrectly attributes focus preservation to inert.
In the row describing ChatPage.vue’s input area, state only that v-show keeps
the editor mounted and preserves draft state; if focus restoration is required,
document an explicit focus action after the panel closes rather than relying on
inert.

In `@src/renderer/src/pages/ChatPage.vue`:
- Around line 1062-1081: Guard the post-load restoration in the history-loading
handler against session changes: capture the active session identifier before
awaiting messageStore.loadOlderMessages(), then verify it is unchanged before
adjusting scrollTop or syncing viewport metrics. Return early when the session
differs, using the handler and messageStore.loadOlderMessages() symbols to
locate the logic.
- Around line 1021-1041: Update the scroll-intent handling around
isSessionRestoreScrollSettleActive and scheduleScrollMetricsRead so keyboard
scrolling is classified as user-initiated, not only wheel, touch, or pointer
input. Track relevant keyboard navigation events (especially PageUp and End),
arm the same user-scroll classification for them, and ensure End at the bottom
re-enables shouldAutoFollow.
- Around line 738-768: Delay flushPendingMeasures until scroll-away intent
expires and anchor restoration has completed, rather than flushing immediately
after the 140ms idle delay; update finishListScrollingAfterIdle to retain or
reschedule pending measurements while the gesture/scroll-away state remains
active. Also make the requestAnimationFrame callback in flushPendingMeasures
pause processing when a new list gesture starts, preserving the remaining queue
for a later flush instead of applying measurements during active scrolling.

---

Outside diff comments:
In `@src/renderer/src/pages/ChatPage.vue`:
- Around line 101-110: Update the layout reservation associated with the
planFloatLayer and its related sections so it accounts for question-only as well
as combined plan-and-question overlays, not just latestPlanSnapshot. Use
activePendingInteraction when determining the reserved space, and ensure the
corresponding sizing logic near the other referenced interaction-overlay
sections stays consistent so final messages are not obscured.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 79eeafcb-d057-41ea-ad7e-c0685c9fad26

📥 Commits

Reviewing files that changed from the base of the PR and between 9c4e671 and 625bd8c.

📒 Files selected for processing (6)
  • docs/ux-interaction-audit-2026-07-09.md
  • src/renderer/src/assets/style.css
  • src/renderer/src/components/chat/ChatSearchBar.vue
  • src/renderer/src/components/chat/ChatTopBar.vue
  • src/renderer/src/components/icons/ModelIcon.vue
  • src/renderer/src/pages/ChatPage.vue
✅ Files skipped from review due to trivial changes (1)
  • src/renderer/src/components/chat/ChatSearchBar.vue
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/renderer/src/components/chat/ChatTopBar.vue
  • src/renderer/src/components/icons/ModelIcon.vue
  • src/renderer/src/assets/style.css

Comment thread docs/ux-interaction-audit-2026-07-09.md
Comment thread src/renderer/src/pages/ChatPage.vue
Comment thread src/renderer/src/pages/ChatPage.vue
Comment on lines 1062 to +1081
const previousScrollHeight = el.scrollHeight
const previousScrollTop = el.scrollTop
const loadedCount = await messageStore.loadOlderMessages()
if (loadedCount === 0) {
return
}

await nextTick()
const nextScrollHeight = el.scrollHeight
el.scrollTop = previousScrollTop + (nextScrollHeight - previousScrollHeight)
syncMessageViewportMetrics(el)
const container = scrollContainer.value
if (!container) {
return
}

// Use the *current* scrollTop after await, not the pre-await snapshot. The
// user may keep scrolling while history loads; writing back the old value
// feels like the viewport "snapped to a few seconds ago".
const heightDelta = container.scrollHeight - previousScrollHeight
if (heightDelta !== 0) {
container.scrollTop += heightDelta
}
syncMessageViewportMetrics(container)

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Guard history restoration against session switches.

If the session changes while loadOlderMessages() is pending, this continuation adjusts the newly mounted session’s scrollTop using the previous session’s height.

Proposed fix
   const previousScrollHeight = el.scrollHeight
+  const sessionId = props.sessionId
+  const requestId = sessionRestoreRequestId
   const loadedCount = await messageStore.loadOlderMessages()
-  if (loadedCount === 0) {
+  if (
+    loadedCount === 0 ||
+    props.sessionId !== sessionId ||
+    sessionRestoreRequestId !== requestId
+  ) {
     return
   }

   await nextTick()
+  if (props.sessionId !== sessionId || sessionRestoreRequestId !== requestId) {
+    return
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const previousScrollHeight = el.scrollHeight
const previousScrollTop = el.scrollTop
const loadedCount = await messageStore.loadOlderMessages()
if (loadedCount === 0) {
return
}
await nextTick()
const nextScrollHeight = el.scrollHeight
el.scrollTop = previousScrollTop + (nextScrollHeight - previousScrollHeight)
syncMessageViewportMetrics(el)
const container = scrollContainer.value
if (!container) {
return
}
// Use the *current* scrollTop after await, not the pre-await snapshot. The
// user may keep scrolling while history loads; writing back the old value
// feels like the viewport "snapped to a few seconds ago".
const heightDelta = container.scrollHeight - previousScrollHeight
if (heightDelta !== 0) {
container.scrollTop += heightDelta
}
syncMessageViewportMetrics(container)
const previousScrollHeight = el.scrollHeight
const sessionId = props.sessionId
const requestId = sessionRestoreRequestId
const loadedCount = await messageStore.loadOlderMessages()
if (
loadedCount === 0 ||
props.sessionId !== sessionId ||
sessionRestoreRequestId !== requestId
) {
return
}
await nextTick()
if (props.sessionId !== sessionId || sessionRestoreRequestId !== requestId) {
return
}
const container = scrollContainer.value
if (!container) {
return
}
// Use the *current* scrollTop after await, not the pre-await snapshot. The
// user may keep scrolling while history loads; writing back the old value
// feels like the viewport "snapped to a few seconds ago".
const heightDelta = container.scrollHeight - previousScrollHeight
if (heightDelta !== 0) {
container.scrollTop += heightDelta
}
syncMessageViewportMetrics(container)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/renderer/src/pages/ChatPage.vue` around lines 1062 - 1081, Guard the
post-load restoration in the history-loading handler against session changes:
capture the active session identifier before awaiting
messageStore.loadOlderMessages(), then verify it is unchanged before adjusting
scrollTop or syncing viewport metrics. Return early when the session differs,
using the handler and messageStore.loadOlderMessages() symbols to locate the
logic.

@zerob13 zerob13 merged commit 959b882 into dev Jul 10, 2026
2 checks passed
@zhangmo8 zhangmo8 deleted the fix/chat-input-scroll-performance branch July 10, 2026 04:50
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.

2 participants