@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,55 @@
1
+ /**
2
+ * The memo comparator for one tab's content cell (TabContent in Sidebar.tsx).
3
+ *
4
+ * Kept as a pure module so the "what re-renders a tab cell" contract is unit
5
+ * testable: geometry/store re-renders of the Sidebar shell must NOT reconcile
6
+ * every mounted tab (xterm/CodeMirror/FileTree subtrees — issue #315), while
7
+ * locale switches (localeRevision), tab-registry updates (tabsVersion), and
8
+ * a tab MOVING BETWEEN PANES (paneId — onOpenDiff closes over the pane id,
9
+ * and moveTab reuses the SAME tab object, so only the pane id identifies the
10
+ * move) must re-render the cell.
11
+ */
12
+ import type { SidebarTab } from './state.ts'
13
+
14
+ /** The fields whose change invalidates a tab cell's memo. Anything a cell's
15
+ * render (or its callbacks) depends on must be listed here. */
16
+ export interface TabContentMemoKey {
17
+ tab: SidebarTab
18
+ /** The pane this tab currently lives in. onOpenDiff closes over it, and
19
+ * moveTab reuses the same tab object when the tab moves panes — without
20
+ * comparing paneId the cell would keep the source pane's callback. */
21
+ paneId: string
22
+ sessionId: string
23
+ cwd: string | undefined
24
+ visible: boolean
25
+ expanded: string[]
26
+ /** Files highlighted in the file tree (the "Show in folder" reveal). */
27
+ revealed: string[]
28
+ localeRevision: string
29
+ tabsVersion: number
30
+ /** When the tab is a pinned virtual tab (injected from another session),
31
+ * this overrides `tab.id` passed to the tab descriptor's component so
32
+ * TerminalView connects to the home session's PTY by the original id.
33
+ * Undefined for regular tabs (no override). */
34
+ effectiveTabId: string | undefined
35
+ }
36
+
37
+ /** True when the cell may skip a re-render (all render-affecting fields
38
+ * unchanged). Callback/context identities are deliberately ignored: their
39
+ * captured dependencies are stable or covered by the compared fields
40
+ * (onReferenceFile → sessionId/cwd, onSubagentJump/onToggleDir → stable
41
+ * refs/closures, onOpenDiff → paneId). */
42
+ export function tabContentCompare(prev: TabContentMemoKey, next: TabContentMemoKey): boolean {
43
+ return (
44
+ prev.tab === next.tab &&
45
+ prev.paneId === next.paneId &&
46
+ prev.sessionId === next.sessionId &&
47
+ prev.cwd === next.cwd &&
48
+ prev.visible === next.visible &&
49
+ prev.expanded === next.expanded &&
50
+ prev.revealed === next.revealed &&
51
+ prev.localeRevision === next.localeRevision &&
52
+ prev.tabsVersion === next.tabsVersion &&
53
+ prev.effectiveTabId === next.effectiveTabId
54
+ )
55
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Terminal font resolution: the user's custom font prefs (SidebarPrefs,
3
+ * configured under the terminal card's secondary settings) turned into the
4
+ * xterm options. Kept as a pure module (no DOM, no xterm) so the fallback
5
+ * chain and clamping are unit-testable without mounting a terminal.
6
+ */
7
+ import { clampTerminalFontSize, type SidebarPrefs } from '../prefs-shared.ts'
8
+
9
+ /** The built-in fallback stack when neither the user nor the theme sets one. */
10
+ export const DEFAULT_TERMINAL_FONT_FAMILY = '"SF Mono", Menlo, Consolas, "Liberation Mono", monospace'
11
+
12
+ /**
13
+ * Icon fonts appended to whichever base stack wins, so shell prompts that
14
+ * draw glyphs from the Nerd Font Private Use Areas resolve to a real glyph
15
+ * instead of the missing-glyph box (aka tofu).
16
+ *
17
+ * Why this is needed even though the OS "should" fall back automatically:
18
+ * prompt frameworks (starship, powerlevel10k, oh-my-posh) take their icons
19
+ * from the PUA. Chromium's implicit system fallback reliably covers the
20
+ * *BMP* PUA (U+E000–U+F8FF — e.g. the U+E0B0 powerline separator) but NOT
21
+ * the *supplementary-plane* PUA-B (U+F0000+) where Nerd Fonts v3 relocated
22
+ * the Material Design icon set. Naming the families explicitly makes the
23
+ * browser consult them per character, which covers both planes.
24
+ *
25
+ * Deliberately NOT listed: color-emoji families. Chromium routes genuine
26
+ * emoji code points through a dedicated emoji fallback path (which is why
27
+ * emoji already render), so naming them buys nothing here — while placing a
28
+ * color font ahead of the generic family risks capturing BMP symbols the
29
+ * prompt expects in monospace (U+26A0 ⚠, U+2714 ✔ …) and rendering them as
30
+ * wide color glyphs that break the cell grid.
31
+ *
32
+ * Ordering rationale: the symbols-only patches ship glyphs without Latin,
33
+ * so they can never hijack ASCII metrics — the safest first hop. The
34
+ * fully-patched distributions follow for users who installed one of those
35
+ * instead. Both the `… Mono` and proportional family names are listed
36
+ * because the Nerd Fonts installers register them as distinct families.
37
+ *
38
+ * These are strictly *appended*, never prepended: xterm derives its cell
39
+ * metrics from the first entry, so the base font must stay in front or the
40
+ * whole grid would be re-measured against an icon font.
41
+ */
42
+ export const ICON_FONT_FALLBACKS: readonly string[] = [
43
+ // Symbols-only patches (glyph coverage without Latin) — ideal fallbacks.
44
+ '"Symbols Nerd Font Mono"',
45
+ '"Symbols Nerd Font"',
46
+ // Common fully-patched Nerd Font distributions.
47
+ '"Hack Nerd Font Mono"',
48
+ '"Hack Nerd Font"',
49
+ '"JetBrainsMono Nerd Font Mono"',
50
+ '"JetBrainsMono Nerd Font"',
51
+ '"FiraCode Nerd Font Mono"',
52
+ '"FiraCode Nerd Font"',
53
+ '"CaskaydiaCove Nerd Font Mono"',
54
+ '"CaskaydiaCove Nerd Font"',
55
+ '"SauceCodePro Nerd Font Mono"',
56
+ '"UbuntuMono Nerd Font Mono"',
57
+ '"Iosevka Nerd Font Mono"',
58
+ '"MesloLGS Nerd Font Mono"',
59
+ '"MesloLGS NF"',
60
+ ]
61
+
62
+ /** The symbols-only patch count at the head of {@link ICON_FONT_FALLBACKS}
63
+ * (these carry no Latin glyphs, so they can never hijack ASCII metrics). */
64
+ const SYMBOLS_ONLY_COUNT = 2
65
+
66
+ /**
67
+ * CSS generic font families. A generic is a catch-all that always resolves,
68
+ * so icon fonts must be spliced in *before* the first one to stay reachable.
69
+ */
70
+ const GENERIC_FAMILIES = new Set([
71
+ 'monospace', 'serif', 'sans-serif', 'cursive', 'fantasy',
72
+ 'system-ui', 'ui-monospace', 'ui-serif', 'ui-sans-serif', 'ui-rounded',
73
+ 'math', 'emoji', 'fangsong',
74
+ ])
75
+
76
+ /**
77
+ * CSS-wide keywords. These are only valid as an *entire* declaration value,
78
+ * so a stack cannot be appended to one.
79
+ */
80
+ const CSS_WIDE_KEYWORDS = new Set(['inherit', 'initial', 'unset', 'revert', 'revert-layer'])
81
+
82
+ /** Normalize one family name for comparison: unquote, collapse runs of
83
+ * whitespace, casefold. */
84
+ function normalizeFamily(family: string): string {
85
+ return family
86
+ .trim()
87
+ .replace(/^["']|["']$/g, '')
88
+ .replace(/\s+/g, ' ')
89
+ .trim()
90
+ .toLowerCase()
91
+ }
92
+
93
+ /**
94
+ * Split a CSS font-family stack on its top-level commas.
95
+ *
96
+ * Naive `split(',')` would corrupt quoted family names containing a comma
97
+ * and function values such as `var(--x, monospace)`, so quotes (with
98
+ * backslash escapes) and parentheses are tracked.
99
+ *
100
+ * @param stack - a CSS font-family stack.
101
+ * @returns the trimmed, non-empty family entries in source order.
102
+ */
103
+ function splitFamilies(stack: string): string[] {
104
+ const entries: string[] = []
105
+ let buffer = ''
106
+ let quote: string | null = null
107
+ let depth = 0
108
+ for (let i = 0; i < stack.length; i += 1) {
109
+ const char = stack[i] as string
110
+ if (quote !== null) {
111
+ // Escapes are copied verbatim so `\"` does not close the quote.
112
+ if (char === '\\' && i + 1 < stack.length) {
113
+ buffer += char + (stack[i + 1] as string)
114
+ i += 1
115
+ continue
116
+ }
117
+ buffer += char
118
+ if (char === quote) quote = null
119
+ continue
120
+ }
121
+ if (char === '"' || char === "'") {
122
+ quote = char
123
+ buffer += char
124
+ continue
125
+ }
126
+ if (char === '(') depth += 1
127
+ else if (char === ')') depth = Math.max(0, depth - 1)
128
+ else if (char === ',' && depth === 0) {
129
+ entries.push(buffer)
130
+ buffer = ''
131
+ continue
132
+ }
133
+ buffer += char
134
+ }
135
+ entries.push(buffer)
136
+ return entries.map(entry => entry.trim()).filter(entry => entry !== '')
137
+ }
138
+
139
+ /**
140
+ * Append {@link ICON_FONT_FALLBACKS} to a CSS font-family stack, keeping
141
+ * the caller's own entries and order intact.
142
+ *
143
+ * - Families already named in `stack` are not duplicated (quote-, case- and
144
+ * whitespace-insensitive), so a user who already lists their Nerd Font
145
+ * keeps their exact priority.
146
+ * - The icon fonts are spliced in ahead of the *first* generic family
147
+ * (`monospace` etc.), because a generic always resolves: anything after it
148
+ * would never be consulted. A stack that OPENS with a generic is the one
149
+ * exception: only the symbols-only patches (no Latin) may precede it — a
150
+ * fully-patched Nerd Font there would become xterm's measuring base font
151
+ * and override the user/theme family precedence, so it is placed after the
152
+ * generic instead.
153
+ * - Idempotent — re-applying to an already-topped-up stack is a no-op, which
154
+ * matters because `TerminalView` diffs the resolved value against the live
155
+ * `term.options.fontFamily` before reflowing.
156
+ *
157
+ * @param stack - a CSS font-family stack (base font first).
158
+ * @returns the stack with icon fallbacks merged in.
159
+ */
160
+ export function withIconFontFallbacks(stack: string): string {
161
+ const entries = splitFamilies(stack)
162
+ if (entries.length === 0) return ICON_FONT_FALLBACKS.join(', ')
163
+
164
+ const present = new Set(entries.map(normalizeFamily))
165
+ const notPresent = (family: string): boolean => !present.has(normalizeFamily(family))
166
+ const symbolsOnly = ICON_FONT_FALLBACKS.slice(0, SYMBOLS_ONLY_COUNT).filter(notPresent)
167
+ const patched = ICON_FONT_FALLBACKS.slice(SYMBOLS_ONLY_COUNT).filter(notPresent)
168
+ if (symbolsOnly.length === 0 && patched.length === 0) return entries.join(', ')
169
+
170
+ const firstGeneric = entries.findIndex(entry => GENERIC_FAMILIES.has(normalizeFamily(entry)))
171
+ const cut = firstGeneric === -1 ? entries.length : firstGeneric
172
+ // A fully-patched distribution ships Latin glyphs: placed ahead of a
173
+ // LEADING generic (cut === 0) it would become xterm's measuring/base font
174
+ // and override the user/theme family precedence. Only the symbols-only
175
+ // patches — which carry no Latin — may precede it; the patched fonts go
176
+ // after the generic (still reachable per character, never the base).
177
+ if (cut === 0) {
178
+ return [...symbolsOnly, entries[0]!, ...patched, ...entries.slice(1)].join(', ')
179
+ }
180
+ return [...entries.slice(0, cut), ...symbolsOnly, ...patched, ...entries.slice(cut)].join(', ')
181
+ }
182
+
183
+ /**
184
+ * Reduce one link of the base-family chain to a usable stack, or `''` when
185
+ * it cannot carry appended fallbacks so the next link should win.
186
+ *
187
+ * Rejecting CSS-wide keywords matters because the theme font arrives as a
188
+ * raw token value (`tokenValue('--ds-font-family-code')`); skins do set
189
+ * tokens to `initial`/`inherit`/`unset` (see `effectiveTokenValue` in
190
+ * `theme.ts`, which guards the color tokens for the same reason). Appending
191
+ * to such a value yields an invalid `font-family`, which the CSSOM discards
192
+ * silently — the terminal would lose the theme font *and* the icon fonts.
193
+ */
194
+ function usableBase(value: string | undefined): string {
195
+ const trimmed = (value ?? '').trim()
196
+ if (trimmed === '') return ''
197
+ if (CSS_WIDE_KEYWORDS.has(trimmed.toLowerCase())) return ''
198
+ return trimmed
199
+ }
200
+
201
+ /**
202
+ * Resolve the xterm font options for the given prefs.
203
+ *
204
+ * The base family keeps its existing precedence — user pref > theme code
205
+ * font > built-in stack — and then {@link withIconFontFallbacks} tops it up
206
+ * so prompt icons resolve regardless of which base won.
207
+ *
208
+ * @param prefs - the current side card preferences.
209
+ * @param themeFontFamily - the app's theme code font (`--ds-font-family-code`
210
+ * token value, read live by the caller); undefined when the token is absent.
211
+ * @returns the `fontFamily` / `fontSize` xterm options.
212
+ */
213
+ export function resolveTerminalFont(
214
+ prefs: SidebarPrefs,
215
+ themeFontFamily: string | undefined,
216
+ ): { fontFamily: string; fontSize: number } {
217
+ const base = usableBase(prefs.terminalFontFamily)
218
+ || usableBase(themeFontFamily)
219
+ || DEFAULT_TERMINAL_FONT_FAMILY
220
+ return {
221
+ fontFamily: withIconFontFallbacks(base),
222
+ fontSize: clampTerminalFontSize(prefs.terminalFontSize),
223
+ }
224
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Terminal URL hyperlinks: xterm's `registerLinkProvider` is fed per-line
3
+ * link descriptors built from the pty stream, so URLs printed by any tool
4
+ * become hoverable / clickable spans.
5
+ *
6
+ * To stay out of the user's way (a terminal's primary interaction is text
7
+ * selection, not browsing), a click only activates when the user holds
8
+ * Ctrl (Win/Linux) or Cmd (mac) — a plain click is left for xterm's
9
+ * normal selection handling. Only http(s) URLs are dispatched to the
10
+ * browser; other schemes (`file://`, `mailto:`, `javascript:`, …) are
11
+ * underlined for visibility but rejected at activation, so a `file://`
12
+ * URL printed by a tool stays inert instead of being handed to
13
+ * `window.open`.
14
+ *
15
+ * Kept as a pure module (no xterm import) so the regex, the line
16
+ * scanner, the modifier gate and the scheme guard are unit-testable
17
+ * without mounting a terminal. `buildTerminalLinks` returns plain-object
18
+ * descriptors whose shape matches xterm's `ILink` minus the `activate`
19
+ * callback — the caller attaches `activate` (which closes over the
20
+ * event modifier check + `openTerminalUrl`) so this module never
21
+ * imports xterm types.
22
+ */
23
+
24
+ /** Scheme allowlist for activation. Only http(s) is opened externally. */
25
+ const OPENABLE_SCHEMES = new Set(['http:', 'https:'])
26
+
27
+ /**
28
+ * The URL pattern used to scan each terminal line.
29
+ *
30
+ * A word boundary (`\b`) guards the leading scheme so `notttps://…` does
31
+ * not match. The character class excludes ASCII whitespace and the
32
+ * wrapping punctuation that shells commonly emit around URLs (quotes,
33
+ * angle brackets, brackets/braces, pipes, backslashes, backticks) so a
34
+ * URL printed as `"https://example.com"` or `<https://example.com>` does
35
+ * not drag the wrapping character into the link target.
36
+ *
37
+ * Carries the `g` flag so `findTerminalUrlsInLine` can iterate every
38
+ * match on a line; callers must reset `lastIndex` before reuse (the
39
+ * helper does this defensively).
40
+ */
41
+ export const TERMINAL_URL_REGEX = /\bhttps?:\/\/[^\s"'<>\[\]{}|\\^`]+/gi
42
+
43
+ /** A URL match in a terminal line: the text and its 0-based start offset. */
44
+ export interface TerminalUrlMatch {
45
+ /** 0-based start index of the URL within the line string. */
46
+ start: number
47
+ /** The matched URL text. */
48
+ text: string
49
+ }
50
+
51
+ /**
52
+ * Strip trailing closing parens that are not balanced by an opening paren
53
+ * earlier in the URL.
54
+ *
55
+ * The regex's character class keeps `(` and `)` (Wikipedia-style URLs
56
+ * carry real parens: `…/Python_(programming_language)`), so a URL wrapped
57
+ * in parens by a shell (`(https://example.com)`) captures the trailing
58
+ * `)`. Balanced pairs are kept intact; only the unmatched excess is
59
+ * trimmed, so:
60
+ * - `https://example.com)` → `https://example.com`
61
+ * - `https://en.wikipedia.org/wiki/Python_(programming_language)` → unchanged
62
+ * - `https://example.com/(` → unchanged (more openers than closers; the
63
+ * trailing `(` is the URL's own, not a wrapper)
64
+ */
65
+ function trimUnbalancedTrailingParens(url: string): string {
66
+ let opens = 0
67
+ let closers = 0
68
+ for (let i = 0; i < url.length; i += 1) {
69
+ const ch = url[i]
70
+ if (ch === '(') opens += 1
71
+ else if (ch === ')') closers += 1
72
+ }
73
+ const excess = closers - opens
74
+ if (excess <= 0) return url
75
+ let end = url.length
76
+ let stripped = 0
77
+ while (end > 0 && url[end - 1] === ')' && stripped < excess) {
78
+ end -= 1
79
+ stripped += 1
80
+ }
81
+ return url.slice(0, end)
82
+ }
83
+
84
+ /**
85
+ * Find every http(s) URL in a line of terminal text, in source order
86
+ * with 0-based start offsets. A link provider maps these to buffer
87
+ * ranges for xterm's `registerLinkProvider`.
88
+ *
89
+ * Trailing unmatched closing parens are trimmed from each match (see
90
+ * {@link trimUnbalancedTrailingParens}), so a URL wrapped in parens by
91
+ * a shell opens without the trailing `)`, while Wikipedia-style URLs
92
+ * with balanced parens stay intact.
93
+ *
94
+ * Resets the regex's `lastIndex` before and after the scan so a
95
+ * previous partial iteration can't desynchronize a later one (the
96
+ * regex carries the `g` flag and is module-shared).
97
+ */
98
+ export function findTerminalUrlsInLine(line: string): TerminalUrlMatch[] {
99
+ TERMINAL_URL_REGEX.lastIndex = 0
100
+ const matches: TerminalUrlMatch[] = []
101
+ let m: RegExpExecArray | null
102
+ while ((m = TERMINAL_URL_REGEX.exec(line)) !== null) {
103
+ const trimmed = trimUnbalancedTrailingParens(m[0])
104
+ if (trimmed.length > 0) {
105
+ matches.push({ start: m.index, text: trimmed })
106
+ }
107
+ }
108
+ TERMINAL_URL_REGEX.lastIndex = 0
109
+ return matches
110
+ }
111
+
112
+ /**
113
+ * A buffer range in xterm's coordinate system. xterm's cell `x` is
114
+ * 1-based (1..cols) and `y` is the buffer line number; the `end` is
115
+ * inclusive (the last cell covered by the link). Structurally
116
+ * compatible with xterm's `IBufferRange` so the caller can pass it
117
+ * straight through without importing xterm types here.
118
+ */
119
+ export interface TerminalLinkRange {
120
+ start: { x: number; y: number }
121
+ end: { x: number; y: number }
122
+ }
123
+
124
+ /**
125
+ * A link descriptor: a buffer range plus the URL text. The caller
126
+ * (the link provider in `TerminalView`) attaches the `activate`
127
+ * callback, which closes over the modifier gate and `openTerminalUrl`.
128
+ */
129
+ export interface TerminalLinkDescriptor {
130
+ range: TerminalLinkRange
131
+ text: string
132
+ }
133
+
134
+ /**
135
+ * Build link descriptors for every URL found in a terminal line.
136
+ *
137
+ * @param lineText - the line's text (e.g. from
138
+ * `IBufferLine.translateToString(true)`).
139
+ * @param lineNumber - the buffer line number xterm passed to
140
+ * `ILinkProvider.provideLinks` (used as the `y` of every range; URLs
141
+ * never span wrapped lines because each wrapped row is its own
142
+ * buffer line).
143
+ * @returns descriptors in source order; empty when the line has no URL.
144
+ */
145
+ export function buildTerminalLinks(lineText: string, lineNumber: number): TerminalLinkDescriptor[] {
146
+ return findTerminalUrlsInLine(lineText).map(({ start, text: url }) => ({
147
+ range: {
148
+ // xterm's cell `x` is 1-based; `start` is 0-based from
149
+ // translateToString, so the first URL character sits at x = start + 1.
150
+ start: { x: start + 1, y: lineNumber },
151
+ // `end` is inclusive: the last URL character is at 0-based
152
+ // (start + url.length - 1), which is 1-based (start + url.length).
153
+ end: { x: start + url.length, y: lineNumber },
154
+ },
155
+ text: url,
156
+ }))
157
+ }
158
+
159
+ /**
160
+ * Decide whether a click on a terminal link should activate (open the URL).
161
+ *
162
+ * Mirrors what every modern terminal does (Windows Terminal, iTerm2, the
163
+ * VSCode integrated terminal): a plain click stays a text-selection
164
+ * gesture, and only Ctrl (Win/Linux) or Cmd (mac) hands the URL to the
165
+ * browser. The modifier is read off the activating `MouseEvent`, not
166
+ * tracked separately, so a key-up between hover and click never
167
+ * desynchronizes the gate.
168
+ */
169
+ export function shouldActivateTerminalLink(event: MouseEvent): boolean {
170
+ return event.ctrlKey || event.metaKey
171
+ }
172
+
173
+ /**
174
+ * Open a URL matched in the terminal, with a scheme guard so a printed
175
+ * `file://` or anything that slipped past the regex cannot reach
176
+ * `window.open`. The URL is constructed via `new URL(...)` which throws
177
+ * on malformed input; the catch makes the function total so the xterm
178
+ * handler never throws into the terminal's event loop.
179
+ *
180
+ * @returns `true` when the URL was dispatched to `window.open`, `false`
181
+ * when it was rejected (bad URL, disallowed scheme, no `window`).
182
+ */
183
+ export function openTerminalUrl(uri: string): boolean {
184
+ if (typeof window === 'undefined') return false
185
+ let url: URL
186
+ try {
187
+ url = new URL(uri)
188
+ } catch {
189
+ return false
190
+ }
191
+ if (!OPENABLE_SCHEMES.has(url.protocol)) return false
192
+ window.open(url.toString(), '_blank', 'noopener,noreferrer')
193
+ return true
194
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Live theme access for surfaces that cannot consume the token colors
3
+ * directly — xterm's palette and CodeMirror's theme extensions need concrete
4
+ * values, but the app's scheme flips at runtime (ui-layout's ThemePresenter
5
+ * projects prefers-color-scheme and the user's choice onto
6
+ * body[data-ds-dark-theme] and html { color-scheme }). This module reads the
7
+ * resolved scheme and token values, and notifies subscribers on flips, so
8
+ * the terminal and the editor re-theme in place instead of freezing in the
9
+ * scheme they happened to be created under.
10
+ */
11
+
12
+ /** Whether the app shell resolved to the dark scheme.
13
+ *
14
+ * The presenter sets `html { color-scheme }` together with the body palette
15
+ * attribute, so a set color-scheme means the decision is authoritative (an
16
+ * absent attribute is then LIGHT even when the OS prefers dark — the user
17
+ * chose light). Before the presenter has run, fall back to the OS media
18
+ * query as the best guess.
19
+ */
20
+ export function isDarkScheme(): boolean {
21
+ if (typeof document === 'undefined') return true
22
+ const decided = document.documentElement.style.colorScheme !== ''
23
+ if (decided) return document.body.hasAttribute('data-ds-dark-theme')
24
+ return typeof matchMedia !== 'undefined' && matchMedia('(prefers-color-scheme: dark)').matches
25
+ }
26
+
27
+ /** One token's computed value on <body> ('' while the theme has not applied). */
28
+ export function tokenValue(name: string): string {
29
+ if (typeof document === 'undefined') return ''
30
+ return getComputedStyle(document.body).getPropertyValue(name).trim()
31
+ }
32
+
33
+ /** Minimal alpha for a token color to count as effectively opaque. Skin
34
+ * systems turn `--dsw-alias-bg-base` translucent for glass panels (the
35
+ * dsh-web-ui skins use rgba 0.16–0.7; `transparent` is 0); below this
36
+ * floor a text surface (terminal, editor) would render over the skin's
37
+ * backdrop art, so callers fall back to an opaque color. Values at or
38
+ * above the floor (e.g. a skin's scoped 0.96 porcelain) pass through —
39
+ * the skin still controls the surface. */
40
+ const OPAQUE_ALPHA_MIN = 0.9
41
+
42
+ /** The alpha channel of a computed CSS color, or null when the format is
43
+ * not parseable (named colors, `color()`… — treated as opaque). Handles
44
+ * the shapes getComputedStyle actually returns: the rgb()/rgba() and
45
+ * hsl()/hsla() function forms (comma or space syntax, with or without the
46
+ * `/ alpha` slot) and the #rgb/#rgba/#rrggbb/#rrggbbaa hex family. */
47
+ export function colorAlpha(color: string): number | null {
48
+ const s = color.trim()
49
+ // #rgb / #rgba / #rrggbb / #rrggbbaa
50
+ const hex = /^#([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(s)
51
+ if (hex !== null) {
52
+ const digits = hex[1]!
53
+ if (digits.length === 3 || digits.length === 4) {
54
+ const a = digits.length === 4 ? digits[3]! : 'f'
55
+ return parseInt(a + a, 16) / 255
56
+ }
57
+ const alphaHex = digits.length === 8 ? digits.slice(6) : 'ff'
58
+ return parseInt(alphaHex, 16) / 255
59
+ }
60
+ // rgb(r g b) / rgb(r g b / a) / rgba(r, g, b, a) — the trailing slot is alpha.
61
+ const fn = /^(rgba?|hsla?)\(([^)]+)\)$/i.exec(s)
62
+ if (fn !== null) {
63
+ const parts = fn[2]!.split(/[,\s/]+/).filter(Boolean)
64
+ const alphaPart = parts[3]
65
+ if (alphaPart === undefined) return 1
66
+ const alpha = Number.parseFloat(alphaPart)
67
+ return Number.isFinite(alpha) ? alpha : 1
68
+ }
69
+ return null
70
+ }
71
+
72
+ /**
73
+ * A token value that actually PAINTS something — the guard for text
74
+ * surfaces (issue #90). Skin systems routinely set global tokens to
75
+ * `transparent` (glass skins) or translucent glass values (`rgba(…,0.16–0.7)`,
76
+ * e.g. the dsh-web-ui skins) — both are truthy strings, so callers using
77
+ * `|| fallback` never fire and the terminal/editor goes see-through over
78
+ * the skin's backdrop. This returns '' for visually inert values (unset
79
+ * keywords, transparent, and any color below the opacity floor) so the
80
+ * caller's fallback chain engages; effectively opaque values pass through.
81
+ */
82
+ export function effectiveTokenValue(name: string): string {
83
+ const raw = tokenValue(name)
84
+ switch (raw) {
85
+ case '':
86
+ case 'transparent':
87
+ case 'initial':
88
+ case 'inherit':
89
+ case 'unset':
90
+ return ''
91
+ default: {
92
+ const alpha = colorAlpha(raw)
93
+ if (alpha !== null && alpha < OPAQUE_ALPHA_MIN) return ''
94
+ return raw
95
+ }
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Subscribe to color-scheme flips (the presenter toggles the body
101
+ * attribute). The callback fires after the attribute changed; re-read the
102
+ * scheme inside it.
103
+ * @returns the disposer.
104
+ */
105
+ export function subscribeColorScheme(callback: () => void): () => void {
106
+ if (typeof document === 'undefined') return () => {}
107
+ const observer = new MutationObserver(() => { callback() })
108
+ observer.observe(document.body, { attributes: true, attributeFilter: ['data-ds-dark-theme'] })
109
+ return () => { observer.disconnect() }
110
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The title-bar strip resolution chain — the ONE place that decides how
3
+ * many pixels the sidebar yields at the top. Standard signals first, then
4
+ * the user's chosen scheme; never a per-shell branch:
5
+ *
6
+ * 0. `web` scheme — EXPLICIT "ChatCode CLI Web": never adapt, not even
7
+ * standard WCO geometry (the user declares the plain web UI).
8
+ * 1. Window Controls Overlay real geometry (standard API, authoritative
9
+ * when present — even 0, e.g. the overlay is hidden while maximized).
10
+ * 2. The `dsh-desktop-titlebar-inset` URL contract parameter (a shell
11
+ * declares the exact pixels it reserves).
12
+ * 3. The active shell preset's strip (scheme `preset` — opt-in data).
13
+ * 4. The legacy manual `titleBarStripPx` (scheme `custom`).
14
+ * 5. 0 — plain-browser semantics, nothing modified.
15
+ *
16
+ * The result drives `--dsh-title-bar-strip` + `body[data-dsh-title-bar-compat]`
17
+ * exactly like the legacy boolean did; only the VALUE source changed.
18
+ */
19
+ import type { DesktopEnv } from './desktop-env.ts'
20
+ import type { TitleBarScheme } from '../prefs-shared.ts'
21
+ import type { WcoSnapshot } from './wco.ts'
22
+ import { presetStripFor, type ShellPreset } from './shell-presets.ts'
23
+
24
+ export function computeTitleBarStrip(
25
+ env: DesktopEnv,
26
+ wco: WcoSnapshot,
27
+ scheme: TitleBarScheme,
28
+ preset: ShellPreset | undefined,
29
+ customStripPx: number,
30
+ ): number {
31
+ if (scheme === 'web') return 0
32
+ if (wco.present) return wco.height
33
+ if (env.titlebarInset > 0) return env.titlebarInset
34
+ if (scheme === 'preset') return presetStripFor(preset, env) ?? 0
35
+ if (scheme === 'custom') return customStripPx
36
+ return 0
37
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Official app icons for the 打开项目 menu — JetBrains product marks are
3
+ * the full-color SVGs from resources.jetbrains.com (gradient ids namespaced
4
+ * per icon so several can coexist on one page), VS Code from simple-icons,
5
+ * Explorer a filled Windows-yellow folder. Sizing lives in CSS.
6
+ * @module dsh-ui-tweaks/client/appicons
7
+ */
8
+
9
+ /** target key -> inline SVG markup (trusted build-time constants). */
10
+ export const APP_ICONS: {
11
+ explorer: string
12
+ vscode: string
13
+ idea: string
14
+ goland: string
15
+ webstorm: string
16
+ pycharm: string
17
+ } = {
18
+ explorer: '<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path fill="#FFB900" d="M1.5 3.2A1.7 1.7 0 0 1 3.2 1.5h3.1l1.7 2.1h4.8A1.7 1.7 0 0 1 14.5 5.3v7.5a1.7 1.7 0 0 1-1.7 1.7H3.2a1.7 1.7 0 0 1-1.7-1.7Z"/></svg>',
19
+ vscode: "<svg viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path fill=\"#007ACC\" d=\"M23.15 2.587L18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z\"/></svg>",
20
+ idea: "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 64 64\"> <defs> <linearGradient id=\"gbi-idea-a\" x1=\"-.717383\" x2=\"24.1455\" y1=\"7.61946\" y2=\"61.2456\" gradientUnits=\"userSpaceOnUse\"> <stop offset=\".1\" stop-color=\"#FC801D\"/> <stop offset=\".59\" stop-color=\"#FE2857\"/> </linearGradient> <linearGradient id=\"gbi-idea-b\" x1=\"4.22243\" x2=\"62.9273\" y1=\"60.0186\" y2=\"1.31316\" gradientUnits=\"userSpaceOnUse\"> <stop offset=\".21\" stop-color=\"#FE2857\"/> <stop offset=\".7\" stop-color=\"#007EFF\"/> </linearGradient> </defs> <path fill=\"#FF8100\" d=\"m15.9476 5.81641-11.87545.00174C1.82284 5.81815 0 7.64157 0 9.89088V21.3975c0 1.1887.519564 2.3185 1.42196 3.0924L39.5828 57.1997c.7384.6324 1.6786.9803 2.6508.9803h11.8755c2.2493 0 4.0727-1.8234 4.0727-4.0727V42.599c0-1.1887-.5195-2.3186-1.4219-3.0924L18.599 6.79735c-.7383-.63302-1.6786-.98036-2.6514-.98036v-.00058Z\"/> <path fill=\"url(#gbi-idea-a)\" d=\"M14.5193 5.81641H4.07273C1.82342 5.81641 0 7.63982 0 9.88913V22.9818c0 .1926.013964.3852.040727.576L5.31782 60.5015c.28683 2.0067 2.00494 3.4967 4.032 3.4967H25.0228c2.2499 0 4.0733-1.824 4.0728-4.0739l-.0047-18.5384c0-.4376-.0704-.8722-.2089-1.287L18.3825 8.60099c-.5544-1.66284-2.1108-2.78458-3.8638-2.78458h.0006Z\"/> <path fill=\"url(#gbi-idea-b)\" d=\"M59.9275 0H25.9592c-1.6291 0-3.1017.971054-3.7435 2.46807L6.14767 39.9587c-.21702.5068-.32931 1.0531-.32931 1.6046v18.364C5.81836 62.1766 7.64178 64 9.89109 64H27.8571c.8046 0 1.5912-.2385 2.2609-.6854l32.0687-21.4033c1.1322-.7552 1.8117-2.0265 1.8117-3.3874l.0018-34.45117C64.0002 1.82342 62.1768 0 59.9275 0Z\"/> <path fill=\"#000\" d=\"M52 12H12v40h40V12Z\"/> <path fill=\"#fff\" d=\"M17 29.3856h2.9788v-9.7712H17V17h8.839v2.6144h-2.9788v9.7712h2.9788V32H17v-2.6144Z\"/> <path fill=\"#fff\" d=\"M27.3389 29.3002h2.1538c.4354 0 .8233-.0928 1.1625-.2784.3392-.1857.6016-.4481.7872-.7873.1857-.3392.2785-.7265.2785-1.1625V17h2.9249v10.2748c0 .9001-.2074 1.7092-.6216 2.4271-.4143.7179-.9855 1.2805-1.7143 1.6873-.7288.4074-1.5464.6108-2.4534.6108h-2.5176v-2.6998Z\"/> <path fill=\"#fff\" d=\"M17 44h16v3H17v-3Z\"/> </svg>",
21
+ goland: "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 64 64\"> <defs> <linearGradient id=\"gbi-goland-a\" x1=\"63.6351\" x2=\"39.318\" y1=\"54.0451\" y2=\"1.59593\" gradientUnits=\"userSpaceOnUse\"> <stop offset=\".24\" stop-color=\"#00D886\"/> <stop offset=\".51\" stop-color=\"#007DFE\"/> </linearGradient> <linearGradient id=\"gbi-goland-b\" x1=\"59.3792\" x2=\".674325\" y1=\"4.38051\" y2=\"63.0854\" gradientUnits=\"userSpaceOnUse\"> <stop offset=\".27\" stop-color=\"#007DFE\"/> <stop offset=\".7\" stop-color=\"#D249FC\"/> </linearGradient> </defs> <path fill=\"#00D886\" d=\"m48.0521 58.18 11.8755-.0017c2.2493 0 4.0722-1.8234 4.0722-4.0727V42.599c0-1.1887-.5196-2.3186-1.422-3.0924L24.4164 6.79677c-.7384-.63244-1.6786-.98036-2.6508-.98036H9.89011c-2.24931 0-4.07273 1.82341-4.07273 4.07272V21.3975c0 1.1887.51957 2.3185 1.42197 3.0924L45.4002 57.1991c.7383.633 1.6786.9804 2.6514.9804l.0005.0005Z\"/> <path fill=\"url(#gbi-goland-a)\" d=\"M49.4806 58.1818h10.4465c2.2493 0 4.0728-1.8234 4.0728-4.0727V41.0164c0-.1925-.014-.3851-.0408-.576L58.6815 3.49673C58.3952 1.49062 56.6765 0 54.65 0H38.977c-2.2499 0-4.0733 1.824-4.0727 4.07389l.0047 18.53851c0 .4375.0704.8721.2088 1.2869l10.4995 31.4979c.5545 1.6629 2.1109 2.7846 3.8639 2.7846h-.0006Z\"/> <path fill=\"url(#gbi-goland-b)\" d=\"M4.07273 64H38.041c1.6291 0 3.1017-.9711 3.7434-2.4681l16.0681-37.4906c.217-.5068.3293-1.0531.3293-1.6046V4.07273C58.1818 1.82342 56.3584 0 54.1091 0h-17.966c-.8046 0-1.5912.238545-2.2609.685382L1.81353 22.0881C.681309 22.8439.001745 24.1146.001745 25.4755L0 59.9273C0 62.1766 1.82342 64 4.07273 64Z\"/> <path fill=\"#000\" d=\"M52 12H12v40h40V12Z\"/> <path fill=\"#fff\" d=\"M19.764 31.6245c1.174.6765 2.4847 1.0155 3.9318 1.0155 1.3899 0 2.6427-.3095 3.7589-.9293 1.1231-.6197 2.005-1.4651 2.6461-2.5384.6481-1.0798.9721-2.2857.9721-3.6183v-1.1879h-6.405v2.3221h3.516c-.0722.5605-.2761 1.0718-.6106 1.5339-.3958.5468-.9322.9762-1.6092 1.2852-.6701.3096-1.4188.4646-2.2469.4646-.8928 0-1.6988-.2227-2.4193-.6695-.7204-.4467-1.2857-1.0583-1.6959-1.836-.4034-.784-.6047-1.667-.6047-2.6461 0-.979.2013-1.858.6047-2.6357.4108-.7846.9761-1.4003 1.6959-1.847.7199-.4467 1.5265-.6695 2.4193-.6695.6053 0 1.1706.0972 1.696.2916.526.1875.9831.4577 1.372.8101.3958.3455.7094.7563.9397 1.2314h3.1542c-.2737-.9866-.7529-1.8575-1.4368-2.6137-.677-.7563-1.5126-1.3436-2.5061-1.7608-.9866-.4172-2.0669-.6267-3.2404-.6267-1.4477 0-2.7583.3414-3.9318 1.0259-1.1741.6765-2.0959 1.6133-2.7653 2.8082-.6695 1.1879-1.0046 2.5165-1.0046 3.9856 0 1.4692.3351 2.8018 1.0046 3.9967.67 1.1879 1.5918 2.1236 2.7653 2.8081Z\"/> <path fill=\"#fff\" fill-rule=\"evenodd\" d=\"M36.1829 31.6245c1.181.6765 2.4951 1.0155 3.9422 1.0155 1.4541 0 2.7688-.339 3.9423-1.0155 1.181-.6845 2.1062-1.6202 2.7757-2.8081.6695-1.1949 1.0045-2.5275 1.0045-3.9967 0-1.4691-.335-2.7977-1.0045-3.9856-.6695-1.1949-1.5947-2.1317-2.7757-2.8082C42.8939 17.3414 41.5723 17 40.1251 17c-1.4471 0-2.7612.3414-3.9422 1.0259-1.1735.6765-2.0988 1.6133-2.7758 2.8082-.6695 1.1879-1.0045 2.5165-1.0045 3.9856 0 1.4692.335 2.8018 1.0045 3.9967.6765 1.1879 1.6023 2.1236 2.7758 2.8081Zm6.3621-2.2463c-.7129.4468-1.516.6695-2.4089.6695-.8928 0-1.6994-.2227-2.4193-.6695-.7128-.4536-1.2747-1.0756-1.685-1.8684-.4033-.7991-.6046-1.696-.6046-2.6895 0-.9935.2013-1.8858.6046-2.6785.4103-.7991.9722-1.4217 1.685-1.8684.7199-.4537 1.5265-.6805 2.4193-.6805.8929 0 1.696.2268 2.4089.6805.7129.4467 1.2707 1.0693 1.674 1.8684.4033.7921.6047 1.685.6047 2.6785s-.2014 1.8904-.6047 2.6895c-.4033.7922-.9611 1.4148-1.674 1.8684Z\" clip-rule=\"evenodd\"/> <path fill=\"#fff\" d=\"M16.9941 44h16v3h-16v-3Z\"/> </svg>",
22
+ webstorm: "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 64 64\"> <defs> <linearGradient id=\"gbi-webstorm-a\" x1=\"7.62141\" x2=\"61.2476\" y1=\"64.7192\" y2=\"39.8564\" gradientUnits=\"userSpaceOnUse\"> <stop offset=\".22\" stop-color=\"#F0EB18\"/> <stop offset=\".59\" stop-color=\"#00C4F4\"/> </linearGradient> <linearGradient id=\"gbi-webstorm-b\" x1=\"60.0186\" x2=\"1.31316\" y1=\"59.7777\" y2=\"1.07287\" gradientUnits=\"userSpaceOnUse\"> <stop offset=\".19\" stop-color=\"#00C4F4\"/> <stop offset=\".83\" stop-color=\"#007DFE\"/> </linearGradient> </defs> <path fill=\"#F0EB18\" d=\"m5.81836 48.0512.00174 11.8755c0 2.2493 1.82342 4.0721 4.07273 4.0721H21.3994c1.1887 0 2.3186-.5196 3.0924-1.422l32.7098-38.1614c.6325-.7383.9804-1.6786.9804-2.6508V9.88913c0-2.24931-1.8234-4.07272-4.0727-4.07272H42.6009c-1.1887 0-2.3185.51956-3.0924 1.42196L6.79872 45.3998c-.63302.7383-.98036 1.6786-.98036 2.6514Z\"/> <path fill=\"url(#gbi-webstorm-a)\" d=\"M5.81836 49.4825v10.4466c0 2.2493 1.82342 4.0727 4.07273 4.0727H22.9837c.1926 0 .3852-.014.576-.0407l36.9437-5.2771c2.0067-.2868 3.4968-2.005 3.4968-4.032V38.979c0-2.2499-1.824-4.0733-4.0739-4.0727l-18.5385.0046c-.4375 0-.8721.0704-1.287.2089L8.60294 45.6193c-1.66284.5544-2.78458 2.1108-2.78458 3.8638v-.0006Z\"/> <path fill=\"url(#gbi-webstorm-b)\" d=\"M0 4.07273V38.041c0 1.6291.971054 3.1017 2.46807 3.7434L39.9587 57.8525c.5067.217 1.0531.3293 1.6046.3293h18.364c2.2493 0 4.0727-1.8234 4.0727-4.0727v-17.966c0-.8046-.2386-1.5913-.6854-2.2609L41.9113 1.81353C41.1561.681309 39.8854.001745 38.5239.001745L4.07273 0C1.82342 0 0 1.82342 0 4.07273Z\"/> <path fill=\"#000\" d=\"M52 12H12v40h40V12Z\"/> <path fill=\"#fff\" d=\"m21.2377 27.961-2.4224-10.9532h-3.0321l3.6224 15.0044h3.3226l2.5613-10.8998 2.5292 10.8998h3.333l3.6441-15.0044h-2.9689L29.4797 27.961l-2.7008-10.9532h-2.9581L21.2377 27.961Z\"/> <path fill=\"#fff\" d=\"M38.2224 31.6804c.8429.3927 1.8074.5896 2.8937.5896s2.0515-.1998 2.8944-.6005c.8428-.4008 1.4968-.9503 1.9613-1.6507.4714-.7074.7073-1.5049.7073-2.3903 0-.7222-.1607-1.3865-.4822-1.9934-.3216-.6149-.7717-1.1294-1.3505-1.5434-.5718-.422-1.2252-.7073-1.9613-.8572l-2.551-.5575c-.4644-.1137-.8325-.3066-1.1041-.5787-.2646-.2722-.3967-.615-.3967-1.0289 0-.364.0965-.6815.2894-.9537.1929-.2785.461-.4932.8038-.643.3497-.1568.7539-.236 1.2109-.236.457 0 .8607.0815 1.2109.2463.3571.1567.6321.3818.8251.6752.1929.2854.2893.6109.2893.9755h2.9259c-.0074-.8572-.236-1.614-.6861-2.272-.4432-.6648-1.0645-1.1833-1.8649-1.5542-.8003-.3715-1.7253-.5575-2.7329-.5575-1.0077 0-1.908.1929-2.7009.5788-.7929.3789-1.4107.9106-1.8539 1.5967-.4427.6792-.6643 1.4469-.6643 2.3041 0 .7073.1464 1.3441.4392 1.9079.2934.5644.7039 1.0398 1.2327 1.4256.5288.379 1.1506.6471 1.8649.8038l2.6474.5897c.5076.1223.9037.3393 1.1897.654.2928.3146.4392.7113.4392 1.1896 0 .3859-.1074.7286-.3215 1.0289-.2073.3003-.5036.5357-.8894.7074-.3789.1648-.8004.2463-1.3292.2463-.5288 0-.9967-.0925-1.4038-.2785-.4071-.186-.7251-.4432-.9537-.7716-.2285-.3279-.3427-.7114-.3427-1.1466h-2.9368c.0143.9215.2572 1.7362.7286 2.4436.4788.7073 1.1362 1.2579 1.9722 1.6507Z\"/> <path fill=\"#fff\" d=\"M16.9932 44h16v3h-16v-3Z\"/> </svg>",
23
+ pycharm: "<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 64 64\"> <defs> <linearGradient id=\"gbi-pycharm-a\" x1=\"7.62141\" x2=\"61.2476\" y1=\"64.7192\" y2=\"39.8558\" gradientUnits=\"userSpaceOnUse\"> <stop offset=\".1\" stop-color=\"#00D886\"/> <stop offset=\".59\" stop-color=\"#F0EB18\"/> </linearGradient> <linearGradient id=\"gbi-pycharm-b\" x1=\"60.0186\" x2=\"1.31317\" y1=\"59.7778\" y2=\"1.07229\" gradientUnits=\"userSpaceOnUse\"> <stop offset=\".3\" stop-color=\"#F0EB18\"/> <stop offset=\".7\" stop-color=\"#00C4F4\"/> </linearGradient> </defs> <path fill=\"#00D886\" d=\"m5.81934 48.0512.00174 11.8755c0 2.2493 1.82342 4.0721 4.07273 4.0721H21.4004c1.1887 0 2.3186-.5196 3.0924-1.422L57.202 24.4154c.6325-.7383.9804-1.6786.9804-2.6508V9.88913c0-2.24931-1.8234-4.07272-4.0727-4.07272H42.6013c-1.1887 0-2.3185.51956-3.0924 1.42196L6.7997 45.3998c-.63302.7384-.98036 1.6786-.98036 2.6514Z\"/> <path fill=\"url(#gbi-pycharm-a)\" d=\"M5.81836 49.4825v10.4466c0 2.2493 1.82342 4.0727 4.07273 4.0727H22.9837c.1926 0 .3852-.0139.576-.0407l36.9438-5.2771c2.0066-.2868 3.4967-2.0049 3.4967-4.032V38.979c0-2.2499-1.824-4.0733-4.0739-4.0727l-18.5385.0046c-.4375 0-.8721.0704-1.287.2089L8.60294 45.6193c-1.66284.5544-2.78458 2.1108-2.78458 3.8638v-.0006Z\"/> <path fill=\"url(#gbi-pycharm-b)\" d=\"M0 4.07273V38.041c0 1.6291.971054 3.1017 2.46807 3.7434L39.9587 57.8525c.5068.217 1.0531.3293 1.6046.3293h18.364c2.2493 0 4.0727-1.8234 4.0727-4.0727v-17.966c0-.8046-.2385-1.5912-.6854-2.2609L41.9119 1.81353C41.1561.681309 39.8854.001745 38.5245.001745L4.07273 0C1.82342 0 0 1.82342 0 4.07273Z\"/> <path fill=\"#000\" d=\"M52 12H12v40h40V12Z\"/> <path fill=\"#fff\" fill-rule=\"evenodd\" d=\"M23.5363 16.9676h-6.4407v15.0055h2.9261v-5.6702h3.4296c1.0715 0 2.0115-.1929 2.8188-.5788.8148-.3927 1.4401-.9434 1.8759-1.6508.4427-.7074.6643-1.5434.6643-2.465 0-.9216-.2182-1.7324-.654-2.4329-.4289-.7005-1.0433-1.2431-1.8437-1.629-.8005-.3859-1.7261-.5788-2.7763-.5788Zm1.0927 6.6349c-.3646.1785-.7929.2681-1.2862.2681h-3.3229v-4.4695h3.3229c.4933 0 .9216.0924 1.2862.2784.3715.178.6569.4353.8573.7718.2004.3278.3003.7286.3003 1.1788 0 .4502-.1005.8469-.3003 1.1897-.1998.3365-.4858.5966-.8573.7827Z\" clip-rule=\"evenodd\"/> <path fill=\"#fff\" d=\"M33.3821 31.2232c1.1651.6713 2.4656 1.0077 3.9017 1.0077v-.0011c1.2144 0 2.3295-.2251 3.3441-.6753 1.0146-.4501 1.8575-1.0783 2.5294-1.8862.6787-.8148 1.1328-1.7473 1.3614-2.7975H41.453c-.2004.5426-.5001 1.0221-.9003 1.4361-.3933.4076-.8688.7223-1.4257.9434-.557.221-1.1645.3324-1.822.3324-.886 0-1.6864-.221-2.4007-.6643-.7149-.4433-1.2759-1.0502-1.683-1.8219-.4002-.7781-.6-1.6543-.6-2.6259 0-.9715.1998-1.8437.6-2.6154.4077-.7786.9681-1.3896 1.683-1.8329.7143-.4432 1.5147-.6643 2.4007-.6643.6569 0 1.2644.1114 1.822.3325.5575.221 1.0324.5397 1.4257.9537.4002.4077.6999.8831.9003 1.4257h3.0657c-.2291-1.0502-.6827-1.9787-1.3614-2.7866-.6719-.8147-1.5148-1.4469-2.5294-1.8971-1.0146-.4502-2.1297-.6753-3.3441-.6753-1.4367 0-2.7372.3388-3.9017 1.0181-1.165.6712-2.0797 1.6009-2.7441 2.7866-.6643 1.1788-.9968 2.4972-.9968 3.955 0 1.4579.3325 2.7803.9968 3.966.6649 1.1789 1.5791 2.1073 2.7441 2.7866Z\"/> <path fill=\"#fff\" d=\"M16.9941 44.001h16v3h-16v-3Z\"/> </svg>",
24
+ }