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.
- package/README.md +27 -42
- package/lib/client-editor.js +449 -4278
- package/lib/client-registry.js +641 -1343
- package/lib/client-terminal.js +172 -262
- package/lib/client.js +645 -1347
- package/lib/index.js +24 -122
- package/lib/types/bundle-route.d.ts +1 -1
- package/lib/types/client/SideCardSection.d.ts +3 -3
- package/lib/types/client/TextEditor.d.ts +20 -10
- package/lib/types/client/add-plugin-modal.d.ts +0 -4
- package/lib/types/client/api.d.ts +8 -11
- package/lib/types/client/builtins/index.d.ts +10 -10
- package/lib/types/client/chunk-loader.d.ts +7 -8
- package/lib/types/client/locales.d.ts +0 -26
- package/lib/types/client/native-avoid.d.ts +2 -0
- package/lib/types/client/plugins-shared.d.ts +6 -6
- package/lib/types/client/service.d.ts +13 -83
- package/lib/types/context-types.d.ts +19 -10
- package/lib/types/index.d.ts +1 -1
- package/lib/types/prefs-shared.d.ts +1 -24
- package/package.json +26 -30
- package/src/bundle-route.ts +1 -1
- package/src/client/EditorHost.tsx +52 -122
- package/src/client/SandboxStatusBar.tsx +12 -9
- package/src/client/SideCardSection.module.css +2 -2
- package/src/client/SideCardSection.tsx +28 -81
- package/src/client/SideChatView.module.css +12 -12
- package/src/client/TextEditor.tsx +89 -266
- package/src/client/add-plugin-modal.tsx +16 -24
- package/src/client/api.ts +1 -25
- package/src/client/builtins/index.ts +10 -14
- package/src/client/chunk-loader.ts +7 -8
- package/src/client/index.tsx +12 -3
- package/src/client/locales-ar.ts +0 -6
- package/src/client/locales-de.ts +0 -6
- package/src/client/locales-fr.ts +0 -6
- package/src/client/locales-hi.ts +0 -6
- package/src/client/locales-id.ts +0 -6
- package/src/client/locales-it.ts +0 -6
- package/src/client/locales-ja.ts +0 -6
- package/src/client/locales-ko.ts +0 -6
- package/src/client/locales-nl.ts +0 -6
- package/src/client/locales-pl.ts +0 -6
- package/src/client/locales-pt.ts +0 -6
- package/src/client/locales-ru.ts +0 -6
- package/src/client/locales-sv.ts +0 -6
- package/src/client/locales-th.ts +0 -6
- package/src/client/locales-tr.ts +0 -6
- package/src/client/locales-vi.ts +0 -6
- package/src/client/locales-zh-HK.ts +0 -6
- package/src/client/locales-zh-MO.ts +0 -6
- package/src/client/locales-zh-TW.ts +0 -6
- package/src/client/locales.ts +0 -52
- package/src/client/native-avoid.ts +155 -0
- package/src/client/plugins-shared.ts +6 -6
- package/src/client/prefs.ts +0 -7
- package/src/client/service.ts +13 -150
- package/src/client/sidebar.module.css +1 -425
- package/src/config.ts +1 -4
- package/src/context-types.ts +12 -7
- package/src/index.ts +12 -78
- package/src/jobs-routes.ts +3 -2
- package/src/prefs-shared.ts +1 -27
- package/src/sidechat-routes.ts +13 -8
- package/src/subagent-live-route.ts +2 -1
- package/lib/client-mermaid.js +0 -200753
- package/lib/types/client/MarkdownHtml.d.ts +0 -32
- package/lib/types/client/PdfView.d.ts +0 -6
- package/lib/types/client/builtins/viewers.d.ts +0 -3
- package/lib/types/client/chunks/mermaid.d.ts +0 -10
- package/lib/types/client/editor-load.d.ts +0 -66
- package/lib/types/client/image-types.d.ts +0 -3
- package/lib/types/client/markdown-html.d.ts +0 -96
- package/lib/types/client/markdown-images.d.ts +0 -45
- package/lib/types/client/md-toc.d.ts +0 -4
- package/lib/types/client/mermaid-blocks.d.ts +0 -47
- package/lib/types/client/mermaid-sanitize.d.ts +0 -2
- package/lib/types/client/mermaid.d.ts +0 -12
- package/lib/types/client/pdf-types.d.ts +0 -2
- package/lib/types/client/plugins-viewers.d.ts +0 -3
- package/lib/types/html-route.d.ts +0 -59
- package/src/client/MarkdownHtml.tsx +0 -296
- package/src/client/PdfView.tsx +0 -110
- package/src/client/builtins/viewers.tsx +0 -125
- package/src/client/chunks/mermaid.tsx +0 -10
- package/src/client/editor-load.ts +0 -92
- package/src/client/image-types.ts +0 -8
- package/src/client/markdown-html.ts +0 -331
- package/src/client/markdown-images.ts +0 -137
- package/src/client/md-toc.tsx +0 -127
- package/src/client/mermaid-blocks.ts +0 -110
- package/src/client/mermaid-sanitize.ts +0 -93
- package/src/client/mermaid.tsx +0 -406
- package/src/client/pdf-types.ts +0 -4
- package/src/client/plugins-viewers.ts +0 -30
- 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
|
|
3
|
-
* the GitHub topic URL. The
|
|
4
|
-
* `plugins-tabs.ts` (tab registrations) and
|
|
5
|
-
*
|
|
6
|
-
* (
|
|
7
|
-
*
|
|
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`. */
|
package/src/client/prefs.ts
CHANGED
|
@@ -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
|
}
|
package/src/client/service.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The BetterSidebar client service: a registry that external plugins use
|
|
3
|
-
* to contribute sidebar tab types
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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,
|