dsh-coding-sidebar 1.0.2 → 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 (96) hide show
  1. package/README.md +27 -42
  2. package/lib/client-editor.js +449 -4278
  3. package/lib/client-registry.js +641 -1343
  4. package/lib/client-terminal.js +172 -262
  5. package/lib/client.js +645 -1347
  6. package/lib/index.js +24 -122
  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/context-types.d.ts +19 -10
  19. package/lib/types/index.d.ts +1 -1
  20. package/lib/types/prefs-shared.d.ts +1 -24
  21. package/package.json +26 -30
  22. package/src/bundle-route.ts +1 -1
  23. package/src/client/EditorHost.tsx +52 -122
  24. package/src/client/SandboxStatusBar.tsx +12 -9
  25. package/src/client/SideCardSection.module.css +2 -2
  26. package/src/client/SideCardSection.tsx +28 -81
  27. package/src/client/SideChatView.module.css +12 -12
  28. package/src/client/TextEditor.tsx +89 -266
  29. package/src/client/add-plugin-modal.tsx +16 -24
  30. package/src/client/api.ts +1 -25
  31. package/src/client/builtins/index.ts +10 -14
  32. package/src/client/chunk-loader.ts +7 -8
  33. package/src/client/index.tsx +12 -3
  34. package/src/client/locales-ar.ts +0 -6
  35. package/src/client/locales-de.ts +0 -6
  36. package/src/client/locales-fr.ts +0 -6
  37. package/src/client/locales-hi.ts +0 -6
  38. package/src/client/locales-id.ts +0 -6
  39. package/src/client/locales-it.ts +0 -6
  40. package/src/client/locales-ja.ts +0 -6
  41. package/src/client/locales-ko.ts +0 -6
  42. package/src/client/locales-nl.ts +0 -6
  43. package/src/client/locales-pl.ts +0 -6
  44. package/src/client/locales-pt.ts +0 -6
  45. package/src/client/locales-ru.ts +0 -6
  46. package/src/client/locales-sv.ts +0 -6
  47. package/src/client/locales-th.ts +0 -6
  48. package/src/client/locales-tr.ts +0 -6
  49. package/src/client/locales-vi.ts +0 -6
  50. package/src/client/locales-zh-HK.ts +0 -6
  51. package/src/client/locales-zh-MO.ts +0 -6
  52. package/src/client/locales-zh-TW.ts +0 -6
  53. package/src/client/locales.ts +0 -52
  54. package/src/client/native-avoid.ts +155 -0
  55. package/src/client/plugins-shared.ts +6 -6
  56. package/src/client/prefs.ts +0 -7
  57. package/src/client/service.ts +13 -150
  58. package/src/client/sidebar.module.css +1 -425
  59. package/src/config.ts +1 -4
  60. package/src/context-types.ts +12 -7
  61. package/src/index.ts +12 -78
  62. package/src/jobs-routes.ts +3 -2
  63. package/src/prefs-shared.ts +1 -27
  64. package/src/sidechat-routes.ts +13 -8
  65. package/src/subagent-live-route.ts +2 -1
  66. package/lib/client-mermaid.js +0 -200753
  67. package/lib/types/client/MarkdownHtml.d.ts +0 -32
  68. package/lib/types/client/PdfView.d.ts +0 -6
  69. package/lib/types/client/builtins/viewers.d.ts +0 -3
  70. package/lib/types/client/chunks/mermaid.d.ts +0 -10
  71. package/lib/types/client/editor-load.d.ts +0 -66
  72. package/lib/types/client/image-types.d.ts +0 -3
  73. package/lib/types/client/markdown-html.d.ts +0 -96
  74. package/lib/types/client/markdown-images.d.ts +0 -45
  75. package/lib/types/client/md-toc.d.ts +0 -4
  76. package/lib/types/client/mermaid-blocks.d.ts +0 -47
  77. package/lib/types/client/mermaid-sanitize.d.ts +0 -2
  78. package/lib/types/client/mermaid.d.ts +0 -12
  79. package/lib/types/client/pdf-types.d.ts +0 -2
  80. package/lib/types/client/plugins-viewers.d.ts +0 -3
  81. package/lib/types/html-route.d.ts +0 -59
  82. package/src/client/MarkdownHtml.tsx +0 -296
  83. package/src/client/PdfView.tsx +0 -110
  84. package/src/client/builtins/viewers.tsx +0 -125
  85. package/src/client/chunks/mermaid.tsx +0 -10
  86. package/src/client/editor-load.ts +0 -92
  87. package/src/client/image-types.ts +0 -8
  88. package/src/client/markdown-html.ts +0 -331
  89. package/src/client/markdown-images.ts +0 -137
  90. package/src/client/md-toc.tsx +0 -127
  91. package/src/client/mermaid-blocks.ts +0 -110
  92. package/src/client/mermaid-sanitize.ts +0 -93
  93. package/src/client/mermaid.tsx +0 -406
  94. package/src/client/pdf-types.ts +0 -4
  95. package/src/client/plugins-viewers.ts +0 -30
  96. package/src/html-route.ts +0 -106
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Native-sidebar avoidance: when the DSH shell's own sidebar expands to its
3
+ * wide form, an expanded sidebar panel would crowd the page into several
4
+ * columns — so the panel yields (collapses; the fact that WE collapsed it
5
+ * is remembered) and restores when the native sidebar returns to its rail.
6
+ *
7
+ * Detection: the host exposes no programmable open/close event — the
8
+ * `settings.trigger` slot's `{ wide }` owner prop is pushed only to the
9
+ * slot's occupant, and that slot is single-masked (occupying it would take
10
+ * over the settings button). But the shell stamps every slot outlet with
11
+ * `data-slot="<key>"` (the conversation package's own CSS targets
12
+ * `[data-slot=conversation\.session]`), and the native sidebar hosts the
13
+ * `sidebar.*` slots — its documented collapsed form is a 56px icon rail,
14
+ * the expanded form a full column. So: watch the first
15
+ * `[data-slot^="sidebar."]` element with a ResizeObserver; width > 80px
16
+ * means expanded. No anchor (older host, attribute renamed) → the feature
17
+ * stays inert.
18
+ *
19
+ * The yield rides the regular `panelOpen` state (same contract as every
20
+ * other collapse: the toggle button, content-open auto-expansion and the
21
+ * per-session persistence all keep working). A panel transition we did not
22
+ * apply — the user reopening the panel, or a content open expanding it —
23
+ * hands ownership back (no restore on native close); a session switch
24
+ * re-runs the rule against the newly active session.
25
+ */
26
+ import { togglePanel } from './state.ts'
27
+ import type { SidebarStore } from './state.ts'
28
+
29
+ /** The documented rail is 56px; anything wider is an expanded sidebar. */
30
+ const RAIL_MAX_WIDTH = 80
31
+
32
+ /** The native sidebar's slot outlets (sidebar.settings and siblings). */
33
+ const ANCHOR_SELECTOR = '[data-slot^="sidebar."]'
34
+
35
+ export function installNativeSidebarAvoidance(store: SidebarStore): () => void {
36
+ if (typeof ResizeObserver === 'undefined' || typeof MutationObserver === 'undefined'
37
+ || typeof document === 'undefined' || document.body === null) {
38
+ return () => {}
39
+ }
40
+
41
+ let disposed = false
42
+ /** null until the anchor is found and measured (unknown = never act). */
43
+ let nativeWide: boolean | null = null
44
+ /** The currently open panel was collapsed by US (restore on native close). */
45
+ let yielded = false
46
+ /** Guards the store subscription: the in-flight reduce is ours. */
47
+ let applying = false
48
+ let lastSeenOpen: boolean | null = null
49
+ let lastSessionId: string | undefined
50
+
51
+ const snapshotOpen = (): boolean => store.getSnapshot().state?.panelOpen === true
52
+
53
+ /** Apply the avoidance rule for the current native-sidebar state. */
54
+ const reevaluate = (): void => {
55
+ if (disposed || nativeWide === null) return
56
+ if (nativeWide && !yielded && snapshotOpen()) {
57
+ // Yield: collapse the panel, remember it was ours.
58
+ yielded = true
59
+ applying = true
60
+ store.reduce(togglePanel)
61
+ } else if (!nativeWide && yielded) {
62
+ // Restore: the native sidebar closed — reopen what we collapsed.
63
+ yielded = false
64
+ if (snapshotOpen()) return
65
+ applying = true
66
+ store.reduce(togglePanel)
67
+ }
68
+ }
69
+
70
+ // Store watcher: a panel transition we did not apply hands ownership back
71
+ // (the user or a content open owns the state now); a session switch
72
+ // re-runs the rule against the newly active session.
73
+ lastSeenOpen = snapshotOpen()
74
+ lastSessionId = store.getSnapshot().sessionId
75
+ const unsubscribe = store.subscribe(() => {
76
+ if (disposed) return
77
+ const snapshot = store.getSnapshot()
78
+ const open = snapshotOpen()
79
+ if (applying) {
80
+ applying = false
81
+ lastSeenOpen = open
82
+ lastSessionId = snapshot.sessionId
83
+ return
84
+ }
85
+ if (snapshot.sessionId !== lastSessionId) {
86
+ lastSessionId = snapshot.sessionId
87
+ lastSeenOpen = open
88
+ reevaluate()
89
+ return
90
+ }
91
+ if (open !== lastSeenOpen) {
92
+ lastSeenOpen = open
93
+ // Reopened by someone else while yielded: they own it now.
94
+ if (yielded && open) yielded = false
95
+ }
96
+ })
97
+
98
+ let resizeObserver: ResizeObserver | null = null
99
+ let finder: MutationObserver | null = null
100
+
101
+ const applyWidth = (width: number): void => {
102
+ const wide = width > RAIL_MAX_WIDTH
103
+ if (wide !== nativeWide) {
104
+ nativeWide = wide
105
+ reevaluate()
106
+ }
107
+ }
108
+
109
+ const watch = (element: Element): void => {
110
+ resizeObserver = new ResizeObserver(() => {
111
+ // SPA teardown can remove the anchor after observation started —
112
+ // re-arm the search instead of trusting a detached width.
113
+ if (!element.isConnected) {
114
+ resizeObserver?.disconnect()
115
+ resizeObserver = null
116
+ nativeWide = null
117
+ startSearch()
118
+ return
119
+ }
120
+ applyWidth(element.getBoundingClientRect().width)
121
+ })
122
+ resizeObserver.observe(element)
123
+ applyWidth(element.getBoundingClientRect().width)
124
+ }
125
+
126
+ const startSearch = (): void => {
127
+ const found = document.querySelector(ANCHOR_SELECTOR)
128
+ if (found !== null) {
129
+ watch(found)
130
+ return
131
+ }
132
+ // The shell renders later than us (route mount, lazy shell) — watch for
133
+ // the anchor's appearance (childList only; disconnected once found).
134
+ finder = new MutationObserver(() => {
135
+ const element = document.querySelector(ANCHOR_SELECTOR)
136
+ if (element !== null) {
137
+ finder?.disconnect()
138
+ finder = null
139
+ watch(element)
140
+ }
141
+ })
142
+ finder.observe(document.body, { childList: true, subtree: true })
143
+ }
144
+
145
+ startSearch()
146
+
147
+ return () => {
148
+ disposed = true
149
+ unsubscribe()
150
+ resizeObserver?.disconnect()
151
+ resizeObserver = null
152
+ finder?.disconnect()
153
+ finder = null
154
+ }
155
+ }
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Shared vocabulary of the recommended plugin catalogs: the entry shape and
3
- * the GitHub topic URL. The two catalogs live in sibling modules —
4
- * `plugins-tabs.ts` (tab registrations) and `plugins-viewers.ts` (file
5
- * previewer registrations) and are shown in the two "add plugin" modals
6
- * (Side card settings → the dashed cards at the end of the 侧边栏内容 /
7
- * 文件预览 grids).
2
+ * Shared vocabulary of the recommended plugin catalog: the entry shape and
3
+ * the GitHub topic URL. The catalog lives in the sibling module
4
+ * `plugins-tabs.ts` (tab registrations) and is shown in the "add plugin"
5
+ * modal (Side card settings the dashed card at the end of the 侧边栏内容
6
+ * grid). (v1.0.4: the file-previewer catalog was retired with the
7
+ * file-viewer registry.)
8
8
  */
9
9
 
10
10
  /** The GitHub topic page listing every repo tagged `dsh-coding-sidebar`. */
@@ -101,12 +101,6 @@ export function parsePrefs(value: unknown): SidebarPrefs {
101
101
  titleBarStripPx: typeof record.titleBarStripPx === 'number' && Number.isFinite(record.titleBarStripPx)
102
102
  ? clampTitleBarStrip(record.titleBarStripPx)
103
103
  : SIDEBAR_PREFS_DEFAULTS.titleBarStripPx,
104
- htmlViewerNoSandbox: typeof record.htmlViewerNoSandbox === 'boolean'
105
- ? record.htmlViewerNoSandbox
106
- : SIDEBAR_PREFS_DEFAULTS.htmlViewerNoSandbox,
107
- htmlViewerDefaultUnsafe: typeof record.htmlViewerDefaultUnsafe === 'boolean'
108
- ? record.htmlViewerDefaultUnsafe
109
- : SIDEBAR_PREFS_DEFAULTS.htmlViewerDefaultUnsafe,
110
104
  browserNoSandbox: typeof record.browserNoSandbox === 'boolean'
111
105
  ? record.browserNoSandbox
112
106
  : SIDEBAR_PREFS_DEFAULTS.browserNoSandbox,
@@ -123,7 +117,6 @@ export function parsePrefs(value: unknown): SidebarPrefs {
123
117
  ? record.browserAllowedLoopback
124
118
  : SIDEBAR_PREFS_DEFAULTS.browserAllowedLoopback,
125
119
  tabsEnabled: booleanMapOf(record.tabsEnabled),
126
- viewersEnabled: booleanMapOf(record.viewersEnabled),
127
120
  pluginSettings: pluginSettingsMapOf(record.pluginSettings),
128
121
  }
129
122
  }
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * The BetterSidebar client service: a registry that external plugins use
3
- * to contribute sidebar tab types and file previewers. The service is
4
- * published to the cordis context as `ctx.betterSidebar` (see
5
- * {@link ../context-types.ts}); consumers declare it in `inject` and call
6
- * `registerTab` / `registerFileViewer`, both returning a disposer that
7
- * cordis auto-invokes on fiber disposal (HMR-safe).
3
+ * to contribute sidebar tab types. The service is published to the cordis
4
+ * context as `ctx.betterSidebar` (see {@link ../context-types.ts});
5
+ * consumers declare it in `inject` and call `registerTab`, which returns a
6
+ * disposer that cordis auto-invokes on fiber disposal (HMR-safe).
7
+ *
8
+ * (v1.0.4: the file-viewer registry — `registerFileViewer` /
9
+ * `matchFileViewer` — was retired; file preview is the host's job now and
10
+ * the sidebar edits text only.)
8
11
  *
9
12
  * Design notes:
10
13
  * - The registry is synchronous-snapshot (Map + listener set) so React
@@ -15,9 +18,6 @@
15
18
  * `single: true` is sugar for `dedupeKey: () => id`.
16
19
  * - `createTab` lets a descriptor own tab instantiation (the terminal
17
20
  * builtin uses it to mint `terminal:<n>` ids and bump `nextTerminal`).
18
- * - `matchFileViewer` walks descriptors in priority order (desc, stable):
19
- * per descriptor it tries `detect` first (when `head` bytes are given),
20
- * then `exts`; `exts: []` is a catch-all that matches any path.
21
21
  */
22
22
  import type { ReactNode } from 'react'
23
23
  import type { Context } from '../context-types.ts'
@@ -65,7 +65,7 @@ export interface SidebarSettingSelectOption {
65
65
  icon?: ReactNode | ((size: number) => ReactNode)
66
66
  }
67
67
 
68
- /** One declarative setting of a tab/viewer, rendered as a nested row in the
68
+ /** One declarative setting of a registered tab, rendered as a nested row in the
69
69
  * Side card settings page (e.g. the Subagent page's "auto-open when a
70
70
  * subagent appears" switch, or the terminal's custom font rows). `type`
71
71
  * selects the control: 'switch' (default) renders the custom switch,
@@ -110,7 +110,7 @@ export interface SidebarSettingsRenderProps {
110
110
  close(): void
111
111
  }
112
112
 
113
- /** Declarative settings of one registered tab or file viewer. */
113
+ /** Declarative settings of one registered tab. */
114
114
  export interface SidebarSettingsDeclaration {
115
115
  /**
116
116
  * Extra settings rows rendered under the feature's own row in the
@@ -240,88 +240,19 @@ export interface TabDescriptor {
240
240
  component: (props: TabComponentProps) => ReactNode
241
241
  }
242
242
 
243
- /** How the host loads a file's bytes for one viewer. */
244
- export type FileFetchStrategy =
245
- | 'none' // no bytes needed (image/pdf/office fetch through mediaUrl themselves)
246
- | 'fsRead' // text read through /sidebar/api fs.read
247
- | 'mediaUrl' // the viewer gets a media URL string
248
- | 'custom' // the viewer's load() fetches its own bytes
249
- | 'binary-download' // show a download button (no client-side renderer)
250
-
251
- /** Props every file viewer component receives. */
252
- export interface FileViewerProps {
253
- ctx: Context
254
- store: SidebarStore
255
- scope: SessionScope
256
- path: string
257
- title: string
258
- /** The matching descriptor's id (`'code'`, `'my-plugin:csv'`). */
259
- viewerId: string
260
- /** fsRead text content (fetchStrategy='fsRead'). */
261
- content?: string
262
- truncated?: boolean
263
- /** mediaUrl for the path (fetchStrategy='mediaUrl'). */
264
- mediaUrl?: string
265
- /** custom load() return value (fetchStrategy='custom'). */
266
- customData?: unknown
267
- /** Internal (built-in text editor): 'host' asks the viewer to skip its own
268
- * toolbar row — the editor host's merged-mode header renders it instead,
269
- * fed through the two callbacks below. Viewers that ignore these fields
270
- * render exactly as before. */
271
- toolbar?: 'self' | 'host'
272
- /** Internal: the viewer reports its toolbar state (mode/dirty/save). */
273
- onToolbarState?: (state: EditorToolbarState) => void
274
- /** Internal: the viewer registers its toolbar commands on mount (null on
275
- * unmount). */
276
- onToolbarControls?: (controls: EditorToolbarControls | null) => void
277
- }
278
-
279
- /** The toolbar state a text editor reports to the host's merged-mode header. */
243
+ /** The toolbar state the text editor reports to the host's merged-mode header. */
280
244
  export interface EditorToolbarState {
281
- /** Whether the preview/edit mode toggle applies (markdown/html). */
282
- modes: boolean
283
- mode: 'preview' | 'edit'
284
245
  dirty: boolean
285
246
  /** Whether saving applies (text content loaded). */
286
247
  editable: boolean
287
248
  saveState: 'idle' | 'saving' | 'saved' | 'failed'
288
249
  }
289
250
 
290
- /** The commands the host's merged-mode header sends back to the viewer. */
251
+ /** The commands the host's merged-mode header sends back to the editor. */
291
252
  export interface EditorToolbarControls {
292
- setMode(mode: 'preview' | 'edit'): void
293
253
  save(): void
294
254
  }
295
255
 
296
- /** Describes one file previewer (builtins register themselves too). */
297
- export interface FileViewerDescriptor {
298
- /** Unique id (`'image'`, `'pdf'`, `'my-plugin:csv'`). */
299
- id: string
300
- /** Display name for the settings inventory (falls back to `id` when absent). */
301
- title?: string | (() => string)
302
- /** Icon shown in the settings inventory. */
303
- icon?: ReactNode | ((size: number) => ReactNode)
304
- /** Lowercase extensions without leading dot (`['png','jpg']`). `[]` = match any (catch-all). */
305
- exts: readonly string[]
306
- /** Higher wins; default 0. Builtins use 0; the catch-all `code` viewer uses -100. */
307
- priority?: number
308
- fetchStrategy: FileFetchStrategy
309
- /**
310
- * Content sniff: when `head` bytes are available the descriptor's `detect`
311
- * is consulted before its `exts` (per-descriptor, in priority order).
312
- */
313
- detect?: (path: string, head: Uint8Array) => boolean
314
- /** fetchStrategy='custom' loader. `signal` (v0.12.0+) aborts on viewer
315
- * teardown / re-match; loaders that ignore it keep working. */
316
- load?: (path: string, scope: SessionScope, signal?: AbortSignal) => Promise<unknown>
317
- /**
318
- * Declarative settings shown in the Side card settings page: every
319
- * registered viewer gets an enable/disable switch (icon + title + exts).
320
- */
321
- settings?: SidebarSettingsDeclaration
322
- component: (props: FileViewerProps) => ReactNode
323
- }
324
-
325
256
  /** One `openTab` request. */
326
257
  export interface OpenTabSeed {
327
258
  type: string
@@ -344,9 +275,7 @@ export interface OpenTabSeed {
344
275
  */
345
276
  export interface BetterSidebarService {
346
277
  registerTab(descriptor: TabDescriptor): () => void
347
- registerFileViewer(descriptor: FileViewerDescriptor): () => void
348
278
  getTabs(): readonly TabDescriptor[]
349
- getFileViewers(): readonly FileViewerDescriptor[]
350
279
  /** Find a tab descriptor by id (undefined if not registered). */
351
280
  getTab(id: string): TabDescriptor | undefined
352
281
  /**
@@ -356,13 +285,6 @@ export interface BetterSidebarService {
356
285
  * derived flows gate on it).
357
286
  */
358
287
  isTabEnabled(id: string): boolean
359
- /** Whether a file viewer is enabled (absent `viewersEnabled[id]` = enabled). */
360
- isViewerEnabled(id: string): boolean
361
- /**
362
- * Find a file viewer for a path (priority desc; detect first, then exts).
363
- * Disabled viewers are skipped, so files fall through to the next match.
364
- */
365
- matchFileViewer(path: string, head?: Uint8Array): FileViewerDescriptor | undefined
366
288
  /**
367
289
  * Open a tab (used by external tabs and the + menu). `title` overrides
368
290
  * the descriptor's title when given (the editor tab shows the file name);
@@ -424,14 +346,6 @@ export interface BetterSidebarService {
424
346
  openFile(scope: SessionScope, path: string, title?: string): void
425
347
  }
426
348
 
427
- /** Extract the lowercase extension without leading dot from a path. */
428
- function extOfPath(path: string): string {
429
- const at = path.lastIndexOf('.')
430
- if (at === -1) return ''
431
- const base = path.slice(at + 1).toLowerCase()
432
- return base.includes('/') || base.includes('\\') ? '' : base
433
- }
434
-
435
349
  /** The file name of a path (both separators). */
436
350
  function baseNameOf(path: string): string {
437
351
  const at = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
@@ -517,12 +431,11 @@ function safeCall(fn: () => void): void {
517
431
 
518
432
  /**
519
433
  * Create one BetterSidebar service bound to a store. The service owns the
520
- * tab/viewer registries (Map + listener set) and proxies openTab/closeTab
434
+ * tab registry (Map + listener set) and proxies openTab/closeTab
521
435
  * to the store's reducer. One instance per client plugin activation.
522
436
  */
523
437
  export function createBetterSidebarService(store: SidebarStore): BetterSidebarService {
524
438
  const tabs = new Map<string, TabDescriptor>()
525
- const viewers = new Map<string, FileViewerDescriptor>()
526
439
  const listeners = new Set<() => void>()
527
440
 
528
441
  const notify = (): void => {
@@ -548,58 +461,12 @@ export function createBetterSidebarService(store: SidebarStore): BetterSidebarSe
548
461
  }
549
462
  }
550
463
 
551
- const registerFileViewer = (descriptor: FileViewerDescriptor): (() => void) => {
552
- if (viewers.has(descriptor.id)) {
553
- throw new Error(`[dsh-coding-sidebar] file viewer "${descriptor.id}" already registered`)
554
- }
555
- viewers.set(descriptor.id, descriptor)
556
- notify()
557
- return () => {
558
- if (viewers.get(descriptor.id) === descriptor) {
559
- viewers.delete(descriptor.id)
560
- notify()
561
- }
562
- }
563
- }
564
-
565
464
  const getTabs = (): readonly TabDescriptor[] => Array.from(tabs.values())
566
- const getFileViewers = (): readonly FileViewerDescriptor[] => Array.from(viewers.values())
567
465
  const getTab = (id: string): TabDescriptor | undefined => tabs.get(id)
568
466
 
569
467
  // The enable switches come from the user's side card prefs (the shared
570
468
  // store the service is bound to): an absent key means enabled.
571
469
  const isTabEnabled = (id: string): boolean => store.getPrefs().tabsEnabled[id] !== false
572
- const isViewerEnabled = (id: string): boolean => store.getPrefs().viewersEnabled[id] !== false
573
-
574
- const matchFileViewer = (path: string, head?: Uint8Array): FileViewerDescriptor | undefined => {
575
- const ext = extOfPath(path)
576
- // Single pass in priority order (descending; stable for equal
577
- // priorities — insertion order). Each descriptor gets first refusal in
578
- // its own turn: `detect` (when head bytes are available) beats its own
579
- // `exts`, and `exts: []` is a catch-all matching any path — so the
580
- // catch-all `code` viewer (-100) only sees paths no higher-priority
581
- // descriptor claimed. Disabled viewers are skipped entirely.
582
- for (const v of Array.from(viewers.values()).sort(
583
- (a, b) => (b.priority ?? 0) - (a.priority ?? 0),
584
- )) {
585
- if (!isViewerEnabled(v.id)) continue
586
- // Content sniff first (only when head bytes are available).
587
- if (head !== undefined && v.detect !== undefined) {
588
- if (v.detect(path, head)) return v
589
- // A catch-all with detect is SNIFF-ONLY: it must not blind-claim
590
- // paths it never sniffed (a magic-number viewer must not swallow
591
- // every file before the real viewers get their turn).
592
- if (v.exts.length === 0) continue
593
- } else if (v.exts.length === 0) {
594
- // Blind catch-all (no detect) claims anything; a sniff-only
595
- // catch-all (detect defined, no head yet) yields this round.
596
- if (v.detect === undefined) return v
597
- continue
598
- }
599
- if (v.exts.includes(ext)) return v
600
- }
601
- return undefined
602
- }
603
470
 
604
471
  const openTab = (seed: OpenTabSeed, scope?: SessionScope): void => {
605
472
  // A type the user disabled in settings never opens — neither from the
@@ -807,13 +674,9 @@ export function createBetterSidebarService(store: SidebarStore): BetterSidebarSe
807
674
 
808
675
  return {
809
676
  registerTab,
810
- registerFileViewer,
811
677
  getTabs,
812
- getFileViewers,
813
678
  getTab,
814
679
  isTabEnabled,
815
- isViewerEnabled,
816
- matchFileViewer,
817
680
  openTab,
818
681
  closeTab,
819
682
  subscribe,