Skip to content

Commit 6c5a4e6

Browse files
authored
fix(theme): enhance theme handling and appearance synchronization (#1414)
* fix(theme): enhance theme handling and appearance synchronization * chore: fmt
1 parent 3bf46a1 commit 6c5a4e6

3 files changed

Lines changed: 123 additions & 120 deletions

File tree

src/renderer/src/App.vue

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { DEEPLINK_EVENTS, NOTIFICATION_EVENTS, SHORTCUT_EVENTS } from './events'
1212
import { Toaster } from '@shadcn/components/ui/sonner'
1313
import { useToast } from '@/components/use-toast'
1414
import { useUiSettingsStore } from '@/stores/uiSettingsStore'
15-
import { useThemeStore } from '@/stores/theme'
15+
import { useThemeStore, type ThemeMode } from '@/stores/theme'
1616
import { useLanguageStore } from '@/stores/language'
1717
import { useI18n } from 'vue-i18n'
1818
import TranslatePopup from '@/components/popup/TranslatePopup.vue'
@@ -56,25 +56,32 @@ const currentErrorId = ref<string | null>(null)
5656
const errorDisplayTimer = ref<number | null>(null)
5757
5858
const { setup: setupMcpDeeplink, cleanup: cleanupMcpDeeplink } = useMcpInstallDeeplinkHandler()
59-
// Watch theme and font size changes, update body class directly
59+
60+
const resolveThemeName = (themeMode: ThemeMode, isDark: boolean) => {
61+
return themeMode === 'system' ? (isDark ? 'dark' : 'light') : themeMode
62+
}
63+
64+
const syncAppearanceClasses = (themeName: string, fontSizeClass: string) => {
65+
if (typeof document === 'undefined') {
66+
return
67+
}
68+
69+
for (const target of [document.documentElement, document.body]) {
70+
target.classList.remove('light', 'dark', 'system')
71+
target.classList.add(themeName)
72+
target.classList.remove('text-xs', 'text-sm', 'text-base', 'text-lg', 'text-xl', 'text-2xl')
73+
target.classList.add(fontSizeClass)
74+
}
75+
}
76+
6077
watch(
61-
[() => themeStore.themeMode, () => uiSettingsStore.fontSizeClass],
62-
([newTheme, newFontSizeClass], [oldTheme, oldFontSizeClass]) => {
63-
let newThemeName = newTheme
64-
if (newTheme === 'system') {
65-
newThemeName = themeStore.isDark ? 'dark' : 'light'
66-
}
67-
if (oldTheme) {
68-
document.documentElement.classList.remove(oldTheme)
69-
}
70-
if (oldFontSizeClass) {
71-
document.documentElement.classList.remove(oldFontSizeClass)
72-
}
73-
document.documentElement.classList.add(newThemeName)
74-
document.documentElement.classList.add(newFontSizeClass)
75-
console.log('newTheme', newThemeName)
78+
[() => themeStore.themeMode, () => themeStore.isDark, () => uiSettingsStore.fontSizeClass],
79+
([themeMode, isDark, fontSizeClass]) => {
80+
const nextThemeName = resolveThemeName(themeMode, isDark)
81+
syncAppearanceClasses(nextThemeName, fontSizeClass)
82+
console.log('newTheme', nextThemeName)
7683
},
77-
{ immediate: false } // Initialization is handled in onMounted
84+
{ immediate: true }
7885
)
7986
8087
// Handle error notifications
@@ -304,10 +311,6 @@ watch(
304311
)
305312
306313
onMounted(() => {
307-
// Set initial body class
308-
document.body.classList.add(themeStore.themeMode)
309-
document.body.classList.add(uiSettingsStore.fontSizeClass)
310-
311314
window.addEventListener('keydown', handleEscKey)
312315
313316
// initialize store data

src/renderer/src/components/markdown/MarkdownRenderer.vue

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
<NodeRenderer
44
:content="debouncedContent"
55
:isDark="themeStore.isDark"
6+
:codeBlockDarkTheme="codeBlockDarkTheme"
7+
:codeBlockLightTheme="codeBlockLightTheme"
68
:codeBlockMonacoOptions="codeBlockMonacoOption"
79
@copy="$emit('copy', $event)"
810
/>
@@ -44,6 +46,9 @@ const referenceNode = ref<HTMLElement | null>(null)
4446
const debouncedContent = ref(props.content)
4547
const effectiveMessageId = computed(() => props.messageId ?? fallbackMessageId)
4648
const effectiveThreadId = computed(() => props.threadId ?? fallbackThreadId)
49+
const codeBlockThemes = ['vitesse-dark', 'vitesse-light'] as const
50+
const codeBlockDarkTheme = codeBlockThemes[0]
51+
const codeBlockLightTheme = codeBlockThemes[1]
4752
const codeBlockMonacoOption = computed(() => ({
4853
fontFamily: uiSettingsStore.formattedCodeFontFamily
4954
}))
@@ -114,6 +119,11 @@ setCustomComponents({
114119
}
115120
return h(CodeBlockNode, {
116121
..._props,
122+
isDark: themeStore.isDark,
123+
darkTheme: codeBlockDarkTheme,
124+
lightTheme: codeBlockLightTheme,
125+
themes: [...codeBlockThemes],
126+
monacoOptions: codeBlockMonacoOption.value,
117127
onPreviewCode(v) {
118128
artifactStore.showArtifact(
119129
{

src/renderer/src/components/sidepanel/viewer/WorkspaceCodePane.vue

Lines changed: 88 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
</template>
1111

1212
<script setup lang="ts">
13-
import * as monaco from 'monaco-editor'
14-
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
13+
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue'
14+
import { useMonaco } from 'stream-monaco'
15+
import { useThemeStore } from '@/stores/theme'
1516
import { useUiSettingsStore } from '@/stores/uiSettingsStore'
1617
1718
type WorkspaceCodeSource = {
@@ -26,13 +27,31 @@ const props = defineProps<{
2627
}>()
2728
2829
const uiSettingsStore = useUiSettingsStore()
30+
const themeStore = useThemeStore()
2931
const editorRef = ref<HTMLElement | null>(null)
30-
const editor = shallowRef<monaco.editor.IStandaloneCodeEditor | null>(null)
31-
const model = shallowRef<monaco.editor.ITextModel | null>(null)
32-
33-
let resizeObserver: ResizeObserver | null = null
34-
let themeObserver: MutationObserver | null = null
35-
let currentSourceId: string | null = null
32+
const editorInitialized = ref(false)
33+
let createEditorTask: Promise<void> | null = null
34+
const resolvedTheme = computed(() => (themeStore.isDark ? 'vitesse-dark' : 'vitesse-light'))
35+
36+
const { createEditor, updateCode, cleanupEditor, getEditorView, getEditor } = useMonaco({
37+
readOnly: true,
38+
domReadOnly: true,
39+
automaticLayout: true,
40+
wordWrap: 'on',
41+
wrappingIndent: 'same',
42+
scrollBeyondLastLine: false,
43+
minimap: { enabled: false },
44+
lineNumbers: 'on',
45+
renderLineHighlight: 'none',
46+
contextmenu: false,
47+
themes: ['vitesse-dark', 'vitesse-light'],
48+
theme: resolvedTheme.value,
49+
fontFamily: uiSettingsStore.formattedCodeFontFamily,
50+
padding: {
51+
top: 12,
52+
bottom: 12
53+
}
54+
})
3655
3756
const LANGUAGE_ALIASES: Record<string, string> = {
3857
md: 'markdown',
@@ -117,105 +136,57 @@ const resolveLanguage = (source: WorkspaceCodeSource): string => {
117136
118137
const resolvedLanguage = computed(() => resolveLanguage(props.source))
119138
120-
const getThemeName = () => {
121-
return document.documentElement.classList.contains('dark') ? 'vs-dark' : 'vs'
122-
}
123-
124-
const applyTheme = () => {
125-
monaco.editor.setTheme(getThemeName())
139+
const applyFontFamily = (fontFamily: string) => {
140+
getEditorView()?.updateOptions({ fontFamily })
126141
}
127142
128-
const layoutEditor = () => {
129-
editor.value?.layout()
130-
}
131-
132-
const disposeModel = () => {
133-
model.value?.dispose()
134-
model.value = null
135-
currentSourceId = null
143+
const applyTheme = async () => {
144+
try {
145+
getEditor().setTheme(resolvedTheme.value)
146+
} catch (error) {
147+
console.warn('[WorkspaceCodePane] Failed to apply Monaco theme:', error)
148+
}
136149
}
137150
138-
const syncModel = () => {
139-
if (!editor.value) {
151+
const syncEditor = async () => {
152+
const editorElement = editorRef.value
153+
if (!editorElement) {
140154
return
141155
}
142156
143-
const nextLanguage = resolvedLanguage.value
144157
const nextContent = props.source.content ?? ''
158+
const nextLanguage = resolvedLanguage.value
159+
const hasEditor = Boolean(editorElement.querySelector('.monaco-editor'))
145160
146-
if (!model.value || currentSourceId !== props.source.id) {
147-
disposeModel()
148-
model.value = monaco.editor.createModel(nextContent, nextLanguage)
149-
currentSourceId = props.source.id
150-
editor.value.setModel(model.value)
151-
return
152-
}
153-
154-
if (model.value.getLanguageId() !== nextLanguage) {
155-
monaco.editor.setModelLanguage(model.value, nextLanguage)
156-
}
157-
158-
if (model.value.getValue() !== nextContent) {
159-
model.value.setValue(nextContent)
160-
}
161-
}
161+
if (!hasEditor || !editorInitialized.value) {
162+
if (createEditorTask) {
163+
await createEditorTask
164+
return
165+
}
162166
163-
const ensureEditor = async () => {
164-
if (editor.value || !editorRef.value) {
167+
createEditorTask = (async () => {
168+
await createEditor(editorElement, nextContent, nextLanguage)
169+
editorInitialized.value = true
170+
await applyTheme()
171+
applyFontFamily(uiSettingsStore.formattedCodeFontFamily)
172+
})()
173+
174+
try {
175+
await createEditorTask
176+
} finally {
177+
createEditorTask = null
178+
}
165179
return
166180
}
167181
168-
applyTheme()
169-
170-
editor.value = monaco.editor.create(editorRef.value, {
171-
readOnly: true,
172-
domReadOnly: true,
173-
automaticLayout: false,
174-
wordWrap: 'on',
175-
wrappingIndent: 'same',
176-
scrollBeyondLastLine: false,
177-
minimap: { enabled: false },
178-
lineNumbers: 'on',
179-
renderLineHighlight: 'none',
180-
contextmenu: false,
181-
fontFamily: uiSettingsStore.formattedCodeFontFamily,
182-
padding: {
183-
top: 12,
184-
bottom: 12
185-
}
186-
})
187-
188-
syncModel()
189-
await nextTick()
190-
layoutEditor()
182+
updateCode(nextContent, nextLanguage)
191183
}
192184
193-
onMounted(() => {
194-
void ensureEditor()
195-
196-
if (typeof ResizeObserver !== 'undefined' && editorRef.value) {
197-
resizeObserver = new ResizeObserver(() => {
198-
layoutEditor()
199-
})
200-
resizeObserver.observe(editorRef.value)
201-
}
202-
203-
themeObserver = new MutationObserver(() => {
204-
applyTheme()
205-
})
206-
themeObserver.observe(document.documentElement, {
207-
attributes: true,
208-
attributeFilter: ['class']
209-
})
210-
})
211-
212185
watch(
213-
() => [props.source.id, props.source.content, props.source.language, props.source.type] as const,
186+
() => [editorRef.value, props.source.id, props.source.content, resolvedLanguage.value] as const,
214187
async () => {
215-
await ensureEditor()
216-
syncModel()
217188
await nextTick()
218-
layoutEditor()
189+
await syncEditor()
219190
},
220191
{
221192
immediate: true,
@@ -226,18 +197,37 @@ watch(
226197
watch(
227198
() => uiSettingsStore.formattedCodeFontFamily,
228199
(fontFamily) => {
229-
editor.value?.updateOptions({ fontFamily })
230-
layoutEditor()
200+
applyFontFamily(fontFamily)
201+
}
202+
)
203+
204+
watch(
205+
resolvedTheme,
206+
() => {
207+
if (!editorInitialized.value) {
208+
return
209+
}
210+
211+
void applyTheme()
212+
},
213+
{
214+
flush: 'post'
231215
}
232216
)
233217
218+
watch(editorRef, (value) => {
219+
if (value) {
220+
return
221+
}
222+
223+
cleanupEditor()
224+
editorInitialized.value = false
225+
createEditorTask = null
226+
})
227+
234228
onBeforeUnmount(() => {
235-
resizeObserver?.disconnect()
236-
resizeObserver = null
237-
themeObserver?.disconnect()
238-
themeObserver = null
239-
editor.value?.dispose()
240-
editor.value = null
241-
disposeModel()
229+
cleanupEditor()
230+
editorInitialized.value = false
231+
createEditorTask = null
242232
})
243233
</script>

0 commit comments

Comments
 (0)