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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-coding-sidebar",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "DSH web plugin: a VSCode-like right sidebar (explorer / editor / terminal / git / browser), isolated per conversation session. Exposes the betterSidebar service for other plugins to register sidebar tabs and file viewers. KCoder-maintained fork of DSH-better-sidebar 0.17.2 (bottom panel removed, upstream-decoupled release line).",
5
5
  "type": "module",
6
6
  "repository": {
@@ -61,7 +61,6 @@
61
61
  "lib/client-registry.js",
62
62
  "lib/client-terminal.js",
63
63
  "lib/client-editor.js",
64
- "lib/client-mermaid.js",
65
64
  "lib/types/**/*.d.ts",
66
65
  "src",
67
66
  "scripts/install.sh",
@@ -85,18 +84,18 @@
85
84
  "license": "MIT",
86
85
  "peerDependencies": {
87
86
  "@deepseek-ai/cordis": "^4.0.1",
88
- "@deepseek-ai/dsh-agent": "^0.1.5-rc.1",
89
- "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.1",
90
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.1",
91
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.1",
92
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.1",
93
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.1",
94
- "@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.1",
95
- "@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
96
- "@deepseek-ai/dsh-session": "^0.1.5-rc.1",
97
- "@deepseek-ai/dsh-settings": "^0.1.5-rc.1",
98
- "@deepseek-ai/dsh-subagent": "^0.1.5-rc.1",
99
- "@deepseek-ai/dsh-tools": "^0.1.5-rc.1",
87
+ "@deepseek-ai/dsh-agent": "^0.1.5-rc.2",
88
+ "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2",
89
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.2",
90
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2",
91
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.2",
92
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
93
+ "@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.2",
94
+ "@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
95
+ "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
96
+ "@deepseek-ai/dsh-settings": "^0.1.5-rc.2",
97
+ "@deepseek-ai/dsh-subagent": "^0.1.5-rc.2",
98
+ "@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
100
99
  "@huanlin/dsh-plugin-better-locale": "^0.1.0",
101
100
  "react": "^18.2.0",
102
101
  "react-dom": "^18.2.0"
@@ -130,8 +129,6 @@
130
129
  "@codemirror/view": "^6.43.8",
131
130
  "@lezer/highlight": "^1.2.3",
132
131
  "clsx": "^2.1.1",
133
- "dompurify": "^3.4.14",
134
- "mermaid": "^11.16.1",
135
132
  "node-pty": "^1.1.0",
136
133
  "react-icons": "5.7.0",
137
134
  "rxjs": "^7.8.2",
@@ -139,20 +136,20 @@
139
136
  "ws": "^8.18.0"
140
137
  },
141
138
  "devDependencies": {
142
- "@deepseek-ai/dsh-invariants": "0.1.5-rc.1",
139
+ "@deepseek-ai/dsh-invariants": "0.1.5-rc.2",
143
140
  "@cordisjs/plugin-loader": "^1.0.0-rc.5",
144
- "@deepseek-ai/dsh-agent": "0.1.5-rc.1",
145
- "@deepseek-ai/dsh-client-locale": "0.1.5-rc.1",
146
- "@deepseek-ai/dsh-client-ui-conversation": "0.1.5-rc.1",
147
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.1",
148
- "@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.1",
149
- "@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.1",
150
- "@deepseek-ai/dsh-host-webserver": "0.1.5-rc.1",
151
- "@deepseek-ai/dsh-llm": "0.1.5-rc.1",
152
- "@deepseek-ai/dsh-session": "0.1.5-rc.1",
153
- "@deepseek-ai/dsh-settings": "0.1.5-rc.1",
154
- "@deepseek-ai/dsh-subagent": "0.1.5-rc.1",
155
- "@deepseek-ai/dsh-tools": "0.1.5-rc.1",
141
+ "@deepseek-ai/dsh-agent": "0.1.5-rc.2",
142
+ "@deepseek-ai/dsh-client-locale": "0.1.5-rc.2",
143
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.5-rc.2",
144
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.2",
145
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.5-rc.2",
146
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.5-rc.2",
147
+ "@deepseek-ai/dsh-host-webserver": "0.1.5-rc.2",
148
+ "@deepseek-ai/dsh-llm": "0.1.5-rc.2",
149
+ "@deepseek-ai/dsh-session": "0.1.5-rc.2",
150
+ "@deepseek-ai/dsh-settings": "0.1.5-rc.2",
151
+ "@deepseek-ai/dsh-subagent": "0.1.5-rc.2",
152
+ "@deepseek-ai/dsh-tools": "0.1.5-rc.2",
156
153
  "@huanlin/dsh-plugin-better-locale": "^0.1.0",
157
154
  "@types/node": "^24.0.0",
158
155
  "@types/react": "~18.3.1",
@@ -19,7 +19,7 @@ import { fileURLToPath } from 'node:url'
19
19
  import type { Context, SidebarHttpRequest, SidebarHttpResponse } from './context-types.ts'
20
20
 
21
21
  /** The chunk names the client may request (mirror of src/client/chunk-loader.ts). */
22
- export const CHUNK_NAMES = ['terminal', 'editor', 'mermaid'] as const
22
+ export const CHUNK_NAMES = ['terminal', 'editor'] as const
23
23
  export type ChunkName = (typeof CHUNK_NAMES)[number]
24
24
 
25
25
  /** Directory of this host-half module (lib/ — the chunk scripts live next to it). */
@@ -1,10 +1,11 @@
1
1
  /**
2
- * The editor tab host: the single FILES WINDOW. It resolves a file's
3
- * previewer through the sidebar registry (`matchFileViewer`), fetches bytes
4
- * per the matched viewer's fetch strategy, and renders its component — or
5
- * the shared download pane when nothing can render the file. A tab without
6
- * a path (the seeded "Files" home) renders an empty-state hint instead of
7
- * the viewer loading flow; that path-less window IS the file explorer.
2
+ * The editor tab host: the single FILES WINDOW. It fetches a file's text
3
+ * through the host fs.read — text renders in the (chunk-lazy) sidebar text
4
+ * editor, a binary file renders the shared download pane. (v1.0.4: the
5
+ * file-viewer registry/matching was retired file preview is the host's
6
+ * job now, the sidebar edits text only.) A tab without a path (the seeded
7
+ * "Files" home) renders an empty-state hint instead of the loading flow;
8
+ * that path-less window IS the file explorer.
8
9
  *
9
10
  * The chrome depends on the `editorExplorer` mode (read reactively so
10
11
  * toggling it re-renders without a reload):
@@ -18,18 +19,14 @@
18
19
  * The tree's context menu offers the explicit escapes in both modes: open
19
20
  * in a new tab (per-path dedupe) or to the side (a fresh tab in a fresh
20
21
  * rightward split of the current pane).
21
- *
22
- * The strategy dispatch is pure (planFirstMatch / planFsReadOutcome in
23
- * editor-load.ts); this component only wires it to the host APIs.
24
22
  */
25
23
  import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
26
- import { createElement } from 'react'
24
+ import type { ComponentType } from 'react'
27
25
  import clsx from 'clsx'
28
26
  import { IconCheckOutline16, IconFolderOpen16, IconRefreshOutline14 } from '@deepseek-ai/dsh-client-ui-primitives'
29
27
  import type { Context } from '../context-types.ts'
30
- import { api, mediaUrl, type SessionScope } from './api.ts'
28
+ import { api, type SessionScope } from './api.ts'
31
29
  import { BinaryDownload } from './binary-download.tsx'
32
- import { planFirstMatch, planFsReadOutcome, type EditorLoadAction } from './editor-load.ts'
33
30
  import { baseName } from './FileTree.tsx'
34
31
  import { createFrameBatcher } from './frame-batcher.ts'
35
32
  import { openSidebarFile } from './intercept.tsx'
@@ -39,16 +36,27 @@ import { TreePanel } from './TreePanel.tsx'
39
36
  import { t } from './locales.ts'
40
37
  import { relativeTo } from './paths.ts'
41
38
  import { resolveSidebarPath } from './produced-files.ts'
42
- import type { EditorToolbarControls, EditorToolbarState, FileViewerDescriptor } from './service.ts'
39
+ import type { EditorToolbarControls, EditorToolbarState } from './service.ts'
40
+ import { lazyChunkComponent } from './lazy-chunk.tsx'
41
+ import type { TextEditorProps } from './TextEditor.tsx'
43
42
  import { firstLeaf, insertLeafAt, leafWithTab, mintTabId, treeOf, type SidebarStore, type SidebarTab } from './state.ts'
44
43
  import css from './sidebar.module.css'
45
44
 
46
45
  type EditorLoad =
47
46
  | { status: 'loading' }
48
47
  | { status: 'error'; message: string }
49
- | { status: 'ready'; viewer: FileViewerDescriptor; content?: string; truncated?: boolean; mediaUrl?: string; customData?: unknown }
48
+ | { status: 'ready'; content: string; truncated: boolean }
50
49
  | { status: 'binary' }
51
50
 
51
+ /**
52
+ * Lazy wrapper over the chunk-resident text editor: CodeMirror and the
53
+ * language packs are fetched only when a file tab first renders (see
54
+ * chunk-loader.ts). The `pick` function is module-level (stable identity —
55
+ * the wrapper effect depends on it); the cast bridges the chunk exports
56
+ * record to the prop shape.
57
+ */
58
+ const LazyTextEditor = lazyChunkComponent<TextEditorProps>('editor', (mod) => mod.TextEditor as ComponentType<TextEditorProps> | undefined)
59
+
52
60
  /** The docked tree panel's width bounds (drag-resize clamps into them). */
53
61
  const TREE_WIDTH_DEFAULT = 240
54
62
  const TREE_WIDTH_MIN = 160
@@ -103,11 +111,10 @@ export function EditorHost(props: {
103
111
  }) {
104
112
  const { ctx, store, scope, tab, expanded, revealed, onToggleDir, onReferenceFile } = props
105
113
  const path = tab.path ?? ''
106
- const title = tab.title
107
114
  // A folder window: the model's `sidebar_open` (or any caller) opens a
108
115
  // directory as an editor tab carrying `meta.dir: true` with the directory
109
116
  // as its path. It renders the file tree rooted at that folder instead of
110
- // the viewer loading flow (a directory is not a file).
117
+ // the file loading flow (a directory is not a file).
111
118
  const isDir = metaOf(tab).dir === true
112
119
  const [load, setLoad] = useState<EditorLoad>({ status: 'loading' })
113
120
  // Manual refresh (issue #167): bumping the sequence re-runs the load effect
@@ -223,9 +230,8 @@ export function EditorHost(props: {
223
230
  })
224
231
  }
225
232
 
226
- // The viewer's toolbar, hoisted into THIS header: the text editor reports
227
- // its state and registers its commands (both null/absent for viewers
228
- // without a toolbar — image, pdf, binary download).
233
+ // The editor's toolbar, hoisted into THIS header: the text editor reports
234
+ // its state and registers its commands (null while nothing is loaded).
229
235
  const [toolbar, setToolbar] = useState<EditorToolbarState | null>(null)
230
236
  const controlsRef = useRef<EditorToolbarControls | null>(null)
231
237
  const onToolbarState = useCallback((next: EditorToolbarState) => {
@@ -279,77 +285,27 @@ export function EditorHost(props: {
279
285
 
280
286
  useEffect(() => {
281
287
  // A (re)load or a path-less tab clears any hoisted toolbar state — the
282
- // fresh viewer re-registers its own.
288
+ // fresh editor re-registers its own.
283
289
  setToolbar(null)
284
- // The seeded home tab (no path) never loads a viewer — the empty-state
290
+ // The seeded home tab (no path) never loads a file — the empty-state
285
291
  // hint renders until the user picks a file. A folder tab never loads a
286
- // viewer either — its tree is rooted at the folder.
292
+ // file either — its tree is rooted at the folder.
287
293
  if (showEmpty || isDir) return
288
294
  let cancelled = false
289
- // Aborts the matched viewer's `load` when the editor tears down (tab
290
- // closed, path changed, session switched) or re-matches the viewer.
291
- const controller = new AbortController()
292
295
  setLoad({ status: 'loading' })
293
- const mediaUrlOf = (): string => mediaUrl(scope, path)
294
- const apply = (action: EditorLoadAction): void => {
296
+ api.fsRead(scope, path).then((result) => {
295
297
  if (cancelled) return
296
- switch (action.kind) {
297
- case 'binary':
298
- setLoad({ status: 'binary' })
299
- return
300
- case 'render':
301
- setLoad({
302
- status: 'ready',
303
- viewer: action.viewer,
304
- content: action.content,
305
- truncated: action.truncated,
306
- mediaUrl: action.mediaUrl,
307
- customData: action.customData,
308
- })
309
- return
310
- case 'customLoad':
311
- void action.viewer.load?.(path, scope, controller.signal).then((data) => {
312
- if (cancelled) return
313
- setLoad({ status: 'ready', viewer: action.viewer, customData: data })
314
- }).catch((error: unknown) => {
315
- if (cancelled) return
316
- setLoad({ status: 'error', message: error instanceof Error ? error.message : String(error) })
317
- })
318
- return
319
- case 'fetchFsRead':
320
- api.fsRead(scope, path).then((result) => {
321
- if (cancelled) return
322
- // Binary reads carry the head bytes for the detect re-match.
323
- const outcome = planFsReadOutcome(action.viewer, {
324
- binary: result.kind === 'binary',
325
- content: result.kind === 'text' ? result.content : '',
326
- truncated: result.truncated,
327
- head: result.kind === 'binary' ? result.head : undefined,
328
- }, (head) => ctx.get('betterSidebar')?.matchFileViewer(path, head), mediaUrlOf)
329
- apply(outcome)
330
- }).catch((error: unknown) => {
331
- if (cancelled) return
332
- setLoad({ status: 'error', message: error instanceof Error ? error.message : String(error) })
333
- })
334
- return
298
+ if (result.kind === 'binary') {
299
+ setLoad({ status: 'binary' })
300
+ return
335
301
  }
336
- }
337
- apply(planFirstMatch(ctx.get('betterSidebar')?.matchFileViewer(path), mediaUrlOf))
338
- return () => { cancelled = true; controller.abort() }
339
- }, [scope.sessionId, scope.cwd, path, ctx, showEmpty, isDir, reloadSeq])
340
-
341
- // Save-then-refresh in preview mode (issue #167 part C): the edge into
342
- // 'saved' (never a lingering 'saved' state) triggers exactly one reload, so
343
- // a preview-mode Ctrl+S shows the fresh content immediately. Edit mode is
344
- // left alone — reloading would remount the editor and drop the caret.
345
- const prevSaveState = useRef<EditorToolbarState['saveState'] | undefined>(undefined)
346
- useEffect(() => {
347
- const current = toolbar?.saveState
348
- if (prevSaveState.current !== 'saved' && current === 'saved' && toolbar?.mode === 'preview') {
349
- setReloadSeq(sequence => sequence + 1)
350
- }
351
- prevSaveState.current = current
352
- }, [toolbar?.saveState, toolbar?.mode])
302
+ setLoad({ status: 'ready', content: result.content, truncated: result.truncated })
303
+ }).catch((error: unknown) => {
304
+ if (cancelled) return
305
+ setLoad({ status: 'error', message: error instanceof Error ? error.message : String(error) })
306
+ })
307
+ return () => { cancelled = true }
308
+ }, [scope.sessionId, scope.cwd, path, showEmpty, isDir, reloadSeq])
353
309
 
354
310
  const treeOpen = treeOpenOf(tab)
355
311
  /** Persist the panel flag on the tab (survives reloads with the layout). */
@@ -392,33 +348,6 @@ export function EditorHost(props: {
392
348
  <div className={css.editor}>
393
349
  <div className={css.editorHeader}>
394
350
  <EditorPathInput key={path} path={path} cwd={scope.cwd} onOpen={openFile} />
395
- {toolbar?.modes === true && (
396
- <div className={css.editorModeToggle}>
397
- <button
398
- type="button"
399
- className={clsx(css.editorModeButton, toolbar.mode === 'preview' && css.editorModeActive)}
400
- onClick={() => {
401
- // Issue #167 part B: returning from edit to preview reloads so
402
- // the preview renders the just-saved content. A dirty draft
403
- // (or a failed save) suppresses the reload — the draft only
404
- // lives in the editor instance and a remount would drop it.
405
- if (toolbar.mode === 'edit' && toolbar.dirty !== true && toolbar.saveState !== 'failed') {
406
- setReloadSeq(sequence => sequence + 1)
407
- }
408
- controlsRef.current?.setMode('preview')
409
- }}
410
- >
411
- {t('preview')}
412
- </button>
413
- <button
414
- type="button"
415
- className={clsx(css.editorModeButton, toolbar.mode === 'edit' && css.editorModeActive)}
416
- onClick={() => { controlsRef.current?.setMode('edit') }}
417
- >
418
- {t('edit')}
419
- </button>
420
- </div>
421
- )}
422
351
  {toolbar?.dirty === true && <span className={css.dirtyDot} title={t('unsaved')} />}
423
352
  {toolbar?.editable === true && (
424
353
  <button
@@ -462,18 +391,19 @@ export function EditorHost(props: {
462
391
  {!showEmpty && load.status === 'loading' && <div className={css.editorPlaceholder}>{t('loading')}</div>}
463
392
  {!showEmpty && load.status === 'error' && <div className={css.editorError}>{load.message}</div>}
464
393
  {!showEmpty && load.status === 'binary' && <BinaryDownload scope={scope} path={path} />}
465
- {!showEmpty && load.status === 'ready' && createElement(load.viewer.component, {
466
- ctx, store, scope, path, title,
467
- viewerId: load.viewer.id,
468
- content: load.content,
469
- truncated: load.truncated,
470
- mediaUrl: load.mediaUrl,
471
- customData: load.customData,
472
- // The viewer's toolbar always hoists into this host's header.
473
- toolbar: 'host',
474
- onToolbarState,
475
- onToolbarControls,
476
- })}
394
+ {!showEmpty && load.status === 'ready' && (
395
+ <LazyTextEditor
396
+ ctx={ctx}
397
+ store={store}
398
+ scope={scope}
399
+ path={path}
400
+ content={load.content}
401
+ truncated={load.truncated}
402
+ toolbar="host"
403
+ onToolbarState={onToolbarState}
404
+ onToolbarControls={onToolbarControls}
405
+ />
406
+ )}
477
407
  </div>
478
408
  {treeOpen && (
479
409
  <div className={css.editorTreeDock} style={{ width: treeWidth }}>
@@ -1,15 +1,18 @@
1
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.
2
+ * The live sandbox status row of the built-in browser tab: a green
3
+ * "sandbox on" state with a one-tap TEMPORARY unlock, or a RED "sandbox
4
+ * off" state (global setting or the temporary unlock) with a restore
5
+ * action.
6
6
  *
7
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.
8
+ * global side card setting (`browserNoSandbox`); it lasts until the
9
+ * surface unmounts (tab switch) or the user restores the sandbox from the
10
+ * row. When the global setting already drops the sandbox, no
11
+ * unlock/restore action is offered (changing the global setting is the
12
+ * settings page's job) — the red warning stands.
13
+ *
14
+ * (v1.0.4: the HTML preview surface was retired with the file-viewer line;
15
+ * this row now serves the browser tab only.)
13
16
  */
14
17
  import clsx from 'clsx'
15
18
  import { t } from './locales.ts'
@@ -12,8 +12,9 @@
12
12
  * external plugins alike), laid out in a responsive grid that wraps
13
13
  * several cards per row — icon chip + title + type id, clicked to toggle
14
14
  * the switch persisted in `prefs.tabsEnabled[id]`.
15
- * - 文件预览: one SMALL CARD per REGISTERED file viewer — icon chip + title
16
- * + the extensions it covers, clicked to toggle `prefs.viewersEnabled[id]`.
15
+ *
16
+ * (v1.0.4: the 文件预览 viewer inventory was retired with the file-viewer
17
+ * registry — file preview is the host's job now.)
17
18
  *
18
19
  * Every group lives in a container card (the DSH PluginCard recipe: l2
19
20
  * hairline, 16px radius, layer-3 fill) with a heading and an inventory count
@@ -64,14 +65,13 @@ import {
64
65
  } from '../prefs-shared.ts'
65
66
  import { api } from './api.ts'
66
67
  import { parsePrefs } from './prefs.ts'
67
- import { AddPluginModal, type PluginKind } from './add-plugin-modal.tsx'
68
+ import { AddPluginModal } from './add-plugin-modal.tsx'
68
69
  import { t } from './locales.ts'
69
70
  import { parseDesktopEnv } from './desktop-env.ts'
70
71
  import { getShellPreset, getShellPresets } from './shell-presets.ts'
71
72
  import type { SidebarStore } from './state.ts'
72
73
  import type {
73
74
  BetterSidebarService,
74
- FileViewerDescriptor,
75
75
  SidebarSettingsRenderProps,
76
76
  SidebarSettingToggle,
77
77
  TabDescriptor,
@@ -124,13 +124,8 @@ function titleBarSchemeValue(prefs: SidebarPrefs): string {
124
124
  return preset !== undefined ? `preset:${preset.id}` : 'auto'
125
125
  }
126
126
 
127
- /** Viewer inventory order: priority desc (the catch-all `code` comes last). */
128
- function viewerOrder(a: FileViewerDescriptor, b: FileViewerDescriptor): number {
129
- return (b.priority ?? 0) - (a.priority ?? 0)
130
- }
131
-
132
127
  /** Whether a feature declares any secondary settings (gear button shows). */
133
- function hasSettings(feature: TabDescriptor | FileViewerDescriptor): boolean {
128
+ function hasSettings(feature: TabDescriptor): boolean {
134
129
  const settings = feature.settings
135
130
  return settings !== undefined && (
136
131
  (settings.toggles?.length ?? 0) > 0
@@ -139,9 +134,9 @@ function hasSettings(feature: TabDescriptor | FileViewerDescriptor): boolean {
139
134
  )
140
135
  }
141
136
 
142
- /** A feature's display name (viewers fall back to their id). */
143
- function featureNameOf(feature: TabDescriptor | FileViewerDescriptor): string {
144
- return textOf('title' in feature ? feature.title : undefined) || feature.id
137
+ /** A feature's display name. */
138
+ function featureNameOf(feature: TabDescriptor): string {
139
+ return textOf(feature.title) || feature.id
145
140
  }
146
141
 
147
142
  /**
@@ -491,7 +486,7 @@ function SelectRow(props: {
491
486
  }
492
487
 
493
488
  /**
494
- * The secondary settings popup body of one feature (tab or viewer):
489
+ * The secondary settings popup body of one registered tab:
495
490
  * - the host-prefs `toggles` rows, then the plugin-owned `pluginToggles`
496
491
  * rows (their values live in `pluginSettings[feature.id]`, projected onto
497
492
  * the prefs face so the shared row renderer reads them);
@@ -501,7 +496,7 @@ function SelectRow(props: {
501
496
  * open-behavior picker) and still ship a custom configuration area.
502
497
  */
503
498
  export function SettingsBody(props: {
504
- feature: TabDescriptor | FileViewerDescriptor
499
+ feature: TabDescriptor
505
500
  prefs: SidebarPrefs
506
501
  store: SidebarStore
507
502
  service: BetterSidebarService
@@ -576,19 +571,18 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
576
571
  const [widthDraft, setWidthDraft] = useState<string>(String(store.getPrefs().defaultWidthPercent))
577
572
  const [error, setError] = useState<string | null>(null)
578
573
  // Which feature's secondary settings popup is open (null = closed).
579
- const [settingsFor, setSettingsFor] = useState<TabDescriptor | FileViewerDescriptor | null>(null)
574
+ const [settingsFor, setSettingsFor] = useState<TabDescriptor | null>(null)
580
575
  // Whether the position-compat strip popup (the gear on the 常规 row) is open.
581
576
  const [stripSettingsOpen, setStripSettingsOpen] = useState(false)
582
577
  // The parsed desktop environment (URL stamps — see desktop-env.ts). Used
583
578
  // ONLY to badge matching presets in the scheme dropdown ("已检测");
584
579
  // nothing is auto-applied.
585
580
  const detectedEnv = useMemo(() => parseDesktopEnv(), [])
586
- // Whether the "add plugin" modal (a dashed card at the end of the
587
- // 侧边栏内容 / 文件预览 grids) is open, and for which extension point
588
- // (null = closed).
589
- const [addPluginsOpen, setAddPluginsOpen] = useState<PluginKind | null>(null)
581
+ // Whether the "add plugin" modal (the dashed card at the end of the
582
+ // 侧边栏内容 grid) is open.
583
+ const [addPluginsOpen, setAddPluginsOpen] = useState(false)
590
584
  // The LATEST optimistic prefs, kept in sync with the state. Nested-map
591
- // merges (tabsEnabled / viewersEnabled / pluginSettings) MUST build from
585
+ // merges (tabsEnabled / pluginSettings) MUST build from
592
586
  // this ref, not from the render-time `prefs`: two same-tick writes (e.g.
593
587
  // a settings panel updating several plugin keys at once) would otherwise
594
588
  // both spread the stale map and the later patch would drop the earlier
@@ -596,14 +590,12 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
596
590
  const optimisticRef = useRef(prefs)
597
591
  useEffect(() => { optimisticRef.current = prefs }, [prefs])
598
592
 
599
- // The declarative inventory: the registered tab types and file viewers.
600
- // Local state + service.subscribe (registry changes are rare — plugin
601
- // load/unload — so a plain effect is enough; no external-store ceremony).
593
+ // The declarative inventory: the registered tab types. Local state +
594
+ // service.subscribe (registry changes are rare — plugin load/unload — so
595
+ // a plain effect is enough; no external-store ceremony).
602
596
  const [tabs, setTabs] = useState<TabDescriptor[]>(() => [...service.getTabs()].sort(tabOrder))
603
- const [viewers, setViewers] = useState<FileViewerDescriptor[]>(() => [...service.getFileViewers()].sort(viewerOrder))
604
597
  useEffect(() => service.subscribe(() => {
605
598
  setTabs([...service.getTabs()].sort(tabOrder))
606
- setViewers([...service.getFileViewers()].sort(viewerOrder))
607
599
  }), [service])
608
600
 
609
601
  // The settings document revision (guards concurrent writes). A ref: commits
@@ -682,11 +674,6 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
682
674
  applyPref({ tabsEnabled: { ...optimisticRef.current.tabsEnabled, [id]: next } })
683
675
  }
684
676
 
685
- /** Flip one per-viewer enable switch (merge into the viewersEnabled map). */
686
- const onToggleViewer = (id: string, next: boolean): void => {
687
- applyPref({ viewersEnabled: { ...optimisticRef.current.viewersEnabled, [id]: next } })
688
- }
689
-
690
677
  /** Flip one declaratively-declared toggle (a SidebarPrefs boolean field). */
691
678
  const onToggleSetting = (toggle: SidebarSettingToggle, next: boolean): void => {
692
679
  applyPref({ [toggle.key]: next })
@@ -1001,7 +988,7 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
1001
988
  <button
1002
989
  type="button"
1003
990
  className={clsx(css.card, css.addCard)}
1004
- onClick={() => { setAddPluginsOpen('tab') }}
991
+ onClick={() => { setAddPluginsOpen(true) }}
1005
992
  >
1006
993
  <span className={css.cardTop}>
1007
994
  <span className={css.cardIconChip}>
@@ -1014,45 +1001,6 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
1014
1001
  </div>
1015
1002
  </div>
1016
1003
 
1017
- {/* 文件预览: one small card per registered file viewer. */}
1018
- <div className={css.group}>
1019
- <div className={css.groupHeading}>
1020
- <span>{t('settingsViewersTitle')}</span>
1021
- <span className={css.count}>{viewers.length}</span>
1022
- </div>
1023
- <div className={css.grid}>
1024
- {viewers.map(viewer => (
1025
- <Fragment key={viewer.id}>
1026
- {renderCard({
1027
- title: textOf(viewer.title) || viewer.id,
1028
- desc: viewer.exts.length === 0 ? t('settingsViewerCatchAll') : viewer.exts.join(' · '),
1029
- icon: iconOf(viewer.icon, 16),
1030
- enabled: prefs.viewersEnabled[viewer.id] !== false,
1031
- onToggle: (next) => { onToggleViewer(viewer.id, next) },
1032
- onOpenSettings: prefs.viewersEnabled[viewer.id] !== false && hasSettings(viewer)
1033
- ? () => { setSettingsFor(viewer) }
1034
- : undefined,
1035
- })}
1036
- </Fragment>
1037
- ))}
1038
- {/* The "add preview plugin" entry: dashed card opening the
1039
- FILE-PREVIEWER registration modal. */}
1040
- <button
1041
- type="button"
1042
- className={clsx(css.card, css.addCard)}
1043
- onClick={() => { setAddPluginsOpen('viewer') }}
1044
- >
1045
- <span className={css.cardTop}>
1046
- <span className={css.cardIconChip}>
1047
- <IconPlusOutline16 size={16} />
1048
- </span>
1049
- <span className={css.cardTitle}>{t('addPluginsViewerCard')}</span>
1050
- </span>
1051
- <span className={css.cardDesc}>{t('addPluginsViewerCardDesc')}</span>
1052
- </button>
1053
- </div>
1054
- </div>
1055
-
1056
1004
  {/* The secondary settings popup: a feature's declared related settings
1057
1005
  as title/desc + switch rows in a wider-than-default Modal with a
1058
1006
  Done footer (Modal chrome is the app's own). Mounted only while a
@@ -1138,18 +1086,17 @@ export function SideCardSection({ store, service }: SideCardSectionProps) {
1138
1086
  </Modal>
1139
1087
  )}
1140
1088
 
1141
- {/* The "add plugin" modal (opened by the dashed cards above): declares
1142
- the extension point of the clicked kind, opens the GitHub topic,
1143
- and lists the matching recommended plugin catalog with per-entry
1144
- install buttons (the install flow opens a ~/.dsh terminal with
1145
- the command pre-typed; failures render inline here, in settings
1146
- only). Mounted only while open (Modal runs hooks unconditionally
1147
- — same SSR rule as the settings popup above). */}
1148
- {addPluginsOpen !== null && (
1089
+ {/* The "add plugin" modal (opened by the dashed card above): declares
1090
+ the tab extension point, opens the GitHub topic, and lists the
1091
+ recommended plugin catalog with per-entry install buttons (the
1092
+ install flow opens a ~/.dsh terminal with the command pre-typed;
1093
+ failures render inline here, in settings only). Mounted only while
1094
+ open (Modal runs hooks unconditionally — same SSR rule as the
1095
+ settings popup above). */}
1096
+ {addPluginsOpen && (
1149
1097
  <AddPluginModal
1150
1098
  service={service}
1151
- onClose={() => { setAddPluginsOpen(null) }}
1152
- kind={addPluginsOpen}
1099
+ onClose={() => { setAddPluginsOpen(false) }}
1153
1100
  />
1154
1101
  )}
1155
1102