dsh-coding-sidebar 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +524 -0
  3. package/cordis.patch.yml +49 -0
  4. package/lib/client-editor.js +48229 -0
  5. package/lib/client-mermaid.js +200745 -0
  6. package/lib/client-registry.js +21867 -0
  7. package/lib/client-terminal.js +9361 -0
  8. package/lib/client.js +21867 -0
  9. package/lib/index.js +4556 -0
  10. package/lib/invariant.js +22 -0
  11. package/lib/types/agent-opens.d.ts +65 -0
  12. package/lib/types/agent-pty.d.ts +226 -0
  13. package/lib/types/browser-probe.d.ts +15 -0
  14. package/lib/types/bundle-route.d.ts +12 -0
  15. package/lib/types/client/BrowserView.d.ts +39 -0
  16. package/lib/types/client/DiffTab.d.ts +6 -0
  17. package/lib/types/client/DiffView.d.ts +51 -0
  18. package/lib/types/client/EditorHost.d.ts +13 -0
  19. package/lib/types/client/FileTree.d.ts +38 -0
  20. package/lib/types/client/FreeWindow.d.ts +29 -0
  21. package/lib/types/client/GitView.d.ts +10 -0
  22. package/lib/types/client/MarkdownHtml.d.ts +32 -0
  23. package/lib/types/client/OrphanedTab.d.ts +11 -0
  24. package/lib/types/client/PdfView.d.ts +6 -0
  25. package/lib/types/client/RenderBoundary.d.ts +32 -0
  26. package/lib/types/client/SandboxStatusBar.d.ts +10 -0
  27. package/lib/types/client/SideCardSection.d.ts +74 -0
  28. package/lib/types/client/SideChatView.d.ts +16 -0
  29. package/lib/types/client/Sidebar.d.ts +6 -0
  30. package/lib/types/client/SubagentView.d.ts +16 -0
  31. package/lib/types/client/TabBar.d.ts +54 -0
  32. package/lib/types/client/TerminalView.d.ts +24 -0
  33. package/lib/types/client/TextEditor.d.ts +10 -0
  34. package/lib/types/client/TreePanel.d.ts +24 -0
  35. package/lib/types/client/UploadOverlay.d.ts +20 -0
  36. package/lib/types/client/add-plugin-modal.d.ts +15 -0
  37. package/lib/types/client/api.d.ts +275 -0
  38. package/lib/types/client/binary-download.d.ts +12 -0
  39. package/lib/types/client/breakpoints.d.ts +17 -0
  40. package/lib/types/client/browser.d.ts +60 -0
  41. package/lib/types/client/builtins/index.d.ts +18 -0
  42. package/lib/types/client/builtins/tabs.d.ts +11 -0
  43. package/lib/types/client/builtins/viewers.d.ts +3 -0
  44. package/lib/types/client/chunk-loader.d.ts +131 -0
  45. package/lib/types/client/chunks/editor.d.ts +9 -0
  46. package/lib/types/client/chunks/mermaid.d.ts +10 -0
  47. package/lib/types/client/chunks/terminal.d.ts +9 -0
  48. package/lib/types/client/cm-themes.d.ts +24 -0
  49. package/lib/types/client/conversation-draft.d.ts +14 -0
  50. package/lib/types/client/desktop-env.d.ts +35 -0
  51. package/lib/types/client/editor-load.d.ts +66 -0
  52. package/lib/types/client/frame-batcher.d.ts +24 -0
  53. package/lib/types/client/icons.d.ts +59 -0
  54. package/lib/types/client/image-types.d.ts +3 -0
  55. package/lib/types/client/ime-guard.d.ts +36 -0
  56. package/lib/types/client/index.d.ts +25 -0
  57. package/lib/types/client/intercept.d.ts +46 -0
  58. package/lib/types/client/lang.d.ts +14 -0
  59. package/lib/types/client/layout-push.d.ts +15 -0
  60. package/lib/types/client/lazy-chunk.d.ts +25 -0
  61. package/lib/types/client/link-intercept.d.ts +42 -0
  62. package/lib/types/client/locales-ar.d.ts +18 -0
  63. package/lib/types/client/locales-de.d.ts +4 -0
  64. package/lib/types/client/locales-fr.d.ts +10 -0
  65. package/lib/types/client/locales-hi.d.ts +17 -0
  66. package/lib/types/client/locales-id.d.ts +15 -0
  67. package/lib/types/client/locales-it.d.ts +8 -0
  68. package/lib/types/client/locales-ja.d.ts +17 -0
  69. package/lib/types/client/locales-ko.d.ts +9 -0
  70. package/lib/types/client/locales-nl.d.ts +16 -0
  71. package/lib/types/client/locales-pl.d.ts +19 -0
  72. package/lib/types/client/locales-pt.d.ts +1 -0
  73. package/lib/types/client/locales-ru.d.ts +13 -0
  74. package/lib/types/client/locales-sv.d.ts +1 -0
  75. package/lib/types/client/locales-th.d.ts +17 -0
  76. package/lib/types/client/locales-tr.d.ts +17 -0
  77. package/lib/types/client/locales-vi.d.ts +17 -0
  78. package/lib/types/client/locales-zh-HK.d.ts +32 -0
  79. package/lib/types/client/locales-zh-MO.d.ts +32 -0
  80. package/lib/types/client/locales-zh-TW.d.ts +32 -0
  81. package/lib/types/client/locales.d.ts +452 -0
  82. package/lib/types/client/markdown-html.d.ts +96 -0
  83. package/lib/types/client/markdown-images.d.ts +45 -0
  84. package/lib/types/client/markdown-labels.d.ts +38 -0
  85. package/lib/types/client/md-toc.d.ts +4 -0
  86. package/lib/types/client/mermaid-blocks.d.ts +47 -0
  87. package/lib/types/client/mermaid-sanitize.d.ts +2 -0
  88. package/lib/types/client/mermaid.d.ts +12 -0
  89. package/lib/types/client/open-when-sized.d.ts +20 -0
  90. package/lib/types/client/open-with-settings.d.ts +4 -0
  91. package/lib/types/client/open-with.d.ts +87 -0
  92. package/lib/types/client/openpath-intercept.d.ts +85 -0
  93. package/lib/types/client/paths.d.ts +43 -0
  94. package/lib/types/client/pdf-types.d.ts +2 -0
  95. package/lib/types/client/pinned.d.ts +86 -0
  96. package/lib/types/client/plugin-settings.d.ts +8 -0
  97. package/lib/types/client/plugins-shared.d.ts +27 -0
  98. package/lib/types/client/plugins-tabs.d.ts +3 -0
  99. package/lib/types/client/plugins-viewers.d.ts +3 -0
  100. package/lib/types/client/prefs.d.ts +41 -0
  101. package/lib/types/client/produced-files.d.ts +29 -0
  102. package/lib/types/client/selection-payload.d.ts +27 -0
  103. package/lib/types/client/service.d.ts +439 -0
  104. package/lib/types/client/settings-nav-icon.d.ts +19 -0
  105. package/lib/types/client/shell-presets.d.ts +48 -0
  106. package/lib/types/client/sidechat-transcript.d.ts +103 -0
  107. package/lib/types/client/split-pane.d.ts +36 -0
  108. package/lib/types/client/state.d.ts +432 -0
  109. package/lib/types/client/subagent-detect.d.ts +54 -0
  110. package/lib/types/client/subagent-jobs.d.ts +63 -0
  111. package/lib/types/client/tab-content-memo.d.ts +40 -0
  112. package/lib/types/client/terminal-font.d.ts +78 -0
  113. package/lib/types/client/terminal-links.d.ts +119 -0
  114. package/lib/types/client/theme.d.ts +45 -0
  115. package/lib/types/client/titlebar-strip.d.ts +23 -0
  116. package/lib/types/client/upload.d.ts +69 -0
  117. package/lib/types/client/wco.d.ts +47 -0
  118. package/lib/types/config.d.ts +64 -0
  119. package/lib/types/context-types.d.ts +605 -0
  120. package/lib/types/fs-operations.d.ts +28 -0
  121. package/lib/types/fs-search.d.ts +24 -0
  122. package/lib/types/fs-tree.d.ts +51 -0
  123. package/lib/types/git.d.ts +124 -0
  124. package/lib/types/html-route.d.ts +59 -0
  125. package/lib/types/index.d.ts +47 -0
  126. package/lib/types/invariant.d.ts +15 -0
  127. package/lib/types/jobs-routes.d.ts +45 -0
  128. package/lib/types/open-external.d.ts +24 -0
  129. package/lib/types/path-security.d.ts +20 -0
  130. package/lib/types/prefs-shared.d.ts +236 -0
  131. package/lib/types/pty-deps.d.ts +78 -0
  132. package/lib/types/pty-manager.d.ts +168 -0
  133. package/lib/types/sidechat-core.d.ts +172 -0
  134. package/lib/types/sidechat-routes.d.ts +30 -0
  135. package/lib/types/subagent-activity.d.ts +44 -0
  136. package/lib/types/subagent-live-route.d.ts +44 -0
  137. package/lib/types/tools.d.ts +34 -0
  138. package/lib/types/trust-fence.d.ts +24 -0
  139. package/lib/types/wire.d.ts +38 -0
  140. package/package.json +172 -0
  141. package/scripts/install.ps1 +306 -0
  142. package/scripts/install.sh +287 -0
  143. package/src/agent-opens.ts +265 -0
  144. package/src/agent-pty.ts +527 -0
  145. package/src/browser-probe.ts +26 -0
  146. package/src/bundle-route.ts +129 -0
  147. package/src/client/BrowserView.tsx +289 -0
  148. package/src/client/DiffTab.tsx +114 -0
  149. package/src/client/DiffView.tsx +305 -0
  150. package/src/client/EditorHost.tsx +558 -0
  151. package/src/client/FileTree.tsx +669 -0
  152. package/src/client/FreeWindow.tsx +337 -0
  153. package/src/client/GitView.tsx +717 -0
  154. package/src/client/MarkdownHtml.tsx +296 -0
  155. package/src/client/OrphanedTab.tsx +27 -0
  156. package/src/client/PdfView.tsx +110 -0
  157. package/src/client/RenderBoundary.tsx +49 -0
  158. package/src/client/SandboxStatusBar.tsx +60 -0
  159. package/src/client/SideCardSection.module.css +960 -0
  160. package/src/client/SideCardSection.tsx +1163 -0
  161. package/src/client/SideChatView.module.css +437 -0
  162. package/src/client/SideChatView.tsx +645 -0
  163. package/src/client/Sidebar.tsx +1370 -0
  164. package/src/client/SubagentView.module.css +547 -0
  165. package/src/client/SubagentView.tsx +883 -0
  166. package/src/client/TabBar.tsx +365 -0
  167. package/src/client/TerminalView.tsx +424 -0
  168. package/src/client/TextEditor.tsx +468 -0
  169. package/src/client/TreePanel.tsx +275 -0
  170. package/src/client/UploadOverlay.tsx +62 -0
  171. package/src/client/add-plugin-modal.tsx +199 -0
  172. package/src/client/api.ts +357 -0
  173. package/src/client/binary-download.tsx +23 -0
  174. package/src/client/breakpoints.ts +61 -0
  175. package/src/client/browser.ts +167 -0
  176. package/src/client/builtins/index.ts +37 -0
  177. package/src/client/builtins/tabs.tsx +340 -0
  178. package/src/client/builtins/viewers.tsx +125 -0
  179. package/src/client/chunk-loader.ts +358 -0
  180. package/src/client/chunks/editor.tsx +9 -0
  181. package/src/client/chunks/mermaid.tsx +10 -0
  182. package/src/client/chunks/terminal.tsx +9 -0
  183. package/src/client/cm-themes.ts +129 -0
  184. package/src/client/conversation-draft.ts +29 -0
  185. package/src/client/css-modules.d.ts +5 -0
  186. package/src/client/desktop-env.ts +72 -0
  187. package/src/client/editor-load.ts +92 -0
  188. package/src/client/frame-batcher.ts +56 -0
  189. package/src/client/icons.tsx +152 -0
  190. package/src/client/image-types.ts +8 -0
  191. package/src/client/ime-guard.ts +47 -0
  192. package/src/client/index.tsx +427 -0
  193. package/src/client/intercept.tsx +188 -0
  194. package/src/client/lang.ts +225 -0
  195. package/src/client/layout-push.ts +24 -0
  196. package/src/client/layout.css +90 -0
  197. package/src/client/lazy-chunk.tsx +89 -0
  198. package/src/client/link-intercept.ts +73 -0
  199. package/src/client/locales-ar.ts +368 -0
  200. package/src/client/locales-de.ts +353 -0
  201. package/src/client/locales-fr.ts +360 -0
  202. package/src/client/locales-hi.ts +367 -0
  203. package/src/client/locales-id.ts +365 -0
  204. package/src/client/locales-it.ts +358 -0
  205. package/src/client/locales-ja.ts +367 -0
  206. package/src/client/locales-ko.ts +359 -0
  207. package/src/client/locales-nl.ts +365 -0
  208. package/src/client/locales-pl.ts +369 -0
  209. package/src/client/locales-pt.ts +350 -0
  210. package/src/client/locales-ru.ts +365 -0
  211. package/src/client/locales-sv.ts +350 -0
  212. package/src/client/locales-th.ts +367 -0
  213. package/src/client/locales-tr.ts +367 -0
  214. package/src/client/locales-vi.ts +367 -0
  215. package/src/client/locales-zh-HK.ts +382 -0
  216. package/src/client/locales-zh-MO.ts +382 -0
  217. package/src/client/locales-zh-TW.ts +382 -0
  218. package/src/client/locales.ts +894 -0
  219. package/src/client/markdown-html.ts +331 -0
  220. package/src/client/markdown-images.ts +137 -0
  221. package/src/client/markdown-labels.tsx +52 -0
  222. package/src/client/md-toc.tsx +127 -0
  223. package/src/client/mermaid-blocks.ts +110 -0
  224. package/src/client/mermaid-sanitize.ts +93 -0
  225. package/src/client/mermaid.tsx +406 -0
  226. package/src/client/open-when-sized.ts +43 -0
  227. package/src/client/open-with-settings.tsx +135 -0
  228. package/src/client/open-with.ts +221 -0
  229. package/src/client/openpath-intercept.ts +143 -0
  230. package/src/client/paths.ts +63 -0
  231. package/src/client/pdf-types.ts +4 -0
  232. package/src/client/pinned.ts +183 -0
  233. package/src/client/plugin-settings.ts +43 -0
  234. package/src/client/plugins-shared.ts +29 -0
  235. package/src/client/plugins-tabs.ts +96 -0
  236. package/src/client/plugins-viewers.ts +30 -0
  237. package/src/client/prefs.ts +210 -0
  238. package/src/client/produced-files.ts +120 -0
  239. package/src/client/selection-payload.ts +86 -0
  240. package/src/client/service.ts +859 -0
  241. package/src/client/settings-nav-icon.ts +45 -0
  242. package/src/client/shell-presets.ts +83 -0
  243. package/src/client/sidebar.module.css +2788 -0
  244. package/src/client/sidechat-transcript.ts +321 -0
  245. package/src/client/split-pane.tsx +331 -0
  246. package/src/client/state.ts +1497 -0
  247. package/src/client/subagent-detect.ts +137 -0
  248. package/src/client/subagent-jobs.ts +161 -0
  249. package/src/client/tab-content-memo.ts +55 -0
  250. package/src/client/terminal-font.ts +224 -0
  251. package/src/client/terminal-links.ts +194 -0
  252. package/src/client/theme.ts +110 -0
  253. package/src/client/titlebar-strip.ts +37 -0
  254. package/src/client/upload.ts +187 -0
  255. package/src/client/wco.ts +120 -0
  256. package/src/config.ts +154 -0
  257. package/src/context-types.ts +611 -0
  258. package/src/fs-operations.ts +95 -0
  259. package/src/fs-search.ts +113 -0
  260. package/src/fs-tree.ts +158 -0
  261. package/src/git.ts +446 -0
  262. package/src/html-route.ts +106 -0
  263. package/src/index.ts +1291 -0
  264. package/src/invariant.ts +32 -0
  265. package/src/jobs-routes.ts +257 -0
  266. package/src/open-external.ts +90 -0
  267. package/src/path-security.ts +75 -0
  268. package/src/prefs-shared.ts +280 -0
  269. package/src/pty-deps.ts +240 -0
  270. package/src/pty-manager.ts +376 -0
  271. package/src/sidechat-core.ts +468 -0
  272. package/src/sidechat-routes.ts +344 -0
  273. package/src/subagent-activity.ts +91 -0
  274. package/src/subagent-live-route.ts +93 -0
  275. package/src/tools.ts +482 -0
  276. package/src/trust-fence.ts +84 -0
  277. package/src/wire.ts +100 -0
@@ -0,0 +1,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 "DSH official 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,187 @@
1
+ /**
2
+ * File-upload plumbing for the files window: turn a file picker or a drag-drop
3
+ * into per-file raw-byte uploads through the sidebar's `/sidebar/upload` route.
4
+ *
5
+ * Folders keep their tree in both flows: the picker's `webkitdirectory`
6
+ * selection arrives as Files with `webkitRelativePath` filled, and dropped
7
+ * folders — which never surface in `dataTransfer.files` — are traversed via
8
+ * `webkitGetAsEntry`, so the relative path is preserved for every nested file
9
+ * and the host recreates the tree under the chosen directory. The File is
10
+ * streamed straight into the POST body (no base64 inflation); uploads run
11
+ * sequentially so one slow file cannot starve the others, and each result
12
+ * reports its own outcome (the tree keeps going after a failure). An
13
+ * optional `AbortSignal` stops the queue at the next item boundary and
14
+ * aborts the in-flight request; the host cleans up its temp file when the
15
+ * request stream dies.
16
+ */
17
+ import { api, SidebarApiError, type SessionScope } from './api.ts'
18
+ import { isAbsolutePath } from './paths.ts'
19
+ import type { CopyKey } from './locales.ts'
20
+
21
+ /** One pending file: the browser File plus the workspace-relative target path. */
22
+ export interface UploadItem {
23
+ file: File
24
+ relativePath: string
25
+ }
26
+
27
+ /** One settled upload. */
28
+ export interface UploadResult {
29
+ relativePath: string
30
+ ok: boolean
31
+ path?: string
32
+ /** Wire error code when the host refused the upload ('too-large', ...). */
33
+ code?: string
34
+ /** The host's error message (English wire text; localize via `code`). */
35
+ error?: string
36
+ }
37
+
38
+ /** Sanitize a relative target: absolute paths, traversal, and empty segments
39
+ * are rejected (the host enforces the same rules with a 400). */
40
+ function sanitizeRelativePath(rel: string): string | undefined {
41
+ if (rel === '' || isAbsolutePath(rel)) return undefined
42
+ if (rel.split(/[\\/]/).some((s) => s === '' || s === '.' || s === '..')) return undefined
43
+ return rel
44
+ }
45
+
46
+ /** The picker's relative path: webkitRelativePath when present, else the name. */
47
+ function relativePathOf(file: File): string | undefined {
48
+ return sanitizeRelativePath(file.webkitRelativePath || file.name || '')
49
+ }
50
+
51
+ /** Collect a picker selection (webkitdirectory folders carry relative paths). */
52
+ export function uploadItemsFromFiles(files: FileList | readonly File[]): UploadItem[] {
53
+ const items: UploadItem[] = []
54
+ for (const file of files) {
55
+ const rel = relativePathOf(file)
56
+ if (rel !== undefined) items.push({ file, relativePath: rel })
57
+ }
58
+ return items
59
+ }
60
+
61
+ /** Read one dropped file-system entry into upload items; directories
62
+ * recurse, prefixing their name onto every descendant's relative path. */
63
+ async function itemsFromEntry(entry: FileSystemEntry, prefix: string): Promise<UploadItem[]> {
64
+ if (entry.isFile) {
65
+ const file = await new Promise<File>((resolve, reject) => {
66
+ ;(entry as FileSystemFileEntry).file(resolve, reject)
67
+ })
68
+ const rel = sanitizeRelativePath(prefix + file.name)
69
+ return rel === undefined ? [] : [{ file, relativePath: rel }]
70
+ }
71
+ if (entry.isDirectory) {
72
+ const reader = (entry as FileSystemDirectoryEntry).createReader()
73
+ const entries: FileSystemEntry[] = []
74
+ // readEntries returns one BATCH per call; drain until an empty batch.
75
+ for (;;) {
76
+ const batch = await new Promise<FileSystemEntry[]>((resolve, reject) => {
77
+ reader.readEntries(resolve, reject)
78
+ })
79
+ if (batch.length === 0) break
80
+ entries.push(...batch)
81
+ }
82
+ const nested = await Promise.all(entries.map(child => itemsFromEntry(child, `${prefix}${entry.name}/`)))
83
+ return nested.flat()
84
+ }
85
+ return []
86
+ }
87
+
88
+ /**
89
+ * Collect a drag-drop payload. Dropped folders do NOT surface in
90
+ * `dataTransfer.files` — they arrive as directory items, so entries are
91
+ * captured via `webkitGetAsEntry` and traversed (draining readEntries
92
+ * batches), keeping each nested file's relative path. MUST be invoked
93
+ * synchronously from the drop handler: the dataTransfer enters protected
94
+ * mode once the event dispatch ends, while the captured entry handles stay
95
+ * readable asynchronously. Falls back to the flat file list when the entry
96
+ * API is unavailable; an entry that fails to read is skipped, not fatal.
97
+ */
98
+ export async function uploadItemsFromDrop(data: DataTransfer | undefined): Promise<UploadItem[]> {
99
+ if (data === undefined) return []
100
+ // Synchronous section: capture every entry handle before returning.
101
+ const entries = [...data.items]
102
+ .map(item => (item.kind === 'file' ? item.webkitGetAsEntry() : null))
103
+ .filter((entry): entry is FileSystemEntry => entry !== null)
104
+ if (entries.length === 0) return uploadItemsFromFiles(data.files)
105
+ const nested = await Promise.all(entries.map(entry => itemsFromEntry(entry, '').catch(() => [])))
106
+ return nested.flat()
107
+ }
108
+
109
+ /**
110
+ * Client-side pre-check cap. Mirrors the host's default (config `uploadLimit`)
111
+ * only — the host enforces the real, configurable limit, so a lowered host
112
+ * limit surfaces as a `too-large` wire error, never as a silent pass.
113
+ */
114
+ export const MAX_UPLOAD_BYTES = 128 * 1024 * 1024
115
+
116
+ /** How long a success hint stays before fading (failures stay until the next action). */
117
+ export const UPLOAD_HINT_MS = 3500
118
+
119
+ /**
120
+ * One-line upload progress text: 'Uploading into {dir}…' while no file is in
121
+ * flight, then 'Uploading {done}/{total}: {name}' per file. Shared by the tree
122
+ * hint and the full-window upload overlay.
123
+ */
124
+ export function uploadHintText(
125
+ done: number,
126
+ total: number,
127
+ current: string,
128
+ dir: string,
129
+ t: (key: CopyKey, params?: Record<string, string | number>) => string,
130
+ ): string {
131
+ return current === '' ? t('uploadingTo', { dir }) : t('uploadProgress', { done, total, name: current })
132
+ }
133
+
134
+ /**
135
+ * Upload every item into `dir` (absolute, inside the session workspace),
136
+ * sequentially, reporting progress as `(done, total, currentRelativePath)`.
137
+ * Resolves with one result per item — never rejects; `signal.aborted` stops
138
+ * the queue at the next item boundary (completed items stay uploaded).
139
+ */
140
+ export async function uploadToDir(
141
+ scope: SessionScope,
142
+ dir: string,
143
+ items: UploadItem[],
144
+ onProgress?: (done: number, total: number, current: string) => void,
145
+ signal?: AbortSignal,
146
+ ): Promise<UploadResult[]> {
147
+ const results: UploadResult[] = []
148
+ let done = 0
149
+ for (const item of items) {
150
+ if (signal?.aborted) break
151
+ onProgress?.(done, items.length, item.relativePath)
152
+ try {
153
+ if (item.file.size > MAX_UPLOAD_BYTES) {
154
+ results.push({ relativePath: item.relativePath, ok: false, code: 'too-large' })
155
+ } else {
156
+ const res = await api.uploadFile(scope, dir, item.relativePath, item.file, signal)
157
+ results.push({ relativePath: item.relativePath, ok: true, path: res.path })
158
+ }
159
+ } catch (error) {
160
+ // An aborted in-flight request is a cancel, not a failure: stop quietly.
161
+ if (error instanceof DOMException && error.name === 'AbortError') break
162
+ results.push({
163
+ relativePath: item.relativePath,
164
+ ok: false,
165
+ code: error instanceof SidebarApiError ? error.code : undefined,
166
+ error: error instanceof Error ? error.message : String(error),
167
+ })
168
+ }
169
+ done++
170
+ }
171
+ onProgress?.(done, items.length, '')
172
+ return results
173
+ }
174
+
175
+ /** Fold a result list into a one-line status for the tree hint. */
176
+ export function summarizeResults(
177
+ results: UploadResult[],
178
+ t: (key: CopyKey, params?: Record<string, string | number>) => string,
179
+ ): string {
180
+ const okCount = results.filter((r) => r.ok).length
181
+ const failed = results.find((r) => !r.ok)
182
+ if (failed !== undefined) {
183
+ const detail = failed.code === 'too-large' ? t('uploadTooLarge') : (failed.error ?? t('uploadFailedUnknown'))
184
+ return t('uploadFailed', { error: detail })
185
+ }
186
+ return t('uploadDone', { count: okCount })
187
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Reactive Window Controls Overlay geometry — the STANDARD web mechanism
3
+ * for frameless Chromium shells that draw the native caption buttons
4
+ * (minimize / maximize / close) OVER the web content (Electron
5
+ * `titleBarOverlay` on Windows; issue #257). Feature-detected: absent in
6
+ * plain browsers, macOS, Tauri etc., where the snapshot stays `NONE` and
7
+ * the sidebar adapts nothing — this is the conservative "auto" signal.
8
+ *
9
+ * The API reports the real titlebar rect (CSS px), which differs between
10
+ * shells and Electron versions (32 / 36 …), and fires `geometrychange`
11
+ * when the window maximizes / restores / moves — so the strip must be
12
+ * reactive, not memoized like the URL stamps.
13
+ *
14
+ * Module-level store so the Sidebar shell can subscribe via
15
+ * `useSyncExternalStore`; `setWcoSourceForTests` swaps the source for unit
16
+ * tests (no real API exists in jsdom/headless).
17
+ */
18
+ export interface WcoSource {
19
+ readonly visible: boolean
20
+ getTitlebarAreaRect(): { readonly x: number; readonly y: number; readonly width: number; readonly height: number }
21
+ addEventListener(type: 'geometrychange', listener: () => void): void
22
+ removeEventListener(type: 'geometrychange', listener: () => void): void
23
+ }
24
+
25
+ /** The current overlay geometry (present=false = API unavailable). */
26
+ export interface WcoSnapshot {
27
+ readonly present: boolean
28
+ readonly height: number
29
+ }
30
+
31
+ /** Snapshot when the API is unavailable (plain browser / non-overlay shell). */
32
+ export const WCO_NONE: WcoSnapshot = Object.freeze({ present: false, height: 0 })
33
+
34
+ type Listener = () => void
35
+
36
+ let source: WcoSource | undefined
37
+ let snapshot: WcoSnapshot = WCO_NONE
38
+ let attached = false
39
+ let sourceListener: (() => void) | undefined
40
+ const listeners = new Set<Listener>()
41
+
42
+ function read(): WcoSnapshot {
43
+ if (source === undefined) return WCO_NONE
44
+ try {
45
+ // A present-but-NOT-visible API is a PHANTOM: headless Chromium and
46
+ // macOS builds expose the interface but draw no overlay (empty rect,
47
+ // visible=false). Only a genuinely visible overlay is authoritative —
48
+ // otherwise the resolution chain would trust a zero rect and silently
49
+ // skip the shell's real reserved space.
50
+ if (source.visible !== true) return { present: false, height: 0 }
51
+ const rect = source.getTitlebarAreaRect()
52
+ const height = Math.round(rect.height)
53
+ return Number.isFinite(height) && height > 0
54
+ ? { present: true, height }
55
+ : { present: true, height: 0 }
56
+ } catch {
57
+ // A hostile/racy API must never break the layout — treat as absent.
58
+ return { present: false, height: 0 }
59
+ }
60
+ }
61
+
62
+ function onGeometryChange(): void {
63
+ snapshot = read()
64
+ emit()
65
+ }
66
+
67
+ function emit(): void {
68
+ for (const listener of listeners) listener()
69
+ }
70
+
71
+ /** Attach the native geometrychange listener (once). */
72
+ function attach(): void {
73
+ if (attached) return
74
+ attached = true
75
+ // The test hook may have installed a source already; otherwise attach to
76
+ // the real navigator API (absent in plain browsers → stay NONE).
77
+ const candidate = source ?? (navigator as unknown as { windowControlsOverlay?: WcoSource }).windowControlsOverlay
78
+ if (candidate === undefined) return
79
+ source = candidate
80
+ sourceListener = onGeometryChange
81
+ snapshot = read()
82
+ source.addEventListener('geometrychange', sourceListener)
83
+ }
84
+
85
+ /** Detach the native listener (last subscriber left or source swapped). */
86
+ function detach(): void {
87
+ if (source !== undefined && sourceListener !== undefined) {
88
+ source.removeEventListener('geometrychange', sourceListener)
89
+ }
90
+ sourceListener = undefined
91
+ attached = false
92
+ }
93
+
94
+ /** Read the current snapshot (returns the frozen NONE when unavailable). */
95
+ export function getWcoSnapshot(): WcoSnapshot {
96
+ return snapshot
97
+ }
98
+
99
+ /**
100
+ * Subscribe to overlay geometry changes. Attaches to the real
101
+ * `navigator.windowControlsOverlay` on first subscribe; the disposer
102
+ * detaches the native listener when the last subscriber leaves.
103
+ */
104
+ export function subscribeWco(onChange: Listener): () => void {
105
+ listeners.add(onChange)
106
+ attach()
107
+ return () => {
108
+ listeners.delete(onChange)
109
+ if (listeners.size === 0) detach()
110
+ }
111
+ }
112
+
113
+ /** Test hook: swap the geometry source (undefined = API unavailable). */
114
+ export function setWcoSourceForTests(next: WcoSource | undefined): void {
115
+ detach()
116
+ source = next
117
+ snapshot = next === undefined ? WCO_NONE : read()
118
+ if (next !== undefined && listeners.size > 0) attach()
119
+ emit()
120
+ }