fix(theme): enhance theme handling and appearance synchronization#1414
fix(theme): enhance theme handling and appearance synchronization#1414
Conversation
📝 WalkthroughWalkthroughUpdated theme synchronization in App.vue to resolve system/dark modes and apply classes to both root and body immediately; MarkdownRenderer now forwards dark/light code-block themes and Monaco options into NodeRenderer/CodeBlockNode; WorkspaceCodePane replaces manual monaco-editor lifecycle with the stream-monaco composable and adapts theme/font updates. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 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
🧹 Nitpick comments (1)
src/renderer/src/components/markdown/MarkdownRenderer.vue (1)
49-51: Rename these static theme values to SCREAMING_SNAKE_CASE.
codeBlockThemes,codeBlockDarkTheme, andcodeBlockLightThemeare immutable constants, so they should follow the repo's constant naming rule. ConsiderCODE_BLOCK_THEMES,CODE_BLOCK_DARK_THEME, andCODE_BLOCK_LIGHT_THEME, ideally hoisted out ofscript setupwhile they stay fully static. As per coding guidelines,src/**/*.{ts,tsx,js,jsx,vue}: "Constants must use SCREAMING_SNAKE_CASE naming".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/renderer/src/components/markdown/MarkdownRenderer.vue` around lines 49 - 51, Rename the immutable constants codeBlockThemes, codeBlockDarkTheme, and codeBlockLightTheme to SCREAMING_SNAKE_CASE (e.g., CODE_BLOCK_THEMES, CODE_BLOCK_DARK_THEME, CODE_BLOCK_LIGHT_THEME) and hoist them out of the script setup block so they remain fully static; update any references to the old identifiers throughout the component to the new constant names to satisfy the repository constant naming rule.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/renderer/src/components/sidepanel/viewer/WorkspaceCodePane.vue`:
- Around line 161-178: The async create path can miss updates and set state on a
replaced pane: guard and replay after createEditorTask completes by making
createEditorTask's closure check a mounted/ref guard before setting
editorInitialized and applying theme/font, and after awaiting createEditorTask
(instead of returning early) call updateCode(props.source?.content ?? '',
resolvedLanguage.value) so any newer props.source is replayed; specifically
update the logic around createEditorTask, createEditor(editorElement,
nextContent, nextLanguage), editorInitialized, applyTheme, applyFontFamily to
use a mounted/ref boolean and invoke updateCode when the in-flight creation
finishes (apply same change to the similar block around lines 223-231).
- Around line 36-54: The Monaco options passed to useMonaco() (used by
createEditor/updateCode/cleanupEditor/getEditorView/getEditor) omit a languages
list, so non-default languages returned by resolveLanguage() may lack
highlighting; add a languages array to the useMonaco() call containing all
supported tokens (markdown, html, javascript, typescript, json, yaml, python,
ruby, rust, shell, powershell, dockerfile, xml, plaintext) and keep the existing
options intact so the editor loads those language grammars explicitly.
---
Nitpick comments:
In `@src/renderer/src/components/markdown/MarkdownRenderer.vue`:
- Around line 49-51: Rename the immutable constants codeBlockThemes,
codeBlockDarkTheme, and codeBlockLightTheme to SCREAMING_SNAKE_CASE (e.g.,
CODE_BLOCK_THEMES, CODE_BLOCK_DARK_THEME, CODE_BLOCK_LIGHT_THEME) and hoist them
out of the script setup block so they remain fully static; update any references
to the old identifiers throughout the component to the new constant names to
satisfy the repository constant naming rule.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 91a8f451-fc60-41bc-91b5-764a7d9d67b0
📒 Files selected for processing (3)
src/renderer/src/App.vuesrc/renderer/src/components/markdown/MarkdownRenderer.vuesrc/renderer/src/components/sidepanel/viewer/WorkspaceCodePane.vue
| const { createEditor, updateCode, cleanupEditor, getEditorView, getEditor } = useMonaco({ | ||
| readOnly: true, | ||
| domReadOnly: true, | ||
| automaticLayout: true, | ||
| wordWrap: 'on', | ||
| wrappingIndent: 'same', | ||
| scrollBeyondLastLine: false, | ||
| minimap: { enabled: false }, | ||
| lineNumbers: 'on', | ||
| renderLineHighlight: 'none', | ||
| contextmenu: false, | ||
| themes: ['vitesse-dark', 'vitesse-light'], | ||
| theme: resolvedTheme.value, | ||
| fontFamily: uiSettingsStore.formattedCodeFontFamily, | ||
| padding: { | ||
| top: 12, | ||
| bottom: 12 | ||
| } | ||
| }) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect the pinned versions and this useMonaco configuration.
# Expect: exact stream-monaco / markstream-vue versions, and no `languages:` entry in WorkspaceCodePane.
rg -n -C2 'stream-monaco|markstream-vue' \
--glob 'package.json' \
--glob 'pnpm-lock.yaml' \
--glob 'package-lock.json' \
--glob 'yarn.lock'
rg -n -C4 'useMonaco\(' src/renderer/src/components/sidepanel/viewer/WorkspaceCodePane.vueRepository: ThinkInAIXYZ/deepchat
Length of output: 940
🏁 Script executed:
# Get the complete WorkspaceCodePane.vue file to check the full useMonaco config and resolveLanguage function
wc -l src/renderer/src/components/sidepanel/viewer/WorkspaceCodePane.vueRepository: ThinkInAIXYZ/deepchat
Length of output: 135
🏁 Script executed:
# Read the complete file to see useMonaco call, resolveLanguage, and overall context
cat -n src/renderer/src/components/sidepanel/viewer/WorkspaceCodePane.vueRepository: ThinkInAIXYZ/deepchat
Length of output: 6909
🏁 Script executed:
# Search for other uses of useMonaco in the codebase to see if languages is used elsewhere
rg -n 'useMonaco' src/renderer --type vue --type ts --type tsx -A 15Repository: ThinkInAIXYZ/deepchat
Length of output: 92
🏁 Script executed:
# Check if there's any documentation or comments about language configuration in the codebase
rg -i 'stream-monaco|language' src/renderer --type vue --type ts --type tsx | head -30Repository: ThinkInAIXYZ/deepchat
Length of output: 92
🏁 Script executed:
# Search for other uses of useMonaco in the codebase
rg -n 'useMonaco' src/renderer -A 15Repository: ThinkInAIXYZ/deepchat
Length of output: 13334
🏁 Script executed:
# Check stream-monaco package in node_modules or look at type definitions
fd -H 'stream-monaco' node_modules 2>/dev/null | head -5Repository: ThinkInAIXYZ/deepchat
Length of output: 47
🏁 Script executed:
# Look for any pnpm-lock.yaml or package-lock to understand stream-monaco dependency
fd 'pnpm-lock.yaml' . --max-depth 2Repository: ThinkInAIXYZ/deepchat
Length of output: 47
🏁 Script executed:
# Check the pnpm-lock.yaml to see full dependency tree for stream-monaco
cat pnpm-lock.yaml | grep -A 20 "stream-monaco"Repository: ThinkInAIXYZ/deepchat
Length of output: 111
🏁 Script executed:
# Look for any test files or documentation that might hint at language support expectations
fd -i 'test\|spec\|readme' src/renderer/src/components/sidepanel/viewer -o src/renderer/src/components/artifacts -o src/renderer/src/composablesRepository: ThinkInAIXYZ/deepchat
Length of output: 258
🏁 Script executed:
# Search for lock files
find . -maxdepth 2 -name '*lock*' -type fRepository: ThinkInAIXYZ/deepchat
Length of output: 47
🏁 Script executed:
# Check the CodeArtifact.vue to see how it uses useMonaco and detectLanguage
sed -n '49,120p' src/renderer/src/components/artifacts/CodeArtifact.vueRepository: ThinkInAIXYZ/deepchat
Length of output: 1825
🏁 Script executed:
# Check stream-monaco version details and whether it's in node_modules
ls -la node_modules/stream-monaco 2>/dev/null || echo "stream-monaco not in node_modules"Repository: ThinkInAIXYZ/deepchat
Length of output: 98
🌐 Web query:
stream-monaco 0.0.20 language configuration documentation
💡 Result:
stream-monaco is a framework-agnostic library for integrating Monaco Editor with Shiki syntax highlighting, optimized for streaming updates. Version 0.0.20 documentation is not explicitly found in searches up to today's date (2026-03-31), with latest published version appearing as 0.0.19. Language configuration is handled via the useMonaco(options) hook's languages parameter, which accepts an array of MonacoLanguage entries (strings for built-in languages or loader objects for custom/lazy bundles). This replaces built-in defaults and registers Shiki-based highlighting for specified languages like ['javascript', 'typescript', 'vue', 'python']. Official documentation is in the GitHub README at https://github.com/Simon-He95/stream-monaco, which details full MonacoOptions including languages, themes, and Monaco editor options. Example: typescript const { createEditor } = useMonaco({ languages: ['javascript', 'python', 'rust'], themes: ['vitesse-dark', 'vitesse-light'] }); For custom languages, use onBeforeCreate hook to register via monaco.languages.register before editor creation. No specific changes noted for 0.0.20; configuration consistent across documented versions.
Citations:
- 1: https://github.com/Simon-He95/stream-monaco/blob/main/README.md
- 2: https://markstream-vue-docs.simonhe.me/guide/monaco.html
- 3: https://github.com/Simon-He95/stream-monaco
- 4: https://registry.npmjs.org/stream-monaco
- 5: https://markstream-vue-docs.simonhe.me/guide/monaco
🌐 Web query:
stream-monaco useMonaco languages parameter default
💡 Result:
The languages parameter in stream-monaco's useMonaco(options) hook is optional. When not provided, it uses a predefined minimal set of default languages to keep the initial load fast. Official documentation states the default as "见默认语言列表" (see default language list) without listing them explicitly, but integration docs confirm a "minimal set of Monaco languages ships with the default integration". Providing a languages array replaces these internal defaults entirely; it does not append to them. Examples commonly use ['javascript', 'typescript', 'vue', 'python', 'json'].
Citations:
- 1: https://github.com/Simon-He95/stream-monaco/blob/main/README.md
- 2: https://registry.npmjs.org/stream-monaco
- 3: https://markstream-vue-docs.simonhe.me/guide/monaco.html
- 4: https://github.com/Simon-He95/stream-monaco
- 5: https://markstream-vue-docs.simonhe.me/guide/monaco
Explicitly configure supported languages for Monaco Editor.
The useMonaco() call omits the languages parameter, which means it uses stream-monaco's minimal default language set. Since resolveLanguage() can return markdown, html, javascript, json, yaml, python, ruby, rust, shell, powershell, dockerfile, and xml, some of these will silently lose syntax highlighting if they aren't in the built-in defaults. Declare the full supported set:
const { createEditor, updateCode, cleanupEditor, getEditorView, getEditor } = useMonaco({
languages: ['markdown', 'html', 'javascript', 'typescript', 'json', 'yaml', 'python', 'ruby', 'rust', 'shell', 'powershell', 'dockerfile', 'xml', 'plaintext'],
readOnly: true,
// ... rest of config
})🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/renderer/src/components/sidepanel/viewer/WorkspaceCodePane.vue` around
lines 36 - 54, The Monaco options passed to useMonaco() (used by
createEditor/updateCode/cleanupEditor/getEditorView/getEditor) omit a languages
list, so non-default languages returned by resolveLanguage() may lack
highlighting; add a languages array to the useMonaco() call containing all
supported tokens (markdown, html, javascript, typescript, json, yaml, python,
ruby, rust, shell, powershell, dockerfile, xml, plaintext) and keep the existing
options intact so the editor loads those language grammars explicitly.
| if (!hasEditor || !editorInitialized.value) { | ||
| if (createEditorTask) { | ||
| await createEditorTask | ||
| return | ||
| } | ||
|
|
||
| const ensureEditor = async () => { | ||
| if (editor.value || !editorRef.value) { | ||
| createEditorTask = (async () => { | ||
| await createEditor(editorElement, nextContent, nextLanguage) | ||
| editorInitialized.value = true | ||
| await applyTheme() | ||
| applyFontFamily(uiSettingsStore.formattedCodeFontFamily) | ||
| })() | ||
|
|
||
| try { | ||
| await createEditorTask | ||
| } finally { | ||
| createEditorTask = null | ||
| } |
There was a problem hiding this comment.
Make the async create path replay-safe.
Line 164 returns immediately after awaiting an in-flight createEditorTask, so any newer props.source state that arrived during creation never gets replayed. The closure from Lines 167-172 also keeps running after cleanup, which can set editorInitialized back to true on an unmounted or replaced pane. Add a mounted/ref guard and replay updateCode(props.source.content ?? '', resolvedLanguage.value) once the create finishes.
🔁 Possible fix
const themeStore = useThemeStore()
const editorRef = ref<HTMLElement | null>(null)
const editorInitialized = ref(false)
+const isAlive = ref(true)
let createEditorTask: Promise<void> | null = null
...
if (!hasEditor || !editorInitialized.value) {
if (createEditorTask) {
await createEditorTask
- return
+ if (!isAlive.value || !editorRef.value) return
+ updateCode(props.source.content ?? '', resolvedLanguage.value)
+ return
}
createEditorTask = (async () => {
await createEditor(editorElement, nextContent, nextLanguage)
+ if (!isAlive.value || editorRef.value !== editorElement) {
+ cleanupEditor()
+ return
+ }
editorInitialized.value = true
await applyTheme()
applyFontFamily(uiSettingsStore.formattedCodeFontFamily)
+ updateCode(props.source.content ?? '', resolvedLanguage.value)
})()
...
onBeforeUnmount(() => {
+ isAlive.value = false
cleanupEditor()
editorInitialized.value = false
createEditorTask = null
})Also applies to: 223-231
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/renderer/src/components/sidepanel/viewer/WorkspaceCodePane.vue` around
lines 161 - 178, The async create path can miss updates and set state on a
replaced pane: guard and replay after createEditorTask completes by making
createEditorTask's closure check a mounted/ref guard before setting
editorInitialized and applying theme/font, and after awaiting createEditorTask
(instead of returning early) call updateCode(props.source?.content ?? '',
resolvedLanguage.value) so any newer props.source is replayed; specifically
update the logic around createEditorTask, createEditor(editorElement,
nextContent, nextLanguage), editorInitialized, applyTheme, applyFontFamily to
use a mounted/ref boolean and invoke updateCode when the in-flight creation
finishes (apply same change to the similar block around lines 223-231).
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/renderer/src/App.vue`:
- Around line 77-85: Remove the leftover debug console.log in the watcher:
inside the watch callback that observes themeStore.themeMode, themeStore.isDark,
and uiSettingsStore.fontSizeClass (the function that computes nextThemeName via
resolveThemeName and calls syncAppearanceClasses), delete the
console.log('newTheme', nextThemeName) statement so the watcher only computes
nextThemeName and calls syncAppearanceClasses without printing to the console.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
| watch( | ||
| [() => themeStore.themeMode, () => uiSettingsStore.fontSizeClass], | ||
| ([newTheme, newFontSizeClass], [oldTheme, oldFontSizeClass]) => { | ||
| let newThemeName = newTheme | ||
| if (newTheme === 'system') { | ||
| newThemeName = themeStore.isDark ? 'dark' : 'light' | ||
| } | ||
| if (oldTheme) { | ||
| document.documentElement.classList.remove(oldTheme) | ||
| } | ||
| if (oldFontSizeClass) { | ||
| document.documentElement.classList.remove(oldFontSizeClass) | ||
| } | ||
| document.documentElement.classList.add(newThemeName) | ||
| document.documentElement.classList.add(newFontSizeClass) | ||
| console.log('newTheme', newThemeName) | ||
| [() => themeStore.themeMode, () => themeStore.isDark, () => uiSettingsStore.fontSizeClass], | ||
| ([themeMode, isDark, fontSizeClass]) => { | ||
| const nextThemeName = resolveThemeName(themeMode, isDark) | ||
| syncAppearanceClasses(nextThemeName, fontSizeClass) | ||
| console.log('newTheme', nextThemeName) | ||
| }, | ||
| { immediate: false } // Initialization is handled in onMounted | ||
| { immediate: true } | ||
| ) |
There was a problem hiding this comment.
Remove debug console.log statement.
Line 82 contains a debug log that should be removed before merging.
🧹 Proposed fix
([themeMode, isDark, fontSizeClass]) => {
const nextThemeName = resolveThemeName(themeMode, isDark)
syncAppearanceClasses(nextThemeName, fontSizeClass)
- console.log('newTheme', nextThemeName)
},📝 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.
| watch( | |
| [() => themeStore.themeMode, () => uiSettingsStore.fontSizeClass], | |
| ([newTheme, newFontSizeClass], [oldTheme, oldFontSizeClass]) => { | |
| let newThemeName = newTheme | |
| if (newTheme === 'system') { | |
| newThemeName = themeStore.isDark ? 'dark' : 'light' | |
| } | |
| if (oldTheme) { | |
| document.documentElement.classList.remove(oldTheme) | |
| } | |
| if (oldFontSizeClass) { | |
| document.documentElement.classList.remove(oldFontSizeClass) | |
| } | |
| document.documentElement.classList.add(newThemeName) | |
| document.documentElement.classList.add(newFontSizeClass) | |
| console.log('newTheme', newThemeName) | |
| [() => themeStore.themeMode, () => themeStore.isDark, () => uiSettingsStore.fontSizeClass], | |
| ([themeMode, isDark, fontSizeClass]) => { | |
| const nextThemeName = resolveThemeName(themeMode, isDark) | |
| syncAppearanceClasses(nextThemeName, fontSizeClass) | |
| console.log('newTheme', nextThemeName) | |
| }, | |
| { immediate: false } // Initialization is handled in onMounted | |
| { immediate: true } | |
| ) | |
| watch( | |
| [() => themeStore.themeMode, () => themeStore.isDark, () => uiSettingsStore.fontSizeClass], | |
| ([themeMode, isDark, fontSizeClass]) => { | |
| const nextThemeName = resolveThemeName(themeMode, isDark) | |
| syncAppearanceClasses(nextThemeName, fontSizeClass) | |
| }, | |
| { immediate: true } | |
| ) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/renderer/src/App.vue` around lines 77 - 85, Remove the leftover debug
console.log in the watcher: inside the watch callback that observes
themeStore.themeMode, themeStore.isDark, and uiSettingsStore.fontSizeClass (the
function that computes nextThemeName via resolveThemeName and calls
syncAppearanceClasses), delete the console.log('newTheme', nextThemeName)
statement so the watcher only computes nextThemeName and calls
syncAppearanceClasses without printing to the console.
fix monaco panel & theme handling
Summary by CodeRabbit
Release Notes