Conversation
Add agent icon sidebar with liquid drop styling and session list with date-grouped mock data. Main content area gets rounded corner and shadow separation. Sidebar/appbar use semi-transparent background.
Move browser and settings buttons from AppBar to WindowSideBar bottom icon column. Remove history button, ThreadView overlay, and all associated toggle/event handling code.
Add a pure visual mock for the redesigned new thread page using shadcn primitives. Includes centered logo/heading, textarea input with attach/mic/send toolbar, and a status bar with model, effort, and permissions selectors.
…rouping toggle Refactor NewThreadMock into reusable mock components (MockInputBox, MockInputToolbar, MockStatusBar). Add MockChatPage with sticky top bar and input area, MockMessageList with sample conversation, and shared mock view state composable. Wire sidebar session clicks to switch between new thread and chat views. Add project grouping toggle to sidebar header.
…at top bar Add a dropdown project/workdir selector to NewThreadMock with recent projects and an open folder option. Thread project directory through mock view state so MockTopBar displays a folder breadcrumb prefix alongside the session title.
…apse - Add MockWelcomePage with provider grid and ACP agent setup option - Add DeepChat as default first agent in sidebar - Add empty state for session list - Add sidebar collapse/expand with macOS traffic light accommodation - Add debug toggle button for welcome page
* docs: add spec for default model setting * feat: #1174 support default models setting * fix(settings): sync model selections with ACP mode and improve error handling - Add ACP mode check when applying default model in NewThread.vue - Add error handling in DefaultModelSettingsSection.vue syncModelSelections - Update imageModel i18n keys to remove 'select' verb for display-only fields
* fix(settings): disable auto-fallback in default model selection Remove automatic model fallback in DefaultModelSettingsSection to prevent unintended model changes when providers load late. Models now only change on explicit user selection. * fix: format
…ideBar Remove multi-tab/multi-window complexity from presenters, simplify shell UI by removing SideBar component and streamlining AppBar, and clean up unused event handlers and shortcut bindings.
… and sidebar integration Implement Phases 1-4 of the new UI architecture: - Phase 1: Add 4 Pinia stores (pageRouter, session, agent, project) with IPC mapping to presenters, event listeners, and error handling - Phase 2: Create 3 page components (WelcomePage, NewThreadPage, ChatPage) and refactor ChatTabView to route via pageRouter store - Phase 3: Create 5 chat components (ChatTopBar, MessageList, ChatInputBox, ChatInputToolbar, ChatStatusBar) matching mock designs exactly - Phase 4: Refactor WindowSideBar to use store data instead of mock data - Add specs and implementation plan documentation
The agentPresenter.sendMessage expects content as JSON-encoded
UserMessageContent ({text, files, links, search, think}), not raw
text. Raw text caused JSON.parse failures in MessageManager and
TypeError when setting properties on non-object content.
ChatPage now uses useChatStore().variantAwareMessages for the message list and chatStore.sendMessage() for sending. MessageList component updated to handle real Message types (UserMessageContent for user messages, AssistantMessageBlock[] for assistant messages) instead of plain text placeholders.
…hase 6) - NewThreadPage passes agentStore.selectedAgentId to session creation, with providerId/modelId set for ACP agents - Session creation uses forceNewAndActivate to prevent backend from reusing empty conversations with different agent settings - ChatStatusBar wired to chatStore.chatConfig and modelStore for real model/effort selection; shows selected agent context on NewThreadPage - Effort selector hidden for ACP agents (not applicable) - MessageList rewritten to use existing MessageItemAssistant and MessageItemUser components for full markdown/code/tool rendering - modelStore.initialize() added to ChatTabView onMounted - Phase 6 specs and todo tracking added
Implement agent-centric architecture replacing old sessionPresenter pattern: - Shared types: IAgentImplementation, Agent, Session, ChatMessageRecord, etc. - DB tables: new_sessions, new_projects, deepchat_sessions, deepchat_messages - DeepChatAgentPresenter: message persistence, LLM coreStream consumption, batched stream flushing (120ms renderer, 600ms DB), crash recovery - NewAgentPresenter: agent registry, session manager, message routing - ProjectPresenter: project CRUD with directory picker - Renderer stores: session, message, agent, project, draft (all new arch) - ChatPage wired to new message store with streaming block display - NewThreadPage resolves model via defaultModel/preferredModel/first enabled - 94 unit + integration tests across 9 test files - Debug logging throughout processing pipeline
- Add contextBuilder with system prompt injection, conversation history assembly, and token-based truncation using approximateTokenSize - Wire buildContext into processMessage, building context before DB persist to avoid duplicate user messages in LLM calls - Add optimistic user message in renderer message store so user messages appear immediately without waiting for stream completion - Add auto-scroll to ChatPage: scroll to bottom on load, on new messages, and during streaming; respect user scroll-up to stop auto-following - Update v0 tests and add new tests for context builder and multi-turn integration (110 tests passing across 10 files)
Add tool calling support to the DeepChat agent via an agent loop that passes tool definitions to coreStream, accumulates tool calls from the stream, executes them via ToolPresenter.callTool(), and re-calls the LLM until it stops requesting tools. Includes block accumulation across loop iterations, server info enrichment, grouped truncation of tool messages in context builder, and proper abort/error path delegation between streamHandler and agentLoop.
DeepSeek Reasoner (and similar models like kimi-k2-thinking, glm-4.7) requires a reasoning_content field on assistant messages that contain tool_calls. Without it, the API returns a 400 error. Extract reasoning blocks separately in both the agent loop and context builder, and include them on the assistant message when the model requires it.
For interleaved thinking models (DeepSeek Reasoner, kimi-k2-thinking), reasoning_content is only required on assistant messages in the current agent loop exchange (after the last user message), not on historical messages from the DB. Also fix content field to always be a string (not undefined) on assistant messages with tool_calls, matching the existing agentLoopHandler behavior.
Replace tangled streamHandler.ts and agentLoop.ts with a clean 5-module architecture: types.ts (shared state), accumulator.ts (pure event→block mutations), echo.ts (batched renderer/DB flushing), dispatch.ts (tool execution and finalization), and process.ts (unified loop). Eliminates tools-vs-no-tools branching in index.ts — single processStream() call handles both simple completions and multi-turn tool calling. Extracts reusable trailing-edge throttle utility to shared/utils/throttle.ts. Pure refactor with no behavior changes.
New agent sessions live in deepchat_sessions, not the legacy conversations table. Passing the session ID as conversationId caused SkillPresenter.getActiveSkills to throw "Conversation not found". Drop the conversationId param since the new agent doesn't use skills.
Mark v2 spec as complete and superseded by v3 module structure. Update v3 spec to match final implementation. Clean up tasks to remove references to deleted files and add v2/v3 task sections with all items checked off.
📝 WalkthroughWalkthroughComprehensive architectural overhaul introducing default model settings, a new multi-page UI system with routing, complete Agent Architecture v0-v3 with multi-turn chat and MCP tool integration, modular stream processing, SQLite schema extensions, removal of legacy tab-switching logic, and extensive internationalization and test coverage. Changes
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120+ minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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.
Actionable comments posted: 2
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
src/renderer/src/components/ModelSelect.vue (1)
72-85: 🛠️ Refactor suggestion | 🟠 MajorUse a typed props interface + withDefaults for defaults.
This file now changes the props block, but it still uses an object literal for
defineProps. The guideline requires TypeScript interfaces for props. Consider switching to an interface withdefineProps<T>()andwithDefaultsfor default values.✅ Suggested refactor
-const props = defineProps({ - type: { - type: Array as PropType<ModelType[]>, - default: undefined // ← explicit for clarity - }, - excludeProviders: { - type: Array as PropType<string[]>, - default: () => [] - }, - visionOnly: { - type: Boolean, - default: false - } -}) +interface ModelSelectProps { + type?: ModelType[] + excludeProviders?: string[] + visionOnly?: boolean +} + +const props = withDefaults(defineProps<ModelSelectProps>(), { + excludeProviders: () => [], + visionOnly: false +})As per coding guidelines, "Define TypeScript interfaces for Vue component props and data structures."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/src/components/ModelSelect.vue` around lines 72 - 85, Replace the object-literal props with a typed props interface and use withDefaults + defineProps to provide defaults: create an interface (e.g., Props) declaring type?: ModelType[], excludeProviders?: string[], visionOnly?: boolean; then call defineProps<Props>() and wrap it with withDefaults to set type to undefined, excludeProviders to () => [], and visionOnly to false, updating any references to the existing props variable (const props) accordingly.src/main/presenter/shortcutPresenter.ts (1)
154-210:⚠️ Potential issue | 🟠 MajorRegistered-but-no-op shortcuts still consume OS-level keybindings.
The four tab-switch methods (lines 200–210) are correctly gutted, but the
globalShortcut.registercall-sites at lines 154–193 are still active. Electron'sglobalShortcutmodule registers shortcuts with the operating system, and they work even when the app does not have keyboard focus. Registering a shortcut with an empty callback means the key combination is captured and silently swallowed — Electron will override shortcuts of other applications; if you try to use this shortcut while Chrome or another app is open, your Electron application will override it.Concretely:
Ctrl+Tab,Ctrl+Shift+Tab,Ctrl+1–Ctrl+8, andCtrl+9will cease to work in any other application while DeepChat is running, and they'll do nothing inside DeepChat either.Fix: Remove the now-purposeless registration blocks entirely:
🔧 Proposed fix — drop the four no-op registration blocks
- // Command+Tab 或 Ctrl+Tab 切换到下一个标签页 - if (this.shortcutKeys.SwitchNextTab) { - globalShortcut.register(this.shortcutKeys.SwitchNextTab, () => { - const focusedWindow = presenter.windowPresenter.getFocusedWindow() - if (focusedWindow?.isFocused()) { - this.switchToNextTab(focusedWindow.id) - } - }) - } - - // Ctrl+Shift+Tab 切换到上一个标签页 - if (this.shortcutKeys.SwitchPrevTab) { - globalShortcut.register(this.shortcutKeys.SwitchPrevTab, () => { - const focusedWindow = presenter.windowPresenter.getFocusedWindow() - if (focusedWindow?.isFocused()) { - this.switchToPreviousTab(focusedWindow.id) - } - }) - } - - // 注册标签页数字快捷键 (1-8) - if (this.shortcutKeys.NumberTabs) { - for (let i = 1; i <= 8; i++) { - globalShortcut.register(`${CommandKey}+${i}`, () => { - const focusedWindow = presenter.windowPresenter.getFocusedWindow() - if (focusedWindow?.isFocused()) { - this.switchToTabByIndex(focusedWindow.id, i - 1) - } - }) - } - } - - // Command+9 或 Ctrl+9 切换到最后一个标签页 - if (this.shortcutKeys.SwtichToLastTab) { - globalShortcut.register(this.shortcutKeys.SwtichToLastTab, () => { - const focusedWindow = presenter.windowPresenter.getFocusedWindow() - if (focusedWindow?.isFocused()) { - this.switchToLastTab(focusedWindow.id) - } - }) - } - this.showHideWindow()If the shortcuts are intended to be re-wired to the new page-routing system in a follow-up, consider leaving a
// TODOcomment explaining the intent rather than silently capturing and discarding the keystrokes.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/presenter/shortcutPresenter.ts` around lines 154 - 210, The globalShortcut.register calls for shortcutKeys.SwitchNextTab, SwitchPrevTab, NumberTabs and SwtichToLastTab are still registering OS-level shortcuts while the corresponding handlers (switchToNextTab, switchToPreviousTab, switchToTabByIndex, switchToLastTab) are no-ops; remove those registration blocks (the globalShortcut.register(…) usages tied to this.shortcutKeys.SwitchNextTab, .SwitchPrevTab, .NumberTabs, and .SwtichToLastTab) so the app stops capturing Ctrl/Cmd+Tab, Ctrl/Cmd+Shift+Tab, Ctrl/Cmd+1–9, etc.; if you plan to rewire them later, replace each removed block with a brief TODO comment referencing the handler names (switchToNextTab, switchToPreviousTab, switchToTabByIndex, switchToLastTab) so future work knows where to re-add registrations.src/main/presenter/sessionPresenter/managers/conversationManager.ts (1)
177-201:⚠️ Potential issue | 🔴 Critical
DEFAULT_SETTINGSis mutated in-place — corrupts state across all subsequent calls.At line 177,
defaultSettingsis assigned by reference toDEFAULT_SETTINGS. When!latestConversation?.settingsis true (precisely the condition required forshouldApplyDefaultModelto betrue), the block at lines 178-186 is skipped, sodefaultSettingsnever becomes a copy — it remains the same object reference. Lines 198-199 then directly write toDEFAULT_SETTINGS.modelIdandDEFAULT_SETTINGS.providerId, permanently polluting the module-level constant for the remainder of the process lifetime. Every subsequent call tocreateConversationthat lacks alatestConversation?.settingswill inherit the wrong model.🐛 Proposed fix — shallow-copy DEFAULT_SETTINGS at the declaration site
- let defaultSettings = DEFAULT_SETTINGS + let defaultSettings = { ...DEFAULT_SETTINGS } if (latestConversation?.settings) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/presenter/sessionPresenter/managers/conversationManager.ts` around lines 177 - 201, defaultSettings is being assigned by reference to DEFAULT_SETTINGS so later writes (e.g., setting modelId/providerId) mutate the module-level constant; change the initialization to create a shallow copy (e.g., use an object spread) when declaring defaultSettings so mutations only affect the local variable, i.e., replace the direct assignment to DEFAULT_SETTINGS with a shallow-copy of DEFAULT_SETTINGS before any modifications in the createConversation flow (references: DEFAULT_SETTINGS and the local variable defaultSettings in conversationManager.ts).src/main/presenter/mcpPresenter/inMemoryServers/imageServer.ts (2)
52-88:⚠️ Potential issue | 🟠 Major
getEffectiveModel()'s live-lookup and error-throw branches are dead code due to constructor hard-coding.Because the constructor unconditionally assigns
this.providerandthis.model— falling back to'openai'/'gpt-4o'when nothing else is configured — both fields are always truthy after construction.getEffectiveModel()therefore always returns the first branch and never reaches thegetDefaultVisionModel()re-fetch or thethrow. Two consequences:
- If the user updates the default vision model while the server is running, the change is not picked up (stale values baked in at construction).
- If no vision model is configured at all, the server silently uses
openai/gpt-4oinstead of surfacing the "no vision model configured" error thatgetEffectiveModel()is supposed to throw.🐛 Proposed fix — remove hard defaults from constructor, rely on `getEffectiveModel()` for live resolution
- private provider: string - private model: string + private provider: string | undefined + private model: string | undefined constructor(provider?: string, model?: string) { - const defaultVisionModel = presenter.configPresenter.getDefaultVisionModel() - this.provider = provider || defaultVisionModel?.providerId || 'openai' - this.model = model || defaultVisionModel?.modelId || 'gpt-4o' + this.provider = provider + this.model = model this.server = new Server(
getEffectiveModel()already handles all resolution paths correctly whenthis.provider/this.modelcan beundefined.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/presenter/mcpPresenter/inMemoryServers/imageServer.ts` around lines 52 - 88, The constructor currently hard-sets this.provider and this.model to defaults, which prevents getEffectiveModel() from performing live lookups or throwing when no model is configured; change the constructor to assign only the incoming parameters (this.provider = provider; this.model = model) so they can be undefined, rely on getEffectiveModel() everywhere that needs the resolved model (call getEffectiveModel() in request handlers or methods that use provider/model), and keep the existing error-throw and default-resolution logic inside getEffectiveModel() intact so updates to presenter.configPresenter.getDefaultVisionModel() are picked up at runtime.
122-122:⚠️ Potential issue | 🟠 MajorMIME type hardcoded to
image/jpegfor all images.Both
queryImageWithModelandocrImageWithModelconstructdata:image/jpeg;base64,...regardless of the file's actual extension. Sending a PNG or WEBP file asimage/jpegcan cause the model to misparse or reject it.🐛 Proposed fix using extension-based lookup
+ const ext = path.extname(filePath).toLowerCase().slice(1) + const mimeMap: Record<string, string> = { + jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', + gif: 'image/gif', webp: 'image/webp', bmp: 'image/bmp' + } + const mime = mimeMap[ext] ?? 'image/jpeg' - const dataUrl = `data:image/jpeg;base64,${base64Image}` + const dataUrl = `data:${mime};base64,${base64Image}`Apply the same fix to the equivalent block in
ocrImageWithModel(Line 168).Also applies to: 168-168
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/presenter/mcpPresenter/inMemoryServers/imageServer.ts` at line 122, The data URL is hardcoded to image/jpeg which breaks non-JPEG inputs; update both queryImageWithModel and ocrImageWithModel so the MIME type is derived from the file extension (or actual buffer detection) and used when building the dataUrl (the line creating `dataUrl = \`data:image/...;base64,${base64Image}\``). Locate the `dataUrl` and `base64Image` usage in both functions, map extensions (e.g., .png → image/png, .webp → image/webp, .gif → image/gif) or use a mime lookup utility, and replace the constant "image/jpeg" with the resolved MIME type; ensure fallback to image/octet-stream or image/jpeg if resolution fails and apply the same change at the analogous line in `ocrImageWithModel`.src/main/presenter/index.ts (1)
350-362:⚠️ Potential issue | 🟠 MajorAdd cleanup for
newAgentPresenterindestroy().
newAgentPresenterholdsDeepChatAgentPresenter(which maintainsabortControllersandruntimeStatemaps) and manages sessions/messages. These resources are never cleaned up on app shutdown, causing potential leaks of active abort controllers and in-memory state.Add
this.newAgentPresenter.destroy()to the destroy method (after implementing the method inNewAgentPresenterandDeepChatAgentPresenterto clean up their internal resources).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/presenter/index.ts` around lines 350 - 362, The destroy() in presenter index omits cleanup for newAgentPresenter which leaves DeepChatAgentPresenter's abortControllers and runtimeState maps and session/message state leaking; implement a destroy method on NewAgentPresenter (and ensure DeepChatAgentPresenter exposes/cleans its internal abortControllers and runtimeState maps) to abort any pending controllers, clear maps and session/message state, then add this.newAgentPresenter.destroy() into the index's destroy() sequence (place it alongside other presenters' destroy calls) so all agent-related resources are released on shutdown.
♻️ Duplicate comments (5)
src/renderer/src/i18n/da-DK/settings.json (1)
62-67: Duplicate: locale completeness for new settings keys.Same concern as in
src/renderer/src/i18n/fa-IR/settings.jsonLine 62-67—please ensure the new keys exist across all locales and stay consistent.Based on learnings: Add new translations to ALL language files (da-DK, en-US, fa-IR, fr-FR, he-IL, ja-JP, ko-KR, pt-BR, ru-RU, zh-CN, zh-HK, zh-TW) with consistent key names across all locales.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/src/i18n/da-DK/settings.json` around lines 62 - 67, The new i18n keys ("fileMaxSizeHint" and the nested "defaultModel" keys: "title", "chatModel", "visionModel") were added to da-DK but are missing or inconsistent across locales; update every locale file (en-US, fa-IR, fr-FR, he-IL, ja-JP, ko-KR, pt-BR, ru-RU, zh-CN, zh-HK, zh-TW and da-DK) to include these exact keys with appropriate translations so all locale JSONs have the same key structure and names ("fileMaxSizeHint", "defaultModel.title", "defaultModel.chatModel", "defaultModel.visionModel").src/renderer/src/i18n/pt-BR/settings.json (1)
62-67: Duplicate: locale completeness for new settings keys.Same concern as in
src/renderer/src/i18n/fa-IR/settings.jsonLine 62-67—please ensure the new keys exist across all locales and stay consistent.Based on learnings: Add new translations to ALL language files (da-DK, en-US, fa-IR, fr-FR, he-IL, ja-JP, ko-KR, pt-BR, ru-RU, zh-CN, zh-HK, zh-TW) with consistent key names across all locales.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/src/i18n/pt-BR/settings.json` around lines 62 - 67, Add the new translation keys introduced here—fileMaxSizeHint and the nested defaultModel keys (title, chatModel, visionModel)—to every locale file so key names are consistent across all locales (da-DK, en-US, fa-IR, fr-FR, he-IL, ja-JP, ko-KR, pt-BR, ru-RU, zh-CN, zh-HK, zh-TW); find the same settings JSON structure in each locale and add equivalent translated strings for "fileMaxSizeHint" and defaultModel.{title,chatModel,visionModel} to match the key names used in this diff.src/renderer/src/i18n/zh-TW/settings.json (1)
62-67: Duplicate: locale completeness for new settings keys.Same concern as in
src/renderer/src/i18n/fa-IR/settings.jsonLine 62-67—please ensure the new keys exist across all locales and stay consistent.Based on learnings: Add new translations to ALL language files (da-DK, en-US, fa-IR, fr-FR, he-IL, ja-JP, ko-KR, pt-BR, ru-RU, zh-CN, zh-HK, zh-TW) with consistent key names across all locales.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/src/i18n/zh-TW/settings.json` around lines 62 - 67, The new translation keys fileMaxSizeHint and the defaultModel object (with nested keys title, chatModel, visionModel) are present in zh-TW but missing or inconsistent across other locales; update all locale settings.json files (da-DK, en-US, fa-IR, fr-FR, he-IL, ja-JP, ko-KR, pt-BR, ru-RU, zh-CN, zh-HK) to include these exact keys with appropriate translations so key names remain consistent across all locale files.src/renderer/src/components/icons/MinimizeIcon.vue (1)
1-19: Same Iconify lucide guideline concern asRestoreIcon.vue.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/src/components/icons/MinimizeIcon.vue` around lines 1 - 19, The MinimizeIcon.vue SVG does not follow the Iconify/lucide accessibility/consistency guideline (same issue as RestoreIcon.vue); update the <svg> in MinimizeIcon.vue to match the standardized icon pattern used in RestoreIcon.vue by adding the required attributes (e.g., role="img", focusable="false", aria-hidden="true", keep xmlns and viewBox), keep the :fill="fill" prop and the withDefaults/defineProps setup for fill, and ensure the SVG markup otherwise mirrors the RestoreIcon.vue implementation for accessibility and consistency.src/renderer/src/components/icons/CloseIcon.vue (1)
1-19: Same Iconify lucide guideline concern asRestoreIcon.vue.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/src/components/icons/CloseIcon.vue` around lines 1 - 19, The CloseIcon.vue SVG doesn't follow the same Iconify/lucide guidelines as RestoreIcon.vue; update the <svg> element (in the template) to use scalable sizing and accessibility attributes: replace fixed width/height "10" with "1em", add role="img", aria-hidden="true" and focusable="false" attributes, and keep using the existing fill prop (defineProps/withDefaults for fill) so color is inherited via 'currentColor'.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (131)
build/entitlements.mac.plistdocs/architecture/new-ui-implementation-plan.mddocs/specs/default-model-settings/plan.mddocs/specs/default-model-settings/spec.mddocs/specs/default-model-settings/tasks.mddocs/specs/new-agent/plan.mddocs/specs/new-agent/spec.mddocs/specs/new-agent/tasks.mddocs/specs/new-agent/v1-spec.mddocs/specs/new-agent/v2-spec.mddocs/specs/new-agent/v3-spec.mddocs/specs/new-ui-agent-session/spec.mddocs/specs/new-ui-agent-store/spec.mddocs/specs/new-ui-chat-components/spec.mddocs/specs/new-ui-implementation/todo.mddocs/specs/new-ui-markdown-rendering/spec.mddocs/specs/new-ui-page-state/spec.mddocs/specs/new-ui-pages/spec.mddocs/specs/new-ui-project-store/spec.mddocs/specs/new-ui-session-store/spec.mddocs/specs/new-ui-sidebar/spec.mddocs/specs/new-ui-status-bar/spec.mdsrc/main/events.tssrc/main/presenter/agentPresenter/utility/utilityHandler.tssrc/main/presenter/configPresenter/index.tssrc/main/presenter/deepchatAgentPresenter/accumulator.tssrc/main/presenter/deepchatAgentPresenter/contextBuilder.tssrc/main/presenter/deepchatAgentPresenter/dispatch.tssrc/main/presenter/deepchatAgentPresenter/echo.tssrc/main/presenter/deepchatAgentPresenter/index.tssrc/main/presenter/deepchatAgentPresenter/messageStore.tssrc/main/presenter/deepchatAgentPresenter/process.tssrc/main/presenter/deepchatAgentPresenter/sessionStore.tssrc/main/presenter/deepchatAgentPresenter/types.tssrc/main/presenter/deeplinkPresenter/index.tssrc/main/presenter/index.tssrc/main/presenter/mcpPresenter/inMemoryServers/builder.tssrc/main/presenter/mcpPresenter/inMemoryServers/imageServer.tssrc/main/presenter/newAgentPresenter/agentRegistry.tssrc/main/presenter/newAgentPresenter/index.tssrc/main/presenter/newAgentPresenter/messageManager.tssrc/main/presenter/newAgentPresenter/sessionManager.tssrc/main/presenter/projectPresenter/index.tssrc/main/presenter/sessionPresenter/index.tssrc/main/presenter/sessionPresenter/managers/conversationManager.tssrc/main/presenter/shortcutPresenter.tssrc/main/presenter/sqlitePresenter/index.tssrc/main/presenter/sqlitePresenter/tables/deepchatMessages.tssrc/main/presenter/sqlitePresenter/tables/deepchatSessions.tssrc/main/presenter/sqlitePresenter/tables/newProjects.tssrc/main/presenter/sqlitePresenter/tables/newSessions.tssrc/main/presenter/syncPresenter/index.tssrc/main/presenter/windowPresenter/index.tssrc/preload/index.tssrc/renderer/settings/components/CommonSettings.vuesrc/renderer/settings/components/common/DefaultModelSettingsSection.vuesrc/renderer/shell/App.vuesrc/renderer/shell/components/AppBar.vuesrc/renderer/src/App.vuesrc/renderer/src/assets/style.csssrc/renderer/src/components/AppBar.vuesrc/renderer/src/components/ModelSelect.vuesrc/renderer/src/components/NewThread.vuesrc/renderer/src/components/NewThreadMock.vuesrc/renderer/src/components/SideBar.vuesrc/renderer/src/components/WindowSideBar.vuesrc/renderer/src/components/chat/ChatInputBox.vuesrc/renderer/src/components/chat/ChatInputToolbar.vuesrc/renderer/src/components/chat/ChatStatusBar.vuesrc/renderer/src/components/chat/ChatTopBar.vuesrc/renderer/src/components/chat/MessageList.vuesrc/renderer/src/components/icons/CloseIcon.vuesrc/renderer/src/components/icons/MaximizeIcon.vuesrc/renderer/src/components/icons/MinimizeIcon.vuesrc/renderer/src/components/icons/ModelIcon.vuesrc/renderer/src/components/icons/RestoreIcon.vuesrc/renderer/src/components/mcp-config/mcpServerForm.vuesrc/renderer/src/components/mock/MockChatPage.vuesrc/renderer/src/components/mock/MockInputBox.vuesrc/renderer/src/components/mock/MockInputToolbar.vuesrc/renderer/src/components/mock/MockMessageList.vuesrc/renderer/src/components/mock/MockStatusBar.vuesrc/renderer/src/components/mock/MockTopBar.vuesrc/renderer/src/components/mock/MockWelcomePage.vuesrc/renderer/src/composables/useMockViewState.tssrc/renderer/src/events.tssrc/renderer/src/i18n/da-DK/settings.jsonsrc/renderer/src/i18n/en-US/settings.jsonsrc/renderer/src/i18n/fa-IR/settings.jsonsrc/renderer/src/i18n/fr-FR/settings.jsonsrc/renderer/src/i18n/he-IL/settings.jsonsrc/renderer/src/i18n/ja-JP/settings.jsonsrc/renderer/src/i18n/ko-KR/settings.jsonsrc/renderer/src/i18n/pt-BR/settings.jsonsrc/renderer/src/i18n/ru-RU/settings.jsonsrc/renderer/src/i18n/zh-CN/settings.jsonsrc/renderer/src/i18n/zh-HK/settings.jsonsrc/renderer/src/i18n/zh-TW/settings.jsonsrc/renderer/src/pages/ChatPage.vuesrc/renderer/src/pages/NewThreadPage.vuesrc/renderer/src/pages/WelcomePage.vuesrc/renderer/src/stores/ui/agent.tssrc/renderer/src/stores/ui/draft.tssrc/renderer/src/stores/ui/message.tssrc/renderer/src/stores/ui/pageRouter.tssrc/renderer/src/stores/ui/project.tssrc/renderer/src/stores/ui/session.tssrc/renderer/src/views/ChatTabView.vuesrc/shadcn/components/ui/tooltip/TooltipContent.vuesrc/shared/types/agent-interface.d.tssrc/shared/types/core/chat-message.tssrc/shared/types/presenters/index.d.tssrc/shared/types/presenters/legacy.presenters.d.tssrc/shared/types/presenters/new-agent.presenter.d.tssrc/shared/types/presenters/project.presenter.d.tssrc/shared/utils/throttle.tstest/main/presenter/configPresenter/defaultModelSettings.test.tstest/main/presenter/deepchatAgentPresenter/accumulator.test.tstest/main/presenter/deepchatAgentPresenter/contextBuilder.test.tstest/main/presenter/deepchatAgentPresenter/deepchatAgentPresenter.test.tstest/main/presenter/deepchatAgentPresenter/dispatch.test.tstest/main/presenter/deepchatAgentPresenter/echo.test.tstest/main/presenter/deepchatAgentPresenter/messageStore.test.tstest/main/presenter/deepchatAgentPresenter/process.test.tstest/main/presenter/newAgentPresenter/agentRegistry.test.tstest/main/presenter/newAgentPresenter/integration.test.tstest/main/presenter/newAgentPresenter/messageManager.test.tstest/main/presenter/newAgentPresenter/newAgentPresenter.test.tstest/main/presenter/newAgentPresenter/sessionManager.test.tstest/main/presenter/projectPresenter/projectPresenter.test.tstest/shared/utils/throttle.test.ts
💤 Files with no reviewable changes (2)
- build/entitlements.mac.plist
- src/renderer/src/components/SideBar.vue
| function handleKeydown(e: KeyboardEvent) { | ||
| if (e.key === 'Enter' && !e.shiftKey) { | ||
| e.preventDefault() | ||
| emit('submit') | ||
| } | ||
| } |
There was a problem hiding this comment.
Missing e.isComposing guard causes premature submit for CJK IME users.
Each keydown in an IME is responded to by the browser, so when a user hits Enter to confirm an IME selection, the code immediately processes a keydown event — triggering submit before the user has finished composing their text. The Enter key is mis-recognized and sends the chat message instead of ending the composition.
The fix is a single additional guard:
🐛 Proposed fix
function handleKeydown(e: KeyboardEvent) {
- if (e.key === 'Enter' && !e.shiftKey) {
+ if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) {
e.preventDefault()
emit('submit')
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/renderer/src/components/chat/ChatInputBox.vue` around lines 30 - 35, The
handleKeydown function submits on Enter currently without checking IME
composition state; update handleKeydown to skip handling when the event
indicates composition (use the KeyboardEvent.isComposing property) so Enter used
to confirm CJK/IME composition won't trigger emit('submit'); locate the function
named handleKeydown and add a guard that returns early if e.isComposing is true
before the existing Enter/shift checks.
| async function initialize(): Promise<void> { | ||
| try { | ||
| // 1. Check if any provider is enabled | ||
| const enabledProviders = configPresenter.getEnabledProviders() | ||
| if (!enabledProviders || enabledProviders.length === 0) { | ||
| route.value = { name: 'welcome' } | ||
| return | ||
| } | ||
|
|
||
| // 2. Check for active session on this tab | ||
| const tabId = window.api.getWebContentsId() | ||
| const activeSession = await sessionPresenter.getActiveSession(tabId) | ||
| if (activeSession) { | ||
| route.value = { name: 'chat', sessionId: activeSession.sessionId } | ||
| return | ||
| } | ||
|
|
||
| // 3. Default to new thread | ||
| route.value = { name: 'newThread' } | ||
| } catch (e) { | ||
| error.value = String(e) | ||
| route.value = { name: 'newThread' } | ||
| } | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find presenter-related files and interfaces
find . -type f \( -name "*.ts" -o -name "*.d.ts" \) | grep -i presenter | head -20Repository: ThinkInAIXYZ/deepchat
Length of output: 1266
🏁 Script executed:
# Search for getEnabledProviders definition
rg -n "getEnabledProviders" --type=ts -A 2 -B 2Repository: ThinkInAIXYZ/deepchat
Length of output: 2848
🏁 Script executed:
# Search for usePresenter implementation
rg -n "usePresenter" --type=ts -l | head -10Repository: ThinkInAIXYZ/deepchat
Length of output: 469
🏁 Script executed:
# Find usePresenter composable
fd -t f "usePresenter" --extension ts --extension tsxRepository: ThinkInAIXYZ/deepchat
Length of output: 109
🏁 Script executed:
# Search for usePresenter definition
rg -n "export.*usePresenter|function usePresenter|const usePresenter" --type=ts -A 10Repository: ThinkInAIXYZ/deepchat
Length of output: 1079
🏁 Script executed:
# Read usePresenter.ts to understand the proxy implementation
cat -n src/renderer/src/composables/usePresenter.tsRepository: ThinkInAIXYZ/deepchat
Length of output: 3993
Add await to getEnabledProviders() calls — the usePresenter proxy wraps all methods as async.
Although getEnabledProviders() is defined as synchronous in the main process, the usePresenter proxy (lines 73–74 of src/renderer/src/composables/usePresenter.ts) wraps every method call as async via ipcRenderer.invoke(). This means at the renderer level, the call returns Promise<LLM_PROVIDER[]> instead of the array itself.
Without await, enabledProviders at lines 24 and 67 becomes a Promise object. The check at line 25 (enabledProviders.length === 0) evaluates to undefined === 0 (false), so the condition fails and the welcome route is never displayed even when all providers are disabled.
Fix by awaiting both calls:
Diff
async function initialize(): Promise<void> {
try {
// 1. Check if any provider is enabled
- const enabledProviders = configPresenter.getEnabledProviders()
+ const enabledProviders = await configPresenter.getEnabledProviders()
if (!enabledProviders || enabledProviders.length === 0) {
route.value = { name: 'welcome' }
return
}The same issue occurs at line 67 in the event listener handler.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/renderer/src/stores/ui/pageRouter.ts` around lines 21 - 44, The renderer
is calling getEnabledProviders() via the usePresenter proxy which returns a
Promise, so update calls in initialize() and the provider-change event handler
to await the result (e.g., const enabledProviders = await
configPresenter.getEnabledProviders()); if the handler is not async, mark it
async so you can await inside it; keep using the enabledProviders variable for
the length check and subsequent routing logic (route.value = { name: 'welcome' }
/ 'newThread' / 'chat') unchanged.
Summary by CodeRabbit
New Features
Chores