pim-agent 0.0.1 → 0.10.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 (275) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -7
  3. package/bin/pim.ts +151 -0
  4. package/package.json +82 -4
  5. package/packages/core/src/attachments/AttachmentStore.ts +99 -0
  6. package/packages/core/src/attachments/Attachments.ts +69 -0
  7. package/packages/core/src/extensions/CoreExtensions.ts +54 -0
  8. package/packages/core/src/extensions/apply-patch/coordinator.ts +53 -0
  9. package/packages/core/src/extensions/apply-patch/executor.ts +550 -0
  10. package/packages/core/src/extensions/apply-patch/index.ts +69 -0
  11. package/packages/core/src/extensions/apply-patch/matcher.ts +47 -0
  12. package/packages/core/src/extensions/apply-patch/model.ts +39 -0
  13. package/packages/core/src/extensions/apply-patch/parser.ts +379 -0
  14. package/packages/core/src/extensions/apply-patch/render.ts +177 -0
  15. package/packages/core/src/extensions/apply-patch/schema.ts +37 -0
  16. package/packages/core/src/extensions/apply-patch/types.ts +30 -0
  17. package/packages/core/src/extensions/bash/capture.ts +86 -0
  18. package/packages/core/src/extensions/bash/format.ts +110 -0
  19. package/packages/core/src/extensions/bash/image.ts +31 -0
  20. package/packages/core/src/extensions/bash/index.ts +88 -0
  21. package/packages/core/src/extensions/bash/render.ts +31 -0
  22. package/packages/core/src/extensions/bash/run.ts +221 -0
  23. package/packages/core/src/extensions/bash/schema.ts +64 -0
  24. package/packages/core/src/extensions/edit/edit.ts +366 -0
  25. package/packages/core/src/extensions/edit/index.ts +36 -0
  26. package/packages/core/src/extensions/edit/render.ts +16 -0
  27. package/packages/core/src/extensions/edit/schema.ts +37 -0
  28. package/packages/core/src/extensions/glob/glob.ts +34 -0
  29. package/packages/core/src/extensions/glob/index.ts +83 -0
  30. package/packages/core/src/extensions/glob/render.ts +72 -0
  31. package/packages/core/src/extensions/glob/schema.ts +66 -0
  32. package/packages/core/src/extensions/grep/grep.ts +256 -0
  33. package/packages/core/src/extensions/grep/index.ts +114 -0
  34. package/packages/core/src/extensions/grep/render.ts +271 -0
  35. package/packages/core/src/extensions/grep/schema.ts +92 -0
  36. package/packages/core/src/extensions/read/ImageMemory.ts +42 -0
  37. package/packages/core/src/extensions/read/image.ts +57 -0
  38. package/packages/core/src/extensions/read/index.ts +88 -0
  39. package/packages/core/src/extensions/read/read.ts +191 -0
  40. package/packages/core/src/extensions/read/render.ts +58 -0
  41. package/packages/core/src/extensions/read/schema.ts +49 -0
  42. package/packages/core/src/extensions/subagent/index.ts +33 -0
  43. package/packages/core/src/extensions/subagent/render.ts +127 -0
  44. package/packages/core/src/extensions/subagent/schema.ts +9 -0
  45. package/packages/core/src/extensions/subagent/subagent.ts +419 -0
  46. package/packages/core/src/extensions/system-prompt/index.ts +36 -0
  47. package/packages/core/src/extensions/system-prompt/prompt.ts +232 -0
  48. package/packages/core/src/extensions/todo/index.ts +123 -0
  49. package/packages/core/src/extensions/todo/render.ts +156 -0
  50. package/packages/core/src/extensions/todo/schema.ts +24 -0
  51. package/packages/core/src/extensions/todo/todo.ts +185 -0
  52. package/packages/core/src/extensions/web-fetch/JinaReaderClient.ts +187 -0
  53. package/packages/core/src/extensions/web-fetch/WebViewFetchClient.ts +183 -0
  54. package/packages/core/src/extensions/web-fetch/WebViewMarkdownSnapshot.ts +512 -0
  55. package/packages/core/src/extensions/web-fetch/fetch.ts +265 -0
  56. package/packages/core/src/extensions/web-fetch/image.ts +165 -0
  57. package/packages/core/src/extensions/web-fetch/index.ts +83 -0
  58. package/packages/core/src/extensions/web-fetch/render.ts +100 -0
  59. package/packages/core/src/extensions/web-fetch/schema.ts +46 -0
  60. package/packages/core/src/extensions/web-search/ExaMcpClient.ts +251 -0
  61. package/packages/core/src/extensions/web-search/SearchBreaker.ts +147 -0
  62. package/packages/core/src/extensions/web-search/SearchChain.ts +77 -0
  63. package/packages/core/src/extensions/web-search/index.ts +90 -0
  64. package/packages/core/src/extensions/web-search/providers/DuckDuckGoProvider.ts +195 -0
  65. package/packages/core/src/extensions/web-search/providers/ExaProvider.ts +54 -0
  66. package/packages/core/src/extensions/web-search/providers/FirecrawlProvider.ts +144 -0
  67. package/packages/core/src/extensions/web-search/providers/SearchProvider.ts +118 -0
  68. package/packages/core/src/extensions/web-search/render.ts +49 -0
  69. package/packages/core/src/extensions/web-search/schema.ts +29 -0
  70. package/packages/core/src/extensions/web-search/search.ts +23 -0
  71. package/packages/core/src/extensions/write/index.ts +58 -0
  72. package/packages/core/src/extensions/write/render.ts +16 -0
  73. package/packages/core/src/extensions/write/schema.ts +14 -0
  74. package/packages/core/src/extensions/write/write.ts +87 -0
  75. package/packages/core/src/picker/FilePickerSuggestionEngine.ts +14 -0
  76. package/packages/core/src/picker/InProcessFilePickerSuggestionEngine.ts +52 -0
  77. package/packages/core/src/picker/PickerItem.ts +6 -0
  78. package/packages/core/src/picker/PickerService.ts +132 -0
  79. package/packages/core/src/picker/RemoteFilePickerSuggestionEngine.ts +56 -0
  80. package/packages/core/src/picker/WorkerFilePickerSuggestionEngine.ts +268 -0
  81. package/packages/core/src/picker/catalog.ts +220 -0
  82. package/packages/core/src/picker/commandRanker.ts +17 -0
  83. package/packages/core/src/picker/filePickerWorker.ts +66 -0
  84. package/packages/core/src/picker/filePickerWorkerMessages.ts +39 -0
  85. package/packages/core/src/picker/ranker.ts +245 -0
  86. package/packages/core/src/picker/token.ts +77 -0
  87. package/packages/core/src/session/AgentRuntime.ts +85 -0
  88. package/packages/core/src/session/EventLog.ts +156 -0
  89. package/packages/core/src/session/MessageText.ts +21 -0
  90. package/packages/core/src/session/ReadCursors.ts +94 -0
  91. package/packages/core/src/session/SearchIndex.ts +752 -0
  92. package/packages/core/src/session/SearchTokens.ts +97 -0
  93. package/packages/core/src/session/SessionCache.ts +68 -0
  94. package/packages/core/src/session/SessionDigest.ts +161 -0
  95. package/packages/core/src/session/SessionHost.ts +892 -0
  96. package/packages/core/src/session/SessionLease.ts +279 -0
  97. package/packages/core/src/session/SessionMeta.ts +267 -0
  98. package/packages/core/src/session/SessionName.ts +18 -0
  99. package/packages/core/src/session/SessionRegistry.ts +244 -0
  100. package/packages/core/src/session/WriteMark.ts +51 -0
  101. package/packages/core/src/shared/Cli.ts +25 -0
  102. package/packages/core/src/shared/DaemonUnit.ts +13 -0
  103. package/packages/core/src/shared/DiffLines.ts +304 -0
  104. package/packages/core/src/shared/DiffPatch.ts +70 -0
  105. package/packages/core/src/shared/DiffRenderer.ts +283 -0
  106. package/packages/core/src/shared/DiffView.ts +29 -0
  107. package/packages/core/src/shared/Directories.ts +61 -0
  108. package/packages/core/src/shared/EditMatcher.ts +767 -0
  109. package/packages/core/src/shared/Errors.ts +9 -0
  110. package/packages/core/src/shared/ExtensionToggles.ts +130 -0
  111. package/packages/core/src/shared/FileEnumerator.ts +361 -0
  112. package/packages/core/src/shared/FileScanner.ts +57 -0
  113. package/packages/core/src/shared/FileWatch.ts +121 -0
  114. package/packages/core/src/shared/Format.ts +74 -0
  115. package/packages/core/src/shared/Fs.ts +91 -0
  116. package/packages/core/src/shared/FsErrors.ts +67 -0
  117. package/packages/core/src/shared/FuzzyMatcher.ts +71 -0
  118. package/packages/core/src/shared/Git.ts +530 -0
  119. package/packages/core/src/shared/GitMonitor.ts +221 -0
  120. package/packages/core/src/shared/GlobExclusions.ts +13 -0
  121. package/packages/core/src/shared/Http.ts +14 -0
  122. package/packages/core/src/shared/ImageMime.ts +36 -0
  123. package/packages/core/src/shared/Images.ts +357 -0
  124. package/packages/core/src/shared/Json.ts +19 -0
  125. package/packages/core/src/shared/Languages.ts +95 -0
  126. package/packages/core/src/shared/Levenshtein.ts +83 -0
  127. package/packages/core/src/shared/Lines.ts +79 -0
  128. package/packages/core/src/shared/McpClient.ts +444 -0
  129. package/packages/core/src/shared/OutputBudget.ts +84 -0
  130. package/packages/core/src/shared/PatchSummary.ts +40 -0
  131. package/packages/core/src/shared/Paths.ts +80 -0
  132. package/packages/core/src/shared/PimSettings.ts +128 -0
  133. package/packages/core/src/shared/PimVersion.ts +93 -0
  134. package/packages/core/src/shared/Pool.ts +26 -0
  135. package/packages/core/src/shared/Proc.ts +50 -0
  136. package/packages/core/src/shared/RateLimiter.ts +50 -0
  137. package/packages/core/src/shared/Renderer.ts +371 -0
  138. package/packages/core/src/shared/RepoDiff.ts +741 -0
  139. package/packages/core/src/shared/SafePath.ts +17 -0
  140. package/packages/core/src/shared/SearchRender.ts +82 -0
  141. package/packages/core/src/shared/SendFile.ts +23 -0
  142. package/packages/core/src/shared/SpillCache.ts +78 -0
  143. package/packages/core/src/shared/SubagentLogs.ts +102 -0
  144. package/packages/core/src/shared/Supervisor.ts +460 -0
  145. package/packages/core/src/shared/Surface.ts +2 -0
  146. package/packages/core/src/shared/Sweeper.ts +32 -0
  147. package/packages/core/src/shared/Tools.ts +801 -0
  148. package/packages/core/src/shared/Updater.ts +209 -0
  149. package/packages/core/src/view/AnsiPainter.ts +227 -0
  150. package/packages/core/src/view/BodyRenderer.ts +102 -0
  151. package/packages/core/src/view/DiffBlocks.ts +52 -0
  152. package/packages/core/src/view/DiffExpand.ts +214 -0
  153. package/packages/core/src/view/DiffLayout.ts +69 -0
  154. package/packages/core/src/view/DiffPairs.ts +47 -0
  155. package/packages/core/src/view/MarkdownPainter.ts +314 -0
  156. package/packages/core/src/view/MovePath.ts +70 -0
  157. package/packages/core/src/view/Painting.ts +182 -0
  158. package/packages/core/src/view/ViewBlock.ts +124 -0
  159. package/packages/daemon/src/Daemon.ts +63 -0
  160. package/packages/daemon/src/DaemonInstall.ts +115 -0
  161. package/packages/daemon/src/Surfaces.ts +66 -0
  162. package/packages/daemon/src/TelegramSurface.ts +30 -0
  163. package/packages/daemon/src/WebSurface.ts +56 -0
  164. package/packages/daemon/src/index.ts +37 -0
  165. package/packages/protocol/src/Command.ts +219 -0
  166. package/packages/protocol/src/Diff.ts +15 -0
  167. package/packages/protocol/src/ImageRoute.ts +23 -0
  168. package/packages/protocol/src/ServerEvent.ts +406 -0
  169. package/packages/server/src/AttachmentEndpoint.ts +184 -0
  170. package/packages/server/src/ClientConnection.ts +277 -0
  171. package/packages/server/src/ImageEndpoint.ts +36 -0
  172. package/packages/server/src/ProbeClient.ts +374 -0
  173. package/packages/server/src/Reloader.ts +93 -0
  174. package/packages/server/src/SendFileTool.ts +88 -0
  175. package/packages/server/src/SessionCatalogue.ts +456 -0
  176. package/packages/server/src/SessionProjection.ts +194 -0
  177. package/packages/server/src/SessionStream.ts +627 -0
  178. package/packages/server/src/StaticClient.ts +178 -0
  179. package/packages/server/src/WebOptions.ts +72 -0
  180. package/packages/server/src/WsGateway.ts +798 -0
  181. package/packages/server/src/probe.ts +200 -0
  182. package/packages/telegram/src/Bot.ts +229 -0
  183. package/packages/telegram/src/Commands.ts +768 -0
  184. package/packages/telegram/src/Config.ts +148 -0
  185. package/packages/telegram/src/Markdown.ts +235 -0
  186. package/packages/telegram/src/Message.ts +152 -0
  187. package/packages/telegram/src/Renderer.ts +456 -0
  188. package/packages/telegram/src/SendFileSchema.ts +18 -0
  189. package/packages/telegram/src/SendFileTool.ts +65 -0
  190. package/packages/telegram/src/Session.ts +253 -0
  191. package/packages/telegram/src/SessionRegistry.ts +126 -0
  192. package/packages/telegram/src/TaskScheduler.ts +296 -0
  193. package/packages/telegram/src/TaskSchema.ts +88 -0
  194. package/packages/telegram/src/TaskStore.ts +67 -0
  195. package/packages/telegram/src/TaskTool.ts +208 -0
  196. package/packages/telegram/src/TelegramHtml.ts +288 -0
  197. package/packages/telegram/src/TypingIndicator.ts +43 -0
  198. package/packages/telegram/src/UpdateConfirm.ts +61 -0
  199. package/packages/tui/src/extensions/_init/index.ts +144 -0
  200. package/packages/tui/src/extensions/command-picker/index.ts +75 -0
  201. package/packages/tui/src/extensions/file-picker/index.ts +156 -0
  202. package/packages/tui/src/extensions/footer/index.ts +93 -0
  203. package/packages/tui/src/extensions/footer/powerline.ts +87 -0
  204. package/packages/tui/src/extensions/footer/segments.ts +202 -0
  205. package/packages/tui/src/extensions/pim/index.ts +89 -0
  206. package/packages/tui/src/extensions/session-lease/index.ts +253 -0
  207. package/packages/tui/src/extensions/tps/index.ts +118 -0
  208. package/packages/tui/src/extensions/working-indicator/index.ts +75 -0
  209. package/packages/tui/src/extensions/wrapProvider.ts +18 -0
  210. package/packages/tui/src/themes/pim-dark.json +84 -0
  211. package/packages/tui/src/themes/pim-light.json +84 -0
  212. package/packages/tui/src/themes/themeCliArgs.ts +20 -0
  213. package/packages/web/dist/client/apple-touch-icon.png +0 -0
  214. package/packages/web/dist/client/assets/bash-LmRjUKlR.js +1 -0
  215. package/packages/web/dist/client/assets/c-DBVbJFpC.js +1 -0
  216. package/packages/web/dist/client/assets/clojure-CR7dvLJ4.js +1 -0
  217. package/packages/web/dist/client/assets/cmake-CgP6gB4W.js +1 -0
  218. package/packages/web/dist/client/assets/commit-mono-latin-300-italic-B48RJMeK.woff +0 -0
  219. package/packages/web/dist/client/assets/commit-mono-latin-300-italic-DBq5bJCE.woff2 +0 -0
  220. package/packages/web/dist/client/assets/commit-mono-latin-300-normal-B-iV2FbL.woff2 +0 -0
  221. package/packages/web/dist/client/assets/commit-mono-latin-300-normal-kM0OTZCv.woff +0 -0
  222. package/packages/web/dist/client/assets/commit-mono-latin-400-italic-BXinMwCi.woff +0 -0
  223. package/packages/web/dist/client/assets/commit-mono-latin-400-italic-DjSHLl2N.woff2 +0 -0
  224. package/packages/web/dist/client/assets/commit-mono-latin-400-normal-s0S3qwFW.woff +0 -0
  225. package/packages/web/dist/client/assets/commit-mono-latin-400-normal-wzhe4RuD.woff2 +0 -0
  226. package/packages/web/dist/client/assets/commit-mono-latin-600-normal-BuQDXT0M.woff +0 -0
  227. package/packages/web/dist/client/assets/commit-mono-latin-600-normal-SwgSWHaV.woff2 +0 -0
  228. package/packages/web/dist/client/assets/commit-mono-latin-700-normal-1-GU0IUE.woff +0 -0
  229. package/packages/web/dist/client/assets/commit-mono-latin-700-normal-DU8mrtj2.woff2 +0 -0
  230. package/packages/web/dist/client/assets/core-CA0aSzPu.js +3 -0
  231. package/packages/web/dist/client/assets/cpp-N2d8M7Nc.js +1 -0
  232. package/packages/web/dist/client/assets/csharp-yYt0Ikks.js +1 -0
  233. package/packages/web/dist/client/assets/css-D8M8GYaw.js +1 -0
  234. package/packages/web/dist/client/assets/diff-BhdZizse.js +1 -0
  235. package/packages/web/dist/client/assets/dockerfile-C0zufGsU.js +1 -0
  236. package/packages/web/dist/client/assets/elixir-DXIHlGsR.js +1 -0
  237. package/packages/web/dist/client/assets/erlang-BjYI8dhz.js +1 -0
  238. package/packages/web/dist/client/assets/go-CYstVVJV.js +1 -0
  239. package/packages/web/dist/client/assets/graphql-BHHhacaR.js +1 -0
  240. package/packages/web/dist/client/assets/haskell-DUkhXIda.js +1 -0
  241. package/packages/web/dist/client/assets/index-B9be-i9d.css +1 -0
  242. package/packages/web/dist/client/assets/index-CpinM5NK.js +45 -0
  243. package/packages/web/dist/client/assets/ini-B1CroMsf.js +1 -0
  244. package/packages/web/dist/client/assets/java-Dde-Wnb4.js +1 -0
  245. package/packages/web/dist/client/assets/javascript-8AmHuH5O.js +1 -0
  246. package/packages/web/dist/client/assets/json-PmyKWHmm.js +1 -0
  247. package/packages/web/dist/client/assets/kotlin-DgSK8bu-.js +2 -0
  248. package/packages/web/dist/client/assets/less-Bp0mgmpG.js +1 -0
  249. package/packages/web/dist/client/assets/lua-1dXYaL-H.js +1 -0
  250. package/packages/web/dist/client/assets/makefile-BX-o3EIz.js +1 -0
  251. package/packages/web/dist/client/assets/markdown-D6FrnlqJ.js +1 -0
  252. package/packages/web/dist/client/assets/ocaml-B-P1Uyq8.js +1 -0
  253. package/packages/web/dist/client/assets/perl-CvqMZYuf.js +1 -0
  254. package/packages/web/dist/client/assets/php-DuZl5e-L.js +2 -0
  255. package/packages/web/dist/client/assets/powershell-BUe16zE4.js +1 -0
  256. package/packages/web/dist/client/assets/protobuf-DGOW1qT8.js +1 -0
  257. package/packages/web/dist/client/assets/python-gHmu6VPG.js +1 -0
  258. package/packages/web/dist/client/assets/r-DNWPCnjT.js +1 -0
  259. package/packages/web/dist/client/assets/ruby-BGpZ3p4_.js +1 -0
  260. package/packages/web/dist/client/assets/rust-BIKPVoi6.js +1 -0
  261. package/packages/web/dist/client/assets/scala-C9lP4FWY.js +1 -0
  262. package/packages/web/dist/client/assets/scss-gOyOKiXd.js +1 -0
  263. package/packages/web/dist/client/assets/sql-DWRKyvJ-.js +1 -0
  264. package/packages/web/dist/client/assets/swift-BTn9__hL.js +1 -0
  265. package/packages/web/dist/client/assets/typescript-CxkPLmSc.js +1 -0
  266. package/packages/web/dist/client/assets/vim-Dhhcn_CA.js +1 -0
  267. package/packages/web/dist/client/assets/xml-COl5yFSC.js +1 -0
  268. package/packages/web/dist/client/assets/yaml-DjOhhVLG.js +1 -0
  269. package/packages/web/dist/client/favicon.svg +1 -0
  270. package/packages/web/dist/client/icon-192.png +0 -0
  271. package/packages/web/dist/client/icon-512.png +0 -0
  272. package/packages/web/dist/client/icon-maskable.png +0 -0
  273. package/packages/web/dist/client/index.html +24 -0
  274. package/packages/web/dist/client/manifest.webmanifest +21 -0
  275. package/packages/web/dist/client/wordmark.svg +1 -0
@@ -0,0 +1,214 @@
1
+ import type { ToolDiffHunk, ToolDiffLine } from "../shared/DiffLines";
2
+ import type { LineSpan } from "../shared/RepoDiff";
3
+
4
+ /**
5
+ * A stretch of a file neither side changed and no hunk shows, numbered on the
6
+ * new side. `above` and `below` say which ends of it sit against a hunk, which
7
+ * is where a reader asking for more of it is given it.
8
+ */
9
+ export type DiffGap = {
10
+ readonly start: number;
11
+ readonly end: number;
12
+ readonly count: number;
13
+ readonly above: boolean;
14
+ readonly below: boolean;
15
+ };
16
+
17
+ /** What a diff is painted from, top to bottom: hunks and the gaps between them. */
18
+ export type DiffPart =
19
+ | { readonly hunk: ToolDiffHunk }
20
+ | { readonly gap: DiffGap };
21
+
22
+ /** Lines one click reveals at each end of a gap. */
23
+ const STEP = 25;
24
+
25
+ /** A remainder this small is taken with the click rather than left as a row of its own. */
26
+ const MIN_REMAINDER = 10;
27
+
28
+ function newEnd(hunk: ToolDiffHunk): number {
29
+ return hunk.newStart + hunk.newLines - 1;
30
+ }
31
+
32
+ function oldEnd(hunk: ToolDiffHunk): number {
33
+ return hunk.oldStart + hunk.oldLines - 1;
34
+ }
35
+
36
+ function gapOf(
37
+ start: number,
38
+ end: number,
39
+ above: boolean,
40
+ below: boolean
41
+ ): readonly DiffGap[] {
42
+ return end < start
43
+ ? []
44
+ : [{ start, end, count: end - start + 1, above, below }];
45
+ }
46
+
47
+ /**
48
+ * Every gap in one file's hunks: before the first, between each pair, and
49
+ * after the last once `total` says where the file ends. A file with no new
50
+ * side to number has none of them.
51
+ */
52
+ function gaps(
53
+ hunks: readonly ToolDiffHunk[],
54
+ total?: number
55
+ ): readonly DiffGap[] {
56
+ const first = hunks[0];
57
+ const last = hunks.at(-1);
58
+ if (first === undefined || last === undefined) {
59
+ return [];
60
+ }
61
+
62
+ return [
63
+ ...gapOf(1, first.newStart - 1, false, true),
64
+ ...hunks.flatMap((hunk, index) => {
65
+ const next = hunks[index + 1];
66
+ return next === undefined
67
+ ? []
68
+ : gapOf(newEnd(hunk) + 1, next.newStart - 1, true, true);
69
+ }),
70
+ ...(total === undefined ? [] : gapOf(newEnd(last) + 1, total, true, false)),
71
+ ];
72
+ }
73
+
74
+ /**
75
+ * What to ask the server for when a reader opens a gap: a step against each
76
+ * hunk it touches, or the whole gap when what would be left of it is a sliver.
77
+ */
78
+ function spans(gap: DiffGap): readonly LineSpan[] {
79
+ const edges = (gap.above ? 1 : 0) + (gap.below ? 1 : 0);
80
+ if (gap.count <= edges * STEP + MIN_REMAINDER) {
81
+ return [{ start: gap.start, end: gap.end }];
82
+ }
83
+ return [
84
+ ...(gap.above ? [{ start: gap.start, end: gap.start + STEP - 1 }] : []),
85
+ ...(gap.below ? [{ start: gap.end - STEP + 1, end: gap.end }] : []),
86
+ ];
87
+ }
88
+
89
+ /** How many lines opening a gap would put on the page. */
90
+ function revealed(gap: DiffGap): number {
91
+ return spans(gap).reduce(
92
+ (total, span) => total + span.end - span.start + 1,
93
+ 0
94
+ );
95
+ }
96
+
97
+ /** The hunks with each gap in its place among them. */
98
+ function parts(
99
+ hunks: readonly ToolDiffHunk[],
100
+ total?: number
101
+ ): readonly DiffPart[] {
102
+ const found = gaps(hunks, total);
103
+ const tail = found.find((gap) => !gap.below);
104
+ // Keyed by the line a gap ends on, which is the line before the hunk it
105
+ // leads into: one lookup per hunk rather than a scan of every gap.
106
+ const leading = new Map(found.map((gap) => [gap.end, gap]));
107
+ return [
108
+ ...hunks.flatMap((hunk) => {
109
+ const gap = leading.get(hunk.newStart - 1);
110
+ return gap === undefined ? [{ hunk }] : [{ gap }, { hunk }];
111
+ }),
112
+ ...(tail === undefined ? [] : [{ gap: tail }]),
113
+ ];
114
+ }
115
+
116
+ type Run = {
117
+ readonly oldStart: number;
118
+ readonly newStart: number;
119
+ readonly lines: ToolDiffLine[];
120
+ /** Lines of the run that stand on the new side, counted as they are added. */
121
+ newCount: number;
122
+ };
123
+
124
+ function context(line: number, offset: number, text: string): ToolDiffLine {
125
+ return { kind: "context", oldLine: line + offset, newLine: line, text };
126
+ }
127
+
128
+ function newLinesOf(lines: readonly ToolDiffLine[]): number {
129
+ return lines.filter((line) => line.kind !== "removed").length;
130
+ }
131
+
132
+ function hunkOf(run: Run): ToolDiffHunk {
133
+ return {
134
+ oldStart: run.oldStart,
135
+ newStart: run.newStart,
136
+ oldLines: run.lines.filter((line) => line.kind !== "added").length,
137
+ newLines: run.newCount,
138
+ lines: run.lines,
139
+ };
140
+ }
141
+
142
+ /**
143
+ * The hunks with every line a reader has since been given spliced back into
144
+ * the gaps around them, and hunks that now touch merged into one. Only lines
145
+ * running from a hunk's own edge are taken: an island in the middle of a gap
146
+ * is not a thing this ever asks for.
147
+ */
148
+ function expand(
149
+ hunks: readonly ToolDiffHunk[],
150
+ known: ReadonlyMap<number, string>
151
+ ): readonly ToolDiffHunk[] {
152
+ if (known.size === 0) {
153
+ return hunks;
154
+ }
155
+
156
+ const runs: Run[] = [];
157
+
158
+ const reached = (): number => {
159
+ const last = runs.at(-1);
160
+ return last === undefined ? 0 : last.newStart + last.newCount - 1;
161
+ };
162
+
163
+ const add = (run: Run): void => {
164
+ const last = runs.at(-1);
165
+ if (last !== undefined && reached() + 1 === run.newStart) {
166
+ last.lines.push(...run.lines);
167
+ last.newCount += run.newCount;
168
+ return;
169
+ }
170
+ runs.push(run);
171
+ };
172
+
173
+ for (const [index, hunk] of hunks.entries()) {
174
+ const before: ToolDiffLine[] = [];
175
+ const floor = reached();
176
+ for (let line = hunk.newStart - 1; line > floor; line -= 1) {
177
+ const text = known.get(line);
178
+ if (text === undefined) {
179
+ break;
180
+ }
181
+ before.push(context(line, hunk.oldStart - hunk.newStart, text));
182
+ }
183
+ before.reverse();
184
+ const lines = [...before, ...hunk.lines];
185
+ add({
186
+ oldStart: hunk.oldStart - before.length,
187
+ newStart: hunk.newStart - before.length,
188
+ lines,
189
+ newCount: before.length + newLinesOf(hunk.lines),
190
+ });
191
+
192
+ const after: ToolDiffLine[] = [];
193
+ const ceiling = hunks[index + 1]?.newStart ?? Infinity;
194
+ for (let line = newEnd(hunk) + 1; line < ceiling; line += 1) {
195
+ const text = known.get(line);
196
+ if (text === undefined) {
197
+ break;
198
+ }
199
+ after.push(context(line, oldEnd(hunk) - newEnd(hunk), text));
200
+ }
201
+ if (after.length > 0) {
202
+ add({
203
+ oldStart: oldEnd(hunk) + 1,
204
+ newStart: newEnd(hunk) + 1,
205
+ lines: after,
206
+ newCount: after.length,
207
+ });
208
+ }
209
+ }
210
+
211
+ return runs.map(hunkOf);
212
+ }
213
+
214
+ export const DiffExpand = { gaps, parts, spans, revealed, expand };
@@ -0,0 +1,69 @@
1
+ import type { ToolDiffHunk, ToolDiffLine } from "../shared/DiffLines";
2
+
3
+ const TAB = " ";
4
+
5
+ function detab(text: string): string {
6
+ return text.replace(/\t/g, TAB);
7
+ }
8
+
9
+ // Highlight each side as one block, never line by line, or multi-line strings and comments mis-tokenise.
10
+ function mapSides<T>(
11
+ lines: readonly ToolDiffLine[],
12
+ highlight: (block: string) => readonly T[]
13
+ ): readonly (T | undefined)[] {
14
+ const oldIndices: (number | undefined)[] = [];
15
+ const newIndices: (number | undefined)[] = [];
16
+ const oldBlock: string[] = [];
17
+ const newBlock: string[] = [];
18
+
19
+ for (const line of lines) {
20
+ // A context line must hold a slot in both blocks to keep tokeniser state aligned.
21
+ if (line.kind !== "added") {
22
+ oldIndices.push(oldBlock.length);
23
+ oldBlock.push(line.text);
24
+ } else {
25
+ oldIndices.push(undefined);
26
+ }
27
+
28
+ if (line.kind !== "removed") {
29
+ newIndices.push(newBlock.length);
30
+ newBlock.push(line.text);
31
+ } else {
32
+ newIndices.push(undefined);
33
+ }
34
+ }
35
+
36
+ const oldText = oldBlock.length === 0 ? [] : highlight(oldBlock.join("\n"));
37
+ const newText = newBlock.length === 0 ? [] : highlight(newBlock.join("\n"));
38
+
39
+ return lines.map((line, index) => {
40
+ const side = line.kind === "removed" ? oldText : newText;
41
+ const at = line.kind === "removed" ? oldIndices[index] : newIndices[index];
42
+ return at === undefined ? undefined : side[at];
43
+ });
44
+ }
45
+
46
+ function gutterWidth(hunks: readonly ToolDiffHunk[]): number {
47
+ let max = 0;
48
+
49
+ for (const hunk of hunks) {
50
+ max = Math.max(max, hunk.oldStart + hunk.oldLines - 1);
51
+ max = Math.max(max, hunk.newStart + hunk.newLines - 1);
52
+ }
53
+
54
+ return Math.max(1, String(max).length);
55
+ }
56
+
57
+ function lineNumber(line: ToolDiffHunk["lines"][number]): number | undefined {
58
+ if (line.kind === "added") {
59
+ return line.newLine;
60
+ }
61
+
62
+ if (line.kind === "removed") {
63
+ return line.oldLine;
64
+ }
65
+
66
+ return line.newLine ?? line.oldLine;
67
+ }
68
+
69
+ export const DiffLayout = { detab, mapSides, gutterWidth, lineNumber };
@@ -0,0 +1,47 @@
1
+ import type { ToolDiffHunk, ToolDiffLine } from "../shared/DiffLines";
2
+
3
+ /** One row of a split view: the old side, the new side, or both. */
4
+ export type DiffPair = {
5
+ readonly left?: ToolDiffLine;
6
+ readonly right?: ToolDiffLine;
7
+ };
8
+
9
+ function pair(hunk: ToolDiffHunk): readonly DiffPair[] {
10
+ const pairs: DiffPair[] = [];
11
+ let removed: ToolDiffLine[] = [];
12
+ let added: ToolDiffLine[] = [];
13
+
14
+ const zip = (): void => {
15
+ const rows = Math.max(removed.length, added.length);
16
+ for (let index = 0; index < rows; index += 1) {
17
+ pairs.push({ left: removed[index], right: added[index] });
18
+ }
19
+ removed = [];
20
+ added = [];
21
+ };
22
+
23
+ for (const line of hunk.lines) {
24
+ if (line.kind === "context") {
25
+ zip();
26
+ pairs.push({ left: line, right: line });
27
+ continue;
28
+ }
29
+
30
+ if (line.kind === "added") {
31
+ added.push(line);
32
+ continue;
33
+ }
34
+
35
+ // A removal after an addition starts a new replacement rather than joining the last.
36
+ if (added.length > 0) {
37
+ zip();
38
+ }
39
+ removed.push(line);
40
+ }
41
+
42
+ zip();
43
+
44
+ return pairs;
45
+ }
46
+
47
+ export const DiffPairs = { pair };
@@ -0,0 +1,314 @@
1
+ import { basename } from "node:path";
2
+ import { Painting, type PainterMap } from "./Painting";
3
+ import type {
4
+ BlockOf,
5
+ DiffHunk,
6
+ NoticeSeverity,
7
+ Span,
8
+ Tone,
9
+ ToolIcon,
10
+ ToolView,
11
+ ViewBlock,
12
+ } from "./ViewBlock";
13
+
14
+ type Mode = "inline" | "block";
15
+
16
+ /** How a block sits in a body; `embed` payloads must never be re-wrapped. */
17
+ export type MarkdownFrame = "flow" | "embed" | "heading";
18
+
19
+ export type MarkdownGroup = {
20
+ readonly frame: MarkdownFrame;
21
+ readonly lines: readonly string[];
22
+ };
23
+
24
+ export type PaintedTool = {
25
+ readonly icon: string;
26
+ readonly lines: readonly string[];
27
+ };
28
+
29
+ const INLINE_LIMIT = 180;
30
+
31
+ const ENTITIES: Readonly<Record<string, string>> = {
32
+ "&": "&amp;",
33
+ "<": "&lt;",
34
+ ">": "&gt;",
35
+ '"': "&quot;",
36
+ };
37
+
38
+ function escape(text: string): string {
39
+ return text.replace(/[&<>"]/g, (char) => ENTITIES[char] ?? char);
40
+ }
41
+
42
+ function paint(blocks: readonly ViewBlock[]): string[] {
43
+ return blocks.flatMap((block) => paintBlock(block, "block"));
44
+ }
45
+
46
+ function paintInline(blocks: readonly ViewBlock[]): string {
47
+ return blocks
48
+ .flatMap((block) => paintBlock(block, "inline"))
49
+ .filter((line) => line !== "")
50
+ .join(" ");
51
+ }
52
+
53
+ function paintBody(blocks: readonly ViewBlock[]): MarkdownGroup[] {
54
+ return Painting.groupByFrame(blocks, Painting.FRAMES).map((group) => ({
55
+ frame: group.frame,
56
+ lines: group.blocks.flatMap((block) => paintBlock(block, "block")),
57
+ }));
58
+ }
59
+
60
+ function icon(toolIcon: ToolIcon | undefined): string {
61
+ return toolIcon === undefined ? DEFAULT_ICON : ICONS[toolIcon];
62
+ }
63
+
64
+ function paintTool(view: ToolView): PaintedTool {
65
+ const head = [view.title, view.summary ?? []]
66
+ .map((blocks) => paintInline(blocks))
67
+ .filter((text) => text !== "")
68
+ .join(" ");
69
+ const outline = outlined(view.body ?? []).flatMap((block) =>
70
+ paintBlock(block, "block")
71
+ );
72
+
73
+ return {
74
+ icon: icon(view.icon),
75
+ lines: [head, ...outline].filter((line) => line !== ""),
76
+ };
77
+ }
78
+
79
+ /** A chat row carries a body's headings and any payload that fits a line; an embedded one needs a screen. */
80
+ function outlined(blocks: readonly ViewBlock[]): readonly ViewBlock[] {
81
+ return blocks.filter((block) => {
82
+ const frame = Painting.FRAMES[block.kind];
83
+ return (
84
+ frame === "heading" ||
85
+ (frame === "flow" && Painting.WEIGHT[block.kind] === "payload")
86
+ );
87
+ });
88
+ }
89
+
90
+ const ICONS = {
91
+ file: "📄",
92
+ edit: "✏️",
93
+ trash: "🗑️",
94
+ terminal: "⚡️",
95
+ search: "🔎",
96
+ checklist: "📋",
97
+ globe: "🌐",
98
+ upload: "📤",
99
+ clock: "⏰",
100
+ robot: "🤖",
101
+ } as const satisfies Record<ToolIcon, string>;
102
+
103
+ const DEFAULT_ICON = "⚙️";
104
+
105
+ const TONE_WRAPPERS = {
106
+ default: identity,
107
+ muted: identity,
108
+ dim: identity,
109
+ error: bold,
110
+ warning: identity,
111
+ accent: identity,
112
+ added: identity,
113
+ removed: identity,
114
+ title: identity,
115
+ } as const satisfies Record<Tone, (text: string) => string>;
116
+
117
+ const NOTICE_PREFIXES = {
118
+ info: "",
119
+ warn: "⚠️ ",
120
+ error: "❌ ",
121
+ } as const satisfies Record<NoticeSeverity, string>;
122
+
123
+ function identity(text: string): string {
124
+ return text;
125
+ }
126
+
127
+ function bold(text: string): string {
128
+ return `<b>${text}</b>`;
129
+ }
130
+
131
+ function paintBlock(block: ViewBlock, mode: Mode): readonly string[] {
132
+ return Painting.dispatch(PAINTERS, block, mode);
133
+ }
134
+
135
+ function oneLine(text: string): string {
136
+ const index = text.indexOf("\n");
137
+ const first = index < 0 ? text : `${text.slice(0, index).trimEnd()} …`;
138
+ return first.length <= INLINE_LIMIT
139
+ ? first
140
+ : `${first.slice(0, INLINE_LIMIT - 1)}…`;
141
+ }
142
+
143
+ function escapeIn(text: string, mode: Mode): string {
144
+ return escape(mode === "inline" ? oneLine(text) : text);
145
+ }
146
+
147
+ function lines(text: string, mode: Mode): readonly string[] {
148
+ return mode === "inline"
149
+ ? [escapeIn(text, mode)]
150
+ : text.split("\n").map((line) => escape(line));
151
+ }
152
+
153
+ function paintText(block: BlockOf<"text">, mode: Mode): readonly string[] {
154
+ const wrap = TONE_WRAPPERS[block.tone ?? "default"];
155
+ return lines(block.text, mode).map(wrap);
156
+ }
157
+
158
+ function paintSpans(block: BlockOf<"spans">, mode: Mode): readonly string[] {
159
+ return [spansText(block.spans, mode)];
160
+ }
161
+
162
+ function spansText(spans: readonly Span[], mode: Mode): string {
163
+ return spans
164
+ .filter((span) => span.text !== "")
165
+ .map((span) => {
166
+ let text = escapeIn(span.text, mode);
167
+ if (span.code === true) {
168
+ text = `<code>${text}</code>`;
169
+ }
170
+ if (span.strike === true) {
171
+ text = `<s>${text}</s>`;
172
+ }
173
+ if (span.strong === true) {
174
+ text = bold(text);
175
+ }
176
+ return TONE_WRAPPERS[span.tone ?? "default"](text);
177
+ })
178
+ .join("");
179
+ }
180
+
181
+ function paintSection(block: BlockOf<"section">): readonly string[] {
182
+ const lead =
183
+ block.icon === undefined ? bold(escape(block.label)) : icon(block.icon);
184
+ const content = paintInline(block.content);
185
+ return [content === "" ? lead : `${lead} ${content}`];
186
+ }
187
+
188
+ function paintCode(block: BlockOf<"code">, mode: Mode): readonly string[] {
189
+ if (mode === "inline") {
190
+ return [`<code>${escapeIn(block.text, mode)}</code>`];
191
+ }
192
+ const start = block.startLine;
193
+ const body = numbered(block.text.split("\n"), start)
194
+ .map((line) => escape(line))
195
+ .join("\n");
196
+ return [preformatted(body, block.lang)];
197
+ }
198
+
199
+ function numbered(
200
+ source: readonly string[],
201
+ start: number | undefined
202
+ ): readonly string[] {
203
+ if (start === undefined) {
204
+ return source;
205
+ }
206
+ const width = Painting.lineNumberWidth(start, source.length);
207
+ return source.map(
208
+ (line, index) => `${String(start + index).padStart(width)} ${line}`
209
+ );
210
+ }
211
+
212
+ function preformatted(body: string, lang?: string): string {
213
+ return lang === undefined || lang === ""
214
+ ? `<pre>${body}</pre>`
215
+ : `<pre><code class="language-${escape(lang)}">${body}</code></pre>`;
216
+ }
217
+
218
+ function paintDiff(block: BlockOf<"diff">, mode: Mode): readonly string[] {
219
+ const body = block.hunks.flatMap((hunk) => diffLines(hunk));
220
+ if (body.length === 0) {
221
+ return [];
222
+ }
223
+ if (mode === "inline") {
224
+ return [`<code>${escapeIn(body[0] ?? "", mode)}</code>`];
225
+ }
226
+ return [preformatted(body.map((line) => escape(line)).join("\n"), "diff")];
227
+ }
228
+
229
+ function diffLines(hunk: DiffHunk): readonly string[] {
230
+ const header = `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`;
231
+ return [
232
+ header,
233
+ ...hunk.lines.map((line) => `${DIFF_MARKERS[line.kind]}${line.text}`),
234
+ ];
235
+ }
236
+
237
+ const DIFF_MARKERS = {
238
+ context: " ",
239
+ added: "+",
240
+ removed: "-",
241
+ } as const;
242
+
243
+ function paintFile(block: BlockOf<"file">, mode: Mode): readonly string[] {
244
+ const path = mode === "inline" ? basename(block.path) : block.path;
245
+ const { range, truncated } = Painting.fileSuffix(block);
246
+ return [`<code>${escape(oneLine(path) + range)}</code>${truncated}`];
247
+ }
248
+
249
+ function paintList(block: BlockOf<"list">, mode: Mode): readonly string[] {
250
+ return Painting.hangingList(block.items, block.ordered === true, (item) =>
251
+ paintBlock(item, mode)
252
+ );
253
+ }
254
+
255
+ function paintKv(block: BlockOf<"kv">, mode: Mode): readonly string[] {
256
+ return block.pairs.map(
257
+ ([key, value]) =>
258
+ `${bold(`${escapeIn(key, mode)}:`)} ${escapeIn(value, mode)}`
259
+ );
260
+ }
261
+
262
+ function paintLink(block: BlockOf<"link">, mode: Mode): readonly string[] {
263
+ const label = Painting.linkLabel(block);
264
+ return [`<a href="${escape(block.href)}">${escapeIn(label, mode)}</a>`];
265
+ }
266
+
267
+ function paintAttachment(
268
+ block: BlockOf<"attachment">,
269
+ mode: Mode
270
+ ): readonly string[] {
271
+ return [escapeIn(block.name, mode)];
272
+ }
273
+
274
+ function paintImage(block: BlockOf<"image">, mode: Mode): readonly string[] {
275
+ return [escapeIn(Painting.imageSummary(block), mode)];
276
+ }
277
+
278
+ function paintMarkdown(
279
+ block: BlockOf<"markdown">,
280
+ mode: Mode
281
+ ): readonly string[] {
282
+ return lines(block.text, mode);
283
+ }
284
+
285
+ function paintNotice(block: BlockOf<"notice">, mode: Mode): readonly string[] {
286
+ const prefix = NOTICE_PREFIXES[block.severity];
287
+ return lines(block.text, mode).map((line) => `${prefix}${line}`);
288
+ }
289
+
290
+ const PAINTERS: PainterMap<readonly string[], [Mode]> = {
291
+ text: paintText,
292
+ markdown: paintMarkdown,
293
+ spans: paintSpans,
294
+ section: paintSection,
295
+ code: paintCode,
296
+ diff: paintDiff,
297
+ file: paintFile,
298
+ list: paintList,
299
+ kv: paintKv,
300
+ link: paintLink,
301
+ attachment: paintAttachment,
302
+ image: paintImage,
303
+ notice: paintNotice,
304
+ };
305
+
306
+ /** Paints a `ViewBlock` tree to Telegram-flavoured HTML; the caller joins lines. */
307
+ export const MarkdownPainter = {
308
+ escape,
309
+ paint,
310
+ paintInline,
311
+ paintBody,
312
+ icon,
313
+ paintTool,
314
+ };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * A file that moved, read as one path rather than two. Everything the old and
3
+ * new paths agree on is said once, and only the segments that actually changed
4
+ * are named on both sides — `packages/{web ➝ tui}/src/App.tsx` rather than two
5
+ * near-identical paths a reader has to diff by eye.
6
+ */
7
+ export type MoveParts = {
8
+ /** The leading directories both paths share, with its trailing `/`. */
9
+ readonly prefix: string;
10
+ /** What those segments used to be. */
11
+ readonly from: string;
12
+ /** What they are now. */
13
+ readonly to: string;
14
+ /** The trailing segments both paths share, with its leading `/`. */
15
+ readonly suffix: string;
16
+ };
17
+
18
+ /** The arrow every surface draws a move with. */
19
+ const ARROW = "➝";
20
+
21
+ /**
22
+ * The shared head and tail of a move, or `undefined` when there is nothing
23
+ * worth folding: two paths that share no segment read better whole, since a
24
+ * brace around the entire path only adds punctuation. Two bare filenames are
25
+ * the exception — `{old.ts ➝ new.ts}` shares no segment but is still one name.
26
+ */
27
+ function fold(oldPath: string, newPath: string): MoveParts | undefined {
28
+ const oldParts = oldPath.split("/");
29
+ const newParts = newPath.split("/");
30
+
31
+ let head = 0;
32
+ while (
33
+ head < oldParts.length &&
34
+ head < newParts.length &&
35
+ oldParts[head] === newParts[head]
36
+ ) {
37
+ head += 1;
38
+ }
39
+
40
+ let tail = 0;
41
+ while (
42
+ tail < oldParts.length - head &&
43
+ tail < newParts.length - head &&
44
+ oldParts[oldParts.length - tail - 1] ===
45
+ newParts[newParts.length - tail - 1]
46
+ ) {
47
+ tail += 1;
48
+ }
49
+
50
+ const from = oldParts.slice(head, oldParts.length - tail);
51
+ const to = newParts.slice(head, newParts.length - tail);
52
+ const bareNames = oldParts.length === 1 && newParts.length === 1;
53
+
54
+ if (
55
+ from.length === 0 ||
56
+ to.length === 0 ||
57
+ (head === 0 && tail === 0 && !bareNames)
58
+ ) {
59
+ return undefined;
60
+ }
61
+
62
+ return {
63
+ prefix: head > 0 ? `${oldParts.slice(0, head).join("/")}/` : "",
64
+ from: from.join("/"),
65
+ to: to.join("/"),
66
+ suffix: tail > 0 ? `/${oldParts.slice(-tail).join("/")}` : "",
67
+ };
68
+ }
69
+
70
+ export const MovePath = { ARROW, fold };