Skip to content

fix(theme): enhance theme handling and appearance synchronization#1414

Merged
zerob13 merged 2 commits intodevfrom
fix-monaco-panel
Mar 31, 2026
Merged

fix(theme): enhance theme handling and appearance synchronization#1414
zerob13 merged 2 commits intodevfrom
fix-monaco-panel

Conversation

@zhangmo8
Copy link
Copy Markdown
Collaborator

@zhangmo8 zhangmo8 commented Mar 31, 2026

fix monaco panel & theme handling

Summary by CodeRabbit

Release Notes

  • Improvements
    • More reliable theme and font-size synchronization across the page (immediate initial sync and consistent body/document updates).
    • Code blocks now receive explicit light/dark themes and editor display options so they match UI theme and font settings.
    • Editor lifecycle and theming are more robust and responsive, improving theme switches, font updates, and cleanup during navigation.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 31, 2026

📝 Walkthrough

Walkthrough

Updated 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

Cohort / File(s) Summary
Theme Management
src/renderer/src/App.vue
Import updated to include ThemeMode. Replaced previous watch/onMounted logic with a single immediate watcher on [themeStore.themeMode, themeStore.isDark, uiSettingsStore.fontSizeClass]. Compute effective theme when system and synchronize appearance classes on both document.documentElement and document.body, clearing known theme and text-* font-size classes before applying new ones. Removed initial body-class setup in onMounted.
Markdown Rendering
src/renderer/src/components/markdown/MarkdownRenderer.vue
Added two-theme constants and computed dark/light code-block themes. Passes codeBlockDarkTheme and codeBlockLightTheme into NodeRenderer. When rendering code_block, supplies CodeBlockNode with isDark, explicit darkTheme/lightTheme, full themes array, and monacoOptions computed from uiSettingsStore.formattedCodeFontFamily.
Code Editor Integration
src/renderer/src/components/sidepanel/viewer/WorkspaceCodePane.vue
Replaced manual monaco-editor create/model/dispose and observers with useMonaco (stream-monaco). Introduced editorInitialized/createEditorTask flow and getEditor().setTheme(...) guarded async application. Replaced earlier multi-prop sync with targeted syncEditor() and updateCode, applied font-family changes via getEditorView()?.updateOptions, and use cleanupEditor() for teardown (removed explicit model disposal and observers).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • PR #1038: Adds/updates the stream-monaco integration—directly related to WorkspaceCodePane's switch to useMonaco.
  • PR #770: Changes App.vue theme resolution and class syncing, aligning with the theme sync updates here.
  • PR #1128: Modifies markdown rendering imports/NodeRenderer usage—closely related to MarkdownRenderer theme/prop changes.

Suggested reviewers

  • zerob13

Poem

🐰 I hop through classes, dark and light,
I sync the body, and root feels right,
Editors wake with composable cheer,
Code blocks glow in themes sincere,
A rabbit nods—deploy and delight!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix(theme): enhance theme handling and appearance synchronization' directly aligns with the main changes across all three files, which focus on refactoring theme handling logic in App.vue, propagating theme data to MarkdownRenderer, and updating the Monaco editor's theme management in WorkspaceCodePane.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-monaco-panel

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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, and codeBlockLightTheme are immutable constants, so they should follow the repo's constant naming rule. Consider CODE_BLOCK_THEMES, CODE_BLOCK_DARK_THEME, and CODE_BLOCK_LIGHT_THEME, ideally hoisted out of script setup while 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

📥 Commits

Reviewing files that changed from the base of the PR and between b3644b6 and 8edb164.

📒 Files selected for processing (3)
  • src/renderer/src/App.vue
  • src/renderer/src/components/markdown/MarkdownRenderer.vue
  • src/renderer/src/components/sidepanel/viewer/WorkspaceCodePane.vue

Comment on lines +36 to +54
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
}
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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.vue

Repository: 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.vue

Repository: 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.vue

Repository: 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 15

Repository: 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 -30

Repository: ThinkInAIXYZ/deepchat

Length of output: 92


🏁 Script executed:

# Search for other uses of useMonaco in the codebase
rg -n 'useMonaco' src/renderer -A 15

Repository: 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 -5

Repository: 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 2

Repository: 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/composables

Repository: ThinkInAIXYZ/deepchat

Length of output: 258


🏁 Script executed:

# Search for lock files
find . -maxdepth 2 -name '*lock*' -type f

Repository: 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.vue

Repository: 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:


🌐 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:


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.

Comment on lines +161 to +178
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
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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).

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 229d3bea-50c5-47f5-b793-017738e36f3f

📥 Commits

Reviewing files that changed from the base of the PR and between 8edb164 and 88e6fbb.

📒 Files selected for processing (1)
  • src/renderer/src/App.vue

Comment on lines 77 to 85
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 }
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

@zerob13 zerob13 merged commit 6c5a4e6 into dev Mar 31, 2026
3 checks passed
@zhangmo8 zhangmo8 deleted the fix-monaco-panel branch March 31, 2026 13:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants