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.
Files changed (277) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +524 -0
  3. package/cordis.patch.yml +49 -0
  4. package/lib/client-editor.js +48229 -0
  5. package/lib/client-mermaid.js +200745 -0
  6. package/lib/client-registry.js +21867 -0
  7. package/lib/client-terminal.js +9361 -0
  8. package/lib/client.js +21867 -0
  9. package/lib/index.js +4556 -0
  10. package/lib/invariant.js +22 -0
  11. package/lib/types/agent-opens.d.ts +65 -0
  12. package/lib/types/agent-pty.d.ts +226 -0
  13. package/lib/types/browser-probe.d.ts +15 -0
  14. package/lib/types/bundle-route.d.ts +12 -0
  15. package/lib/types/client/BrowserView.d.ts +39 -0
  16. package/lib/types/client/DiffTab.d.ts +6 -0
  17. package/lib/types/client/DiffView.d.ts +51 -0
  18. package/lib/types/client/EditorHost.d.ts +13 -0
  19. package/lib/types/client/FileTree.d.ts +38 -0
  20. package/lib/types/client/FreeWindow.d.ts +29 -0
  21. package/lib/types/client/GitView.d.ts +10 -0
  22. package/lib/types/client/MarkdownHtml.d.ts +32 -0
  23. package/lib/types/client/OrphanedTab.d.ts +11 -0
  24. package/lib/types/client/PdfView.d.ts +6 -0
  25. package/lib/types/client/RenderBoundary.d.ts +32 -0
  26. package/lib/types/client/SandboxStatusBar.d.ts +10 -0
  27. package/lib/types/client/SideCardSection.d.ts +74 -0
  28. package/lib/types/client/SideChatView.d.ts +16 -0
  29. package/lib/types/client/Sidebar.d.ts +6 -0
  30. package/lib/types/client/SubagentView.d.ts +16 -0
  31. package/lib/types/client/TabBar.d.ts +54 -0
  32. package/lib/types/client/TerminalView.d.ts +24 -0
  33. package/lib/types/client/TextEditor.d.ts +10 -0
  34. package/lib/types/client/TreePanel.d.ts +24 -0
  35. package/lib/types/client/UploadOverlay.d.ts +20 -0
  36. package/lib/types/client/add-plugin-modal.d.ts +15 -0
  37. package/lib/types/client/api.d.ts +275 -0
  38. package/lib/types/client/binary-download.d.ts +12 -0
  39. package/lib/types/client/breakpoints.d.ts +17 -0
  40. package/lib/types/client/browser.d.ts +60 -0
  41. package/lib/types/client/builtins/index.d.ts +18 -0
  42. package/lib/types/client/builtins/tabs.d.ts +11 -0
  43. package/lib/types/client/builtins/viewers.d.ts +3 -0
  44. package/lib/types/client/chunk-loader.d.ts +131 -0
  45. package/lib/types/client/chunks/editor.d.ts +9 -0
  46. package/lib/types/client/chunks/mermaid.d.ts +10 -0
  47. package/lib/types/client/chunks/terminal.d.ts +9 -0
  48. package/lib/types/client/cm-themes.d.ts +24 -0
  49. package/lib/types/client/conversation-draft.d.ts +14 -0
  50. package/lib/types/client/desktop-env.d.ts +35 -0
  51. package/lib/types/client/editor-load.d.ts +66 -0
  52. package/lib/types/client/frame-batcher.d.ts +24 -0
  53. package/lib/types/client/icons.d.ts +59 -0
  54. package/lib/types/client/image-types.d.ts +3 -0
  55. package/lib/types/client/ime-guard.d.ts +36 -0
  56. package/lib/types/client/index.d.ts +25 -0
  57. package/lib/types/client/intercept.d.ts +46 -0
  58. package/lib/types/client/lang.d.ts +14 -0
  59. package/lib/types/client/layout-push.d.ts +15 -0
  60. package/lib/types/client/lazy-chunk.d.ts +25 -0
  61. package/lib/types/client/link-intercept.d.ts +42 -0
  62. package/lib/types/client/locales-ar.d.ts +18 -0
  63. package/lib/types/client/locales-de.d.ts +4 -0
  64. package/lib/types/client/locales-fr.d.ts +10 -0
  65. package/lib/types/client/locales-hi.d.ts +17 -0
  66. package/lib/types/client/locales-id.d.ts +15 -0
  67. package/lib/types/client/locales-it.d.ts +8 -0
  68. package/lib/types/client/locales-ja.d.ts +17 -0
  69. package/lib/types/client/locales-ko.d.ts +9 -0
  70. package/lib/types/client/locales-nl.d.ts +16 -0
  71. package/lib/types/client/locales-pl.d.ts +19 -0
  72. package/lib/types/client/locales-pt.d.ts +1 -0
  73. package/lib/types/client/locales-ru.d.ts +13 -0
  74. package/lib/types/client/locales-sv.d.ts +1 -0
  75. package/lib/types/client/locales-th.d.ts +17 -0
  76. package/lib/types/client/locales-tr.d.ts +17 -0
  77. package/lib/types/client/locales-vi.d.ts +17 -0
  78. package/lib/types/client/locales-zh-HK.d.ts +32 -0
  79. package/lib/types/client/locales-zh-MO.d.ts +32 -0
  80. package/lib/types/client/locales-zh-TW.d.ts +32 -0
  81. package/lib/types/client/locales.d.ts +452 -0
  82. package/lib/types/client/markdown-html.d.ts +96 -0
  83. package/lib/types/client/markdown-images.d.ts +45 -0
  84. package/lib/types/client/markdown-labels.d.ts +38 -0
  85. package/lib/types/client/md-toc.d.ts +4 -0
  86. package/lib/types/client/mermaid-blocks.d.ts +47 -0
  87. package/lib/types/client/mermaid-sanitize.d.ts +2 -0
  88. package/lib/types/client/mermaid.d.ts +12 -0
  89. package/lib/types/client/open-when-sized.d.ts +20 -0
  90. package/lib/types/client/open-with-settings.d.ts +4 -0
  91. package/lib/types/client/open-with.d.ts +87 -0
  92. package/lib/types/client/openpath-intercept.d.ts +85 -0
  93. package/lib/types/client/paths.d.ts +43 -0
  94. package/lib/types/client/pdf-types.d.ts +2 -0
  95. package/lib/types/client/pinned.d.ts +86 -0
  96. package/lib/types/client/plugin-settings.d.ts +8 -0
  97. package/lib/types/client/plugins-shared.d.ts +27 -0
  98. package/lib/types/client/plugins-tabs.d.ts +3 -0
  99. package/lib/types/client/plugins-viewers.d.ts +3 -0
  100. package/lib/types/client/prefs.d.ts +41 -0
  101. package/lib/types/client/produced-files.d.ts +29 -0
  102. package/lib/types/client/selection-payload.d.ts +27 -0
  103. package/lib/types/client/service.d.ts +439 -0
  104. package/lib/types/client/settings-nav-icon.d.ts +19 -0
  105. package/lib/types/client/shell-presets.d.ts +48 -0
  106. package/lib/types/client/sidechat-transcript.d.ts +103 -0
  107. package/lib/types/client/split-pane.d.ts +36 -0
  108. package/lib/types/client/state.d.ts +432 -0
  109. package/lib/types/client/subagent-detect.d.ts +54 -0
  110. package/lib/types/client/subagent-jobs.d.ts +63 -0
  111. package/lib/types/client/tab-content-memo.d.ts +40 -0
  112. package/lib/types/client/terminal-font.d.ts +78 -0
  113. package/lib/types/client/terminal-links.d.ts +119 -0
  114. package/lib/types/client/theme.d.ts +45 -0
  115. package/lib/types/client/titlebar-strip.d.ts +23 -0
  116. package/lib/types/client/upload.d.ts +69 -0
  117. package/lib/types/client/wco.d.ts +47 -0
  118. package/lib/types/config.d.ts +64 -0
  119. package/lib/types/context-types.d.ts +605 -0
  120. package/lib/types/fs-operations.d.ts +28 -0
  121. package/lib/types/fs-search.d.ts +24 -0
  122. package/lib/types/fs-tree.d.ts +51 -0
  123. package/lib/types/git.d.ts +124 -0
  124. package/lib/types/html-route.d.ts +59 -0
  125. package/lib/types/index.d.ts +47 -0
  126. package/lib/types/invariant.d.ts +15 -0
  127. package/lib/types/jobs-routes.d.ts +45 -0
  128. package/lib/types/open-external.d.ts +24 -0
  129. package/lib/types/path-security.d.ts +20 -0
  130. package/lib/types/prefs-shared.d.ts +236 -0
  131. package/lib/types/pty-deps.d.ts +78 -0
  132. package/lib/types/pty-manager.d.ts +168 -0
  133. package/lib/types/sidechat-core.d.ts +172 -0
  134. package/lib/types/sidechat-routes.d.ts +30 -0
  135. package/lib/types/subagent-activity.d.ts +44 -0
  136. package/lib/types/subagent-live-route.d.ts +44 -0
  137. package/lib/types/tools.d.ts +34 -0
  138. package/lib/types/trust-fence.d.ts +24 -0
  139. package/lib/types/wire.d.ts +38 -0
  140. package/package.json +172 -0
  141. package/scripts/install.ps1 +306 -0
  142. package/scripts/install.sh +287 -0
  143. package/src/agent-opens.ts +265 -0
  144. package/src/agent-pty.ts +527 -0
  145. package/src/browser-probe.ts +26 -0
  146. package/src/bundle-route.ts +129 -0
  147. package/src/client/BrowserView.tsx +289 -0
  148. package/src/client/DiffTab.tsx +114 -0
  149. package/src/client/DiffView.tsx +305 -0
  150. package/src/client/EditorHost.tsx +558 -0
  151. package/src/client/FileTree.tsx +669 -0
  152. package/src/client/FreeWindow.tsx +337 -0
  153. package/src/client/GitView.tsx +717 -0
  154. package/src/client/MarkdownHtml.tsx +296 -0
  155. package/src/client/OrphanedTab.tsx +27 -0
  156. package/src/client/PdfView.tsx +110 -0
  157. package/src/client/RenderBoundary.tsx +49 -0
  158. package/src/client/SandboxStatusBar.tsx +60 -0
  159. package/src/client/SideCardSection.module.css +960 -0
  160. package/src/client/SideCardSection.tsx +1163 -0
  161. package/src/client/SideChatView.module.css +437 -0
  162. package/src/client/SideChatView.tsx +645 -0
  163. package/src/client/Sidebar.tsx +1370 -0
  164. package/src/client/SubagentView.module.css +547 -0
  165. package/src/client/SubagentView.tsx +883 -0
  166. package/src/client/TabBar.tsx +365 -0
  167. package/src/client/TerminalView.tsx +424 -0
  168. package/src/client/TextEditor.tsx +468 -0
  169. package/src/client/TreePanel.tsx +275 -0
  170. package/src/client/UploadOverlay.tsx +62 -0
  171. package/src/client/add-plugin-modal.tsx +199 -0
  172. package/src/client/api.ts +357 -0
  173. package/src/client/binary-download.tsx +23 -0
  174. package/src/client/breakpoints.ts +61 -0
  175. package/src/client/browser.ts +167 -0
  176. package/src/client/builtins/index.ts +37 -0
  177. package/src/client/builtins/tabs.tsx +340 -0
  178. package/src/client/builtins/viewers.tsx +125 -0
  179. package/src/client/chunk-loader.ts +358 -0
  180. package/src/client/chunks/editor.tsx +9 -0
  181. package/src/client/chunks/mermaid.tsx +10 -0
  182. package/src/client/chunks/terminal.tsx +9 -0
  183. package/src/client/cm-themes.ts +129 -0
  184. package/src/client/conversation-draft.ts +29 -0
  185. package/src/client/css-modules.d.ts +5 -0
  186. package/src/client/desktop-env.ts +72 -0
  187. package/src/client/editor-load.ts +92 -0
  188. package/src/client/frame-batcher.ts +56 -0
  189. package/src/client/icons.tsx +152 -0
  190. package/src/client/image-types.ts +8 -0
  191. package/src/client/ime-guard.ts +47 -0
  192. package/src/client/index.tsx +427 -0
  193. package/src/client/intercept.tsx +188 -0
  194. package/src/client/lang.ts +225 -0
  195. package/src/client/layout-push.ts +24 -0
  196. package/src/client/layout.css +90 -0
  197. package/src/client/lazy-chunk.tsx +89 -0
  198. package/src/client/link-intercept.ts +73 -0
  199. package/src/client/locales-ar.ts +368 -0
  200. package/src/client/locales-de.ts +353 -0
  201. package/src/client/locales-fr.ts +360 -0
  202. package/src/client/locales-hi.ts +367 -0
  203. package/src/client/locales-id.ts +365 -0
  204. package/src/client/locales-it.ts +358 -0
  205. package/src/client/locales-ja.ts +367 -0
  206. package/src/client/locales-ko.ts +359 -0
  207. package/src/client/locales-nl.ts +365 -0
  208. package/src/client/locales-pl.ts +369 -0
  209. package/src/client/locales-pt.ts +350 -0
  210. package/src/client/locales-ru.ts +365 -0
  211. package/src/client/locales-sv.ts +350 -0
  212. package/src/client/locales-th.ts +367 -0
  213. package/src/client/locales-tr.ts +367 -0
  214. package/src/client/locales-vi.ts +367 -0
  215. package/src/client/locales-zh-HK.ts +382 -0
  216. package/src/client/locales-zh-MO.ts +382 -0
  217. package/src/client/locales-zh-TW.ts +382 -0
  218. package/src/client/locales.ts +894 -0
  219. package/src/client/markdown-html.ts +331 -0
  220. package/src/client/markdown-images.ts +137 -0
  221. package/src/client/markdown-labels.tsx +52 -0
  222. package/src/client/md-toc.tsx +127 -0
  223. package/src/client/mermaid-blocks.ts +110 -0
  224. package/src/client/mermaid-sanitize.ts +93 -0
  225. package/src/client/mermaid.tsx +406 -0
  226. package/src/client/open-when-sized.ts +43 -0
  227. package/src/client/open-with-settings.tsx +135 -0
  228. package/src/client/open-with.ts +221 -0
  229. package/src/client/openpath-intercept.ts +143 -0
  230. package/src/client/paths.ts +63 -0
  231. package/src/client/pdf-types.ts +4 -0
  232. package/src/client/pinned.ts +183 -0
  233. package/src/client/plugin-settings.ts +43 -0
  234. package/src/client/plugins-shared.ts +29 -0
  235. package/src/client/plugins-tabs.ts +96 -0
  236. package/src/client/plugins-viewers.ts +30 -0
  237. package/src/client/prefs.ts +210 -0
  238. package/src/client/produced-files.ts +120 -0
  239. package/src/client/selection-payload.ts +86 -0
  240. package/src/client/service.ts +859 -0
  241. package/src/client/settings-nav-icon.ts +45 -0
  242. package/src/client/shell-presets.ts +83 -0
  243. package/src/client/sidebar.module.css +2788 -0
  244. package/src/client/sidechat-transcript.ts +321 -0
  245. package/src/client/split-pane.tsx +331 -0
  246. package/src/client/state.ts +1497 -0
  247. package/src/client/subagent-detect.ts +137 -0
  248. package/src/client/subagent-jobs.ts +161 -0
  249. package/src/client/tab-content-memo.ts +55 -0
  250. package/src/client/terminal-font.ts +224 -0
  251. package/src/client/terminal-links.ts +194 -0
  252. package/src/client/theme.ts +110 -0
  253. package/src/client/titlebar-strip.ts +37 -0
  254. package/src/client/upload.ts +187 -0
  255. package/src/client/wco.ts +120 -0
  256. package/src/config.ts +154 -0
  257. package/src/context-types.ts +611 -0
  258. package/src/fs-operations.ts +95 -0
  259. package/src/fs-search.ts +113 -0
  260. package/src/fs-tree.ts +158 -0
  261. package/src/git.ts +446 -0
  262. package/src/html-route.ts +106 -0
  263. package/src/index.ts +1291 -0
  264. package/src/invariant.ts +32 -0
  265. package/src/jobs-routes.ts +257 -0
  266. package/src/open-external.ts +90 -0
  267. package/src/path-security.ts +75 -0
  268. package/src/prefs-shared.ts +280 -0
  269. package/src/pty-deps.ts +240 -0
  270. package/src/pty-manager.ts +376 -0
  271. package/src/sidechat-core.ts +468 -0
  272. package/src/sidechat-routes.ts +344 -0
  273. package/src/subagent-activity.ts +91 -0
  274. package/src/subagent-live-route.ts +93 -0
  275. package/src/tools.ts +482 -0
  276. package/src/trust-fence.ts +84 -0
  277. 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
+ }