dsh-coding-sidebar 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +524 -0
- package/cordis.patch.yml +49 -0
- package/lib/client-editor.js +48229 -0
- package/lib/client-mermaid.js +200745 -0
- package/lib/client-registry.js +21867 -0
- package/lib/client-terminal.js +9361 -0
- package/lib/client.js +21867 -0
- package/lib/index.js +4556 -0
- package/lib/invariant.js +22 -0
- package/lib/types/agent-opens.d.ts +65 -0
- package/lib/types/agent-pty.d.ts +226 -0
- package/lib/types/browser-probe.d.ts +15 -0
- package/lib/types/bundle-route.d.ts +12 -0
- package/lib/types/client/BrowserView.d.ts +39 -0
- package/lib/types/client/DiffTab.d.ts +6 -0
- package/lib/types/client/DiffView.d.ts +51 -0
- package/lib/types/client/EditorHost.d.ts +13 -0
- package/lib/types/client/FileTree.d.ts +38 -0
- package/lib/types/client/FreeWindow.d.ts +29 -0
- package/lib/types/client/GitView.d.ts +10 -0
- package/lib/types/client/MarkdownHtml.d.ts +32 -0
- package/lib/types/client/OrphanedTab.d.ts +11 -0
- package/lib/types/client/PdfView.d.ts +6 -0
- package/lib/types/client/RenderBoundary.d.ts +32 -0
- package/lib/types/client/SandboxStatusBar.d.ts +10 -0
- package/lib/types/client/SideCardSection.d.ts +74 -0
- package/lib/types/client/SideChatView.d.ts +16 -0
- package/lib/types/client/Sidebar.d.ts +6 -0
- package/lib/types/client/SubagentView.d.ts +16 -0
- package/lib/types/client/TabBar.d.ts +54 -0
- package/lib/types/client/TerminalView.d.ts +24 -0
- package/lib/types/client/TextEditor.d.ts +10 -0
- package/lib/types/client/TreePanel.d.ts +24 -0
- package/lib/types/client/UploadOverlay.d.ts +20 -0
- package/lib/types/client/add-plugin-modal.d.ts +15 -0
- package/lib/types/client/api.d.ts +275 -0
- package/lib/types/client/binary-download.d.ts +12 -0
- package/lib/types/client/breakpoints.d.ts +17 -0
- package/lib/types/client/browser.d.ts +60 -0
- package/lib/types/client/builtins/index.d.ts +18 -0
- package/lib/types/client/builtins/tabs.d.ts +11 -0
- package/lib/types/client/builtins/viewers.d.ts +3 -0
- package/lib/types/client/chunk-loader.d.ts +131 -0
- package/lib/types/client/chunks/editor.d.ts +9 -0
- package/lib/types/client/chunks/mermaid.d.ts +10 -0
- package/lib/types/client/chunks/terminal.d.ts +9 -0
- package/lib/types/client/cm-themes.d.ts +24 -0
- package/lib/types/client/conversation-draft.d.ts +14 -0
- package/lib/types/client/desktop-env.d.ts +35 -0
- package/lib/types/client/editor-load.d.ts +66 -0
- package/lib/types/client/frame-batcher.d.ts +24 -0
- package/lib/types/client/icons.d.ts +59 -0
- package/lib/types/client/image-types.d.ts +3 -0
- package/lib/types/client/ime-guard.d.ts +36 -0
- package/lib/types/client/index.d.ts +25 -0
- package/lib/types/client/intercept.d.ts +46 -0
- package/lib/types/client/lang.d.ts +14 -0
- package/lib/types/client/layout-push.d.ts +15 -0
- package/lib/types/client/lazy-chunk.d.ts +25 -0
- package/lib/types/client/link-intercept.d.ts +42 -0
- package/lib/types/client/locales-ar.d.ts +18 -0
- package/lib/types/client/locales-de.d.ts +4 -0
- package/lib/types/client/locales-fr.d.ts +10 -0
- package/lib/types/client/locales-hi.d.ts +17 -0
- package/lib/types/client/locales-id.d.ts +15 -0
- package/lib/types/client/locales-it.d.ts +8 -0
- package/lib/types/client/locales-ja.d.ts +17 -0
- package/lib/types/client/locales-ko.d.ts +9 -0
- package/lib/types/client/locales-nl.d.ts +16 -0
- package/lib/types/client/locales-pl.d.ts +19 -0
- package/lib/types/client/locales-pt.d.ts +1 -0
- package/lib/types/client/locales-ru.d.ts +13 -0
- package/lib/types/client/locales-sv.d.ts +1 -0
- package/lib/types/client/locales-th.d.ts +17 -0
- package/lib/types/client/locales-tr.d.ts +17 -0
- package/lib/types/client/locales-vi.d.ts +17 -0
- package/lib/types/client/locales-zh-HK.d.ts +32 -0
- package/lib/types/client/locales-zh-MO.d.ts +32 -0
- package/lib/types/client/locales-zh-TW.d.ts +32 -0
- package/lib/types/client/locales.d.ts +452 -0
- package/lib/types/client/markdown-html.d.ts +96 -0
- package/lib/types/client/markdown-images.d.ts +45 -0
- package/lib/types/client/markdown-labels.d.ts +38 -0
- package/lib/types/client/md-toc.d.ts +4 -0
- package/lib/types/client/mermaid-blocks.d.ts +47 -0
- package/lib/types/client/mermaid-sanitize.d.ts +2 -0
- package/lib/types/client/mermaid.d.ts +12 -0
- package/lib/types/client/open-when-sized.d.ts +20 -0
- package/lib/types/client/open-with-settings.d.ts +4 -0
- package/lib/types/client/open-with.d.ts +87 -0
- package/lib/types/client/openpath-intercept.d.ts +85 -0
- package/lib/types/client/paths.d.ts +43 -0
- package/lib/types/client/pdf-types.d.ts +2 -0
- package/lib/types/client/pinned.d.ts +86 -0
- package/lib/types/client/plugin-settings.d.ts +8 -0
- package/lib/types/client/plugins-shared.d.ts +27 -0
- package/lib/types/client/plugins-tabs.d.ts +3 -0
- package/lib/types/client/plugins-viewers.d.ts +3 -0
- package/lib/types/client/prefs.d.ts +41 -0
- package/lib/types/client/produced-files.d.ts +29 -0
- package/lib/types/client/selection-payload.d.ts +27 -0
- package/lib/types/client/service.d.ts +439 -0
- package/lib/types/client/settings-nav-icon.d.ts +19 -0
- package/lib/types/client/shell-presets.d.ts +48 -0
- package/lib/types/client/sidechat-transcript.d.ts +103 -0
- package/lib/types/client/split-pane.d.ts +36 -0
- package/lib/types/client/state.d.ts +432 -0
- package/lib/types/client/subagent-detect.d.ts +54 -0
- package/lib/types/client/subagent-jobs.d.ts +63 -0
- package/lib/types/client/tab-content-memo.d.ts +40 -0
- package/lib/types/client/terminal-font.d.ts +78 -0
- package/lib/types/client/terminal-links.d.ts +119 -0
- package/lib/types/client/theme.d.ts +45 -0
- package/lib/types/client/titlebar-strip.d.ts +23 -0
- package/lib/types/client/upload.d.ts +69 -0
- package/lib/types/client/wco.d.ts +47 -0
- package/lib/types/config.d.ts +64 -0
- package/lib/types/context-types.d.ts +605 -0
- package/lib/types/fs-operations.d.ts +28 -0
- package/lib/types/fs-search.d.ts +24 -0
- package/lib/types/fs-tree.d.ts +51 -0
- package/lib/types/git.d.ts +124 -0
- package/lib/types/html-route.d.ts +59 -0
- package/lib/types/index.d.ts +47 -0
- package/lib/types/invariant.d.ts +15 -0
- package/lib/types/jobs-routes.d.ts +45 -0
- package/lib/types/open-external.d.ts +24 -0
- package/lib/types/path-security.d.ts +20 -0
- package/lib/types/prefs-shared.d.ts +236 -0
- package/lib/types/pty-deps.d.ts +78 -0
- package/lib/types/pty-manager.d.ts +168 -0
- package/lib/types/sidechat-core.d.ts +172 -0
- package/lib/types/sidechat-routes.d.ts +30 -0
- package/lib/types/subagent-activity.d.ts +44 -0
- package/lib/types/subagent-live-route.d.ts +44 -0
- package/lib/types/tools.d.ts +34 -0
- package/lib/types/trust-fence.d.ts +24 -0
- package/lib/types/wire.d.ts +38 -0
- package/package.json +172 -0
- package/scripts/install.ps1 +306 -0
- package/scripts/install.sh +287 -0
- package/src/agent-opens.ts +265 -0
- package/src/agent-pty.ts +527 -0
- package/src/browser-probe.ts +26 -0
- package/src/bundle-route.ts +129 -0
- package/src/client/BrowserView.tsx +289 -0
- package/src/client/DiffTab.tsx +114 -0
- package/src/client/DiffView.tsx +305 -0
- package/src/client/EditorHost.tsx +558 -0
- package/src/client/FileTree.tsx +669 -0
- package/src/client/FreeWindow.tsx +337 -0
- package/src/client/GitView.tsx +717 -0
- package/src/client/MarkdownHtml.tsx +296 -0
- package/src/client/OrphanedTab.tsx +27 -0
- package/src/client/PdfView.tsx +110 -0
- package/src/client/RenderBoundary.tsx +49 -0
- package/src/client/SandboxStatusBar.tsx +60 -0
- package/src/client/SideCardSection.module.css +960 -0
- package/src/client/SideCardSection.tsx +1163 -0
- package/src/client/SideChatView.module.css +437 -0
- package/src/client/SideChatView.tsx +645 -0
- package/src/client/Sidebar.tsx +1370 -0
- package/src/client/SubagentView.module.css +547 -0
- package/src/client/SubagentView.tsx +883 -0
- package/src/client/TabBar.tsx +365 -0
- package/src/client/TerminalView.tsx +424 -0
- package/src/client/TextEditor.tsx +468 -0
- package/src/client/TreePanel.tsx +275 -0
- package/src/client/UploadOverlay.tsx +62 -0
- package/src/client/add-plugin-modal.tsx +199 -0
- package/src/client/api.ts +357 -0
- package/src/client/binary-download.tsx +23 -0
- package/src/client/breakpoints.ts +61 -0
- package/src/client/browser.ts +167 -0
- package/src/client/builtins/index.ts +37 -0
- package/src/client/builtins/tabs.tsx +340 -0
- package/src/client/builtins/viewers.tsx +125 -0
- package/src/client/chunk-loader.ts +358 -0
- package/src/client/chunks/editor.tsx +9 -0
- package/src/client/chunks/mermaid.tsx +10 -0
- package/src/client/chunks/terminal.tsx +9 -0
- package/src/client/cm-themes.ts +129 -0
- package/src/client/conversation-draft.ts +29 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/desktop-env.ts +72 -0
- package/src/client/editor-load.ts +92 -0
- package/src/client/frame-batcher.ts +56 -0
- package/src/client/icons.tsx +152 -0
- package/src/client/image-types.ts +8 -0
- package/src/client/ime-guard.ts +47 -0
- package/src/client/index.tsx +427 -0
- package/src/client/intercept.tsx +188 -0
- package/src/client/lang.ts +225 -0
- package/src/client/layout-push.ts +24 -0
- package/src/client/layout.css +90 -0
- package/src/client/lazy-chunk.tsx +89 -0
- package/src/client/link-intercept.ts +73 -0
- package/src/client/locales-ar.ts +368 -0
- package/src/client/locales-de.ts +353 -0
- package/src/client/locales-fr.ts +360 -0
- package/src/client/locales-hi.ts +367 -0
- package/src/client/locales-id.ts +365 -0
- package/src/client/locales-it.ts +358 -0
- package/src/client/locales-ja.ts +367 -0
- package/src/client/locales-ko.ts +359 -0
- package/src/client/locales-nl.ts +365 -0
- package/src/client/locales-pl.ts +369 -0
- package/src/client/locales-pt.ts +350 -0
- package/src/client/locales-ru.ts +365 -0
- package/src/client/locales-sv.ts +350 -0
- package/src/client/locales-th.ts +367 -0
- package/src/client/locales-tr.ts +367 -0
- package/src/client/locales-vi.ts +367 -0
- package/src/client/locales-zh-HK.ts +382 -0
- package/src/client/locales-zh-MO.ts +382 -0
- package/src/client/locales-zh-TW.ts +382 -0
- package/src/client/locales.ts +894 -0
- package/src/client/markdown-html.ts +331 -0
- package/src/client/markdown-images.ts +137 -0
- package/src/client/markdown-labels.tsx +52 -0
- package/src/client/md-toc.tsx +127 -0
- package/src/client/mermaid-blocks.ts +110 -0
- package/src/client/mermaid-sanitize.ts +93 -0
- package/src/client/mermaid.tsx +406 -0
- package/src/client/open-when-sized.ts +43 -0
- package/src/client/open-with-settings.tsx +135 -0
- package/src/client/open-with.ts +221 -0
- package/src/client/openpath-intercept.ts +143 -0
- package/src/client/paths.ts +63 -0
- package/src/client/pdf-types.ts +4 -0
- package/src/client/pinned.ts +183 -0
- package/src/client/plugin-settings.ts +43 -0
- package/src/client/plugins-shared.ts +29 -0
- package/src/client/plugins-tabs.ts +96 -0
- package/src/client/plugins-viewers.ts +30 -0
- package/src/client/prefs.ts +210 -0
- package/src/client/produced-files.ts +120 -0
- package/src/client/selection-payload.ts +86 -0
- package/src/client/service.ts +859 -0
- package/src/client/settings-nav-icon.ts +45 -0
- package/src/client/shell-presets.ts +83 -0
- package/src/client/sidebar.module.css +2788 -0
- package/src/client/sidechat-transcript.ts +321 -0
- package/src/client/split-pane.tsx +331 -0
- package/src/client/state.ts +1497 -0
- package/src/client/subagent-detect.ts +137 -0
- package/src/client/subagent-jobs.ts +161 -0
- package/src/client/tab-content-memo.ts +55 -0
- package/src/client/terminal-font.ts +224 -0
- package/src/client/terminal-links.ts +194 -0
- package/src/client/theme.ts +110 -0
- package/src/client/titlebar-strip.ts +37 -0
- package/src/client/upload.ts +187 -0
- package/src/client/wco.ts +120 -0
- package/src/config.ts +154 -0
- package/src/context-types.ts +611 -0
- package/src/fs-operations.ts +95 -0
- package/src/fs-search.ts +113 -0
- package/src/fs-tree.ts +158 -0
- package/src/git.ts +446 -0
- package/src/html-route.ts +106 -0
- package/src/index.ts +1291 -0
- package/src/invariant.ts +32 -0
- package/src/jobs-routes.ts +257 -0
- package/src/open-external.ts +90 -0
- package/src/path-security.ts +75 -0
- package/src/prefs-shared.ts +280 -0
- package/src/pty-deps.ts +240 -0
- package/src/pty-manager.ts +376 -0
- package/src/sidechat-core.ts +468 -0
- package/src/sidechat-routes.ts +344 -0
- package/src/subagent-activity.ts +91 -0
- package/src/subagent-live-route.ts +93 -0
- package/src/tools.ts +482 -0
- package/src/trust-fence.ts +84 -0
- package/src/wire.ts +100 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The code/markdown file viewer: a CodeMirror 6 editor with line wrapping,
|
|
3
|
+
* syntax highlighting (extension-keyed language), a dirty dot and Ctrl/Cmd+S
|
|
4
|
+
* save, and a preview/edit toggle for markdown files. Registered as the
|
|
5
|
+
* `code` (catch-all) and `markdown` built-in viewers; the editor tab host
|
|
6
|
+
* fetches the content through the fsRead strategy and passes it in props,
|
|
7
|
+
* so this component never fetches or dispatches — it only edits.
|
|
8
|
+
*
|
|
9
|
+
* The toolbar (mode toggle / dirty dot / save / status) renders as its own
|
|
10
|
+
* row below the host's title bar, VSCode-style — unless the host passes
|
|
11
|
+
* `toolbar: 'host'` (the merged editor-explorer mode), in which case this
|
|
12
|
+
* component skips the row and reports state + registers commands through
|
|
13
|
+
* the FileViewerProps toolbar callbacks so the host's path-input header
|
|
14
|
+
* renders the controls instead.
|
|
15
|
+
*/
|
|
16
|
+
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
17
|
+
import { createPortal } from 'react-dom'
|
|
18
|
+
import clsx from 'clsx'
|
|
19
|
+
import { EditorState } from '@codemirror/state'
|
|
20
|
+
import { EditorView as CodeMirrorView, keymap, lineNumbers } from '@codemirror/view'
|
|
21
|
+
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'
|
|
22
|
+
import { IconCheckOutline16, MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
23
|
+
import { markdownTextProps } from './markdown-labels.tsx'
|
|
24
|
+
import { api, htmlUrl } from './api.ts'
|
|
25
|
+
import { rewriteLocalImageUrls } from './markdown-images.ts'
|
|
26
|
+
import { languageForPath } from './lang.ts'
|
|
27
|
+
import { cmSurfaceTheme, CmThemeCompartment } from './cm-themes.ts'
|
|
28
|
+
import { isDarkScheme, subscribeColorScheme } from './theme.ts'
|
|
29
|
+
import { SandboxStatusBar } from './SandboxStatusBar.tsx'
|
|
30
|
+
import { appendToDraft } from './conversation-draft.ts'
|
|
31
|
+
import { buildSelectionInsert, linesOfSelection } from './selection-payload.ts'
|
|
32
|
+
import { lazyChunkComponent } from './lazy-chunk.tsx'
|
|
33
|
+
import { analyzeMarkdownHtml } from './markdown-html.ts'
|
|
34
|
+
import { LazyMermaidMarkdown, MarkdownDocument, type MarkdownHtmlMedia } from './MarkdownHtml.tsx'
|
|
35
|
+
import { MdToc } from './md-toc.tsx'
|
|
36
|
+
import { splitMermaidBlocks } from './mermaid-blocks.ts'
|
|
37
|
+
import { t } from './locales.ts'
|
|
38
|
+
import type { EditorToolbarState, FileViewerProps } from './service.ts'
|
|
39
|
+
import css from './sidebar.module.css'
|
|
40
|
+
|
|
41
|
+
/** Previewable files (rendered output vs source editing). */
|
|
42
|
+
type ViewMode = 'preview' | 'edit'
|
|
43
|
+
|
|
44
|
+
/** The floating "add to conversation" action: payload + viewport anchor. */
|
|
45
|
+
interface SelectionPopup {
|
|
46
|
+
insert: string
|
|
47
|
+
left: number
|
|
48
|
+
top: number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The sandbox tokens of the HTML preview iframe. NO allow-same-origin (the
|
|
53
|
+
* preview must stay in an opaque origin — with the route's own origin it
|
|
54
|
+
* could read session data) and NO allow-top-navigation (a previewed page
|
|
55
|
+
* must not hijack the GUI). The user can disable the sandbox per-feature
|
|
56
|
+
* in the side card settings (warned); the toggle below reflects it.
|
|
57
|
+
*/
|
|
58
|
+
export const HTML_IFRAME_SANDBOX = 'allow-scripts allow-popups allow-downloads allow-modals'
|
|
59
|
+
|
|
60
|
+
export function TextEditor(props: FileViewerProps) {
|
|
61
|
+
const { ctx, scope, path, viewerId, content, truncated } = props
|
|
62
|
+
const [mode, setMode] = useState<ViewMode>('preview')
|
|
63
|
+
/** The editor's current text (null while clean); preview renders this. */
|
|
64
|
+
const [draft, setDraft] = useState<string | null>(null)
|
|
65
|
+
const [dirty, setDirty] = useState(false)
|
|
66
|
+
const [saveState, setSaveState] = useState<'idle' | 'saving' | 'saved' | 'failed'>('idle')
|
|
67
|
+
const hostRef = useRef<HTMLDivElement>(null)
|
|
68
|
+
const viewRef = useRef<CodeMirrorView | null>(null)
|
|
69
|
+
const savingRef = useRef(false)
|
|
70
|
+
/** The theme compartment of the current view (reconfigured on scheme flip). */
|
|
71
|
+
const themeCompRef = useRef<CmThemeCompartment | null>(null)
|
|
72
|
+
/** The app's resolved color scheme; the editor re-themes in place on flips. */
|
|
73
|
+
const [dark, setDark] = useState(() => isDarkScheme())
|
|
74
|
+
/** The floating "add to conversation" popup (viewport-anchored; null = hidden). */
|
|
75
|
+
const [popup, setPopup] = useState<SelectionPopup | null>(null)
|
|
76
|
+
/** Live mirror of the popup state for click-time reads (no re-render race). */
|
|
77
|
+
const popupRef = useRef<SelectionPopup | null>(null)
|
|
78
|
+
/** The markdown preview container (selection-containment + line lookup). */
|
|
79
|
+
const mdRef = useRef<HTMLDivElement>(null)
|
|
80
|
+
|
|
81
|
+
const hidePopup = (): void => {
|
|
82
|
+
popupRef.current = null
|
|
83
|
+
setPopup(null)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Anchor the popup above the selection center; clamp inside the viewport. */
|
|
87
|
+
const showPopup = (insert: string, left: number, top: number): void => {
|
|
88
|
+
const next: SelectionPopup = {
|
|
89
|
+
insert,
|
|
90
|
+
left: Math.min(Math.max(left, 80), window.innerWidth - 80),
|
|
91
|
+
top,
|
|
92
|
+
}
|
|
93
|
+
popupRef.current = next
|
|
94
|
+
setPopup(next)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The popup button's click: insert the stored payload into the draft. */
|
|
98
|
+
const commitPopup = (): void => {
|
|
99
|
+
const current = popupRef.current
|
|
100
|
+
if (current === null) return
|
|
101
|
+
appendToDraft(ctx, scope.sessionId, current.insert)
|
|
102
|
+
hidePopup()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
useEffect(() => subscribeColorScheme(() => { setDark(isDarkScheme()) }), [])
|
|
106
|
+
|
|
107
|
+
// A new file (tab switch) starts clean: fresh preview mode, no draft.
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
setMode('preview')
|
|
110
|
+
setDraft(null)
|
|
111
|
+
setDirty(false)
|
|
112
|
+
setSaveState('idle')
|
|
113
|
+
hidePopup()
|
|
114
|
+
}, [content])
|
|
115
|
+
|
|
116
|
+
// Create the CodeMirror editor once the content is loaded. The view owns
|
|
117
|
+
// the document; React only tracks dirty/draft state through the update
|
|
118
|
+
// listener. For markdown the view stays mounted while previewing (hidden),
|
|
119
|
+
// so unsaved edits survive the preview/edit toggle. The theme + syntax
|
|
120
|
+
// colors live in a compartment so a scheme flip reconfigures only that
|
|
121
|
+
// part — the document, undo history and scroll position survive.
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (content === undefined) return
|
|
124
|
+
const host = hostRef.current
|
|
125
|
+
if (host === null) return
|
|
126
|
+
const language = languageForPath(path)
|
|
127
|
+
const themeComp = new CmThemeCompartment()
|
|
128
|
+
themeCompRef.current = themeComp
|
|
129
|
+
const state = EditorState.create({
|
|
130
|
+
doc: content,
|
|
131
|
+
extensions: [
|
|
132
|
+
CodeMirrorView.lineWrapping,
|
|
133
|
+
lineNumbers(),
|
|
134
|
+
history(),
|
|
135
|
+
EditorState.tabSize.of(2),
|
|
136
|
+
CodeMirrorView.contentAttributes.of({ spellcheck: 'false' }),
|
|
137
|
+
cmSurfaceTheme,
|
|
138
|
+
themeComp.of(dark),
|
|
139
|
+
...(language !== null ? [language] : []),
|
|
140
|
+
CodeMirrorView.updateListener.of((update) => {
|
|
141
|
+
if (update.docChanged) {
|
|
142
|
+
setDraft(update.state.doc.toString())
|
|
143
|
+
setDirty(true)
|
|
144
|
+
}
|
|
145
|
+
}),
|
|
146
|
+
keymap.of([
|
|
147
|
+
{
|
|
148
|
+
key: 'Mod-s',
|
|
149
|
+
preventDefault: true,
|
|
150
|
+
run: () => { save(); return true },
|
|
151
|
+
},
|
|
152
|
+
...defaultKeymap,
|
|
153
|
+
...historyKeymap,
|
|
154
|
+
]),
|
|
155
|
+
// Selection popup (the code and markdown editors): a non-empty
|
|
156
|
+
// selection anchors the floating "add to conversation" button above
|
|
157
|
+
// its head. Scrolling (geometry/viewport change) or losing focus
|
|
158
|
+
// hides it; typing collapses the selection and hides it too.
|
|
159
|
+
...(viewerId === 'code' || viewerId === 'markdown' ? [
|
|
160
|
+
CodeMirrorView.updateListener.of((update) => {
|
|
161
|
+
if (update.geometryChanged || update.viewportChanged) {
|
|
162
|
+
hidePopup()
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
if (!update.view.hasFocus) {
|
|
166
|
+
hidePopup()
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
if (!(update.selectionSet || update.docChanged || update.focusChanged)) return
|
|
170
|
+
const sel = update.state.selection.main
|
|
171
|
+
if (sel.empty) {
|
|
172
|
+
hidePopup()
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
const text = update.state.sliceDoc(sel.from, sel.to)
|
|
176
|
+
if (text.trim() === '') {
|
|
177
|
+
hidePopup()
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
// Page coordinates (the document root may scroll); the popup is
|
|
181
|
+
// position:fixed, so convert to viewport coordinates.
|
|
182
|
+
const rect = update.view.coordsAtPos(sel.head)
|
|
183
|
+
if (rect === null) {
|
|
184
|
+
hidePopup()
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
const doc = update.state.doc
|
|
188
|
+
showPopup(
|
|
189
|
+
buildSelectionInsert(path, scope.cwd, {
|
|
190
|
+
start: doc.lineAt(sel.from).number,
|
|
191
|
+
end: doc.lineAt(sel.to).number,
|
|
192
|
+
}, text),
|
|
193
|
+
rect.left - window.scrollX + (rect.right - rect.left) / 2,
|
|
194
|
+
rect.top - window.scrollY,
|
|
195
|
+
)
|
|
196
|
+
}),
|
|
197
|
+
] : []),
|
|
198
|
+
],
|
|
199
|
+
})
|
|
200
|
+
const view = new CodeMirrorView({ state, parent: host })
|
|
201
|
+
viewRef.current = view
|
|
202
|
+
return () => {
|
|
203
|
+
view.destroy()
|
|
204
|
+
viewRef.current = null
|
|
205
|
+
themeCompRef.current = null
|
|
206
|
+
}
|
|
207
|
+
// The keymap's save() reads live refs; scope/path are stable for a
|
|
208
|
+
// tab's lifetime, and the dark flip is handled by the reconfigure
|
|
209
|
+
// effect below (recreating the view here would drop the draft).
|
|
210
|
+
}, [content, path])
|
|
211
|
+
|
|
212
|
+
// Scheme flip: re-theme in place (the compartment holds only the
|
|
213
|
+
// scheme-dependent extensions; everything else is untouched).
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
const view = viewRef.current
|
|
216
|
+
const themeComp = themeCompRef.current
|
|
217
|
+
if (view === null || themeComp === null) return
|
|
218
|
+
view.dispatch({ effects: themeComp.reconfigure(dark) })
|
|
219
|
+
}, [dark])
|
|
220
|
+
|
|
221
|
+
// The editor may have been display:none while previewing; re-measure when
|
|
222
|
+
// it becomes visible again (CodeMirror sizes itself on reveal). A mode
|
|
223
|
+
// flip also invalidates any anchored selection popup.
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
hidePopup()
|
|
226
|
+
if (mode === 'edit') viewRef.current?.requestMeasure()
|
|
227
|
+
}, [mode])
|
|
228
|
+
|
|
229
|
+
const save = (): void => {
|
|
230
|
+
const view = viewRef.current
|
|
231
|
+
if (view === null || savingRef.current) return
|
|
232
|
+
savingRef.current = true
|
|
233
|
+
setSaveState('saving')
|
|
234
|
+
api.fsWrite(scope, path, view.state.doc.toString()).then(() => {
|
|
235
|
+
savingRef.current = false
|
|
236
|
+
setDraft(null)
|
|
237
|
+
setDirty(false)
|
|
238
|
+
setSaveState('saved')
|
|
239
|
+
}).catch(() => {
|
|
240
|
+
savingRef.current = false
|
|
241
|
+
setSaveState('failed')
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const markdown = viewerId === 'markdown'
|
|
246
|
+
const html = viewerId === 'html'
|
|
247
|
+
/** The markdown source the preview renders (draft wins over saved content). */
|
|
248
|
+
const mdText = draft ?? content ?? ''
|
|
249
|
+
/** The preview source: `mdText` with local image destinations rewritten to
|
|
250
|
+
* absolute media URLs (see {@link rewriteLocalImageUrls}); the raw
|
|
251
|
+
* `mdText` stays untouched for selection/line lookup and for mermaid-block
|
|
252
|
+
* detection, which are unaffected by image syntax. */
|
|
253
|
+
const previewText = markdown
|
|
254
|
+
? rewriteLocalImageUrls(mdText, scope, path, window.location.origin)
|
|
255
|
+
: mdText
|
|
256
|
+
/** md/mermaid block split for the preview (mermaid fences lift out). Split
|
|
257
|
+
* only in preview mode: edit-mode keystrokes must not re-scan the source. */
|
|
258
|
+
const mdBlocks = useMemo(
|
|
259
|
+
() => (markdown && mode === 'preview' ? splitMermaidBlocks(mdText) : []),
|
|
260
|
+
[markdown, mode, mdText],
|
|
261
|
+
)
|
|
262
|
+
/** Raw-HTML analysis (block runs lifted out + inline gate). Non-null only
|
|
263
|
+
* for documents that actually contain HTML — plain markdown keeps the
|
|
264
|
+
* legacy single-pass render path below, byte-for-byte. */
|
|
265
|
+
const htmlInfo = useMemo(
|
|
266
|
+
() => (markdown && mode === 'preview' ? analyzeMarkdownHtml(mdText) : null),
|
|
267
|
+
[markdown, mode, mdText],
|
|
268
|
+
)
|
|
269
|
+
const hasMermaid = useMemo(
|
|
270
|
+
() => htmlInfo !== null
|
|
271
|
+
? htmlInfo.segments.some((segment) => segment.kind === 'markdown'
|
|
272
|
+
&& splitMermaidBlocks(segment.text).some((block) => block.kind === 'mermaid'))
|
|
273
|
+
: mdBlocks.some((block) => block.kind === 'mermaid'),
|
|
274
|
+
[htmlInfo, mdBlocks],
|
|
275
|
+
)
|
|
276
|
+
/** The media context for the split renderer (local-src rewriting inside
|
|
277
|
+
* sanitized HTML). Memoized on primitives: MarkdownDocument sanitizes per
|
|
278
|
+
* `media` identity, so a fresh object per render would re-sanitize every
|
|
279
|
+
* keystroke. */
|
|
280
|
+
const htmlMedia = useMemo<MarkdownHtmlMedia>(
|
|
281
|
+
() => ({ scope, path, origin: window.location.origin }),
|
|
282
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
283
|
+
[scope.sessionId, scope.cwd, path],
|
|
284
|
+
)
|
|
285
|
+
const codeLabels = { copyLabel: t('copy'), copiedLabel: t('copied') }
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Selection popup for the markdown preview: a mouse-up inside the preview
|
|
289
|
+
* container anchors the floating "add to conversation" button above the
|
|
290
|
+
* selection. Line numbers come from a best-effort reverse-search of the
|
|
291
|
+
* selected text in the source ({@link linesOfSelection} — an ambiguous or
|
|
292
|
+
* missing hit omits them). The button's own mousedown preventDefaults so
|
|
293
|
+
* the selection survives until the click commits.
|
|
294
|
+
*/
|
|
295
|
+
const handlePreviewMouseUp = (): void => {
|
|
296
|
+
const sel = window.getSelection()
|
|
297
|
+
if (sel === null || sel.isCollapsed || sel.anchorNode === null || sel.focusNode === null) {
|
|
298
|
+
hidePopup()
|
|
299
|
+
return
|
|
300
|
+
}
|
|
301
|
+
const host = mdRef.current
|
|
302
|
+
if (host === null || !host.contains(sel.anchorNode) || !host.contains(sel.focusNode)) {
|
|
303
|
+
hidePopup()
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
const text = sel.toString()
|
|
307
|
+
if (text.trim() === '') {
|
|
308
|
+
hidePopup()
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
const rect = sel.getRangeAt(0).getBoundingClientRect()
|
|
312
|
+
const lines = linesOfSelection(mdText, text)
|
|
313
|
+
showPopup(
|
|
314
|
+
buildSelectionInsert(path, scope.cwd, lines ?? undefined, text),
|
|
315
|
+
rect.left + rect.width / 2,
|
|
316
|
+
rect.top,
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
const editable = content !== undefined
|
|
320
|
+
const saveLabel = saveState === 'saving' ? t('loading') : saveState === 'saved' ? t('saved') : saveState === 'failed' ? t('saveFailed') : ''
|
|
321
|
+
// Per-feature sandbox escape hatch: the global side card setting (warned)
|
|
322
|
+
// plus a per-surface temporary unlock. The unlock state starts at the
|
|
323
|
+
// "default unsafe" pref so a preview can open straight into the red
|
|
324
|
+
// unsandboxed state (still restorable from the status row). With the
|
|
325
|
+
// sandbox OFF the preview iframe drops its sandbox attribute entirely —
|
|
326
|
+
// the previewed page then runs on the GUI's own origin with full session
|
|
327
|
+
// access.
|
|
328
|
+
const [localUnlock, setLocalUnlock] = useState(() => props.store?.getPrefs().htmlViewerDefaultUnsafe === true)
|
|
329
|
+
const htmlNoSandbox = props.store?.getPrefs().htmlViewerNoSandbox === true || localUnlock
|
|
330
|
+
|
|
331
|
+
// Host-toolbar mode (the merged editor header renders the controls): skip
|
|
332
|
+
// the own toolbar row, report the state after every relevant render (the
|
|
333
|
+
// JSON key guards redundant calls), and register the commands on mount.
|
|
334
|
+
const hostToolbar = props.toolbar === 'host'
|
|
335
|
+
const lastToolbarRef = useRef('')
|
|
336
|
+
useEffect(() => {
|
|
337
|
+
if (!hostToolbar) return
|
|
338
|
+
const state: EditorToolbarState = { modes: markdown || html, mode, dirty, editable, saveState }
|
|
339
|
+
const key = JSON.stringify(state)
|
|
340
|
+
if (lastToolbarRef.current === key) return
|
|
341
|
+
lastToolbarRef.current = key
|
|
342
|
+
props.onToolbarState?.(state)
|
|
343
|
+
})
|
|
344
|
+
useEffect(() => {
|
|
345
|
+
if (!hostToolbar) return
|
|
346
|
+
// `save` reads live refs only, and `setMode` is the stable state setter —
|
|
347
|
+
// registering this render's closures is safe for the mount's lifetime.
|
|
348
|
+
props.onToolbarControls?.({ setMode, save })
|
|
349
|
+
return () => { props.onToolbarControls?.(null) }
|
|
350
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
351
|
+
}, [hostToolbar])
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
<>
|
|
355
|
+
{!hostToolbar && (
|
|
356
|
+
<div className={css.editorHeader}>
|
|
357
|
+
{(markdown || html) && (
|
|
358
|
+
<div className={css.editorModeToggle}>
|
|
359
|
+
<button
|
|
360
|
+
type="button"
|
|
361
|
+
className={clsx(css.editorModeButton, mode === 'preview' && css.editorModeActive)}
|
|
362
|
+
onClick={() => { setMode('preview') }}
|
|
363
|
+
>
|
|
364
|
+
{t('preview')}
|
|
365
|
+
</button>
|
|
366
|
+
<button
|
|
367
|
+
type="button"
|
|
368
|
+
className={clsx(css.editorModeButton, mode === 'edit' && css.editorModeActive)}
|
|
369
|
+
onClick={() => { setMode('edit') }}
|
|
370
|
+
>
|
|
371
|
+
{t('edit')}
|
|
372
|
+
</button>
|
|
373
|
+
</div>
|
|
374
|
+
)}
|
|
375
|
+
{dirty && <span className={css.dirtyDot} title={t('unsaved')} />}
|
|
376
|
+
{editable && (
|
|
377
|
+
<button
|
|
378
|
+
type="button"
|
|
379
|
+
className={css.iconButton}
|
|
380
|
+
aria-label={t('save')}
|
|
381
|
+
title={`${t('save')} (Ctrl/Cmd+S)`}
|
|
382
|
+
onClick={save}
|
|
383
|
+
>
|
|
384
|
+
<IconCheckOutline16 />
|
|
385
|
+
</button>
|
|
386
|
+
)}
|
|
387
|
+
{saveLabel !== '' && <span className={clsx(css.editorStatus, saveState === 'failed' && css.editorStatusError)}>{saveLabel}</span>}
|
|
388
|
+
</div>
|
|
389
|
+
)}
|
|
390
|
+
{editable && (
|
|
391
|
+
<>
|
|
392
|
+
{truncated === true && mode === 'edit' && <div className={css.editorBanner}>{t('truncation')}</div>}
|
|
393
|
+
<div
|
|
394
|
+
className={clsx(css.editorCm, (markdown || html) && mode === 'preview' && css.editorCmHidden)}
|
|
395
|
+
ref={hostRef}
|
|
396
|
+
/>
|
|
397
|
+
</>
|
|
398
|
+
)}
|
|
399
|
+
{markdown && mode === 'preview' && (
|
|
400
|
+
<div
|
|
401
|
+
className={css.editorMd}
|
|
402
|
+
ref={mdRef}
|
|
403
|
+
onMouseUp={handlePreviewMouseUp}
|
|
404
|
+
onScroll={hidePopup}
|
|
405
|
+
>
|
|
406
|
+
{/* The fence copy-button labels must come from this plugin's own
|
|
407
|
+
dictionary: the DSH MarkdownText/CodeBlock are cordis-free and
|
|
408
|
+
fall back to hardcoded Chinese otherwise (same pattern as the
|
|
409
|
+
chat's AssistantMarkdown). Render-time t() keeps them following
|
|
410
|
+
the active locale on live switches. Plain markdown (no HTML)
|
|
411
|
+
renders exactly as before — one MarkdownText pass for the whole
|
|
412
|
+
document, or the mermaid lazy chunk (single markdown parse;
|
|
413
|
+
cross-fence references/footnotes stay intact) when a mermaid
|
|
414
|
+
fence exists. Documents containing HTML (block runs or inline
|
|
415
|
+
tags) render through the split document renderer: markdown runs
|
|
416
|
+
keep the MarkdownText/mermaid path while raw-HTML runs render
|
|
417
|
+
as sanitized DOM (see markdown-html.tsx). */}
|
|
418
|
+
{/* The outline button rides on top of the preview scroll container
|
|
419
|
+
(sticky, zero-height — first child so it pins from the very
|
|
420
|
+
top) once the document has enough headings. */}
|
|
421
|
+
<MdToc />
|
|
422
|
+
{htmlInfo !== null
|
|
423
|
+
? <MarkdownDocument info={htmlInfo} media={htmlMedia} codeLabels={codeLabels} />
|
|
424
|
+
: hasMermaid
|
|
425
|
+
? <LazyMermaidMarkdown text={previewText} codeLabels={codeLabels} />
|
|
426
|
+
: <MarkdownText {...markdownTextProps(previewText, codeLabels)} />}
|
|
427
|
+
</div>
|
|
428
|
+
)}
|
|
429
|
+
{html && mode === 'preview' && (
|
|
430
|
+
<>
|
|
431
|
+
<SandboxStatusBar
|
|
432
|
+
sandboxed={!htmlNoSandbox}
|
|
433
|
+
local={localUnlock}
|
|
434
|
+
dangerCopy={t('htmlNoSandboxWarning')}
|
|
435
|
+
onUnlock={() => { setLocalUnlock(true) }}
|
|
436
|
+
onRestore={() => { setLocalUnlock(false) }}
|
|
437
|
+
/>
|
|
438
|
+
{/* Route-src (never srcdoc — a srcdoc frame inherits the parent
|
|
439
|
+
origin when unsandboxed; the route URL keeps the frame
|
|
440
|
+
cross-origin by construction). The preview shows the SAVED
|
|
441
|
+
file; the draft is only visible in edit mode. */}
|
|
442
|
+
<iframe
|
|
443
|
+
className={css.editorHtml}
|
|
444
|
+
src={htmlUrl(scope, path)}
|
|
445
|
+
sandbox={htmlNoSandbox ? undefined : HTML_IFRAME_SANDBOX}
|
|
446
|
+
referrerPolicy="no-referrer"
|
|
447
|
+
allow=""
|
|
448
|
+
title={path}
|
|
449
|
+
/>
|
|
450
|
+
</>
|
|
451
|
+
)}
|
|
452
|
+
{popup !== null && createPortal(
|
|
453
|
+
<button
|
|
454
|
+
type="button"
|
|
455
|
+
className={css.selectionPopup}
|
|
456
|
+
style={{ left: popup.left, top: popup.top }}
|
|
457
|
+
// Keep the selection (and CodeMirror focus) alive until the click
|
|
458
|
+
// commits — without this the popup unmounts before click lands.
|
|
459
|
+
onMouseDown={(event) => { event.preventDefault() }}
|
|
460
|
+
onClick={commitPopup}
|
|
461
|
+
>
|
|
462
|
+
{t('addToConversation')}
|
|
463
|
+
</button>,
|
|
464
|
+
document.body,
|
|
465
|
+
)}
|
|
466
|
+
</>
|
|
467
|
+
)
|
|
468
|
+
}
|