Skip to content

Conversation

@zerob13
Copy link
Collaborator

@zerob13 zerob13 commented Jan 14, 2026

Summary by CodeRabbit

  • New Features

    • Collapsible AI reasoning blocks now persist collapse state preferences
  • Improvements

    • Enhanced message list responsiveness with dynamic content height tracking
    • Optimized scrolling behavior with improved retry logic and timing

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 14, 2026

📝 Walkthrough

Walkthrough

The changes introduce a collapse event emission system for think blocks and enhance message list height tracking. MessageBlockThink now emits a toggle-collapse event when collapse state changes, MessageItemAssistant listens to this event and propagates it upstream, and MessageList implements resize observers to track message height changes and trigger scroll updates accordingly. Scroll retry constants are also adjusted in useMessageScroll.

Changes

Cohort / File(s) Change Summary
Think block collapse event emission
src/renderer/src/components/message/MessageBlockThink.vue, src/renderer/src/components/message/MessageItemAssistant.vue
Added toggle-collapse event definition in MessageBlockThink with inverted boolean payload; watcher now emits event on collapse state change. MessageItemAssistant wires event binding and introduces handleCollapseToggle handler to emit variantChanged event carrying message ID for parent-level variant updates.
Message list height and scroll tracking
src/renderer/src/components/message/MessageList.vue
Added resize observer infrastructure (setupMessageResizeObserver, cleanupMessageResizeObservers) to track message height changes; introduced internal state maps for previous heights and active observers; added trackMessageHeightChange logic with threshold-based debouncing to trigger scrollerUpdate on DynamicScroller when heights change; extended dependency arrays in size calculations to include rendering state key; integrated observers into lifecycle hooks and prop change handlers.
Scroll configuration
src/renderer/src/composables/message/useMessageScroll.ts
Increased MAX_SCROLL_RETRIES from 8 to 12 and SCROLL_RETRY_DELAY from 50 to 80 milliseconds.

Possibly related PRs

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A thought block collapses with grace,
Events propagate through the place,
Heights now dance with observers so keen,
Scrollers adjust to what's been seen,
Messages flow in a smooth, measured pace! 📜✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: scroll to bottom' directly relates to the main objective of the PR, which addresses scrolling functionality through enhanced scroll retry logic and height tracking.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing touches
  • 📝 Generate docstrings

🧹 Recent nitpick comments
src/renderer/src/components/message/MessageBlockThink.vue (2)

26-28: Clarify the inverted boolean semantics.

The emit sends !newValue where newValue is the collapse state (true = collapsed). This means when the block collapses (collapse=true), you emit isCollapsed=false, which is semantically inverted.

While the parent currently ignores the boolean value, this could cause confusion for future maintainers. Consider either:

  1. Emit the actual collapse state: emit('toggle-collapse', newValue)
  2. Rename the parameter to clarify: (e: 'toggle-collapse', isExpanded: boolean)

48-49: Translate comment to English.

As per coding guidelines, all comments must be in English.

Suggested change
-  // 保留小数点后最多两位,去除尾随的0
+  // Keep up to two decimal places, remove trailing zeros
   return parseFloat(duration.toFixed(2))
src/renderer/src/components/message/MessageItemAssistant.vue (1)

283-284: Translate error message to English.

As per coding guidelines, all log messages must be in English.

Suggested change
   } catch (error) {
-    console.error('创建对话分支失败:', error)
+    console.error('Failed to create conversation fork:', error)
   }
src/renderer/src/components/message/MessageList.vue (2)

198-215: Minor: setTimeout not cleared on unmount.

The setTimeout at line 207 isn't tracked or cleared during component unmount. If the component unmounts while this timeout is pending, it may attempt to update pendingHeightUpdate on an unmounted component.

Consider tracking this timeout for cleanup:

Suggested fix
+let heightUpdateTimer: number | null = null
+
 const trackMessageHeightChange = (messageId: string, newHeight: number) => {
   const previousHeight = previousHeights.get(messageId) ?? 0
   const heightDiff = Math.abs(newHeight - previousHeight)

   if (heightDiff > HEIGHT_CHANGE_THRESHOLD && heightDiff < MAX_REASONABLE_HEIGHT) {
     if (!pendingHeightUpdate.value) {
       pendingHeightUpdate.value = true
       nextTick(() => {
         scrollerUpdate()
-        setTimeout(() => {
+        heightUpdateTimer = window.setTimeout(() => {
+          heightUpdateTimer = null
           pendingHeightUpdate.value = false
         }, SCROLL_CHECK_DELAY)
       })
     }
   }

   previousHeights.set(messageId, newHeight)
 }

Then in onBeforeUnmount:

+  if (heightUpdateTimer) {
+    clearTimeout(heightUpdateTimer)
+    heightUpdateTimer = null
+  }
   cleanupMessageResizeObservers()

222-256: Consider hoisting the debounced callback.

useDebounceFn is called inside setupMessageResizeObserver, which is invoked multiple times (on mount, virtual updates, and refreshes). Each invocation creates a new debounced function instance, which is wasteful.

Consider hoisting the debounced callback outside the function:

Suggested refactor
+const resizeObserverCallback = (entries: ResizeObserverEntry[]) => {
+  for (const entry of entries) {
+    const messageId = entry.target.getAttribute('data-message-id')
+    if (!messageId) continue
+    const newHeight = entry.contentRect.height
+    trackMessageHeightChange(messageId, newHeight)
+  }
+}
+
+const debouncedResizeCallback = useDebounceFn(resizeObserverCallback, 100)
+
 const setupMessageResizeObserver = () => {
   const container = messagesContainer.value
   if (!container) return

-  const observerCallback = (entries: ResizeObserverEntry[]) => {
-    for (const entry of entries) {
-      const messageId = entry.target.getAttribute('data-message-id')
-      if (!messageId) continue
-
-      const newHeight = entry.contentRect.height
-      trackMessageHeightChange(messageId, newHeight)
-    }
-  }
-
-  const debouncedObserverCallback = useDebounceFn(observerCallback, 100)

   const visibleMessages = container.querySelectorAll('[data-message-id]')

   visibleMessages.forEach((element) => {
     const el = element as HTMLElement
     const messageId = el.getAttribute('data-message-id')
     if (!messageId) return

     const previousObserver = messageResizeObservers.get(messageId)
     if (previousObserver) {
       previousObserver.disconnect()
     }

-    const observer = new ResizeObserver(debouncedObserverCallback)
+    const observer = new ResizeObserver(debouncedResizeCallback)
     observer.observe(el)
     messageResizeObservers.set(messageId, observer)

     previousHeights.set(messageId, el.getBoundingClientRect().height)
   })
 }

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d4c6a49 and e9212a2.

📒 Files selected for processing (4)
  • src/renderer/src/components/message/MessageBlockThink.vue
  • src/renderer/src/components/message/MessageItemAssistant.vue
  • src/renderer/src/components/message/MessageList.vue
  • src/renderer/src/composables/message/useMessageScroll.ts
🧰 Additional context used
📓 Path-based instructions (16)
**/*.{js,ts,tsx,jsx,vue,mjs,cjs}

📄 CodeRabbit inference engine (.cursor/rules/development-setup.mdc)

All logs and comments must be in English

Files:

  • src/renderer/src/components/message/MessageItemAssistant.vue
  • src/renderer/src/components/message/MessageBlockThink.vue
  • src/renderer/src/composables/message/useMessageScroll.ts
  • src/renderer/src/components/message/MessageList.vue
**/*.{js,ts,tsx,jsx,vue,json,mjs,cjs}

📄 CodeRabbit inference engine (.cursor/rules/development-setup.mdc)

Use Prettier as the code formatter

Files:

  • src/renderer/src/components/message/MessageItemAssistant.vue
  • src/renderer/src/components/message/MessageBlockThink.vue
  • src/renderer/src/composables/message/useMessageScroll.ts
  • src/renderer/src/components/message/MessageList.vue
src/renderer/src/**/*.{vue,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/i18n.mdc)

src/renderer/src/**/*.{vue,ts,tsx}: Use vue-i18n framework for internationalization located at src/renderer/src/i18n/
All user-facing strings must use i18n keys, not hardcoded text

src/renderer/src/**/*.{vue,ts,tsx}: Use ref for primitives and references, reactive for objects in Vue 3 Composition API
Prefer computed properties over methods for derived state in Vue components
Import Shadcn Vue components from @/shadcn/components/ui/ path alias
Use the cn() utility function combining clsx and tailwind-merge for dynamic Tailwind classes
Use defineAsyncComponent() for lazy loading heavy Vue components
Use TypeScript for all Vue components and composables with explicit type annotations
Define TypeScript interfaces for Vue component props and data structures
Use usePresenter composable for main process communication instead of direct IPC calls

Files:

  • src/renderer/src/components/message/MessageItemAssistant.vue
  • src/renderer/src/components/message/MessageBlockThink.vue
  • src/renderer/src/composables/message/useMessageScroll.ts
  • src/renderer/src/components/message/MessageList.vue
src/renderer/src/**/*.vue

📄 CodeRabbit inference engine (.cursor/rules/i18n.mdc)

Import useI18n from vue-i18n in Vue components to access translation functions t and locale

src/renderer/src/**/*.vue: Use <script setup> syntax for concise Vue 3 component definitions with Composition API
Define props and emits explicitly in Vue components using defineProps and defineEmits with TypeScript interfaces
Use provide/inject for dependency injection in Vue components instead of prop drilling
Use Tailwind CSS for all styling instead of writing scoped CSS files
Use mobile-first responsive design approach with Tailwind breakpoints
Use Iconify Vue with lucide icons as primary choice, following pattern lucide:{icon-name}
Use v-memo directive for memoizing expensive computations in templates
Use v-once directive for rendering static content without reactivity updates
Use virtual scrolling with RecycleScroller component for rendering long lists
Subscribe to events using rendererEvents.on() and unsubscribe in onUnmounted lifecycle hook

Files:

  • src/renderer/src/components/message/MessageItemAssistant.vue
  • src/renderer/src/components/message/MessageBlockThink.vue
  • src/renderer/src/components/message/MessageList.vue
src/renderer/src/**/*.{ts,tsx,vue}

📄 CodeRabbit inference engine (.cursor/rules/i18n.mdc)

Ensure all code comments are in English and all log messages are in English, with no non-English text in code comments or console statements

Use VueUse composables for common utilities like useLocalStorage, useClipboard, useDebounceFn

Vue 3 renderer app code should be organized in src/renderer/src with subdirectories for components/, stores/, views/, i18n/, and lib/

Files:

  • src/renderer/src/components/message/MessageItemAssistant.vue
  • src/renderer/src/components/message/MessageBlockThink.vue
  • src/renderer/src/composables/message/useMessageScroll.ts
  • src/renderer/src/components/message/MessageList.vue
src/renderer/src/components/**/*.vue

📄 CodeRabbit inference engine (.cursor/rules/vue-stack-guide.mdc)

Name Vue components using PascalCase (e.g., ChatInput.vue, MessageItemUser.vue)

Files:

  • src/renderer/src/components/message/MessageItemAssistant.vue
  • src/renderer/src/components/message/MessageBlockThink.vue
  • src/renderer/src/components/message/MessageList.vue
**/*.vue

📄 CodeRabbit inference engine (AGENTS.md)

Vue components must be named in PascalCase (e.g., ChatInput.vue) and use Vue 3 Composition API with Pinia for state management and Tailwind for styling

**/*.vue: Use Vue 3 Composition API with <script setup> syntax
Use Pinia stores for state management
Style Vue components using Tailwind CSS (v4) with shadcn/ui (reka-ui)

Files:

  • src/renderer/src/components/message/MessageItemAssistant.vue
  • src/renderer/src/components/message/MessageBlockThink.vue
  • src/renderer/src/components/message/MessageList.vue
**/*.{ts,tsx,vue}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx,vue}: Use camelCase for variable and function names; use PascalCase for types and classes; use SCREAMING_SNAKE_CASE for constants
Configure Prettier with single quotes, no semicolons, and line width of 100 characters. Run pnpm run format after completing features

Files:

  • src/renderer/src/components/message/MessageItemAssistant.vue
  • src/renderer/src/components/message/MessageBlockThink.vue
  • src/renderer/src/composables/message/useMessageScroll.ts
  • src/renderer/src/components/message/MessageList.vue
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (CLAUDE.md)

Use English for all logs and comments

Files:

  • src/renderer/src/components/message/MessageItemAssistant.vue
  • src/renderer/src/components/message/MessageBlockThink.vue
  • src/renderer/src/composables/message/useMessageScroll.ts
  • src/renderer/src/components/message/MessageList.vue
src/renderer/**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (CLAUDE.md)

All user-facing strings must use i18n keys (supports 12 languages)

Files:

  • src/renderer/src/components/message/MessageItemAssistant.vue
  • src/renderer/src/components/message/MessageBlockThink.vue
  • src/renderer/src/composables/message/useMessageScroll.ts
  • src/renderer/src/components/message/MessageList.vue
**/*.{js,ts,tsx,jsx,mjs,cjs}

📄 CodeRabbit inference engine (.cursor/rules/development-setup.mdc)

Use OxLint as the linter

Files:

  • src/renderer/src/composables/message/useMessageScroll.ts
src/renderer/src/composables/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/vue-stack-guide.mdc)

Name composables using camelCase with use prefix (e.g., useChatState.ts, useMessageList.ts)

Files:

  • src/renderer/src/composables/message/useMessageScroll.ts
src/renderer/src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.cursor/rules/vue-stack-guide.mdc)

Use class-variance-authority (CVA) for defining component variants with Tailwind classes

Files:

  • src/renderer/src/composables/message/useMessageScroll.ts
src/renderer/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/vue-stack-guide.mdc)

src/renderer/src/**/*.{ts,tsx}: Use shallowRef and shallowReactive for optimizing reactivity with large objects
Prefer type over interface in TypeScript unless using inheritance with extends

Files:

  • src/renderer/src/composables/message/useMessageScroll.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use OxLint for linting JavaScript and TypeScript files; ensure lint-staged hooks and typecheck pass before commits

Enable strict TypeScript type checking

Files:

  • src/renderer/src/composables/message/useMessageScroll.ts
{src/main,src/renderer,test}/**/*.{ts,tsx,js}

📄 CodeRabbit inference engine (CLAUDE.md)

Use IPC communication: Renderer calls main process via usePresenter composable, Main sends to Renderer via EventBus

Files:

  • src/renderer/src/composables/message/useMessageScroll.ts
🧠 Learnings (2)
📚 Learning: 2026-01-05T02:41:31.661Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-stack-guide.mdc:0-0
Timestamp: 2026-01-05T02:41:31.661Z
Learning: Applies to src/renderer/src/**/*.vue : Define props and emits explicitly in Vue components using `defineProps` and `defineEmits` with TypeScript interfaces

Applied to files:

  • src/renderer/src/components/message/MessageBlockThink.vue
📚 Learning: 2026-01-05T02:41:31.661Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-stack-guide.mdc:0-0
Timestamp: 2026-01-05T02:41:31.661Z
Learning: Applies to src/renderer/src/**/*.vue : Use virtual scrolling with `RecycleScroller` component for rendering long lists

Applied to files:

  • src/renderer/src/components/message/MessageList.vue
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-check (x64)
🔇 Additional comments (9)
src/renderer/src/composables/message/useMessageScroll.ts (1)

8-9: LGTM - Retry parameters tuned for dynamic content.

The increased retry count (12) and delay (80ms) extend the maximum scroll retry window from ~400ms to ~960ms. This aligns well with the new resize observer-based height tracking in MessageList.vue, giving more time for dynamic content (like collapsing think blocks) to settle before finalizing scroll position.

src/renderer/src/components/message/MessageItemAssistant.vue (2)

38-38: LGTM - Collapse toggle wiring is correct.

The event binding properly connects MessageBlockThink's collapse toggle to the parent's variant change notification, enabling scroll position updates when think blocks expand/collapse.


298-300: LGTM - Handler correctly triggers scroll update.

The handler emits variantChanged which MessageList uses to refresh the virtual scroller and update scroll position. Ignoring the boolean parameter is fine since only the toggle event matters here.

src/renderer/src/components/message/MessageList.vue (6)

19-23: LGTM - Size dependencies updated for rendering state.

Adding getRenderingStateKey(item) to size-dependencies ensures the virtual scroller re-measures items when loading states change, which is essential for proper scroll behavior during streaming responses.


136-138: LGTM - State variables appropriately declared.

Using plain Map objects for previousHeights and messageResizeObservers is correct since they don't need Vue reactivity. The pendingHeightUpdate ref appropriately gates the debounced update logic.


304-326: LGTM - Rendering state key logic is sound.

The function correctly identifies loading blocks in assistant messages and generates deterministic keys by sorting block types. This ensures proper virtual scroller re-measurement during streaming.


668-670: LGTM - Double-frame update for reliable layout.

The additional requestAnimationFrame + updateVisibleItems call ensures the virtual scroller re-measures items after the DOM has fully settled. This pattern is appropriate for handling dynamic content sizing.


745-753: LGTM - Last message key includes rendering state.

Including getRenderingStateKey in the watch key ensures scroll-to-bottom triggers when message loading states change. The follow-up scrollerUpdate via nextTick properly refreshes the virtual scroller.


766-766: LGTM - Proper cleanup on unmount.

Calling cleanupMessageResizeObservers ensures all ResizeObserver instances are disconnected and memory is released when the component unmounts.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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.

@zerob13 zerob13 merged commit a71c3da into dev Jan 14, 2026
2 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.

2 participants