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,296 @@
1
+ /**
2
+ * The markdown preview's raw-HTML renderer. `markdown-html.ts` lifts HTML
3
+ * runs out of the markdown stream; this module renders them as sanitized DOM
4
+ * alongside the markdown runs (which keep flowing through the shared
5
+ * `MarkdownText`), nests markdown into unclosed block elements the way
6
+ * GitHub's linear HTML output does (`<details>` … fence … `</details>`), and
7
+ * runs an inline pass that turns literal tag text inside rendered markdown
8
+ * (table cells with `<br/>`, `<sub>`, `<img>`) back into elements.
9
+ *
10
+ * Security posture: every HTML string (block leaves, inline text, wrapper
11
+ * open-tag attributes) goes through DOMPurify with an explicit denylist on
12
+ * top of its defaults (no script/style/iframe/forms), anchors are forced to
13
+ * open in a new tab with noopener, and local media `src` attributes are
14
+ * rewritten through the session-scoped `/sidebar/file` media route — the same
15
+ * trust fence the markdown image rewriter (`markdown-images.ts`) uses.
16
+ */
17
+ import { useLayoutEffect, useMemo, useRef } from 'react'
18
+ import { createElement, type ReactNode } from 'react'
19
+ import DOMPurify from 'dompurify'
20
+ import { MarkdownText } from '@deepseek-ai/dsh-client-ui-primitives'
21
+ import type { ComponentType } from 'react'
22
+ import { markdownTextProps } from './markdown-labels.tsx'
23
+ import { lazyChunkComponent } from './lazy-chunk.tsx'
24
+ import { resolveLocalMediaDest } from './markdown-images.ts'
25
+ import {
26
+ analyzeHtmlSegment,
27
+ type AnalyzedMarkdownHtml,
28
+ } from './markdown-html.ts'
29
+ import { splitMermaidBlocks, type MermaidMarkdownProps } from './mermaid-blocks.ts'
30
+ import type { SessionScope } from './api.ts'
31
+ import css from './sidebar.module.css'
32
+
33
+ /** The chunk-resident markdown renderer (mermaid lazy chunk), shared with the
34
+ * legacy no-HTML preview path in TextEditor. */
35
+ export const LazyMermaidMarkdown = lazyChunkComponent<MermaidMarkdownProps>(
36
+ 'mermaid',
37
+ (mod) => mod.MermaidMarkdown as ComponentType<MermaidMarkdownProps> | undefined,
38
+ )
39
+
40
+ /** Everything the sanitizers need to resolve local media + scope the route. */
41
+ export interface MarkdownHtmlMedia {
42
+ scope: SessionScope
43
+ path: string
44
+ origin: string
45
+ }
46
+
47
+ /** Tag-like text in a rendered text node — the inline pass gate. */
48
+ const TAGLIKE_TEXT_RE = /<\/?[a-zA-Z][a-zA-Z0-9-]*[\s/>]/
49
+
50
+ /** Explicit denylist on top of DOMPurify's defaults: no active content, no
51
+ * form chrome, no document-level elements inside a preview. */
52
+ const PURIFY_FORBID_TAGS = [
53
+ 'script', 'style', 'iframe', 'object', 'embed', 'form', 'input', 'button',
54
+ 'select', 'textarea', 'meta', 'link', 'base', 'frame', 'frameset', 'applet',
55
+ ]
56
+ const PURIFY_FORBID_ATTR = ['srcdoc', 'formaction']
57
+
58
+ /**
59
+ * Post-sanitize hardening on a detached element tree: anchors open in a new
60
+ * tab (never navigate the GUI), and local media sources go through the media
61
+ * route so they render instead of being dropped by protocol allowlists.
62
+ */
63
+ function postProcessSanitized(root: Element, media: MarkdownHtmlMedia): void {
64
+ for (const anchor of root.querySelectorAll('a[href]')) {
65
+ anchor.setAttribute('target', '_blank')
66
+ anchor.setAttribute('rel', 'noopener noreferrer')
67
+ }
68
+ for (const element of root.querySelectorAll('img, video, audio, source')) {
69
+ const src = element.getAttribute('src')
70
+ if (src === null) continue
71
+ element.setAttribute('src', resolveLocalMediaDest(src, media.scope, media.path, media.origin))
72
+ }
73
+ }
74
+
75
+ /** Sanitize one balanced HTML span into markup for dangerouslySetInnerHTML. */
76
+ function sanitizeHtmlBlock(source: string, media: MarkdownHtmlMedia): string {
77
+ const holder = document.createElement('div')
78
+ holder.innerHTML = DOMPurify.sanitize(source, {
79
+ FORBID_TAGS: PURIFY_FORBID_TAGS,
80
+ FORBID_ATTR: PURIFY_FORBID_ATTR,
81
+ })
82
+ postProcessSanitized(holder, media)
83
+ return holder.innerHTML
84
+ }
85
+
86
+ /**
87
+ * Sanitize literal tag text from a rendered markdown text node. Returns null
88
+ * when nothing real survived (pure prose like `a < b` — the DOMPurify output
89
+ * has no element children), so the caller leaves the text node untouched.
90
+ */
91
+ function sanitizeInlineHtml(text: string, media: MarkdownHtmlMedia): string | null {
92
+ const holder = document.createElement('span')
93
+ holder.innerHTML = DOMPurify.sanitize(text, {
94
+ FORBID_TAGS: PURIFY_FORBID_TAGS,
95
+ FORBID_ATTR: PURIFY_FORBID_ATTR,
96
+ })
97
+ if (holder.firstElementChild === null) return null
98
+ postProcessSanitized(holder, media)
99
+ return holder.innerHTML
100
+ }
101
+
102
+ /**
103
+ * Sanitize a wrapper open tag (`<details open>`) into React props. Returns
104
+ * null when DOMPurify dropped the whole tag (denied element) — the renderer
105
+ * then treats the wrapper as transparent. `class`/`for` map to their React
106
+ * names; `style` is dropped (React needs an object; wrappers with inline
107
+ * styles are vanishingly rare and not worth a CSS parser).
108
+ */
109
+ function sanitizeTagProps(tag: string, attrs: string): Record<string, string> | null {
110
+ const probe = DOMPurify.sanitize(`<${tag}${attrs}></${tag}>`, {
111
+ FORBID_TAGS: PURIFY_FORBID_TAGS,
112
+ FORBID_ATTR: PURIFY_FORBID_ATTR,
113
+ })
114
+ const holder = document.createElement('div')
115
+ holder.innerHTML = probe
116
+ const element = holder.firstElementChild
117
+ if (element === null || element.tagName.toLowerCase() !== tag) return null
118
+ const props: Record<string, string> = {}
119
+ for (const attr of element.attributes) {
120
+ if (/^on/i.test(attr.name) || !/^[a-zA-Z][a-zA-Z0-9:._-]*$/.test(attr.name)) continue
121
+ if (attr.name === 'style') continue
122
+ props[attr.name === 'class' ? 'className' : attr.name === 'for' ? 'htmlFor' : attr.name] = attr.value
123
+ }
124
+ return props
125
+ }
126
+
127
+ /**
128
+ * The inline pass: walk the rendered markdown's text nodes and swap any that
129
+ * contain tag-like text for a sanitized `<span data-html-inline>`. Rendered
130
+ * code (inline `code`, `pre`, the host `.md-code-block`, mermaid mounts, and
131
+ * spans this pass already produced) is skipped. The same commit-then-operate
132
+ * pattern the mermaid swap uses: React keeps owning the host tree, only leaf
133
+ * text nodes are replaced, and a text change re-renders the subtree fresh.
134
+ */
135
+ function runInlineHtmlPass(container: HTMLElement, media: MarkdownHtmlMedia): void {
136
+ const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, {
137
+ acceptNode: (node) => {
138
+ const parent = (node as Text).parentElement
139
+ if (parent === null) return NodeFilter.FILTER_REJECT
140
+ if (parent.closest('code, pre, .md-code-block, [data-mermaid-processed], [data-html-inline]')) {
141
+ return NodeFilter.FILTER_REJECT
142
+ }
143
+ return TAGLIKE_TEXT_RE.test((node as Text).data)
144
+ ? NodeFilter.FILTER_ACCEPT
145
+ : NodeFilter.FILTER_REJECT
146
+ },
147
+ })
148
+ const targets: Text[] = []
149
+ for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) targets.push(node as Text)
150
+ for (const node of targets) {
151
+ const html = sanitizeInlineHtml(node.data, media)
152
+ if (html === null) continue
153
+ const span = document.createElement('span')
154
+ span.setAttribute('data-html-inline', '')
155
+ span.innerHTML = html
156
+ node.replaceWith(span)
157
+ }
158
+ }
159
+
160
+ interface MarkdownSegmentProps {
161
+ text: string
162
+ hasMermaid: boolean
163
+ media: MarkdownHtmlMedia
164
+ codeLabels: { copyLabel: string; copiedLabel: string }
165
+ }
166
+
167
+ /**
168
+ * One markdown run of a split document: the shared MarkdownText pass (or the
169
+ * mermaid chunk renderer when the run contains a mermaid fence), plus the
170
+ * inline HTML pass. The pass runs after every text change and is re-armed by
171
+ * a MutationObserver so it also catches content that appears late (the lazy
172
+ * mermaid chunk mounting, shiki highlighting settling) — it is idempotent and
173
+ * skips its own output, so mutation feedback settles after one extra pass.
174
+ */
175
+ function MarkdownSegment({ text, hasMermaid, media, codeLabels }: MarkdownSegmentProps): ReactNode {
176
+ const containerRef = useRef<HTMLDivElement>(null)
177
+ useLayoutEffect(() => {
178
+ const container = containerRef.current
179
+ if (container === null) return
180
+ runInlineHtmlPass(container, media)
181
+ let scheduled = false
182
+ const observer = new MutationObserver(() => {
183
+ if (scheduled) return
184
+ scheduled = true
185
+ queueMicrotask(() => {
186
+ scheduled = false
187
+ runInlineHtmlPass(container, media)
188
+ })
189
+ })
190
+ observer.observe(container, { childList: true, subtree: true })
191
+ return () => { observer.disconnect() }
192
+ }, [text, media])
193
+ return (
194
+ <div ref={containerRef}>
195
+ {hasMermaid
196
+ ? <LazyMermaidMarkdown text={text} codeLabels={codeLabels} />
197
+ : <MarkdownText {...markdownTextProps(text, codeLabels)} />}
198
+ </div>
199
+ )
200
+ }
201
+
202
+ /** A sanitized, balanced HTML span rendered as its own block. */
203
+ function HtmlLeaf({ html }: { html: string }): ReactNode {
204
+ return (
205
+ <div className={css.editorHtmlBlock} data-dsh-html-segment dangerouslySetInnerHTML={{ __html: html }} />
206
+ )
207
+ }
208
+
209
+ /** One fully-prepared HTML run: sanitized leaves + wrapper opens/closes. */
210
+ type PreparedHtmlPart =
211
+ | { kind: 'html'; html: string }
212
+ | { kind: 'open'; tag: string; props: Record<string, string> | null }
213
+ | { kind: 'close' }
214
+
215
+ type PreparedSegment =
216
+ | { kind: 'markdown'; text: string; hasMermaid: boolean }
217
+ | { kind: 'html'; parts: PreparedHtmlPart[] }
218
+
219
+ interface MarkdownDocumentProps {
220
+ info: AnalyzedMarkdownHtml
221
+ media: MarkdownHtmlMedia
222
+ codeLabels: { copyLabel: string; copiedLabel: string }
223
+ }
224
+
225
+ /**
226
+ * The split-document renderer: markdown runs render through MarkdownSegment,
227
+ * HTML runs render as sanitized leaves, and unclosed block elements lower the
228
+ * following runs into themselves until their close part pops the frame (the
229
+ * renderer's frame stack persists across segments). Stray closes at the top
230
+ * level render nothing (the sanitizer/parser would drop them anyway), and
231
+ * frames still open at the end of the document are closed like a browser
232
+ * parser would. Sanitization runs once per prepared change, in a memo.
233
+ */
234
+ export function MarkdownDocument({ info, media, codeLabels }: MarkdownDocumentProps): ReactNode {
235
+ const prepared = useMemo<PreparedSegment[]>(() => info.segments.map((segment): PreparedSegment => {
236
+ if (segment.kind === 'markdown') {
237
+ const defs = info.referenceDefinitions
238
+ const text = defs === '' ? segment.text : `${segment.text}\n\n${defs}`
239
+ return {
240
+ kind: 'markdown',
241
+ text,
242
+ hasMermaid: splitMermaidBlocks(text).some((block) => block.kind === 'mermaid'),
243
+ }
244
+ }
245
+ return {
246
+ kind: 'html',
247
+ parts: analyzeHtmlSegment(segment.text).parts.map((part): PreparedHtmlPart => {
248
+ if (part.kind === 'html') return { kind: 'html', html: sanitizeHtmlBlock(part.html, media) }
249
+ if (part.kind === 'open') return { kind: 'open', tag: part.tag, props: sanitizeTagProps(part.tag, part.attrs) }
250
+ return { kind: 'close' }
251
+ }),
252
+ }
253
+ // `media` is a memoized object in the host (TextEditor); identity tracks
254
+ // scope/path/origin changes so sanitization re-runs exactly when needed.
255
+ }), [info, media])
256
+
257
+ const nodes: ReactNode[] = []
258
+ const frames: { tag: string; props: Record<string, string> | null; children: ReactNode[] }[] = []
259
+ const emit = (node: ReactNode): void => {
260
+ const frame = frames[frames.length - 1]
261
+ if (frame !== undefined) frame.children.push(node)
262
+ else nodes.push(node)
263
+ }
264
+ let key = 0
265
+ for (const segment of prepared) {
266
+ if (segment.kind === 'markdown') {
267
+ emit(<MarkdownSegment key={`md-${key += 1}`} text={segment.text} hasMermaid={segment.hasMermaid} media={media} codeLabels={codeLabels} />)
268
+ continue
269
+ }
270
+ for (const part of segment.parts) {
271
+ if (part.kind === 'html') {
272
+ if (part.html.trim() === '') continue
273
+ emit(<HtmlLeaf key={`html-${key += 1}`} html={part.html} />)
274
+ } else if (part.kind === 'open') {
275
+ frames.push({ tag: part.tag, props: part.props, children: [] })
276
+ } else {
277
+ const frame = frames.pop()
278
+ if (frame === undefined) continue
279
+ if (frame.props === null) {
280
+ for (const child of frame.children) emit(child)
281
+ } else {
282
+ emit(createElement(frame.tag, { ...frame.props, key: `wrap-${key += 1}` }, ...frame.children))
283
+ }
284
+ }
285
+ }
286
+ }
287
+ while (frames.length > 0) {
288
+ const frame = frames.pop()!
289
+ if (frame.props === null) {
290
+ for (const child of frame.children) emit(child)
291
+ } else {
292
+ emit(createElement(frame.tag, { ...frame.props, key: `wrap-${key += 1}` }, ...frame.children))
293
+ }
294
+ }
295
+ return <>{nodes}</>
296
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Placeholder shown when a persisted tab's type is not registered (the
3
+ * contributing plugin is not installed / not yet loaded). The tab is kept
4
+ * in state so it can recover if the plugin loads later; the user can close
5
+ * it through the tab bar's X button as usual. This is the graceful-
6
+ * degradation path for the open tab-type set: {@link Sidebar} renders this
7
+ * instead of crashing on an unknown type.
8
+ */
9
+ import type { ReactNode } from 'react'
10
+ import { t } from './locales.ts'
11
+ import css from './sidebar.module.css'
12
+ import type { TabComponentProps } from './service.ts'
13
+
14
+ export function OrphanedTab(props: TabComponentProps): ReactNode {
15
+ const { tab } = props
16
+ return (
17
+ <div className={css.editor}>
18
+ <div className={css.editorHeader}>
19
+ <span className={css.editorTitle} title={tab.type}>{tab.title}</span>
20
+ </div>
21
+ <div className={css.editorPlaceholder}>
22
+ <span>{t('pluginNotLoaded')}</span>
23
+ <code className={css.orphanedType}>{tab.type}</code>
24
+ </div>
25
+ </div>
26
+ )
27
+ }
@@ -0,0 +1,110 @@
1
+ /** Browser-native PDF preview with an always-available download fallback. */
2
+ import { useEffect, useRef, useState } from 'react'
3
+ import clsx from 'clsx'
4
+ import { downloadUrl, mediaUrl, type SessionScope } from './api.ts'
5
+ import { t } from './locales.ts'
6
+ import css from './sidebar.module.css'
7
+
8
+ export function PdfView(props: { scope: SessionScope; path: string; title: string }) {
9
+ const { scope, path, title } = props
10
+ const [load, setLoad] = useState<
11
+ | { status: 'loading' }
12
+ | { status: 'ready'; url: string }
13
+ | { status: 'error'; message: string }
14
+ >({ status: 'loading' })
15
+ const [interactionBlocked, setInteractionBlocked] = useState(false)
16
+ const frameRef = useRef<HTMLIFrameElement>(null)
17
+ const shieldRef = useRef<HTMLDivElement>(null)
18
+
19
+ useEffect(() => {
20
+ const controller = new AbortController()
21
+ let objectUrl: string | undefined
22
+ setLoad({ status: 'loading' })
23
+ void (async () => {
24
+ try {
25
+ const response = await fetch(mediaUrl(scope, path), { signal: controller.signal })
26
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
27
+ const bytes = await response.arrayBuffer()
28
+ if (controller.signal.aborted) return
29
+ // A direct iframe navigation may download when an old host process or
30
+ // proxy cached application/octet-stream. The Blob URL owns an explicit
31
+ // PDF MIME and therefore consistently opens the browser PDF viewer.
32
+ objectUrl = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' }))
33
+ setLoad({ status: 'ready', url: objectUrl })
34
+ } catch (error) {
35
+ if (controller.signal.aborted) return
36
+ setLoad({ status: 'error', message: error instanceof Error ? error.message : String(error) })
37
+ }
38
+ })()
39
+ return () => {
40
+ controller.abort()
41
+ if (objectUrl !== undefined) URL.revokeObjectURL(objectUrl)
42
+ }
43
+ }, [scope.sessionId, scope.cwd, path])
44
+
45
+ useEffect(() => {
46
+ const block = (): void => {
47
+ setInteractionBlocked(true)
48
+ if (frameRef.current !== null) frameRef.current.style.pointerEvents = 'none'
49
+ if (shieldRef.current !== null) shieldRef.current.style.pointerEvents = 'auto'
50
+ }
51
+ const unblock = (): void => {
52
+ setInteractionBlocked(false)
53
+ if (frameRef.current !== null) frameRef.current.style.pointerEvents = ''
54
+ if (shieldRef.current !== null) shieldRef.current.style.pointerEvents = 'none'
55
+ }
56
+ const blockForResize = (event: PointerEvent): void => {
57
+ const target = event.target
58
+ if (target instanceof Element
59
+ && target.closest(`.${css.panelResize}, .${css.divider}`) !== null) {
60
+ block()
61
+ }
62
+ }
63
+ // An iframe is a separate browsing context and otherwise swallows the
64
+ // dragover/pointer stream as soon as the cursor crosses it. Temporarily
65
+ // remove it from hit-testing while a tab drag or pane resize is active.
66
+ document.addEventListener('dragstart', block, true)
67
+ document.addEventListener('dragend', unblock, true)
68
+ document.addEventListener('drop', unblock, true)
69
+ window.addEventListener('pointerdown', blockForResize, true)
70
+ window.addEventListener('pointerup', unblock, true)
71
+ window.addEventListener('pointercancel', unblock, true)
72
+ window.addEventListener('blur', unblock)
73
+ return () => {
74
+ document.removeEventListener('dragstart', block, true)
75
+ document.removeEventListener('dragend', unblock, true)
76
+ document.removeEventListener('drop', unblock, true)
77
+ window.removeEventListener('pointerdown', blockForResize, true)
78
+ window.removeEventListener('pointerup', unblock, true)
79
+ window.removeEventListener('pointercancel', unblock, true)
80
+ window.removeEventListener('blur', unblock)
81
+ }
82
+ }, [])
83
+
84
+ return (
85
+ <div className={css.editorPdf}>
86
+ <div className={css.editorPdfToolbar}>
87
+ <a className={css.editorDownloadLink} href={downloadUrl(scope, path)} download>
88
+ {t('downloadToView')}
89
+ </a>
90
+ </div>
91
+ <div className={css.editorPdfStage}>
92
+ {load.status === 'loading' && <div className={css.editorPlaceholder}>{t('loading')}</div>}
93
+ {load.status === 'error' && <div className={css.editorError}>{load.message}</div>}
94
+ {load.status === 'ready' && (
95
+ <iframe
96
+ ref={frameRef}
97
+ className={clsx(css.editorPdfFrame, interactionBlocked && css.editorPdfFrameBlocked)}
98
+ src={load.url}
99
+ title={title}
100
+ />
101
+ )}
102
+ <div
103
+ ref={shieldRef}
104
+ className={clsx(css.editorPdfDragShield, interactionBlocked && css.editorPdfDragShieldActive)}
105
+ aria-hidden="true"
106
+ />
107
+ </div>
108
+ </div>
109
+ )
110
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The generic render error boundary for the sidebar tree: a render error in
3
+ * the wrapped subtree shows a dismissible error strip (retry re-renders the
4
+ * children) instead of blanking the shell. Used at two scopes:
5
+ *
6
+ * - ROOT (index.tsx, `css.boundaryError`): last-resort containment for
7
+ * errors in the sidebar shell itself (Workbench, drag layout, …) — a full
8
+ * swap keeps the page alive.
9
+ * - PER-TAB (Sidebar.tsx TabContent, `css.tabBoundaryError`): a crashing
10
+ * viewer/editor shows a strip inside ITS OWN pane; the toggle cluster, the
11
+ * other tabs, and the panel itself stay alive (issue #31 — a tab crash
12
+ * must never take down the whole sidebar).
13
+ *
14
+ * The className prop selects the strip's geometry: the root's full-height
15
+ * fixed rail vs. the tab's pane-filling block.
16
+ */
17
+ import { Component, type ErrorInfo, type ReactNode } from 'react'
18
+ import { t } from './locales.ts'
19
+ import css from './sidebar.module.css'
20
+
21
+ export class RenderBoundary extends Component<{ children?: ReactNode; className?: string }, { error: string | null }> {
22
+ state = { error: null as string | null }
23
+
24
+ static getDerivedStateFromError(error: unknown): { error: string } {
25
+ return { error: error instanceof Error ? error.message : String(error) }
26
+ }
27
+
28
+ componentDidCatch(error: Error, info: ErrorInfo): void {
29
+ console.error('[dsh-coding-sidebar] render error:', error, info.componentStack)
30
+ }
31
+
32
+ render(): ReactNode {
33
+ if (this.state.error !== null) {
34
+ return (
35
+ <div className={this.props.className}>
36
+ <span>dsh-coding-sidebar: {this.state.error}</span>
37
+ <button
38
+ type="button"
39
+ className={css.terminalRetry}
40
+ onClick={() => { this.setState({ error: null }) }}
41
+ >
42
+ {t('terminalRetry')}
43
+ </button>
44
+ </div>
45
+ )
46
+ }
47
+ return this.props.children
48
+ }
49
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The live sandbox status row of the two built-in web surfaces (HTML
3
+ * preview and the browser tab): a green "sandbox on" state with a one-tap
4
+ * TEMPORARY unlock, or a RED "sandbox off" state (global setting or the
5
+ * temporary unlock) with a restore action.
6
+ *
7
+ * The temporary unlock is component state only — it never writes the
8
+ * global side card setting (`htmlViewerNoSandbox` / `browserNoSandbox`);
9
+ * it lasts until the surface unmounts (tab switch / file switch) or the
10
+ * user restores the sandbox from the row. When the global setting already
11
+ * drops the sandbox, no unlock/restore action is offered (changing the
12
+ * global setting is the settings page's job) — the red warning stands.
13
+ */
14
+ import clsx from 'clsx'
15
+ import { t } from './locales.ts'
16
+ import css from './sidebar.module.css'
17
+
18
+ export function SandboxStatusBar(props: {
19
+ /** The effective sandbox state (global pref OR the local temporary unlock). */
20
+ sandboxed: boolean
21
+ /** Whether the sandbox is off due to the LOCAL temporary unlock (shows the restore action). */
22
+ local: boolean
23
+ /** The red-state explanation (e.g. "the page runs with full GUI privileges"). */
24
+ dangerCopy: string
25
+ onUnlock: () => void
26
+ onRestore: () => void
27
+ }) {
28
+ const { sandboxed, local, dangerCopy, onUnlock, onRestore } = props
29
+ if (sandboxed) {
30
+ const copy = t('sandboxStatusOn')
31
+ return (
32
+ <div className={clsx(css.sandboxStatus, css.sandboxStatusOn)}>
33
+ <span className={css.sandboxDot} />
34
+ <span className={css.sandboxStatusText} title={copy}>{copy}</span>
35
+ <button
36
+ type="button"
37
+ className={css.sandboxAction}
38
+ onClick={onUnlock}
39
+ >
40
+ {t('sandboxUnlock')}
41
+ </button>
42
+ </div>
43
+ )
44
+ }
45
+ return (
46
+ <div className={clsx(css.sandboxStatus, css.sandboxStatusOff)}>
47
+ <span className={css.sandboxDot} />
48
+ <span className={css.sandboxStatusText} title={dangerCopy}>{dangerCopy}</span>
49
+ {local && (
50
+ <button
51
+ type="button"
52
+ className={css.sandboxAction}
53
+ onClick={onRestore}
54
+ >
55
+ {t('sandboxRestore')}
56
+ </button>
57
+ )}
58
+ </div>
59
+ )
60
+ }