@chatcode/cco-better-sidebar 0.18.0-alpha.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 (329) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +554 -0
  3. package/README_EN.md +563 -0
  4. package/assets/logo.svg +1 -0
  5. package/cordis.patch.yml +79 -0
  6. package/lib/client-editor.js +55744 -0
  7. package/lib/client-mermaid.js +208256 -0
  8. package/lib/client-registry.js +30378 -0
  9. package/lib/client-terminal.js +16894 -0
  10. package/lib/client.js +30378 -0
  11. package/lib/index.js +6897 -0
  12. package/lib/invariant.js +22 -0
  13. package/lib/types/agent-opens.d.ts +65 -0
  14. package/lib/types/agent-pty.d.ts +226 -0
  15. package/lib/types/browser-probe.d.ts +15 -0
  16. package/lib/types/bundle-route.d.ts +12 -0
  17. package/lib/types/chatcode-update.d.ts +47 -0
  18. package/lib/types/client/AccountMenu.d.ts +1 -0
  19. package/lib/types/client/AiRatioSection.d.ts +3 -0
  20. package/lib/types/client/BrowserView.d.ts +39 -0
  21. package/lib/types/client/ChatcodeGate.d.ts +6 -0
  22. package/lib/types/client/CodingPlanModelsSettings.d.ts +9 -0
  23. package/lib/types/client/DiffTab.d.ts +6 -0
  24. package/lib/types/client/DiffView.d.ts +51 -0
  25. package/lib/types/client/EditorHost.d.ts +13 -0
  26. package/lib/types/client/FenceErrorNotice.d.ts +5 -0
  27. package/lib/types/client/FileTree.d.ts +41 -0
  28. package/lib/types/client/FreeWindow.d.ts +29 -0
  29. package/lib/types/client/GitView.d.ts +12 -0
  30. package/lib/types/client/MarkdownHtml.d.ts +32 -0
  31. package/lib/types/client/OrphanedTab.d.ts +11 -0
  32. package/lib/types/client/PdfView.d.ts +6 -0
  33. package/lib/types/client/ReleaseNotesDialog.d.ts +11 -0
  34. package/lib/types/client/RenderBoundary.d.ts +32 -0
  35. package/lib/types/client/SandboxStatusBar.d.ts +10 -0
  36. package/lib/types/client/SideCardSection.d.ts +74 -0
  37. package/lib/types/client/Sidebar.d.ts +8 -0
  38. package/lib/types/client/TabBar.d.ts +54 -0
  39. package/lib/types/client/TerminalView.d.ts +24 -0
  40. package/lib/types/client/TextEditor.d.ts +10 -0
  41. package/lib/types/client/TreePanel.d.ts +27 -0
  42. package/lib/types/client/UpdateDialog.d.ts +10 -0
  43. package/lib/types/client/UpdateSettingsPanel.d.ts +1 -0
  44. package/lib/types/client/UpdateSettingsSection.d.ts +10 -0
  45. package/lib/types/client/UploadOverlay.d.ts +20 -0
  46. package/lib/types/client/UsageHeatmapSection.d.ts +4 -0
  47. package/lib/types/client/add-plugin-modal.d.ts +15 -0
  48. package/lib/types/client/api.d.ts +345 -0
  49. package/lib/types/client/binary-download.d.ts +12 -0
  50. package/lib/types/client/breakpoints.d.ts +17 -0
  51. package/lib/types/client/browser.d.ts +60 -0
  52. package/lib/types/client/builtins/index.d.ts +18 -0
  53. package/lib/types/client/builtins/tabs.d.ts +11 -0
  54. package/lib/types/client/builtins/viewers.d.ts +3 -0
  55. package/lib/types/client/chat-presentation.d.ts +13 -0
  56. package/lib/types/client/chunk-loader.d.ts +124 -0
  57. package/lib/types/client/chunks/editor.d.ts +9 -0
  58. package/lib/types/client/chunks/mermaid.d.ts +10 -0
  59. package/lib/types/client/chunks/terminal.d.ts +9 -0
  60. package/lib/types/client/cm-themes.d.ts +24 -0
  61. package/lib/types/client/conversation-draft.d.ts +14 -0
  62. package/lib/types/client/desktop-env.d.ts +35 -0
  63. package/lib/types/client/editor-load.d.ts +66 -0
  64. package/lib/types/client/frame-batcher.d.ts +24 -0
  65. package/lib/types/client/icons.d.ts +66 -0
  66. package/lib/types/client/image-types.d.ts +3 -0
  67. package/lib/types/client/ime-guard.d.ts +36 -0
  68. package/lib/types/client/index.d.ts +19 -0
  69. package/lib/types/client/intercept.d.ts +44 -0
  70. package/lib/types/client/lang.d.ts +14 -0
  71. package/lib/types/client/layout-push.d.ts +15 -0
  72. package/lib/types/client/lazy-chunk.d.ts +25 -0
  73. package/lib/types/client/link-intercept.d.ts +42 -0
  74. package/lib/types/client/locales-ar.d.ts +18 -0
  75. package/lib/types/client/locales-de.d.ts +4 -0
  76. package/lib/types/client/locales-fr.d.ts +10 -0
  77. package/lib/types/client/locales-hi.d.ts +17 -0
  78. package/lib/types/client/locales-id.d.ts +15 -0
  79. package/lib/types/client/locales-it.d.ts +8 -0
  80. package/lib/types/client/locales-ja.d.ts +17 -0
  81. package/lib/types/client/locales-ko.d.ts +9 -0
  82. package/lib/types/client/locales-nl.d.ts +16 -0
  83. package/lib/types/client/locales-pl.d.ts +19 -0
  84. package/lib/types/client/locales-pt.d.ts +1 -0
  85. package/lib/types/client/locales-ru.d.ts +13 -0
  86. package/lib/types/client/locales-sv.d.ts +1 -0
  87. package/lib/types/client/locales-th.d.ts +17 -0
  88. package/lib/types/client/locales-tr.d.ts +17 -0
  89. package/lib/types/client/locales-vi.d.ts +17 -0
  90. package/lib/types/client/locales-zh-HK.d.ts +32 -0
  91. package/lib/types/client/locales-zh-MO.d.ts +32 -0
  92. package/lib/types/client/locales-zh-TW.d.ts +32 -0
  93. package/lib/types/client/locales.d.ts +571 -0
  94. package/lib/types/client/markdown-html.d.ts +96 -0
  95. package/lib/types/client/markdown-images.d.ts +45 -0
  96. package/lib/types/client/markdown-labels.d.ts +21 -0
  97. package/lib/types/client/md-toc.d.ts +4 -0
  98. package/lib/types/client/mermaid-blocks.d.ts +47 -0
  99. package/lib/types/client/mermaid-sanitize.d.ts +2 -0
  100. package/lib/types/client/mermaid.d.ts +12 -0
  101. package/lib/types/client/open-when-sized.d.ts +20 -0
  102. package/lib/types/client/open-with-settings.d.ts +4 -0
  103. package/lib/types/client/open-with.d.ts +87 -0
  104. package/lib/types/client/openpath-intercept.d.ts +52 -0
  105. package/lib/types/client/paths.d.ts +43 -0
  106. package/lib/types/client/pdf-types.d.ts +2 -0
  107. package/lib/types/client/pinned.d.ts +86 -0
  108. package/lib/types/client/plugin-settings.d.ts +8 -0
  109. package/lib/types/client/plugins-shared.d.ts +27 -0
  110. package/lib/types/client/plugins-tabs.d.ts +3 -0
  111. package/lib/types/client/plugins-viewers.d.ts +3 -0
  112. package/lib/types/client/prefs.d.ts +41 -0
  113. package/lib/types/client/produced-files.d.ts +29 -0
  114. package/lib/types/client/selection-payload.d.ts +27 -0
  115. package/lib/types/client/service.d.ts +445 -0
  116. package/lib/types/client/session-pins.d.ts +14 -0
  117. package/lib/types/client/settings-nav-icon.d.ts +27 -0
  118. package/lib/types/client/shell-presets.d.ts +48 -0
  119. package/lib/types/client/split-pane.d.ts +38 -0
  120. package/lib/types/client/state.d.ts +480 -0
  121. package/lib/types/client/tab-content-memo.d.ts +40 -0
  122. package/lib/types/client/terminal-font.d.ts +78 -0
  123. package/lib/types/client/terminal-links.d.ts +119 -0
  124. package/lib/types/client/theme.d.ts +45 -0
  125. package/lib/types/client/titlebar-strip.d.ts +23 -0
  126. package/lib/types/client/ui-tweaks/appicons.d.ts +16 -0
  127. package/lib/types/client/ui-tweaks/archive.d.ts +42 -0
  128. package/lib/types/client/ui-tweaks/branding.d.ts +45 -0
  129. package/lib/types/client/ui-tweaks/cachehit.d.ts +41 -0
  130. package/lib/types/client/ui-tweaks/gitbar.d.ts +103 -0
  131. package/lib/types/client/ui-tweaks/index.d.ts +279 -0
  132. package/lib/types/client/ui-tweaks/mcp.d.ts +28 -0
  133. package/lib/types/client/ui-tweaks/notifier.d.ts +125 -0
  134. package/lib/types/client/ui-tweaks/whale.d.ts +55 -0
  135. package/lib/types/client/upload.d.ts +69 -0
  136. package/lib/types/client/useChatcodeAccount.d.ts +10 -0
  137. package/lib/types/client/wco.d.ts +47 -0
  138. package/lib/types/codingplan.d.ts +37 -0
  139. package/lib/types/config.d.ts +95 -0
  140. package/lib/types/context-types.d.ts +540 -0
  141. package/lib/types/fs-operations.d.ts +30 -0
  142. package/lib/types/fs-search.d.ts +24 -0
  143. package/lib/types/fs-tree.d.ts +51 -0
  144. package/lib/types/git.d.ts +124 -0
  145. package/lib/types/html-route.d.ts +59 -0
  146. package/lib/types/index.d.ts +47 -0
  147. package/lib/types/invariant.d.ts +15 -0
  148. package/lib/types/login-route.d.ts +2 -0
  149. package/lib/types/open-external.d.ts +24 -0
  150. package/lib/types/path-security.d.ts +26 -0
  151. package/lib/types/prefs-shared.d.ts +257 -0
  152. package/lib/types/pty-deps.d.ts +79 -0
  153. package/lib/types/pty-manager.d.ts +168 -0
  154. package/lib/types/tools.d.ts +34 -0
  155. package/lib/types/trust-fence.d.ts +24 -0
  156. package/lib/types/ui-tweaks/archive.d.ts +71 -0
  157. package/lib/types/ui-tweaks/config.d.ts +186 -0
  158. package/lib/types/ui-tweaks/git-web.d.ts +43 -0
  159. package/lib/types/ui-tweaks/git.d.ts +311 -0
  160. package/lib/types/ui-tweaks/index.d.ts +17 -0
  161. package/lib/types/ui-tweaks/mcp.d.ts +103 -0
  162. package/lib/types/ui-tweaks/turn-outcome.d.ts +57 -0
  163. package/lib/types/ui-tweaks/web.d.ts +55 -0
  164. package/lib/types/wire.d.ts +38 -0
  165. package/package.json +213 -0
  166. package/scripts/install.ps1 +310 -0
  167. package/scripts/install.sh +287 -0
  168. package/src/agent-opens.ts +265 -0
  169. package/src/agent-pty.ts +527 -0
  170. package/src/browser-probe.ts +26 -0
  171. package/src/bundle-route.ts +129 -0
  172. package/src/chatcode-update.ts +193 -0
  173. package/src/client/AccountMenu.tsx +177 -0
  174. package/src/client/AiRatioSection.module.css +329 -0
  175. package/src/client/AiRatioSection.tsx +292 -0
  176. package/src/client/BrowserView.tsx +289 -0
  177. package/src/client/ChatcodeGate.tsx +35 -0
  178. package/src/client/CodingPlanModelsSettings.module.css +118 -0
  179. package/src/client/CodingPlanModelsSettings.tsx +101 -0
  180. package/src/client/DiffTab.tsx +114 -0
  181. package/src/client/DiffView.tsx +305 -0
  182. package/src/client/EditorHost.tsx +563 -0
  183. package/src/client/FenceErrorNotice.tsx +40 -0
  184. package/src/client/FileTree.tsx +701 -0
  185. package/src/client/FreeWindow.tsx +337 -0
  186. package/src/client/GitView.tsx +722 -0
  187. package/src/client/MarkdownHtml.tsx +296 -0
  188. package/src/client/OrphanedTab.tsx +27 -0
  189. package/src/client/PdfView.tsx +110 -0
  190. package/src/client/ReleaseNotesDialog.tsx +91 -0
  191. package/src/client/RenderBoundary.tsx +49 -0
  192. package/src/client/SandboxStatusBar.tsx +60 -0
  193. package/src/client/SideCardSection.module.css +932 -0
  194. package/src/client/SideCardSection.tsx +1169 -0
  195. package/src/client/Sidebar.tsx +1857 -0
  196. package/src/client/TabBar.tsx +365 -0
  197. package/src/client/TerminalView.tsx +424 -0
  198. package/src/client/TextEditor.tsx +468 -0
  199. package/src/client/TreePanel.tsx +279 -0
  200. package/src/client/UpdateDialog.tsx +186 -0
  201. package/src/client/UpdateSettingsPanel.tsx +29 -0
  202. package/src/client/UpdateSettingsSection.tsx +22 -0
  203. package/src/client/UploadOverlay.tsx +62 -0
  204. package/src/client/UsageHeatmapSection.module.css +440 -0
  205. package/src/client/UsageHeatmapSection.tsx +301 -0
  206. package/src/client/add-plugin-modal.tsx +199 -0
  207. package/src/client/api.ts +433 -0
  208. package/src/client/binary-download.tsx +23 -0
  209. package/src/client/breakpoints.ts +61 -0
  210. package/src/client/browser.ts +167 -0
  211. package/src/client/builtins/index.ts +37 -0
  212. package/src/client/builtins/tabs.tsx +275 -0
  213. package/src/client/builtins/viewers.tsx +125 -0
  214. package/src/client/chat-presentation.ts +95 -0
  215. package/src/client/chunk-loader.ts +350 -0
  216. package/src/client/chunks/editor.tsx +9 -0
  217. package/src/client/chunks/mermaid.tsx +10 -0
  218. package/src/client/chunks/terminal.tsx +9 -0
  219. package/src/client/cm-themes.ts +129 -0
  220. package/src/client/conversation-draft.ts +29 -0
  221. package/src/client/css-modules.d.ts +5 -0
  222. package/src/client/desktop-env.ts +72 -0
  223. package/src/client/editor-load.ts +92 -0
  224. package/src/client/frame-batcher.ts +56 -0
  225. package/src/client/icons.tsx +170 -0
  226. package/src/client/image-types.ts +8 -0
  227. package/src/client/ime-guard.ts +47 -0
  228. package/src/client/index.tsx +501 -0
  229. package/src/client/intercept.tsx +172 -0
  230. package/src/client/lang.ts +225 -0
  231. package/src/client/layout-push.ts +37 -0
  232. package/src/client/layout.css +156 -0
  233. package/src/client/lazy-chunk.tsx +89 -0
  234. package/src/client/link-intercept.ts +73 -0
  235. package/src/client/locales-ar.ts +387 -0
  236. package/src/client/locales-de.ts +372 -0
  237. package/src/client/locales-fr.ts +379 -0
  238. package/src/client/locales-hi.ts +386 -0
  239. package/src/client/locales-id.ts +384 -0
  240. package/src/client/locales-it.ts +377 -0
  241. package/src/client/locales-ja.ts +486 -0
  242. package/src/client/locales-ko.ts +378 -0
  243. package/src/client/locales-nl.ts +384 -0
  244. package/src/client/locales-pl.ts +388 -0
  245. package/src/client/locales-pt.ts +369 -0
  246. package/src/client/locales-ru.ts +384 -0
  247. package/src/client/locales-sv.ts +369 -0
  248. package/src/client/locales-th.ts +386 -0
  249. package/src/client/locales-tr.ts +386 -0
  250. package/src/client/locales-vi.ts +386 -0
  251. package/src/client/locales-zh-HK.ts +401 -0
  252. package/src/client/locales-zh-MO.ts +401 -0
  253. package/src/client/locales-zh-TW.ts +401 -0
  254. package/src/client/locales.ts +1229 -0
  255. package/src/client/markdown-html.ts +331 -0
  256. package/src/client/markdown-images.ts +137 -0
  257. package/src/client/markdown-labels.tsx +31 -0
  258. package/src/client/md-toc.tsx +127 -0
  259. package/src/client/mermaid-blocks.ts +110 -0
  260. package/src/client/mermaid-sanitize.ts +93 -0
  261. package/src/client/mermaid.tsx +406 -0
  262. package/src/client/open-when-sized.ts +43 -0
  263. package/src/client/open-with-settings.tsx +135 -0
  264. package/src/client/open-with.ts +221 -0
  265. package/src/client/openpath-intercept.ts +79 -0
  266. package/src/client/paths.ts +63 -0
  267. package/src/client/pdf-types.ts +4 -0
  268. package/src/client/pinned.ts +184 -0
  269. package/src/client/plugin-settings.ts +43 -0
  270. package/src/client/plugins-shared.ts +29 -0
  271. package/src/client/plugins-tabs.ts +153 -0
  272. package/src/client/plugins-viewers.ts +44 -0
  273. package/src/client/prefs.ts +216 -0
  274. package/src/client/produced-files.ts +120 -0
  275. package/src/client/selection-payload.ts +86 -0
  276. package/src/client/service.ts +873 -0
  277. package/src/client/session-pins.tsx +334 -0
  278. package/src/client/settings-nav-icon.ts +55 -0
  279. package/src/client/shell-presets.ts +83 -0
  280. package/src/client/sidebar.module.css +3607 -0
  281. package/src/client/split-pane.tsx +333 -0
  282. package/src/client/state.ts +1646 -0
  283. package/src/client/tab-content-memo.ts +55 -0
  284. package/src/client/terminal-font.ts +224 -0
  285. package/src/client/terminal-links.ts +194 -0
  286. package/src/client/theme.ts +110 -0
  287. package/src/client/titlebar-strip.ts +37 -0
  288. package/src/client/ui-tweaks/appicons.ts +24 -0
  289. package/src/client/ui-tweaks/archive.tsx +281 -0
  290. package/src/client/ui-tweaks/branding.tsx +232 -0
  291. package/src/client/ui-tweaks/cachehit.tsx +122 -0
  292. package/src/client/ui-tweaks/gitbar.tsx +2559 -0
  293. package/src/client/ui-tweaks/index.tsx +1696 -0
  294. package/src/client/ui-tweaks/mcp.tsx +582 -0
  295. package/src/client/ui-tweaks/notifier.ts +514 -0
  296. package/src/client/ui-tweaks/whale.tsx +171 -0
  297. package/src/client/upload.ts +187 -0
  298. package/src/client/useChatcodeAccount.ts +103 -0
  299. package/src/client/wco.ts +120 -0
  300. package/src/codingplan.ts +146 -0
  301. package/src/config.ts +207 -0
  302. package/src/context-types.ts +559 -0
  303. package/src/fs-operations.ts +97 -0
  304. package/src/fs-search.ts +113 -0
  305. package/src/fs-tree.ts +158 -0
  306. package/src/git.ts +446 -0
  307. package/src/html-route.ts +106 -0
  308. package/src/index.ts +1524 -0
  309. package/src/invariant.ts +32 -0
  310. package/src/login-route.ts +26 -0
  311. package/src/open-external.ts +90 -0
  312. package/src/path-security.ts +81 -0
  313. package/src/prefs-shared.ts +303 -0
  314. package/src/pty-deps.ts +241 -0
  315. package/src/pty-manager.ts +376 -0
  316. package/src/tools.ts +482 -0
  317. package/src/trust-fence.ts +84 -0
  318. package/src/ui-tweaks/archive.ts +293 -0
  319. package/src/ui-tweaks/config.ts +257 -0
  320. package/src/ui-tweaks/git-web.ts +421 -0
  321. package/src/ui-tweaks/git.ts +1524 -0
  322. package/src/ui-tweaks/index.ts +61 -0
  323. package/src/ui-tweaks/mcp.ts +535 -0
  324. package/src/ui-tweaks/turn-outcome.ts +105 -0
  325. package/src/ui-tweaks/web.ts +225 -0
  326. package/src/wire.ts +110 -0
  327. package/vendor/addon-fit.js +2 -0
  328. package/vendor/xterm.css +218 -0
  329. package/vendor/xterm.js +5 -0
@@ -0,0 +1,559 @@
1
+ /**
2
+ * Structural types for the cordis services this plugin consumes, plus the
3
+ * Context face both halves share.
4
+ *
5
+ * The type base is the vendored `@deepseek-ai/cordis` Context (the runtime
6
+ * DSH actually runs); the service members this plugin touches are restated
7
+ * below as structural mirrors and combined with the base by INTERSECTION.
8
+ * Intersection (not `declare module` augmentation) is deliberate: DSH's own
9
+ * packages already augment `@deepseek-ai/cordis`, and the host and client
10
+ * packages declare *different* types for the same member — host
11
+ * `sessions: SessionStore` vs client runtime `sessions: ISessions` — so a
12
+ * single program that re-declares them would fail interface merging
13
+ * (TS2717). Intersecting keeps every face available and lets each call site
14
+ * resolve against the member it needs without any module-level conflict.
15
+ *
16
+ * `effect`, `get`, `provide`, `inject`, `logger`, `emit`, `isolate` and the
17
+ * event helpers come from the vendored cordis base and are intentionally NOT
18
+ * restated here: their strict shapes are the runtime contract (e.g. an
19
+ * effect body must return a disposer). Only the string-keyed session-feed
20
+ * `on` overload is added, because the cordis `on` is keyed to its own
21
+ * typed `Events` map and the harness session feed is a plain string event.
22
+ *
23
+ * This file must stay FREE of Node.js types (`node:http`, `node:stream`,
24
+ * `Buffer`): it is part of the CLIENT-reachable declaration graph (the
25
+ * `Context` in `TabComponentProps` and the `betterSidebar` augmentation),
26
+ * so a Node import here would leak into browser-only consumer builds. The
27
+ * webServer faces below are therefore structural mirrors with plain
28
+ * interfaces (the host casts to real Node types at the few boundaries that
29
+ * need them — e.g. the `ws` upgrade hook in src/index.ts).
30
+ */
31
+ import type { Context as CordisContext } from '@deepseek-ai/cordis'
32
+ import type { BetterSidebarService } from './client/service.ts'
33
+
34
+ /** The request face route handlers see (structural subset of node's
35
+ * IncomingMessage: the URL/method/header reads and the async body
36
+ * iteration `readJsonBody` uses). */
37
+ export interface SidebarHttpRequest {
38
+ url?: string
39
+ method?: string
40
+ headers: Record<string, string | string[] | undefined>
41
+ [Symbol.asyncIterator](): AsyncIterator<string | Uint8Array>
42
+ }
43
+
44
+ /** The response face route handlers write to (structural subset of node's
45
+ * ServerResponse: the status/header/body writes the routes use). */
46
+ export interface SidebarHttpResponse {
47
+ statusCode: number
48
+ writeHead(status: number, headers?: Record<string, string>): void
49
+ end(body?: string | Uint8Array): void
50
+ }
51
+
52
+ /** The upgrade socket face (structural subset: the destroy the fences use). */
53
+ export interface SidebarUpgradeSocket {
54
+ destroy(): void
55
+ }
56
+
57
+ /** The upgrade head bytes (Buffer at runtime; typed as bytes so no Node
58
+ * global leaks into the declaration graph). */
59
+ export type SidebarUpgradeHead = Uint8Array
60
+
61
+ /** One named webserver route (mirror of the host-webserver WebRoute). */
62
+ export interface SidebarWebRoute {
63
+ kind: 'exact' | 'prefix'
64
+ path: string
65
+ handler: (req: SidebarHttpRequest, res: SidebarHttpResponse) => void | Promise<void>
66
+ }
67
+
68
+ /** One exact-path HTTP upgrade registration (mirror of WebUpgradeRoute). */
69
+ export interface SidebarWebUpgradeRoute {
70
+ path: string
71
+ handler: (req: SidebarHttpRequest, socket: SidebarUpgradeSocket, head: SidebarUpgradeHead) => void | Promise<void>
72
+ }
73
+
74
+ /** The webServer service face this plugin uses. */
75
+ export interface SidebarWebServer {
76
+ register(route: SidebarWebRoute): () => void
77
+ registerUpgrade(route: SidebarWebUpgradeRoute): () => void
78
+ }
79
+
80
+ /** A published session's header slice the sidebar reads (authoritative cwd). */
81
+ export interface SidebarSessionHeader {
82
+ cwd?: string
83
+ }
84
+
85
+ /** The host session store face (`ctx.sessions.get(id)` returns the live session). */
86
+ export interface SidebarSessionStore {
87
+ get(id: string): {
88
+ header: SidebarSessionHeader
89
+ /**
90
+ * The live session's append-only event log (immutable snapshot; absent
91
+ * on sessions the runtime has not hydrated). Read-only access — the
92
+ * jobs.output route replays `job_output` tool/result rows from it.
93
+ */
94
+ events?: readonly SidebarSessionEvent[]
95
+ } | undefined
96
+ }
97
+
98
+ /**
99
+ * The web runtime service face (mirror of @deepseek-ai/dsh-web-app's
100
+ * WebRuntimeValues): the bind-derived trust list the /api gateway's fence
101
+ * accepts — LAN IP literals sampled when the server binds all interfaces,
102
+ * plus explicit `--trusted-host` authorities.
103
+ */
104
+ export interface SidebarWebRuntime {
105
+ trustedHosts: readonly string[]
106
+ }
107
+
108
+ /** Registration options the sidebar passes to `ctx.slots.register` (subset of the real options). */
109
+ export interface SidebarSlotRegisterOptions {
110
+ name: string
111
+ key?: string
112
+ id?: string
113
+ order?: number
114
+ label?: string | (() => string)
115
+ /** Chain routing selector (returns the matched value, or null to pass on). */
116
+ select?: (owner: unknown) => unknown
117
+ priority?: number
118
+ locale?: string
119
+ registrant?: string
120
+ /** Business-face factory; args depend on the slot scope. */
121
+ inject?: (...args: any[]) => Record<string, unknown>
122
+ children?: Record<string, unknown>
123
+ }
124
+
125
+ /** The client slots service face (register returns the disposer). */
126
+ export interface SidebarSlotsService {
127
+ register(options: SidebarSlotRegisterOptions, component: unknown): () => void
128
+ /**
129
+ * Run a callback for each declaration lifetime of a slot (the runtime
130
+ * SlotRegistry.inject): a no-op while the slot is undeclared, so the
131
+ * settings section registration waits for the settings shell.
132
+ */
133
+ inject(key: string, callback: () => () => void): () => void
134
+ }
135
+
136
+ /** The client session list row the sidebar reads (cwd for the explorer). */
137
+ export interface SidebarSessionSummary {
138
+ id: string
139
+ cwd?: string
140
+ displayTitle: string
141
+ /** Coarse durable origin for navigation filtering (subagent children). */
142
+ origin?: 'subagent'
143
+ /** Durable direct parent session id (present on subagent children). */
144
+ parentId?: string
145
+ /** Whether the session's agent is currently running. */
146
+ running?: boolean
147
+ }
148
+
149
+ /** One healthy subagent catalog child row (structural mirror of the runtime). */
150
+ export interface SidebarSubagentChildEntry {
151
+ kind: 'child'
152
+ id: string
153
+ /** Whether the child Agent driver is running at the Host sampling boundary. */
154
+ activity: 'running' | 'inactive'
155
+ /** Whether a direct descendant has durable `origin: 'subagent'`. */
156
+ hasChildren: boolean
157
+ mode: 'one-shot' | 'continuable'
158
+ label?: string
159
+ }
160
+
161
+ /** One unreadable catalog row (corrupt / unsupported / unavailable). */
162
+ export interface SidebarSubagentDiagnosticEntry {
163
+ kind: 'diagnostic'
164
+ id: string
165
+ reason: 'corrupt' | 'unsupported' | 'unavailable'
166
+ }
167
+
168
+ /** The per-parent lazy catalog delivered through the sessions list feed. */
169
+ export interface SidebarSubagentCatalog {
170
+ entries: Array<SidebarSubagentChildEntry | SidebarSubagentDiagnosticEntry>
171
+ parentAvailable: boolean
172
+ state: 'loading' | 'ready' | 'error'
173
+ error: { code?: string; message?: string } | null
174
+ }
175
+
176
+ /** Durable parent/child address that selects subagent transport in the client. */
177
+ export interface SidebarSubagentAddress {
178
+ parentSessionId: string
179
+ childSessionId: string
180
+ mode: 'one-shot' | 'continuable'
181
+ }
182
+
183
+ /** Minimal structural mirror of one session event (the subagent history tail). */
184
+ export interface SidebarSessionEvent {
185
+ type: string
186
+ seq: number
187
+ time: number
188
+ data: Record<string, unknown>
189
+ }
190
+
191
+ /** One history row: the durable event plus an optional tool presentation view. */
192
+ export interface SidebarHistoryEntry {
193
+ event: SidebarSessionEvent
194
+ view?: unknown
195
+ }
196
+
197
+ /** Lifecycle status set of one background job (closed wire union). */
198
+ export type SidebarJobStatus = 'running' | 'stopping' | 'completed' | 'killed' | 'failed'
199
+
200
+ /**
201
+ * One background job as the client mirror sees it (wire `JobView` shape:
202
+ * id/kind/label/status/detail?/startedAt/finishedAt?).
203
+ */
204
+ export interface SidebarJobView {
205
+ /** Registry-issued `<kind>-N` identity, stable for the job's whole life. */
206
+ id: string
207
+ /** Producer kind (`bash`, `pwsh`, `subagent`, …; open string by design). */
208
+ kind: string
209
+ /** Producer-supplied one-line label: the command, or the delegation description. */
210
+ label: string
211
+ /** Current lifecycle state. */
212
+ status: SidebarJobStatus
213
+ /** Kind-specific status detail ('exit code: 3'), present once supplied. */
214
+ detail?: string
215
+ /** Epoch ms when the job was registered. */
216
+ startedAt: number
217
+ /** Epoch ms when the job settled; absent while live. */
218
+ finishedAt?: number
219
+ }
220
+
221
+ /** The host jobs registry face the sidebar routes touch (structural mirror of `JobRegistry`). */
222
+ export interface SidebarJobsService {
223
+ /** Request cancellation; throws for an unknown or foreign job. */
224
+ kill(id: string, caller?: SidebarAgent, reason?: string): 'requested' | 'already-finished'
225
+ }
226
+
227
+ /** The host agent registry face (structural mirror of the runtime `ctx.agents`). */
228
+ export interface SidebarAgentsService {
229
+ /** The live agent registered under a session id, or undefined when not live. */
230
+ get(id: string): SidebarAgent | undefined
231
+ /**
232
+ * Create a session + agent with a custom seed (mirror of the runtime
233
+ * AgentRegistry.create) — the Side Chat thread-creation seam: the SAME
234
+ * public seam api-proxy's session.fork and the subagent fork provider use.
235
+ */
236
+ create?(options: unknown): Promise<{ agent: SidebarAgent; dispose(): Promise<void> }>
237
+ /**
238
+ * Resume an agent on a persisted session (mirror of the runtime
239
+ * AgentRegistry.resume) — the Side Chat cold-continuation seam after a
240
+ * DSH restart or a closed thread.
241
+ */
242
+ resume?(options: unknown): Promise<{ agent: SidebarAgent; dispose(): Promise<void> }>
243
+ }
244
+
245
+ /** The host subagent runtime face (`ctx.subagents`; optional — the live
246
+ * batch route degrades to a 503 when the deployment lacks it). Only the
247
+ * read-only descendant enumeration this plugin needs is mirrored. */
248
+ export interface SidebarSubagentsService {
249
+ /**
250
+ * Enumerate the root's complete session-backed subagent tree in stable
251
+ * pre-order without loading or resuming an Agent (mirror of
252
+ * `SubagentRuntime.listDescendants`).
253
+ */
254
+ listDescendants(
255
+ rootSessionId: string,
256
+ signal?: AbortSignal,
257
+ ): Promise<SidebarSubagentDescendantEntry[]>
258
+ }
259
+
260
+ /** One descendant row of `ctx.subagents.listDescendants` (structural mirror). */
261
+ export type SidebarSubagentDescendantEntry =
262
+ | {
263
+ kind: 'child'
264
+ id: string
265
+ activity: 'running' | 'inactive'
266
+ hasChildren: boolean
267
+ mode: 'one-shot' | 'continuable'
268
+ label?: string
269
+ parentId: string
270
+ depth: number
271
+ }
272
+ | {
273
+ kind: 'diagnostic'
274
+ id: string
275
+ reason: 'corrupt' | 'unsupported' | 'unavailable'
276
+ parentId: string
277
+ depth: number
278
+ }
279
+
280
+ /** The host agent-presets service face (mirror of the runtime agentPresets
281
+ * service): resolves and mounts the preset composition a session recorded,
282
+ * so a resumed or forked session rebuilds the same tool/prompt world its
283
+ * history was produced under. */
284
+ export interface SidebarAgentPresetsService {
285
+ /** Resolve a preset id; undefined resolves the deployment default. */
286
+ resolve(presetId?: string): Promise<{ id: string }>
287
+ /** Mount a preset's composition into an agent scope before publication. */
288
+ mount(agentCtx: unknown, presetId: string): Promise<void>
289
+ }
290
+
291
+ /** The host session-title service face (mirror of the sessionTitle service). */
292
+ export interface SidebarSessionTitleService {
293
+ /** Rename one live session's title (pins it against auto-regeneration). */
294
+ rename(session: unknown, title: string): { title: string; eventSeq: number }
295
+ }
296
+
297
+ /** The host session-persistence face (mirror of the sessionPersistence
298
+ * service): detached inspection of a persisted session, used to compose the
299
+ * recorded preset when a Side Chat thread cold-resumes. */
300
+ export interface SidebarSessionPersistenceService {
301
+ inspect(sessionId: string): Promise<{
302
+ meta: { cwd?: string; agentPreset?: string }
303
+ events: readonly SidebarSessionEvent[]
304
+ }>
305
+ }
306
+
307
+ /** The client session list snapshot the sidebar subscribes to. */
308
+ export interface SidebarSessionList {
309
+ current: string | undefined
310
+ byId: Record<string, SidebarSessionSummary>
311
+ /** Direct durable catalogs keyed by their selected parent address. */
312
+ subagentsByParent?: Readonly<Record<string, SidebarSubagentCatalog>>
313
+ /**
314
+ * Background jobs per session, last-wins from the harness's `session/jobs`
315
+ * push (a missing key is an empty set). Absent on runtime snapshots older
316
+ * than the jobs mirror — the sidebar simply shows no job rows.
317
+ */
318
+ jobsBySession?: Readonly<Record<string, readonly SidebarJobView[]>>
319
+ }
320
+
321
+ /** The client sessions service face (only the list feed is needed). */
322
+ export interface SidebarSessionsService {
323
+ list: {
324
+ getSnapshot(): SidebarSessionList
325
+ subscribe(fn: () => void): () => void
326
+ }
327
+ /**
328
+ * Select a listed session as current (mirror of the runtime ISessions.open)
329
+ * — used to jump back to the main agent from the topology root node.
330
+ */
331
+ open?(id: string): void
332
+ /**
333
+ * Fork a session from a completed-turn prefix of the source and resolve
334
+ * the child session id (mirror of the runtime ISessions.fork — throws on
335
+ * failure). The Side Chat "save as new session" action uses this to
336
+ * promote a hidden side thread into a top-level session.
337
+ */
338
+ fork?(opts: { sessionId: string; atSeq?: number; increaseTitle?: boolean }): Promise<string>
339
+ /**
340
+ * Resolve the stable session binding of one listed session (mirror of the
341
+ * runtime ISessions.binding); the saved-session rename uses the face's
342
+ * behavior verbs.
343
+ */
344
+ binding?(id: string): {
345
+ session: {
346
+ rename(title: string): Promise<unknown>
347
+ }
348
+ } | undefined
349
+ /**
350
+ * Resolve an Agent-scoped context view for one session (mirror of the
351
+ * runtime ISessions.scope) — the ticket `ctx.conversation.input.for`
352
+ * requires to reach that session's composer.
353
+ */
354
+ scope(id: string): Context | undefined
355
+ /**
356
+ * Open a healthy catalog child through its exact direct-parent address
357
+ * (mirror of the runtime ISessions.openSubagent).
358
+ */
359
+ openSubagent?(address: SidebarSubagentAddress): void
360
+ /**
361
+ * Resolve an already discovered direct-parent address without opening it.
362
+ */
363
+ subagentAddress?(id: string): SidebarSubagentAddress | undefined
364
+ /**
365
+ * Mark whether a catalog surface is consuming live membership updates.
366
+ */
367
+ setSubagentCatalogOpen?(parentSessionId: string, open: boolean): void
368
+ /**
369
+ * Refresh one direct-child catalog.
370
+ */
371
+ refreshSubagents?(parentSessionId: string): Promise<void>
372
+ }
373
+
374
+ /**
375
+ * The client locale service face (mirror of @deepseek-ai/dsh-client-locale's
376
+ * LocaleRuntime — only the slices the sidebar touches). The sidebar follows
377
+ * the DSH i18n system: the active locale is the Host-backed preference
378
+ * (`locale.preference` in settings.yaml) rather than the raw browser
379
+ * language, and the sidebar's zh/en dictionaries register into the service's
380
+ * namespace registry under `betterSidebar`.
381
+ */
382
+ export interface SidebarLocaleService {
383
+ /** Current immutable locale snapshot (uSES-safe; `active` is 'zh' | 'en' today). */
384
+ getSnapshot(): { active: string }
385
+ /** Subscribe to snapshot changes (locale switch or dictionary registration). */
386
+ subscribe(fn: () => void): () => void
387
+ /** Register one locale's dictionary for a namespace; returns the disposer. */
388
+ register(ns: string, locale: string, dict: Record<string, string>): () => void
389
+ }
390
+
391
+ /** The composer draft face the sidebar reaches through `ctx.conversation.input`. */
392
+ export interface SidebarSessionInput {
393
+ /** The live input store (draft read for append). */
394
+ state: {
395
+ getSnapshot(): { draft: string }
396
+ }
397
+ /** Replace the draft text (the input machine's single public write path). */
398
+ setDraft(text: string): void
399
+ }
400
+
401
+ /** The composer draft face the sidebar reaches through `ctx.get('conversation')`. */
402
+ export interface SidebarConversation {
403
+ input: {
404
+ for(actx: Context): SidebarSessionInput
405
+ }
406
+ }
407
+
408
+ /**
409
+ * The client workspaces service face (mirror of the runtime IWorkspaces). Only
410
+ * the chat's file-open funnel is touched: `openPath` hands an absolute path
411
+ * to the Host OS's default application, and every chat-side file open
412
+ * (tool rows, produced-files, prose mentions) funnels through it.
413
+ */
414
+ export interface SidebarWorkspacesService {
415
+ /** Open a filesystem path with the Host operating system's default application. */
416
+ openPath(path: string): Promise<void>
417
+ }
418
+
419
+ /**
420
+ * The invariant service face (mirror of @deepseek-ai/dsh-invariants'
421
+ * InvariantRegistry). The upstream augmentation does not reach this Context
422
+ * (dual-cordis-instance resolution), so the register signature is restated
423
+ * structurally, exactly like the other service faces above.
424
+ */
425
+ export interface SidebarInvariantsService {
426
+ /** Reserve one package's checks and install them in the service's child fiber. */
427
+ register(
428
+ packageName: string,
429
+ installer: (ctx: Context, fail: (message: string) => never) => void | Promise<void>,
430
+ ): () => void
431
+ }
432
+
433
+ /** The settings service face (mirror of @deepseek-ai/dsh-settings' SettingsProvider). */
434
+ export interface SidebarSettingsService {
435
+ /**
436
+ * Register one namespace schema (the resolved value layers schema defaults,
437
+ * then the composition base, then the user document).
438
+ */
439
+ register<T>(
440
+ ns: string,
441
+ schema: unknown,
442
+ options?: { base?: Partial<T>; applies?: 'live' | 'restart' },
443
+ ): {
444
+ get(): T
445
+ watch(callback: (next: T, prev: T) => void | Promise<void>): () => void
446
+ update(patch: object): Promise<void>
447
+ replace(section: object): Promise<void>
448
+ }
449
+ /** Redacted descriptors of every registered namespace (secrets stripped). */
450
+ describe(options?: { redactSecrets?: boolean }): Array<{
451
+ ns: string
452
+ value?: unknown
453
+ base?: unknown
454
+ user?: unknown
455
+ applies: 'live' | 'restart'
456
+ revision: number
457
+ }>
458
+ /** Service-level merge write with the revision guard (a stale writer is refused). */
459
+ update(ns: string, patch: object, expectedRevision?: number): Promise<void>
460
+ }
461
+
462
+ /**
463
+ * The tools service face (mirror of @deepseek-ai/dsh-tools' ToolRuntime).
464
+ * The host half registers model-facing tools here; the registry attaches the
465
+ * returned disposer to the contributing fiber so unloading unregisters them.
466
+ */
467
+ export interface SidebarToolsService {
468
+ /** Register one tool definition (raw JSON-Schema or defineTool-sugar form). */
469
+ register(tool: unknown): () => void
470
+ }
471
+
472
+ /**
473
+ * The agent face a tool sees on `exec.agent` (mirror of @deepseek-ai/dsh-agent's
474
+ * Agent). Only the slices the terminal tools touch are restated: the live
475
+ * session identity and its header cwd, both readonly.
476
+ */
477
+ export interface SidebarAgent {
478
+ /** The live session identity shared with the session log. */
479
+ readonly id: string
480
+ /** The live session this agent drives. */
481
+ readonly session: {
482
+ /** The session's header (validated cwd, lineage metadata). */
483
+ readonly header: { readonly cwd?: string }
484
+ }
485
+ }
486
+
487
+ /**
488
+ * The shape this plugin actually consumes, intersected with the vendored
489
+ * cordis `Context` below (see the file header for why intersection is used
490
+ * instead of module augmentation).
491
+ */
492
+ export interface SidebarContextShape {
493
+ /** The webServer service face this plugin uses. */
494
+ webServer: SidebarWebServer
495
+ /** The session store (host `.get`) and the client list feed (`.list`) faces. */
496
+ sessions: SidebarSessionStore & SidebarSessionsService
497
+ /** The web runtime trust list (bind-derived). */
498
+ webRuntime: SidebarWebRuntime
499
+ /** The client slot registry (register/inject). */
500
+ slots: SidebarSlotsService
501
+ /** The client workspaces service face (file-open funnel). */
502
+ workspaces: SidebarWorkspacesService
503
+ /** The settings service face (prefs persistence + namespace reads). */
504
+ settings: SidebarSettingsService
505
+ /** The invariant registry face. */
506
+ invariants: SidebarInvariantsService
507
+ /** The tool registry face. */
508
+ tools: SidebarToolsService
509
+ /** The client locale service face. */
510
+ locale: SidebarLocaleService
511
+ /** The client module system (rc.8+ chunk-loader externals). */
512
+ modules: { import(specifier: string): Promise<unknown> }
513
+ /** The host background-job registry (optional; routes degrade to 503). */
514
+ jobs: SidebarJobsService
515
+ /** The host live-agent registry (optional; side chat thread agents). */
516
+ agents: SidebarAgentsService
517
+ /** The host subagent runtime (optional; live topology batch route). */
518
+ subagents: SidebarSubagentsService
519
+ /** The host agent-presets service (optional; side chat cold resume). */
520
+ agentPresets: SidebarAgentPresetsService
521
+ /** The host session-title service (optional; side chat thread label pin). */
522
+ sessionTitle: SidebarSessionTitleService
523
+ /** The host session-persistence service (optional; side chat cold resume). */
524
+ sessionPersistence: SidebarSessionPersistenceService
525
+ /** The composer draft face (client ui-conversation, lazy `ctx.get` probe). */
526
+ conversation: SidebarConversation
527
+ /**
528
+ * The client-side sidebar registry: external plugins register tab types
529
+ * and file previewers here. Provided by the client half (see
530
+ * {@link ./client/index.tsx}); undefined on the host side.
531
+ */
532
+ betterSidebar: BetterSidebarService
533
+ /**
534
+ * String-keyed session feed subscribe (the vendored cordis `on` is keyed
535
+ * to its typed Events map; the harness session feed is a plain string
536
+ * event). The listener receives every appended session event with the
537
+ * LIVE Session instance that appended it.
538
+ */
539
+ on(event: string, listener: (session: unknown, event: SidebarSessionEvent) => void): () => void
540
+ }
541
+
542
+ /**
543
+ * The Context this plugin sees: the vendored cordis Context intersected with
544
+ * the structural service faces above. Re-exported from the package root so a
545
+ * consumer can `import type { Context } from 'dsh-better-sidebar'`.
546
+ */
547
+ export type Context = CordisContext & SidebarContextShape
548
+
549
+ /**
550
+ * Consumer-facing augmentation (deliberately the only one kept): a plugin
551
+ * that imports `Context` from `@deepseek-ai/cordis` and does
552
+ * `import type {} from 'dsh-better-sidebar'` sees `ctx.betterSidebar`
553
+ * without importing this package's own Context type.
554
+ */
555
+ declare module '@deepseek-ai/cordis' {
556
+ interface Context {
557
+ betterSidebar: BetterSidebarService
558
+ }
559
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Workspace-safe file mutations for the sidebar (the upload route today).
3
+ *
4
+ * Every write is confined to the real session workspace: the upload
5
+ * directory is resolved absolute and its target is checked through existing
6
+ * filesystem ancestors, the relative path is sanitized (absolute paths, '.',
7
+ * '..' and empty segments are refused), and the final target must stay inside
8
+ * the workspace after symlink resolution. Bytes stream from the request body
9
+ * to a uniquely named temp sibling
10
+ * and are renamed into place, so a failed, aborted, or oversized upload never
11
+ * leaves a partial file at the target path.
12
+ */
13
+ import { randomUUID } from 'node:crypto'
14
+ import { once } from 'node:events'
15
+ import { createWriteStream } from 'node:fs'
16
+ import { mkdir, rename, rm, stat } from 'node:fs/promises'
17
+ import { basename, dirname, join } from 'node:path'
18
+ import type { SidebarHttpRequest } from './context-types.ts'
19
+ import { requireAbsolute } from './fs-tree.ts'
20
+ import { ensureWorkspacePath, ensureWorkspaceWritePath } from './path-security.ts'
21
+ import { SidebarError } from './wire.ts'
22
+
23
+ /** Inputs of one upload: the session scope plus the request body stream. */
24
+ export interface WorkspaceUploadInput {
25
+ /** The session workspace root; target and directory must stay inside it. */
26
+ cwd: string
27
+ /** Absolute upload directory chosen by the client (inside `cwd`). */
28
+ dir: string
29
+ /** Relative path below `dir` (absolute paths, '.', '..' and empty segments refused). */
30
+ relativePath: string
31
+ /** The request body stream (raw bytes). */
32
+ chunks: AsyncIterable<string | Uint8Array>
33
+ /** Byte cap; an oversized upload is refused without touching the target. */
34
+ limit: number
35
+ /** Whether workspace containment is enforced (the `workspaceFence` setting; on by default). */
36
+ fence?: boolean
37
+ }
38
+
39
+ /**
40
+ * Stream `chunks` into `dir/relativePath` atomically: a uniquely named temp
41
+ * sibling receives the bytes, then is renamed over the target. The parent
42
+ * directory is created on demand (recursive), so folder uploads work before
43
+ * any level exists. The unique temp name keeps concurrent uploads to the same
44
+ * target independent (each writes and renames its own file; the last rename
45
+ * wins) and never blocks later uploads after a crashed process.
46
+ *
47
+ * @throws SidebarError with a wire code for containment, shape, and size
48
+ * failures; the temp file is always removed on failure.
49
+ */
50
+ export async function writeWorkspaceUpload(input: WorkspaceUploadInput): Promise<{ path: string; size: number }> {
51
+ const { cwd, dir, relativePath, chunks, limit, fence = true } = input
52
+ const base = requireAbsolute(dir)
53
+ await ensureWorkspacePath(cwd, base, fence)
54
+ if (relativePath === '' || relativePath.startsWith('/') || relativePath.startsWith('\\')) {
55
+ throw new SidebarError('bad-request', 'relativePath must stay below the upload directory', 400)
56
+ }
57
+ const segments = relativePath.split(/[\\/]/)
58
+ if (segments.some(part => part === '' || part === '.' || part === '..')) {
59
+ throw new SidebarError('bad-request', 'relativePath must stay below the upload directory', 400)
60
+ }
61
+ const target = join(base, ...segments)
62
+ const safeTarget = await ensureWorkspaceWritePath(cwd, target, fence)
63
+ const tmp = join(dirname(safeTarget), `.${basename(safeTarget)}.dsh-upload-${randomUUID()}.tmp`)
64
+ await mkdir(dirname(safeTarget), { recursive: true })
65
+ const stream = createWriteStream(tmp, { flags: 'wx' })
66
+ // Resolves once the stream fully closes; created up front so a stream that
67
+ // already closed (successful end, later failure) cannot leave the wait hanging.
68
+ const closed = new Promise<void>((resolve) => { stream.once('close', () => resolve()) })
69
+ let size = 0
70
+ let streamError: unknown
71
+ // A permanent 'error' listener keeps a failing disk from crashing the host:
72
+ // every await below surfaces the failure through the promise chain instead.
73
+ stream.on('error', (error) => { streamError = error })
74
+ try {
75
+ for await (const chunk of chunks) {
76
+ const buffer = Buffer.from(chunk)
77
+ size += buffer.length
78
+ if (size > limit) throw new SidebarError('too-large', `upload exceeds the ${limit} byte limit`, 413)
79
+ if (!stream.write(buffer)) await once(stream, 'drain')
80
+ if (streamError !== undefined) throw streamError
81
+ }
82
+ await new Promise<void>((resolve, reject) => {
83
+ stream.end((error?: Error | null) => (error === undefined || error === null ? resolve() : reject(error)))
84
+ })
85
+ if (streamError !== undefined) throw streamError
86
+ await rename(tmp, safeTarget)
87
+ const info = await stat(safeTarget)
88
+ return { path: target, size: info.size }
89
+ } catch (error) {
90
+ // Wait for the stream to fully close before unlinking (Windows locks open
91
+ // files), then remove our own uniquely named temp file.
92
+ stream.destroy()
93
+ await closed.catch(() => {})
94
+ await rm(tmp, { force: true }).catch(() => {})
95
+ throw error
96
+ }
97
+ }