pim-agent 0.0.1 → 0.9.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 (261) 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 +51 -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 +216 -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 +28 -0
  47. package/packages/core/src/extensions/system-prompt/prompt.ts +228 -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 +110 -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 +70 -0
  87. package/packages/core/src/session/AgentRuntime.ts +85 -0
  88. package/packages/core/src/session/EventLog.ts +213 -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/SessionCache.ts +68 -0
  92. package/packages/core/src/session/SessionHost.ts +876 -0
  93. package/packages/core/src/session/SessionLease.ts +279 -0
  94. package/packages/core/src/session/SessionRegistry.ts +169 -0
  95. package/packages/core/src/session/WriteMark.ts +37 -0
  96. package/packages/core/src/shared/Cli.ts +25 -0
  97. package/packages/core/src/shared/DaemonUnit.ts +13 -0
  98. package/packages/core/src/shared/DiffLines.ts +304 -0
  99. package/packages/core/src/shared/DiffRenderer.ts +283 -0
  100. package/packages/core/src/shared/DiffView.ts +29 -0
  101. package/packages/core/src/shared/Directories.ts +61 -0
  102. package/packages/core/src/shared/EditMatcher.ts +767 -0
  103. package/packages/core/src/shared/Errors.ts +9 -0
  104. package/packages/core/src/shared/ExtensionToggles.ts +130 -0
  105. package/packages/core/src/shared/FileEnumerator.ts +361 -0
  106. package/packages/core/src/shared/FileScanner.ts +57 -0
  107. package/packages/core/src/shared/FileWatch.ts +107 -0
  108. package/packages/core/src/shared/Format.ts +74 -0
  109. package/packages/core/src/shared/Fs.ts +91 -0
  110. package/packages/core/src/shared/FsErrors.ts +67 -0
  111. package/packages/core/src/shared/FuzzyMatcher.ts +71 -0
  112. package/packages/core/src/shared/Git.ts +89 -0
  113. package/packages/core/src/shared/GlobExclusions.ts +13 -0
  114. package/packages/core/src/shared/Http.ts +14 -0
  115. package/packages/core/src/shared/ImageMime.ts +36 -0
  116. package/packages/core/src/shared/Images.ts +357 -0
  117. package/packages/core/src/shared/Json.ts +19 -0
  118. package/packages/core/src/shared/Languages.ts +95 -0
  119. package/packages/core/src/shared/Levenshtein.ts +30 -0
  120. package/packages/core/src/shared/Lines.ts +79 -0
  121. package/packages/core/src/shared/McpClient.ts +444 -0
  122. package/packages/core/src/shared/OutputBudget.ts +84 -0
  123. package/packages/core/src/shared/PatchSummary.ts +40 -0
  124. package/packages/core/src/shared/Paths.ts +80 -0
  125. package/packages/core/src/shared/PimSettings.ts +128 -0
  126. package/packages/core/src/shared/PimVersion.ts +93 -0
  127. package/packages/core/src/shared/Pool.ts +26 -0
  128. package/packages/core/src/shared/Proc.ts +30 -0
  129. package/packages/core/src/shared/RateLimiter.ts +50 -0
  130. package/packages/core/src/shared/Renderer.ts +371 -0
  131. package/packages/core/src/shared/SafePath.ts +17 -0
  132. package/packages/core/src/shared/SearchRender.ts +82 -0
  133. package/packages/core/src/shared/SendFile.ts +23 -0
  134. package/packages/core/src/shared/SpillCache.ts +78 -0
  135. package/packages/core/src/shared/SubagentLogs.ts +102 -0
  136. package/packages/core/src/shared/Supervisor.ts +460 -0
  137. package/packages/core/src/shared/Sweeper.ts +32 -0
  138. package/packages/core/src/shared/Tools.ts +801 -0
  139. package/packages/core/src/shared/Updater.ts +193 -0
  140. package/packages/core/src/view/AnsiPainter.ts +227 -0
  141. package/packages/core/src/view/BodyRenderer.ts +102 -0
  142. package/packages/core/src/view/DiffBlocks.ts +52 -0
  143. package/packages/core/src/view/DiffLayout.ts +69 -0
  144. package/packages/core/src/view/MarkdownPainter.ts +314 -0
  145. package/packages/core/src/view/Painting.ts +182 -0
  146. package/packages/core/src/view/ViewBlock.ts +124 -0
  147. package/packages/daemon/src/Daemon.ts +63 -0
  148. package/packages/daemon/src/DaemonInstall.ts +115 -0
  149. package/packages/daemon/src/Surfaces.ts +66 -0
  150. package/packages/daemon/src/TelegramSurface.ts +30 -0
  151. package/packages/daemon/src/WebSurface.ts +57 -0
  152. package/packages/daemon/src/index.ts +37 -0
  153. package/packages/protocol/src/Command.ts +92 -0
  154. package/packages/protocol/src/ImageRoute.ts +23 -0
  155. package/packages/protocol/src/Protocol.ts +6 -0
  156. package/packages/protocol/src/ServerEvent.ts +251 -0
  157. package/packages/server/src/AttachmentEndpoint.ts +184 -0
  158. package/packages/server/src/ClientConnection.ts +265 -0
  159. package/packages/server/src/ImageEndpoint.ts +36 -0
  160. package/packages/server/src/ProbeClient.ts +312 -0
  161. package/packages/server/src/Reloader.ts +93 -0
  162. package/packages/server/src/SendFileTool.ts +88 -0
  163. package/packages/server/src/SessionCatalogue.ts +202 -0
  164. package/packages/server/src/SessionProjection.ts +194 -0
  165. package/packages/server/src/SessionStream.ts +601 -0
  166. package/packages/server/src/StaticClient.ts +95 -0
  167. package/packages/server/src/WebOptions.ts +72 -0
  168. package/packages/server/src/WsGateway.ts +563 -0
  169. package/packages/server/src/probe.ts +146 -0
  170. package/packages/telegram/src/Bot.ts +229 -0
  171. package/packages/telegram/src/Commands.ts +768 -0
  172. package/packages/telegram/src/Config.ts +148 -0
  173. package/packages/telegram/src/Markdown.ts +235 -0
  174. package/packages/telegram/src/Message.ts +152 -0
  175. package/packages/telegram/src/Renderer.ts +456 -0
  176. package/packages/telegram/src/SendFileSchema.ts +18 -0
  177. package/packages/telegram/src/SendFileTool.ts +65 -0
  178. package/packages/telegram/src/Session.ts +252 -0
  179. package/packages/telegram/src/SessionRegistry.ts +126 -0
  180. package/packages/telegram/src/TaskScheduler.ts +296 -0
  181. package/packages/telegram/src/TaskSchema.ts +88 -0
  182. package/packages/telegram/src/TaskStore.ts +67 -0
  183. package/packages/telegram/src/TaskTool.ts +208 -0
  184. package/packages/telegram/src/TelegramHtml.ts +288 -0
  185. package/packages/telegram/src/TypingIndicator.ts +43 -0
  186. package/packages/telegram/src/UpdateConfirm.ts +61 -0
  187. package/packages/tui/src/extensions/_init/index.ts +144 -0
  188. package/packages/tui/src/extensions/command-picker/index.ts +43 -0
  189. package/packages/tui/src/extensions/file-picker/index.ts +156 -0
  190. package/packages/tui/src/extensions/footer/index.ts +117 -0
  191. package/packages/tui/src/extensions/footer/powerline.ts +87 -0
  192. package/packages/tui/src/extensions/footer/segments.ts +202 -0
  193. package/packages/tui/src/extensions/pim/index.ts +89 -0
  194. package/packages/tui/src/extensions/session-lease/index.ts +248 -0
  195. package/packages/tui/src/extensions/tps/index.ts +118 -0
  196. package/packages/tui/src/extensions/working-indicator/index.ts +75 -0
  197. package/packages/tui/src/extensions/wrapProvider.ts +18 -0
  198. package/packages/tui/src/themes/pim-dark.json +84 -0
  199. package/packages/tui/src/themes/pim-light.json +84 -0
  200. package/packages/tui/src/themes/themeCliArgs.ts +20 -0
  201. package/packages/web/dist/client/apple-touch-icon.png +0 -0
  202. package/packages/web/dist/client/assets/bash-LmRjUKlR.js +1 -0
  203. package/packages/web/dist/client/assets/c-DBVbJFpC.js +1 -0
  204. package/packages/web/dist/client/assets/clojure-CR7dvLJ4.js +1 -0
  205. package/packages/web/dist/client/assets/cmake-CgP6gB4W.js +1 -0
  206. package/packages/web/dist/client/assets/commit-mono-latin-300-italic-B48RJMeK.woff +0 -0
  207. package/packages/web/dist/client/assets/commit-mono-latin-300-italic-DBq5bJCE.woff2 +0 -0
  208. package/packages/web/dist/client/assets/commit-mono-latin-400-italic-BXinMwCi.woff +0 -0
  209. package/packages/web/dist/client/assets/commit-mono-latin-400-italic-DjSHLl2N.woff2 +0 -0
  210. package/packages/web/dist/client/assets/commit-mono-latin-400-normal-s0S3qwFW.woff +0 -0
  211. package/packages/web/dist/client/assets/commit-mono-latin-400-normal-wzhe4RuD.woff2 +0 -0
  212. package/packages/web/dist/client/assets/commit-mono-latin-600-normal-BuQDXT0M.woff +0 -0
  213. package/packages/web/dist/client/assets/commit-mono-latin-600-normal-SwgSWHaV.woff2 +0 -0
  214. package/packages/web/dist/client/assets/commit-mono-latin-700-normal-1-GU0IUE.woff +0 -0
  215. package/packages/web/dist/client/assets/commit-mono-latin-700-normal-DU8mrtj2.woff2 +0 -0
  216. package/packages/web/dist/client/assets/core-CGdQx9la.js +3 -0
  217. package/packages/web/dist/client/assets/cpp-N2d8M7Nc.js +1 -0
  218. package/packages/web/dist/client/assets/csharp-yYt0Ikks.js +1 -0
  219. package/packages/web/dist/client/assets/css-D8M8GYaw.js +1 -0
  220. package/packages/web/dist/client/assets/diff-BhdZizse.js +1 -0
  221. package/packages/web/dist/client/assets/dockerfile-C0zufGsU.js +1 -0
  222. package/packages/web/dist/client/assets/elixir-DXIHlGsR.js +1 -0
  223. package/packages/web/dist/client/assets/erlang-BjYI8dhz.js +1 -0
  224. package/packages/web/dist/client/assets/go-CYstVVJV.js +1 -0
  225. package/packages/web/dist/client/assets/graphql-BHHhacaR.js +1 -0
  226. package/packages/web/dist/client/assets/haskell-DUkhXIda.js +1 -0
  227. package/packages/web/dist/client/assets/index-BMNwleWO.js +40 -0
  228. package/packages/web/dist/client/assets/index-CqjB2RPr.css +1 -0
  229. package/packages/web/dist/client/assets/ini-B1CroMsf.js +1 -0
  230. package/packages/web/dist/client/assets/java-Dde-Wnb4.js +1 -0
  231. package/packages/web/dist/client/assets/javascript-8AmHuH5O.js +1 -0
  232. package/packages/web/dist/client/assets/json-PmyKWHmm.js +1 -0
  233. package/packages/web/dist/client/assets/kotlin-DgSK8bu-.js +2 -0
  234. package/packages/web/dist/client/assets/less-Bp0mgmpG.js +1 -0
  235. package/packages/web/dist/client/assets/lua-1dXYaL-H.js +1 -0
  236. package/packages/web/dist/client/assets/makefile-BX-o3EIz.js +1 -0
  237. package/packages/web/dist/client/assets/markdown-D6FrnlqJ.js +1 -0
  238. package/packages/web/dist/client/assets/ocaml-B-P1Uyq8.js +1 -0
  239. package/packages/web/dist/client/assets/perl-CvqMZYuf.js +1 -0
  240. package/packages/web/dist/client/assets/php-DuZl5e-L.js +2 -0
  241. package/packages/web/dist/client/assets/powershell-BUe16zE4.js +1 -0
  242. package/packages/web/dist/client/assets/protobuf-DGOW1qT8.js +1 -0
  243. package/packages/web/dist/client/assets/python-gHmu6VPG.js +1 -0
  244. package/packages/web/dist/client/assets/r-DNWPCnjT.js +1 -0
  245. package/packages/web/dist/client/assets/ruby-BGpZ3p4_.js +1 -0
  246. package/packages/web/dist/client/assets/rust-BIKPVoi6.js +1 -0
  247. package/packages/web/dist/client/assets/scala-C9lP4FWY.js +1 -0
  248. package/packages/web/dist/client/assets/scss-gOyOKiXd.js +1 -0
  249. package/packages/web/dist/client/assets/sql-DWRKyvJ-.js +1 -0
  250. package/packages/web/dist/client/assets/swift-BTn9__hL.js +1 -0
  251. package/packages/web/dist/client/assets/typescript-CxkPLmSc.js +1 -0
  252. package/packages/web/dist/client/assets/vim-Dhhcn_CA.js +1 -0
  253. package/packages/web/dist/client/assets/xml-COl5yFSC.js +1 -0
  254. package/packages/web/dist/client/assets/yaml-DjOhhVLG.js +1 -0
  255. package/packages/web/dist/client/favicon.svg +1 -0
  256. package/packages/web/dist/client/icon-192.png +0 -0
  257. package/packages/web/dist/client/icon-512.png +0 -0
  258. package/packages/web/dist/client/icon-maskable.png +0 -0
  259. package/packages/web/dist/client/index.html +24 -0
  260. package/packages/web/dist/client/manifest.webmanifest +21 -0
  261. package/packages/web/dist/client/wordmark.svg +1 -0
@@ -0,0 +1,304 @@
1
+ import * as Diff from "diff";
2
+
3
+ export type ToolDiffLineKind = "context" | "added" | "removed";
4
+
5
+ export type IntraLineRange = {
6
+ readonly start: number;
7
+ readonly end: number;
8
+ };
9
+
10
+ export type ToolDiffLine = {
11
+ readonly kind: ToolDiffLineKind;
12
+ readonly oldLine?: number;
13
+ readonly newLine?: number;
14
+ readonly text: string;
15
+ readonly emphasis?: readonly IntraLineRange[];
16
+ };
17
+
18
+ export type ToolDiffHunk = {
19
+ readonly oldStart: number;
20
+ readonly oldLines: number;
21
+ readonly newStart: number;
22
+ readonly newLines: number;
23
+ readonly lines: readonly ToolDiffLine[];
24
+ };
25
+
26
+ export type ToolDiff = {
27
+ readonly path: string;
28
+ readonly hunks: readonly ToolDiffHunk[];
29
+ };
30
+
31
+ // Keep the EOF-newline flag out of `lines`, or `joinComparable` emits a phantom blank line.
32
+ export type ToolDiffSide = {
33
+ readonly lines: readonly string[];
34
+ readonly hasTrailingNewline: boolean;
35
+ };
36
+
37
+ function buildToolDiff(
38
+ path: string,
39
+ oldSide: ToolDiffSide,
40
+ newSide: ToolDiffSide,
41
+ contextSize: number
42
+ ): ToolDiff | undefined {
43
+ if (contextSize < 0 || !Number.isFinite(contextSize)) {
44
+ throw new Error(
45
+ `contextSize must be a non-negative finite number, got ${contextSize}.`
46
+ );
47
+ }
48
+
49
+ const lines = build(oldSide.lines, newSide.lines);
50
+
51
+ if (!lines.some((line) => line.kind !== "context")) {
52
+ return undefined;
53
+ }
54
+
55
+ const emphasized = attachEmphasis(lines);
56
+
57
+ return {
58
+ path,
59
+ hunks: buildHunks(emphasized, contextSize),
60
+ };
61
+ }
62
+
63
+ function fromText(text: string): ToolDiffSide {
64
+ if (text.length === 0) {
65
+ return emptySide;
66
+ }
67
+
68
+ const hasTrailingNewline = text.endsWith("\n");
69
+ const parts = text.split("\n");
70
+
71
+ if (hasTrailingNewline) {
72
+ parts.pop();
73
+ }
74
+
75
+ return { lines: parts, hasTrailingNewline };
76
+ }
77
+
78
+ function attachEmphasis(
79
+ lines: readonly ToolDiffLine[]
80
+ ): readonly ToolDiffLine[] {
81
+ const result: ToolDiffLine[] = [...lines];
82
+ let i = 0;
83
+
84
+ while (i < result.length) {
85
+ if (result[i]?.kind !== "removed") {
86
+ i += 1;
87
+ continue;
88
+ }
89
+
90
+ let removedEnd = i;
91
+ while (
92
+ removedEnd < result.length &&
93
+ result[removedEnd]?.kind === "removed"
94
+ ) {
95
+ removedEnd += 1;
96
+ }
97
+
98
+ let addedEnd = removedEnd;
99
+ while (addedEnd < result.length && result[addedEnd]?.kind === "added") {
100
+ addedEnd += 1;
101
+ }
102
+
103
+ const removedCount = removedEnd - i;
104
+ const addedCount = addedEnd - removedEnd;
105
+
106
+ if (removedCount > 0 && removedCount === addedCount) {
107
+ for (let k = 0; k < removedCount; k += 1) {
108
+ const removed = result[i + k];
109
+ const added = result[removedEnd + k];
110
+
111
+ if (removed === undefined || added === undefined) {
112
+ continue;
113
+ }
114
+
115
+ const ranges = computeIntraLineRanges(removed.text, added.text);
116
+
117
+ if (ranges === undefined) {
118
+ continue;
119
+ }
120
+
121
+ result[i + k] = { ...removed, emphasis: ranges.removed };
122
+ result[removedEnd + k] = { ...added, emphasis: ranges.added };
123
+ }
124
+ }
125
+
126
+ i = addedEnd > i ? addedEnd : i + 1;
127
+ }
128
+
129
+ return result;
130
+ }
131
+
132
+ function computeIntraLineRanges(
133
+ oldText: string,
134
+ newText: string
135
+ ):
136
+ | {
137
+ readonly removed: readonly IntraLineRange[];
138
+ readonly added: readonly IntraLineRange[];
139
+ }
140
+ | undefined {
141
+ const parts = Diff.diffWords(oldText, newText);
142
+ const removedRanges: IntraLineRange[] = [];
143
+ const addedRanges: IntraLineRange[] = [];
144
+ let oldPos = 0;
145
+ let newPos = 0;
146
+ let sharedLen = 0;
147
+ let firstRemoved = true;
148
+ let firstAdded = true;
149
+
150
+ for (const part of parts) {
151
+ const len = part.value.length;
152
+
153
+ if (part.added === true) {
154
+ const leading = firstAdded ? leadingWsLen(part.value) : 0;
155
+ firstAdded = false;
156
+ if (len - leading > 0) {
157
+ addedRanges.push({ start: newPos + leading, end: newPos + len });
158
+ }
159
+ newPos += len;
160
+ continue;
161
+ }
162
+
163
+ if (part.removed === true) {
164
+ const leading = firstRemoved ? leadingWsLen(part.value) : 0;
165
+ firstRemoved = false;
166
+ if (len - leading > 0) {
167
+ removedRanges.push({ start: oldPos + leading, end: oldPos + len });
168
+ }
169
+ oldPos += len;
170
+ continue;
171
+ }
172
+
173
+ sharedLen += len;
174
+ oldPos += len;
175
+ newPos += len;
176
+ }
177
+
178
+ if (sharedLen === 0) {
179
+ return undefined;
180
+ }
181
+
182
+ return { removed: removedRanges, added: addedRanges };
183
+ }
184
+
185
+ function build(
186
+ oldLines: readonly string[],
187
+ newLines: readonly string[]
188
+ ): readonly ToolDiffLine[] {
189
+ const parts = Diff.diffLines(
190
+ joinComparable(oldLines),
191
+ joinComparable(newLines)
192
+ );
193
+ const lines: ToolDiffLine[] = [];
194
+ let oldLine = 1;
195
+ let newLine = 1;
196
+
197
+ for (const part of parts) {
198
+ const values = partLines(part.value);
199
+
200
+ for (const text of values) {
201
+ if (part.added === true) {
202
+ lines.push({ kind: "added", newLine, text });
203
+ newLine += 1;
204
+ continue;
205
+ }
206
+
207
+ if (part.removed === true) {
208
+ lines.push({ kind: "removed", oldLine, text });
209
+ oldLine += 1;
210
+ continue;
211
+ }
212
+
213
+ lines.push({ kind: "context", oldLine, newLine, text });
214
+ oldLine += 1;
215
+ newLine += 1;
216
+ }
217
+ }
218
+
219
+ return lines;
220
+ }
221
+
222
+ function buildHunks(
223
+ lines: readonly ToolDiffLine[],
224
+ contextSize: number
225
+ ): readonly ToolDiffHunk[] {
226
+ const changeIndexes = lines
227
+ .map((line, index) => (line.kind === "context" ? -1 : index))
228
+ .filter((index) => index >= 0);
229
+ const hunks: ToolDiffHunk[] = [];
230
+ let firstChange = changeIndexes[0];
231
+ let lastChange = changeIndexes[0];
232
+
233
+ if (firstChange === undefined || lastChange === undefined) {
234
+ return hunks;
235
+ }
236
+
237
+ const pushHunk = (startChange: number, endChange: number): void => {
238
+ const hunkLines = lines.slice(
239
+ Math.max(0, startChange - contextSize),
240
+ Math.min(lines.length, endChange + contextSize + 1)
241
+ );
242
+ const oldNumbers = hunkLines.flatMap((line) =>
243
+ line.oldLine === undefined ? [] : [line.oldLine]
244
+ );
245
+ const newNumbers = hunkLines.flatMap((line) =>
246
+ line.newLine === undefined ? [] : [line.newLine]
247
+ );
248
+ const oldStart =
249
+ oldNumbers.length === 0
250
+ ? Math.max(0, (newNumbers[0] ?? 1) - 1)
251
+ : Math.min(...oldNumbers);
252
+ const newStart =
253
+ newNumbers.length === 0
254
+ ? Math.max(0, (oldNumbers[0] ?? 1) - 1)
255
+ : Math.min(...newNumbers);
256
+
257
+ hunks.push({
258
+ oldStart,
259
+ oldLines:
260
+ oldNumbers.length === 0 ? 0 : Math.max(...oldNumbers) - oldStart + 1,
261
+ newStart,
262
+ newLines:
263
+ newNumbers.length === 0 ? 0 : Math.max(...newNumbers) - newStart + 1,
264
+ lines: hunkLines,
265
+ });
266
+ };
267
+
268
+ for (const changeIndex of changeIndexes.slice(1)) {
269
+ if (changeIndex - lastChange <= contextSize * 2 + 1) {
270
+ lastChange = changeIndex;
271
+ continue;
272
+ }
273
+
274
+ pushHunk(firstChange, lastChange);
275
+ firstChange = changeIndex;
276
+ lastChange = changeIndex;
277
+ }
278
+
279
+ pushHunk(firstChange, lastChange);
280
+
281
+ return hunks;
282
+ }
283
+
284
+ function leadingWsLen(value: string): number {
285
+ return value.match(/^\s*/)?.[0].length ?? 0;
286
+ }
287
+
288
+ function joinComparable(lines: readonly string[]): string {
289
+ return lines.length === 0 ? "" : `${lines.join("\n")}\n`;
290
+ }
291
+
292
+ function partLines(value: string): readonly string[] {
293
+ const lines = value.split("\n");
294
+
295
+ if (lines.at(-1) === "") {
296
+ lines.pop();
297
+ }
298
+
299
+ return lines;
300
+ }
301
+
302
+ const emptySide: ToolDiffSide = { lines: [], hasTrailingNewline: false };
303
+
304
+ export const DiffLines = { buildToolDiff, fromText, emptySide };
@@ -0,0 +1,283 @@
1
+ import { highlightCode, type Theme } from "@earendil-works/pi-coding-agent";
2
+ import { DiffLayout } from "../view/DiffLayout";
3
+ import type {
4
+ IntraLineRange,
5
+ ToolDiff,
6
+ ToolDiffHunk,
7
+ ToolDiffLine,
8
+ } from "./DiffLines";
9
+ import { Languages } from "./Languages";
10
+
11
+ export type DiffRenderOptions = {
12
+ readonly toolDiff: ToolDiff;
13
+ readonly theme: Theme;
14
+ };
15
+
16
+ export type DiffHighlighter = (block: string) => readonly string[];
17
+
18
+ type DiffBackgrounds = {
19
+ readonly added: string;
20
+ readonly removed: string;
21
+ readonly addedEmph: string;
22
+ readonly removedEmph: string;
23
+ };
24
+
25
+ const DARK_BG: DiffBackgrounds = {
26
+ added: "\x1b[48;2;13;40;24m",
27
+ removed: "\x1b[48;2;58;20;20m",
28
+ addedEmph: "\x1b[48;2;26;81;47m",
29
+ removedEmph: "\x1b[48;2;100;35;35m",
30
+ };
31
+ const LIGHT_BG: DiffBackgrounds = {
32
+ added: "\x1b[48;2;218;251;225m",
33
+ removed: "\x1b[48;2;255;235;233m",
34
+ addedEmph: "\x1b[48;2;172;238;187m",
35
+ removedEmph: "\x1b[48;2;255;195;188m",
36
+ };
37
+ const CLEAR_TO_EOL = "\x1b[K";
38
+ const BG_RESET = "\x1b[49m";
39
+
40
+ const KINDS = {
41
+ context: { sign: " ", role: "toolDiffContext" },
42
+ added: {
43
+ sign: "+",
44
+ role: "toolDiffAdded",
45
+ bg: "added",
46
+ emph: "addedEmph",
47
+ },
48
+ removed: {
49
+ sign: "−",
50
+ role: "toolDiffRemoved",
51
+ bg: "removed",
52
+ emph: "removedEmph",
53
+ },
54
+ } as const satisfies Record<
55
+ ToolDiffLine["kind"],
56
+ {
57
+ readonly sign: string;
58
+ readonly role: string;
59
+ readonly bg?: keyof DiffBackgrounds;
60
+ readonly emph?: keyof DiffBackgrounds;
61
+ }
62
+ >;
63
+
64
+ function renderLines(options: DiffRenderOptions): string[] {
65
+ const { toolDiff, theme } = options;
66
+
67
+ if (toolDiff.hunks.length === 0) {
68
+ return [];
69
+ }
70
+
71
+ const highlighter = makeHighlighter(Languages.fromPath(toolDiff.path));
72
+ const numberWidth = DiffLayout.gutterWidth(toolDiff.hunks);
73
+ const backgrounds = backgroundsFor(theme);
74
+ const separator = renderHunkSeparator(theme, numberWidth);
75
+ const lines: string[] = [];
76
+
77
+ for (const [index, hunk] of toolDiff.hunks.entries()) {
78
+ if (index > 0) {
79
+ lines.push(separator);
80
+ }
81
+
82
+ lines.push(
83
+ ...renderHunk(hunk, highlighter, theme, numberWidth, backgrounds)
84
+ );
85
+ }
86
+
87
+ return lines;
88
+ }
89
+
90
+ function render(options: DiffRenderOptions): string {
91
+ return renderLines(options).join("\n");
92
+ }
93
+
94
+ function highlightHunkLines(
95
+ hunk: ToolDiffHunk,
96
+ highlighter: DiffHighlighter
97
+ ): readonly string[] {
98
+ const highlighted = DiffLayout.mapSides(hunk, highlighter);
99
+ return hunk.lines.map((line, index) => highlighted[index] ?? line.text);
100
+ }
101
+
102
+ function makeHighlighter(lang: string | undefined): DiffHighlighter {
103
+ if (lang === undefined) {
104
+ return (block) => DiffLayout.detab(block).split("\n");
105
+ }
106
+
107
+ return (block) => highlightCode(DiffLayout.detab(block), lang);
108
+ }
109
+
110
+ function backgroundsFor(theme: Theme): DiffBackgrounds {
111
+ return isLightTheme(theme) ? LIGHT_BG : DARK_BG;
112
+ }
113
+
114
+ function isLightTheme(theme: Theme): boolean {
115
+ const name = theme.name?.toLowerCase() ?? "";
116
+ return name === "light" || name.includes("light");
117
+ }
118
+
119
+ function renderHunk(
120
+ hunk: ToolDiffHunk,
121
+ highlighter: DiffHighlighter,
122
+ theme: Theme,
123
+ numberWidth: number,
124
+ backgrounds: DiffBackgrounds
125
+ ): readonly string[] {
126
+ const highlightedLines = highlightHunkLines(hunk, highlighter);
127
+
128
+ return hunk.lines.map((line, i) =>
129
+ renderLine(
130
+ line,
131
+ highlightedLines[i] ?? line.text,
132
+ theme,
133
+ numberWidth,
134
+ backgrounds
135
+ )
136
+ );
137
+ }
138
+
139
+ function renderLine(
140
+ line: ToolDiffLine,
141
+ content: string,
142
+ theme: Theme,
143
+ numberWidth: number,
144
+ backgrounds: DiffBackgrounds
145
+ ): string {
146
+ const prefix = formatPrefix(line, theme, numberWidth);
147
+ const emphasized = applyLineEmphasis(line, content, backgrounds);
148
+ return applyBackground(line.kind, ` ${prefix}${emphasized}`, backgrounds);
149
+ }
150
+
151
+ function applyLineEmphasis(
152
+ line: ToolDiffLine,
153
+ content: string,
154
+ backgrounds: DiffBackgrounds
155
+ ): string {
156
+ const ranges = line.emphasis;
157
+ const kind = KINDS[line.kind];
158
+
159
+ if (ranges === undefined || ranges.length === 0 || !("emph" in kind)) {
160
+ return content;
161
+ }
162
+
163
+ return applyEmphasis(
164
+ content,
165
+ ranges,
166
+ backgrounds[kind.bg],
167
+ backgrounds[kind.emph]
168
+ );
169
+ }
170
+
171
+ function applyEmphasis(
172
+ text: string,
173
+ ranges: readonly IntraLineRange[],
174
+ lineBg: string,
175
+ emphBg: string
176
+ ): string {
177
+ if (ranges.length === 0) {
178
+ return text;
179
+ }
180
+
181
+ const starts = new Set<number>();
182
+ const ends = new Set<number>();
183
+
184
+ for (const range of ranges) {
185
+ if (range.end > range.start) {
186
+ starts.add(range.start);
187
+ ends.add(range.end);
188
+ }
189
+ }
190
+
191
+ if (starts.size === 0) {
192
+ return text;
193
+ }
194
+
195
+ let result = "";
196
+ let visiblePos = 0;
197
+ let i = 0;
198
+ let segStart = 0;
199
+
200
+ while (i < text.length) {
201
+ if (text.charCodeAt(i) === 0x1b && text[i + 1] === "[") {
202
+ const escEnd = text.indexOf("m", i + 2);
203
+
204
+ if (escEnd === -1) {
205
+ return result + text.slice(segStart);
206
+ }
207
+
208
+ i = escEnd + 1;
209
+ continue;
210
+ }
211
+
212
+ if (ends.has(visiblePos) || starts.has(visiblePos)) {
213
+ if (i > segStart) {
214
+ result += text.slice(segStart, i);
215
+ }
216
+ if (ends.has(visiblePos)) {
217
+ result += lineBg;
218
+ }
219
+ if (starts.has(visiblePos)) {
220
+ result += emphBg;
221
+ }
222
+ segStart = i;
223
+ }
224
+
225
+ visiblePos += 1;
226
+ i += 1;
227
+ }
228
+
229
+ if (i > segStart) {
230
+ result += text.slice(segStart, i);
231
+ }
232
+ if (ends.has(visiblePos)) {
233
+ result += lineBg;
234
+ }
235
+ if (starts.has(visiblePos)) {
236
+ result += emphBg;
237
+ }
238
+
239
+ return result;
240
+ }
241
+
242
+ function applyBackground(
243
+ kind: ToolDiffLine["kind"],
244
+ text: string,
245
+ backgrounds: DiffBackgrounds
246
+ ): string {
247
+ const spec = KINDS[kind];
248
+
249
+ return "bg" in spec
250
+ ? `${backgrounds[spec.bg]}${text}${CLEAR_TO_EOL}${BG_RESET}`
251
+ : text;
252
+ }
253
+
254
+ function formatPrefix(
255
+ line: ToolDiffLine,
256
+ theme: Theme,
257
+ numberWidth: number
258
+ ): string {
259
+ const numLabel = formatLineNumber(DiffLayout.lineNumber(line), numberWidth);
260
+ const kind = KINDS[line.kind];
261
+
262
+ return theme.fg(kind.role, `${numLabel} ${kind.sign} `);
263
+ }
264
+
265
+ function formatLineNumber(value: number | undefined, width: number): string {
266
+ if (value === undefined) {
267
+ return " ".repeat(width);
268
+ }
269
+
270
+ return String(value).padStart(width, " ");
271
+ }
272
+
273
+ function renderHunkSeparator(theme: Theme, numberWidth: number): string {
274
+ const filler = " ".repeat(numberWidth);
275
+ return theme.fg("toolDiffContext", ` ${filler} ⋯`);
276
+ }
277
+
278
+ export const DiffRenderer = {
279
+ render,
280
+ renderLines,
281
+ highlightHunkLines,
282
+ applyEmphasis,
283
+ };
@@ -0,0 +1,29 @@
1
+ import type { ToolDiff } from "./DiffLines";
2
+
3
+ export type DiffStats = {
4
+ readonly added: number;
5
+ readonly removed: number;
6
+ };
7
+
8
+ function countStats(diff: ToolDiff | undefined): DiffStats {
9
+ if (!diff) {
10
+ return { added: 0, removed: 0 };
11
+ }
12
+
13
+ let added = 0;
14
+ let removed = 0;
15
+
16
+ for (const hunk of diff.hunks) {
17
+ for (const line of hunk.lines) {
18
+ if (line.kind === "added") {
19
+ added += 1;
20
+ } else if (line.kind === "removed") {
21
+ removed += 1;
22
+ }
23
+ }
24
+ }
25
+
26
+ return { added, removed };
27
+ }
28
+
29
+ export const DiffView = { countStats };
@@ -0,0 +1,61 @@
1
+ import { readdir, stat } from "node:fs/promises";
2
+ import { dirname, isAbsolute, join, resolve } from "node:path";
3
+
4
+ import { FsErrors } from "./FsErrors";
5
+ import { Paths } from "./Paths";
6
+
7
+ export type DirectoryEntry = {
8
+ readonly name: string;
9
+ readonly path: string;
10
+ };
11
+
12
+ export type DirectoryListing = {
13
+ /** What was read: absolute, resolved, and with `~` expanded. */
14
+ readonly path: string;
15
+ /** Absent at the root of the filesystem, which is its own parent. */
16
+ readonly parent?: string;
17
+ /** The directories inside it, name-sorted, hidden ones included. */
18
+ readonly entries: readonly DirectoryEntry[];
19
+ };
20
+
21
+ async function check(path: string): Promise<string | undefined> {
22
+ try {
23
+ const found = await stat(path);
24
+ return found.isDirectory() ? undefined : `not a directory: ${path}`;
25
+ } catch (err) {
26
+ return FsErrors.code(err) === "ENOENT"
27
+ ? `path does not exist: ${path}`
28
+ : `stat failed: ${(err as Error).message}`;
29
+ }
30
+ }
31
+
32
+ async function list(path: string): Promise<DirectoryListing> {
33
+ const expanded = Paths.expandHome(path);
34
+ if (!isAbsolute(expanded)) {
35
+ throw new Error(`not an absolute path: ${path}`);
36
+ }
37
+ const resolved = resolve(expanded);
38
+ const reason = await check(resolved);
39
+ if (reason !== undefined) {
40
+ throw new Error(reason);
41
+ }
42
+ const found = await Promise.all(
43
+ (await readdir(resolved, { withFileTypes: true })).map(async (entry) => {
44
+ const child = join(resolved, entry.name);
45
+ const directory = entry.isSymbolicLink()
46
+ ? ((await stat(child).catch(() => undefined))?.isDirectory() ?? false)
47
+ : entry.isDirectory();
48
+ return directory ? { name: entry.name, path: child } : undefined;
49
+ })
50
+ );
51
+ const parent = dirname(resolved);
52
+ return {
53
+ path: resolved,
54
+ ...(parent === resolved ? {} : { parent }),
55
+ entries: found
56
+ .filter((entry): entry is DirectoryEntry => entry !== undefined)
57
+ .sort((a, b) => a.name.localeCompare(b.name)),
58
+ };
59
+ }
60
+
61
+ export const Directories = { check, list };