-
Notifications
You must be signed in to change notification settings - Fork 650
fix(theme): enhance theme handling and appearance synchronization #1414
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,8 +10,9 @@ | |
| </template> | ||
|
|
||
| <script setup lang="ts"> | ||
| import * as monaco from 'monaco-editor' | ||
| import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue' | ||
| import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue' | ||
| import { useMonaco } from 'stream-monaco' | ||
| import { useThemeStore } from '@/stores/theme' | ||
| import { useUiSettingsStore } from '@/stores/uiSettingsStore' | ||
|
|
||
| type WorkspaceCodeSource = { | ||
|
|
@@ -26,13 +27,31 @@ const props = defineProps<{ | |
| }>() | ||
|
|
||
| const uiSettingsStore = useUiSettingsStore() | ||
| const themeStore = useThemeStore() | ||
| const editorRef = ref<HTMLElement | null>(null) | ||
| const editor = shallowRef<monaco.editor.IStandaloneCodeEditor | null>(null) | ||
| const model = shallowRef<monaco.editor.ITextModel | null>(null) | ||
|
|
||
| let resizeObserver: ResizeObserver | null = null | ||
| let themeObserver: MutationObserver | null = null | ||
| let currentSourceId: string | null = null | ||
| const editorInitialized = ref(false) | ||
| let createEditorTask: Promise<void> | null = null | ||
| const resolvedTheme = computed(() => (themeStore.isDark ? 'vitesse-dark' : 'vitesse-light')) | ||
|
|
||
| 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 | ||
| } | ||
| }) | ||
|
Comment on lines
+36
to
+54
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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:
💡 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:
💡 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 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 |
||
|
|
||
| const LANGUAGE_ALIASES: Record<string, string> = { | ||
| md: 'markdown', | ||
|
|
@@ -117,105 +136,57 @@ const resolveLanguage = (source: WorkspaceCodeSource): string => { | |
|
|
||
| const resolvedLanguage = computed(() => resolveLanguage(props.source)) | ||
|
|
||
| const getThemeName = () => { | ||
| return document.documentElement.classList.contains('dark') ? 'vs-dark' : 'vs' | ||
| } | ||
|
|
||
| const applyTheme = () => { | ||
| monaco.editor.setTheme(getThemeName()) | ||
| const applyFontFamily = (fontFamily: string) => { | ||
| getEditorView()?.updateOptions({ fontFamily }) | ||
| } | ||
|
|
||
| const layoutEditor = () => { | ||
| editor.value?.layout() | ||
| } | ||
|
|
||
| const disposeModel = () => { | ||
| model.value?.dispose() | ||
| model.value = null | ||
| currentSourceId = null | ||
| const applyTheme = async () => { | ||
| try { | ||
| getEditor().setTheme(resolvedTheme.value) | ||
| } catch (error) { | ||
| console.warn('[WorkspaceCodePane] Failed to apply Monaco theme:', error) | ||
| } | ||
| } | ||
|
|
||
| const syncModel = () => { | ||
| if (!editor.value) { | ||
| const syncEditor = async () => { | ||
| const editorElement = editorRef.value | ||
| if (!editorElement) { | ||
| return | ||
| } | ||
|
|
||
| const nextLanguage = resolvedLanguage.value | ||
| const nextContent = props.source.content ?? '' | ||
| const nextLanguage = resolvedLanguage.value | ||
| const hasEditor = Boolean(editorElement.querySelector('.monaco-editor')) | ||
|
|
||
| if (!model.value || currentSourceId !== props.source.id) { | ||
| disposeModel() | ||
| model.value = monaco.editor.createModel(nextContent, nextLanguage) | ||
| currentSourceId = props.source.id | ||
| editor.value.setModel(model.value) | ||
| return | ||
| } | ||
|
|
||
| if (model.value.getLanguageId() !== nextLanguage) { | ||
| monaco.editor.setModelLanguage(model.value, nextLanguage) | ||
| } | ||
|
|
||
| if (model.value.getValue() !== nextContent) { | ||
| model.value.setValue(nextContent) | ||
| } | ||
| } | ||
| 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 | ||
| } | ||
|
Comment on lines
+161
to
+178
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make the async create path replay-safe. Line 164 returns immediately after awaiting an in-flight 🔁 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 |
||
| return | ||
| } | ||
|
|
||
| applyTheme() | ||
|
|
||
| editor.value = monaco.editor.create(editorRef.value, { | ||
| readOnly: true, | ||
| domReadOnly: true, | ||
| automaticLayout: false, | ||
| wordWrap: 'on', | ||
| wrappingIndent: 'same', | ||
| scrollBeyondLastLine: false, | ||
| minimap: { enabled: false }, | ||
| lineNumbers: 'on', | ||
| renderLineHighlight: 'none', | ||
| contextmenu: false, | ||
| fontFamily: uiSettingsStore.formattedCodeFontFamily, | ||
| padding: { | ||
| top: 12, | ||
| bottom: 12 | ||
| } | ||
| }) | ||
|
|
||
| syncModel() | ||
| await nextTick() | ||
| layoutEditor() | ||
| updateCode(nextContent, nextLanguage) | ||
| } | ||
|
|
||
| onMounted(() => { | ||
| void ensureEditor() | ||
|
|
||
| if (typeof ResizeObserver !== 'undefined' && editorRef.value) { | ||
| resizeObserver = new ResizeObserver(() => { | ||
| layoutEditor() | ||
| }) | ||
| resizeObserver.observe(editorRef.value) | ||
| } | ||
|
|
||
| themeObserver = new MutationObserver(() => { | ||
| applyTheme() | ||
| }) | ||
| themeObserver.observe(document.documentElement, { | ||
| attributes: true, | ||
| attributeFilter: ['class'] | ||
| }) | ||
| }) | ||
|
|
||
| watch( | ||
| () => [props.source.id, props.source.content, props.source.language, props.source.type] as const, | ||
| () => [editorRef.value, props.source.id, props.source.content, resolvedLanguage.value] as const, | ||
| async () => { | ||
| await ensureEditor() | ||
| syncModel() | ||
| await nextTick() | ||
| layoutEditor() | ||
| await syncEditor() | ||
| }, | ||
| { | ||
| immediate: true, | ||
|
|
@@ -226,18 +197,37 @@ watch( | |
| watch( | ||
| () => uiSettingsStore.formattedCodeFontFamily, | ||
| (fontFamily) => { | ||
| editor.value?.updateOptions({ fontFamily }) | ||
| layoutEditor() | ||
| applyFontFamily(fontFamily) | ||
| } | ||
| ) | ||
|
|
||
| watch( | ||
| resolvedTheme, | ||
| () => { | ||
| if (!editorInitialized.value) { | ||
| return | ||
| } | ||
|
|
||
| void applyTheme() | ||
| }, | ||
| { | ||
| flush: 'post' | ||
| } | ||
| ) | ||
|
|
||
| watch(editorRef, (value) => { | ||
| if (value) { | ||
| return | ||
| } | ||
|
|
||
| cleanupEditor() | ||
| editorInitialized.value = false | ||
| createEditorTask = null | ||
| }) | ||
|
|
||
| onBeforeUnmount(() => { | ||
| resizeObserver?.disconnect() | ||
| resizeObserver = null | ||
| themeObserver?.disconnect() | ||
| themeObserver = null | ||
| editor.value?.dispose() | ||
| editor.value = null | ||
| disposeModel() | ||
| cleanupEditor() | ||
| editorInitialized.value = false | ||
| createEditorTask = null | ||
| }) | ||
| </script> | ||
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.
Remove debug
console.logstatement.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
🤖 Prompt for AI Agents