@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,113 @@
1
+ /**
2
+ * Recursive file-name search for the editor's merged-mode side panel.
3
+ * Streams the tree with opendir and matches the query as a case-insensitive
4
+ * substring of each entry's NAME (paths stay relative to the search root —
5
+ * the client resolves them against the session cwd). No .gitignore semantics
6
+ * (this is a name lookup, not a code search), but known noise directories
7
+ * (`.git`, `node_modules`, package-manager stores, build caches) are
8
+ * skipped outright and symlink directories are NOT descended (cycle safety).
9
+ *
10
+ * Two performance budgets bound the walk: `maxMatches` (the client renders
11
+ * the flat list) and `maxVisited` (a runaway tree — a home directory root
12
+ * — must not stall the host). Exceeding either stops early with
13
+ * `truncated: true`.
14
+ */
15
+ import { opendir } from 'node:fs/promises'
16
+ import { join, relative, sep } from 'node:path'
17
+
18
+ /** One search: the relative paths of the matching entries (dirs included so
19
+ * the client can hint where matches live) plus the truncation flag. */
20
+ export interface FsSearchResult {
21
+ matches: string[]
22
+ truncated: boolean
23
+ }
24
+
25
+ /** Search budgets (both injectable for tests). */
26
+ export interface FsSearchOptions {
27
+ /** Row cap of the result list (default 200). */
28
+ maxMatches?: number
29
+ /** Total entries visited before the walk gives up (default 100_000). */
30
+ maxVisited?: number
31
+ }
32
+
33
+ const DEFAULT_MAX_MATCHES = 200
34
+ const DEFAULT_MAX_VISITED = 100_000
35
+
36
+ /**
37
+ * Directory names that are never useful filename-search results and would
38
+ * burn the visit budget before the walk reaches project files. Compared
39
+ * case-insensitively so `Node_Modules` / `.GIT` stay skipped on every
40
+ * platform. The directory itself is neither matched nor descended.
41
+ */
42
+ const SEARCH_SKIP_DIRS = new Set([
43
+ '.git',
44
+ 'node_modules',
45
+ '.pnpm-store',
46
+ '.yarn',
47
+ '.turbo',
48
+ '.turbopack',
49
+ '.next',
50
+ '.nuxt',
51
+ '.output',
52
+ '.cache',
53
+ '.parcel-cache',
54
+ 'coverage',
55
+ 'dist',
56
+ 'build',
57
+ 'out',
58
+ '.umi',
59
+ '.umi-production',
60
+ '.dumi',
61
+ ])
62
+
63
+ /**
64
+ * Search `root` recursively for entries whose name contains `query`
65
+ * (case-insensitive).
66
+ * @param root - absolute search root.
67
+ * @param query - the name substring; empty matches nothing.
68
+ * @param opts - budget overrides (tests).
69
+ * @returns the matching paths RELATIVE to `root` ('/'-separated), sorted,
70
+ * plus whether a budget cut the walk short. An unreadable level is skipped
71
+ * (permission errors never fail the whole search).
72
+ */
73
+ export async function searchFiles(root: string, query: string, opts: FsSearchOptions = {}): Promise<FsSearchResult> {
74
+ const needle = query.trim().toLowerCase()
75
+ if (needle === '') return { matches: [], truncated: false }
76
+ const maxMatches = opts.maxMatches ?? DEFAULT_MAX_MATCHES
77
+ const maxVisited = opts.maxVisited ?? DEFAULT_MAX_VISITED
78
+
79
+ const matches: string[] = []
80
+ let visited = 0
81
+ let truncated = false
82
+
83
+ const walk = async (dir: string): Promise<void> => {
84
+ if (truncated) return
85
+ const level = await opendir(dir).catch(() => undefined)
86
+ if (level === undefined) return
87
+ for await (const dirent of level) {
88
+ visited += 1
89
+ if (visited > maxVisited) {
90
+ truncated = true
91
+ return
92
+ }
93
+ // Dependency / VCS / build-output forests: never matched, never descended.
94
+ if (dirent.isDirectory() && SEARCH_SKIP_DIRS.has(dirent.name.toLowerCase())) continue
95
+ if (dirent.name.toLowerCase().includes(needle)) {
96
+ matches.push(join(relative(root, dir), dirent.name))
97
+ if (matches.length >= maxMatches) {
98
+ truncated = true
99
+ return
100
+ }
101
+ }
102
+ // Descend real directories only: a symlinked directory may point back
103
+ // up the tree (cycle).
104
+ if (dirent.isDirectory() && !dirent.isSymbolicLink()) {
105
+ await walk(join(dir, dirent.name))
106
+ if (truncated) return
107
+ }
108
+ }
109
+ }
110
+ await walk(root)
111
+ // '/' separators on every platform: the client joins onto the cwd itself.
112
+ return { matches: matches.sort().map(path => path.split(sep).join('/')), truncated }
113
+ }
package/src/fs-tree.ts ADDED
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Single-level directory listing for the sidebar explorer. Streams the level
3
+ * with opendir, sorts directories first then names (case-insensitive), and
4
+ * marks POSIX-hidden entries (dot-prefixed) for dimmed display. Symlinks are
5
+ * stat'ed once to expose their target kind — a symlink to a directory
6
+ * expands like a directory — and dangling links are flagged broken. The
7
+ * probe runs only for entries that are actually symlinks, so levels without
8
+ * links stay as cheap as before.
9
+ */
10
+ import { opendir, stat } from 'node:fs/promises'
11
+ import { basename, dirname, isAbsolute, join, resolve } from 'node:path'
12
+ import { SidebarError } from './wire.ts'
13
+
14
+ /** One explorer row. */
15
+ export interface SidebarFsEntry {
16
+ name: string
17
+ path: string
18
+ isDir: boolean
19
+ hidden: boolean
20
+ /** Whether the row is a symlink; `isDir` then describes the link's target. */
21
+ isSymlink: boolean
22
+ /** For symlinks: the target is missing or unreadable (stat failed). */
23
+ broken: boolean
24
+ }
25
+
26
+ /** One listed level. */
27
+ export interface SidebarFsListing {
28
+ path: string
29
+ entries: SidebarFsEntry[]
30
+ truncated: boolean
31
+ }
32
+
33
+ /** Directory-first, case-insensitive name ordering (VSCode explorer order). */
34
+ export function compareEntries(a: SidebarFsEntry, b: SidebarFsEntry): number {
35
+ if (a.isDir !== b.isDir) return a.isDir ? -1 : 1
36
+ return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })
37
+ }
38
+
39
+ /**
40
+ * List one directory level.
41
+ * @param path - absolute directory path.
42
+ * @param maxEntries - row bound of one level (extra rows flag `truncated`).
43
+ * @returns the sorted listing.
44
+ * @throws {SidebarError} fs-error when the level is unreadable or not a directory.
45
+ */
46
+ export async function listDirectory(path: string, maxEntries = 1000): Promise<SidebarFsListing> {
47
+ let level
48
+ try {
49
+ level = await opendir(path)
50
+ } catch (error) {
51
+ throw new SidebarError('fs-error', `cannot list "${path}": ${messageOf(error)}`, 400)
52
+ }
53
+ const rows: SidebarFsEntry[] = []
54
+ let overflow = 0
55
+ try {
56
+ for await (const dirent of level) {
57
+ if (rows.length >= maxEntries) {
58
+ overflow += 1
59
+ continue
60
+ }
61
+ // Platform join: on Windows the level path uses '\' — a hardcoded '/'
62
+ // would leak mixed separators into every row's path.
63
+ rows.push({
64
+ name: dirent.name,
65
+ path: join(path, dirent.name),
66
+ isDir: dirent.isDirectory(),
67
+ isSymlink: dirent.isSymbolicLink(),
68
+ broken: false,
69
+ hidden: dirent.name.startsWith('.'),
70
+ })
71
+ }
72
+ } catch (error) {
73
+ throw new SidebarError('fs-error', `cannot list "${path}": ${messageOf(error)}`, 400)
74
+ }
75
+ // Probe symlink targets AFTER the readdir stream closes, with bounded
76
+ // concurrency: a symlink-heavy level (UNC/network targets) would otherwise
77
+ // serialize up to maxEntries stat calls and stall the explorer. Non-symlink
78
+ // rows are skipped by the probe, so levels without links stay as cheap as
79
+ // before.
80
+ await probeSymlinkTargets(rows)
81
+ rows.sort(compareEntries)
82
+ return { path, entries: rows, truncated: overflow > 0 }
83
+ }
84
+
85
+ /** How many symlink target stats run in flight during one level listing. */
86
+ const SYMLINK_PROBE_CONCURRENCY = 32
87
+
88
+ /** Probe each symlink row's target once (bounded concurrency, order-preserving). */
89
+ async function probeSymlinkTargets(rows: SidebarFsEntry[], concurrency = SYMLINK_PROBE_CONCURRENCY): Promise<void> {
90
+ let next = 0
91
+ const workers = Array.from({ length: Math.min(concurrency, rows.length) }, async () => {
92
+ for (;;) {
93
+ const index = next
94
+ next += 1
95
+ if (index >= rows.length) return
96
+ const row = rows[index]!
97
+ if (!row.isSymlink) continue
98
+ // stat follows the chain; any failure (missing target, ELOOP, permission)
99
+ // leaves the row as a broken file-shaped link the editor refuses to read.
100
+ const info = await stat(row.path).catch(() => undefined)
101
+ row.isDir = info !== undefined ? info.isDirectory() : row.isDir
102
+ row.broken = info === undefined
103
+ }
104
+ })
105
+ await Promise.all(workers)
106
+ }
107
+
108
+ /** The root row label of a listing: the last path segment (or the full path at the filesystem root). */
109
+ export function rootLabel(path: string): string {
110
+ const base = basename(path)
111
+ return base !== '' ? base : path
112
+ }
113
+
114
+ /** Parent of a path, or undefined at the filesystem root (the explorer's "up" target). */
115
+ export function parentOf(path: string): string | undefined {
116
+ const parent = dirname(path)
117
+ return parent === path ? undefined : parent
118
+ }
119
+
120
+ /**
121
+ * Normalize a caller-supplied path to an absolute, resolved path or throw
122
+ * fs-error. `path.isAbsolute()` is the OS's own notion of absolute: POSIX
123
+ * roots (`/...`), Windows drive letters (`C:\...`) and — on win32 — UNC
124
+ * network shares (`\\server\share\...`); drive-relative forms (`C:foo`)
125
+ * stay rejected.
126
+ */
127
+ export function requireAbsolute(path: string): string {
128
+ if (!isAbsolute(path)) {
129
+ throw new SidebarError('fs-error', `"${path}" is not an absolute path`, 400)
130
+ }
131
+ return resolve(path)
132
+ }
133
+
134
+ /**
135
+ * Whether `target` lies under `base` (or equals it), tolerant of separator
136
+ * style and — on Windows, where the filesystem is case-insensitive — of
137
+ * letter case. The media route uses this instead of a raw `startsWith` so a
138
+ * case-mismatched or mixed-separator path can never be misclassified
139
+ * (e.g. `C:\Users\Me` vs `c:/users/me/file.png`).
140
+ * @param platform - filesystem semantics; injectable so both branches are
141
+ * unit-testable on any host.
142
+ */
143
+ export function isWithin(base: string, target: string, platform: NodeJS.Platform = process.platform): boolean {
144
+ const norm = (value: string): string => value.replace(/[\\/]+/g, '/').replace(/\/$/, '')
145
+ const b = norm(base)
146
+ const t = norm(target)
147
+ if (platform === 'win32') {
148
+ const lb = b.toLowerCase()
149
+ const lt = t.toLowerCase()
150
+ return lt === lb || lt.startsWith(`${lb}/`)
151
+ }
152
+ return t === b || t.startsWith(`${b}/`)
153
+ }
154
+
155
+ /** Message text of an unknown thrown value. */
156
+ export function messageOf(error: unknown): string {
157
+ return error instanceof Error ? error.message : String(error)
158
+ }