feat(ui): add Monaco changes/files right drawer viewers

Use OpenCode v2 file APIs for browsing and Monaco DiffEditor for session snapshot diffs, with local baseline language metadata and optional CDN language loading.
This commit is contained in:
Shantur Rathore
2026-02-09 21:00:40 +00:00
parent 4eded9e204
commit b59e85abda
17 changed files with 1035 additions and 30 deletions

View File

@@ -0,0 +1,110 @@
import { createEffect, createSignal, onCleanup, onMount } from "solid-js"
import { loadMonaco } from "../../lib/monaco/setup"
import { getOrCreateTextModel } from "../../lib/monaco/model-cache"
import { inferMonacoLanguageId } from "../../lib/monaco/language"
import { ensureMonacoLanguageLoaded } from "../../lib/monaco/setup"
import { useTheme } from "../../lib/theme"
interface MonacoDiffViewerProps {
scopeKey: string
path: string
before: string
after: string
viewMode?: "split" | "unified"
contextMode?: "expanded" | "collapsed"
}
export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
const { isDark } = useTheme()
let host: HTMLDivElement | undefined
let diffEditor: any = null
let monaco: any = null
const [ready, setReady] = createSignal(false)
const disposeEditor = () => {
try {
diffEditor?.setModel(null as any)
} catch {
// ignore
}
try {
diffEditor?.dispose()
} catch {
// ignore
}
diffEditor = null
}
onMount(() => {
let cancelled = false
void (async () => {
monaco = await loadMonaco()
if (cancelled) return
if (!host || !monaco) return
monaco.editor.setTheme(isDark() ? "vs-dark" : "vs")
diffEditor = monaco.editor.createDiffEditor(host, {
readOnly: true,
automaticLayout: true,
renderSideBySide: true,
renderSideBySideInlineBreakpoint: 0,
minimap: { enabled: false },
scrollBeyondLastLine: false,
renderWhitespace: "selection",
fontSize: 13,
wordWrap: "off",
})
setReady(true)
})()
onCleanup(() => {
cancelled = true
setReady(false)
disposeEditor()
})
})
createEffect(() => {
if (!ready() || !monaco || !diffEditor) return
monaco.editor.setTheme(isDark() ? "vs-dark" : "vs")
})
createEffect(() => {
if (!ready() || !monaco || !diffEditor) return
const viewMode = props.viewMode === "unified" ? "unified" : "split"
const contextMode = props.contextMode === "collapsed" ? "collapsed" : "expanded"
diffEditor.updateOptions({
renderSideBySide: viewMode === "split",
renderSideBySideInlineBreakpoint: 0,
hideUnchangedRegions:
contextMode === "collapsed"
? { enabled: true }
: { enabled: false },
})
})
createEffect(() => {
if (!ready() || !monaco || !diffEditor) return
const languageId = inferMonacoLanguageId(monaco, props.path)
const beforeKey = `${props.scopeKey}:diff:${props.path}:before`
const afterKey = `${props.scopeKey}:diff:${props.path}:after`
const original = getOrCreateTextModel({ monaco, cacheKey: beforeKey, value: props.before, languageId })
const modified = getOrCreateTextModel({ monaco, cacheKey: afterKey, value: props.after, languageId })
diffEditor.setModel({ original, modified })
void ensureMonacoLanguageLoaded(languageId).then(() => {
try {
monaco.editor.setModelLanguage(original, languageId)
monaco.editor.setModelLanguage(modified, languageId)
} catch {
// ignore
}
})
})
return <div class="monaco-viewer" ref={host} />
}

View File

@@ -0,0 +1,89 @@
import { createEffect, createSignal, onCleanup, onMount } from "solid-js"
import { loadMonaco } from "../../lib/monaco/setup"
import { getOrCreateTextModel } from "../../lib/monaco/model-cache"
import { inferMonacoLanguageId } from "../../lib/monaco/language"
import { ensureMonacoLanguageLoaded } from "../../lib/monaco/setup"
import { useTheme } from "../../lib/theme"
interface MonacoFileViewerProps {
scopeKey: string
path: string
content: string
}
export function MonacoFileViewer(props: MonacoFileViewerProps) {
const { isDark } = useTheme()
let host: HTMLDivElement | undefined
let editor: any = null
let monaco: any = null
const [ready, setReady] = createSignal(false)
const disposeEditor = () => {
try {
editor?.setModel(null)
} catch {
// ignore
}
try {
editor?.dispose()
} catch {
// ignore
}
editor = null
}
onMount(() => {
let cancelled = false
void (async () => {
monaco = await loadMonaco()
if (cancelled) return
if (!host || !monaco) return
monaco.editor.setTheme(isDark() ? "vs-dark" : "vs")
editor = monaco.editor.create(host, {
value: "",
language: "plaintext",
readOnly: true,
automaticLayout: true,
lineNumbers: "on",
minimap: { enabled: false },
scrollBeyondLastLine: false,
wordWrap: "off",
renderWhitespace: "selection",
fontSize: 13,
})
setReady(true)
})()
onCleanup(() => {
cancelled = true
setReady(false)
disposeEditor()
})
})
createEffect(() => {
if (!ready() || !monaco || !editor) return
monaco.editor.setTheme(isDark() ? "vs-dark" : "vs")
})
createEffect(() => {
if (!ready() || !monaco || !editor) return
const languageId = inferMonacoLanguageId(monaco, props.path)
const cacheKey = `${props.scopeKey}:file:${props.path}`
const model = getOrCreateTextModel({ monaco, cacheKey, value: props.content, languageId })
editor.setModel(model)
void ensureMonacoLanguageLoaded(languageId).then(() => {
try {
monaco.editor.setModelLanguage(model, languageId)
} catch {
// ignore
}
})
})
return <div class="monaco-viewer" ref={host} />
}