@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,369 @@
1
+ /**
2
+ * Client half of dsh-better-sidebar: resolves the user's "Side card"
3
+ * preferences through the plugin's own fenced settings route, mounts the
4
+ * right sidebar portal (inside an error boundary so a rendering failure
5
+ * shows an error strip instead of a blank panel), registers the turn-tail
6
+ * interception, and contributes the Side card settings section to the DSH
7
+ * Settings shell. Requires the runtime's slots and sessions services; the
8
+ * bundle itself is a module-table consumer only (react + ui-primitives +
9
+ * xterm, all provided or inlined).
10
+ */
11
+ import { createElement } from 'react'
12
+ import { createRoot, type Root } from 'react-dom/client'
13
+ import type { Context } from '../context-types.ts'
14
+ import { allLeaves, createSidebarStore, isAgentTabId } from './state.ts'
15
+ import { createBetterSidebarService, matchUrlTarget } from './service.ts'
16
+ import { revalidateChunksOnReactivate, setChunkModuleSystem } from './chunk-loader.ts'
17
+ import { registerBuiltins } from './builtins/index.ts'
18
+ import { Sidebar } from './Sidebar.tsx'
19
+ import { RenderBoundary } from './RenderBoundary.tsx'
20
+ import { registerOpenPathInterception, registerTurnTailInterception } from './intercept.tsx'
21
+ import { registerLinkInterception } from './link-intercept.ts'
22
+ import { registerImeGuard } from './ime-guard.ts'
23
+ import { registerSettingsNavIcon } from './settings-nav-icon.ts'
24
+ import { loadExternalDisable, loadPrefs } from './prefs.ts'
25
+ import { SideCardSection } from './SideCardSection.tsx'
26
+ import { api } from './api.ts'
27
+ import { LOCALE_NS, attachLocale, t, zh, en } from './locales.ts'
28
+ import css from './sidebar.module.css'
29
+ import './layout.css'
30
+
31
+ /** Services required before mounting (provided by the client runtime; the
32
+ * locale service backs the sidebar's copy — see locales.ts). `modules`
33
+ * (rc.8+) is the client module system the chunk loader resolves its
34
+ * externals through — Cordis guards service access without inject. */
35
+ export const inject = ['slots', 'sessions', 'connection', 'workspaces', 'locale', 'modules']
36
+
37
+ /**
38
+ * Error boundary over the sidebar tree (root scope): a render error in the
39
+ * sidebar SHELL itself must never blank the page silently — the shared
40
+ * RenderBoundary shows a dismissible error strip and logs the stack. The
41
+ * per-tab scope (Sidebar.tsx) catches viewer/editor crashes first; this root
42
+ * boundary stays as the last resort for Workbench/shell errors.
43
+ */
44
+ /**
45
+ * Client plugin body.
46
+ * @param ctx - the client cordis context (slots, sessions).
47
+ */
48
+ export function apply(ctx: Context): void {
49
+ // The sidebar follows the DSH i18n system: attach the locale service so
50
+ // the module-level t()/isZh() resolve the Host-backed language preference
51
+ // (and switch live — the Sidebar root subscribes to it), and register the
52
+ // plugin's dictionaries into the shared locale registry. The disposers
53
+ // run on fiber disposal, so re-activation (HMR) re-registers cleanly.
54
+ attachLocale(ctx.locale)
55
+ ctx.effect(() => {
56
+ const offZh = ctx.locale.register(LOCALE_NS, 'zh', zh)
57
+ const offEn = ctx.locale.register(LOCALE_NS, 'en', en)
58
+ return () => { offZh(); offEn() }
59
+ }, 'dsh-better-sidebar: dictionaries')
60
+ // One store instance per activation: production code creates it only here,
61
+ // then hands it to the mounted panel and closes over it in the slot
62
+ // registrations (the official createXXXStore() factory rule — no
63
+ // module-level singleton).
64
+ const sidebarStore = createSidebarStore()
65
+ // The sidebar registry service: external plugins register tab types and
66
+ // file previewers through `ctx.betterSidebar.registerTab/registerFileViewer`.
67
+ // Published before the panel mounts so consumers injecting 'betterSidebar'
68
+ // are ready by the time the sidebar renders.
69
+ const service = createBetterSidebarService(sidebarStore)
70
+ ctx.provide('betterSidebar', service)
71
+ // Terminal tab titles use the host's effective shell name (e.g. bash/zsh)
72
+ // instead of "Terminal 1". Start with a safe fallback and replace it as
73
+ // soon as the host shell info resolves. Tabs created before the response
74
+ // arrives keep the fallback title, so also retitle any already-open UI
75
+ // terminal tabs that still carry it.
76
+ const fallbackTitle = t('terminal')
77
+ let terminalTitle = fallbackTitle
78
+ void api.shellGet().then(({ name }) => {
79
+ terminalTitle = name
80
+ const snapshot = service.getSnapshot()
81
+ if (snapshot.state === undefined) return
82
+ const tabs = allLeaves(snapshot.state.splits)
83
+ .concat(allLeaves(snapshot.state.bottomSplits))
84
+ .flatMap(leaf => leaf.tabs)
85
+ for (const tab of tabs) {
86
+ if (tab.type === 'terminal' && !isAgentTabId(tab.id) && tab.title === fallbackTitle) {
87
+ service.updateTab(tab.id, { title: name })
88
+ }
89
+ }
90
+ }).catch(() => { /* keep fallback */ })
91
+ // Register the plugin's own built-in tabs and viewers through the same
92
+ // service (eating our own dogfood). The disposer unregisters them on
93
+ // fiber disposal (HMR-safe).
94
+ ctx.effect(
95
+ () => registerBuiltins(ctx, service, { terminalTitle: () => terminalTitle }),
96
+ 'dsh-better-sidebar: register built-in tabs and viewers',
97
+ )
98
+ // A failure anywhere in the client lifecycle must never take the app down
99
+ // silently: log with the plugin prefix and pin a visible diagnostic strip
100
+ // to the page so a blank panel is never the only symptom.
101
+ const fail = (phase: string, error: unknown): void => {
102
+ console.error(`[dsh-better-sidebar] ${phase} error:`, error)
103
+ try {
104
+ const bar = document.createElement('div')
105
+ bar.style.cssText = 'position:fixed;left:8px;bottom:8px;z-index:2147483000;max-width:70vw;padding:8px 12px;'
106
+ + 'font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;color:#f2a1a1;background:#1b1b22;'
107
+ + 'border:1px solid #f2a1a1;border-radius:8px;white-space:pre-wrap'
108
+ bar.textContent = `[dsh-better-sidebar] ${phase} error: ${error instanceof Error ? error.message : String(error)}`
109
+ document.body.appendChild(bar)
110
+ } catch {
111
+ // Nothing left to report with.
112
+ }
113
+ }
114
+ try {
115
+ // rc.8+ exposes the client module system as the `ctx.modules` service
116
+ // (no window.__DSH_MODULES__ page global anymore); the chunk loader needs
117
+ // it to resolve its externals, so inject it before anything can load a
118
+ // lazy chunk. The loader falls back to the rc.7 global when absent.
119
+ setChunkModuleSystem(ctx.modules)
120
+ // Fresh chunk state for this activation: drop per-test fixtures and
121
+ // revalidate loaded chunk scripts against the bundle route's ETags —
122
+ // unchanged chunks keep their resolved exports (no re-inject /
123
+ // re-execute on HMR), changed ones are dropped for a clean re-fetch.
124
+ void revalidateChunksOnReactivate()
125
+ ctx.effect(() => {
126
+ let disposed = false
127
+ let root: Root | undefined
128
+ let host: HTMLDivElement | undefined
129
+ let mounted = false
130
+ let bodyObserver: MutationObserver | undefined
131
+ let hostCheckFrame: number | null = null
132
+ const unmount = (): void => {
133
+ if (!mounted) return
134
+ mounted = false
135
+ bodyObserver?.disconnect()
136
+ bodyObserver = undefined
137
+ if (hostCheckFrame !== null) {
138
+ cancelAnimationFrame(hostCheckFrame)
139
+ hostCheckFrame = null
140
+ }
141
+ root?.unmount()
142
+ root = undefined
143
+ host?.remove()
144
+ host = undefined
145
+ }
146
+ /** Re-attach the host if the page (a desktop shell wrapper, SPA
147
+ * navigation, …) ever removes it from <body>. Cheap: childList only,
148
+ * no subtree, no attribute filtering. */
149
+ const guardAnchor = (): void => {
150
+ if (bodyObserver !== undefined) return
151
+ bodyObserver = new MutationObserver(() => {
152
+ if (host !== undefined && !document.body.contains(host)) {
153
+ document.body.appendChild(host)
154
+ }
155
+ })
156
+ bodyObserver.observe(document.body, { childList: true })
157
+ }
158
+ /** One-shot geometry self-check: if the host page transforms
159
+ * <html>/<body> itself (exotic shells), a fixed panel host would
160
+ * track the transformed box instead of the viewport. Flip the
161
+ * degraded mode and pin the host to the viewport every frame until
162
+ * the ancestor transform is actually gone. The normal path (no
163
+ * page-level transform) never runs the sync loop. */
164
+ const scheduleHostCheck = (): void => {
165
+ hostCheckFrame ??= requestAnimationFrame(() => {
166
+ hostCheckFrame = null
167
+ const layer = host?.querySelector<HTMLElement>('[data-dsh-panel-host]')
168
+ if (layer === null || layer === undefined) return
169
+ const rect = layer.getBoundingClientRect()
170
+ const mismatched = Math.abs(rect.left) > 8 || Math.abs(rect.top) > 8
171
+ || Math.abs(rect.width - window.innerWidth) > 8 || Math.abs(rect.height - window.innerHeight) > 8
172
+ if (!mismatched) {
173
+ layer.removeAttribute('data-dsh-panel-host-degraded')
174
+ layer.style.transform = ''
175
+ return
176
+ }
177
+ layer.setAttribute('data-dsh-panel-host-degraded', '')
178
+ console.warn('[dsh-better-sidebar] panel host geometry mismatch — a page-level transform was detected; using degraded viewport sync')
179
+ // Track our own compensating translation so the loop judges the
180
+ // UNCORRECTED geometry: clearing degraded mode must wait for the
181
+ // ancestor transform to actually disappear — the frame right after
182
+ // our correction applies would otherwise look "fixed" and the
183
+ // offset would return immediately (CR #232 P1).
184
+ let applied = { x: 0, y: 0 }
185
+ const sync = (): void => {
186
+ const r = layer.getBoundingClientRect()
187
+ const rawLeft = r.left - applied.x
188
+ const rawTop = r.top - applied.y
189
+ if (Math.abs(rawLeft) <= 1 && Math.abs(rawTop) <= 1
190
+ && Math.abs(r.width - window.innerWidth) <= 1 && Math.abs(r.height - window.innerHeight) <= 1) {
191
+ layer.removeAttribute('data-dsh-panel-host-degraded')
192
+ layer.style.transform = ''
193
+ return
194
+ }
195
+ const next = { x: -rawLeft, y: -rawTop }
196
+ if (next.x !== applied.x || next.y !== applied.y) {
197
+ applied = next
198
+ layer.style.transform = `translate(${applied.x}px, ${applied.y}px)`
199
+ }
200
+ hostCheckFrame = requestAnimationFrame(sync)
201
+ }
202
+ hostCheckFrame = requestAnimationFrame(sync)
203
+ })
204
+ }
205
+ const mount = (): void => {
206
+ if (mounted || disposed) return
207
+ try {
208
+ host = document.createElement('div')
209
+ host.setAttribute('data-dsh-better-sidebar', '')
210
+ document.body.appendChild(host)
211
+ root = createRoot(host)
212
+ root.render(createElement(RenderBoundary, { className: css.boundaryError }, createElement(Sidebar, { ctx, store: sidebarStore })))
213
+ mounted = true
214
+ guardAnchor()
215
+ scheduleHostCheck()
216
+ } catch (error) {
217
+ fail('mount', error)
218
+ }
219
+ }
220
+ const sync = async (): Promise<void> => {
221
+ if (disposed) return
222
+ // Resolve the user's side card prefs BEFORE the first session seeds,
223
+ // so a brand-new conversation opens (or stays closed) at the chosen
224
+ // width from first paint. A settings route failure falls back to the
225
+ // schema defaults; the sidebar still mounts (a stalled wire gives up
226
+ // after the timeout and mounts on the defaults).
227
+ const prefs = await Promise.race([
228
+ loadPrefs(api),
229
+ new Promise<null>(resolve => { const timer = window.setTimeout(() => resolve(null), 2000) }),
230
+ ])
231
+ if (prefs !== null) sidebarStore.setPrefs(prefs)
232
+ if (disposed) return
233
+ // Mutual exclusion with the dsh-web-ui family right panel: while the
234
+ // aionui-panel provider is selected, the sidebar must not mount at
235
+ // all. Re-evaluated on every settings-document update (live switch).
236
+ const suspended = await loadExternalDisable(api)
237
+ if (disposed) return
238
+ sidebarStore.setSuspended(suspended)
239
+ if (suspended) unmount()
240
+ else mount()
241
+ }
242
+ void sync()
243
+ // Live re-evaluation: the runtime broadcasts settings-document updates
244
+ // (the aionui card saves through the same document). Best effort —
245
+ // deployments without the 'remote' service fall back to boot-time
246
+ // evaluation only.
247
+ const remote = ctx.get('remote') as { $on?: (event: string, listener: () => void) => () => void } | undefined
248
+ const offRemote = remote?.$on?.('settings/document-updated', () => { void sync() })
249
+ return () => {
250
+ disposed = true
251
+ offRemote?.()
252
+ unmount()
253
+ }
254
+ }, 'dsh-better-sidebar: sidebar mount')
255
+
256
+ ctx.effect(
257
+ () => {
258
+ try {
259
+ return registerTurnTailInterception(ctx, sidebarStore)
260
+ } catch (error) {
261
+ fail('interception', error)
262
+ return undefined
263
+ }
264
+ },
265
+ 'dsh-better-sidebar: turn-tail interception',
266
+ )
267
+
268
+ ctx.effect(
269
+ () => {
270
+ try {
271
+ return registerOpenPathInterception(ctx, sidebarStore)
272
+ } catch (error) {
273
+ fail('interception', error)
274
+ return undefined
275
+ }
276
+ },
277
+ 'dsh-better-sidebar: open-path interception',
278
+ )
279
+
280
+ ctx.effect(
281
+ () => {
282
+ try {
283
+ // External http(s) links in the chat/GUI open the sidebar instead
284
+ // of a new window. Gated on the browserInterceptLinks MASTER pref,
285
+ // the URL's protocol flag (browserInterceptHttp / Https — https
286
+ // defaults OFF: most https sites refuse iframe embedding), and the
287
+ // target tab's enable switch; Ctrl/Cmd+click always bypasses. The
288
+ // target is the first registered tab whose `urlTarget` claims the
289
+ // URL (enabled tabs only), else the built-in browser tab.
290
+ const urlTargetOf = (url: URL): string | undefined => {
291
+ const prefs = sidebarStore.getPrefs()
292
+ const enabled = service.getTabs().filter(tab => prefs.tabsEnabled[tab.id] !== false)
293
+ return matchUrlTarget(enabled, url)?.id
294
+ }
295
+ return registerLinkInterception({
296
+ takeoverEnabled: (url) => {
297
+ if (sidebarStore.getSuspended()) return false
298
+ const prefs = sidebarStore.getPrefs()
299
+ if (prefs.browserInterceptLinks === false) return false
300
+ const protocolOn = url.protocol === 'https:'
301
+ ? prefs.browserInterceptHttps !== false
302
+ : prefs.browserInterceptHttp !== false
303
+ if (!protocolOn) return false
304
+ // A plugin claim is the target (already enabled-filtered);
305
+ // otherwise the built-in browser must be enabled.
306
+ return urlTargetOf(url) !== undefined || prefs.tabsEnabled['browser'] !== false
307
+ },
308
+ openInSidebar: (url) => {
309
+ let title: string | undefined
310
+ try { title = new URL(url).hostname } catch { /* keep the default title */ }
311
+ const type = urlTargetOf(new URL(url)) ?? 'browser'
312
+ ctx.betterSidebar?.openTab({ type, url, title })
313
+ },
314
+ selfOrigin: window.location.origin,
315
+ })
316
+ } catch (error) {
317
+ fail('interception', error)
318
+ return undefined
319
+ }
320
+ },
321
+ 'dsh-better-sidebar: link interception',
322
+ )
323
+
324
+ // The IME guard: composition keys (candidate arrows, confirm, cancel)
325
+ // belong to the input method, never to page JS. Inlined third-party UI
326
+ // (formerly Univer's office controls, #562 regression) has shipped
327
+ // unguarded keydown handlers that hijack ArrowUp/ArrowDown and break
328
+ // Chinese input; the document-capture guard neutralizes the whole class
329
+ // before React or any native listener sees the event. Registered as
330
+ // early as possible so no other capture-phase listener can win the
331
+ // ordering race.
332
+ ctx.effect(
333
+ () => {
334
+ try {
335
+ return registerImeGuard()
336
+ } catch (error) {
337
+ fail('ime guard', error)
338
+ return undefined
339
+ }
340
+ },
341
+ 'dsh-better-sidebar: IME composition guard',
342
+ )
343
+
344
+ // DSH 0.1.x does not yet carry an icon through the settings.section
345
+ // registration contract: its shell renders a generic gear for every
346
+ // external section. Mark only this plugin's localized nav row so
347
+ // layout.css can paint the requested Side card SVG; the disposer clears
348
+ // the marker for HMR / plugin disable.
349
+ ctx.effect(
350
+ () => registerSettingsNavIcon(() => t('settingsNav')),
351
+ 'dsh-better-sidebar: settings navigation icon',
352
+ )
353
+
354
+ // The "Side card" settings section: appears in the DSH Settings shell
355
+ // once the shell's declaration is on the ledger (slots.inject waits for
356
+ // it); the section reads/writes the prefs through the plugin's own
357
+ // fenced settings route, keeps the shared store in sync, and renders the
358
+ // declarative enable/disable inventory from the tab/viewer registry.
359
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
360
+ name: 'settings.section',
361
+ id: 'better-sidebar',
362
+ order: 100,
363
+ label: () => t('settingsNav'),
364
+ inject: () => ({ store: sidebarStore, service }),
365
+ }, SideCardSection))
366
+ } catch (error) {
367
+ fail('load', error)
368
+ }
369
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Interception of the chat's produced-files row: the turn-tail chain entry
3
+ * that replaces ui-deliverables' row when the closing turn produced files.
4
+ * The takeover looks identical (same chip row); the chips open the file in
5
+ * the sidebar instead of the host OS. Priority -1 runs before the default-0
6
+ * deliverables entry; when nothing was produced the selector returns null
7
+ * and the original row renders unchanged.
8
+ */
9
+ import { IconCodeOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
10
+ import type { Context } from '../context-types.ts'
11
+ import type { SidebarStore } from './state.ts'
12
+ import { t } from './locales.ts'
13
+ import { resolveSidebarPath, selectProducedFiles } from './produced-files.ts'
14
+ import { wrapOpenPath } from './openpath-intercept.ts'
15
+ import css from './sidebar.module.css'
16
+
17
+ /** Open a file in the sidebar's editor (used by the intercepted row and the explorer). */
18
+ export function openSidebarFile(ctx: Context, store: SidebarStore, sessionId: string, path: string): void {
19
+ const summary = ctx.sessions.list.getSnapshot().byId[sessionId]
20
+ const absolute = resolveSidebarPath(summary?.cwd, path)
21
+ const at = Math.max(absolute.lastIndexOf('/'), absolute.lastIndexOf('\\'))
22
+ const title = at === -1 ? absolute : absolute.slice(at + 1)
23
+ // Route through the sidebar service so the editor descriptor's dedupeKey
24
+ // (per-path) applies; the id is path-derived so multiple editors coexist.
25
+ ctx.betterSidebar?.openTab({ type: 'editor', title, path: absolute, id: `editor:${absolute}` })
26
+ }
27
+
28
+ /** The intercepted produced-files row (visual twin of the deliverables chips). */
29
+ export function SidebarProducedFiles(props: {
30
+ matched: readonly string[]
31
+ openInSidebar: (path: string) => void
32
+ }) {
33
+ const { matched, openInSidebar } = props
34
+ const shown = matched.slice(0, 6)
35
+ const hidden = matched.length - shown.length
36
+ return (
37
+ <div className={css.producedRow}>
38
+ <span className={css.producedLabel}>{t('produced')}</span>
39
+ {shown.map(path => {
40
+ const at = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
41
+ const name = at === -1 ? path : path.slice(at + 1)
42
+ return (
43
+ <button
44
+ key={path}
45
+ type="button"
46
+ className={css.producedChip}
47
+ title={path}
48
+ onClick={() => { openInSidebar(path) }}
49
+ >
50
+ <IconCodeOutline16 size={12} />
51
+ <span>{name}</span>
52
+ </button>
53
+ )
54
+ })}
55
+ {hidden > 0 && <span className={css.producedMore}>+{hidden}</span>}
56
+ </div>
57
+ )
58
+ }
59
+
60
+ /**
61
+ * Register the turn-tail interception (returns the disposer).
62
+ *
63
+ * The slot is a CHILD slot the host's ui-conversation declares in its
64
+ * `conversation.chat.node` children table (kind: chain, scope: session).
65
+ * Registering it directly races the declaration — the ui-slots core's
66
+ * load-time validation throws "not declared (a parent entry's children
67
+ * table must declare it)" when the parent entry is not on the ledger yet.
68
+ * slots.inject waits for the declaration: the callback runs synchronously
69
+ * when the slot is already declared, otherwise it runs inside the declaring
70
+ * register() call once the declaration commits; declaration collapse
71
+ * disposes the entry and a later declaration re-registers it. This mirrors
72
+ * @deepseek-ai/dsh-client-ui-deliverables' registration of the same slot.
73
+ */
74
+ export function registerTurnTailInterception(ctx: Context, store: SidebarStore): () => void {
75
+ return ctx.slots.inject('conversation.chat.turnTail', () => ctx.slots.register({
76
+ name: 'conversation.chat.turnTail',
77
+ // Decline the takeover while the editor tab type is disabled in the side
78
+ // card settings: the produced-files row falls back to the default
79
+ // deliverables behavior instead of offering chips that cannot open. Also
80
+ // while the sidebar is externally disabled (aionui-panel chosen).
81
+ select: (owner) => {
82
+ if (store.getSuspended()) return null
83
+ if (store.getPrefs().tabsEnabled['editor'] === false) return null
84
+ return selectProducedFiles(owner)
85
+ },
86
+ priority: -1,
87
+ registrant: 'dsh-better-sidebar',
88
+ inject: (sessionId: string) => ({
89
+ openInSidebar: (path: string) => { openSidebarFile(ctx, store, sessionId, path) },
90
+ }),
91
+ }, SidebarProducedFiles))
92
+ }
93
+
94
+ /**
95
+ * Register the chat file-open interception: wraps `ctx.workspaces.openPath`
96
+ * — the single funnel every chat-side file open goes through (tool-row path
97
+ * links, the produced-files row, prose mentions) — so opens land in the
98
+ * sidebar editor instead of the Host OS. Gated by BOTH the `interceptOpenPath`
99
+ * pref and the editor tab's enable switch; declined opens fall through to
100
+ * the original method. Returns the disposer restoring the original (HMR-safe).
101
+ */
102
+ export function registerOpenPathInterception(ctx: Context, store: SidebarStore): () => void {
103
+ return wrapOpenPath(ctx.workspaces, {
104
+ takeoverEnabled: () => !store.getSuspended()
105
+ && store.getPrefs().interceptOpenPath !== false
106
+ && store.getPrefs().tabsEnabled['editor'] !== false,
107
+ currentSessionId: () => ctx.sessions.list.getSnapshot().current,
108
+ openInSidebar: (path, sessionId) => { openSidebarFile(ctx, store, sessionId, path) },
109
+ })
110
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Syntax highlighting for the file editor: extension → CodeMirror language
3
+ * mapping. The key derivation is pure and unit-tested; the factories pull in
4
+ * the CodeMirror language packages (bundled into the client).
5
+ */
6
+ import { Language, LanguageSupport, StreamLanguage } from '@codemirror/language'
7
+ import { javascript } from '@codemirror/lang-javascript'
8
+ import { json } from '@codemirror/lang-json'
9
+ import { markdown } from '@codemirror/lang-markdown'
10
+ import { python } from '@codemirror/lang-python'
11
+ import { html } from '@codemirror/lang-html'
12
+ import { css } from '@codemirror/lang-css'
13
+ import { xml } from '@codemirror/lang-xml'
14
+ import { yaml } from '@codemirror/lang-yaml'
15
+ import { sql } from '@codemirror/lang-sql'
16
+ import { java } from '@codemirror/lang-java'
17
+ import { cpp } from '@codemirror/lang-cpp'
18
+ import { rust } from '@codemirror/lang-rust'
19
+ import { go } from '@codemirror/lang-go'
20
+ import { php } from '@codemirror/lang-php'
21
+ import { shell } from '@codemirror/legacy-modes/mode/shell'
22
+ import { toml } from '@codemirror/legacy-modes/mode/toml'
23
+ import { nginx } from '@codemirror/legacy-modes/mode/nginx'
24
+ import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile'
25
+ import { properties } from '@codemirror/legacy-modes/mode/properties'
26
+ import { csharp, kotlin } from '@codemirror/legacy-modes/mode/clike'
27
+ import { swift } from '@codemirror/legacy-modes/mode/swift'
28
+
29
+ /** The lowercased file extension of a path ('' when none). */
30
+ export function extOf(path: string): string {
31
+ const at = path.lastIndexOf('.')
32
+ if (at === -1) return ''
33
+ const base = path.slice(at + 1).toLowerCase()
34
+ return base.includes('/') || base.includes('\\') ? '' : base
35
+ }
36
+
37
+ /** Language key for an extension, or null for plain text. Pure (tested). */
38
+ export function languageKeyForExt(ext: string): string | null {
39
+ switch (ext) {
40
+ case 'js': case 'mjs': case 'cjs': return 'js'
41
+ case 'jsx': return 'jsx'
42
+ case 'ts': case 'mts': case 'cts': return 'ts'
43
+ case 'tsx': return 'tsx'
44
+ case 'json': case 'jsonc': return 'json'
45
+ case 'md': case 'markdown': return 'md'
46
+ case 'py': case 'pyw': return 'python'
47
+ case 'html': case 'htm': return 'html'
48
+ case 'css': return 'css'
49
+ case 'xml': case 'xsl': return 'xml'
50
+ case 'yaml': case 'yml': return 'yaml'
51
+ case 'sql': return 'sql'
52
+ case 'java': return 'java'
53
+ case 'cs': return 'csharp'
54
+ case 'kt': case 'kts': return 'kotlin'
55
+ case 'swift': return 'swift'
56
+ case 'c': case 'h': return 'c'
57
+ case 'cc': case 'cpp': case 'cxx': case 'hpp': case 'hh': case 'hxx': return 'cpp'
58
+ case 'rs': return 'rust'
59
+ case 'go': return 'go'
60
+ case 'php': return 'php'
61
+ case 'sh': case 'bash': case 'zsh': return 'shell'
62
+ case 'toml': return 'toml'
63
+ case 'nginx': case 'conf': return 'nginx'
64
+ case 'dockerfile': case 'docker': return 'dockerfile'
65
+ case 'properties': case 'env': return 'properties'
66
+ default: return null
67
+ }
68
+ }
69
+
70
+ const FACTORIES: Record<string, () => Language | LanguageSupport> = {
71
+ js: () => javascript({ jsx: true }),
72
+ jsx: () => javascript({ jsx: true }),
73
+ ts: () => javascript({ typescript: true }),
74
+ tsx: () => javascript({ typescript: true, jsx: true }),
75
+ json: () => json(),
76
+ md: () => markdown(),
77
+ python: () => python(),
78
+ html: () => html(),
79
+ css: () => css(),
80
+ xml: () => xml(),
81
+ yaml: () => yaml(),
82
+ sql: () => sql(),
83
+ java: () => java(),
84
+ csharp: () => StreamLanguage.define(csharp),
85
+ kotlin: () => StreamLanguage.define(kotlin),
86
+ swift: () => StreamLanguage.define(swift),
87
+ c: () => cpp(),
88
+ cpp: () => cpp(),
89
+ rust: () => rust(),
90
+ go: () => go(),
91
+ php: () => php(),
92
+ shell: () => StreamLanguage.define(shell),
93
+ toml: () => StreamLanguage.define(toml),
94
+ nginx: () => StreamLanguage.define(nginx),
95
+ dockerfile: () => StreamLanguage.define(dockerFile),
96
+ properties: () => StreamLanguage.define(properties),
97
+ }
98
+
99
+ /** The CodeMirror language support for a path, or null for plain text. */
100
+ export function languageForPath(path: string): Language | LanguageSupport | null {
101
+ const key = languageKeyForExt(extOf(path))
102
+ return key === null ? null : FACTORIES[key]!()
103
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Layout push: when a panel is open it OCCUPIES the layout instead of
3
+ * floating over it — the app shell (#root, the AppFrame three-column grid)
4
+ * gives up space. Only the center column is flexible (1fr), so the right
5
+ * panel's width squeeze (margin-right on #root) lands exactly on the
6
+ * conversation output and the input bar, like a VSCode sidebar.
7
+ *
8
+ * The width rides `calc(100% - var(...))` instead of a bare margin on a
9
+ * full-width box: some desktop shells (DSH Desktop, #208) set #root to
10
+ * width:100%, where a margin would overflow the viewport additively —
11
+ * the calc keeps the box at exactly 100% minus the push in every shell.
12
+ * Width and margin transition in lockstep (same variable, same duration
13
+ * and easing), so expand/collapse animates the content width exactly as
14
+ * the bare-margin version did.
15
+ *
16
+ * The bottom panel squeezes ONLY the center column — it must not cover the
17
+ * app's own left sidebar or the right panel. The anchor is the frame's
18
+ * center grid item via its stable data attribute ([data-pane="conversation"]
19
+ * on the centerCol inside [data-dsh-frame]), not a positional path: the
20
+ * frame's child order also contains an overlay layer and drag handles
21
+ * (verified against a live DSH 0.1.x page), so nth-child is brittle. A
22
+ * stretched grid item shrinks by its margins, so the conversation content
23
+ * (output + input bar) lifts without touching the sidebars.
24
+ *
25
+ * The sizes ride CSS variables updated by the Sidebar shell (0 while
26
+ * collapsed); expand/collapse animates both the margins and the panel
27
+ * slides on the same theme duration. Drags disable the transition so the
28
+ * layout tracks the pointer.
29
+ */
30
+ #root {
31
+ margin-right: var(--dsh-sidebar-width, 0px);
32
+ width: calc(100% - var(--dsh-sidebar-width, 0px));
33
+ transition:
34
+ margin-right var(--ds-transition-duration-slow) var(--ds-ease-in-out),
35
+ width var(--ds-transition-duration-slow) var(--ds-ease-in-out);
36
+ }
37
+
38
+ /* The AppFrame's center column, anchored by data attributes (see above).
39
+ Composite selector: DSH 0.1.x versions name the center grid item
40
+ `[data-pane="conversation"]`, while rc.8-era shells put a
41
+ `[data-slot="conversation"]` child inside it — both selectors resolve to
42
+ the SAME element on live pages (verified), and keeping both future-proofs
43
+ the rule against a host rename without touching shell-specific markup. */
44
+ #root [data-dsh-frame] > [data-pane="conversation"],
45
+ #root :has(> [data-slot="conversation"]) {
46
+ margin-bottom: var(--dsh-sidebar-height, 0px);
47
+ transition: margin-bottom var(--ds-transition-duration-slow) var(--ds-ease-in-out);
48
+ }
49
+
50
+ /* When the sidebar is collapsed, the toggle cluster reclaims the top-right
51
+ corner. Push the DSH session header's right padding out so its right-aligned
52
+ utilities (the "Session log" download capsule) yield the corner instead of
53
+ hiding under the cluster. The header default right-pads 28px; the 2-button
54
+ cluster spans right 10→70px, so 78px clears it with an 8px gap. Anchor on
55
+ the header's slot host wrapper ([data-slot="conversation.session.header"])
56
+ rather than a positional path: DSH 0.1.x nests the header several levels
57
+ under the center column. The Sidebar shell toggles the body attribute with
58
+ the panel open state. */
59
+ body[data-dsh-sidebar-collapsed] [data-slot="conversation.session.header"] > header {
60
+ padding-right: 78px;
61
+ }
62
+
63
+ body[data-dsh-sidebar-dragging] #root,
64
+ body[data-dsh-sidebar-dragging] #root [data-dsh-frame] > [data-pane="conversation"],
65
+ body[data-dsh-sidebar-dragging] #root :has(> [data-slot="conversation"]) {
66
+ transition: none;
67
+ }
68
+
69
+ /* DSH 0.1.x gives external settings sections a generic gear and exposes no
70
+ icon field in the settings.section contract. settings-nav-icon.ts marks
71
+ only this plugin's localized row; render the requested Lucide
72
+ gallery-horizontal-end SVG as a currentColor mask so it follows the native
73
+ nav hover/active colors without changing the shell's 16px icon rhythm. */
74
+ [data-dsh-better-sidebar-settings-nav] > svg:first-child {
75
+ display: none;
76
+ }
77
+
78
+ [data-dsh-better-sidebar-settings-nav]::before {
79
+ content: '';
80
+ flex: none;
81
+ width: 16px;
82
+ height: 16px;
83
+ background: currentColor;
84
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 7v10'/%3E%3Cpath d='M6 5v14'/%3E%3Crect width='12' height='18' x='10' y='3' rx='2'/%3E%3C/svg%3E") center / contain no-repeat;
85
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 7v10'/%3E%3Cpath d='M6 5v14'/%3E%3Crect width='12' height='18' x='10' y='3' rx='2'/%3E%3C/svg%3E") center / contain no-repeat;
86
+ }
87
+
88
+ @media (prefers-reduced-motion: reduce) {
89
+ #root,
90
+ #root [data-dsh-frame] > [data-pane="conversation"],
91
+ #root :has(> [data-slot="conversation"]) {
92
+ transition: none;
93
+ }
94
+ }