From b6c573617120f393f3d090c90168768bdd719009 Mon Sep 17 00:00:00 2001 From: KiKaraage Date: Wed, 19 Aug 2026 20:37:07 +0700 Subject: [PATCH 1/2] Add Markdown Preview mode to the file editor - Add "Preview mode" toggle (eye emoji as icon) beside Comment and Edit, only shown for .md/.markdown/.mdx files - Previewer uses MarkdownContent component with identical styling to chat interface, centered against the modal - The markdown contents is rendered once per entry from a buffer snapshot (no per-keystroke re-rendering); the Monaco editor stays mounted (undo/cursor preserved), read-only in preview - Preview mode can be toggled via Ctrl+Shift+K when EditableFileModal is opened - Local images dropped (same as chat rendering without a messageId); no server changes Assisted-by: DeepSeek V4 Pro in Shelley --- ui/src/styles.css | 13 ++++ ui/src/vue/components/EditableFileModal.vue | 75 ++++++++++++++++++--- ui/src/vue/composables/monacoComments.ts | 2 +- 3 files changed, 81 insertions(+), 9 deletions(-) diff --git a/ui/src/styles.css b/ui/src/styles.css index d7c441b0b..485518431 100644 --- a/ui/src/styles.css +++ b/ui/src/styles.css @@ -6607,6 +6607,19 @@ svg { width: 100%; } +/* Markdown preview panel inside the editor modal. Chat-style rendering via + .markdown-content, scrollable, prose width capped for readability. */ +.diff-viewer-preview { + height: 100%; + overflow: auto; + padding: 12px 20px; +} + +.diff-viewer-preview .markdown-content { + max-width: 80ch; + margin: 0 auto; +} + /* Diff viewer line hover highlighting (via decoration API) */ .diff-viewer-editor .monaco-editor .diff-viewer-line-hover { background-color: rgba(37, 99, 235, 0.08) !important; diff --git a/ui/src/vue/components/EditableFileModal.vue b/ui/src/vue/components/EditableFileModal.vue index 0b7dd25b8..85cad33ab 100644 --- a/ui/src/vue/components/EditableFileModal.vue +++ b/ui/src/vue/components/EditableFileModal.vue @@ -30,8 +30,9 @@ -
+
+
- +
@@ -96,6 +96,7 @@ v-if=" mode === 'preview' && monacoLoaded && content !== null && loadStatus === 'loaded' " + ref="previewRef" class="diff-viewer-preview" > @@ -206,6 +207,9 @@ let monacoMod: typeof Monaco | null = null; const containerRef = ref(null); const contentRef = ref(null); const vimStatusRef = ref(null); +// Scrollable preview panel (markdown preview mode); target for vim-style +// navigation keys. +const previewRef = ref(null); let saveTimeout: number | null = null; let statusTimeout: number | null = null; const [vimEnabledRef, setVimEnabledFn] = useVimEnabled(); @@ -460,8 +464,80 @@ watch( { immediate: true }, ); +// --- Preview vim-style navigation --- +// When vim is enabled and the preview panel is shown, normal-mode keys scroll +// the rendered document. `i`/`a`/`o` return to the editor (vim's "insert +// mode" ≈ edit mode). Only active while focus isn't in a form control. +const PREVIEW_VIM_KEYS: Record void> = { + j: () => previewRef.value?.scrollBy({ top: 40, behavior: "auto" }), + k: () => previewRef.value?.scrollBy({ top: -40, behavior: "auto" }), + "Ctrl+d": () => + previewRef.value?.scrollBy({ top: previewRef.value.clientHeight / 2, behavior: "auto" }), + "Ctrl+u": () => + previewRef.value?.scrollBy({ top: -previewRef.value.clientHeight / 2, behavior: "auto" }), + "Ctrl+f": () => + previewRef.value?.scrollBy({ top: previewRef.value.clientHeight, behavior: "auto" }), + "Ctrl+b": () => + previewRef.value?.scrollBy({ top: -previewRef.value.clientHeight, behavior: "auto" }), +}; + +let previewVimPendingG = false; +function handlePreviewVimKey(e: KeyboardEvent): boolean { + if (!previewRef.value || !vimEnabled.value || mode.value !== "preview") return false; + const t = e.target as HTMLElement | null; + if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) { + return false; + } + const key = e.key.toLowerCase(); + const plain = !e.ctrlKey && !e.altKey && !e.metaKey; + if (plain && key === "g" && !previewVimPendingG) { + previewVimPendingG = true; + setTimeout(() => (previewVimPendingG = false), 500); + return true; + } + if (plain && key === "g" && previewVimPendingG) { + previewVimPendingG = false; + previewRef.value?.scrollTo({ top: 0, behavior: "auto" }); + return true; + } + previewVimPendingG = false; + // Shift+G jumps to the bottom; check the raw key (lowercase would collide + // with the gg pending state). + if (plain && e.key === "G") { + e.preventDefault(); + previewRef.value?.scrollTo({ top: previewRef.value.scrollHeight, behavior: "auto" }); + return true; + } + if (plain && (key === "i" || key === "a" || key === "o")) { + e.preventDefault(); + mode.value = "edit"; + editor.value?.focus(); + return true; + } + if (plain && (key === "j" || key === "k")) { + e.preventDefault(); + PREVIEW_VIM_KEYS[key](); + return true; + } + if ( + e.ctrlKey && + !e.altKey && + !e.metaKey && + (key === "d" || key === "u" || key === "f" || key === "b") + ) { + e.preventDefault(); + PREVIEW_VIM_KEYS[`Ctrl+${key}`](); + return true; + } + return false; +} + // --- Escape handling (capture phase; vim-aware guard) --- function handleKeyDown(e: KeyboardEvent) { + // Vim-style navigation in the markdown preview (only when vim is enabled). + if (mode.value === "preview" && vimEnabled.value && handlePreviewVimKey(e)) { + return; + } if (e.key === "k" || e.key === "K") { if (e.ctrlKey && e.shiftKey && !e.altKey && !e.metaKey && isMarkdown.value) { e.preventDefault();