Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 25 additions & 22 deletions src/renderer/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { DEEPLINK_EVENTS, NOTIFICATION_EVENTS, SHORTCUT_EVENTS } from './events'
import { Toaster } from '@shadcn/components/ui/sonner'
import { useToast } from '@/components/use-toast'
import { useUiSettingsStore } from '@/stores/uiSettingsStore'
import { useThemeStore } from '@/stores/theme'
import { useThemeStore, type ThemeMode } from '@/stores/theme'
import { useLanguageStore } from '@/stores/language'
import { useI18n } from 'vue-i18n'
import TranslatePopup from '@/components/popup/TranslatePopup.vue'
Expand Down Expand Up @@ -56,25 +56,32 @@ const currentErrorId = ref<string | null>(null)
const errorDisplayTimer = ref<number | null>(null)

const { setup: setupMcpDeeplink, cleanup: cleanupMcpDeeplink } = useMcpInstallDeeplinkHandler()
// Watch theme and font size changes, update body class directly

const resolveThemeName = (themeMode: ThemeMode, isDark: boolean) => {
return themeMode === 'system' ? (isDark ? 'dark' : 'light') : themeMode
}

const syncAppearanceClasses = (themeName: string, fontSizeClass: string) => {
if (typeof document === 'undefined') {
return
}

for (const target of [document.documentElement, document.body]) {
target.classList.remove('light', 'dark', 'system')
target.classList.add(themeName)
target.classList.remove('text-xs', 'text-sm', 'text-base', 'text-lg', 'text-xl', 'text-2xl')
target.classList.add(fontSizeClass)
}
}

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 }
)
Comment on lines 77 to 85
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.


// Handle error notifications
Expand Down Expand Up @@ -304,10 +311,6 @@ watch(
)

onMounted(() => {
// Set initial body class
document.body.classList.add(themeStore.themeMode)
document.body.classList.add(uiSettingsStore.fontSizeClass)

window.addEventListener('keydown', handleEscKey)

// initialize store data
Expand Down
10 changes: 10 additions & 0 deletions src/renderer/src/components/markdown/MarkdownRenderer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
<NodeRenderer
:content="debouncedContent"
:isDark="themeStore.isDark"
:codeBlockDarkTheme="codeBlockDarkTheme"
:codeBlockLightTheme="codeBlockLightTheme"
:codeBlockMonacoOptions="codeBlockMonacoOption"
@copy="$emit('copy', $event)"
/>
Expand Down Expand Up @@ -44,6 +46,9 @@ const referenceNode = ref<HTMLElement | null>(null)
const debouncedContent = ref(props.content)
const effectiveMessageId = computed(() => props.messageId ?? fallbackMessageId)
const effectiveThreadId = computed(() => props.threadId ?? fallbackThreadId)
const codeBlockThemes = ['vitesse-dark', 'vitesse-light'] as const
const codeBlockDarkTheme = codeBlockThemes[0]
const codeBlockLightTheme = codeBlockThemes[1]
const codeBlockMonacoOption = computed(() => ({
fontFamily: uiSettingsStore.formattedCodeFontFamily
}))
Expand Down Expand Up @@ -114,6 +119,11 @@ setCustomComponents({
}
return h(CodeBlockNode, {
..._props,
isDark: themeStore.isDark,
darkTheme: codeBlockDarkTheme,
lightTheme: codeBlockLightTheme,
themes: [...codeBlockThemes],
monacoOptions: codeBlockMonacoOption.value,
onPreviewCode(v) {
artifactStore.showArtifact(
{
Expand Down
186 changes: 88 additions & 98 deletions src/renderer/src/components/sidepanel/viewer/WorkspaceCodePane.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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
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.


const LANGUAGE_ALIASES: Record<string, string> = {
md: 'markdown',
Expand Down Expand Up @@ -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
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).

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,
Expand All @@ -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>
Loading