Skip to content

Conversation

@zhangmo8
Copy link
Collaborator

@zhangmo8 zhangmo8 commented Dec 31, 2025

MessageList replace to DynamicScroller

feat: enhance message handling and rendering

  • Refactor MarkdownRenderer to utilize global KaTeX and Mermaid workers.
  • Introduce MessageItemPlaceholder component for loading states in message lists.
  • Update MessageList to handle message items more efficiently and integrate placeholders.
  • Enhance ThinkContent with deferred rendering options for improved performance.
  • Implement message runtime caching for better message retrieval and management.
  • Modify chat store to utilize cached messages and improve message loading logic.
  • Add prefetching capabilities for messages to enhance user experience.
  • Update ChatTabView to reflect total message count and improve navigation.
  • Adjust playground demo to align with new message handling structure.
  • Extend legacy presenters with new methods for message ID retrieval and batch fetching.

Summary by CodeRabbit

  • New Features

    • Virtualized message list for much smoother scrolling in large conversations
    • Programmatic scrolling to a specific message and improved prefetching for faster message loading
    • Message skeleton placeholder UI and total-message counts in navigation
  • Bug Fixes

    • More reliable scroll/selection highlight behavior with retries and synchronized updates
    • Improved reveal of parent messages when child items are not visible

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

@chatgpt-codex-connector
Copy link

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 31, 2025

📝 Walkthrough

Walkthrough

Replaces the static message list with a virtualized DynamicScroller-based MessageList, adds programmatic scroll-to-message with retries, shifts prop from messages to items, introduces a runtime message DOM cache, and adds batch message ID/message-by-ID APIs across presenter layers.

Changes

Cohort / File(s) Summary
MessageList / Virtualization
src/renderer/src/components/message/MessageList.vue
Replaced static list with DynamicScroller/DynamicScrollerItem; swapped messagesitems prop; added per-item templates (assistant/user/placeholder), size keys, container lifecycle hooks, and virtual update handling. Exposed scrollToMessage, scrollToBottom, scrollToSelectionHighlight, and aboveThreshold.
MessageList UI helper
src/renderer/src/components/message/MessageItemPlaceholder.vue
New placeholder component for virtualized items (prop: messageId: string).
Runtime cache / DOM info
src/renderer/src/lib/messageRuntimeCache.ts
New in-memory runtime cache for messages, thread mapping, and per-message DOM info; APIs to get/cache/delete messages and set/get DOM info; eviction/prune logic.
Store / consumer updates
src/renderer/src/stores/chat.ts, src/renderer/src/components/ChatView.vue, src/renderer/src/views/ChatTabView.vue, src/renderer/src/views/playground/demos/MessageListDemo.vue
Store switched to ID-centric view caches and introduced MessageListItem + messageItems computed. Consumers updated to pass :items="chatStore.messageItems" / messageItems and to use chatStore.getMessageIds() and messageCount. Added prefetching and DOM-info helpers in store.
Scroll/navigation UI
src/renderer/src/components/MessageNavigationSidebar.vue
Added totalMessages: number prop and updated usage for totals.
Presenter / DB batch APIs
src/main/presenter/sessionPresenter/index.ts, src/main/presenter/sessionPresenter/managers/messageManager.ts, src/main/presenter/sqlitePresenter/index.ts, src/main/presenter/sqlitePresenter/tables/messages.ts
Added getMessageIds(conversationId) and getMessagesByIds(messageIds) across presenter/manager/sqlite layers; MessagesTable adds getByIds and queryIds implementations.
Type declarations
src/shared/types/presenters/legacy.presenters.d.ts, src/shared/types/presenters/thread.presenter.d.ts
Added signatures for queryMessageIds, getMessagesByIds, and corresponding presenter/manager interface methods.
Markdown workers
src/renderer/src/components/markdown/MarkdownRenderer.vue, src/renderer/src/main.ts
Removed in-file KaTeX/Mermaid worker setup from MarkdownRenderer; centralized worker creation/teardown in main.ts (global worker cache + beforeunload termination).
ThinkContent rendering tweaks
src/renderer/src/components/think-content/ThinkContent.vue
Set deferNodesUntilVisible=true, maxLiveNodes=120, liveNodeBuffer=30 on NodeRenderer.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant UI as Client UI
participant MsgList as MessageList (vue)
participant Scroller as DynamicScroller
participant Store as ChatStore
participant Cache as MessageRuntimeCache
participant Presenter as SessionPresenter / SQLite
Note over UI, MsgList: Programmatic scroll request (new API)
UI->>MsgList: scrollToMessage(messageId)
MsgList->>Store: getMessageIds() / messageItems (index lookup)
alt id -> index found
MsgList->>Scroller: scrollToItem(index)
Scroller-->>MsgList: itemRendered? (async)
alt itemRendered
MsgList->>Cache: getMessageDomInfo(messageId)
Cache-->>MsgList: dom top/height
MsgList->>MsgList: adjust DOM scroll / highlight
MsgList-->>UI: resolve (scrolled)
else notRendered
loop retry (with delay)
Scroller->>Scroller: request render range
Scroller-->>MsgList: update event
end
MsgList->>Presenter: getMessagesByIds([messageId,...]) (fallback)
Presenter-->>Store: deliver messages
Store-->>MsgList: messageItems updated
MsgList->>MsgList: DOM adjustment fallback (scrollIntoView)
end
else id missing
MsgList->>Presenter: getMessageIds(conversationId)
Presenter-->>Store: IDs delivered
Store-->>MsgList: update, then retry scroll flow
end

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

codex

Poem

🐇 I hop through lists both large and small,
Now only visible pieces I call.
I nudge the scroller, find the right thread,
Retry a few hops till the target's ahead.
Cheery crumbs of DOM and a caching paw — ta‑da! 🎩

Pre-merge checks and finishing touches

✅ 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 accurately describes the main objective: implementing virtual scrolling (via DynamicScroller) for MessageList to improve performance with large message lists.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
  • 📝 Generate docstrings

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

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/components/message/MessageList.vue (1)

449-477: Return value is incorrect after parent message navigation fallback.

The function always returns false at line 476, even when the parent message navigation and subsequent revealHighlight() call in nextTick might succeed. This could cause incorrect behavior in callers that depend on the return value.

🔎 Proposed fix
 const scrollToSelectionHighlight = (childConversationId: string) => {
   if (!childConversationId) return false
   const container = messagesContainer.value
   if (!container) return false
   const revealHighlight = () => {
     const highlight = container.querySelector(
       `.${HIGHLIGHT_CLASS}[data-child-conversation-id="${childConversationId}"]`
     ) as HTMLElement | null
     if (!highlight) return false
     highlight.scrollIntoView({ block: 'center' })
     highlight.classList.add('selection-highlight-active')
     setTimeout(() => {
       highlight.classList.remove('selection-highlight-active')
     }, 2000)
     return true
   }

   if (revealHighlight()) return true

   const parentMessageId = findParentMessageIdForChild(childConversationId)
   if (parentMessageId) {
     scrollToMessage(parentMessageId)
     nextTick(() => {
       revealHighlight()
     })
+    return true // Navigation initiated, highlight reveal attempted
   }

   return false
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0b9bf2f and 0ae4232.

📒 Files selected for processing (1)
  • src/renderer/src/components/message/MessageList.vue
🧰 Additional context used
📓 Path-based instructions (14)
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (CLAUDE.md)

Use English for logs and comments (Chinese text exists in legacy code, but new code should use English)

Files:

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

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.vue: Use Vue 3 Composition API for all components instead of Options API
Use Tailwind CSS with scoped styles for component styling

Files:

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

📄 CodeRabbit inference engine (CLAUDE.md)

src/renderer/**/*.vue: All user-facing strings must use i18n keys via vue-i18n for internationalization
Ensure proper error handling and loading states in all UI components
Implement responsive design using Tailwind CSS utilities for all UI components

src/renderer/**/*.vue: Use composition API and declarative programming patterns; avoid options API
Structure files: exported component, composables, helpers, static content, types
Use PascalCase for component names (e.g., AuthWizard.vue)
Use Vue 3 with TypeScript, leveraging defineComponent and PropType
Use template syntax for declarative rendering
Use Shadcn Vue, Radix Vue, and Tailwind for components and styling
Implement responsive design with Tailwind CSS; use a mobile-first approach
Use Suspense for asynchronous components
Use <script setup> syntax for concise component definitions
Prefer 'lucide:' icon family as the primary choice for Iconify icons
Import Icon component from '@iconify/vue' and use with lucide icons following pattern '{collection}:{icon-name}'

Files:

  • 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}: All user-facing strings must use i18n keys with vue-i18n framework in the renderer
Import and use useI18n() composable with the t() function to access translations in Vue components and TypeScript files
Use the dynamic locale.value property to switch languages at runtime
Avoid hardcoding user-facing text and ensure all user-visible text uses the i18n translation system

Files:

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

📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)

New features should be developed in the src directory

Files:

  • src/renderer/src/components/message/MessageList.vue
src/renderer/**/*.{vue,js,ts}

📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)

Renderer process code should be placed in src/renderer (Vue 3 application)

Files:

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

📄 CodeRabbit inference engine (.cursor/rules/vue-best-practices.mdc)

src/renderer/src/**/*.{vue,ts,tsx,js,jsx}: Use the Composition API for better code organization and reusability in Vue.js applications
Implement proper state management with Pinia in Vue.js applications
Utilize Vue Router for navigation and route management in Vue.js applications
Leverage Vue's built-in reactivity system for efficient data handling

Files:

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

📄 CodeRabbit inference engine (.cursor/rules/vue-best-practices.mdc)

Use scoped styles to prevent CSS conflicts between Vue components

Files:

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

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

src/renderer/**/*.{ts,tsx,vue}: Write concise, technical TypeScript code with accurate examples
Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError)
Avoid enums; use const objects instead
Use arrow functions for methods and computed properties
Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements

Vue 3 app code in src/renderer/src should be organized into components/, stores/, views/, i18n/, lib/ directories with shell UI in src/renderer/shell/

Files:

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

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

Use lowercase with dashes for directories (e.g., components/auth-wizard)

Files:

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

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

src/renderer/**/*.{ts,vue}: Use useFetch and useAsyncData for data fetching
Leverage ref, reactive, and computed for reactive state management
Use provide/inject for dependency injection when appropriate
Use Iconify/Vue for icon implementation

Files:

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

📄 CodeRabbit inference engine (AGENTS.md)

src/renderer/src/**/*.{ts,tsx,vue}: Use TypeScript with Vue 3 Composition API for the renderer application
All user-facing strings must use vue-i18n keys in src/renderer/src/i18n

Files:

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

📄 CodeRabbit inference engine (AGENTS.md)

src/renderer/src/components/**/*.vue: Use Tailwind for styles in Vue components
Vue component files must use PascalCase naming (e.g., ChatInput.vue)

Files:

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

📄 CodeRabbit inference engine (AGENTS.md)

Use Prettier with single quotes, no semicolons, and 100 character width

Files:

  • src/renderer/src/components/message/MessageList.vue
🧠 Learnings (11)
📚 Learning: 2025-11-25T05:27:45.545Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-best-practices.mdc:0-0
Timestamp: 2025-11-25T05:27:45.545Z
Learning: Applies to src/renderer/src/**/*.{vue,ts,tsx,js,jsx} : Use the Composition API for better code organization and reusability in Vue.js applications

Applied to files:

  • src/renderer/src/components/message/MessageList.vue
📚 Learning: 2025-11-25T05:26:43.510Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/i18n.mdc:0-0
Timestamp: 2025-11-25T05:26:43.510Z
Learning: Applies to src/renderer/src/**/*.{vue,ts,tsx} : Avoid hardcoding user-facing text and ensure all user-visible text uses the i18n translation system

Applied to files:

  • src/renderer/src/components/message/MessageList.vue
📚 Learning: 2025-11-25T05:28:04.454Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-shadcn.mdc:0-0
Timestamp: 2025-11-25T05:28:04.454Z
Learning: Applies to src/renderer/**/composables/*.ts : Use VueUse for common composables and utility functions

Applied to files:

  • src/renderer/src/components/message/MessageList.vue
📚 Learning: 2025-11-25T05:28:04.454Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-shadcn.mdc:0-0
Timestamp: 2025-11-25T05:28:04.454Z
Learning: Applies to src/renderer/(components|composables)/**/*.{ts,vue} : Utilize Nuxt's auto-imports feature for components and composables

Applied to files:

  • src/renderer/src/components/message/MessageList.vue
📚 Learning: 2025-11-25T05:28:04.454Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-shadcn.mdc:0-0
Timestamp: 2025-11-25T05:28:04.454Z
Learning: Applies to src/renderer/(components|composables)/**/*.{ts,vue} : Use Nuxt's auto-imports for components and composables

Applied to files:

  • src/renderer/src/components/message/MessageList.vue
📚 Learning: 2025-11-25T05:28:04.454Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-shadcn.mdc:0-0
Timestamp: 2025-11-25T05:28:04.454Z
Learning: Applies to src/renderer/**/*.vue : Use Shadcn Vue, Radix Vue, and Tailwind for components and styling

Applied to files:

  • src/renderer/src/components/message/MessageList.vue
📚 Learning: 2025-11-25T05:28:04.454Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-shadcn.mdc:0-0
Timestamp: 2025-11-25T05:28:04.454Z
Learning: Applies to src/renderer/**/*.vue : Structure files: exported component, composables, helpers, static content, types

Applied to files:

  • src/renderer/src/components/message/MessageList.vue
📚 Learning: 2025-11-25T05:28:04.454Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-shadcn.mdc:0-0
Timestamp: 2025-11-25T05:28:04.454Z
Learning: Applies to src/renderer/**/*.vue : Use Suspense for asynchronous components

Applied to files:

  • src/renderer/src/components/message/MessageList.vue
📚 Learning: 2025-11-25T05:26:43.510Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/i18n.mdc:0-0
Timestamp: 2025-11-25T05:26:43.510Z
Learning: Applies to src/renderer/src/**/*.{vue,ts,tsx} : Import and use useI18n() composable with the t() function to access translations in Vue components and TypeScript files

Applied to files:

  • src/renderer/src/components/message/MessageList.vue
📚 Learning: 2025-11-25T05:28:04.454Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-shadcn.mdc:0-0
Timestamp: 2025-11-25T05:28:04.454Z
Learning: Applies to src/renderer/**/*.vue : Use composition API and declarative programming patterns; avoid options API

Applied to files:

  • src/renderer/src/components/message/MessageList.vue
📚 Learning: 2025-11-25T05:28:04.454Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-shadcn.mdc:0-0
Timestamp: 2025-11-25T05:28:04.454Z
Learning: Applies to src/renderer/(pages|components)/**/*.vue : Implement lazy loading for routes and components

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 (11)
src/renderer/src/components/message/MessageList.vue (11)

3-36: LGTM! Well-structured virtual scroller integration.

The DynamicScroller template correctly implements:

  • Callback ref pattern for capturing both scroller instance and DOM element
  • Proper slot destructuring with item, index, active for DynamicScrollerItem
  • Scroll anchor in the #after slot for bottom detection

100-102: Constants are appropriately defined.

MIN_ITEM_SIZE of 96px is a reasonable minimum estimate for message items in a virtual scroller, and NEAR_BOTTOM_THRESHOLD provides sensible hysteresis for auto-follow behavior.


141-144: Clean callback ref pattern for bridging scroller instance and DOM element.

The setMessageScroller correctly synchronizes both the scroller instance (for programmatic scrolling) and the underlying DOM element (for existing scroll composables).


146-152: Efficient index lookup map for programmatic scrolling.

The computed messageIndexMap provides O(1) lookups when scrolling to specific messages. The O(n) reconstruction cost on message changes is acceptable given the reactive requirements.


185-197: Good auto-follow restoration logic.

The maybeRestoreAutoFollow function provides good UX by automatically re-enabling auto-follow when the user scrolls near the bottom. The threshold check and conditional restoration avoid unnecessary state updates.


388-395: Correct parent-child lookup implementation.

The helper function correctly traverses the thread structure to find the parent message containing a child conversation. The complexity is acceptable for typical conversation thread counts.


503-505: Resize observer correctly connected to virtual scroller container.

The useResizeObserver will properly track the DynamicScroller's root element via messagesContainer, maintaining the auto-scroll behavior on container resize.


117-117: Clear naming distinction between scroll methods.

Renaming to scrollToMessageViaDom clearly distinguishes the DOM-based scroll helper from the new composite scrollToMessage that coordinates virtual scroller and DOM scrolling.


541-547: Public API maintained with improved internal implementation.

The defineExpose preserves the existing public contract while the internal implementation now leverages virtual scrolling. This is a clean encapsulation of the performance optimization.


79-79: No action required — vue-virtual-scroller is properly configured.

The dependency is installed ("vue-virtual-scroller": "^2.0.0-beta.8" in package.json) and the CSS is correctly imported in src/renderer/src/main.ts via import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'. The import statement in MessageList.vue is valid.


215-226: The nested nextTick pattern is the standard approach for vue-virtual-scroller and is correct.

The code follows the official vue-virtual-scroller and Vue recommendations: scrollToItem() does not provide a callback API, so using nextTick() after calling it is the documented way to ensure the item renders before performing subsequent DOM operations. The double-scroll approach (virtual scroller positioning followed by DOM-based scrollIntoView) is intentional and working as designed.

Likely an incorrect or invalid review comment.

@zerob13
Copy link
Collaborator

zerob13 commented Dec 31, 2025

This change will cause multiple long conversations to fail to automatically scroll to the bottom when switching between them.

@zerob13
Copy link
Collaborator

zerob13 commented Dec 31, 2025

output_small

@zhangmo8 zhangmo8 closed this Jan 4, 2026
@zhangmo8 zhangmo8 force-pushed the messageList-DynamicScroller branch from 0ae4232 to 84940b2 Compare January 4, 2026 07:47
- Refactor MarkdownRenderer to utilize global KaTeX and Mermaid workers.
- Introduce MessageItemPlaceholder component for loading states in message lists.
- Update MessageList to handle message items more efficiently and integrate placeholders.
- Enhance ThinkContent with deferred rendering options for improved performance.
- Implement message runtime caching for better message retrieval and management.
- Modify chat store to utilize cached messages and improve message loading logic.
- Add prefetching capabilities for messages to enhance user experience.
- Update ChatTabView to reflect total message count and improve navigation.
- Adjust playground demo to align with new message handling structure.
- Extend legacy presenters with new methods for message ID retrieval and batch fetching.
@zhangmo8 zhangmo8 reopened this Jan 5, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Fix all issues with AI Agents 🤖
In @src/renderer/src/components/message/MessageList.vue:
- Around line 574-612: The race is that scrollToMessage calls
chatStore.ensureMessagesLoadedByIds but does not await it before starting the
retry loop (attemptScroll), so retries can exhaust before the message is loaded;
fix by awaiting ensureMessagesLoadedByIds (or awaiting its promise resolution)
before initializing pendingScrollTargetId/scrollRetryToken and calling
attemptScroll, or alternatively defer starting the retry loop until
ensureMessagesLoadedByIds resolves (e.g., call
ensureMessagesLoadedByIds(...).then(() => { /* set pendingScrollTargetId,
increment scrollRetryToken and start attemptScroll */ })), keeping the existing
retry/cleanup logic (scrollRetryTimer, scrollRetryToken,
tryScrollToRenderedMessage, scroller.scrollToItem) intact.

In @src/renderer/src/lib/messageRuntimeCache.ts:
- Around line 20-28: prune() currently assumes all three maps (messageCache,
messageThreadMap, messageDomInfo) stay perfectly synchronized and uses oldestId
without validating it; change it to iterate deterministically and validate keys:
while any of the three map sizes exceed MAX_CACHE_ENTRIES (check
messageCache.size, messageThreadMap.size, messageDomInfo.size), obtain a
candidate id from the same deterministic source (e.g., first key of
messageCache), verify typeof oldestId === 'string' and that it exists in each
map before deleting, and if it is missing or invalid, skip or fall back to a
union-of-keys strategy to pick a valid id so the loop reliably reduces all map
sizes to ≤ MAX_CACHE_ENTRIES; apply these checks inside prune() referencing
messageCache, messageThreadMap, messageDomInfo, MAX_CACHE_ENTRIES, and oldestId.

In @src/renderer/src/stores/chat.ts:
- Around line 266-282: Remove the unreachable guard and prevent MessageList from
seeing null last messages: delete the `if (cacheVersion < 0) return []` check in
the `messageItems` computed and instead ensure `loadMessages` waits for the
final message to be cached before UI scroll occurs; specifically, in
`loadMessages` (the function that calls `getMessageIds` and prefetches the first
50), after fetching IDs call whatever cache-fill/prefetch function (or
`getCachedMessage` wrapper) for the last message ID and await it (or
synchronously load it) before resolving so `messageItems` (which uses
`getMessageIds`, `getCachedMessage`, `resolveVariantMessage` and
`selectedVariantsMap`) will not produce `message: null` for the last item and
MessageList.vue can safely scroll to bottom.
- Around line 1110-1121: The tautological checks using if (getActiveThreadId()
=== getActiveThreadId()) should be replaced to compare the active thread against
the message's thread id; change both occurrences to if (getActiveThreadId() ===
cached.threadId) (or the actual message/thread variable in scope) so that the
blocks calling cacheMessageForView(enrichedMainMessage/enrichedMessage) and
ensureMessageId(...) only run when the message belongs to the active thread;
verify the variable name for the cached message (e.g., cached.threadId) is in
scope and adjust accordingly.

Caution

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

⚠️ Outside diff range comments (2)
src/renderer/src/components/ChatView.vue (1)

91-93: Fix scroll-to-bottom behavior with DynamicScroller virtual rendering.

The scroll implementation assumes container.scrollHeight represents the total content height, but DynamicScroller only renders visible messages. This causes scrollToBottom() to calculate an incorrect scroll position, especially when switching to conversations with 100+ virtualized messages.

The fallback path in scrollToBottomImmediate() (lines 91-93, 99, 107) uses DOM measurements that are incompatible with virtual scrolling:

const targetTop = Math.max(container.scrollHeight - container.clientHeight, 0)
container.scrollTop = targetTop

When container.scrollHeight reflects only rendered items (not the full virtual list), the scroll position is wrong. Additionally, the check for scroller.scrollToBottom() relies on a method that likely doesn't exist in DynamicScroller's API.

Required verification:

  1. Confirm DynamicScroller from vue-virtual-scroller@2.0.0-beta.8 lacks a public scrollToBottom() method or requires a different API
  2. Implement a virtual-scroll-aware approach—either use DynamicScroller's intended scroll API or restore scroll position at the component boundary (e.g., via key remounting)
  3. Test scenario: Switch between 3 conversations with 100+ messages each and confirm auto-scroll to bottom works
src/renderer/src/stores/chat.ts (1)

621-642: Root cause of reported scroll-to-bottom bug when switching conversations.

The loadMessages function (Line 635) only prefetches the first 50 messages. When switching to a long conversation:

  1. Message IDs are set immediately
  2. Only the first 50 messages are prefetched
  3. MessageList.vue attempts to scroll to bottom
  4. The DynamicScroller doesn't have accurate heights for unloaded messages at the end
  5. Scroll position is incorrect

This matches the bug report: "multiple long conversations fail to automatically scroll to the bottom when switching between them."

🔎 Proposed fix

Prefetch both the beginning and end of the message list to ensure scroll-to-bottom works correctly:

 const loadMessages = async () => {
   if (!getActiveThreadId()) return

   try {
     childThreadsByMessageId.value = new Map()
     const messageIds = (await threadP.getMessageIds(getActiveThreadId()!)) || []
     setMessageIds(Array.isArray(messageIds) ? messageIds : [])
     const activeThread = getActiveThreadId()
     for (const [, cached] of getGeneratingMessagesCache()) {
       if (cached.threadId === activeThread) {
         cacheMessageForView(await enrichMessageWithExtra(cached.message))
         ensureMessageId(cached.message.id)
       }
     }
-    await prefetchMessagesForRange(0, Math.min(messageIds.length - 1, 50))
+    // Prefetch beginning for initial render and end for scroll-to-bottom
+    const prefetchStart = prefetchMessagesForRange(0, Math.min(49, messageIds.length - 1))
+    const prefetchEnd = messageIds.length > 50
+      ? prefetchMessagesForRange(Math.max(0, messageIds.length - 30), messageIds.length - 1)
+      : Promise.resolve()
+    await Promise.all([prefetchStart, prefetchEnd])
     await refreshChildThreadsForActiveThread()
     await maybeQueueContextMention()
   } catch (error) {
     console.error('Failed to load messages:', error)
     throw error
   }
 }
🧹 Nitpick comments (7)
src/renderer/src/components/think-content/ThinkContent.vue (1)

35-37: API props are valid; consider documenting the virtualization buffer values.

The deferNodesUntilVisible, maxLiveNodes, and liveNodeBuffer props are supported by markstream-vue and properly configured for virtualization of thinking content.

The values 120 (maxLiveNodes) and 30 (liveNodeBuffer) are reasonable for balancing memory and scroll performance. However, consider extracting these as named constants with explanatory comments:

const MAX_THINKING_NODES = 120 // Maximum nodes rendered in DOM virtualization window
const THINKING_NODE_BUFFER = 30 // Buffer nodes ahead/behind viewport to prevent pop-in

This improves maintainability if these values need future tuning based on performance metrics.

src/renderer/src/main.ts (1)

25-38: Consider adding error handling and HMR cleanup for worker initialization.

The worker initialization logic lacks error handling and may leave orphaned workers during development hot-reloads. If worker instantiation fails or if the module is reloaded during HMR, the global cache could retain stale references.

🔎 Recommended improvements
+const cleanupWorkers = () => {
+  const workers = globalScope.__markdownWorkers
+  if (workers) {
+    workers.katex.terminate()
+    workers.mermaid.terminate()
+    globalScope.__markdownWorkers = undefined
+  }
+  clearKaTeXWorker()
+  clearMermaidWorker()
+  terminateWorker()
+}
+
+// Clean up on HMR
+if (import.meta.hot) {
+  import.meta.hot.dispose(() => {
+    cleanupWorkers()
+  })
+}
+
 if (!globalScope.__markdownWorkers) {
-  const katex = new KatexWorker()
-  const mermaid = new MermaidWorker()
-  globalScope.__markdownWorkers = { katex, mermaid }
-  setKaTeXWorker(katex)
-  setMermaidWorker(mermaid)
+  try {
+    const katex = new KatexWorker()
+    const mermaid = new MermaidWorker()
+    globalScope.__markdownWorkers = { katex, mermaid }
+    setKaTeXWorker(katex)
+    setMermaidWorker(mermaid)
+  } catch (error) {
+    console.error('Failed to initialize markdown workers:', error)
+  }
 }
 
 window.addEventListener('beforeunload', () => {
-  const workers = globalScope.__markdownWorkers
-  if (workers) {
-    workers.katex.terminate()
-    workers.mermaid.terminate()
-    globalScope.__markdownWorkers = undefined
-  }
-  clearKaTeXWorker()
-  clearMermaidWorker()
-  terminateWorker()
+  cleanupWorkers()
 })
src/renderer/src/lib/messageRuntimeCache.ts (3)

14-18: Simplify the touch implementation to avoid redundant operations.

The current touch implementation deletes and re-inserts the same key-value pair to move it to the end of the Map (LRU behavior). However, this operation is called immediately after messageCache.set() in cacheMessage (line 42), which means the entry is already at the end. This makes the touch call redundant in that context.

🔎 Suggested refactor
 const touch = (messageId: string, message: Message) => {
-  if (!messageCache.has(messageId)) return
-  messageCache.delete(messageId)
-  messageCache.set(messageId, message)
+  if (messageCache.has(messageId)) {
+    messageCache.delete(messageId)
+    messageCache.set(messageId, message)
+  }
 }
 
 export const cacheMessage = (message: Message) => {
   messageCache.set(message.id, message)
   messageThreadMap.set(message.id, message.conversationId)
-  touch(message.id, message)
+  // No need to touch here - set() already places it at the end
   prune()
 }

8-8: Document the rationale for MAX_CACHE_ENTRIES = 800.

The cache size limit of 800 entries appears arbitrary. For context, this value directly impacts memory usage and cache effectiveness. Consider documenting why 800 was chosen or making it configurable based on available memory or conversation size.


41-46: cacheMessage updates three maps without atomicity guarantees.

If any operation in cacheMessage throws an error (unlikely but possible), the three maps could become inconsistent. While JavaScript is single-threaded and errors in Map operations are rare, consider whether error boundaries or validation should be added for robustness.

src/renderer/src/components/message/MessageItemPlaceholder.vue (1)

1-12: Consider adding accessibility attributes for screen readers.

The placeholder component lacks ARIA attributes that would help screen reader users understand that content is loading. While this is a visual placeholder, adding semantic loading indicators improves accessibility.

🔎 Suggested accessibility improvements
 <template>
-  <div :data-message-id="messageId" class="px-4 py-3">
+  <div 
+    :data-message-id="messageId" 
+    class="px-4 py-3"
+    role="status"
+    aria-busy="true"
+    aria-label="Loading message"
+  >
     <div class="flex items-center gap-2 mb-2">
       <div class="h-4 w-4 rounded-sm bg-muted/60 animate-pulse" />
       <div class="h-3 w-24 rounded bg-muted/40 animate-pulse" />
     </div>
     <div class="space-y-2">
       <div class="h-3 w-full rounded bg-muted/40 animate-pulse" />
       <div class="h-3 w-5/6 rounded bg-muted/40 animate-pulse" />
     </div>
   </div>
 </template>
src/main/presenter/sessionPresenter/managers/messageManager.ts (1)

171-190: N+1 query pattern for variant fetching.

For each assistant message, this method makes a separate getMessageVariants call (Line 181). With many assistant messages, this creates N+1 database queries.

Consider batching variant fetches by collecting all parent_id values first, then fetching all variants in one query, and distributing them to their respective messages.

🔎 Proposed optimization
 async getMessagesByIds(messageIds: string[]): Promise<Message[]> {
   if (messageIds.length === 0) return []
   const sqliteMessages = await this.sqlitePresenter.getMessagesByIds(messageIds)
   const sqliteById = new Map(sqliteMessages.map((msg) => [msg.id, msg]))
   const result: Message[] = []

+  // Batch fetch all variants for assistant messages
+  const parentIds = sqliteMessages
+    .filter((msg) => msg.role === 'assistant' && msg.parent_id)
+    .map((msg) => msg.parent_id!)
+  
+  const allVariants = parentIds.length > 0 
+    ? await this.sqlitePresenter.getMessageVariantsByParentIds(parentIds)
+    : []
+  const variantsByParentId = new Map<string, SQLITE_MESSAGE[]>()
+  for (const variant of allVariants) {
+    const existing = variantsByParentId.get(variant.parent_id!) ?? []
+    existing.push(variant)
+    variantsByParentId.set(variant.parent_id!, existing)
+  }

   for (const messageId of messageIds) {
     const sqliteMessage = sqliteById.get(messageId)
     if (!sqliteMessage) continue
     if (sqliteMessage.role === 'assistant' && sqliteMessage.parent_id) {
-      const variants = await this.sqlitePresenter.getMessageVariants(sqliteMessage.parent_id)
+      const variants = variantsByParentId.get(sqliteMessage.parent_id) ?? []
       if (variants.length > 0) {
         sqliteMessage.variants = variants
       }
     }
     result.push(this.convertToMessage(sqliteMessage))
   }

   return result
 }

This would require adding a getMessageVariantsByParentIds method to the SQLite presenter.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0ae4232 and 7a1d332.

📒 Files selected for processing (17)
  • src/main/presenter/sessionPresenter/index.ts
  • src/main/presenter/sessionPresenter/managers/messageManager.ts
  • src/main/presenter/sqlitePresenter/index.ts
  • src/main/presenter/sqlitePresenter/tables/messages.ts
  • src/renderer/src/components/ChatView.vue
  • src/renderer/src/components/MessageNavigationSidebar.vue
  • src/renderer/src/components/markdown/MarkdownRenderer.vue
  • src/renderer/src/components/message/MessageItemPlaceholder.vue
  • src/renderer/src/components/message/MessageList.vue
  • src/renderer/src/components/think-content/ThinkContent.vue
  • src/renderer/src/lib/messageRuntimeCache.ts
  • src/renderer/src/main.ts
  • src/renderer/src/stores/chat.ts
  • src/renderer/src/views/ChatTabView.vue
  • src/renderer/src/views/playground/demos/MessageListDemo.vue
  • src/shared/types/presenters/legacy.presenters.d.ts
  • src/shared/types/presenters/thread.presenter.d.ts
💤 Files with no reviewable changes (1)
  • src/renderer/src/components/markdown/MarkdownRenderer.vue
🧰 Additional context used
📓 Path-based instructions (19)
src/renderer/**/*.vue

📄 CodeRabbit inference engine (CLAUDE.md)

src/renderer/**/*.vue: Use Vue 3 Composition API for all components
Use Tailwind CSS for styling with scoped styles
All user-facing strings must use i18n keys via vue-i18n

Files:

  • src/renderer/src/components/MessageNavigationSidebar.vue
  • src/renderer/src/views/playground/demos/MessageListDemo.vue
  • src/renderer/src/components/message/MessageItemPlaceholder.vue
  • src/renderer/src/components/think-content/ThinkContent.vue
  • src/renderer/src/views/ChatTabView.vue
  • src/renderer/src/components/ChatView.vue
  • src/renderer/src/components/message/MessageList.vue
src/renderer/src/**/*.{ts,tsx,vue}

📄 CodeRabbit inference engine (CLAUDE.md)

Use usePresenter.ts composable for renderer-to-main IPC communication via direct presenter method calls

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/MessageNavigationSidebar.vue
  • src/renderer/src/views/playground/demos/MessageListDemo.vue
  • src/renderer/src/main.ts
  • src/renderer/src/components/message/MessageItemPlaceholder.vue
  • src/renderer/src/components/think-content/ThinkContent.vue
  • src/renderer/src/views/ChatTabView.vue
  • src/renderer/src/components/ChatView.vue
  • src/renderer/src/lib/messageRuntimeCache.ts
  • src/renderer/src/components/message/MessageList.vue
  • src/renderer/src/stores/chat.ts
**/*.{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/MessageNavigationSidebar.vue
  • src/renderer/src/views/playground/demos/MessageListDemo.vue
  • src/main/presenter/sqlitePresenter/index.ts
  • src/shared/types/presenters/thread.presenter.d.ts
  • src/renderer/src/main.ts
  • src/main/presenter/sqlitePresenter/tables/messages.ts
  • src/renderer/src/components/message/MessageItemPlaceholder.vue
  • src/renderer/src/components/think-content/ThinkContent.vue
  • src/shared/types/presenters/legacy.presenters.d.ts
  • src/renderer/src/views/ChatTabView.vue
  • src/main/presenter/sessionPresenter/index.ts
  • src/renderer/src/components/ChatView.vue
  • src/renderer/src/lib/messageRuntimeCache.ts
  • src/main/presenter/sessionPresenter/managers/messageManager.ts
  • src/renderer/src/components/message/MessageList.vue
  • src/renderer/src/stores/chat.ts
**/*.{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/MessageNavigationSidebar.vue
  • src/renderer/src/views/playground/demos/MessageListDemo.vue
  • src/main/presenter/sqlitePresenter/index.ts
  • src/shared/types/presenters/thread.presenter.d.ts
  • src/renderer/src/main.ts
  • src/main/presenter/sqlitePresenter/tables/messages.ts
  • src/renderer/src/components/message/MessageItemPlaceholder.vue
  • src/renderer/src/components/think-content/ThinkContent.vue
  • src/shared/types/presenters/legacy.presenters.d.ts
  • src/renderer/src/views/ChatTabView.vue
  • src/main/presenter/sessionPresenter/index.ts
  • src/renderer/src/components/ChatView.vue
  • src/renderer/src/lib/messageRuntimeCache.ts
  • src/main/presenter/sessionPresenter/managers/messageManager.ts
  • src/renderer/src/components/message/MessageList.vue
  • src/renderer/src/stores/chat.ts
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/MessageNavigationSidebar.vue
  • src/renderer/src/views/playground/demos/MessageListDemo.vue
  • src/renderer/src/main.ts
  • src/renderer/src/components/message/MessageItemPlaceholder.vue
  • src/renderer/src/components/think-content/ThinkContent.vue
  • src/renderer/src/views/ChatTabView.vue
  • src/renderer/src/components/ChatView.vue
  • src/renderer/src/lib/messageRuntimeCache.ts
  • src/renderer/src/components/message/MessageList.vue
  • src/renderer/src/stores/chat.ts
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/MessageNavigationSidebar.vue
  • src/renderer/src/views/playground/demos/MessageListDemo.vue
  • src/renderer/src/components/message/MessageItemPlaceholder.vue
  • src/renderer/src/components/think-content/ThinkContent.vue
  • src/renderer/src/views/ChatTabView.vue
  • src/renderer/src/components/ChatView.vue
  • 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/MessageNavigationSidebar.vue
  • src/renderer/src/components/message/MessageItemPlaceholder.vue
  • src/renderer/src/components/think-content/ThinkContent.vue
  • src/renderer/src/components/ChatView.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

Files:

  • src/renderer/src/components/MessageNavigationSidebar.vue
  • src/renderer/src/views/playground/demos/MessageListDemo.vue
  • src/renderer/src/components/message/MessageItemPlaceholder.vue
  • src/renderer/src/components/think-content/ThinkContent.vue
  • src/renderer/src/views/ChatTabView.vue
  • src/renderer/src/components/ChatView.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/MessageNavigationSidebar.vue
  • src/renderer/src/views/playground/demos/MessageListDemo.vue
  • src/main/presenter/sqlitePresenter/index.ts
  • src/shared/types/presenters/thread.presenter.d.ts
  • src/renderer/src/main.ts
  • src/main/presenter/sqlitePresenter/tables/messages.ts
  • src/renderer/src/components/message/MessageItemPlaceholder.vue
  • src/renderer/src/components/think-content/ThinkContent.vue
  • src/shared/types/presenters/legacy.presenters.d.ts
  • src/renderer/src/views/ChatTabView.vue
  • src/main/presenter/sessionPresenter/index.ts
  • src/renderer/src/components/ChatView.vue
  • src/renderer/src/lib/messageRuntimeCache.ts
  • src/main/presenter/sessionPresenter/managers/messageManager.ts
  • src/renderer/src/components/message/MessageList.vue
  • src/renderer/src/stores/chat.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use English for logs and comments in TypeScript/JavaScript code

Files:

  • src/main/presenter/sqlitePresenter/index.ts
  • src/shared/types/presenters/thread.presenter.d.ts
  • src/renderer/src/main.ts
  • src/main/presenter/sqlitePresenter/tables/messages.ts
  • src/shared/types/presenters/legacy.presenters.d.ts
  • src/main/presenter/sessionPresenter/index.ts
  • src/renderer/src/lib/messageRuntimeCache.ts
  • src/main/presenter/sessionPresenter/managers/messageManager.ts
  • src/renderer/src/stores/chat.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use TypeScript with strict type checking enabled

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

Files:

  • src/main/presenter/sqlitePresenter/index.ts
  • src/shared/types/presenters/thread.presenter.d.ts
  • src/renderer/src/main.ts
  • src/main/presenter/sqlitePresenter/tables/messages.ts
  • src/shared/types/presenters/legacy.presenters.d.ts
  • src/main/presenter/sessionPresenter/index.ts
  • src/renderer/src/lib/messageRuntimeCache.ts
  • src/main/presenter/sessionPresenter/managers/messageManager.ts
  • src/renderer/src/stores/chat.ts
src/main/presenter/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

src/main/presenter/**/*.ts: Use EventBus to broadcast events from main to renderer via mainWindow.webContents.send()
Implement one presenter per functional domain in the main process

Files:

  • src/main/presenter/sqlitePresenter/index.ts
  • src/main/presenter/sqlitePresenter/tables/messages.ts
  • src/main/presenter/sessionPresenter/index.ts
  • src/main/presenter/sessionPresenter/managers/messageManager.ts
src/main/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

src/main/**/*.ts: Use EventBus from src/main/eventbus.ts for decoupled inter-process communication
Context isolation must be enabled with preload scripts for secure IPC communication

Electron main process code should reside in src/main/, with presenters organized in presenter/ subdirectory (Window, Tab, Thread, Mcp, Config, LLMProvider), and app events managed via eventbus.ts

Files:

  • src/main/presenter/sqlitePresenter/index.ts
  • src/main/presenter/sqlitePresenter/tables/messages.ts
  • src/main/presenter/sessionPresenter/index.ts
  • src/main/presenter/sessionPresenter/managers/messageManager.ts
**/*.{js,ts,tsx,jsx,mjs,cjs}

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

Use OxLint as the linter

Files:

  • src/main/presenter/sqlitePresenter/index.ts
  • src/shared/types/presenters/thread.presenter.d.ts
  • src/renderer/src/main.ts
  • src/main/presenter/sqlitePresenter/tables/messages.ts
  • src/shared/types/presenters/legacy.presenters.d.ts
  • src/main/presenter/sessionPresenter/index.ts
  • src/renderer/src/lib/messageRuntimeCache.ts
  • src/main/presenter/sessionPresenter/managers/messageManager.ts
  • src/renderer/src/stores/chat.ts
src/shared/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

src/shared/**/*.ts: Shared types between main and renderer processes must be placed in src/shared/
IPC contract definitions must be placed in src/shared/

Shared TypeScript types and utilities should be placed in src/shared/

Files:

  • src/shared/types/presenters/thread.presenter.d.ts
  • src/shared/types/presenters/legacy.presenters.d.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/main.ts
  • src/renderer/src/lib/messageRuntimeCache.ts
  • src/renderer/src/stores/chat.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/main.ts
  • src/renderer/src/lib/messageRuntimeCache.ts
  • src/renderer/src/stores/chat.ts
src/renderer/src/**/stores/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use Pinia for frontend state management

Files:

  • src/renderer/src/stores/chat.ts
src/renderer/src/stores/**/*.ts

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

src/renderer/src/stores/**/*.ts: Use Setup Store syntax with defineStore function pattern in Pinia stores
Use getters (computed properties) for derived state in Pinia stores
Keep Pinia store actions focused on state mutations and async operations

Files:

  • src/renderer/src/stores/chat.ts
🧠 Learnings (21)
📚 Learning: 2026-01-05T02:41:31.619Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-stack-guide.mdc:0-0
Timestamp: 2026-01-05T02:41:31.619Z
Learning: Applies to src/renderer/src/**/*.{vue,ts,tsx} : Define TypeScript interfaces for Vue component props and data structures

Applied to files:

  • src/renderer/src/views/playground/demos/MessageListDemo.vue
  • src/renderer/src/components/message/MessageItemPlaceholder.vue
  • src/renderer/src/components/message/MessageList.vue
📚 Learning: 2026-01-05T02:41:31.619Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-stack-guide.mdc:0-0
Timestamp: 2026-01-05T02:41:31.619Z
Learning: Applies to src/renderer/src/components/**/*.vue : Name Vue components using PascalCase (e.g., `ChatInput.vue`, `MessageItemUser.vue`)

Applied to files:

  • src/renderer/src/views/playground/demos/MessageListDemo.vue
  • src/renderer/src/components/message/MessageItemPlaceholder.vue
  • src/renderer/src/components/ChatView.vue
  • src/renderer/src/components/message/MessageList.vue
📚 Learning: 2026-01-05T02:41:31.619Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-stack-guide.mdc:0-0
Timestamp: 2026-01-05T02:41:31.619Z
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/views/playground/demos/MessageListDemo.vue
  • src/renderer/src/components/message/MessageItemPlaceholder.vue
  • src/renderer/src/components/think-content/ThinkContent.vue
  • src/renderer/src/components/ChatView.vue
  • src/renderer/src/components/message/MessageList.vue
📚 Learning: 2026-01-05T02:41:31.619Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-stack-guide.mdc:0-0
Timestamp: 2026-01-05T02:41:31.619Z
Learning: Applies to src/renderer/src/**/*.{vue,ts,tsx} : Prefer `computed` properties over methods for derived state in Vue components

Applied to files:

  • src/renderer/src/views/playground/demos/MessageListDemo.vue
📚 Learning: 2026-01-05T02:41:31.619Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-stack-guide.mdc:0-0
Timestamp: 2026-01-05T02:41:31.619Z
Learning: Applies to src/renderer/src/**/*.{vue,ts,tsx} : Use `ref` for primitives and references, `reactive` for objects in Vue 3 Composition API

Applied to files:

  • src/renderer/src/views/playground/demos/MessageListDemo.vue
  • src/renderer/src/components/message/MessageList.vue
📚 Learning: 2026-01-05T02:41:31.619Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-stack-guide.mdc:0-0
Timestamp: 2026-01-05T02:41:31.619Z
Learning: Applies to src/renderer/src/**/*.vue : Use `provide`/`inject` for dependency injection in Vue components instead of prop drilling

Applied to files:

  • src/renderer/src/views/playground/demos/MessageListDemo.vue
📚 Learning: 2026-01-05T02:40:52.831Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-05T02:40:52.831Z
Learning: Applies to src/renderer/**/*.vue : All user-facing strings must use i18n keys via vue-i18n

Applied to files:

  • src/renderer/src/views/playground/demos/MessageListDemo.vue
📚 Learning: 2026-01-05T02:41:13.293Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/i18n.mdc:0-0
Timestamp: 2026-01-05T02:41:13.293Z
Learning: Applies to src/renderer/src/**/*.{vue,ts,tsx} : Use vue-i18n framework for internationalization located at src/renderer/src/i18n/

Applied to files:

  • src/renderer/src/views/playground/demos/MessageListDemo.vue
  • src/renderer/src/main.ts
  • src/renderer/src/components/message/MessageList.vue
📚 Learning: 2026-01-05T02:41:31.619Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-stack-guide.mdc:0-0
Timestamp: 2026-01-05T02:41:31.619Z
Learning: Applies to src/renderer/src/components/ : Organize Vue component directories using lowercase with dashes (e.g., `chat-input/`, `message/`)

Applied to files:

  • src/renderer/src/views/playground/demos/MessageListDemo.vue
  • src/renderer/src/components/ChatView.vue
📚 Learning: 2026-01-05T02:41:13.293Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/i18n.mdc:0-0
Timestamp: 2026-01-05T02:41:13.293Z
Learning: Applies to src/renderer/src/**/*.{vue,ts,tsx} : All user-facing strings must use i18n keys, not hardcoded text

Applied to files:

  • src/renderer/src/views/playground/demos/MessageListDemo.vue
📚 Learning: 2025-08-28T08:07:05.182Z
Learnt from: neoragex2002
Repo: ThinkInAIXYZ/deepchat PR: 807
File: src/renderer/src/components/markdown/MarkdownRenderer.vue:58-58
Timestamp: 2025-08-28T08:07:05.182Z
Learning: In src/renderer/src/components/markdown/MarkdownRenderer.vue, the unscoped <style> tag is intentionally used for global prose styles and the anchor .markdown-renderer fix, as confirmed by user neoragex2002.

Applied to files:

  • src/renderer/src/main.ts
📚 Learning: 2026-01-05T02:41:13.293Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/i18n.mdc:0-0
Timestamp: 2026-01-05T02:41:13.293Z
Learning: Applies to src/renderer/src/**/*.vue : Import useI18n from vue-i18n in Vue components to access translation functions t and locale

Applied to files:

  • src/renderer/src/main.ts
📚 Learning: 2026-01-05T02:40:52.831Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-05T02:40:52.831Z
Learning: Applies to src/renderer/src/**/*.{ts,tsx,vue} : Use `usePresenter.ts` composable for renderer-to-main IPC communication via direct presenter method calls

Applied to files:

  • src/main/presenter/sessionPresenter/index.ts
📚 Learning: 2026-01-05T02:40:52.831Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-05T02:40:52.831Z
Learning: Applies to src/main/presenter/**/*.ts : Implement one presenter per functional domain in the main process

Applied to files:

  • src/main/presenter/sessionPresenter/index.ts
📚 Learning: 2026-01-05T02:41:31.619Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-stack-guide.mdc:0-0
Timestamp: 2026-01-05T02:41:31.619Z
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
📚 Learning: 2026-01-05T02:41:31.619Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-stack-guide.mdc:0-0
Timestamp: 2026-01-05T02:41:31.619Z
Learning: Applies to src/renderer/src/**/*.vue : Use `<script setup>` syntax for concise Vue 3 component definitions with Composition API

Applied to files:

  • src/renderer/src/components/message/MessageList.vue
📚 Learning: 2026-01-05T02:41:31.619Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-stack-guide.mdc:0-0
Timestamp: 2026-01-05T02:41:31.619Z
Learning: Applies to src/renderer/src/**/*.{ts,tsx,vue} : Use VueUse composables for common utilities like `useLocalStorage`, `useClipboard`, `useDebounceFn`

Applied to files:

  • src/renderer/src/components/message/MessageList.vue
📚 Learning: 2026-01-05T02:41:31.619Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-stack-guide.mdc:0-0
Timestamp: 2026-01-05T02:41:31.619Z
Learning: Applies to src/renderer/src/**/*.{vue,ts,tsx} : Use `defineAsyncComponent()` for lazy loading heavy Vue components

Applied to files:

  • src/renderer/src/components/message/MessageList.vue
📚 Learning: 2026-01-05T02:41:31.619Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-stack-guide.mdc:0-0
Timestamp: 2026-01-05T02:41:31.619Z
Learning: Applies to src/renderer/src/**/*.{vue,ts,tsx} : Use TypeScript for all Vue components and composables with explicit type annotations

Applied to files:

  • src/renderer/src/components/message/MessageList.vue
📚 Learning: 2026-01-05T02:41:31.619Z
Learnt from: CR
Repo: ThinkInAIXYZ/deepchat PR: 0
File: .cursor/rules/vue-stack-guide.mdc:0-0
Timestamp: 2026-01-05T02:41:31.619Z
Learning: Applies to src/renderer/src/**/*.{vue,ts,tsx} : Use `usePresenter` composable for main process communication instead of direct IPC calls

Applied to files:

  • src/renderer/src/components/message/MessageList.vue
📚 Learning: 2025-06-21T15:49:17.044Z
Learnt from: neoragex2002
Repo: ThinkInAIXYZ/deepchat PR: 550
File: src/renderer/src/stores/chat.ts:1011-1035
Timestamp: 2025-06-21T15:49:17.044Z
Learning: In src/renderer/src/stores/chat.ts, the user prefers to keep both `text` and `content` properties in the `handleMeetingInstruction` function's `sendMessage` call, even though they are redundant, rather than removing the `content` property.

Applied to files:

  • src/renderer/src/stores/chat.ts
🧬 Code graph analysis (4)
src/shared/types/presenters/thread.presenter.d.ts (2)
src/shared/types/core/chat.ts (1)
  • Message (3-10)
src/shared/chat.d.ts (1)
  • Message (3-32)
src/main/presenter/sqlitePresenter/tables/messages.ts (1)
src/shared/types/presenters/legacy.presenters.d.ts (1)
  • SQLITE_MESSAGE (23-37)
src/renderer/src/lib/messageRuntimeCache.ts (1)
src/main/presenter/workspacePresenter/fileCache.ts (1)
  • prune (68-76)
src/renderer/src/stores/chat.ts (4)
src/shared/chat.d.ts (3)
  • Message (3-32)
  • AssistantMessage (39-42)
  • UserMessage (34-37)
src/main/presenter/sessionPresenter/index.ts (1)
  • getMessageIds (470-472)
src/main/presenter/sessionPresenter/managers/messageManager.ts (1)
  • getMessageIds (234-236)
src/renderer/src/lib/messageRuntimeCache.ts (9)
  • getCachedMessage (30-35)
  • cacheMessage (41-46)
  • cacheMessages (48-52)
  • clearCachedMessagesForThread (60-68)
  • clearMessageDomInfo (86-88)
  • hasCachedMessage (37-39)
  • setMessageDomInfo (76-80)
  • getMessageDomInfo (82-84)
  • deleteCachedMessage (54-58)
⏰ 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). (2)
  • GitHub Check: build-check (x64)
  • GitHub Check: build-check (x64)
🔇 Additional comments (21)
src/renderer/src/views/playground/demos/MessageListDemo.vue (1)

172-177: LGTM!

The computed property correctly transforms the messages array into the new MessageListItem[] format required by the updated MessageList API. The mapping is straightforward and properly typed.

src/renderer/src/components/MessageNavigationSidebar.vue (1)

137-142: LGTM!

The addition of the totalMessages prop provides explicit control over the displayed message count, which aligns with the virtualization changes where the messages array may not contain all messages. The prop is properly typed and consistently used throughout the template.

src/renderer/src/main.ts (1)

40-50: Add a code comment explaining the purpose of terminateWorker() in the cleanup sequence.

The cleanup calls workers.katex.terminate() and workers.mermaid.terminate() directly, then also calls clearKaTeXWorker(), clearMermaidWorker(), and terminateWorker() from the markstream-vue library. Since all three library functions are called in sequence, clarify whether terminateWorker() handles additional worker cleanup beyond the individual worker terminations or if this creates redundant termination calls. Consider adding a comment or checking the library documentation to document the intended cleanup flow.

src/renderer/src/views/ChatTabView.vue (4)

33-33: LGTM: Total messages prop added to navigation sidebar.

The total-messages prop binding is now passed to MessageNavigationSidebar for both large-screen and mobile contexts. This aligns with the expanded public API mentioned in the AI summary.

Also applies to: 56-56


251-251: LGTM: Watcher dependency updated to use messageCount.

Replacing variantAwareMessages.length with chatStore.messageCount is cleaner and likely more efficient, as it avoids recomputing the entire message array just to check length.


260-267: LGTM: Prefetch messages when navigation opens.

The new watcher triggers prefetchAllMessages() when message navigation is opened. This should improve UX by ensuring messages are available for navigation display.


215-215: No changes needed—the code is correct.

chatStore.getMessageIds() is a synchronous function that returns an array directly, not a Promise. It's defined as const getMessageIds = () => messageIdsMap.value.get(getTabId()) ?? [], which retrieves cached message IDs for the active tab/conversation and returns them immediately. The .includes() calls at lines 215 and 236 are valid. No parameters are required—the function uses getTabId() internally to scope to the active conversation.

Likely an incorrect or invalid review comment.

src/main/presenter/sqlitePresenter/index.ts (1)

339-341: LGTM: Batch message retrieval methods added.

Both queryMessageIds and getMessagesByIds follow the established delegation pattern, passing calls directly to the underlying messagesTable. These additions provide efficient batch operations for the virtual scrolling implementation.

Also applies to: 372-374

src/shared/types/presenters/legacy.presenters.d.ts (1)

356-356: LGTM: Type definitions match implementations.

The ISQLitePresenter interface additions for queryMessageIds and getMessagesByIds correctly match the implementations in sqlitePresenter/index.ts. Method signatures and return types are properly declared.

Also applies to: 362-362

src/renderer/src/components/ChatView.vue (1)

9-9: Data structure compatibility confirmed after prop rename.

The prop binding change from :messages="chatStore.getMessages()" to :items="chatStore.messageItems" is correctly aligned. chatStore.messageItems is a computed property returning MessageListItem[], which matches the expected prop type in MessageList.vue.

src/main/presenter/sessionPresenter/index.ts (1)

470-476: LGTM: Session presenter correctly delegates batch retrieval to message manager.

Both getMessageIds and getMessagesByIds are properly declared in IThreadPresenter interface and correctly implemented with delegation to the messageManager layer. Signatures match between interface and implementation, and the async/await pattern is consistent with other methods in the class.

src/main/presenter/sqlitePresenter/tables/messages.ts (2)

397-410: LGTM!

The queryIds method correctly mirrors the filtering and ordering logic of the existing query method, ensuring consistency between ID-only and full message queries.


189-213: Order preservation is correctly handled through the MessageManager layer.

The getByIds method returns messages in database order (not input order) when using the IN clause. However, MessageManager.getMessagesByIds explicitly re-orders results by iterating through the input messageIds array and building the output using a Map lookup, ensuring input order is always preserved for all public callers. All usage paths go through this re-ordering layer via SessionPresenter.getMessagesByIds.

src/main/presenter/sessionPresenter/managers/messageManager.ts (1)

234-236: LGTM!

Clean delegation to the SQLite layer.

src/shared/types/presenters/thread.presenter.d.ts (2)

161-162: LGTM!

Type declarations align with the implementations in MessageManager and SessionPresenter.


203-213: LGTM!

The IMessageManager interface additions are consistent with the implementation.

src/renderer/src/stores/chat.ts (1)

577-619: LGTM!

The prefetch utilities are well-designed with appropriate buffer sizes and batch handling. The ensureMessagesLoadedByIds correctly filters to only fetch missing messages.

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

3-46: Virtual scroller integration looks correct.

The DynamicScroller configuration with :min-item-size="48", :buffer="200", and :emit-update="true" provides a reasonable baseline. The size-dependencies using getMessageSizeKey and getVariantSizeKey help the scroller recalculate heights when content changes.

Note: The MessageItemPlaceholder (Line 39) renders when item.message is null (uncached), which is good for showing loading states during virtualized scrolling.


614-636: LGTM!

The handleVirtualUpdate correctly coordinates prefetching with visible range, updates DOM info for other features, and handles pending scroll targets after items are rendered.


684-693: LGTM!

Watching the last message's size key ensures auto-scroll works correctly during streaming when the last message's content grows.


696-710: LGTM!

Proper cleanup of timers and pending state to prevent memory leaks and stale scroll operations.

Comment on lines +574 to +612
const scrollToMessage = (messageId: string) => {
void chatStore.ensureMessagesLoadedByIds([messageId])
const index = props.items.findIndex((msg) => msg.id === messageId)
const scroller = dynamicScrollerRef.value
const tryScrollToRenderedMessage = () => {
const container = messagesContainer.value
if (!container) return false
const target = container.querySelector(`[data-message-id="${messageId}"]`) as HTMLElement | null
if (!target) return false
scrollToMessageBase(messageId)
return true
}
if (index !== -1 && scroller && typeof scroller.scrollToItem === 'function') {
pendingScrollTargetId = messageId
if (scrollRetryTimer) {
clearTimeout(scrollRetryTimer)
scrollRetryTimer = null
}
const currentToken = ++scrollRetryToken
const attemptScroll = (attempt: number) => {
if (currentToken !== scrollRetryToken) return
scroller.scrollToItem(index)
scroller.forceUpdate?.()
nextTick(() => {
if (tryScrollToRenderedMessage()) return
if (attempt >= MAX_SCROLL_RETRIES) return
scrollRetryTimer = window.setTimeout(() => {
scrollRetryTimer = null
attemptScroll(attempt + 1)
}, 32)
})
}
attemptScroll(0)
return
}
scrollToMessageBase(messageId)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Race condition between message loading and scroll retries.

Line 575 calls ensureMessagesLoadedByIds but doesn't await it before starting the scroll retry loop. If the message takes longer than ~256ms (8 retries × 32ms) to load, all retries will fail.

🔎 Proposed fix
-const scrollToMessage = (messageId: string) => {
-  void chatStore.ensureMessagesLoadedByIds([messageId])
+const scrollToMessage = async (messageId: string) => {
+  await chatStore.ensureMessagesLoadedByIds([messageId])
   const index = props.items.findIndex((msg) => msg.id === messageId)
   const scroller = dynamicScrollerRef.value
   // ... rest of the function

Alternatively, if you want to keep it non-blocking, start the retry loop only after the message is loaded:

 const scrollToMessage = (messageId: string) => {
-  void chatStore.ensureMessagesLoadedByIds([messageId])
+  chatStore.ensureMessagesLoadedByIds([messageId]).then(() => {
+    const index = props.items.findIndex((msg) => msg.id === messageId)
+    // ... scroll logic here
+  })
+  return
   const index = props.items.findIndex((msg) => msg.id === messageId)
📝 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 scrollToMessage = (messageId: string) => {
void chatStore.ensureMessagesLoadedByIds([messageId])
const index = props.items.findIndex((msg) => msg.id === messageId)
const scroller = dynamicScrollerRef.value
const tryScrollToRenderedMessage = () => {
const container = messagesContainer.value
if (!container) return false
const target = container.querySelector(`[data-message-id="${messageId}"]`) as HTMLElement | null
if (!target) return false
scrollToMessageBase(messageId)
return true
}
if (index !== -1 && scroller && typeof scroller.scrollToItem === 'function') {
pendingScrollTargetId = messageId
if (scrollRetryTimer) {
clearTimeout(scrollRetryTimer)
scrollRetryTimer = null
}
const currentToken = ++scrollRetryToken
const attemptScroll = (attempt: number) => {
if (currentToken !== scrollRetryToken) return
scroller.scrollToItem(index)
scroller.forceUpdate?.()
nextTick(() => {
if (tryScrollToRenderedMessage()) return
if (attempt >= MAX_SCROLL_RETRIES) return
scrollRetryTimer = window.setTimeout(() => {
scrollRetryTimer = null
attemptScroll(attempt + 1)
}, 32)
})
}
attemptScroll(0)
return
}
scrollToMessageBase(messageId)
}
const scrollToMessage = async (messageId: string) => {
await chatStore.ensureMessagesLoadedByIds([messageId])
const index = props.items.findIndex((msg) => msg.id === messageId)
const scroller = dynamicScrollerRef.value
const tryScrollToRenderedMessage = () => {
const container = messagesContainer.value
if (!container) return false
const target = container.querySelector(`[data-message-id="${messageId}"]`) as HTMLElement | null
if (!target) return false
scrollToMessageBase(messageId)
return true
}
if (index !== -1 && scroller && typeof scroller.scrollToItem === 'function') {
pendingScrollTargetId = messageId
if (scrollRetryTimer) {
clearTimeout(scrollRetryTimer)
scrollRetryTimer = null
}
const currentToken = ++scrollRetryToken
const attemptScroll = (attempt: number) => {
if (currentToken !== scrollRetryToken) return
scroller.scrollToItem(index)
scroller.forceUpdate?.()
nextTick(() => {
if (tryScrollToRenderedMessage()) return
if (attempt >= MAX_SCROLL_RETRIES) return
scrollRetryTimer = window.setTimeout(() => {
scrollRetryTimer = null
attemptScroll(attempt + 1)
}, 32)
})
}
attemptScroll(0)
return
}
scrollToMessageBase(messageId)
}
🤖 Prompt for AI Agents
In @src/renderer/src/components/message/MessageList.vue around lines 574-612,
The race is that scrollToMessage calls chatStore.ensureMessagesLoadedByIds but
does not await it before starting the retry loop (attemptScroll), so retries can
exhaust before the message is loaded; fix by awaiting ensureMessagesLoadedByIds
(or awaiting its promise resolution) before initializing
pendingScrollTargetId/scrollRetryToken and calling attemptScroll, or
alternatively defer starting the retry loop until ensureMessagesLoadedByIds
resolves (e.g., call ensureMessagesLoadedByIds(...).then(() => { /* set
pendingScrollTargetId, increment scrollRetryToken and start attemptScroll */
})), keeping the existing retry/cleanup logic (scrollRetryTimer,
scrollRetryToken, tryScrollToRenderedMessage, scroller.scrollToItem) intact.

Comment on lines +20 to +28
const prune = () => {
while (messageCache.size > MAX_CACHE_ENTRIES) {
const oldestId = messageCache.keys().next().value as string | undefined
if (!oldestId) return
messageCache.delete(oldestId)
messageThreadMap.delete(oldestId)
messageDomInfo.delete(oldestId)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Prune logic could fail to maintain cache size limit in edge cases.

The prune() function only checks messageCache.size but relies on the assumption that all three maps (messageCache, messageThreadMap, messageDomInfo) remain synchronized. If any of the maps becomes desynchronized (e.g., due to direct deletion from one map but not others), the cache size check could become inaccurate.

Additionally, the while loop retrieves oldestId from messageCache.keys().next().value but doesn't verify it's a valid string before using it as a key for the other maps.

🔎 Suggested improvements
 const prune = () => {
   while (messageCache.size > MAX_CACHE_ENTRIES) {
     const oldestId = messageCache.keys().next().value as string | undefined
-    if (!oldestId) return
+    if (!oldestId) {
+      console.warn('Prune encountered undefined oldest entry')
+      return
+    }
     messageCache.delete(oldestId)
     messageThreadMap.delete(oldestId)
     messageDomInfo.delete(oldestId)
   }
 }
🤖 Prompt for AI Agents
In @src/renderer/src/lib/messageRuntimeCache.ts around lines 20-28, prune()
currently assumes all three maps (messageCache, messageThreadMap,
messageDomInfo) stay perfectly synchronized and uses oldestId without validating
it; change it to iterate deterministically and validate keys: while any of the
three map sizes exceed MAX_CACHE_ENTRIES (check messageCache.size,
messageThreadMap.size, messageDomInfo.size), obtain a candidate id from the same
deterministic source (e.g., first key of messageCache), verify typeof oldestId
=== 'string' and that it exists in each map before deleting, and if it is
missing or invalid, skip or fall back to a union-of-keys strategy to pick a
valid id so the loop reliably reduces all map sizes to ≤ MAX_CACHE_ENTRIES;
apply these checks inside prune() referencing messageCache, messageThreadMap,
messageDomInfo, MAX_CACHE_ENTRIES, and oldestId.

Comment on lines +266 to +282
const messageItems = computed((): MessageListItem[] => {
const ids = getMessageIds()
const cacheVersion = messageCacheVersion.value
const currentSelectedVariants = selectedVariantsMap.value
if (cacheVersion < 0) return []

return newMsg
}
return ids.map((messageId) => {
const cached = getCachedMessage(messageId)
if (!cached) {
return { id: messageId, message: null }
}
return {
id: messageId,
message: resolveVariantMessage(cached, currentSelectedVariants)
}
})
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Unreachable guard condition and potential scroll-to-bottom issue.

  1. Line 270: if (cacheVersion < 0) return [] - Since messageCacheVersion is initialized to 0 and only incremented, this condition is unreachable.

  2. Potential cause of reported scroll bug: The messageItems computed property returns items with message: null for uncached messages. When switching conversations, loadMessages (Line 621) fetches IDs first, then prefetches only the first 50 messages asynchronously. The scroll-to-bottom in MessageList.vue may execute before all messages are loaded, causing the scroll position to be incorrect for long conversations.

🔎 Suggested fixes

For the unreachable guard:

-    if (cacheVersion < 0) return []
+    // cacheVersion is used only to trigger reactivity updates
+    void cacheVersion

For the scroll issue, consider ensuring the last message is loaded before scrolling:

// In loadMessages:
-      await prefetchMessagesForRange(0, Math.min(messageIds.length - 1, 50))
+      // Prefetch both beginning and end to support immediate scroll-to-bottom
+      const prefetchPromises = [
+        prefetchMessagesForRange(0, Math.min(49, messageIds.length - 1))
+      ]
+      if (messageIds.length > 50) {
+        prefetchPromises.push(
+          prefetchMessagesForRange(messageIds.length - 20, messageIds.length - 1)
+        )
+      }
+      await Promise.all(prefetchPromises)
📝 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 messageItems = computed((): MessageListItem[] => {
const ids = getMessageIds()
const cacheVersion = messageCacheVersion.value
const currentSelectedVariants = selectedVariantsMap.value
if (cacheVersion < 0) return []
return newMsg
}
return ids.map((messageId) => {
const cached = getCachedMessage(messageId)
if (!cached) {
return { id: messageId, message: null }
}
return {
id: messageId,
message: resolveVariantMessage(cached, currentSelectedVariants)
}
})
})
const messageItems = computed((): MessageListItem[] => {
const ids = getMessageIds()
const cacheVersion = messageCacheVersion.value
const currentSelectedVariants = selectedVariantsMap.value
// cacheVersion is used only to trigger reactivity updates
void cacheVersion
return ids.map((messageId) => {
const cached = getCachedMessage(messageId)
if (!cached) {
return { id: messageId, message: null }
}
return {
id: messageId,
message: resolveVariantMessage(cached, currentSelectedVariants)
}
})
})
🤖 Prompt for AI Agents
In @src/renderer/src/stores/chat.ts around lines 266-282, Remove the unreachable
guard and prevent MessageList from seeing null last messages: delete the `if
(cacheVersion < 0) return []` check in the `messageItems` computed and instead
ensure `loadMessages` waits for the final message to be cached before UI scroll
occurs; specifically, in `loadMessages` (the function that calls `getMessageIds`
and prefetches the first 50), after fetching IDs call whatever
cache-fill/prefetch function (or `getCachedMessage` wrapper) for the last
message ID and await it (or synchronously load it) before resolving so
`messageItems` (which uses `getMessageIds`, `getCachedMessage`,
`resolveVariantMessage` and `selectedVariantsMap`) will not produce `message:
null` for the last item and MessageList.vue can safely scroll to bottom.

Comment on lines 1110 to 1121
if (getActiveThreadId() === getActiveThreadId()) {
const mainMsgIndex = getMessages().findIndex((m) => m.id === mainMessage.id)
if (mainMsgIndex !== -1) {
getMessages()[mainMsgIndex] = enrichedMainMessage as AssistantMessage | UserMessage
}
cacheMessageForView(enrichedMainMessage as AssistantMessage | UserMessage)
ensureMessageId(enrichedMainMessage.id)
}
}
} else {
// 如果是当前激活的会话,更新显示
if (getActiveThreadId() === getActiveThreadId()) {
const msgIndex = getMessages().findIndex((m) => m.id === msg.eventId)
if (msgIndex !== -1) {
getMessages()[msgIndex] = enrichedMessage as AssistantMessage | UserMessage
}
cacheMessageForView(enrichedMessage as AssistantMessage | UserMessage)
ensureMessageId(enrichedMessage.id)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Redundant tautological condition.

Lines 1110 and 1117 contain if (getActiveThreadId() === getActiveThreadId()) which is always true. This appears to be a copy-paste error - these should likely compare against cached.threadId:

🔎 Proposed fix
         if (mainMessage) {
           const enrichedMainMessage = await enrichMessageWithExtra(mainMessage)
           // If this is the active conversation, update display
-          if (getActiveThreadId() === getActiveThreadId()) {
+          if (cached.threadId === getActiveThreadId()) {
             cacheMessageForView(enrichedMainMessage as AssistantMessage | UserMessage)
             ensureMessageId(enrichedMainMessage.id)
           }
         }
       } else {
         // If this is the active conversation, update display
-        if (getActiveThreadId() === getActiveThreadId()) {
+        if (cached.threadId === getActiveThreadId()) {
           cacheMessageForView(enrichedMessage as AssistantMessage | UserMessage)
           ensureMessageId(enrichedMessage.id)
         }
       }
📝 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
if (getActiveThreadId() === getActiveThreadId()) {
const mainMsgIndex = getMessages().findIndex((m) => m.id === mainMessage.id)
if (mainMsgIndex !== -1) {
getMessages()[mainMsgIndex] = enrichedMainMessage as AssistantMessage | UserMessage
}
cacheMessageForView(enrichedMainMessage as AssistantMessage | UserMessage)
ensureMessageId(enrichedMainMessage.id)
}
}
} else {
// 如果是当前激活的会话,更新显示
if (getActiveThreadId() === getActiveThreadId()) {
const msgIndex = getMessages().findIndex((m) => m.id === msg.eventId)
if (msgIndex !== -1) {
getMessages()[msgIndex] = enrichedMessage as AssistantMessage | UserMessage
}
cacheMessageForView(enrichedMessage as AssistantMessage | UserMessage)
ensureMessageId(enrichedMessage.id)
}
}
if (cached.threadId === getActiveThreadId()) {
cacheMessageForView(enrichedMainMessage as AssistantMessage | UserMessage)
ensureMessageId(enrichedMainMessage.id)
}
}
} else {
// If this is the active conversation, update display
if (cached.threadId === getActiveThreadId()) {
cacheMessageForView(enrichedMessage as AssistantMessage | UserMessage)
ensureMessageId(enrichedMessage.id)
}
}
🤖 Prompt for AI Agents
In @src/renderer/src/stores/chat.ts around lines 1110-1121, The tautological
checks using if (getActiveThreadId() === getActiveThreadId()) should be replaced
to compare the active thread against the message's thread id; change both
occurrences to if (getActiveThreadId() === cached.threadId) (or the actual
message/thread variable in scope) so that the blocks calling
cacheMessageForView(enrichedMainMessage/enrichedMessage) and
ensureMessageId(...) only run when the message belongs to the active thread;
verify the variable name for the cached message (e.g., cached.threadId) is in
scope and adjust accordingly.

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