-
Notifications
You must be signed in to change notification settings - Fork 614
feat: add message navigation sidebar with search functionality #776
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
Conversation
WalkthroughAdds a message navigation sidebar with search and jump-to-message capabilities, integrates toggle and close behaviors across views, exposes message scrolling APIs, and introduces new i18n strings. Updates the chat store with UI state and a getter. Minor layout/interaction tweaks in ChatTabView, TitleView, ThreadsView, ChatView, and MessageList. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant TitleView
participant ChatTabView
participant Store as ChatStore
participant Nav as MessageNavigationSidebar
participant ChatView
participant MsgList as MessageList
User->>TitleView: Click navigation toggle
TitleView->>Store: Toggle isMessageNavigationOpen
Store-->>ChatTabView: isMessageNavigationOpen changed
ChatTabView->>Nav: Mount/show with messages, activeMessageId
User->>Nav: Search / click message
Nav-->>ChatTabView: emit scroll-to-message(messageId)
ChatTabView->>ChatView: access exposed messageList
ChatView->>MsgList: call scrollToMessage(messageId)
MsgList-->>User: Smooth scroll + temporary highlight
alt Small screen
ChatTabView->>Store: Close navigation after scroll (delayed)
end
User-->>ChatTabView: Click outside overlay (mobile)
ChatTabView->>Store: Close navigation (if outside Nav)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
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: 3
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/ChatView.vue (1)
84-91: Localize hardcoded thread title "新会话"Renderer user-visible strings must use i18n. Replace the literal with a translation key (e.g., chat.threads.newThread).
Example change (outside this hunk):
-const threadId = await chatStore.createThread('新会话', { +const threadId = await chatStore.createThread(t('chat.threads.newThread'), {If chat.threads.newThread doesn't exist yet, add it across locales in chat.json.
Also applies to: 99-106
🧹 Nitpick comments (25)
src/renderer/src/stores/chat.ts (3)
42-42: UI state addition is fine; consider persisting the toggle across sessionsisMessageNavigationOpen is a good fit for Pinia state. Optional: persist this UI preference (e.g., per-tab or globally) so the sidebar opens in the user’s last-chosen state after app reload.
I can wire this via a small Pinia plugin/localStorage bridge if desired.
86-90: Simplify getter; avoid redundant activeThreadId guard and annotate return typegetMessages() already returns [] when no active thread. You can simplify and make the return type explicit to improve readability.
Apply:
-const getCurrentThreadMessages = () => { - const activeThreadId = getActiveThreadId() - if (!activeThreadId) return [] - return getMessages() -} +const getCurrentThreadMessages = (): (AssistantMessage | UserMessage)[] => getMessages()
1188-1196: Export additions look good; avoid API duplication confusionExposing isMessageNavigationOpen and getCurrentThreadMessages is reasonable. Minor: since getMessages already returns the active tab’s messages, consider documenting that getCurrentThreadMessages is an alias intended for navigation context, or export an alias to reduce cognitive overhead in consumers.
Optionally, in the future, replace getCurrentThreadMessages calls with getMessages and deprecate the alias to keep a single source of truth.
Also applies to: 1226-1231
src/renderer/src/components/ThreadsView.vue (1)
218-219: Close both side panels — DRY the repeated logicYou close both panels in two places. Extracting a small helper keeps this consistent and easier to change.
Apply within these ranges:
- chatStore.isSidebarOpen = false - chatStore.isMessageNavigationOpen = false + closeSidePanels()- chatStore.isSidebarOpen = false - chatStore.isMessageNavigationOpen = false + closeSidePanels()Add this helper near existing consts (outside the selected ranges):
const closeSidePanels = () => { chatStore.isSidebarOpen = false chatStore.isMessageNavigationOpen = false }Also applies to: 230-231
src/renderer/src/components/message/MessageList.vue (2)
278-297: Scope the selector to this list and escape messageId; also make the API awaitable and signal successUsing document.querySelector risks matching elements from another list (e.g., multiple tabs) and breaks if messageId contains special CSS characters. Scope the query to messagesContainer and use CSS.escape when available. Returning a Promise makes the public API more reliable for callers.
Apply this diff:
-const scrollToMessage = (messageId: string) => { - nextTick(() => { - const messageElement = document.querySelector(`[data-message-id="${messageId}"]`) - if (messageElement) { - messageElement.scrollIntoView({ - behavior: 'smooth', - block: 'center' - }) - - // 添加高亮效果 - messageElement.classList.add('message-highlight') - setTimeout(() => { - messageElement.classList.remove('message-highlight') - }, 2000) - } - }) -} +const scrollToMessage = async (messageId: string): Promise<boolean> => { + await nextTick() + const container = messagesContainer.value + if (!container) return false + const safeId = + (window.CSS && 'escape' in window.CSS) ? window.CSS.escape(messageId) : messageId + const messageElement = container.querySelector<HTMLElement>( + `[data-message-id="${safeId}"]` + ) + if (!messageElement) return false + + messageElement.scrollIntoView({ + behavior: 'smooth', + block: 'center', + inline: 'nearest' + }) + + // 添加高亮效果 + messageElement.classList.add('message-highlight') + window.setTimeout(() => { + messageElement.classList.remove('message-highlight') + }, 2000) + return true +}
391-395: Type the exposed API and reflect the awaitable scrollToMessageExpose explicit types so downstream components get correct intellisense and can await the operation.
-defineExpose({ - scrollToBottom, - scrollToMessage, - aboveThreshold -}) +defineExpose<{ + scrollToBottom: () => void + scrollToMessage: (id: string) => Promise<boolean> + aboveThreshold: typeof aboveThreshold +}>({ + scrollToBottom, + scrollToMessage, + aboveThreshold +})src/renderer/src/i18n/ru-RU/chat.json (1)
58-67: Use ICU pluralization for Russian countsRussian requires plural rules; fixed strings like “Всего {count} сообщений” will be grammatically wrong for count=1. Use vue-i18n ICU plurals for both totalMessages and searchResults.
Suggested replacements:
- "totalMessages": "Всего {count} сообщений", - "searchResults": "Найдено {count} результатов из {total} сообщений", + "totalMessages": "Всего {count} {count, plural, one {сообщение} few {сообщения} many {сообщений} other {сообщения}}", + "searchResults": "Найден{count, plural, one {# результат} few {# результата} many {# результатов} other {# результата}} из {total} {total, plural, one {сообщения} few {сообщений} many {сообщений} other {сообщений}}"If your i18n setup uses list-based formatting instead of ICU, I can adapt to that format.
src/renderer/src/i18n/fr-FR/chat.json (1)
63-64: Optional: add pluralization for count-dependent stringsConsider using vue-i18n pluralization for better French grammar, e.g., define two forms:
- totalMessages: "{count} message au total | {count} messages au total"
- searchResults: "{count} résultat trouvé sur {total} messages | {count} résultats trouvés sur {total} messages"
Note: switching to pluralization requires using tc() (or equivalent) at call sites.
src/renderer/src/components/ChatView.vue (1)
116-118: Expose a minimal, typed public API instead of the raw child refExposing the entire messageList ref couples parents to internals and weakens type safety. Prefer exposing just the method(s) parents need (e.g., scrollToMessage) and type the ref.
Apply this diff locally to the exposed API:
-defineExpose({ - messageList -}) +defineExpose({ + scrollToMessage: (id: string) => messageList.value?.scrollToMessage?.(id) +})And (outside this hunk) tighten the ref type near Line 35:
// near Line 35 -type MessageListPublicApi = { scrollToBottom: (smooth?: boolean) => void; scrollToMessage?: (id: string) => void } -const messageList = ref<MessageListPublicApi | null>(null) +type MessageListPublicApi = { + scrollToBottom: (smooth?: boolean) => void + scrollToMessage: (id: string) => void +} +const messageList = ref<MessageListPublicApi | null>(null)src/renderer/src/components/TitleView.vue (1)
46-59: Add accessible labels and i18n to the navigation toggle; convert comment to EnglishIcon-only buttons should expose an aria-label (and optionally title) and reflect pressed state. Also, comments in code should be English per guidelines.
- <!-- 消息导航按钮 --> + <!-- Message navigation toggle --> <Button class="w-7 h-7 rounded-md relative !p-0" size="icon" variant="outline" :class="{ 'bg-accent': chatStore.isMessageNavigationOpen }" + :aria-pressed="chatStore.isMessageNavigationOpen" + :aria-label="t('chat.navigation.title')" + :title="t('chat.navigation.title')" @click="chatStore.isMessageNavigationOpen = !chatStore.isMessageNavigationOpen" > <Icon icon="lucide:list" class="w-4 h-4 absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2" /> </Button>src/renderer/src/i18n/zh-TW/chat.json (1)
61-62: Nit: 用語更貼近繁體中文考慮將「匹配」改為「符合」以更貼近慣用語。
- "noResults": "沒有找到匹配的訊息", + "noResults": "沒有找到符合的訊息",src/renderer/src/components/MessageNavigationSidebar.vue (4)
199-215: Localize date/time format to the active UI localeHardcoding 'zh-CN' breaks formatting for other locales. Use useI18n().locale.
-const { t } = useI18n() +const { t, locale } = useI18n()- if (diff < 24 * 60 * 60 * 1000 && date.getDate() === now.getDate()) { - return date.toLocaleTimeString('zh-CN', { + if (diff < 24 * 60 * 60 * 1000 && date.getDate() === now.getDate()) { + return date.toLocaleTimeString(locale.value, { hour: '2-digit', minute: '2-digit' }) } - return date.toLocaleDateString('zh-CN', { + return date.toLocaleDateString(locale.value, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })Also applies to: 134-136
121-128: Remove unused prop isOpen or use it for UI stateisOpen is declared but unused. Either remove it to avoid API noise or wire it to classes/transitions.
-interface Props { - messages: Message[] - isOpen: boolean - activeMessageId?: string -} +interface Props { + messages: Message[] + activeMessageId?: string +}
7-11: Add accessible label to the close button (i18n)Provide aria-label/title for the close icon; renderer strings must be localized.
- <Button variant="ghost" size="icon" class="h-6 w-6" @click="$emit('close')"> + <Button + variant="ghost" + size="icon" + class="h-6 w-6" + :aria-label="t('chat.navigation.title')" + :title="t('chat.navigation.title')" + @click="$emit('close')" + >
139-178: Content extraction assumptions may miss non-text blocksgetFullMessageContent ignores assistant blocks not typed as "content" and assumes user content lives in text. If your schema includes code/tool blocks or attachments, consider including their textual previews to improve search coverage.
src/renderer/src/views/ChatTabView.vue (10)
102-108: Lazy-load MessageNavigationSidebar; add type-only import for ChatView instance
- MessageNavigationSidebar is always imported eagerly while all other heavy components are lazy-loaded. Load it async to reduce initial bundle.
- Also import ChatView as a type for stronger typing on the ref.
Apply this diff within the selected lines:
-import MessageNavigationSidebar from '@/components/MessageNavigationSidebar.vue' const ThreadsView = defineAsyncComponent(() => import('@/components/ThreadsView.vue')) const TitleView = defineAsyncComponent(() => import('@/components/TitleView.vue')) const ChatView = defineAsyncComponent(() => import('@/components/ChatView.vue')) const NewThread = defineAsyncComponent(() => import('@/components/NewThread.vue')) +const MessageNavigationSidebar = defineAsyncComponent(() => import('@/components/MessageNavigationSidebar.vue'))Additionally, add a type-only import outside the selected lines to enable accurate typing of the template ref (see a related suggestion on Line 115):
import type ChatView from '@/components/ChatView.vue'
6-8: Keep sidebar width and content spacing in syncYou’re coupling the desktop sidebar width (
w-80) with a separate content margin (mr-80). If one changes, the other can drift. Consider centralizing the width in a single source of truth (CSS var or a small computed that returns both classes) so updates stay consistent.
50-54: v-show + hidden/lg:block is redundant
v-show="... && isLargeScreen"already gates rendering visibility by viewport. The extrahidden lg:blockis unnecessary and makes the state harder to reason about.- class="fixed right-0 top-0 w-80 max-w-80 h-full z-10 hidden lg:block" + class="fixed right-0 top-0 w-80 max-w-80 h-full z-10"
63-85: Duplicate close paths on mobile overlayThe overlay backdrop click closes the nav, and the global
onClickOutsidehandler also tries to close it. Keeping both makes behavior drift-prone. Prefer one path (backdrop click is enough for mobile) and simplify the outside-click handler accordingly.
151-160: Simplify onClickOutside: limit it to the threads sidebarLet the mobile overlay handle closing the message nav. This reduces branching and removes the need to special-case clicks inside
messageNavigationRef.-onClickOutside(sidebarRef, (event) => { - const isClickInMessageNavigation = messageNavigationRef.value?.contains(event.target as Node) - - if (chatStore.isSidebarOpen && !isLargeScreen.value) { - chatStore.isSidebarOpen = false - } - if (chatStore.isMessageNavigationOpen && !isLargeScreen.value && !isClickInMessageNavigation) { - chatStore.isMessageNavigationOpen = false - } -}) +onClickOutside(sidebarRef, () => { + if (chatStore.isSidebarOpen && !isLargeScreen.value) { + chatStore.isSidebarOpen = false + } +})
76-76: Remove messageNavigationRef if not neededIf you adopt the previous simplification,
messageNavigationRefbecomes unused. Drop the template ref and the variable to avoid dead code.- <div ref="messageNavigationRef" class="w-80 max-w-80"> + <div class="w-80 max-w-80">-const messageNavigationRef = ref<HTMLElement>()Also applies to: 148-149
115-115: Type the chatViewRef for safer calls
chatViewRefis untyped; calls likechatViewRef.value.messageList.scrollToMessage(...)won’t be checked. Type the ref to catch regressions at compile time.-const chatViewRef = ref() +const chatViewRef = ref<InstanceType<typeof ChatView> | null>(null)Note: This assumes you add a type-only import:
import type ChatView from '@/components/ChatView.vue'.
If possible, expose a dedicated API from ChatView (e.g.,defineExpose({ scrollToMessage })) so callers don’t reach intomessageList.
216-227: Avoid hard-coded 1s delay; cancel on unmountClosing the nav after a fixed 1s timeout is brittle and can fire after the component is torn down or the panel is re-opened. Either (a) expose a
scrollToMessage(...): Promise<void>from ChatView and close when it resolves, or (b) at minimum, store and clear the timeout on unmount.Minimal mitigation:
- if (!isLargeScreen.value && chatStore.isMessageNavigationOpen) { - setTimeout(() => { - chatStore.isMessageNavigationOpen = false - }, 1000) // 延迟1秒关闭,让用户看到滚动效果 - } + if (!isLargeScreen.value && chatStore.isMessageNavigationOpen) { + if (mobileNavCloseTimer) clearTimeout(mobileNavCloseTimer) + mobileNavCloseTimer = window.setTimeout(() => { + // Guard in case the panel was re-opened + if (chatStore.isMessageNavigationOpen) { + chatStore.isMessageNavigationOpen = false + } + }, 600) // shorter delay still shows motion while feeling snappier + }And outside this block, add:
// at top-level in <script setup> let mobileNavCloseTimer: number | undefined onBeforeUnmount(() => mobileNavCloseTimer && clearTimeout(mobileNavCloseTimer))If you want, I can sketch an exposed
scrollToMessagePromise-based API for ChatView.
56-57: Pass a computed messages reference to avoid churnIf
chatStore.getMessages()creates a new array each call, this will cause needless updates to MessageNavigationSidebar. Cache via a computed and bind that instead.- :messages="chatStore.getMessages()" + :messages="messages"Add this (outside the selected lines):
const messages = computed(() => chatStore.getMessages())Also applies to: 78-79
50-50: Use English for comments (repo guideline)Several new comments are in Chinese. The repo requires English for logs/comments in .vue/.ts files. Please translate these to keep consistency.
- <!-- 消息导航侧边栏 (大屏幕) --> + <!-- Message navigation sidebar (desktop) --> - <!-- 小屏幕模式下的消息导航侧边栏 --> + <!-- Message navigation sidebar (mobile) --> - <!-- 背景遮罩 --> + <!-- Backdrop --> - <!-- 侧边栏 --> + <!-- Sidebar --> -/** - * 处理滚动到指定消息 - */ +/** + * Scroll to target message + */Also applies to: 63-63, 72-72, 75-75, 212-215
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (16)
src/renderer/src/components/ChatView.vue(1 hunks)src/renderer/src/components/MessageNavigationSidebar.vue(1 hunks)src/renderer/src/components/ThreadsView.vue(2 hunks)src/renderer/src/components/TitleView.vue(1 hunks)src/renderer/src/components/message/MessageList.vue(2 hunks)src/renderer/src/i18n/en-US/chat.json(1 hunks)src/renderer/src/i18n/fa-IR/chat.json(1 hunks)src/renderer/src/i18n/fr-FR/chat.json(1 hunks)src/renderer/src/i18n/ja-JP/chat.json(1 hunks)src/renderer/src/i18n/ko-KR/chat.json(1 hunks)src/renderer/src/i18n/ru-RU/chat.json(1 hunks)src/renderer/src/i18n/zh-CN/chat.json(1 hunks)src/renderer/src/i18n/zh-HK/chat.json(1 hunks)src/renderer/src/i18n/zh-TW/chat.json(1 hunks)src/renderer/src/stores/chat.ts(4 hunks)src/renderer/src/views/ChatTabView.vue(6 hunks)
🧰 Additional context used
📓 Path-based instructions (13)
**/*.{ts,tsx,js,jsx,vue}
📄 CodeRabbit inference engine (CLAUDE.md)
Use English for logs and comments
Files:
src/renderer/src/components/TitleView.vuesrc/renderer/src/components/ThreadsView.vuesrc/renderer/src/components/message/MessageList.vuesrc/renderer/src/components/ChatView.vuesrc/renderer/src/components/MessageNavigationSidebar.vuesrc/renderer/src/stores/chat.tssrc/renderer/src/views/ChatTabView.vue
src/renderer/src/**/*.vue
📄 CodeRabbit inference engine (CLAUDE.md)
src/renderer/src/**/*.vue: Use Composition API for all Vue 3 components
Use Tailwind CSS with scoped styles for styling
Organize components by feature in src/renderer/src/
Follow existing component patterns in src/renderer/src/ when creating new UI components
Use Composition API with proper TypeScript typing for new UI components
Implement responsive design with Tailwind CSS for new UI components
Add proper error handling and loading states for new UI componentsUse scoped styles to prevent CSS conflicts between components
Files:
src/renderer/src/components/TitleView.vuesrc/renderer/src/components/ThreadsView.vuesrc/renderer/src/components/message/MessageList.vuesrc/renderer/src/components/ChatView.vuesrc/renderer/src/components/MessageNavigationSidebar.vuesrc/renderer/src/views/ChatTabView.vue
src/renderer/src/**/*.{ts,tsx,vue}
📄 CodeRabbit inference engine (CLAUDE.md)
src/renderer/src/**/*.{ts,tsx,vue}: Use Pinia for frontend state management
Renderer to Main: Use usePresenter.ts composable for direct presenter method calls
Files:
src/renderer/src/components/TitleView.vuesrc/renderer/src/components/ThreadsView.vuesrc/renderer/src/components/message/MessageList.vuesrc/renderer/src/components/ChatView.vuesrc/renderer/src/components/MessageNavigationSidebar.vuesrc/renderer/src/stores/chat.tssrc/renderer/src/views/ChatTabView.vue
src/renderer/src/**/*
📄 CodeRabbit inference engine (.cursor/rules/i18n.mdc)
src/renderer/src/**/*: All user-facing strings must use i18n keys (avoid hardcoded user-visible text in code)
Use the 'vue-i18n' framework for all internationalization in the renderer
Ensure all user-visible text in the renderer uses the translation system
Files:
src/renderer/src/components/TitleView.vuesrc/renderer/src/i18n/fa-IR/chat.jsonsrc/renderer/src/components/ThreadsView.vuesrc/renderer/src/i18n/zh-HK/chat.jsonsrc/renderer/src/i18n/ja-JP/chat.jsonsrc/renderer/src/i18n/zh-TW/chat.jsonsrc/renderer/src/i18n/en-US/chat.jsonsrc/renderer/src/i18n/ru-RU/chat.jsonsrc/renderer/src/i18n/zh-CN/chat.jsonsrc/renderer/src/i18n/ko-KR/chat.jsonsrc/renderer/src/components/message/MessageList.vuesrc/renderer/src/components/ChatView.vuesrc/renderer/src/components/MessageNavigationSidebar.vuesrc/renderer/src/i18n/fr-FR/chat.jsonsrc/renderer/src/stores/chat.tssrc/renderer/src/views/ChatTabView.vue
src/renderer/**/*.{vue,ts,js,tsx,jsx}
📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)
渲染进程代码放在
src/renderer
Files:
src/renderer/src/components/TitleView.vuesrc/renderer/src/components/ThreadsView.vuesrc/renderer/src/components/message/MessageList.vuesrc/renderer/src/components/ChatView.vuesrc/renderer/src/components/MessageNavigationSidebar.vuesrc/renderer/src/stores/chat.tssrc/renderer/src/views/ChatTabView.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
Implement proper state management with Pinia
Utilize Vue Router for navigation and route management
Leverage Vue's built-in reactivity system for efficient data handling
Files:
src/renderer/src/components/TitleView.vuesrc/renderer/src/components/ThreadsView.vuesrc/renderer/src/components/message/MessageList.vuesrc/renderer/src/components/ChatView.vuesrc/renderer/src/components/MessageNavigationSidebar.vuesrc/renderer/src/stores/chat.tssrc/renderer/src/views/ChatTabView.vue
src/renderer/**/*.{ts,tsx,vue}
📄 CodeRabbit inference engine (.cursor/rules/vue-shadcn.mdc)
src/renderer/**/*.{ts,tsx,vue}: Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
Use TypeScript for all code; prefer types over interfaces.
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.
Files:
src/renderer/src/components/TitleView.vuesrc/renderer/src/components/ThreadsView.vuesrc/renderer/src/components/message/MessageList.vuesrc/renderer/src/components/ChatView.vuesrc/renderer/src/components/MessageNavigationSidebar.vuesrc/renderer/src/stores/chat.tssrc/renderer/src/views/ChatTabView.vue
src/renderer/**/*.{vue,ts}
📄 CodeRabbit inference engine (.cursor/rules/vue-shadcn.mdc)
Implement lazy loading for routes and components.
Files:
src/renderer/src/components/TitleView.vuesrc/renderer/src/components/ThreadsView.vuesrc/renderer/src/components/message/MessageList.vuesrc/renderer/src/components/ChatView.vuesrc/renderer/src/components/MessageNavigationSidebar.vuesrc/renderer/src/stores/chat.tssrc/renderer/src/views/ChatTabView.vue
src/renderer/**/*.{ts,vue}
📄 CodeRabbit inference engine (.cursor/rules/vue-shadcn.mdc)
src/renderer/**/*.{ts,vue}: Use useFetch and useAsyncData for data fetching.
Implement SEO best practices using Nuxt's useHead and useSeoMeta.
Files:
src/renderer/src/components/TitleView.vuesrc/renderer/src/components/ThreadsView.vuesrc/renderer/src/components/message/MessageList.vuesrc/renderer/src/components/ChatView.vuesrc/renderer/src/components/MessageNavigationSidebar.vuesrc/renderer/src/stores/chat.tssrc/renderer/src/views/ChatTabView.vue
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Strict type checking enabled for TypeScript
**/*.{ts,tsx}: 始终使用 try-catch 处理可能的错误
提供有意义的错误信息
记录详细的错误日志
优雅降级处理
日志应包含时间戳、日志级别、错误代码、错误描述、堆栈跟踪(如适用)、相关上下文信息
日志级别应包括 ERROR、WARN、INFO、DEBUG
不要吞掉错误
提供用户友好的错误信息
实现错误重试机制
避免记录敏感信息
使用结构化日志
设置适当的日志级别
Files:
src/renderer/src/stores/chat.ts
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/development-setup.mdc)
**/*.{js,jsx,ts,tsx}: 使用 OxLint 进行代码检查
Log和注释使用英文书写
Files:
src/renderer/src/stores/chat.ts
src/{main,renderer}/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/electron-best-practices.mdc)
src/{main,renderer}/**/*.ts: Use context isolation for improved security
Implement proper inter-process communication (IPC) patterns
Optimize application startup time with lazy loading
Implement proper error handling and logging for debugging
Files:
src/renderer/src/stores/chat.ts
src/renderer/src/stores/**/*.{vue,ts,tsx,js,jsx}
📄 CodeRabbit inference engine (.cursor/rules/pinia-best-practices.mdc)
src/renderer/src/stores/**/*.{vue,ts,tsx,js,jsx}: Use modules to organize related state and actions
Implement proper state persistence for maintaining data across sessions
Use getters for computed state properties
Utilize actions for side effects and asynchronous operations
Keep the store focused on global state, not component-specific data
Files:
src/renderer/src/stores/chat.ts
🧠 Learnings (1)
📚 Learning: 2025-07-21T01:45:54.229Z
Learnt from: CR
PR: ThinkInAIXYZ/deepchat#0
File: .cursor/rules/electron-best-practices.mdc:0-0
Timestamp: 2025-07-21T01:45:54.229Z
Learning: Applies to src/main/presenter/index.ts : The IPC messages from the main process to notify the view mainly rely on the EventBus index.ts to listen for events that need to be notified and then send them to the renderer through the mainWindow
Applied to files:
src/renderer/src/components/ChatView.vue
🔇 Additional comments (10)
src/renderer/src/i18n/ko-KR/chat.json (1)
57-68: All chat.navigation keys verified across locales — resolved
Allchat.jsonfiles undersrc/renderer/src/i18ncontain the full set of navigation keys (title,searchPlaceholder,noResults,noMessages,totalMessages,searchResults,userMessage,assistantMessage,unknownMessage). No discrepancies were found, so the navigation block is consistent across all locales.src/renderer/src/i18n/zh-HK/chat.json (1)
57-68: Cantonese translations read well; placeholders consistentGood addition. Same request as other locales: ensure key parity across all chat.json files to prevent fallback to en-US at runtime.
Use the verification script shared in ko-KR comment to validate this file too. It will flag any missing/mismatched keys.
src/renderer/src/i18n/fa-IR/chat.json (1)
57-68: Farsi navigation strings added correctly; RTL-friendly phrasingLooks good. Confirm parity with other locales to avoid missing translations.
Use the same shell script from the ko-KR comment; it validates presence and exact key set for chat.navigation across locales.
src/renderer/src/i18n/zh-CN/chat.json (1)
58-67: LGTM: added navigation keys are complete and consistentThe new navigation block is structurally sound, placeholders look correct ({count}, {total}), and the tone fits the locale.
src/renderer/src/i18n/en-US/chat.json (1)
58-67: Navigation keys consistent across localesVerified via the provided script that every
chat.jsonundersrc/renderer/src/i18n/**/contains the exact set of navigation keys:
assistantMessage, noMessages, noResults, searchPlaceholder, searchResults, title, totalMessages, unknownMessage, userMessage. No discrepancies were detected—ready to merge.src/renderer/src/i18n/ja-JP/chat.json (1)
58-67: LGTM: natural Japanese phrasing and correct placeholdersThe navigation block reads well; counts are expressed idiomatically with 件 and ordering fits Japanese UI norms.
src/renderer/src/i18n/fr-FR/chat.json (2)
58-67: LGTM: navigation i18n block is well-structured and consistentKeys and placeholders look correct and align with the new UI.
58-67: All chat.json navigation keys are consistent across locales
The cross-locale verification script confirmed that everynavigation.*key insrc/renderer/src/i18n/en-US/chat.jsonis present (and no extras) in all otheri18n/*/chat.jsonfiles. No further action required.src/renderer/src/i18n/zh-TW/chat.json (1)
58-67: LGTM: 新增的 navigation 區塊鍵值與佈局合理字詞與語意清楚,佔位符使用正確。
src/renderer/src/components/MessageNavigationSidebar.vue (1)
68-71: Fieldsmodel_providerandtimestampmatch the sharedMessagetypeI’ve confirmed in
src/shared/chat.d.tsthat theMessagetype defines both:
model_provider: stringtimestamp: numberThese align exactly with how they’re used in
MessageNavigationSidebar.vue, so no renaming or mapping is needed.Likely an incorrect or invalid review comment.
| <style scoped> | ||
| .message-highlight { | ||
| background-color: rgba(59, 130, 246, 0.1); | ||
| border-left: 3px solid rgb(59, 130, 246); | ||
| transition: all 0.3s ease; | ||
| } | ||
| .dark .message-highlight { | ||
| background-color: rgba(59, 130, 246, 0.15); | ||
| } | ||
| </style> |
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.
Scoped style won’t reach child component DOM; use :deep to apply highlight
Because this component’s styles are scoped, .message-highlight won’t match elements rendered inside MessageItemUser/Assistant. Use :deep() (scoped piercing) and optionally scope under the container to avoid global bleed.
-<style scoped>
-.message-highlight {
- background-color: rgba(59, 130, 246, 0.1);
- border-left: 3px solid rgb(59, 130, 246);
- transition: all 0.3s ease;
-}
-
-.dark .message-highlight {
- background-color: rgba(59, 130, 246, 0.15);
-}
-</style>
+<style scoped>
+.message-list-container :deep(.message-highlight) {
+ background-color: rgba(59, 130, 246, 0.1);
+ border-left: 3px solid rgb(59, 130, 246);
+ transition: all 0.3s ease;
+}
+.dark .message-list-container :deep(.message-highlight) {
+ background-color: rgba(59, 130, 246, 0.15);
+}
+</style>📝 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.
| <style scoped> | |
| .message-highlight { | |
| background-color: rgba(59, 130, 246, 0.1); | |
| border-left: 3px solid rgb(59, 130, 246); | |
| transition: all 0.3s ease; | |
| } | |
| .dark .message-highlight { | |
| background-color: rgba(59, 130, 246, 0.15); | |
| } | |
| </style> | |
| <style scoped> | |
| .message-list-container :deep(.message-highlight) { | |
| background-color: rgba(59, 130, 246, 0.1); | |
| border-left: 3px solid rgb(59, 130, 246); | |
| transition: all 0.3s ease; | |
| } | |
| .dark .message-list-container :deep(.message-highlight) { | |
| background-color: rgba(59, 130, 246, 0.15); | |
| } | |
| </style> |
🤖 Prompt for AI Agents
In src/renderer/src/components/message/MessageList.vue around lines 398 to 408,
the scoped CSS selectors (.message-highlight and .dark .message-highlight) won't
match elements rendered inside child components (MessageItemUser/Assistant);
update the selectors to use Vue's scoped piercing syntax (e.g.
:deep(.message-highlight)) or scope it under the parent container (e.g.
.message-list :deep(.message-highlight)) and mirror the same change for the dark
variant so the highlight styles apply to child component DOM without leaking
globally.
| <div class="text-sm text-foreground/80 line-clamp-2"> | ||
| <span v-html="highlightSearchQuery(getMessagePreview(message))"></span> | ||
| </div> |
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.
XSS risk: v-html on untrusted message text
Using v-html with user/assistant content enables HTML injection. Replace string-based highlighting with node-safe rendering (no v-html).
Apply this diff to render safely:
- <div class="text-sm text-foreground/80 line-clamp-2">
- <span v-html="highlightSearchQuery(getMessagePreview(message))"></span>
- </div>
+ <div class="text-sm text-foreground/80 line-clamp-2">
+ <span>
+ <template
+ v-for="(seg, i) in getHighlightedSegments(getMessagePreview(message))"
+ :key="i"
+ >
+ <mark
+ v-if="seg.m"
+ class="bg-yellow-200 dark:bg-yellow-800 px-1 rounded"
+ >{{ seg.t }}</mark
+ >
+ <span v-else>{{ seg.t }}</span>
+ </template>
+ </span>
+ </div>And replace the highlighter function near Lines 217-229:
-// 高亮搜索关键词
-const highlightSearchQuery = (text: string): string => {
- if (!searchQuery.value.trim()) {
- return text
- }
-
- const query = searchQuery.value.trim()
- const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi')
- return text.replace(
- regex,
- '<mark class="bg-yellow-200 dark:bg-yellow-800 px-1 rounded">$1</mark>'
- )
-}
+// 安全高亮(不使用 v-html)
+const getHighlightedSegments = (
+ text: string
+): Array<{ t: string; m: boolean }> => {
+ const q = searchQuery.value.trim()
+ if (!q) return [{ t: text, m: false }]
+ const escaped = q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+ const regex = new RegExp(`(${escaped})`, 'gi')
+ const parts = text.split(regex).filter(Boolean)
+ const qLower = q.toLowerCase()
+ return parts.map((p) => ({ t: p, m: p.toLowerCase() === qLower }))
+}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/renderer/src/components/MessageNavigationSidebar.vue around lines 86-88,
remove the v-html usage to eliminate XSS risk and instead render highlighted
text as safe DOM nodes: change the template to iterate over an array of text
segments (using v-for) and render each segment with v-text or plain text binding
and conditional classes for highlights rather than using v-html; update the
highlighter function referenced near lines 217-229 to return a safe array of
{text: string, match: boolean} segments (no HTML), splitting the preview on the
search query in a case-insensitive manner and preserving the original text, and
ensure any input is treated as plain text (no innerHTML, no sanitization library
necessary if you never inject HTML). Also update any computed/props usage so the
template consumes the segments array (e.g.,
highlightSearchQuery(getMessagePreview(message)) returns segments) and adjust
types accordingly.
| <!-- 消息导航侧边栏 (大屏幕) --> | ||
| <div | ||
| v-show="chatStore.isMessageNavigationOpen && isLargeScreen" | ||
| class="fixed right-0 top-0 w-80 max-w-80 h-full z-10 hidden lg:block" |
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.
💡 Verification agent
🧩 Analysis chain
Verify Tailwind max-w-60/max-w-80 availability
max-w-60/max-w-80 aren’t in Tailwind’s default maxWidth scale. If you haven’t extended theme.maxWidth to include spacing values, these classes won’t work.
Run this to verify your Tailwind config and usages:
Also applies to: 76-76
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find Tailwind config
fd -a 'tailwind.config.*' | sed -n '1p' | xargs -I{} sh -c 'echo "==> {}"; cat "{}"'
echo
echo "==> Searching for max-w-60/max-w-80 usages and potential custom definitions"
rg -n --no-heading -C1 'max-w-(60|80)' || true
rg -n --no-heading -C2 'maxWidth|spacing' tailwind.config.* || trueLength of output: 5389
Add custom maxWidth entries for max-w-60/max-w-80 in Tailwind config
Your Tailwind setup does not include max-w-60 or max-w-80 in its theme.maxWidth scale, so those classes won’t generate any CSS. You have two options: add these keys to your config or switch to default max-width utilities.
• In tailwind.config.js, under theme.extend, add:
module.exports = {
// …
theme: {
extend: {
maxWidth: {
'60': '15rem', // for max-w-60
'80': '20rem', // for max-w-80
},
// …other extensions
}
},
// …
}• Or replace max-w-60/max-w-80 in your components with built-ins (e.g., max-w-7xl, max-w-xl, max-w-screen-md, etc.).
Affected usages in ChatTabView.vue (update these or your config accordingly):
- Line 26:
w-60 max-w-60 h-full fixed z-20 lg:relative - Line 53:
class="fixed right-0 top-0 w-80 max-w-80 h-full z-10 hidden lg:block" - Line 76:
<div ref="messageNavigationRef" class="w-80 max-w-80">
Please add the custom maxWidth entries or swap to default classes so your layouts behave as expected.
🤖 Prompt for AI Agents
In src/renderer/src/views/ChatTabView.vue around lines 26, 53 and 76, the
classes use non-existent Tailwind utilities `max-w-60` and `max-w-80` so no CSS
is generated; either add matching entries to your Tailwind config under
theme.extend.maxWidth (`'60': '15rem'` and `'80': '20rem'`) or replace those
classes in this file with built-in max-w utilities (e.g., `max-w-xl`,
`max-w-screen-md`, or another existing scale) so the width constraints take
effect.
|
LGTM |
add message navigation sidebar with search functionality

Summary by CodeRabbit
New Features
Chores