dsh-coding-sidebar 1.0.3 → 1.0.4

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 (89) hide show
  1. package/README.md +27 -42
  2. package/lib/client-editor.js +297 -4179
  3. package/lib/client-registry.js +639 -1341
  4. package/lib/client-terminal.js +176 -266
  5. package/lib/client.js +640 -1342
  6. package/lib/index.js +4 -110
  7. package/lib/types/bundle-route.d.ts +1 -1
  8. package/lib/types/client/SideCardSection.d.ts +3 -3
  9. package/lib/types/client/TextEditor.d.ts +20 -10
  10. package/lib/types/client/add-plugin-modal.d.ts +0 -4
  11. package/lib/types/client/api.d.ts +8 -11
  12. package/lib/types/client/builtins/index.d.ts +10 -10
  13. package/lib/types/client/chunk-loader.d.ts +7 -8
  14. package/lib/types/client/locales.d.ts +0 -26
  15. package/lib/types/client/native-avoid.d.ts +2 -0
  16. package/lib/types/client/plugins-shared.d.ts +6 -6
  17. package/lib/types/client/service.d.ts +13 -83
  18. package/lib/types/index.d.ts +1 -1
  19. package/lib/types/prefs-shared.d.ts +1 -24
  20. package/package.json +26 -29
  21. package/src/bundle-route.ts +1 -1
  22. package/src/client/EditorHost.tsx +52 -122
  23. package/src/client/SandboxStatusBar.tsx +12 -9
  24. package/src/client/SideCardSection.tsx +28 -81
  25. package/src/client/TextEditor.tsx +89 -266
  26. package/src/client/add-plugin-modal.tsx +16 -24
  27. package/src/client/api.ts +1 -25
  28. package/src/client/builtins/index.ts +10 -14
  29. package/src/client/chunk-loader.ts +7 -8
  30. package/src/client/index.tsx +12 -3
  31. package/src/client/locales-ar.ts +0 -6
  32. package/src/client/locales-de.ts +0 -6
  33. package/src/client/locales-fr.ts +0 -6
  34. package/src/client/locales-hi.ts +0 -6
  35. package/src/client/locales-id.ts +0 -6
  36. package/src/client/locales-it.ts +0 -6
  37. package/src/client/locales-ja.ts +0 -6
  38. package/src/client/locales-ko.ts +0 -6
  39. package/src/client/locales-nl.ts +0 -6
  40. package/src/client/locales-pl.ts +0 -6
  41. package/src/client/locales-pt.ts +0 -6
  42. package/src/client/locales-ru.ts +0 -6
  43. package/src/client/locales-sv.ts +0 -6
  44. package/src/client/locales-th.ts +0 -6
  45. package/src/client/locales-tr.ts +0 -6
  46. package/src/client/locales-vi.ts +0 -6
  47. package/src/client/locales-zh-HK.ts +0 -6
  48. package/src/client/locales-zh-MO.ts +0 -6
  49. package/src/client/locales-zh-TW.ts +0 -6
  50. package/src/client/locales.ts +0 -52
  51. package/src/client/native-avoid.ts +155 -0
  52. package/src/client/plugins-shared.ts +6 -6
  53. package/src/client/prefs.ts +0 -7
  54. package/src/client/service.ts +13 -150
  55. package/src/client/sidebar.module.css +1 -425
  56. package/src/config.ts +1 -4
  57. package/src/index.ts +7 -74
  58. package/src/prefs-shared.ts +1 -27
  59. package/lib/client-mermaid.js +0 -200776
  60. package/lib/types/client/MarkdownHtml.d.ts +0 -32
  61. package/lib/types/client/PdfView.d.ts +0 -6
  62. package/lib/types/client/builtins/viewers.d.ts +0 -3
  63. package/lib/types/client/chunks/mermaid.d.ts +0 -10
  64. package/lib/types/client/editor-load.d.ts +0 -66
  65. package/lib/types/client/image-types.d.ts +0 -3
  66. package/lib/types/client/markdown-html.d.ts +0 -96
  67. package/lib/types/client/markdown-images.d.ts +0 -45
  68. package/lib/types/client/md-toc.d.ts +0 -4
  69. package/lib/types/client/mermaid-blocks.d.ts +0 -47
  70. package/lib/types/client/mermaid-sanitize.d.ts +0 -2
  71. package/lib/types/client/mermaid.d.ts +0 -12
  72. package/lib/types/client/pdf-types.d.ts +0 -2
  73. package/lib/types/client/plugins-viewers.d.ts +0 -3
  74. package/lib/types/html-route.d.ts +0 -59
  75. package/src/client/MarkdownHtml.tsx +0 -296
  76. package/src/client/PdfView.tsx +0 -110
  77. package/src/client/builtins/viewers.tsx +0 -125
  78. package/src/client/chunks/mermaid.tsx +0 -10
  79. package/src/client/editor-load.ts +0 -92
  80. package/src/client/image-types.ts +0 -8
  81. package/src/client/markdown-html.ts +0 -331
  82. package/src/client/markdown-images.ts +0 -137
  83. package/src/client/md-toc.tsx +0 -127
  84. package/src/client/mermaid-blocks.ts +0 -110
  85. package/src/client/mermaid-sanitize.ts +0 -93
  86. package/src/client/mermaid.tsx +0 -406
  87. package/src/client/pdf-types.ts +0 -4
  88. package/src/client/plugins-viewers.ts +0 -30
  89. package/src/html-route.ts +0 -106
@@ -1,45 +1,54 @@
1
1
  /**
2
- * The code/markdown file viewer: a CodeMirror 6 editor with line wrapping,
2
+ * The sidebar text editor: a CodeMirror 6 editor with line wrapping,
3
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.
4
+ * save. The editor tab host fetches the content through fs.read and passes
5
+ * it in props, so this component never fetches it only edits.
8
6
  *
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.
7
+ * (v1.0.4: the preview/edit mode toggle, markdown/mermaid/HTML preview
8
+ * surfaces and the HTML sandbox machinery were retired with the file-viewer
9
+ * line file preview is the host's job now; this component edits.)
10
+ *
11
+ * The toolbar (dirty dot / save / status) renders as its own row below the
12
+ * host's title bar — unless the host passes `toolbar: 'host'` (the merged
13
+ * editor-explorer mode), in which case this component skips the row and
14
+ * reports state + registers commands through the toolbar callbacks so the
15
+ * host's path-input header renders the controls instead.
15
16
  */
16
- import { useEffect, useMemo, useRef, useState } from 'react'
17
+ import { useEffect, useRef, useState } from 'react'
17
18
  import { createPortal } from 'react-dom'
18
19
  import clsx from 'clsx'
19
20
  import { EditorState } from '@codemirror/state'
20
21
  import { EditorView as CodeMirrorView, keymap, lineNumbers } from '@codemirror/view'
21
22
  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'
23
+ import { IconCheckOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
24
+ import { api } from './api.ts'
26
25
  import { languageForPath } from './lang.ts'
27
26
  import { cmSurfaceTheme, CmThemeCompartment } from './cm-themes.ts'
28
27
  import { isDarkScheme, subscribeColorScheme } from './theme.ts'
29
- import { SandboxStatusBar } from './SandboxStatusBar.tsx'
30
28
  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'
29
+ import { buildSelectionInsert } from './selection-payload.ts'
37
30
  import { t } from './locales.ts'
38
- import type { EditorToolbarState, FileViewerProps } from './service.ts'
31
+ import type { EditorToolbarControls, EditorToolbarState } from './service.ts'
32
+ import type { Context } from '../context-types.ts'
33
+ import type { SessionScope } from './api.ts'
34
+ import type { SidebarStore } from './state.ts'
39
35
  import css from './sidebar.module.css'
40
36
 
41
- /** Previewable files (rendered output vs source editing). */
42
- type ViewMode = 'preview' | 'edit'
37
+ /** Props of the sidebar text editor (the editor tab's content). */
38
+ export interface TextEditorProps {
39
+ ctx: Context
40
+ store?: SidebarStore
41
+ scope: SessionScope
42
+ path: string
43
+ /** fs.read text content (undefined while loading / for non-editable reads). */
44
+ content?: string
45
+ truncated?: boolean
46
+ /** 'host' skips the own toolbar row — the editor host's merged-mode header
47
+ * renders it instead, fed through the two callbacks below. */
48
+ toolbar?: 'self' | 'host'
49
+ onToolbarState?: (state: EditorToolbarState) => void
50
+ onToolbarControls?: (controls: EditorToolbarControls | null) => void
51
+ }
43
52
 
44
53
  /** The floating "add to conversation" action: payload + viewport anchor. */
45
54
  interface SelectionPopup {
@@ -48,20 +57,8 @@ interface SelectionPopup {
48
57
  top: number
49
58
  }
50
59
 
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)
60
+ export function TextEditor(props: TextEditorProps) {
61
+ const { ctx, scope, path, content, truncated } = props
65
62
  const [dirty, setDirty] = useState(false)
66
63
  const [saveState, setSaveState] = useState<'idle' | 'saving' | 'saved' | 'failed'>('idle')
67
64
  const hostRef = useRef<HTMLDivElement>(null)
@@ -75,8 +72,6 @@ export function TextEditor(props: FileViewerProps) {
75
72
  const [popup, setPopup] = useState<SelectionPopup | null>(null)
76
73
  /** Live mirror of the popup state for click-time reads (no re-render race). */
77
74
  const popupRef = useRef<SelectionPopup | null>(null)
78
- /** The markdown preview container (selection-containment + line lookup). */
79
- const mdRef = useRef<HTMLDivElement>(null)
80
75
 
81
76
  const hidePopup = (): void => {
82
77
  popupRef.current = null
@@ -104,21 +99,18 @@ export function TextEditor(props: FileViewerProps) {
104
99
 
105
100
  useEffect(() => subscribeColorScheme(() => { setDark(isDarkScheme()) }), [])
106
101
 
107
- // A new file (tab switch) starts clean: fresh preview mode, no draft.
102
+ // A new file (tab switch) starts clean: no draft.
108
103
  useEffect(() => {
109
- setMode('preview')
110
- setDraft(null)
111
104
  setDirty(false)
112
105
  setSaveState('idle')
113
106
  hidePopup()
114
107
  }, [content])
115
108
 
116
109
  // 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.
110
+ // the document; React only tracks dirty state through the update listener.
111
+ // The theme + syntax colors live in a compartment so a scheme flip
112
+ // reconfigures only that part the document, undo history and scroll
113
+ // position survive.
122
114
  useEffect(() => {
123
115
  if (content === undefined) return
124
116
  const host = hostRef.current
@@ -139,7 +131,6 @@ export function TextEditor(props: FileViewerProps) {
139
131
  ...(language !== null ? [language] : []),
140
132
  CodeMirrorView.updateListener.of((update) => {
141
133
  if (update.docChanged) {
142
- setDraft(update.state.doc.toString())
143
134
  setDirty(true)
144
135
  }
145
136
  }),
@@ -152,49 +143,47 @@ export function TextEditor(props: FileViewerProps) {
152
143
  ...defaultKeymap,
153
144
  ...historyKeymap,
154
145
  ]),
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
- ] : []),
146
+ // Selection popup: a non-empty selection anchors the floating
147
+ // "add to conversation" button above its head. Scrolling (geometry/
148
+ // viewport change) or losing focus hides it; typing collapses the
149
+ // selection and hides it too.
150
+ CodeMirrorView.updateListener.of((update) => {
151
+ if (update.geometryChanged || update.viewportChanged) {
152
+ hidePopup()
153
+ return
154
+ }
155
+ if (!update.view.hasFocus) {
156
+ hidePopup()
157
+ return
158
+ }
159
+ if (!(update.selectionSet || update.docChanged || update.focusChanged)) return
160
+ const sel = update.state.selection.main
161
+ if (sel.empty) {
162
+ hidePopup()
163
+ return
164
+ }
165
+ const text = update.state.sliceDoc(sel.from, sel.to)
166
+ if (text.trim() === '') {
167
+ hidePopup()
168
+ return
169
+ }
170
+ // Page coordinates (the document root may scroll); the popup is
171
+ // position:fixed, so convert to viewport coordinates.
172
+ const rect = update.view.coordsAtPos(sel.head)
173
+ if (rect === null) {
174
+ hidePopup()
175
+ return
176
+ }
177
+ const doc = update.state.doc
178
+ showPopup(
179
+ buildSelectionInsert(path, scope.cwd, {
180
+ start: doc.lineAt(sel.from).number,
181
+ end: doc.lineAt(sel.to).number,
182
+ }, text),
183
+ rect.left - window.scrollX + (rect.right - rect.left) / 2,
184
+ rect.top - window.scrollY,
185
+ )
186
+ }),
198
187
  ],
199
188
  })
200
189
  const view = new CodeMirrorView({ state, parent: host })
@@ -218,14 +207,6 @@ export function TextEditor(props: FileViewerProps) {
218
207
  view.dispatch({ effects: themeComp.reconfigure(dark) })
219
208
  }, [dark])
220
209
 
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
210
  const save = (): void => {
230
211
  const view = viewRef.current
231
212
  if (view === null || savingRef.current) return
@@ -233,7 +214,6 @@ export function TextEditor(props: FileViewerProps) {
233
214
  setSaveState('saving')
234
215
  api.fsWrite(scope, path, view.state.doc.toString()).then(() => {
235
216
  savingRef.current = false
236
- setDraft(null)
237
217
  setDirty(false)
238
218
  setSaveState('saved')
239
219
  }).catch(() => {
@@ -242,91 +222,8 @@ export function TextEditor(props: FileViewerProps) {
242
222
  })
243
223
  }
244
224
 
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
225
  const editable = content !== undefined
320
226
  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
227
 
331
228
  // Host-toolbar mode (the merged editor header renders the controls): skip
332
229
  // the own toolbar row, report the state after every relevant render (the
@@ -335,7 +232,7 @@ export function TextEditor(props: FileViewerProps) {
335
232
  const lastToolbarRef = useRef('')
336
233
  useEffect(() => {
337
234
  if (!hostToolbar) return
338
- const state: EditorToolbarState = { modes: markdown || html, mode, dirty, editable, saveState }
235
+ const state: EditorToolbarState = { dirty, editable, saveState }
339
236
  const key = JSON.stringify(state)
340
237
  if (lastToolbarRef.current === key) return
341
238
  lastToolbarRef.current = key
@@ -343,9 +240,9 @@ export function TextEditor(props: FileViewerProps) {
343
240
  })
344
241
  useEffect(() => {
345
242
  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 })
243
+ // `save` reads live refs only registering this render's closure is
244
+ // safe for the mount's lifetime.
245
+ props.onToolbarControls?.({ save })
349
246
  return () => { props.onToolbarControls?.(null) }
350
247
  // eslint-disable-next-line react-hooks/exhaustive-deps
351
248
  }, [hostToolbar])
@@ -354,24 +251,6 @@ export function TextEditor(props: FileViewerProps) {
354
251
  <>
355
252
  {!hostToolbar && (
356
253
  <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
254
  {dirty && <span className={css.dirtyDot} title={t('unsaved')} />}
376
255
  {editable && (
377
256
  <button
@@ -389,64 +268,8 @@ export function TextEditor(props: FileViewerProps) {
389
268
  )}
390
269
  {editable && (
391
270
  <>
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
- />
271
+ {truncated === true && <div className={css.editorBanner}>{t('truncation')}</div>}
272
+ <div className={css.editorCm} ref={hostRef} />
450
273
  </>
451
274
  )}
452
275
  {popup !== null && createPortal(
@@ -1,10 +1,12 @@
1
1
  /**
2
- * The "add plugin" modals (Side card settings → the dashed cards at the
3
- * end of the 侧边栏内容 / 文件预览 grids): declare that the sidebar's
4
- * extension points tab pages and file previewers — are open to plugins
5
- * (registered through `ctx.betterSidebar`), point at the GitHub topic page
6
- * for discovery, and show the repo's recommended plugin catalog of the
7
- * matching kind (name / url / description / install script).
2
+ * The "add plugin" modal (Side card settings → the dashed card at the end
3
+ * of the 侧边栏内容 grid): declares that the sidebar's tab extension point
4
+ * is open to plugins (registered through `ctx.betterSidebar`), points at
5
+ * the GitHub topic page for discovery, and shows the repo's recommended
6
+ * plugin catalog (name / url / description / install script).
7
+ *
8
+ * (v1.0.4: the file-viewer extension point was retired with the file-viewer
9
+ * registry — this modal now catalogs tab plugins only.)
8
10
  *
9
11
  * Per entry there are two actions:
10
12
  * - 「跳转」opens the plugin's repo in a REAL new browser tab (window.open
@@ -24,26 +26,16 @@ import { Modal, writeClipboard } from '@deepseek-ai/dsh-client-ui-primitives'
24
26
  import type { BetterSidebarService } from './service.ts'
25
27
  import { PLUGIN_TOPIC_URL, type PluginEntry } from './plugins-shared.ts'
26
28
  import { builtinTabPlugins } from './plugins-tabs.ts'
27
- import { builtinViewerPlugins } from './plugins-viewers.ts'
28
29
  import { t } from './locales.ts'
29
30
  import css from './SideCardSection.module.css'
30
31
 
31
- /** Which extension point the modal is adding a plugin for. */
32
- export type PluginKind = 'tab' | 'viewer'
33
-
34
- /** The catalog of one kind (kept in two repo files: plugins-tabs.ts /
35
- * plugins-viewers.ts). */
36
- function catalogOf(kind: PluginKind): readonly PluginEntry[] {
37
- return kind === 'tab' ? builtinTabPlugins : builtinViewerPlugins
38
- }
39
-
40
32
  /** How long the "已复制" feedback stays on the copy button. */
41
33
  const COPIED_FEEDBACK_MS = 1500
42
34
 
43
35
  /** The modal body: the GitHub topic button + the recommended plugin list
44
36
  * with per-entry jump/copy buttons (extracted for direct testing). */
45
- export function PluginListBody(props: { service: BetterSidebarService; kind: PluginKind }) {
46
- const { service, kind } = props
37
+ export function PluginListBody(props: { service: BetterSidebarService }) {
38
+ const { service } = props
47
39
  // Which entry's copy button currently shows the "已复制" feedback.
48
40
  const [copiedId, setCopiedId] = useState<string | null>(null)
49
41
  // Live catalog filter (name / id / description). A free-text search keeps
@@ -52,7 +44,7 @@ export function PluginListBody(props: { service: BetterSidebarService; kind: Plu
52
44
  // becomes a wall of scrolling.
53
45
  const [query, setQuery] = useState('')
54
46
 
55
- const catalog = catalogOf(kind)
47
+ const catalog = builtinTabPlugins
56
48
  const needle = query.trim().toLowerCase()
57
49
  const matches = (entry: PluginEntry): boolean => {
58
50
  if (needle === '') return true
@@ -177,14 +169,14 @@ export function PluginListBody(props: { service: BetterSidebarService; kind: Plu
177
169
  }
178
170
 
179
171
  /** The modal itself (mounted only while open — see the module comment). */
180
- export function AddPluginModal(props: { service: BetterSidebarService; onClose: () => void; kind: PluginKind }) {
181
- const { service, onClose, kind } = props
172
+ export function AddPluginModal(props: { service: BetterSidebarService; onClose: () => void }) {
173
+ const { service, onClose } = props
182
174
  return (
183
175
  <Modal
184
176
  open
185
177
  onClose={onClose}
186
- title={kind === 'tab' ? t('addPluginsTabCard') : t('addPluginsViewerCard')}
187
- description={kind === 'tab' ? t('addPluginsTabDesc') : t('addPluginsViewerDesc')}
178
+ title={t('addPluginsTabCard')}
179
+ description={t('addPluginsTabDesc')}
188
180
  closeLabel={t('close')}
189
181
  className={css.pluginModal}
190
182
  footer={(
@@ -193,7 +185,7 @@ export function AddPluginModal(props: { service: BetterSidebarService; onClose:
193
185
  </button>
194
186
  )}
195
187
  >
196
- <PluginListBody service={service} kind={kind} />
188
+ <PluginListBody service={service} />
197
189
  </Modal>
198
190
  )
199
191
  }
package/src/client/api.ts CHANGED
@@ -6,7 +6,6 @@
6
6
  * hydrating at page load (a detached session would otherwise fail the
7
7
  * request). Failures surface as {@link SidebarApiError} with the wire code.
8
8
  */
9
- import { encodeHtmlUrl } from '../html-route.ts'
10
9
  import type { LastActivity } from '../subagent-activity.ts'
11
10
  import type { SidechatThreadInfo } from '../sidechat-core.ts'
12
11
  import type { BrowserProbeResult } from './browser.ts'
@@ -325,33 +324,10 @@ export const api = {
325
324
  call<{ started: boolean }>('open.external', payload),
326
325
  }
327
326
 
328
- /** Absolute URL of the media route for one path (images only). */
329
- export function mediaUrl(scope: SessionScope, path: string): string {
330
- return fileUrl(scope, path, false)
331
- }
332
-
333
327
  /** Absolute URL of the download route: serves raw bytes (binary-safe) with
334
328
  * `Content-Disposition: attachment`, so the browser saves the file. */
335
329
  export function downloadUrl(scope: SessionScope, path: string): string {
336
- return fileUrl(scope, path, true)
337
- }
338
-
339
- /** Shared URL builder for the /sidebar/file route (media vs download). */
340
- function fileUrl(scope: SessionScope, path: string, download: boolean): string {
341
- const params = new URLSearchParams({ sessionId: scope.sessionId, path })
330
+ const params = new URLSearchParams({ sessionId: scope.sessionId, path, download: '1' })
342
331
  if (scope.cwd !== undefined && scope.cwd !== '') params.set('cwd', scope.cwd)
343
- if (download) params.set('download', '1')
344
332
  return `/sidebar/file?${params.toString()}`
345
333
  }
346
-
347
- /**
348
- * Absolute URL of the HTML preview route (see html-route.ts): the path is
349
- * fully encoded so the previewed page's relative assets resolve back into
350
- * the same route with the session scope intact. The UNC marker is
351
- * platform-neutral — the host's requireAbsolute resolves the decoded
352
- * forward-slash `//server/share/...` form on both win32 and POSIX — so no
353
- * client-side platform signal is needed.
354
- */
355
- export function htmlUrl(scope: SessionScope, path: string): string {
356
- return encodeHtmlUrl(scope.sessionId, path)
357
- }
@@ -1,21 +1,20 @@
1
1
  /**
2
- * Built-in registration: the plugin registers its own tab pages and file
3
- * previewers through the same {@link BetterSidebarService} external plugins
4
- * use — eating its own dogfood. The descriptors live next to their feature
5
- * modules (tabs.tsx / viewers.tsx); this module only aggregates them and
6
- * owns the disposer lifecycle (cordis auto-invokes it on fiber disposal,
7
- * HMR-safe).
2
+ * Built-in registration: the plugin registers its own tab pages through the
3
+ * same {@link BetterSidebarService} external plugins use — eating its own
4
+ * dogfood. The descriptors live next to their feature modules (tabs.tsx);
5
+ * this module only aggregates them and owns the disposer lifecycle (cordis
6
+ * auto-invokes it on fiber disposal, HMR-safe).
7
+ *
8
+ * (v1.0.4: the built-in file viewers were retired with the file-viewer
9
+ * registry — file preview is the host's job now.)
8
10
  */
9
11
  import type { Context } from '../../context-types.ts'
10
12
  import type { BetterSidebarService } from '../service.ts'
11
13
  import { builtinTabs, type BuiltinTabOptions } from './tabs.tsx'
12
- import { builtinViewers } from './viewers.tsx'
13
14
 
14
15
  /**
15
- * Register all built-in tabs and viewers with the service. Returns a
16
- * disposer that unregisters everything (cordis auto-invokes it on fiber
17
- * disposal). The `ctx` is threaded into tab descriptors that need it
18
- * (EditorHost reads `ctx.betterSidebar` for file-viewer matching).
16
+ * Register all built-in tabs with the service. Returns a disposer that
17
+ * unregisters everything (cordis auto-invokes it on fiber disposal).
19
18
  */
20
19
  export function registerBuiltins(
21
20
  ctx: Context,
@@ -26,9 +25,6 @@ export function registerBuiltins(
26
25
  for (const tab of builtinTabs(ctx, options)) {
27
26
  disposers.push(service.registerTab(tab))
28
27
  }
29
- for (const viewer of builtinViewers()) {
30
- disposers.push(service.registerFileViewer(viewer))
31
- }
32
28
  return () => {
33
29
  for (const d of disposers) {
34
30
  try { d() } catch { /* already disposed */ }