@keith9681/dsh-sidebar 0.15.2

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 (220) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +0 -0
  3. package/README_EN.md +0 -0
  4. package/cordis.patch.yml +49 -0
  5. package/lib/client-editor.js +34651 -0
  6. package/lib/client-mermaid.js +200629 -0
  7. package/lib/client-registry.js +13388 -0
  8. package/lib/client-terminal.js +8904 -0
  9. package/lib/client.js +13388 -0
  10. package/lib/index.js +3959 -0
  11. package/lib/invariant.js +22 -0
  12. package/lib/types/agent-pty.d.ts +226 -0
  13. package/lib/types/browser-probe.d.ts +15 -0
  14. package/lib/types/bundle-route.d.ts +12 -0
  15. package/lib/types/client/BrowserView.d.ts +22 -0
  16. package/lib/types/client/DiffTab.d.ts +6 -0
  17. package/lib/types/client/DiffView.d.ts +51 -0
  18. package/lib/types/client/EditorHost.d.ts +12 -0
  19. package/lib/types/client/FileTree.d.ts +36 -0
  20. package/lib/types/client/GitView.d.ts +8 -0
  21. package/lib/types/client/OrphanedTab.d.ts +11 -0
  22. package/lib/types/client/PdfView.d.ts +6 -0
  23. package/lib/types/client/RenderBoundary.d.ts +32 -0
  24. package/lib/types/client/SandboxStatusBar.d.ts +10 -0
  25. package/lib/types/client/SideCardSection.d.ts +74 -0
  26. package/lib/types/client/SideChatView.d.ts +16 -0
  27. package/lib/types/client/Sidebar.d.ts +6 -0
  28. package/lib/types/client/SubagentView.d.ts +16 -0
  29. package/lib/types/client/TabBar.d.ts +43 -0
  30. package/lib/types/client/TerminalView.d.ts +24 -0
  31. package/lib/types/client/TextEditor.d.ts +10 -0
  32. package/lib/types/client/TreePanel.d.ts +23 -0
  33. package/lib/types/client/UploadOverlay.d.ts +20 -0
  34. package/lib/types/client/add-plugin-modal.d.ts +15 -0
  35. package/lib/types/client/api.d.ts +268 -0
  36. package/lib/types/client/binary-download.d.ts +12 -0
  37. package/lib/types/client/breakpoints.d.ts +12 -0
  38. package/lib/types/client/browser.d.ts +50 -0
  39. package/lib/types/client/builtins/index.d.ts +18 -0
  40. package/lib/types/client/builtins/tabs.d.ts +11 -0
  41. package/lib/types/client/builtins/viewers.d.ts +3 -0
  42. package/lib/types/client/chunk-loader.d.ts +124 -0
  43. package/lib/types/client/chunks/editor.d.ts +9 -0
  44. package/lib/types/client/chunks/mermaid.d.ts +10 -0
  45. package/lib/types/client/chunks/terminal.d.ts +9 -0
  46. package/lib/types/client/cm-themes.d.ts +24 -0
  47. package/lib/types/client/conversation-draft.d.ts +14 -0
  48. package/lib/types/client/desktop-env.d.ts +35 -0
  49. package/lib/types/client/editable-state.d.ts +27 -0
  50. package/lib/types/client/editor-load.d.ts +66 -0
  51. package/lib/types/client/frame-batcher.d.ts +24 -0
  52. package/lib/types/client/icons.d.ts +57 -0
  53. package/lib/types/client/image-types.d.ts +3 -0
  54. package/lib/types/client/ime-guard.d.ts +36 -0
  55. package/lib/types/client/index.d.ts +19 -0
  56. package/lib/types/client/intercept.d.ts +33 -0
  57. package/lib/types/client/lang.d.ts +12 -0
  58. package/lib/types/client/lazy-chunk.d.ts +25 -0
  59. package/lib/types/client/link-intercept.d.ts +42 -0
  60. package/lib/types/client/locales.d.ts +366 -0
  61. package/lib/types/client/mermaid-blocks.d.ts +41 -0
  62. package/lib/types/client/mermaid-sanitize.d.ts +2 -0
  63. package/lib/types/client/mermaid.d.ts +12 -0
  64. package/lib/types/client/open-when-sized.d.ts +20 -0
  65. package/lib/types/client/open-with-settings.d.ts +4 -0
  66. package/lib/types/client/open-with.d.ts +87 -0
  67. package/lib/types/client/openpath-intercept.d.ts +41 -0
  68. package/lib/types/client/paths.d.ts +35 -0
  69. package/lib/types/client/pdf-types.d.ts +2 -0
  70. package/lib/types/client/plugin-settings.d.ts +8 -0
  71. package/lib/types/client/plugins-shared.d.ts +27 -0
  72. package/lib/types/client/plugins-tabs.d.ts +3 -0
  73. package/lib/types/client/plugins-viewers.d.ts +3 -0
  74. package/lib/types/client/prefs.d.ts +41 -0
  75. package/lib/types/client/produced-files.d.ts +23 -0
  76. package/lib/types/client/selection-payload.d.ts +27 -0
  77. package/lib/types/client/service.d.ts +440 -0
  78. package/lib/types/client/settings-nav-icon.d.ts +19 -0
  79. package/lib/types/client/shell-presets.d.ts +48 -0
  80. package/lib/types/client/sidechat-transcript.d.ts +103 -0
  81. package/lib/types/client/split-pane.d.ts +30 -0
  82. package/lib/types/client/state.d.ts +359 -0
  83. package/lib/types/client/subagent-detect.d.ts +54 -0
  84. package/lib/types/client/subagent-jobs.d.ts +63 -0
  85. package/lib/types/client/tab-content-memo.d.ts +33 -0
  86. package/lib/types/client/terminal-font.d.ts +20 -0
  87. package/lib/types/client/theme.d.ts +45 -0
  88. package/lib/types/client/titlebar-strip.d.ts +23 -0
  89. package/lib/types/client/upload.d.ts +69 -0
  90. package/lib/types/client/wco.d.ts +47 -0
  91. package/lib/types/config.d.ts +64 -0
  92. package/lib/types/context-types.d.ts +603 -0
  93. package/lib/types/fs-operations.d.ts +28 -0
  94. package/lib/types/fs-search.d.ts +24 -0
  95. package/lib/types/fs-tree.d.ts +51 -0
  96. package/lib/types/git.d.ts +82 -0
  97. package/lib/types/html-route.d.ts +59 -0
  98. package/lib/types/index.d.ts +47 -0
  99. package/lib/types/invariant.d.ts +15 -0
  100. package/lib/types/jobs-routes.d.ts +45 -0
  101. package/lib/types/open-external.d.ts +24 -0
  102. package/lib/types/prefs-shared.d.ts +225 -0
  103. package/lib/types/pty-deps.d.ts +78 -0
  104. package/lib/types/pty-manager.d.ts +168 -0
  105. package/lib/types/sidechat-core.d.ts +172 -0
  106. package/lib/types/sidechat-routes.d.ts +30 -0
  107. package/lib/types/subagent-activity.d.ts +44 -0
  108. package/lib/types/subagent-live-route.d.ts +44 -0
  109. package/lib/types/tools.d.ts +31 -0
  110. package/lib/types/trust-fence.d.ts +24 -0
  111. package/lib/types/wire.d.ts +38 -0
  112. package/package.json +173 -0
  113. package/scripts/install.ps1 +289 -0
  114. package/scripts/install.sh +287 -0
  115. package/src/agent-pty.ts +527 -0
  116. package/src/browser-probe.ts +26 -0
  117. package/src/bundle-route.ts +129 -0
  118. package/src/client/BrowserView.tsx +253 -0
  119. package/src/client/DiffTab.tsx +110 -0
  120. package/src/client/DiffView.tsx +305 -0
  121. package/src/client/EditorHost.tsx +525 -0
  122. package/src/client/FileTree.tsx +653 -0
  123. package/src/client/GitView.tsx +558 -0
  124. package/src/client/OrphanedTab.tsx +27 -0
  125. package/src/client/PdfView.tsx +110 -0
  126. package/src/client/RenderBoundary.tsx +49 -0
  127. package/src/client/SandboxStatusBar.tsx +60 -0
  128. package/src/client/SideCardSection.module.css +960 -0
  129. package/src/client/SideCardSection.tsx +1152 -0
  130. package/src/client/SideChatView.module.css +437 -0
  131. package/src/client/SideChatView.tsx +644 -0
  132. package/src/client/Sidebar.tsx +1148 -0
  133. package/src/client/SubagentView.module.css +547 -0
  134. package/src/client/SubagentView.tsx +883 -0
  135. package/src/client/TabBar.tsx +305 -0
  136. package/src/client/TerminalView.tsx +383 -0
  137. package/src/client/TextEditor.tsx +500 -0
  138. package/src/client/TreePanel.tsx +273 -0
  139. package/src/client/UploadOverlay.tsx +62 -0
  140. package/src/client/add-plugin-modal.tsx +199 -0
  141. package/src/client/api.ts +335 -0
  142. package/src/client/binary-download.tsx +23 -0
  143. package/src/client/breakpoints.ts +51 -0
  144. package/src/client/browser.ts +119 -0
  145. package/src/client/builtins/index.ts +37 -0
  146. package/src/client/builtins/tabs.tsx +331 -0
  147. package/src/client/builtins/viewers.tsx +125 -0
  148. package/src/client/chunk-loader.ts +351 -0
  149. package/src/client/chunks/editor.tsx +9 -0
  150. package/src/client/chunks/mermaid.tsx +10 -0
  151. package/src/client/chunks/terminal.tsx +9 -0
  152. package/src/client/cm-themes.ts +129 -0
  153. package/src/client/conversation-draft.ts +29 -0
  154. package/src/client/css-modules.d.ts +5 -0
  155. package/src/client/desktop-env.ts +72 -0
  156. package/src/client/editable-state.ts +70 -0
  157. package/src/client/editor-load.ts +92 -0
  158. package/src/client/frame-batcher.ts +56 -0
  159. package/src/client/icons.tsx +161 -0
  160. package/src/client/image-types.ts +8 -0
  161. package/src/client/ime-guard.ts +47 -0
  162. package/src/client/index.tsx +369 -0
  163. package/src/client/intercept.tsx +110 -0
  164. package/src/client/lang.ts +103 -0
  165. package/src/client/layout.css +94 -0
  166. package/src/client/lazy-chunk.tsx +89 -0
  167. package/src/client/link-intercept.ts +73 -0
  168. package/src/client/locales.ts +734 -0
  169. package/src/client/mermaid-blocks.ts +110 -0
  170. package/src/client/mermaid-sanitize.ts +93 -0
  171. package/src/client/mermaid.tsx +401 -0
  172. package/src/client/open-when-sized.ts +43 -0
  173. package/src/client/open-with-settings.tsx +135 -0
  174. package/src/client/open-with.ts +221 -0
  175. package/src/client/openpath-intercept.ts +62 -0
  176. package/src/client/paths.ts +47 -0
  177. package/src/client/pdf-types.ts +4 -0
  178. package/src/client/plugin-settings.ts +43 -0
  179. package/src/client/plugins-shared.ts +29 -0
  180. package/src/client/plugins-tabs.ts +87 -0
  181. package/src/client/plugins-viewers.ts +30 -0
  182. package/src/client/prefs.ts +207 -0
  183. package/src/client/produced-files.ts +91 -0
  184. package/src/client/selection-payload.ts +86 -0
  185. package/src/client/service.ts +838 -0
  186. package/src/client/settings-nav-icon.ts +45 -0
  187. package/src/client/shell-presets.ts +83 -0
  188. package/src/client/sidebar.module.css +2577 -0
  189. package/src/client/sidechat-transcript.ts +321 -0
  190. package/src/client/split-pane.tsx +322 -0
  191. package/src/client/state.ts +1175 -0
  192. package/src/client/subagent-detect.ts +137 -0
  193. package/src/client/subagent-jobs.ts +161 -0
  194. package/src/client/tab-content-memo.ts +46 -0
  195. package/src/client/terminal-font.ts +28 -0
  196. package/src/client/theme.ts +110 -0
  197. package/src/client/titlebar-strip.ts +37 -0
  198. package/src/client/upload.ts +187 -0
  199. package/src/client/wco.ts +120 -0
  200. package/src/config.ts +155 -0
  201. package/src/context-types.ts +610 -0
  202. package/src/fs-operations.ts +99 -0
  203. package/src/fs-search.ts +86 -0
  204. package/src/fs-tree.ts +158 -0
  205. package/src/git.ts +244 -0
  206. package/src/html-route.ts +106 -0
  207. package/src/index.ts +1132 -0
  208. package/src/invariant.ts +32 -0
  209. package/src/jobs-routes.ts +257 -0
  210. package/src/open-external.ts +90 -0
  211. package/src/prefs-shared.ts +268 -0
  212. package/src/pty-deps.ts +240 -0
  213. package/src/pty-manager.ts +376 -0
  214. package/src/sidechat-core.ts +468 -0
  215. package/src/sidechat-routes.ts +344 -0
  216. package/src/subagent-activity.ts +91 -0
  217. package/src/subagent-live-route.ts +93 -0
  218. package/src/tools.ts +479 -0
  219. package/src/trust-fence.ts +77 -0
  220. package/src/wire.ts +100 -0
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Lazy chunk loader for the client bundle. The heavy preview/terminal
3
+ * libraries (CodeMirror, xterm — the editor/terminal stacks, several MB)
4
+ * live in separate build-time bundles (`lib/client-<name>.js`) fetched only
5
+ * on first use of the feature that needs them, so startup downloads/parses
6
+ * only the ~1MB core bundle. (The office stack — Univer / docx-preview /
7
+ * pptx-renderer — is no longer bundled here: Office previews moved to the
8
+ * recommended office plugin, see plugins-viewers.ts.)
9
+ *
10
+ * How a chunk script works (see tsdown.config.ts chunkBundle):
11
+ *
12
+ * globalThis.__dshChunks__ = globalThis.__dshChunks__ || {};
13
+ * globalThis.__dshChunks__["terminal"] = (require) => { ...exports };
14
+ *
15
+ * The script registers its factory on a plugin-owned global registry (NOT
16
+ * through window.__ModuleLoader__.load — the module loader's import() only
17
+ * resolves seed words, shell-own modules, registered factories, and boot
18
+ * graph rows; a chunk id is none of those, so resolution would be version-
19
+ * dependent). Materialization is plugin-owned:
20
+ *
21
+ * 1. inject <script src="/sidebar/bundle/<name>.js"> (classic same-origin
22
+ * script; the official /plugins/<id>/client.js route cannot serve
23
+ * arbitrary file names, so the plugin's own host route serves the chunks),
24
+ * 2. read the factory from the global registry,
25
+ * 3. call it with a require that resolves the platform externals through
26
+ * `__DSH_MODULES__.import(spec)` — the seed-word branch, the one part of
27
+ * the module system that is stable across versions.
28
+ *
29
+ * Caching contract (three layers, each with a failure path):
30
+ * - In-memory: one in-flight promise per chunk, memoized until
31
+ * {@link resetChunks}; a failed load removes its entry so the next call
32
+ * retries from scratch. HMR re-activation keeps the resolved exports of
33
+ * unchanged chunks (ETag revalidation via
34
+ * {@link revalidateChunksOnReactivate}) — the next lazy open skips the
35
+ * re-inject / re-execute.
36
+ * - Script execution: each re-execution overwrites the global registry slot
37
+ * (assignment, never registration) — no "duplicate factory registration"
38
+ * class of errors; a failed materialization clears the cache so the retry
39
+ * re-injects and re-executes.
40
+ * - HTTP: the bundle route revalidates every request (`cache-control:
41
+ * no-cache` + ETag, 304 when unchanged), so page refreshes and HMR
42
+ * re-activations never re-download a multi-MB chunk that did not change.
43
+ *
44
+ * HMR: each plugin activation calls {@link revalidateChunksOnReactivate},
45
+ * which HEADs every loaded chunk against the bundle route and keeps the
46
+ * in-memory cache for the ones whose ETag is unchanged (a hot-reloaded core
47
+ * bundle therefore does not re-execute multi-MB chunk scripts). Chunk-only
48
+ * source edits still need a manual page refresh (the HMR poll watches only
49
+ * client.js); an edit that does land while a core HMR happens is caught by
50
+ * the ETag comparison on the next activation.
51
+ */
52
+ export type ChunkName = 'terminal' | 'editor' | 'mermaid'
53
+
54
+ /** The module exports a chunk factory provides (namespace-ish record). */
55
+ export type ChunkExports = Record<string, unknown>
56
+
57
+ /** A chunk factory: (require) => exports (the chunk's CJS closure shape). */
58
+ type ChunkFactory = (require: (spec: string) => unknown) => ChunkExports
59
+
60
+ /**
61
+ * The platform externals a chunk bundle may require (mirror of
62
+ * CLIENT_EXTERNALS in tsdown.config.ts — the chunk builds keep these
63
+ * external and the loader resolves them here). A superset is safe: the
64
+ * require only answers what the chunk actually asks for. The shell's static
65
+ * module table seeds React, Cordis, and the UI libraries (primitives/slots);
66
+ * `dsh-client-runtime/client` normalizes onto the runtime package row
67
+ * (stripClientSuffix). dsh-client-web-react / dsh-client-schema-form were
68
+ * dropped in DSH 0.1.0-rc.8 (no rc.8 publish, nothing requires them) — the
69
+ * chunks never asked for them, so they no longer belong here.
70
+ */
71
+ export const CHUNK_EXTERNALS: readonly string[] = [
72
+ 'react',
73
+ 'react/jsx-runtime',
74
+ 'react-dom',
75
+ 'react-dom/client',
76
+ 'cordis',
77
+ '@deepseek-ai/dsh-client-ui-slots',
78
+ '@deepseek-ai/dsh-client-ui-primitives',
79
+ '@deepseek-ai/dsh-client-runtime/client',
80
+ ]
81
+
82
+ /** Chunk script endpoint served by the plugin host half (src/bundle-route.ts). */
83
+ const CHUNK_URL = (name: ChunkName): string => `/sidebar/bundle/${name}.js`
84
+
85
+ /** Bound on the revalidation HEAD round-trip. A timeout fails open (drop +
86
+ * re-fetch on the next open) so a stuck bundle route can never wedge lazy
87
+ * chunk loads behind the revalidation barrier. */
88
+ const CHUNK_REVALIDATE_TIMEOUT_MS = 5_000
89
+
90
+ /**
91
+ * The client module system surface this loader needs to resolve externals.
92
+ * DSH 0.1.0-rc.8 provides it as the `ctx.modules` service (no page global
93
+ * anymore); the plugin injects it at activation via
94
+ * {@link setChunkModuleSystem}. The rc.7-era `window.__DSH_MODULES__` global
95
+ * remains as a fallback so older hosts and the test harness keep working.
96
+ */
97
+ export interface ChunkModuleSystem {
98
+ import(specifier: string): Promise<unknown>
99
+ }
100
+
101
+ /** The module system injected by the client half at activation (rc.8+). */
102
+ let injectedModuleSystem: ChunkModuleSystem | undefined
103
+
104
+ /**
105
+ * Plugin-owned page global carrying the injected module system across
106
+ * bundle copies: the lazy chunk bundles (client-editor.js etc.) inline their
107
+ * own chunk-loader instance, and rc.8 no longer exposes the shell module
108
+ * system as a page global — so the core bundle's injection must be visible
109
+ * to the chunk copies through a namespace of our own.
110
+ */
111
+ const MODULE_SYSTEM_GLOBAL = '__dshSidebarModuleSystem__'
112
+
113
+ /**
114
+ * Inject the client module system the chunk externals resolve through.
115
+ * Called by the client half's apply() with `ctx.modules` (rc.8+); pass
116
+ * undefined to clear (tests). Survives {@link resetChunks} — the module
117
+ * system is shell state, not chunk state, and stays live across HMR.
118
+ */
119
+ export function setChunkModuleSystem(system: ChunkModuleSystem | undefined): void {
120
+ injectedModuleSystem = system
121
+ const g = globalThis as Record<string, unknown>
122
+ if (system === undefined) delete g[MODULE_SYSTEM_GLOBAL]
123
+ else g[MODULE_SYSTEM_GLOBAL] = system
124
+ }
125
+
126
+ /** Resolve the shell-installed module system (injected, then the plugin
127
+ * global shared with chunk-bundle copies, then the rc.7 page global). */
128
+ function moduleSystem(): ChunkModuleSystem | undefined {
129
+ const g = globalThis as Record<string, unknown>
130
+ return injectedModuleSystem
131
+ ?? g[MODULE_SYSTEM_GLOBAL] as ChunkModuleSystem | undefined
132
+ ?? (g as { __DSH_MODULES__?: ChunkModuleSystem }).__DSH_MODULES__
133
+ }
134
+
135
+ /** The plugin-owned chunk factory registry the chunk scripts populate. */
136
+ interface ChunkRegistry {
137
+ [name: string]: ChunkFactory | undefined
138
+ }
139
+
140
+ function chunkRegistry(): ChunkRegistry {
141
+ const g = globalThis as { __dshChunks__?: ChunkRegistry }
142
+ return g.__dshChunks__ ??= {}
143
+ }
144
+
145
+ /** Script-load hook; tests replace it with a stub (the default needs a real DOM + network). */
146
+ export type ChunkScriptLoader = (src: string) => Promise<void>
147
+
148
+ const defaultScriptLoader: ChunkScriptLoader = (src) => new Promise((resolve, reject) => {
149
+ const el = document.createElement('script')
150
+ el.async = true
151
+ el.src = src
152
+ el.addEventListener('load', () => {
153
+ el.remove()
154
+ resolve()
155
+ }, { once: true })
156
+ el.addEventListener('error', () => {
157
+ el.remove()
158
+ reject(new Error(`[dsh-better-sidebar] chunk script ${src} failed to load`))
159
+ }, { once: true })
160
+ document.head.append(el)
161
+ })
162
+
163
+ let scriptLoader: ChunkScriptLoader = defaultScriptLoader
164
+
165
+ /** Test hook: replace the chunk-script loader (pass null to restore the default). */
166
+ export function setChunkScriptLoaderForTests(loader: ChunkScriptLoader | null): void {
167
+ scriptLoader = loader ?? defaultScriptLoader
168
+ }
169
+
170
+ /** Test/dev hook: resolve a chunk without fetching a script (e.g. vitest). */
171
+ const testLoaders = new Map<ChunkName, () => Promise<ChunkExports>>()
172
+ export function registerChunkForTests(name: ChunkName, loader: () => Promise<ChunkExports>): void {
173
+ testLoaders.set(name, loader)
174
+ }
175
+
176
+ /** Memoized externals require, resolved once per page from the seed table. */
177
+ let externalsRequire: ((spec: string) => unknown) | undefined
178
+
179
+ async function buildExternalsRequire(modules: ChunkModuleSystem): Promise<(spec: string) => unknown> {
180
+ if (externalsRequire !== undefined) return externalsRequire
181
+ // Per-spec tolerance: a spec the running DSH version cannot resolve (e.g.
182
+ // the runtime/client exemption row) stays unresolved until a chunk
183
+ // actually requires it — only then it is a loud error.
184
+ const entries = await Promise.all(CHUNK_EXTERNALS.map(async (spec) => {
185
+ try {
186
+ return [spec, await modules.import(spec)] as const
187
+ } catch {
188
+ return [spec, undefined] as const
189
+ }
190
+ }))
191
+ const table = new Map<string, unknown>(entries)
192
+ externalsRequire = (spec: string): unknown => {
193
+ if (!table.has(spec)) {
194
+ // Single quotes: the bundle-consistency scan regexes for require("...")
195
+ // lexical calls — a double-quoted literal here would trip it.
196
+ throw new Error(`[dsh-better-sidebar] chunk require('${spec}') missed the module table`)
197
+ }
198
+ return table.get(spec)
199
+ }
200
+ return externalsRequire
201
+ }
202
+
203
+ /** In-flight/memoized chunk loads; a failure removes its entry so a retry re-fetches. */
204
+ const cache = new Map<ChunkName, Promise<ChunkExports>>()
205
+
206
+ /** Chunk names whose exports are currently cached (loaded successfully). */
207
+ const loadedChunks = new Set<ChunkName>()
208
+
209
+ /** ETags observed for loaded chunks (HEAD revalidation, see
210
+ * {@link revalidateChunksOnReactivate}). */
211
+ const chunkEtags = new Map<ChunkName, string>()
212
+
213
+ /** Pending revalidation barrier: while set, {@link loadChunk} awaits it
214
+ * before serving cache (see revalidateChunksOnReactivate). */
215
+ let revalidation: Promise<void> | null = null
216
+
217
+ /** Best-effort ETag capture for revalidation. The script tag itself exposes
218
+ * no response headers, so after a successful load we HEAD the bundle route
219
+ * once. Failures (including a stuck route — bounded by the timeout) are
220
+ * ignored — revalidation then fails open (re-fetch). */
221
+ async function recordEtag(name: ChunkName): Promise<void> {
222
+ try {
223
+ const res = await fetch(CHUNK_URL(name), {
224
+ method: 'HEAD',
225
+ cache: 'no-cache',
226
+ signal: AbortSignal.timeout(CHUNK_REVALIDATE_TIMEOUT_MS),
227
+ })
228
+ const etag = res.headers.get('etag')
229
+ if (etag !== null && etag !== '') chunkEtags.set(name, etag)
230
+ } catch {
231
+ chunkEtags.delete(name)
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Load (once) and materialize a lazy chunk, returning its module exports.
237
+ * Concurrent callers share one in-flight load; a failure clears the cache
238
+ * entry so the next call retries (the script re-executes and overwrites its
239
+ * global registry slot — assignments are idempotent).
240
+ * @param name - the chunk to load.
241
+ */
242
+ export async function loadChunk(name: ChunkName): Promise<ChunkExports> {
243
+ // Barrier: never serve a cache entry that a pending revalidation is about
244
+ // to inspect — a stale chunk could otherwise render mid-HMR (CR #232 P1).
245
+ if (revalidation !== null) await revalidation
246
+ const cached = cache.get(name)
247
+ if (cached !== undefined) return cached
248
+ let task: Promise<ChunkExports>
249
+ task = (async (): Promise<ChunkExports> => {
250
+ const test = testLoaders.get(name)
251
+ if (test !== undefined) return test()
252
+ const modules = moduleSystem()
253
+ if (modules === undefined) {
254
+ throw new Error(`[dsh-better-sidebar] chunk "${name}": client module system unavailable`)
255
+ }
256
+ await scriptLoader(CHUNK_URL(name))
257
+ const factory = chunkRegistry()[name]
258
+ if (typeof factory !== 'function') {
259
+ throw new Error(`[dsh-better-sidebar] chunk "${name}" script did not register its factory`)
260
+ }
261
+ const require = await buildExternalsRequire(modules)
262
+ const exports = factory(require)
263
+ // Only track production loads whose cache entry survived (a revalidation
264
+ // sweep may have dropped it mid-flight; the caller still gets these
265
+ // exports, they just are not memoized for the next open). Test-registry
266
+ // loads return above and never reach this tracking.
267
+ if (cache.get(name) !== undefined) {
268
+ loadedChunks.add(name)
269
+ void recordEtag(name)
270
+ }
271
+ return exports
272
+ })()
273
+ cache.set(name, task)
274
+ void task.catch(() => {
275
+ cache.delete(name)
276
+ loadedChunks.delete(name)
277
+ chunkEtags.delete(name)
278
+ })
279
+ return task
280
+ }
281
+
282
+ /**
283
+ * Drop all chunk state for a fresh plugin activation (HMR-safe): clear the
284
+ * in-memory cache and any test-registry entries, so the next lazy open
285
+ * re-fetches and re-executes the current chunk scripts (the registry slots
286
+ * are overwritten by the re-execution — no cleanup needed). A pending
287
+ * revalidation barrier is cleared too: it was only guarding the cache reads
288
+ * of the state being dropped, so the next load must not wait on it (the
289
+ * orphaned task still settles and its identity-guarded `finally` no-ops).
290
+ */
291
+ export function resetChunks(): void {
292
+ cache.clear()
293
+ loadedChunks.clear()
294
+ chunkEtags.clear()
295
+ testLoaders.clear()
296
+ externalsRequire = undefined
297
+ revalidation = null
298
+ }
299
+
300
+ /**
301
+ * HMR-safe re-activation hook (index.tsx calls this instead of a full
302
+ * reset): keep the resolved exports of every loaded chunk and drop only the
303
+ * ones whose script changed on disk — the bundle route revalidates every
304
+ * request (cache-control: no-cache + ETag), so an unchanged chunk keeps its
305
+ * memory cache and the next lazy open skips the re-inject / re-execute.
306
+ * Fail-open: an unreachable, ETag-less, or timed-out chunk is dropped
307
+ * (re-fetch on next open). Test-registry entries are always cleared
308
+ * (per-test fixtures).
309
+ * A page refresh remains the authoritative reset (the HMR poll watches only
310
+ * client.js; chunk-only edits surface here on the next core re-activation).
311
+ *
312
+ * The returned promise is also a BARRIER for {@link loadChunk}: while a
313
+ * revalidation is pending, every chunk load awaits it before serving cache,
314
+ * so a lazy tab opening mid-revalidation can never render stale exports
315
+ * that the sweep is about to invalidate (CR #232 P1).
316
+ */
317
+ export function revalidateChunksOnReactivate(): Promise<void> {
318
+ testLoaders.clear()
319
+ const task = (async (): Promise<void> => {
320
+ // Entries not tracked as production-loaded (test fixtures, orphans) never
321
+ // survive a re-activation — their resolved exports came from per-test
322
+ // stubs, not from the bundle route.
323
+ for (const name of [...cache.keys()]) {
324
+ if (!loadedChunks.has(name)) cache.delete(name)
325
+ }
326
+ if (loadedChunks.size === 0) return
327
+ const stale: ChunkName[] = []
328
+ await Promise.all([...loadedChunks].map(async (name) => {
329
+ try {
330
+ const res = await fetch(CHUNK_URL(name), {
331
+ method: 'HEAD',
332
+ cache: 'no-cache',
333
+ signal: AbortSignal.timeout(CHUNK_REVALIDATE_TIMEOUT_MS),
334
+ })
335
+ const etag = res.headers.get('etag')
336
+ if (etag !== null && etag !== '' && chunkEtags.get(name) === etag) return
337
+ } catch {
338
+ // Fail open below (network errors and timeouts alike).
339
+ }
340
+ stale.push(name)
341
+ }))
342
+ for (const name of stale) {
343
+ cache.delete(name)
344
+ loadedChunks.delete(name)
345
+ chunkEtags.delete(name)
346
+ }
347
+ })()
348
+ revalidation = task
349
+ void task.finally(() => { if (revalidation === task) revalidation = null })
350
+ return task
351
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Lazy chunk entry: the code/markdown/html text editor (CodeMirror 6 + the
3
+ * language packages). Built as `lib/client-editor.js` and registered under
4
+ * `dsh-better-sidebar/editor` — fetched only when a text file is first
5
+ * opened (see chunk-loader.ts and docs/plans/2026-08-12-lazy-chunks-design.md).
6
+ * Never import this module from the core bundle: it pulls CodeMirror into
7
+ * the startup path.
8
+ */
9
+ export { TextEditor } from '../TextEditor.tsx'
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Lazy chunk entry: the mermaid diagram renderer for the markdown preview
3
+ * (mermaid + its d3/dagre/cytoscape graph deps). Built as
4
+ * lib/client-mermaid.js and registered under the `mermaid` global chunk
5
+ * slot — fetched only when a previewed markdown file contains a mermaid
6
+ * fence (see chunk-loader.ts and the mermaid markdown preview design doc).
7
+ * Never import this module from the core bundle: it pulls mermaid into the
8
+ * startup path.
9
+ */
10
+ export { MermaidMarkdown } from '../mermaid.tsx'
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Lazy chunk entry: the interactive terminal (xterm + fit addon). Built as
3
+ * `lib/client-terminal.js` and registered under
4
+ * `dsh-better-sidebar/terminal` — fetched only when a terminal tab is first
5
+ * opened (see chunk-loader.ts and docs/plans/2026-08-12-lazy-chunks-design.md).
6
+ * Never import this module from the core bundle: it pulls xterm (and its
7
+ * stylesheet) into the startup path.
8
+ */
9
+ export { TerminalView } from '../TerminalView.tsx'
@@ -0,0 +1,129 @@
1
+ /**
2
+ * CodeMirror 6 theme pieces for the sidebar editor. The editor surface
3
+ * (background, caret, gutter) rides the DSH theme tokens so it blends with
4
+ * the panel in both schemes; only the syntax token colors need concrete
5
+ * values, and those come from the same designed palettes the app's code
6
+ * surfaces use — the one-dark family for dark, the one-light family for
7
+ * light. The scheme flip reconfigures these via a compartment (see
8
+ * TextEditor), so the document, undo history and scroll survive re-theming.
9
+ */
10
+ import { Compartment } from '@codemirror/state'
11
+ import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'
12
+ import { tags, type Tag } from '@lezer/highlight'
13
+ import { EditorView } from '@codemirror/view'
14
+
15
+ /** Token-driven surface shared by both schemes (pure CSS values). */
16
+ export const cmSurfaceTheme = EditorView.theme({
17
+ '&': {
18
+ height: '100%',
19
+ fontSize: '13px',
20
+ backgroundColor: 'transparent',
21
+ color: 'var(--dsw-alias-label-primary)',
22
+ },
23
+ '.cm-scroller': {
24
+ overflow: 'auto',
25
+ fontFamily: 'var(--ds-font-family-code)',
26
+ },
27
+ '.cm-content': {
28
+ caretColor: 'var(--dsw-alias-label-primary)',
29
+ },
30
+ '.cm-gutters': {
31
+ backgroundColor: 'transparent',
32
+ color: 'var(--dsw-alias-label-tertiary)',
33
+ border: 'none',
34
+ },
35
+ })
36
+
37
+ /** Scheme-specific surface tints (selection, active line). */
38
+ function cmSurfaceTint(dark: boolean): ReturnType<typeof EditorView.theme> {
39
+ return EditorView.theme({
40
+ '.cm-selectionBackground, .cm-focused .cm-selectionBackground, ::selection': {
41
+ backgroundColor: dark ? 'rgba(255,255,255,0.22)' : 'rgba(0,0,0,0.12)',
42
+ },
43
+ '.cm-activeLine, .cm-activeLineGutter': {
44
+ backgroundColor: dark ? 'rgba(255,255,255,0.04)' : 'rgba(0,0,0,0.03)',
45
+ },
46
+ })
47
+ }
48
+
49
+ /** One syntax rule: a tag (or tag set) mapped to a concrete color/style. */
50
+ interface HighlightRule {
51
+ tag: Tag | readonly Tag[]
52
+ color?: string
53
+ fontStyle?: string
54
+ }
55
+
56
+ /** one-dark syntax palette (mirrors @codemirror/theme-one-dark). */
57
+ const HIGHLIGHTS_DARK: HighlightRule[] = [
58
+ { tag: tags.comment, color: '#5c6370', fontStyle: 'italic' },
59
+ { tag: tags.keyword, color: '#c678dd' },
60
+ { tag: tags.string, color: '#98c379' },
61
+ { tag: tags.number, color: '#d19a66' },
62
+ { tag: tags.bool, color: '#d19a66' },
63
+ { tag: tags.atom, color: '#d19a66' },
64
+ { tag: tags.typeName, color: '#e5c07b' },
65
+ { tag: tags.className, color: '#e5c07b' },
66
+ { tag: tags.propertyName, color: '#e06c75' },
67
+ { tag: tags.function(tags.variableName), color: '#61afef' },
68
+ { tag: tags.variableName, color: '#e06c75' },
69
+ { tag: tags.operator, color: '#56b6c2' },
70
+ { tag: tags.tagName, color: '#e06c75' },
71
+ { tag: tags.attributeName, color: '#d19a66' },
72
+ { tag: tags.heading, color: '#e06c75', fontStyle: 'bold' },
73
+ { tag: tags.emphasis, fontStyle: 'italic' },
74
+ { tag: tags.strong, fontStyle: 'bold' },
75
+ { tag: tags.link, color: '#61afef', fontStyle: 'underline' },
76
+ { tag: tags.meta, color: '#e5c07b' },
77
+ { tag: tags.invalid, color: '#ffffff', fontStyle: 'bold' },
78
+ ]
79
+
80
+ /** one-light syntax palette (the light counterpart of one-dark). */
81
+ const HIGHLIGHTS_LIGHT: HighlightRule[] = [
82
+ { tag: tags.comment, color: '#a0a1a7', fontStyle: 'italic' },
83
+ { tag: tags.keyword, color: '#a626a4' },
84
+ { tag: tags.string, color: '#50a14f' },
85
+ { tag: tags.number, color: '#986801' },
86
+ { tag: tags.bool, color: '#0184bc' },
87
+ { tag: tags.atom, color: '#0184bc' },
88
+ { tag: tags.typeName, color: '#c18401' },
89
+ { tag: tags.className, color: '#c18401' },
90
+ { tag: tags.propertyName, color: '#e45649' },
91
+ { tag: tags.function(tags.variableName), color: '#c18401' },
92
+ { tag: tags.variableName, color: '#e45649' },
93
+ { tag: tags.operator, color: '#383a42' },
94
+ { tag: tags.tagName, color: '#e45649' },
95
+ { tag: tags.attributeName, color: '#986801' },
96
+ { tag: tags.heading, color: '#e45649', fontStyle: 'bold' },
97
+ { tag: tags.emphasis, fontStyle: 'italic' },
98
+ { tag: tags.strong, fontStyle: 'bold' },
99
+ { tag: tags.link, color: '#4078f2', fontStyle: 'underline' },
100
+ { tag: tags.meta, color: '#c18401' },
101
+ { tag: tags.invalid, color: '#ffffff', fontStyle: 'bold' },
102
+ ]
103
+
104
+ /** The scheme-dependent extension pair (surface tint + syntax highlight). */
105
+ function cmThemeExtensions(dark: boolean): Array<ReturnType<typeof EditorView.theme>> {
106
+ return [
107
+ cmSurfaceTint(dark),
108
+ syntaxHighlighting(HighlightStyle.define(dark ? HIGHLIGHTS_DARK : HIGHLIGHTS_LIGHT)),
109
+ ]
110
+ }
111
+
112
+ /**
113
+ * A Compartment holding the two scheme-dependent extensions. Created once
114
+ * per editor view; a scheme flip dispatches `reconfigure(dark)` on it, so
115
+ * the document, undo history, scroll and keymaps survive re-theming.
116
+ */
117
+ export class CmThemeCompartment {
118
+ private readonly compartment = new Compartment()
119
+
120
+ /** `of(...)` payload for EditorState.create. */
121
+ of(dark: boolean): ReturnType<Compartment['of']> {
122
+ return this.compartment.of(cmThemeExtensions(dark))
123
+ }
124
+
125
+ /** Reconfigure for a new scheme. */
126
+ reconfigure(dark: boolean): ReturnType<Compartment['reconfigure']> {
127
+ return this.compartment.reconfigure(cmThemeExtensions(dark))
128
+ }
129
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Append text to the current session's composer draft through the
3
+ * conversation service — the shared path behind the explorer's @-reference
4
+ * button and the viewer selection popup. The service is resolved lazily
5
+ * through `ctx.get` (the inject-free read the app's own plugins use); a
6
+ * missing service or scope degrades to a logged no-op, never a crash.
7
+ */
8
+ import type { Context, SidebarConversation } from '../context-types.ts'
9
+
10
+ /**
11
+ * Append `text` to the session's composer draft (space-separated, like the
12
+ * @-mentions). Returns false — and logs — when the conversation service or
13
+ * the session scope is unavailable.
14
+ */
15
+ export function appendToDraft(ctx: Context, sessionId: string, text: string): boolean {
16
+ try {
17
+ const actx = ctx.sessions.scope(sessionId)
18
+ if (actx === undefined) return false
19
+ const conversation = ctx.get('conversation') as SidebarConversation | undefined
20
+ if (conversation === undefined) return false
21
+ const input = conversation.input.for(actx)
22
+ const draft = input.state.getSnapshot().draft
23
+ input.setDraft(draft.trim() === '' ? text : `${draft} ${text}`)
24
+ return true
25
+ } catch (error) {
26
+ console.warn('[dsh-better-sidebar] draft insert failed:', error)
27
+ return false
28
+ }
29
+ }
@@ -0,0 +1,5 @@
1
+ /** CSS Modules class-map declaration (tsdown compiles .module.css at build). */
2
+ declare module '*.module.css' {
3
+ const classes: Record<string, string>
4
+ export default classes
5
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Desktop-shell detection for the sidebar. Shells may stamp the render URL
3
+ * with `dsh-desktop-mode` / `dsh-desktop-platform` (the official Electron
4
+ * shell does) and expose `window.__DSH_DESKTOP_FILE_PATH__` through a
5
+ * preload. Parsed once per page and memoized (the URL never changes
6
+ * mid-session); `resetDesktopEnvForTests` clears the memo for unit tests.
7
+ *
8
+ * GEOMETRY POLICY: this module only REPORTS shell facts — it never decides
9
+ * how to adapt. The strip height comes from standard signals first (the
10
+ * Window Controls Overlay API, see wco.ts), then the documented contract
11
+ * parameter `dsh-desktop-titlebar-inset` (a shell may stamp the real pixels
12
+ * it reserves at the top), then the user's chosen scheme (preset / custom).
13
+ * The legacy win32-advanced 32px constant is gone from the core: it lives
14
+ * in the opt-in shell preset (shell-presets.ts) as a fallback for shells
15
+ * without the WCO API.
16
+ */
17
+ export interface DesktopEnv {
18
+ /** Running inside a desktop shell (any URL stamp or preload marker). */
19
+ readonly desktop: boolean
20
+ /** `advanced` = frameless/custom-titlebar shell; `compatibility` = native frame. */
21
+ readonly mode: 'compatibility' | 'advanced' | null
22
+ /** Shell platform stamp ('darwin' | 'win32' | …), lowercased, or null. */
23
+ readonly platform: string | null
24
+ /**
25
+ * Contract parameter `dsh-desktop-titlebar-inset`: pixels the shell
26
+ * reserves at the top of the web content for its own chrome (0–120,
27
+ * clamped; 0 when absent). Standard WCO geometry takes precedence over
28
+ * this whenever the API is available.
29
+ */
30
+ readonly titlebarInset: number
31
+ }
32
+
33
+ let cached: DesktopEnv | undefined
34
+
35
+ /** Read the shell's desktop stamps (memoized per page; SSR-safe). */
36
+ export function parseDesktopEnv(): DesktopEnv {
37
+ if (cached !== undefined) return cached
38
+ const hasWindow = typeof window !== 'undefined'
39
+ const hasPreloadMarker = hasWindow
40
+ && typeof (window as { __DSH_DESKTOP_FILE_PATH__?: unknown }).__DSH_DESKTOP_FILE_PATH__ !== 'undefined'
41
+ // location.search includes the leading '?', which URLSearchParams does NOT
42
+ // strip (it would become part of the first key) — drop it explicitly.
43
+ // SSR (no window) resolves the empty environment: nothing desktop.
44
+ const params = hasWindow
45
+ ? new URLSearchParams(window.location.search.replace(/^\?/, ''))
46
+ : new URLSearchParams()
47
+ const modeParam = params.get('dsh-desktop-mode')
48
+ const mode = modeParam === 'compatibility' || modeParam === 'advanced' ? modeParam : null
49
+ const platformParam = params.get('dsh-desktop-platform')
50
+ const platform = platformParam !== null && platformParam !== '' ? platformParam.toLowerCase() : null
51
+ const desktop = mode !== null || hasPreloadMarker
52
+ cached = {
53
+ desktop,
54
+ mode,
55
+ platform,
56
+ titlebarInset: parseTitlebarInset(params.get('dsh-desktop-titlebar-inset')),
57
+ }
58
+ return cached
59
+ }
60
+
61
+ /** Clamp the contract inset parameter into 0–120 (invalid/absent → 0). */
62
+ function parseTitlebarInset(raw: string | null): number {
63
+ if (raw === null) return 0
64
+ const parsed = Number(raw)
65
+ if (!Number.isFinite(parsed)) return 0
66
+ return Math.min(120, Math.max(0, Math.round(parsed)))
67
+ }
68
+
69
+ /** Test hook: drop the memo so the next parse re-reads the URL/globals. */
70
+ export function resetDesktopEnvForTests(): void {
71
+ cached = undefined
72
+ }