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,120 @@
1
+ /**
2
+ * Pure derivation of one turn's produced files from finalized conversation
3
+ * nodes — a structural replica of ui-deliverables' `producedForClosing`
4
+ * (the mutation tools' follow-along `locations`, by render intent: a diff
5
+ * card or a generic edit card; reads/deletes/failures produce nothing).
6
+ * Kept dependency-free so the takeover logic is unit-testable and the
7
+ * replica is easy to diff against upstream when it drifts.
8
+ */
9
+ import { isAbsolutePath } from './paths.ts'
10
+
11
+ /** Paths a tool-result view reports as produced, by render intent. */
12
+ export function producedPaths(view: unknown): readonly string[] {
13
+ if (view === null || typeof view !== 'object') return []
14
+ const record = view as { card?: unknown; kind?: unknown; locations?: unknown }
15
+ const isEdit = record.card === 'diff' || (record.card === 'generic' && record.kind === 'edit')
16
+ if (!isEdit) return []
17
+ if (!Array.isArray(record.locations)) return []
18
+ const paths: string[] = []
19
+ for (const location of record.locations) {
20
+ if (location !== null && typeof location === 'object' && typeof (location as { path?: unknown }).path === 'string') {
21
+ paths.push((location as { path: string }).path)
22
+ }
23
+ }
24
+ return paths
25
+ }
26
+
27
+ /**
28
+ * Files produced by the turn the assistant at `seq` closes. Accumulation
29
+ * resets on turn boundaries (a user message, or a node reporting a different
30
+ * turn number); paths keep first-seen order and appear once.
31
+ * @param nodes - snapshot nodes in surface order (structural, unknown-safe).
32
+ * @param seq - the closing assistant's seq (the render site's anchor).
33
+ * @returns produced paths; empty when the turn wrote nothing.
34
+ */
35
+ export function producedForClosing(nodes: readonly unknown[], seq: number): readonly string[] {
36
+ let pending: string[] = []
37
+ let seen = new Set<string>()
38
+ let turn: number | undefined
39
+ for (const node of nodes) {
40
+ if (node === null || typeof node !== 'object') continue
41
+ const record = node as { kind?: unknown; isError?: unknown; callView?: unknown; turn?: unknown; seq?: unknown }
42
+ if (record.kind === 'tool-result') {
43
+ if (record.isError === true) continue
44
+ for (const path of producedPaths(record.callView)) {
45
+ if (seen.has(path)) continue
46
+ seen.add(path)
47
+ pending.push(path)
48
+ }
49
+ continue
50
+ }
51
+ if (record.kind === 'user') {
52
+ turn = undefined
53
+ pending = []
54
+ seen = new Set()
55
+ } else if (typeof record.turn === 'number') {
56
+ if (turn !== undefined && record.turn !== turn) {
57
+ pending = []
58
+ seen = new Set()
59
+ }
60
+ turn = record.turn
61
+ }
62
+ if (record.kind === 'assistant' && record.seq === seq) return pending
63
+ }
64
+ return []
65
+ }
66
+
67
+ /**
68
+ * Claim the turn-tail chain only when the closing turn produced files.
69
+ *
70
+ * The authoritative source is the engine Turn data — the same value
71
+ * ui-deliverables reads (`owner.turn.data.get('deliverables')`): a
72
+ * `{ produced: [{ seq, path }, ...] }` record accumulated per Turn. The
73
+ * node-based replica below stays as a fallback for compositions that do not
74
+ * publish it.
75
+ * @param owner - the turn-tail owner currency ({turn, seq, openFile}).
76
+ * @returns produced paths as the matched value, or null to decline.
77
+ */
78
+ export function selectProducedFiles(owner: unknown): readonly string[] | null {
79
+ const record = owner as {
80
+ turn?: { data?: { get?: (key: string) => unknown } }
81
+ nodes?: unknown
82
+ seq?: unknown
83
+ } | null
84
+ if (record === null || typeof record !== 'object') return null
85
+ const seq = typeof record.seq === 'number' ? record.seq : Number.POSITIVE_INFINITY
86
+ const data = record.turn?.data?.get?.('deliverables') as
87
+ | { produced?: unknown }
88
+ | null
89
+ | undefined
90
+ if (data !== null && typeof data === 'object' && Array.isArray(data.produced)) {
91
+ const paths: string[] = []
92
+ const seen = new Set<string>()
93
+ for (const item of data.produced) {
94
+ if (item === null || typeof item !== 'object') continue
95
+ const produced = item as { path?: unknown; seq?: unknown }
96
+ if (typeof produced.path !== 'string' || produced.path === '') continue
97
+ if (typeof produced.seq === 'number' && produced.seq > seq) continue
98
+ if (seen.has(produced.path)) continue
99
+ seen.add(produced.path)
100
+ paths.push(produced.path)
101
+ }
102
+ return paths.length === 0 ? null : paths
103
+ }
104
+ if (!Array.isArray(record.nodes)) return null
105
+ const paths = producedForClosing(record.nodes, seq)
106
+ return paths.length === 0 ? null : paths
107
+ }
108
+
109
+ /**
110
+ * Resolve a (possibly relative) path against the session cwd for the sidebar.
111
+ * Absolute detection mirrors the host (see client/paths.isAbsolutePath):
112
+ * POSIX roots, drive letters and UNC shares must not be joined onto the cwd.
113
+ */
114
+ export function resolveSidebarPath(cwd: string | undefined, path: string): string {
115
+ if (isAbsolutePath(path)) return path
116
+ const base = cwd ?? ''
117
+ if (base === '') return path
118
+ const separator = base.includes('\\') ? '\\' : '/'
119
+ return `${base.replace(/[\\/]+$/, '')}${separator}${path}`
120
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Pure payload builders for the "add selection to conversation" popup in the
3
+ * text viewers (markdown preview + the catch-all code viewer). Everything
4
+ * here is string math — no React, no ctx — so the unit tests cover it
5
+ * directly.
6
+ *
7
+ * Insert shape (agreed with the product owner):
8
+ * - Selection ≤ SELECTION_LIMIT characters: a fenced code block whose info
9
+ * line is `相对路径:起止行` and whose body is the selected text.
10
+ * - Selection over the limit: a single plain-text line `相对路径:起止行`
11
+ * (no fence, no content).
12
+ * - The path is relative to the session cwd (the same projection the
13
+ * explorer's @ button uses); an unknown cwd falls back to the absolute
14
+ * path.
15
+ * - Line numbers: single-line selections write `path:12`, multi-line write
16
+ * `path:12-15`. The markdown preview cannot map rendered DOM back to
17
+ * source lines directly, so it reverse-searches the selected text in the
18
+ * source and only reports lines on an unambiguous hit (see
19
+ * {@link linesOfSelection}).
20
+ */
21
+ import { relativeTo } from './paths.ts'
22
+
23
+ /** Max inserted selection length (UTF-16 code units, i.e. JS `.length`). */
24
+ export const SELECTION_LIMIT = 500
25
+
26
+ /** The source line span a selection maps to (1-based, inclusive). */
27
+ export interface SelectionLines {
28
+ start: number
29
+ end: number
30
+ }
31
+
32
+ /**
33
+ * The fence info line: `rel[:start[-end]]` — lines are omitted entirely
34
+ * when unknown (the preview reverse-search missed).
35
+ */
36
+ export function headerOf(path: string, cwd: string | undefined, lines?: SelectionLines): string {
37
+ const rel = cwd !== undefined ? relativeTo(cwd, path) : path
38
+ if (lines === undefined) return rel
39
+ if (lines.end > lines.start) return `${rel}:${lines.start}-${lines.end}`
40
+ return `${rel}:${lines.start}`
41
+ }
42
+
43
+ /**
44
+ * The full text appended to the composer draft for one selection.
45
+ * Over the limit the content is dropped: the plain path line is the whole
46
+ * payload (an empty fenced block would just occupy the draft).
47
+ */
48
+ export function buildSelectionInsert(
49
+ path: string,
50
+ cwd: string | undefined,
51
+ lines: SelectionLines | undefined,
52
+ selected: string,
53
+ ): string {
54
+ const header = headerOf(path, cwd, lines)
55
+ if (selected.length > SELECTION_LIMIT) return header
56
+ return `\`\`\`${header}\n${selected}\n\`\`\``
57
+ }
58
+
59
+ /** 1-based line number of a character index in a text. */
60
+ function lineAt(source: string, index: number): number {
61
+ let line = 1
62
+ for (let i = 0; i < index && i < source.length; i++) {
63
+ if (source[i] === '\n') line++
64
+ }
65
+ return line
66
+ }
67
+
68
+ /**
69
+ * Reverse-map a rendered-DOM selection back to source line numbers. The
70
+ * preview selection is plain text (block boundaries come out as `\n`), so
71
+ * this is a best-effort substring search: a single trailing newline is
72
+ * stripped first (DOM block selections tend to carry one), and only an
73
+ * EXACTLY-ONE occurrence yields lines — an ambiguous or missing match
74
+ * returns null (the header then carries the path without line numbers).
75
+ */
76
+ export function linesOfSelection(source: string, selected: string): SelectionLines | null {
77
+ const text = selected.endsWith('\n') ? selected.slice(0, -1) : selected
78
+ if (text === '') return null
79
+ const at = source.indexOf(text)
80
+ if (at === -1) return null
81
+ if (source.indexOf(text, at + 1) !== -1) return null
82
+ return {
83
+ start: lineAt(source, at),
84
+ end: lineAt(source, at + Math.max(text.length - 1, 0)),
85
+ }
86
+ }