-
Notifications
You must be signed in to change notification settings - Fork 612
feat: messageList access virtual scrolling #1235
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Conversation
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
📝 WalkthroughWalkthroughReplaces the static message list with a virtualized DynamicScroller-based MessageList, adds programmatic scroll-to-message with retries, shifts prop from Changes
Sequence Diagram(s)mermaid Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
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. Comment |
There was a problem hiding this 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
falseat line 476, even when the parent message navigation and subsequentrevealHighlight()call innextTickmight 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
📒 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
srcdirectory
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 statementsVue 3 app code in
src/renderer/srcshould be organized intocomponents/,stores/,views/,i18n/,lib/directories with shell UI insrc/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 insrc/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,activefor DynamicScrollerItem- Scroll anchor in the
#afterslot for bottom detection
100-102: Constants are appropriately defined.
MIN_ITEM_SIZEof 96px is a reasonable minimum estimate for message items in a virtual scroller, andNEAR_BOTTOM_THRESHOLDprovides sensible hysteresis for auto-follow behavior.
141-144: Clean callback ref pattern for bridging scroller instance and DOM element.The
setMessageScrollercorrectly 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
messageIndexMapprovides 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
maybeRestoreAutoFollowfunction 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
useResizeObserverwill properly track the DynamicScroller's root element viamessagesContainer, maintaining the auto-scroll behavior on container resize.
117-117: Clear naming distinction between scroll methods.Renaming to
scrollToMessageViaDomclearly distinguishes the DOM-based scroll helper from the new compositescrollToMessagethat coordinates virtual scroller and DOM scrolling.
541-547: Public API maintained with improved internal implementation.The
defineExposepreserves 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 insrc/renderer/src/main.tsviaimport 'vue-virtual-scroller/dist/vue-virtual-scroller.css'. The import statement in MessageList.vue is valid.
215-226: The nestednextTickpattern 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 usingnextTick()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.
|
This change will cause multiple long conversations to fail to automatically scroll to the bottom when switching between them. |
0ae4232 to
84940b2
Compare
- 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.
There was a problem hiding this 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.scrollHeightrepresents the total content height, but DynamicScroller only renders visible messages. This causesscrollToBottom()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 = targetTopWhen
container.scrollHeightreflects only rendered items (not the full virtual list), the scroll position is wrong. Additionally, the check forscroller.scrollToBottom()relies on a method that likely doesn't exist in DynamicScroller's API.Required verification:
- Confirm DynamicScroller from
vue-virtual-scroller@2.0.0-beta.8lacks a publicscrollToBottom()method or requires a different API- Implement a virtual-scroll-aware approach—either use DynamicScroller's intended scroll API or restore scroll position at the component boundary (e.g., via
keyremounting)- 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
loadMessagesfunction (Line 635) only prefetches the first 50 messages. When switching to a long conversation:
- Message IDs are set immediately
- Only the first 50 messages are prefetched
MessageList.vueattempts to scroll to bottom- The DynamicScroller doesn't have accurate heights for unloaded messages at the end
- 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, andliveNodeBufferprops are supported by markstream-vue and properly configured for virtualization of thinking content.The values
120(maxLiveNodes) and30(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-inThis 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 thetouchimplementation to avoid redundant operations.The current
touchimplementation 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 aftermessageCache.set()incacheMessage(line 42), which means the entry is already at the end. This makes thetouchcall 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 forMAX_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:cacheMessageupdates three maps without atomicity guarantees.If any operation in
cacheMessagethrows 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
getMessageVariantscall (Line 181). With many assistant messages, this creates N+1 database queries.Consider batching variant fetches by collecting all
parent_idvalues 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
getMessageVariantsByParentIdsmethod to the SQLite presenter.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
src/main/presenter/sessionPresenter/index.tssrc/main/presenter/sessionPresenter/managers/messageManager.tssrc/main/presenter/sqlitePresenter/index.tssrc/main/presenter/sqlitePresenter/tables/messages.tssrc/renderer/src/components/ChatView.vuesrc/renderer/src/components/MessageNavigationSidebar.vuesrc/renderer/src/components/markdown/MarkdownRenderer.vuesrc/renderer/src/components/message/MessageItemPlaceholder.vuesrc/renderer/src/components/message/MessageList.vuesrc/renderer/src/components/think-content/ThinkContent.vuesrc/renderer/src/lib/messageRuntimeCache.tssrc/renderer/src/main.tssrc/renderer/src/stores/chat.tssrc/renderer/src/views/ChatTabView.vuesrc/renderer/src/views/playground/demos/MessageListDemo.vuesrc/shared/types/presenters/legacy.presenters.d.tssrc/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.vuesrc/renderer/src/views/playground/demos/MessageListDemo.vuesrc/renderer/src/components/message/MessageItemPlaceholder.vuesrc/renderer/src/components/think-content/ThinkContent.vuesrc/renderer/src/views/ChatTabView.vuesrc/renderer/src/components/ChatView.vuesrc/renderer/src/components/message/MessageList.vue
src/renderer/src/**/*.{ts,tsx,vue}
📄 CodeRabbit inference engine (CLAUDE.md)
Use
usePresenter.tscomposable for renderer-to-main IPC communication via direct presenter method callsEnsure 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,useDebounceFnVue 3 renderer app code should be organized in
src/renderer/srcwith subdirectories forcomponents/,stores/,views/,i18n/, andlib/
Files:
src/renderer/src/components/MessageNavigationSidebar.vuesrc/renderer/src/views/playground/demos/MessageListDemo.vuesrc/renderer/src/main.tssrc/renderer/src/components/message/MessageItemPlaceholder.vuesrc/renderer/src/components/think-content/ThinkContent.vuesrc/renderer/src/views/ChatTabView.vuesrc/renderer/src/components/ChatView.vuesrc/renderer/src/lib/messageRuntimeCache.tssrc/renderer/src/components/message/MessageList.vuesrc/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.vuesrc/renderer/src/views/playground/demos/MessageListDemo.vuesrc/main/presenter/sqlitePresenter/index.tssrc/shared/types/presenters/thread.presenter.d.tssrc/renderer/src/main.tssrc/main/presenter/sqlitePresenter/tables/messages.tssrc/renderer/src/components/message/MessageItemPlaceholder.vuesrc/renderer/src/components/think-content/ThinkContent.vuesrc/shared/types/presenters/legacy.presenters.d.tssrc/renderer/src/views/ChatTabView.vuesrc/main/presenter/sessionPresenter/index.tssrc/renderer/src/components/ChatView.vuesrc/renderer/src/lib/messageRuntimeCache.tssrc/main/presenter/sessionPresenter/managers/messageManager.tssrc/renderer/src/components/message/MessageList.vuesrc/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.vuesrc/renderer/src/views/playground/demos/MessageListDemo.vuesrc/main/presenter/sqlitePresenter/index.tssrc/shared/types/presenters/thread.presenter.d.tssrc/renderer/src/main.tssrc/main/presenter/sqlitePresenter/tables/messages.tssrc/renderer/src/components/message/MessageItemPlaceholder.vuesrc/renderer/src/components/think-content/ThinkContent.vuesrc/shared/types/presenters/legacy.presenters.d.tssrc/renderer/src/views/ChatTabView.vuesrc/main/presenter/sessionPresenter/index.tssrc/renderer/src/components/ChatView.vuesrc/renderer/src/lib/messageRuntimeCache.tssrc/main/presenter/sessionPresenter/managers/messageManager.tssrc/renderer/src/components/message/MessageList.vuesrc/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}: Usereffor primitives and references,reactivefor objects in Vue 3 Composition API
Prefercomputedproperties over methods for derived state in Vue components
Import Shadcn Vue components from@/shadcn/components/ui/path alias
Use thecn()utility function combining clsx and tailwind-merge for dynamic Tailwind classes
UsedefineAsyncComponent()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
UseusePresentercomposable for main process communication instead of direct IPC calls
Files:
src/renderer/src/components/MessageNavigationSidebar.vuesrc/renderer/src/views/playground/demos/MessageListDemo.vuesrc/renderer/src/main.tssrc/renderer/src/components/message/MessageItemPlaceholder.vuesrc/renderer/src/components/think-content/ThinkContent.vuesrc/renderer/src/views/ChatTabView.vuesrc/renderer/src/components/ChatView.vuesrc/renderer/src/lib/messageRuntimeCache.tssrc/renderer/src/components/message/MessageList.vuesrc/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 usingdefinePropsanddefineEmitswith TypeScript interfaces
Useprovide/injectfor 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 patternlucide:{icon-name}
Usev-memodirective for memoizing expensive computations in templates
Usev-oncedirective for rendering static content without reactivity updates
Use virtual scrolling withRecycleScrollercomponent for rendering long lists
Subscribe to events usingrendererEvents.on()and unsubscribe inonUnmountedlifecycle hook
Files:
src/renderer/src/components/MessageNavigationSidebar.vuesrc/renderer/src/views/playground/demos/MessageListDemo.vuesrc/renderer/src/components/message/MessageItemPlaceholder.vuesrc/renderer/src/components/think-content/ThinkContent.vuesrc/renderer/src/views/ChatTabView.vuesrc/renderer/src/components/ChatView.vuesrc/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.vuesrc/renderer/src/components/message/MessageItemPlaceholder.vuesrc/renderer/src/components/think-content/ThinkContent.vuesrc/renderer/src/components/ChatView.vuesrc/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.vuesrc/renderer/src/views/playground/demos/MessageListDemo.vuesrc/renderer/src/components/message/MessageItemPlaceholder.vuesrc/renderer/src/components/think-content/ThinkContent.vuesrc/renderer/src/views/ChatTabView.vuesrc/renderer/src/components/ChatView.vuesrc/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. Runpnpm run formatafter completing features
Files:
src/renderer/src/components/MessageNavigationSidebar.vuesrc/renderer/src/views/playground/demos/MessageListDemo.vuesrc/main/presenter/sqlitePresenter/index.tssrc/shared/types/presenters/thread.presenter.d.tssrc/renderer/src/main.tssrc/main/presenter/sqlitePresenter/tables/messages.tssrc/renderer/src/components/message/MessageItemPlaceholder.vuesrc/renderer/src/components/think-content/ThinkContent.vuesrc/shared/types/presenters/legacy.presenters.d.tssrc/renderer/src/views/ChatTabView.vuesrc/main/presenter/sessionPresenter/index.tssrc/renderer/src/components/ChatView.vuesrc/renderer/src/lib/messageRuntimeCache.tssrc/main/presenter/sessionPresenter/managers/messageManager.tssrc/renderer/src/components/message/MessageList.vuesrc/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.tssrc/shared/types/presenters/thread.presenter.d.tssrc/renderer/src/main.tssrc/main/presenter/sqlitePresenter/tables/messages.tssrc/shared/types/presenters/legacy.presenters.d.tssrc/main/presenter/sessionPresenter/index.tssrc/renderer/src/lib/messageRuntimeCache.tssrc/main/presenter/sessionPresenter/managers/messageManager.tssrc/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.tssrc/shared/types/presenters/thread.presenter.d.tssrc/renderer/src/main.tssrc/main/presenter/sqlitePresenter/tables/messages.tssrc/shared/types/presenters/legacy.presenters.d.tssrc/main/presenter/sessionPresenter/index.tssrc/renderer/src/lib/messageRuntimeCache.tssrc/main/presenter/sessionPresenter/managers/messageManager.tssrc/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 viamainWindow.webContents.send()
Implement one presenter per functional domain in the main process
Files:
src/main/presenter/sqlitePresenter/index.tssrc/main/presenter/sqlitePresenter/tables/messages.tssrc/main/presenter/sessionPresenter/index.tssrc/main/presenter/sessionPresenter/managers/messageManager.ts
src/main/**/*.ts
📄 CodeRabbit inference engine (CLAUDE.md)
src/main/**/*.ts: Use EventBus fromsrc/main/eventbus.tsfor decoupled inter-process communication
Context isolation must be enabled with preload scripts for secure IPC communicationElectron main process code should reside in
src/main/, with presenters organized inpresenter/subdirectory (Window, Tab, Thread, Mcp, Config, LLMProvider), and app events managed viaeventbus.ts
Files:
src/main/presenter/sqlitePresenter/index.tssrc/main/presenter/sqlitePresenter/tables/messages.tssrc/main/presenter/sessionPresenter/index.tssrc/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.tssrc/shared/types/presenters/thread.presenter.d.tssrc/renderer/src/main.tssrc/main/presenter/sqlitePresenter/tables/messages.tssrc/shared/types/presenters/legacy.presenters.d.tssrc/main/presenter/sessionPresenter/index.tssrc/renderer/src/lib/messageRuntimeCache.tssrc/main/presenter/sessionPresenter/managers/messageManager.tssrc/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 insrc/shared/
IPC contract definitions must be placed insrc/shared/Shared TypeScript types and utilities should be placed in
src/shared/
Files:
src/shared/types/presenters/thread.presenter.d.tssrc/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.tssrc/renderer/src/lib/messageRuntimeCache.tssrc/renderer/src/stores/chat.ts
src/renderer/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/vue-stack-guide.mdc)
src/renderer/src/**/*.{ts,tsx}: UseshallowRefandshallowReactivefor optimizing reactivity with large objects
Prefertypeoverinterfacein TypeScript unless using inheritance withextends
Files:
src/renderer/src/main.tssrc/renderer/src/lib/messageRuntimeCache.tssrc/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 withdefineStorefunction 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.vuesrc/renderer/src/components/message/MessageItemPlaceholder.vuesrc/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.vuesrc/renderer/src/components/message/MessageItemPlaceholder.vuesrc/renderer/src/components/ChatView.vuesrc/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.vuesrc/renderer/src/components/message/MessageItemPlaceholder.vuesrc/renderer/src/components/think-content/ThinkContent.vuesrc/renderer/src/components/ChatView.vuesrc/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.vuesrc/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.vuesrc/renderer/src/main.tssrc/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.vuesrc/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
totalMessagesprop provides explicit control over the displayed message count, which aligns with the virtualization changes where themessagesarray 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 ofterminateWorker()in the cleanup sequence.The cleanup calls
workers.katex.terminate()andworkers.mermaid.terminate()directly, then also callsclearKaTeXWorker(),clearMermaidWorker(), andterminateWorker()from themarkstream-vuelibrary. Since all three library functions are called in sequence, clarify whetherterminateWorker()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-messagesprop 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.lengthwithchatStore.messageCountis 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 asconst 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 usesgetTabId()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
queryMessageIdsandgetMessagesByIdsfollow the established delegation pattern, passing calls directly to the underlyingmessagesTable. 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
queryMessageIdsandgetMessagesByIdscorrectly 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.messageItemsis a computed property returningMessageListItem[], 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
getMessageIdsandgetMessagesByIdsare properly declared inIThreadPresenterinterface 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
queryIdsmethod correctly mirrors the filtering and ordering logic of the existingquerymethod, ensuring consistency between ID-only and full message queries.
189-213: Order preservation is correctly handled through the MessageManager layer.The
getByIdsmethod returns messages in database order (not input order) when using theINclause. However,MessageManager.getMessagesByIdsexplicitly re-orders results by iterating through the inputmessageIdsarray 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 viaSessionPresenter.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
MessageManagerandSessionPresenter.
203-213: LGTM!The
IMessageManagerinterface 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
ensureMessagesLoadedByIdscorrectly 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 usinggetMessageSizeKeyandgetVariantSizeKeyhelp the scroller recalculate heights when content changes.Note: The
MessageItemPlaceholder(Line 39) renders whenitem.messageis null (uncached), which is good for showing loading states during virtualized scrolling.
614-636: LGTM!The
handleVirtualUpdatecorrectly 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.
| 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) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 functionAlternatively, 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.
| 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.
| 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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) | ||
| } | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unreachable guard condition and potential scroll-to-bottom issue.
-
Line 270:
if (cacheVersion < 0) return []- SincemessageCacheVersionis initialized to0and only incremented, this condition is unreachable. -
Potential cause of reported scroll bug: The
messageItemscomputed property returns items withmessage: nullfor uncached messages. When switching conversations,loadMessages(Line 621) fetches IDs first, then prefetches only the first 50 messages asynchronously. The scroll-to-bottom inMessageList.vuemay 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 cacheVersionFor 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.
| 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.
| 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.

MessageList replace to DynamicScroller
feat: enhance message handling and rendering
Summary by CodeRabbit
New Features
Bug Fixes
✏️ Tip: You can customize this high-level summary in your review settings.