dsh-coding-sidebar 1.0.0

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 (277) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +524 -0
  3. package/cordis.patch.yml +49 -0
  4. package/lib/client-editor.js +48229 -0
  5. package/lib/client-mermaid.js +200745 -0
  6. package/lib/client-registry.js +21867 -0
  7. package/lib/client-terminal.js +9361 -0
  8. package/lib/client.js +21867 -0
  9. package/lib/index.js +4556 -0
  10. package/lib/invariant.js +22 -0
  11. package/lib/types/agent-opens.d.ts +65 -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 +39 -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 +13 -0
  19. package/lib/types/client/FileTree.d.ts +38 -0
  20. package/lib/types/client/FreeWindow.d.ts +29 -0
  21. package/lib/types/client/GitView.d.ts +10 -0
  22. package/lib/types/client/MarkdownHtml.d.ts +32 -0
  23. package/lib/types/client/OrphanedTab.d.ts +11 -0
  24. package/lib/types/client/PdfView.d.ts +6 -0
  25. package/lib/types/client/RenderBoundary.d.ts +32 -0
  26. package/lib/types/client/SandboxStatusBar.d.ts +10 -0
  27. package/lib/types/client/SideCardSection.d.ts +74 -0
  28. package/lib/types/client/SideChatView.d.ts +16 -0
  29. package/lib/types/client/Sidebar.d.ts +6 -0
  30. package/lib/types/client/SubagentView.d.ts +16 -0
  31. package/lib/types/client/TabBar.d.ts +54 -0
  32. package/lib/types/client/TerminalView.d.ts +24 -0
  33. package/lib/types/client/TextEditor.d.ts +10 -0
  34. package/lib/types/client/TreePanel.d.ts +24 -0
  35. package/lib/types/client/UploadOverlay.d.ts +20 -0
  36. package/lib/types/client/add-plugin-modal.d.ts +15 -0
  37. package/lib/types/client/api.d.ts +275 -0
  38. package/lib/types/client/binary-download.d.ts +12 -0
  39. package/lib/types/client/breakpoints.d.ts +17 -0
  40. package/lib/types/client/browser.d.ts +60 -0
  41. package/lib/types/client/builtins/index.d.ts +18 -0
  42. package/lib/types/client/builtins/tabs.d.ts +11 -0
  43. package/lib/types/client/builtins/viewers.d.ts +3 -0
  44. package/lib/types/client/chunk-loader.d.ts +131 -0
  45. package/lib/types/client/chunks/editor.d.ts +9 -0
  46. package/lib/types/client/chunks/mermaid.d.ts +10 -0
  47. package/lib/types/client/chunks/terminal.d.ts +9 -0
  48. package/lib/types/client/cm-themes.d.ts +24 -0
  49. package/lib/types/client/conversation-draft.d.ts +14 -0
  50. package/lib/types/client/desktop-env.d.ts +35 -0
  51. package/lib/types/client/editor-load.d.ts +66 -0
  52. package/lib/types/client/frame-batcher.d.ts +24 -0
  53. package/lib/types/client/icons.d.ts +59 -0
  54. package/lib/types/client/image-types.d.ts +3 -0
  55. package/lib/types/client/ime-guard.d.ts +36 -0
  56. package/lib/types/client/index.d.ts +25 -0
  57. package/lib/types/client/intercept.d.ts +46 -0
  58. package/lib/types/client/lang.d.ts +14 -0
  59. package/lib/types/client/layout-push.d.ts +15 -0
  60. package/lib/types/client/lazy-chunk.d.ts +25 -0
  61. package/lib/types/client/link-intercept.d.ts +42 -0
  62. package/lib/types/client/locales-ar.d.ts +18 -0
  63. package/lib/types/client/locales-de.d.ts +4 -0
  64. package/lib/types/client/locales-fr.d.ts +10 -0
  65. package/lib/types/client/locales-hi.d.ts +17 -0
  66. package/lib/types/client/locales-id.d.ts +15 -0
  67. package/lib/types/client/locales-it.d.ts +8 -0
  68. package/lib/types/client/locales-ja.d.ts +17 -0
  69. package/lib/types/client/locales-ko.d.ts +9 -0
  70. package/lib/types/client/locales-nl.d.ts +16 -0
  71. package/lib/types/client/locales-pl.d.ts +19 -0
  72. package/lib/types/client/locales-pt.d.ts +1 -0
  73. package/lib/types/client/locales-ru.d.ts +13 -0
  74. package/lib/types/client/locales-sv.d.ts +1 -0
  75. package/lib/types/client/locales-th.d.ts +17 -0
  76. package/lib/types/client/locales-tr.d.ts +17 -0
  77. package/lib/types/client/locales-vi.d.ts +17 -0
  78. package/lib/types/client/locales-zh-HK.d.ts +32 -0
  79. package/lib/types/client/locales-zh-MO.d.ts +32 -0
  80. package/lib/types/client/locales-zh-TW.d.ts +32 -0
  81. package/lib/types/client/locales.d.ts +452 -0
  82. package/lib/types/client/markdown-html.d.ts +96 -0
  83. package/lib/types/client/markdown-images.d.ts +45 -0
  84. package/lib/types/client/markdown-labels.d.ts +38 -0
  85. package/lib/types/client/md-toc.d.ts +4 -0
  86. package/lib/types/client/mermaid-blocks.d.ts +47 -0
  87. package/lib/types/client/mermaid-sanitize.d.ts +2 -0
  88. package/lib/types/client/mermaid.d.ts +12 -0
  89. package/lib/types/client/open-when-sized.d.ts +20 -0
  90. package/lib/types/client/open-with-settings.d.ts +4 -0
  91. package/lib/types/client/open-with.d.ts +87 -0
  92. package/lib/types/client/openpath-intercept.d.ts +85 -0
  93. package/lib/types/client/paths.d.ts +43 -0
  94. package/lib/types/client/pdf-types.d.ts +2 -0
  95. package/lib/types/client/pinned.d.ts +86 -0
  96. package/lib/types/client/plugin-settings.d.ts +8 -0
  97. package/lib/types/client/plugins-shared.d.ts +27 -0
  98. package/lib/types/client/plugins-tabs.d.ts +3 -0
  99. package/lib/types/client/plugins-viewers.d.ts +3 -0
  100. package/lib/types/client/prefs.d.ts +41 -0
  101. package/lib/types/client/produced-files.d.ts +29 -0
  102. package/lib/types/client/selection-payload.d.ts +27 -0
  103. package/lib/types/client/service.d.ts +439 -0
  104. package/lib/types/client/settings-nav-icon.d.ts +19 -0
  105. package/lib/types/client/shell-presets.d.ts +48 -0
  106. package/lib/types/client/sidechat-transcript.d.ts +103 -0
  107. package/lib/types/client/split-pane.d.ts +36 -0
  108. package/lib/types/client/state.d.ts +432 -0
  109. package/lib/types/client/subagent-detect.d.ts +54 -0
  110. package/lib/types/client/subagent-jobs.d.ts +63 -0
  111. package/lib/types/client/tab-content-memo.d.ts +40 -0
  112. package/lib/types/client/terminal-font.d.ts +78 -0
  113. package/lib/types/client/terminal-links.d.ts +119 -0
  114. package/lib/types/client/theme.d.ts +45 -0
  115. package/lib/types/client/titlebar-strip.d.ts +23 -0
  116. package/lib/types/client/upload.d.ts +69 -0
  117. package/lib/types/client/wco.d.ts +47 -0
  118. package/lib/types/config.d.ts +64 -0
  119. package/lib/types/context-types.d.ts +605 -0
  120. package/lib/types/fs-operations.d.ts +28 -0
  121. package/lib/types/fs-search.d.ts +24 -0
  122. package/lib/types/fs-tree.d.ts +51 -0
  123. package/lib/types/git.d.ts +124 -0
  124. package/lib/types/html-route.d.ts +59 -0
  125. package/lib/types/index.d.ts +47 -0
  126. package/lib/types/invariant.d.ts +15 -0
  127. package/lib/types/jobs-routes.d.ts +45 -0
  128. package/lib/types/open-external.d.ts +24 -0
  129. package/lib/types/path-security.d.ts +20 -0
  130. package/lib/types/prefs-shared.d.ts +236 -0
  131. package/lib/types/pty-deps.d.ts +78 -0
  132. package/lib/types/pty-manager.d.ts +168 -0
  133. package/lib/types/sidechat-core.d.ts +172 -0
  134. package/lib/types/sidechat-routes.d.ts +30 -0
  135. package/lib/types/subagent-activity.d.ts +44 -0
  136. package/lib/types/subagent-live-route.d.ts +44 -0
  137. package/lib/types/tools.d.ts +34 -0
  138. package/lib/types/trust-fence.d.ts +24 -0
  139. package/lib/types/wire.d.ts +38 -0
  140. package/package.json +172 -0
  141. package/scripts/install.ps1 +306 -0
  142. package/scripts/install.sh +287 -0
  143. package/src/agent-opens.ts +265 -0
  144. package/src/agent-pty.ts +527 -0
  145. package/src/browser-probe.ts +26 -0
  146. package/src/bundle-route.ts +129 -0
  147. package/src/client/BrowserView.tsx +289 -0
  148. package/src/client/DiffTab.tsx +114 -0
  149. package/src/client/DiffView.tsx +305 -0
  150. package/src/client/EditorHost.tsx +558 -0
  151. package/src/client/FileTree.tsx +669 -0
  152. package/src/client/FreeWindow.tsx +337 -0
  153. package/src/client/GitView.tsx +717 -0
  154. package/src/client/MarkdownHtml.tsx +296 -0
  155. package/src/client/OrphanedTab.tsx +27 -0
  156. package/src/client/PdfView.tsx +110 -0
  157. package/src/client/RenderBoundary.tsx +49 -0
  158. package/src/client/SandboxStatusBar.tsx +60 -0
  159. package/src/client/SideCardSection.module.css +960 -0
  160. package/src/client/SideCardSection.tsx +1163 -0
  161. package/src/client/SideChatView.module.css +437 -0
  162. package/src/client/SideChatView.tsx +645 -0
  163. package/src/client/Sidebar.tsx +1370 -0
  164. package/src/client/SubagentView.module.css +547 -0
  165. package/src/client/SubagentView.tsx +883 -0
  166. package/src/client/TabBar.tsx +365 -0
  167. package/src/client/TerminalView.tsx +424 -0
  168. package/src/client/TextEditor.tsx +468 -0
  169. package/src/client/TreePanel.tsx +275 -0
  170. package/src/client/UploadOverlay.tsx +62 -0
  171. package/src/client/add-plugin-modal.tsx +199 -0
  172. package/src/client/api.ts +357 -0
  173. package/src/client/binary-download.tsx +23 -0
  174. package/src/client/breakpoints.ts +61 -0
  175. package/src/client/browser.ts +167 -0
  176. package/src/client/builtins/index.ts +37 -0
  177. package/src/client/builtins/tabs.tsx +340 -0
  178. package/src/client/builtins/viewers.tsx +125 -0
  179. package/src/client/chunk-loader.ts +358 -0
  180. package/src/client/chunks/editor.tsx +9 -0
  181. package/src/client/chunks/mermaid.tsx +10 -0
  182. package/src/client/chunks/terminal.tsx +9 -0
  183. package/src/client/cm-themes.ts +129 -0
  184. package/src/client/conversation-draft.ts +29 -0
  185. package/src/client/css-modules.d.ts +5 -0
  186. package/src/client/desktop-env.ts +72 -0
  187. package/src/client/editor-load.ts +92 -0
  188. package/src/client/frame-batcher.ts +56 -0
  189. package/src/client/icons.tsx +152 -0
  190. package/src/client/image-types.ts +8 -0
  191. package/src/client/ime-guard.ts +47 -0
  192. package/src/client/index.tsx +427 -0
  193. package/src/client/intercept.tsx +188 -0
  194. package/src/client/lang.ts +225 -0
  195. package/src/client/layout-push.ts +24 -0
  196. package/src/client/layout.css +90 -0
  197. package/src/client/lazy-chunk.tsx +89 -0
  198. package/src/client/link-intercept.ts +73 -0
  199. package/src/client/locales-ar.ts +368 -0
  200. package/src/client/locales-de.ts +353 -0
  201. package/src/client/locales-fr.ts +360 -0
  202. package/src/client/locales-hi.ts +367 -0
  203. package/src/client/locales-id.ts +365 -0
  204. package/src/client/locales-it.ts +358 -0
  205. package/src/client/locales-ja.ts +367 -0
  206. package/src/client/locales-ko.ts +359 -0
  207. package/src/client/locales-nl.ts +365 -0
  208. package/src/client/locales-pl.ts +369 -0
  209. package/src/client/locales-pt.ts +350 -0
  210. package/src/client/locales-ru.ts +365 -0
  211. package/src/client/locales-sv.ts +350 -0
  212. package/src/client/locales-th.ts +367 -0
  213. package/src/client/locales-tr.ts +367 -0
  214. package/src/client/locales-vi.ts +367 -0
  215. package/src/client/locales-zh-HK.ts +382 -0
  216. package/src/client/locales-zh-MO.ts +382 -0
  217. package/src/client/locales-zh-TW.ts +382 -0
  218. package/src/client/locales.ts +894 -0
  219. package/src/client/markdown-html.ts +331 -0
  220. package/src/client/markdown-images.ts +137 -0
  221. package/src/client/markdown-labels.tsx +52 -0
  222. package/src/client/md-toc.tsx +127 -0
  223. package/src/client/mermaid-blocks.ts +110 -0
  224. package/src/client/mermaid-sanitize.ts +93 -0
  225. package/src/client/mermaid.tsx +406 -0
  226. package/src/client/open-when-sized.ts +43 -0
  227. package/src/client/open-with-settings.tsx +135 -0
  228. package/src/client/open-with.ts +221 -0
  229. package/src/client/openpath-intercept.ts +143 -0
  230. package/src/client/paths.ts +63 -0
  231. package/src/client/pdf-types.ts +4 -0
  232. package/src/client/pinned.ts +183 -0
  233. package/src/client/plugin-settings.ts +43 -0
  234. package/src/client/plugins-shared.ts +29 -0
  235. package/src/client/plugins-tabs.ts +96 -0
  236. package/src/client/plugins-viewers.ts +30 -0
  237. package/src/client/prefs.ts +210 -0
  238. package/src/client/produced-files.ts +120 -0
  239. package/src/client/selection-payload.ts +86 -0
  240. package/src/client/service.ts +859 -0
  241. package/src/client/settings-nav-icon.ts +45 -0
  242. package/src/client/shell-presets.ts +83 -0
  243. package/src/client/sidebar.module.css +2788 -0
  244. package/src/client/sidechat-transcript.ts +321 -0
  245. package/src/client/split-pane.tsx +331 -0
  246. package/src/client/state.ts +1497 -0
  247. package/src/client/subagent-detect.ts +137 -0
  248. package/src/client/subagent-jobs.ts +161 -0
  249. package/src/client/tab-content-memo.ts +55 -0
  250. package/src/client/terminal-font.ts +224 -0
  251. package/src/client/terminal-links.ts +194 -0
  252. package/src/client/theme.ts +110 -0
  253. package/src/client/titlebar-strip.ts +37 -0
  254. package/src/client/upload.ts +187 -0
  255. package/src/client/wco.ts +120 -0
  256. package/src/config.ts +154 -0
  257. package/src/context-types.ts +611 -0
  258. package/src/fs-operations.ts +95 -0
  259. package/src/fs-search.ts +113 -0
  260. package/src/fs-tree.ts +158 -0
  261. package/src/git.ts +446 -0
  262. package/src/html-route.ts +106 -0
  263. package/src/index.ts +1291 -0
  264. package/src/invariant.ts +32 -0
  265. package/src/jobs-routes.ts +257 -0
  266. package/src/open-external.ts +90 -0
  267. package/src/path-security.ts +75 -0
  268. package/src/prefs-shared.ts +280 -0
  269. package/src/pty-deps.ts +240 -0
  270. package/src/pty-manager.ts +376 -0
  271. package/src/sidechat-core.ts +468 -0
  272. package/src/sidechat-routes.ts +344 -0
  273. package/src/subagent-activity.ts +91 -0
  274. package/src/subagent-live-route.ts +93 -0
  275. package/src/tools.ts +482 -0
  276. package/src/trust-fence.ts +84 -0
  277. package/src/wire.ts +100 -0
@@ -0,0 +1,427 @@
1
+ /**
2
+ * Client half of dsh-coding-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, attachBetterLocale, t, zh, en,
28
+ ja, de, fr, pt, ko, ar, hi, id, tr, vi, th, ru, it, nl, sv, pl,
29
+ zhHK, zhTW, zhMO,
30
+ } from './locales.ts'
31
+ import css from './sidebar.module.css'
32
+ import './layout.css'
33
+
34
+ /** Services required before mounting (provided by the client runtime; the
35
+ * locale service backs the sidebar's copy — see locales.ts). `modules`
36
+ * (rc.8+) is the client module system the chunk loader resolves its
37
+ * externals through — Cordis guards service access without inject.
38
+ * `remote` + `remote.session` pin this build to the 0.1.2-alpha.1+ carrier:
39
+ * the open-path interception wraps the session namespace, and the deep path
40
+ * is validated AS A WHOLE by the traceable service proxy (a bare ctx.get
41
+ * clears the first hop, then `cannot get property "remote.session" without
42
+ * inject` on the second). Cordis inject is all-required, so a carrier
43
+ * without these never mounts this plugin — same policy as `modules`. */
44
+ export const inject = ['slots', 'sessions', 'connection', 'workspaces', 'locale', 'modules', 'remote', 'remote.session']
45
+
46
+ /**
47
+ * Error boundary over the sidebar tree (root scope): a render error in the
48
+ * sidebar SHELL itself must never blank the page silently — the shared
49
+ * RenderBoundary shows a dismissible error strip and logs the stack. The
50
+ * per-tab scope (Sidebar.tsx) catches viewer/editor crashes first; this root
51
+ * boundary stays as the last resort for Workbench/shell errors.
52
+ */
53
+ /**
54
+ * Client plugin body.
55
+ * @param ctx - the client cordis context (slots, sessions).
56
+ */
57
+ export function apply(ctx: Context): void {
58
+ // The sidebar follows the DSH i18n system: attach the locale service so
59
+ // the module-level t()/isZh() resolve the Host-backed language preference
60
+ // (and switch live — the Sidebar root subscribes to it), and register the
61
+ // plugin's dictionaries into the shared locale registry. The disposers
62
+ // run on fiber disposal, so re-activation (HMR) re-registers cleanly.
63
+ attachLocale(ctx.locale)
64
+ ctx.effect(() => {
65
+ const offZh = ctx.locale.register(LOCALE_NS, 'zh', zh)
66
+ const offEn = ctx.locale.register(LOCALE_NS, 'en', en)
67
+ return () => { offZh(); offEn() }
68
+ }, 'dsh-coding-sidebar: dictionaries')
69
+
70
+ // Opt-in third-language support through @huanlin/dsh-plugin-better-locale.
71
+ // When that plugin is installed, it publishes `ctx.betterLocale` (the
72
+ // override store) and patches LocaleRuntime.prototype.lookup to consult
73
+ // it. We mirror the same override awareness into the sidebar's own `t()`:
74
+ // attachBetterLocale() makes t() consult the store's getOverride first,
75
+ // so the sidebar's chrome (which bypasses ctx.locale and calls t()
76
+ // directly) also switches to the override language. We also register
77
+ // the ja dict with the better-locale store so external callers of
78
+ // ctx.locale.lookup('betterSidebar', key) get the override text too.
79
+ //
80
+ // Activation-order-safe: ctx.get('betterLocale') is a non-reactive read
81
+ // (cordis only re-evaluates declared `inject` deps). If better-locale
82
+ // activates after dsh-coding-sidebar, the initial read returns undefined.
83
+ // We subscribe to the locale revision — better-locale bumps it on
84
+ // activation (when a persisted override exists) and on every override
85
+ // switch — and re-check ctx.get on each bump, attaching + registering
86
+ // the ja dict once the store becomes available.
87
+ ctx.effect(() => {
88
+ let dispose: (() => void) | undefined
89
+ const sync = (): void => {
90
+ dispose?.()
91
+ dispose = undefined
92
+ const store = ctx.get('betterLocale') as
93
+ | {
94
+ readonly active: string | undefined
95
+ getOverride(dshActive: string, ns: string, key: string): string | undefined
96
+ isOverrideActive(dshActive: string): boolean
97
+ register(ns: string, dicts: Record<string, Record<string, string>>): () => void
98
+ subscribe(listener: () => void): () => void
99
+ }
100
+ | undefined
101
+ attachBetterLocale(store)
102
+ if (store !== undefined) {
103
+ dispose = store.register(LOCALE_NS, {
104
+ ja, de, fr, pt, ko, ar, hi, id, tr, vi, th, ru, it, nl, sv, pl,
105
+ 'zh-HK': zhHK, 'zh-TW': zhTW, 'zh-MO': zhMO,
106
+ })
107
+ }
108
+ }
109
+ // Initial check (picks up the store if better-locale activated first).
110
+ sync()
111
+ // Re-check on every locale revision bump (better-locale bumps when it
112
+ // activates with a persisted override, and when the user switches).
113
+ const unsubscribe = ctx.locale.subscribe(sync)
114
+ return () => {
115
+ unsubscribe()
116
+ dispose?.()
117
+ attachBetterLocale(undefined)
118
+ }
119
+ }, 'dsh-coding-sidebar: better-locale lazy integration')
120
+ // One store instance per activation: production code creates it only here,
121
+ // then hands it to the mounted panel and closes over it in the slot
122
+ // registrations (the official createXXXStore() factory rule — no
123
+ // module-level singleton).
124
+ const sidebarStore = createSidebarStore()
125
+ // The sidebar registry service: external plugins register tab types and
126
+ // file previewers through `ctx.betterSidebar.registerTab/registerFileViewer`.
127
+ // Published before the panel mounts so consumers injecting 'betterSidebar'
128
+ // are ready by the time the sidebar renders.
129
+ const service = createBetterSidebarService(sidebarStore)
130
+ ctx.provide('betterSidebar', service)
131
+ // Terminal tab titles use the host's effective shell name (e.g. bash/zsh)
132
+ // instead of "Terminal 1". Start with a safe fallback and replace it as
133
+ // soon as the host shell info resolves. Tabs created before the response
134
+ // arrives keep the fallback title, so also retitle any already-open UI
135
+ // terminal tabs that still carry it.
136
+ const fallbackTitle = t('terminal')
137
+ let terminalTitle = fallbackTitle
138
+ void api.shellGet().then(({ name }) => {
139
+ terminalTitle = name
140
+ const snapshot = service.getSnapshot()
141
+ if (snapshot.state === undefined) return
142
+ const tabs = allLeaves(snapshot.state.splits).flatMap(leaf => leaf.tabs)
143
+ for (const tab of tabs) {
144
+ if (tab.type === 'terminal' && !isAgentTabId(tab.id) && tab.title === fallbackTitle) {
145
+ service.updateTab(tab.id, { title: name })
146
+ }
147
+ }
148
+ }).catch(() => { /* keep fallback */ })
149
+ // Register the plugin's own built-in tabs and viewers through the same
150
+ // service (eating our own dogfood). The disposer unregisters them on
151
+ // fiber disposal (HMR-safe).
152
+ ctx.effect(
153
+ () => registerBuiltins(ctx, service, { terminalTitle: () => terminalTitle }),
154
+ 'dsh-coding-sidebar: register built-in tabs and viewers',
155
+ )
156
+ // A failure anywhere in the client lifecycle must never take the app down
157
+ // silently: log with the plugin prefix and pin a visible diagnostic strip
158
+ // to the page so a blank panel is never the only symptom.
159
+ const fail = (phase: string, error: unknown): void => {
160
+ console.error(`[dsh-coding-sidebar] ${phase} error:`, error)
161
+ try {
162
+ const bar = document.createElement('div')
163
+ bar.style.cssText = 'position:fixed;left:8px;bottom:8px;z-index:2147483000;max-width:70vw;padding:8px 12px;'
164
+ + 'font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;color:#f2a1a1;background:#1b1b22;'
165
+ + 'border:1px solid #f2a1a1;border-radius:8px;white-space:pre-wrap'
166
+ bar.textContent = `[dsh-coding-sidebar] ${phase} error: ${error instanceof Error ? error.message : String(error)}`
167
+ document.body.appendChild(bar)
168
+ } catch {
169
+ // Nothing left to report with.
170
+ }
171
+ }
172
+ try {
173
+ // rc.8+ exposes the client module system as the `ctx.modules` service
174
+ // (no window.__DSH_MODULES__ page global anymore); the chunk loader needs
175
+ // it to resolve its externals, so inject it before anything can load a
176
+ // lazy chunk. The loader falls back to the rc.7 global when absent.
177
+ setChunkModuleSystem(ctx.modules)
178
+ // Fresh chunk state for this activation: drop per-test fixtures and
179
+ // revalidate loaded chunk scripts against the bundle route's ETags —
180
+ // unchanged chunks keep their resolved exports (no re-inject /
181
+ // re-execute on HMR), changed ones are dropped for a clean re-fetch.
182
+ void revalidateChunksOnReactivate()
183
+ ctx.effect(() => {
184
+ let disposed = false
185
+ let root: Root | undefined
186
+ let host: HTMLDivElement | undefined
187
+ let mounted = false
188
+ let bodyObserver: MutationObserver | undefined
189
+ let hostCheckFrame: number | null = null
190
+ const unmount = (): void => {
191
+ if (!mounted) return
192
+ mounted = false
193
+ bodyObserver?.disconnect()
194
+ bodyObserver = undefined
195
+ if (hostCheckFrame !== null) {
196
+ cancelAnimationFrame(hostCheckFrame)
197
+ hostCheckFrame = null
198
+ }
199
+ root?.unmount()
200
+ root = undefined
201
+ host?.remove()
202
+ host = undefined
203
+ }
204
+ /** Re-attach the host if the page (a desktop shell wrapper, SPA
205
+ * navigation, …) ever removes it from <body>. Cheap: childList only,
206
+ * no subtree, no attribute filtering. */
207
+ const guardAnchor = (): void => {
208
+ if (bodyObserver !== undefined) return
209
+ bodyObserver = new MutationObserver(() => {
210
+ if (host !== undefined && !document.body.contains(host)) {
211
+ document.body.appendChild(host)
212
+ }
213
+ })
214
+ bodyObserver.observe(document.body, { childList: true })
215
+ }
216
+ /** One-shot geometry self-check: if the host page transforms
217
+ * <html>/<body> itself (exotic shells), a fixed panel host would
218
+ * track the transformed box instead of the viewport. Flip the
219
+ * degraded mode and pin the host to the viewport every frame until
220
+ * the ancestor transform is actually gone. The normal path (no
221
+ * page-level transform) never runs the sync loop. */
222
+ const scheduleHostCheck = (): void => {
223
+ hostCheckFrame ??= requestAnimationFrame(() => {
224
+ hostCheckFrame = null
225
+ const layer = host?.querySelector<HTMLElement>('[data-dsh-panel-host]')
226
+ if (layer === null || layer === undefined) return
227
+ const rect = layer.getBoundingClientRect()
228
+ const mismatched = Math.abs(rect.left) > 8 || Math.abs(rect.top) > 8
229
+ || Math.abs(rect.width - window.innerWidth) > 8 || Math.abs(rect.height - window.innerHeight) > 8
230
+ if (!mismatched) {
231
+ layer.removeAttribute('data-dsh-panel-host-degraded')
232
+ layer.style.transform = ''
233
+ return
234
+ }
235
+ layer.setAttribute('data-dsh-panel-host-degraded', '')
236
+ console.warn('[dsh-coding-sidebar] panel host geometry mismatch — a page-level transform was detected; using degraded viewport sync')
237
+ // Track our own compensating translation so the loop judges the
238
+ // UNCORRECTED geometry: clearing degraded mode must wait for the
239
+ // ancestor transform to actually disappear — the frame right after
240
+ // our correction applies would otherwise look "fixed" and the
241
+ // offset would return immediately (CR #232 P1).
242
+ let applied = { x: 0, y: 0 }
243
+ const sync = (): void => {
244
+ const r = layer.getBoundingClientRect()
245
+ const rawLeft = r.left - applied.x
246
+ const rawTop = r.top - applied.y
247
+ if (Math.abs(rawLeft) <= 1 && Math.abs(rawTop) <= 1
248
+ && Math.abs(r.width - window.innerWidth) <= 1 && Math.abs(r.height - window.innerHeight) <= 1) {
249
+ layer.removeAttribute('data-dsh-panel-host-degraded')
250
+ layer.style.transform = ''
251
+ return
252
+ }
253
+ const next = { x: -rawLeft, y: -rawTop }
254
+ if (next.x !== applied.x || next.y !== applied.y) {
255
+ applied = next
256
+ layer.style.transform = `translate(${applied.x}px, ${applied.y}px)`
257
+ }
258
+ hostCheckFrame = requestAnimationFrame(sync)
259
+ }
260
+ hostCheckFrame = requestAnimationFrame(sync)
261
+ })
262
+ }
263
+ const mount = (): void => {
264
+ if (mounted || disposed) return
265
+ try {
266
+ host = document.createElement('div')
267
+ host.setAttribute('data-dsh-better-sidebar', '')
268
+ document.body.appendChild(host)
269
+ root = createRoot(host)
270
+ root.render(createElement(RenderBoundary, { className: css.boundaryError }, createElement(Sidebar, { ctx, store: sidebarStore })))
271
+ mounted = true
272
+ guardAnchor()
273
+ scheduleHostCheck()
274
+ } catch (error) {
275
+ fail('mount', error)
276
+ }
277
+ }
278
+ const sync = async (): Promise<void> => {
279
+ if (disposed) return
280
+ // Resolve the user's side card prefs BEFORE the first session seeds,
281
+ // so a brand-new conversation opens (or stays closed) at the chosen
282
+ // width from first paint. A settings route failure falls back to the
283
+ // schema defaults; the sidebar still mounts (a stalled wire gives up
284
+ // after the timeout and mounts on the defaults).
285
+ const prefs = await Promise.race([
286
+ loadPrefs(api),
287
+ new Promise<null>(resolve => { const timer = window.setTimeout(() => resolve(null), 2000) }),
288
+ ])
289
+ if (prefs !== null) sidebarStore.setPrefs(prefs)
290
+ if (disposed) return
291
+ // Mutual exclusion with the dsh-web-ui family right panel: while the
292
+ // aionui-panel provider is selected, the sidebar must not mount at
293
+ // all. Re-evaluated on every settings-document update (live switch).
294
+ const suspended = await loadExternalDisable(api)
295
+ if (disposed) return
296
+ sidebarStore.setSuspended(suspended)
297
+ if (suspended) unmount()
298
+ else mount()
299
+ }
300
+ void sync()
301
+ // Live re-evaluation: the runtime broadcasts settings-document updates
302
+ // (the aionui card saves through the same document). Best effort —
303
+ // deployments without the 'remote' service fall back to boot-time
304
+ // evaluation only.
305
+ const remote = ctx.get('remote') as { $on?: (event: string, listener: () => void) => () => void } | undefined
306
+ const offRemote = remote?.$on?.('settings/document-updated', () => { void sync() })
307
+ return () => {
308
+ disposed = true
309
+ offRemote?.()
310
+ unmount()
311
+ }
312
+ }, 'dsh-coding-sidebar: sidebar mount')
313
+
314
+ ctx.effect(
315
+ () => {
316
+ try {
317
+ return registerTurnTailInterception(ctx, sidebarStore)
318
+ } catch (error) {
319
+ fail('interception', error)
320
+ return () => {}
321
+ }
322
+ },
323
+ 'dsh-coding-sidebar: turn-tail interception',
324
+ )
325
+
326
+ ctx.effect(
327
+ () => {
328
+ try {
329
+ return registerOpenPathInterception(ctx, sidebarStore)
330
+ } catch (error) {
331
+ fail('interception', error)
332
+ return () => {}
333
+ }
334
+ },
335
+ 'dsh-coding-sidebar: open-path interception',
336
+ )
337
+
338
+ ctx.effect(
339
+ () => {
340
+ try {
341
+ // External http(s) links in the chat/GUI open the sidebar instead
342
+ // of a new window. Gated on the browserInterceptLinks MASTER pref,
343
+ // the URL's protocol flag (browserInterceptHttp / Https — https
344
+ // defaults OFF: most https sites refuse iframe embedding), and the
345
+ // target tab's enable switch; Ctrl/Cmd+click always bypasses. The
346
+ // target is the first registered tab whose `urlTarget` claims the
347
+ // URL (enabled tabs only), else the built-in browser tab.
348
+ const urlTargetOf = (url: URL): string | undefined => {
349
+ const prefs = sidebarStore.getPrefs()
350
+ const enabled = service.getTabs().filter(tab => prefs.tabsEnabled[tab.id] !== false)
351
+ return matchUrlTarget(enabled, url)?.id
352
+ }
353
+ return registerLinkInterception({
354
+ takeoverEnabled: (url) => {
355
+ if (sidebarStore.getSuspended()) return false
356
+ const prefs = sidebarStore.getPrefs()
357
+ if (prefs.browserInterceptLinks === false) return false
358
+ const protocolOn = url.protocol === 'https:'
359
+ ? prefs.browserInterceptHttps !== false
360
+ : prefs.browserInterceptHttp !== false
361
+ if (!protocolOn) return false
362
+ // A plugin claim is the target (already enabled-filtered);
363
+ // otherwise the built-in browser must be enabled.
364
+ return urlTargetOf(url) !== undefined || prefs.tabsEnabled['browser'] !== false
365
+ },
366
+ openInSidebar: (url) => {
367
+ let title: string | undefined
368
+ try { title = new URL(url).hostname } catch { /* keep the default title */ }
369
+ const type = urlTargetOf(new URL(url)) ?? 'browser'
370
+ ctx.get('betterSidebar')?.openTab({ type, url, title })
371
+ },
372
+ selfOrigin: window.location.origin,
373
+ })
374
+ } catch (error) {
375
+ fail('interception', error)
376
+ return () => {}
377
+ }
378
+ },
379
+ 'dsh-coding-sidebar: link interception',
380
+ )
381
+
382
+ // The IME guard: composition keys (candidate arrows, confirm, cancel)
383
+ // belong to the input method, never to page JS. Inlined third-party UI
384
+ // (formerly Univer's office controls, #562 regression) has shipped
385
+ // unguarded keydown handlers that hijack ArrowUp/ArrowDown and break
386
+ // Chinese input; the document-capture guard neutralizes the whole class
387
+ // before React or any native listener sees the event. Registered as
388
+ // early as possible so no other capture-phase listener can win the
389
+ // ordering race.
390
+ ctx.effect(
391
+ () => {
392
+ try {
393
+ return registerImeGuard()
394
+ } catch (error) {
395
+ fail('ime guard', error)
396
+ return () => {}
397
+ }
398
+ },
399
+ 'dsh-coding-sidebar: IME composition guard',
400
+ )
401
+
402
+ // DSH 0.1.x does not yet carry an icon through the settings.section
403
+ // registration contract: its shell renders a generic gear for every
404
+ // external section. Mark only this plugin's localized nav row so
405
+ // layout.css can paint the requested Side card SVG; the disposer clears
406
+ // the marker for HMR / plugin disable.
407
+ ctx.effect(
408
+ () => registerSettingsNavIcon(() => t('settingsNav')),
409
+ 'dsh-coding-sidebar: settings navigation icon',
410
+ )
411
+
412
+ // The "Side card" settings section: appears in the DSH Settings shell
413
+ // once the shell's declaration is on the ledger (slots.inject waits for
414
+ // it); the section reads/writes the prefs through the plugin's own
415
+ // fenced settings route, keeps the shared store in sync, and renders the
416
+ // declarative enable/disable inventory from the tab/viewer registry.
417
+ ctx.slots.inject('settings.section', () => ctx.slots.register({
418
+ name: 'settings.section',
419
+ id: 'dsh-coding-sidebar',
420
+ order: 100,
421
+ label: () => t('settingsNav'),
422
+ inject: () => ({ store: sidebarStore, service }),
423
+ }, SideCardSection))
424
+ } catch (error) {
425
+ fail('load', error)
426
+ }
427
+ }
@@ -0,0 +1,188 @@
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, SidebarRemoteService } from '../context-types.ts'
11
+ import { firstLeaf, revealPaths, togglePanel, type SidebarStore } from './state.ts'
12
+ import { t } from './locales.ts'
13
+ import { resolveSidebarPath, selectProducedFiles } from './produced-files.ts'
14
+ import { wrapOpenPath, wrapRemoteOpenPath } 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.get('betterSidebar')?.openTab({ type: 'editor', title, path: absolute, id: `editor:${absolute}` })
26
+ }
27
+
28
+ /**
29
+ * The produced files the turn-tail selector last matched for the visible
30
+ * session. The "Show in folder" gesture carries no file path of its own
31
+ * (`'.'`), so the reveal highlights exactly these rows when available.
32
+ */
33
+ let lastProduced: readonly string[] = []
34
+
35
+ /**
36
+ * Reveal the produced files in the sidebar explorer: expand their parent
37
+ * directories, highlight the rows, and focus the explorer tab (expanding the
38
+ * hosting panel when it is collapsed). Unknown files fall back to revealing
39
+ * the workspace root itself.
40
+ */
41
+ export function revealInExplorer(
42
+ ctx: Context,
43
+ store: SidebarStore,
44
+ sessionId: string,
45
+ files: readonly string[],
46
+ ): void {
47
+ const summary = ctx.sessions.list.getSnapshot().byId[sessionId]
48
+ const cwd = summary?.cwd
49
+ // Deliverables report paths as-is (often relative to the session cwd), but
50
+ // the explorer tree and revealPaths work on absolute paths — resolve every
51
+ // target so the ancestors expand and the row actually matches.
52
+ const targets = files.length > 0
53
+ ? files.map(path => resolveSidebarPath(cwd, path))
54
+ : cwd === undefined ? [] : [cwd]
55
+ store.reduce(state => revealPaths(state, cwd, targets))
56
+ // A type-only open never auto-expands the panel (only content opens do,
57
+ // see service.openTab) — so a reveal opens the panel itself when it is
58
+ // collapsed, exactly like the subagent auto-open flows, or the highlight
59
+ // would be set on an invisible panel.
60
+ store.reduce(s => (s.panelOpen ? s : togglePanel(s)))
61
+ // Pin the landing to the first pane: the files window must appear in the
62
+ // panel that just expanded, not wherever the user last touched.
63
+ store.reduce(s => ({ ...s, activePane: firstLeaf(s.splits).id }))
64
+ // Focus the single-instance editor home tab (the files window) where the
65
+ // reveal highlight renders. Read via ctx.get like every other internal
66
+ // consumer (#357): the provider is not on this fiber chain, so a direct
67
+ // ctx.betterSidebar read can throw before optional chaining applies.
68
+ ctx.get('betterSidebar')?.openTab({ type: 'editor', title: t('files') })
69
+ }
70
+
71
+ /** The intercepted produced-files row (visual twin of the deliverables chips). */
72
+ export function SidebarProducedFiles(props: {
73
+ matched: readonly string[]
74
+ openInSidebar: (path: string) => void
75
+ /** Reveal the produced files in the explorer ("Show in folder" twin). */
76
+ onShowInFolder: (files: readonly string[]) => void
77
+ }) {
78
+ const { matched, openInSidebar, onShowInFolder } = props
79
+ const shown = matched.slice(0, 6)
80
+ const hidden = matched.length - shown.length
81
+ return (
82
+ <div className={css.producedRow}>
83
+ <span className={css.producedLabel}>{t('produced')}</span>
84
+ {shown.map(path => {
85
+ const at = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
86
+ const name = at === -1 ? path : path.slice(at + 1)
87
+ return (
88
+ <button
89
+ key={path}
90
+ type="button"
91
+ className={css.producedChip}
92
+ title={path}
93
+ onClick={() => { openInSidebar(path) }}
94
+ >
95
+ <IconCodeOutline16 size={12} />
96
+ <span>{name}</span>
97
+ </button>
98
+ )
99
+ })}
100
+ {hidden > 0 && <span className={css.producedMore}>+{hidden}</span>}
101
+ {hidden > 0 && (
102
+ <button
103
+ type="button"
104
+ className={css.producedMore}
105
+ style={{ cursor: 'pointer', textDecoration: 'underline', textUnderlineOffset: 2 }}
106
+ onClick={() => { onShowInFolder(matched) }}
107
+ >
108
+ {t('showInFolder')}
109
+ </button>
110
+ )}
111
+ </div>
112
+ )
113
+ }
114
+
115
+ /**
116
+ * Register the turn-tail interception (returns the disposer).
117
+ *
118
+ * The slot is a CHILD slot the host's ui-conversation declares in its
119
+ * `conversation.chat.node` children table (kind: chain, scope: session).
120
+ * Registering it directly races the declaration — the ui-slots core's
121
+ * load-time validation throws "not declared (a parent entry's children
122
+ * table must declare it)" when the parent entry is not on the ledger yet.
123
+ * slots.inject waits for the declaration: the callback runs synchronously
124
+ * when the slot is already declared, otherwise it runs inside the declaring
125
+ * register() call once the declaration commits; declaration collapse
126
+ * disposes the entry and a later declaration re-registers it. This mirrors
127
+ * @deepseek-ai/dsh-client-ui-deliverables' registration of the same slot.
128
+ */
129
+ export function registerTurnTailInterception(ctx: Context, store: SidebarStore): () => void {
130
+ return ctx.slots.inject('conversation.chat.turnTail', () => ctx.slots.register({
131
+ name: 'conversation.chat.turnTail',
132
+ // Decline the takeover while the editor tab type is disabled in the side
133
+ // card settings: the produced-files row falls back to the default
134
+ // deliverables behavior instead of offering chips that cannot open. Also
135
+ // while the sidebar is externally disabled (aionui-panel chosen).
136
+ select: (owner) => {
137
+ if (store.getSuspended()) return null
138
+ if (store.getPrefs().tabsEnabled['editor'] === false) return null
139
+ const matched = selectProducedFiles(owner)
140
+ if (matched !== null) lastProduced = matched
141
+ return matched
142
+ },
143
+ priority: -1,
144
+ registrant: 'dsh-coding-sidebar',
145
+ inject: (sessionId: string) => ({
146
+ openInSidebar: (path: string) => { openSidebarFile(ctx, store, sessionId, path) },
147
+ onShowInFolder: (files: readonly string[]) => { revealInExplorer(ctx, store, sessionId, files) },
148
+ }),
149
+ }, SidebarProducedFiles))
150
+ }
151
+
152
+ /**
153
+ * Register the chat file-open interception: wraps BOTH file-open doors so
154
+ * opens land in the sidebar editor instead of the Host OS — the folder-reveal
155
+ * gesture ("Show in folder" passes `'.'`) is the one exception, routed to the
156
+ * explorer. The doors: `ctx.workspaces.openPath` (the pre-0.1.2 funnel) and
157
+ * `ctx.remote.session.openWorkspacePath` (the 0.1.2-alpha.1 funnel, a direct
158
+ * RPC that left the old door dead); each is wrapped only when present, so one
159
+ * build intercepts baselines on either side of the migration. Gated by BOTH
160
+ * the `interceptOpenPath` pref and the editor tab's enable switch; declined
161
+ * opens fall through to the original method. Returns the disposer restoring
162
+ * both doors (HMR-safe).
163
+ */
164
+ export function registerOpenPathInterception(ctx: Context, store: SidebarStore): () => void {
165
+ const deps = {
166
+ takeoverEnabled: () => !store.getSuspended()
167
+ && store.getPrefs().interceptOpenPath !== false
168
+ && store.getPrefs().tabsEnabled['editor'] !== false,
169
+ currentSessionId: () => ctx.sessions.list.getSnapshot().current,
170
+ openInSidebar: (path: string, sessionId: string) => { openSidebarFile(ctx, store, sessionId, path) },
171
+ revealInExplorer: (_path: string, sessionId: string) => { revealInExplorer(ctx, store, sessionId, lastProduced) },
172
+ }
173
+ const disposeOld = wrapOpenPath(ctx.workspaces, deps)
174
+ // Optional probe: the Remote carrier is absent on pre-migration baselines
175
+ // (and the wrap itself no-ops when the method is missing). Read via
176
+ // ctx.get like every other optional service (same recipe as the
177
+ // settings/document-updated listener): a direct `ctx.remote` property
178
+ // read throws under cordis's inject enforcement ("cannot get property
179
+ // ... without inject") — ctx.get just returns undefined instead.
180
+ const remote = ctx.get('remote') as SidebarRemoteService | undefined
181
+ const disposeRemote = remote === undefined
182
+ ? () => {}
183
+ : wrapRemoteOpenPath(remote.session, deps)
184
+ return () => {
185
+ disposeOld()
186
+ disposeRemote()
187
+ }
188
+ }