@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,421 @@
1
+ /**
2
+ * dsh-ui-tweaks — Git Web routes.
3
+ *
4
+ * Same-origin JSON endpoints backing the GitBar browser half. Every mutating
5
+ * action is a POST guarded by the same origin/same-site checks as the Settings
6
+ * route; the session id arrives as a query/body field and the backend resolves
7
+ * the working directory from the session header.
8
+ *
9
+ * The terminal panel rides two extra surfaces on the same prefix:
10
+ * - `GET /vendor/<file>` serves the vendored xterm.js UMD builds the browser
11
+ * half lazy-loads on first panel open (whitelist only — no path traversal).
12
+ * - `WS /terminal-ws` upgrades into the persistent PTY bridge: server→client
13
+ * frames are RAW pty output (plus one JSON `{"type":"exit"}` notice),
14
+ * client→server frames are JSON (`input` / `resize` / `close`) so a pasted
15
+ * brace can never be mistaken for a control frame.
16
+ * @module dsh-ui-tweaks/git-web
17
+ */
18
+
19
+ import type { IncomingMessage, ServerResponse } from 'node:http'
20
+ import { readFileSync } from 'node:fs'
21
+ import type { Duplex } from 'node:stream'
22
+ import type { Context } from '@deepseek-ai/cordis'
23
+ // Type-only import activates the optional webServer Context declaration.
24
+ import type {} from '@deepseek-ai/dsh-host-webserver'
25
+ import { WebSocketServer, WebSocket } from 'ws'
26
+ import { sameOriginPost } from './web.ts'
27
+ import { GitBackend, type TerminalClient } from './git.ts'
28
+
29
+ /** Route prefix shared with the browser half. */
30
+ export const GIT_ROUTE = '/_dsh/ui-tweaks/git'
31
+
32
+ /** Exact path of the terminal WebSocket upgrade route. */
33
+ export const TERMINAL_WS_PATH = `${GIT_ROUTE}/terminal-ws`
34
+
35
+ const MAX_BODY_BYTES = 64 * 1024
36
+
37
+ /** Vendored browser assets served from the plugin's `vendor/` directory. */
38
+ const VENDOR_FILES: Record<string, { type: string }> = {
39
+ 'xterm.js': { type: 'text/javascript; charset=utf-8' },
40
+ 'addon-fit.js': { type: 'text/javascript; charset=utf-8' },
41
+ 'xterm.css': { type: 'text/css; charset=utf-8' },
42
+ }
43
+
44
+ function json<T>(res: ServerResponse, status: number, body: { ok: true; value: T } | { ok: false; error: { code: string; message: string } }): void {
45
+ const bytes = Buffer.from(JSON.stringify(body))
46
+ res.setHeader('Content-Type', 'application/json; charset=utf-8')
47
+ res.setHeader('Content-Length', String(bytes.length))
48
+ res.setHeader('Cache-Control', 'no-store')
49
+ res.setHeader('X-Content-Type-Options', 'nosniff')
50
+ res.writeHead(status)
51
+ res.end(bytes)
52
+ }
53
+
54
+ function ok<T>(res: ServerResponse, value: T): void {
55
+ json(res, 200, { ok: true, value })
56
+ }
57
+
58
+ function fail(res: ServerResponse, status: number, code: string, message: string): void {
59
+ json(res, status, { ok: false, error: { code, message } })
60
+ }
61
+
62
+ function messageOf(error: unknown): string {
63
+ if (error instanceof Error) return error.message
64
+ return String(error)
65
+ }
66
+
67
+ /** Read a bounded JSON POST body. */
68
+ async function readJson(req: IncomingMessage): Promise<unknown> {
69
+ const contentType = req.headers['content-type']?.split(';', 1)[0]?.trim().toLowerCase()
70
+ if (contentType !== 'application/json') throw new TypeError('Content-Type must be application/json')
71
+ const chunks: Buffer[] = []
72
+ let bytes = 0
73
+ for await (const chunk of req) {
74
+ const part = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
75
+ bytes += part.length
76
+ if (bytes > MAX_BODY_BYTES) throw new RangeError('request body too large')
77
+ chunks.push(part)
78
+ }
79
+ if (chunks.length === 0) throw new TypeError('request body is empty')
80
+ return JSON.parse(Buffer.concat(chunks).toString('utf8')) as unknown
81
+ }
82
+
83
+ function isRecord(value: unknown): value is Record<string, unknown> {
84
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
85
+ }
86
+
87
+ function str(value: unknown): string | undefined {
88
+ return typeof value === 'string' && value !== '' ? value : undefined
89
+ }
90
+
91
+ /** URL-decoded session/workspace + file query params, with a sane file length cap. */
92
+ function queryParams(req: IncomingMessage): { session?: string | undefined; ws?: string | undefined; file?: string | undefined; mode?: string | undefined } {
93
+ const url = new URL(req.url ?? '/', 'http://localhost')
94
+ const file = url.searchParams.get('file')
95
+ return {
96
+ session: str(url.searchParams.get('session')),
97
+ ws: str(url.searchParams.get('ws')),
98
+ file: file !== null && file.length <= 4096 ? file : undefined,
99
+ mode: str(url.searchParams.get('mode')),
100
+ }
101
+ }
102
+
103
+ /** Validate that a POST body carries the expected string fields. */
104
+ function requireFields<K extends string>(body: unknown, fields: readonly K[]): Record<K, string> {
105
+ if (!isRecord(body)) throw new TypeError('request body must be an object')
106
+ const out = {} as Record<K, string>
107
+ for (const field of fields) {
108
+ const value = body[field]
109
+ if (typeof value !== 'string' || value === '') throw new TypeError(`field "${field}" must be a non-empty string`)
110
+ out[field] = value
111
+ }
112
+ return out
113
+ }
114
+
115
+ /**
116
+ * Same-origin Git endpoints. GETs are read-only; POSTs mutate the repository
117
+ * and reject cross-site requests.
118
+ */
119
+ export class GitWebHandler {
120
+ constructor(private readonly backend: GitBackend) {}
121
+
122
+ async handle(req: IncomingMessage, res: ServerResponse): Promise<void> {
123
+ const url = new URL(req.url ?? '/', 'http://localhost')
124
+ const path = url.pathname
125
+
126
+ if (req.method === 'GET') {
127
+ // Vendored xterm builds for the terminal panel (exact-name whitelist).
128
+ if (path.startsWith(`${GIT_ROUTE}/vendor/`)) {
129
+ this.serveVendor(res, path.slice(`${GIT_ROUTE}/vendor/`.length))
130
+ return
131
+ }
132
+
133
+ const { session, ws, file, mode } = queryParams(req)
134
+
135
+ try {
136
+ const cwd = this.backend.resolveTargetCwd({ session, ws })
137
+ if (cwd === undefined) {
138
+ // Distinguish "target not resolvable YET" (right after a restart the
139
+ // host has not materialized its session/workspace store, so even a
140
+ // valid session id has no cwd) from a genuine non-repo directory.
141
+ // The browser half chases the former with a short backoff instead of
142
+ // caching it as final and sitting blank for a whole poll period.
143
+ ok(res, { isRepo: false, unresolved: true })
144
+ return
145
+ }
146
+ if (path === `${GIT_ROUTE}/status`) {
147
+ ok(res, await this.backend.snapshot(cwd))
148
+ return
149
+ }
150
+ if (path === `${GIT_ROUTE}/branches`) {
151
+ ok(res, await this.backend.branches(cwd))
152
+ return
153
+ }
154
+ if (path === `${GIT_ROUTE}/diff`) {
155
+ if (file === undefined) throw new TypeError('file query parameter is required')
156
+ if (mode !== 'hunk' && mode !== 'full') throw new TypeError('mode must be "hunk" or "full"')
157
+ ok(res, await this.backend.diff(cwd, file, mode))
158
+ return
159
+ }
160
+ if (path === `${GIT_ROUTE}/graph`) {
161
+ const raw = url.searchParams.get('limit')
162
+ const limit = raw === null ? undefined : Number(raw)
163
+ ok(res, await this.backend.graph(cwd, limit))
164
+ return
165
+ }
166
+ } catch (error) {
167
+ fail(res, 400, 'git-request-failed', messageOf(error))
168
+ return
169
+ }
170
+ fail(res, 404, 'not-found', 'unknown git endpoint')
171
+ return
172
+ }
173
+
174
+ if (req.method !== 'POST') {
175
+ res.setHeader('Allow', 'GET, POST')
176
+ fail(res, 405, 'method-not-allowed', 'Use GET or POST')
177
+ return
178
+ }
179
+ if (!sameOriginPost(req)) {
180
+ fail(res, 403, 'origin-rejected', 'The request must originate from this ChatCode CLI Web application')
181
+ return
182
+ }
183
+
184
+ let body: unknown
185
+ try {
186
+ body = await readJson(req)
187
+ } catch (error) {
188
+ fail(res, error instanceof RangeError ? 413 : 400, 'invalid-request', messageOf(error))
189
+ return
190
+ }
191
+ const session = isRecord(body) ? str(body.session) : undefined
192
+ const ws = isRecord(body) ? str(body.ws) : undefined
193
+
194
+ try {
195
+ if (path === `${GIT_ROUTE}/open`) {
196
+ const fields = requireFields(body, ['target'])
197
+ if (fields.target !== 'explorer' && fields.target !== 'vscode' && fields.target !== 'idea'
198
+ && fields.target !== 'goland' && fields.target !== 'webstorm' && fields.target !== 'pycharm') {
199
+ throw new TypeError('target must be "explorer", "vscode", "idea", "goland", "webstorm" or "pycharm"')
200
+ }
201
+ const cwd = this.requireCwd({ session, ws })
202
+ await this.backend.openFolder(cwd, fields.target as 'explorer' | 'vscode' | 'idea' | 'goland' | 'webstorm' | 'pycharm')
203
+ ok(res, { opened: fields.target })
204
+ return
205
+ }
206
+ if (path === `${GIT_ROUTE}/commit`) {
207
+ const fields = requireFields(body, ['message'])
208
+ const push = isRecord(body) && body.push === true
209
+ const exclude = isRecord(body) && Array.isArray(body.exclude)
210
+ ? body.exclude.filter((entry): entry is string => typeof entry === 'string' && entry !== '')
211
+ : undefined
212
+ const cwd = this.requireCwd({ session, ws })
213
+ ok(res, await this.backend.commit(cwd, fields.message, {
214
+ push,
215
+ ...(exclude !== undefined && exclude.length > 0 ? { exclude } : {}),
216
+ }))
217
+ return
218
+ }
219
+ if (path === `${GIT_ROUTE}/push`) {
220
+ const cwd = this.requireCwd({ session, ws })
221
+ await this.backend.push(cwd)
222
+ ok(res, { pushed: true })
223
+ return
224
+ }
225
+ if (path === `${GIT_ROUTE}/checkout`) {
226
+ const fields = requireFields(body, ['branch'])
227
+ const cwd = this.requireCwd({ session, ws })
228
+ await this.backend.checkout(cwd, fields.branch)
229
+ ok(res, { branch: fields.branch })
230
+ return
231
+ }
232
+ if (path === `${GIT_ROUTE}/create`) {
233
+ const fields = requireFields(body, ['name'])
234
+ const base = isRecord(body) ? str(body.base) : undefined
235
+ const pushRemote = isRecord(body) && body.push === true
236
+ const cwd = this.requireCwd({ session, ws })
237
+ await this.backend.createBranch(cwd, fields.name, base, pushRemote)
238
+ ok(res, { branch: fields.name, pushed: pushRemote })
239
+ return
240
+ }
241
+ if (path === `${GIT_ROUTE}/branch-delete`) {
242
+ const fields = requireFields(body, ['name'])
243
+ const cwd = this.requireCwd({ session, ws })
244
+ await this.backend.deleteBranch(cwd, fields.name)
245
+ ok(res, { branch: fields.name })
246
+ return
247
+ }
248
+ if (path === `${GIT_ROUTE}/remote-delete`) {
249
+ const fields = requireFields(body, ['name'])
250
+ const cwd = this.requireCwd({ session, ws })
251
+ await this.backend.deleteRemoteBranch(cwd, fields.name)
252
+ ok(res, { branch: fields.name })
253
+ return
254
+ }
255
+ if (path === `${GIT_ROUTE}/suggest`) {
256
+ const cwd = this.requireCwd({ session, ws })
257
+ // `via` / `reason` let the UI say when the offline heuristic wrote the
258
+ // message; `message` stays the first-class field.
259
+ ok(res, await this.backend.suggestDetailed(cwd))
260
+ return
261
+ }
262
+ } catch (error) {
263
+ fail(res, 400, 'git-action-failed', messageOf(error))
264
+ return
265
+ }
266
+ fail(res, 404, 'not-found', 'unknown git endpoint')
267
+ }
268
+
269
+ private requireCwd(target: { session?: string | undefined; ws?: string | undefined }): string {
270
+ const cwd = this.backend.resolveTargetCwd(target)
271
+ if (cwd === undefined) throw new Error('no working directory for this session')
272
+ return cwd
273
+ }
274
+
275
+ /** Serve one whitelisted vendored asset; anything else is a 404. */
276
+ private serveVendor(res: ServerResponse, name: string): void {
277
+ const entry = VENDOR_FILES[name]
278
+ if (entry === undefined) {
279
+ fail(res, 404, 'not-found', 'unknown vendor asset')
280
+ return
281
+ }
282
+ try {
283
+ // import.meta.url = <packageRoot>/lib/git-web.js → ../vendor = package
284
+ // root's vendor/ directory (ships in the npm `files` list).
285
+ const body = readFileSync(new URL(`../vendor/${name}`, import.meta.url))
286
+ res.setHeader('Content-Type', entry.type)
287
+ res.setHeader('Content-Length', String(body.length))
288
+ res.setHeader('Cache-Control', 'public, max-age=86400')
289
+ res.setHeader('X-Content-Type-Options', 'nosniff')
290
+ res.writeHead(200)
291
+ res.end(body)
292
+ } catch (error) {
293
+ fail(res, 500, 'vendor-read-failed', messageOf(error))
294
+ }
295
+ }
296
+ }
297
+
298
+ /** Registry key for one target's terminal: `session:<id>` or `ws:<name>`. */
299
+ function terminalKeyOf(session: string | undefined, ws: string | undefined): string | undefined {
300
+ if (session !== undefined) return `session:${session}`
301
+ if (ws !== undefined) return `ws:${ws}`
302
+ return undefined
303
+ }
304
+
305
+ /** Clamp an integer query param into [min, max], falling back to `fallback`. */
306
+ function intParam(url: URL, name: string, fallback: number, min: number, max: number): number {
307
+ const raw = url.searchParams.get(name)
308
+ const parsed = raw === null ? Number.NaN : Number(raw)
309
+ if (!Number.isInteger(parsed) || parsed < min || parsed > max) return fallback
310
+ return parsed
311
+ }
312
+
313
+ /**
314
+ * Attach the Git routes plus the terminal WebSocket whenever a webServer
315
+ * service is present.
316
+ * @param ctx - plugin context owning route effects.
317
+ * @param backend - Git backend.
318
+ */
319
+ export function installGitWeb(ctx: Context, backend: GitBackend): void {
320
+ ctx.inject(['webServer'], (webCtx) => {
321
+ webCtx.effect(() => {
322
+ const handler = new GitWebHandler(backend)
323
+ return webCtx.webServer.register({
324
+ kind: 'prefix',
325
+ path: GIT_ROUTE,
326
+ handler: (req, res) => handler.handle(req, res),
327
+ })
328
+ }, 'dsh-ui-tweaks: Git routes')
329
+
330
+ // ── Terminal WebSocket ────────────────────────────────────────────────
331
+ // One upgrade endpoint bridges xterm.js in the browser to the target's
332
+ // persistent PTY. Wire protocol (mirrors dsh-better-sidebar):
333
+ // server → client: raw pty output chunks; one JSON {"type":"exit"} line
334
+ // when the shell dies.
335
+ // client → server: JSON frames only — {"type":"input","data":...},
336
+ // {"type":"resize",cols,rows}, {"type":"close"}.
337
+ // On connect the host replays the session's transcript ring before live
338
+ // data, so a panel reopen / page refresh restores the exact screen.
339
+ webCtx.effect(() => {
340
+ const wss = new WebSocketServer({ noServer: true })
341
+ return webCtx.webServer.registerUpgrade({
342
+ path: TERMINAL_WS_PATH,
343
+ handler: (req, socket, head) => {
344
+ const url = new URL(req.url ?? '/', 'http://localhost')
345
+ const session = str(url.searchParams.get('session'))
346
+ const wsName = str(url.searchParams.get('ws'))
347
+ const key = terminalKeyOf(session, wsName)
348
+ const cwd = key === undefined ? undefined : backend.resolveTargetCwd({ session, ws: wsName })
349
+ if (key === undefined || cwd === undefined) {
350
+ socket.destroy()
351
+ return
352
+ }
353
+ wss.handleUpgrade(req as IncomingMessage, socket as unknown as Duplex, head as Buffer, (ws) => {
354
+ attachTerminalSocket(backend, key, cwd, {
355
+ cols: intParam(url, 'cols', 80, 2, 500),
356
+ rows: intParam(url, 'rows', 24, 2, 200),
357
+ }, ws)
358
+ })
359
+ },
360
+ })
361
+ }, 'dsh-ui-tweaks: terminal WebSocket')
362
+
363
+ webCtx.effect(() => () => {
364
+ backend.terminals.disposeAll()
365
+ }, 'dsh-ui-tweaks: terminal teardown')
366
+ })
367
+ }
368
+
369
+ /** Bridge one upgraded browser socket to the target's PTY session. */
370
+ function attachTerminalSocket(backend: GitBackend, key: string, cwd: string, dims: { cols: number; rows: number }, ws: WebSocket): void {
371
+ if (backend.terminals.unavailable) {
372
+ ws.close(1011, 'pty-unavailable')
373
+ return
374
+ }
375
+ const client: TerminalClient = {
376
+ send: (text) => { ws.send(text) },
377
+ get alive(): boolean { return ws.readyState === WebSocket.OPEN },
378
+ }
379
+ let session
380
+ try {
381
+ session = backend.terminals.attach(key, cwd, dims.cols, dims.rows, client)
382
+ } catch (error) {
383
+ ws.close(1011, messageOf(error).slice(0, 120))
384
+ return
385
+ }
386
+ // Replay what the shell printed before this socket attached (a fresh spawn
387
+ // has an empty ring). Synchronous, so it lands ahead of any live chunk.
388
+ if (session.transcript !== '') ws.send(session.transcript)
389
+
390
+ ws.on('message', (raw) => {
391
+ const text = typeof raw === 'string' ? raw : raw.toString('utf8')
392
+ if (text.startsWith('{')) {
393
+ try {
394
+ const frame = JSON.parse(text) as { type?: unknown; data?: unknown; cols?: unknown; rows?: unknown }
395
+ if (frame.type === 'input' && typeof frame.data === 'string') {
396
+ backend.terminals.input(key, frame.data)
397
+ return
398
+ }
399
+ if (frame.type === 'resize' && typeof frame.cols === 'number' && typeof frame.rows === 'number') {
400
+ backend.terminals.resize(key, frame.cols, frame.rows)
401
+ return
402
+ }
403
+ if (frame.type === 'close') {
404
+ backend.terminals.close(key)
405
+ ws.close(1000)
406
+ return
407
+ }
408
+ } catch {
409
+ // Malformed JSON that starts with '{' falls through as literal input —
410
+ // a user pasting braces into the shell must still work.
411
+ }
412
+ }
413
+ backend.terminals.input(key, text)
414
+ })
415
+ ws.on('close', () => {
416
+ backend.terminals.detach(key, client)
417
+ })
418
+ ws.on('error', () => {
419
+ // 'close' always follows; nothing else to clean here.
420
+ })
421
+ }