## Summary - Adds file writing capability to Monaco editor in the file viewer - Implements writeFile API on the server for workspace files - Integrates save functionality into the file viewer UI with proper state management ## Bug Fixes (Review Feedback) - Fixed failed save discarding edits when switching files - now checks save result and only proceeds if successful - Fixed refresh overwriting dirty editor state - now prompts for confirmation before discarding edits - Fixed save button unable to save empty files - changed check from `if (content)` to `if (content !== undefined && content !== null)` - Added agent edit conflict detection - when agent edits file while user has unsaved changes, shows conflict dialog with Overwrite/Cancel options - Fixed dialog appearing behind unpinned sidebar - increased alert dialog z-index to z-100 ## Related Issues - Closes #251 --------- Co-authored-by: Jess Chadwick <jchadwick@gmail.com>
105 lines
2.7 KiB
TypeScript
105 lines
2.7 KiB
TypeScript
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
|
|
onSave?: (content: string) => void
|
|
onContentChange?: (content: string) => void
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
const saveContent = () => {
|
|
if (!editor || !props.onSave) return
|
|
props.onSave(editor.getValue())
|
|
}
|
|
|
|
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: false,
|
|
automaticLayout: true,
|
|
lineNumbers: "on",
|
|
minimap: { enabled: false },
|
|
scrollBeyondLastLine: false,
|
|
wordWrap: "off",
|
|
renderWhitespace: "selection",
|
|
fontSize: 13,
|
|
})
|
|
|
|
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, saveContent)
|
|
|
|
editor.onDidChangeModelContent(() => {
|
|
if (props.onContentChange) {
|
|
props.onContentChange(editor.getValue())
|
|
}
|
|
})
|
|
|
|
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} />
|
|
}
|