pim-agent 0.12.1 → 0.14.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 (264) hide show
  1. package/package.json +4 -3
  2. package/packages/core/src/attachments/AttachmentStore.ts +8 -13
  3. package/packages/core/src/attachments/Attachments.ts +2 -4
  4. package/packages/core/src/extensions/CoreExtensions.ts +1 -1
  5. package/packages/core/src/extensions/apply-patch/coordinator.ts +1 -1
  6. package/packages/core/src/extensions/apply-patch/executor.ts +21 -25
  7. package/packages/core/src/extensions/apply-patch/matcher.ts +1 -1
  8. package/packages/core/src/extensions/apply-patch/model.ts +2 -6
  9. package/packages/core/src/extensions/apply-patch/parser.ts +60 -162
  10. package/packages/core/src/extensions/apply-patch/schema.ts +0 -1
  11. package/packages/core/src/extensions/bash/MemoryCap.ts +5 -4
  12. package/packages/core/src/extensions/bash/capture.ts +16 -23
  13. package/packages/core/src/extensions/bash/format.ts +2 -2
  14. package/packages/core/src/extensions/bash/image.ts +4 -8
  15. package/packages/core/src/extensions/bash/index.ts +1 -1
  16. package/packages/core/src/extensions/bash/render.ts +1 -1
  17. package/packages/core/src/extensions/bash/run.ts +9 -15
  18. package/packages/core/src/extensions/bash/schema.ts +4 -4
  19. package/packages/core/src/extensions/edit/edit.ts +14 -33
  20. package/packages/core/src/extensions/glob/glob.ts +1 -3
  21. package/packages/core/src/extensions/glob/index.ts +7 -9
  22. package/packages/core/src/extensions/glob/render.ts +2 -4
  23. package/packages/core/src/extensions/glob/schema.ts +1 -1
  24. package/packages/core/src/extensions/grep/grep.ts +7 -7
  25. package/packages/core/src/extensions/grep/render.ts +5 -6
  26. package/packages/core/src/extensions/grep/schema.ts +3 -7
  27. package/packages/core/src/extensions/read/ImageMemory.ts +2 -8
  28. package/packages/core/src/extensions/read/image.ts +3 -3
  29. package/packages/core/src/extensions/read/index.ts +2 -14
  30. package/packages/core/src/extensions/read/read.ts +5 -6
  31. package/packages/core/src/extensions/read/render.ts +1 -1
  32. package/packages/core/src/extensions/read/schema.ts +2 -2
  33. package/packages/core/src/extensions/subagent/render.ts +2 -2
  34. package/packages/core/src/extensions/subagent/subagent.ts +10 -21
  35. package/packages/core/src/extensions/system-prompt/index.ts +3 -2
  36. package/packages/core/src/extensions/system-prompt/prompt.ts +13 -3
  37. package/packages/core/src/extensions/todo/render.ts +1 -4
  38. package/packages/core/src/extensions/todo/todo.ts +16 -23
  39. package/packages/core/src/extensions/web-fetch/JinaReaderClient.ts +3 -11
  40. package/packages/core/src/extensions/web-fetch/WebViewFetchClient.ts +1 -3
  41. package/packages/core/src/extensions/web-fetch/fetch.ts +4 -12
  42. package/packages/core/src/extensions/web-fetch/image.ts +3 -4
  43. package/packages/core/src/extensions/web-fetch/index.ts +2 -13
  44. package/packages/core/src/extensions/web-fetch/render.ts +3 -4
  45. package/packages/core/src/extensions/web-fetch/schema.ts +3 -4
  46. package/packages/core/src/extensions/web-search/ExaMcpClient.ts +23 -31
  47. package/packages/core/src/extensions/web-search/SearchBreaker.ts +1 -1
  48. package/packages/core/src/extensions/web-search/SearchChain.ts +6 -5
  49. package/packages/core/src/extensions/web-search/providers/DuckDuckGoProvider.ts +2 -2
  50. package/packages/core/src/extensions/web-search/providers/FirecrawlProvider.ts +1 -2
  51. package/packages/core/src/extensions/web-search/providers/SearchProvider.ts +3 -3
  52. package/packages/core/src/extensions/write/write.ts +1 -1
  53. package/packages/core/src/picker/InProcessFilePickerSuggestionEngine.ts +1 -3
  54. package/packages/core/src/picker/PickerItem.ts +1 -1
  55. package/packages/core/src/picker/PickerService.ts +3 -7
  56. package/packages/core/src/picker/RemoteFilePickerSuggestionEngine.ts +1 -2
  57. package/packages/core/src/picker/catalog.ts +7 -15
  58. package/packages/core/src/picker/filePickerWorker.ts +1 -3
  59. package/packages/core/src/picker/ranker.ts +1 -1
  60. package/packages/core/src/picker/token.ts +3 -4
  61. package/packages/core/src/session/AgentRuntime.ts +5 -8
  62. package/packages/core/src/session/EventLog.ts +6 -12
  63. package/packages/core/src/session/ReadCursors.ts +3 -4
  64. package/packages/core/src/session/SearchIndex.ts +49 -86
  65. package/packages/core/src/session/SearchTokens.ts +4 -7
  66. package/packages/core/src/session/SessionCache.ts +8 -6
  67. package/packages/core/src/session/SessionDigest.ts +10 -19
  68. package/packages/core/src/session/SessionHost.ts +53 -83
  69. package/packages/core/src/session/SessionLease.ts +12 -15
  70. package/packages/core/src/session/SessionMeta.ts +16 -32
  71. package/packages/core/src/session/SessionName.ts +3 -7
  72. package/packages/core/src/session/SessionRegistry.ts +13 -29
  73. package/packages/core/src/session/SessionUi.ts +12 -16
  74. package/packages/core/src/session/WriteMark.ts +7 -16
  75. package/packages/core/src/shared/Cli.ts +1 -1
  76. package/packages/core/src/shared/DaemonUnit.ts +1 -2
  77. package/packages/core/src/shared/DiffLines.ts +16 -34
  78. package/packages/core/src/shared/DiffRenderer.ts +8 -27
  79. package/packages/core/src/shared/Directories.ts +4 -4
  80. package/packages/core/src/shared/EditMatcher.ts +18 -50
  81. package/packages/core/src/shared/ExtensionToggles.ts +3 -6
  82. package/packages/core/src/shared/FileEnumerator.ts +73 -57
  83. package/packages/core/src/shared/FileScanner.ts +5 -13
  84. package/packages/core/src/shared/FileWatch.ts +13 -17
  85. package/packages/core/src/shared/Format.ts +2 -3
  86. package/packages/core/src/shared/FuzzyMatcher.ts +18 -19
  87. package/packages/core/src/shared/Git.ts +22 -47
  88. package/packages/core/src/shared/GitMonitor.ts +12 -23
  89. package/packages/core/src/shared/GlobExclusions.ts +0 -4
  90. package/packages/core/src/shared/Http.ts +1 -1
  91. package/packages/core/src/shared/ImageMime.ts +3 -5
  92. package/packages/core/src/shared/Images.ts +17 -27
  93. package/packages/core/src/shared/Languages.ts +2 -2
  94. package/packages/core/src/shared/Levenshtein.ts +1 -6
  95. package/packages/core/src/shared/Lines.ts +1 -1
  96. package/packages/core/src/shared/McpClient.ts +2 -2
  97. package/packages/core/src/shared/PatchSummary.ts +1 -1
  98. package/packages/core/src/shared/Paths.ts +4 -1
  99. package/packages/core/src/shared/PiExtensions.ts +5 -12
  100. package/packages/core/src/shared/PimSettings.ts +14 -15
  101. package/packages/core/src/shared/Proc.ts +2 -3
  102. package/packages/core/src/shared/RateLimiter.ts +1 -1
  103. package/packages/core/src/shared/Renderer.ts +1 -3
  104. package/packages/core/src/shared/RepoDiff.ts +24 -46
  105. package/packages/core/src/shared/SearchRender.ts +3 -4
  106. package/packages/core/src/shared/SpillCache.ts +2 -3
  107. package/packages/core/src/shared/SubagentLogs.ts +4 -6
  108. package/packages/core/src/shared/Supervisor.ts +19 -32
  109. package/packages/core/src/shared/Surface.ts +1 -1
  110. package/packages/core/src/shared/Sweeper.ts +1 -1
  111. package/packages/core/src/shared/Tools.ts +17 -30
  112. package/packages/core/src/shared/Updater.ts +15 -15
  113. package/packages/core/src/view/BodyRenderer.ts +1 -1
  114. package/packages/core/src/view/DiffBlocks.ts +0 -1
  115. package/packages/core/src/view/DiffExpand.ts +10 -25
  116. package/packages/core/src/view/DiffLayout.ts +2 -2
  117. package/packages/core/src/view/DiffPairs.ts +2 -2
  118. package/packages/core/src/view/MarkdownPainter.ts +2 -19
  119. package/packages/core/src/view/MovePath.ts +4 -17
  120. package/packages/core/src/view/Painting.ts +4 -4
  121. package/packages/core/src/view/ViewBlock.ts +11 -12
  122. package/packages/daemon/src/Daemon.ts +2 -7
  123. package/packages/daemon/src/DaemonInstall.ts +5 -12
  124. package/packages/daemon/src/Surfaces.ts +3 -12
  125. package/packages/daemon/src/TelegramSurface.ts +0 -1
  126. package/packages/daemon/src/index.ts +0 -1
  127. package/packages/protocol/src/Command.ts +33 -41
  128. package/packages/protocol/src/Diff.ts +0 -5
  129. package/packages/protocol/src/ImageRoute.ts +2 -5
  130. package/packages/protocol/src/ServerEvent.ts +68 -105
  131. package/packages/server/src/AttachmentEndpoint.ts +45 -19
  132. package/packages/server/src/ClientConnection.ts +11 -12
  133. package/packages/server/src/ImageEndpoint.ts +3 -4
  134. package/packages/server/src/ProbeClient.ts +26 -59
  135. package/packages/server/src/Reloader.ts +4 -7
  136. package/packages/server/src/SendFileTool.ts +1 -1
  137. package/packages/server/src/SessionCatalogue.ts +35 -47
  138. package/packages/server/src/SessionProjection.ts +7 -12
  139. package/packages/server/src/SessionStream.ts +33 -80
  140. package/packages/server/src/StaticClient.ts +7 -8
  141. package/packages/server/src/WebOptions.ts +2 -6
  142. package/packages/server/src/WsGateway.ts +35 -96
  143. package/packages/server/src/probe.ts +6 -18
  144. package/packages/telegram/src/Bot.ts +2 -2
  145. package/packages/telegram/src/Commands.ts +108 -65
  146. package/packages/telegram/src/Config.ts +1 -6
  147. package/packages/telegram/src/Markdown.ts +4 -13
  148. package/packages/telegram/src/Renderer.ts +1 -1
  149. package/packages/telegram/src/Session.ts +3 -10
  150. package/packages/telegram/src/TaskScheduler.ts +31 -10
  151. package/packages/telegram/src/TaskStore.ts +11 -1
  152. package/packages/telegram/src/TaskTool.ts +1 -1
  153. package/packages/telegram/src/UpdateConfirm.ts +2 -2
  154. package/packages/tui/src/extensions/_init/index.ts +2 -5
  155. package/packages/tui/src/extensions/file-picker/index.ts +2 -6
  156. package/packages/tui/src/extensions/footer/index.ts +0 -1
  157. package/packages/tui/src/extensions/footer/powerline.ts +16 -28
  158. package/packages/tui/src/extensions/pim/index.ts +1 -1
  159. package/packages/tui/src/extensions/session-lease/index.ts +5 -6
  160. package/packages/tui/src/extensions/working-indicator/index.ts +1 -2
  161. package/packages/tui/src/themes/themeCliArgs.ts +1 -1
  162. package/packages/web/dist/client/assets/abnfDiagram-O67JEVCF-WweOU5DK.js +1 -0
  163. package/packages/web/dist/client/assets/arc-O2PmZXWt.js +1 -0
  164. package/packages/web/dist/client/assets/architecture-7GRP2DOG-COBxVilU.js +1 -0
  165. package/packages/web/dist/client/assets/architectureDiagram-NJMV4G6O-DwYq1YaM.js +36 -0
  166. package/packages/web/dist/client/assets/array-BifhSqXX.js +1 -0
  167. package/packages/web/dist/client/assets/blockDiagram-BEXU5L5S-CVab041n.js +140 -0
  168. package/packages/web/dist/client/assets/c4Diagram-YGBWAQC7-BmotHez3.js +38 -0
  169. package/packages/web/dist/client/assets/channel-BKV-IcTA.js +1 -0
  170. package/packages/web/dist/client/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  171. package/packages/web/dist/client/assets/chunk-3FUC2YCW-CWNS7-n4.js +2 -0
  172. package/packages/web/dist/client/assets/chunk-5DYCD2WN-DflvXohu.js +1 -0
  173. package/packages/web/dist/client/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  174. package/packages/web/dist/client/assets/chunk-742MDFTN-B_0t6_RE.js +1 -0
  175. package/packages/web/dist/client/assets/chunk-7INBJB4K-wKj0Q3Kv.js +62 -0
  176. package/packages/web/dist/client/assets/chunk-7M6MHVWA-BMvUghc8.js +213 -0
  177. package/packages/web/dist/client/assets/chunk-7PRAP22T-BY41wkcT.js +1 -0
  178. package/packages/web/dist/client/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
  179. package/packages/web/dist/client/assets/chunk-FOHPRMQF-CoA4358r.js +161 -0
  180. package/packages/web/dist/client/assets/chunk-GTNCS2PH-PqY_YoS0.js +88 -0
  181. package/packages/web/dist/client/assets/chunk-GWA4HPMP-fozSU5ty.js +1 -0
  182. package/packages/web/dist/client/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
  183. package/packages/web/dist/client/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  184. package/packages/web/dist/client/assets/chunk-MBY4JIJT-CrHrC3jw.js +72 -0
  185. package/packages/web/dist/client/assets/chunk-NETBCI7D-DWdAxieG.js +1 -0
  186. package/packages/web/dist/client/assets/chunk-O7XYJQB3-B6uOn9iS.js +128 -0
  187. package/packages/web/dist/client/assets/chunk-UA2S7LBM-BxYSHqyy.js +1 -0
  188. package/packages/web/dist/client/assets/chunk-WEXAMYUT-CtwIaQDa.js +1 -0
  189. package/packages/web/dist/client/assets/chunk-XXDRQBXY-fgBFe-PX.js +1 -0
  190. package/packages/web/dist/client/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  191. package/packages/web/dist/client/assets/chunk-Z7XXMR3K-CPonoV3p.js +10 -0
  192. package/packages/web/dist/client/assets/chunk-ZIGJFQKS-Cdu87cTj.js +2 -0
  193. package/packages/web/dist/client/assets/classDiagram-v2-NBCMYWYE-DSWsPaa7.js +217 -0
  194. package/packages/web/dist/client/assets/core-CBoS7AST.js +3 -0
  195. package/packages/web/dist/client/assets/cose-bilkent-JH36ORCC-CTwlldtJ.js +1 -0
  196. package/packages/web/dist/client/assets/cynefin-OW5HDTMX-CZQ4EPNJ.js +1 -0
  197. package/packages/web/dist/client/assets/cynefinDiagram-VND7K2PF-C2gp6H_Q.js +62 -0
  198. package/packages/web/dist/client/assets/cytoscape.esm-B-1iW2ft.js +321 -0
  199. package/packages/web/dist/client/assets/dagre-6A5THRUB-CDxTP47t.js +4 -0
  200. package/packages/web/dist/client/assets/defaultLocale-BFoDCU3G.js +1 -0
  201. package/packages/web/dist/client/assets/diagram-22UHCM2B-BIohE1lI.js +200 -0
  202. package/packages/web/dist/client/assets/diagram-3UASUU5V-TwAUir-w.js +24 -0
  203. package/packages/web/dist/client/assets/diagram-ATOU4E4O-BM1JQrIu.js +3 -0
  204. package/packages/web/dist/client/assets/diagram-CDSNMT55-BKIqhxSI.js +30 -0
  205. package/packages/web/dist/client/assets/diagram-MLGK6HIB-CVe3QDHr.js +24 -0
  206. package/packages/web/dist/client/assets/diagram-MPIPVDR6-Bj-MQIFI.js +41 -0
  207. package/packages/web/dist/client/assets/dist-DlZpcyc2.js +1 -0
  208. package/packages/web/dist/client/assets/ebnfDiagram-ZINNZB2B-C06mmvmz.js +1 -0
  209. package/packages/web/dist/client/assets/elk-276RUBZZ-CizJSNEj.js +27 -0
  210. package/packages/web/dist/client/assets/erDiagram-OPXOYQCR-M3FQkKI1.js +99 -0
  211. package/packages/web/dist/client/assets/eventmodeling-NTZA5JFV-D_UXLpIu.js +1 -0
  212. package/packages/web/dist/client/assets/flowDiagram-KWPJA3E3-BT4eEEVm.js +1 -0
  213. package/packages/web/dist/client/assets/ganttDiagram-FUAMR5RP-CojF9Q16.js +292 -0
  214. package/packages/web/dist/client/assets/gitGraph-4MIJSDKK-YvKqGEPk.js +1 -0
  215. package/packages/web/dist/client/assets/gitGraphDiagram-X574FWY7-XqNqIFo9.js +106 -0
  216. package/packages/web/dist/client/assets/graphlib-DS17s2tU.js +1 -0
  217. package/packages/web/dist/client/assets/index--2CDtzvY.css +1 -0
  218. package/packages/web/dist/client/assets/index-bT6QPJZs.js +47 -0
  219. package/packages/web/dist/client/assets/info-A6RAGUB7-B_1BCzM2.js +1 -0
  220. package/packages/web/dist/client/assets/infoDiagram-VRGFBTTK-zFX8B-dP.js +2 -0
  221. package/packages/web/dist/client/assets/init-C-OQMol4.js +1 -0
  222. package/packages/web/dist/client/assets/ishikawaDiagram-OU5B5YK6-DLzQ_Uyf.js +70 -0
  223. package/packages/web/dist/client/assets/journeyDiagram-ZHPQQLJL-S3WRMmoa.js +139 -0
  224. package/packages/web/dist/client/assets/kanban-definition-PNTS6WVX-B5Z8I-kM.js +89 -0
  225. package/packages/web/dist/client/assets/katex-ZlcWpGUi.js +257 -0
  226. package/packages/web/dist/client/assets/line-BGMH-i2X.js +1 -0
  227. package/packages/web/dist/client/assets/linear-KsrestXG.js +1 -0
  228. package/packages/web/dist/client/assets/mermaid-parser.core-DNffyJJA.js +7 -0
  229. package/packages/web/dist/client/assets/mermaid.core-Cz3vJeQG.js +44 -0
  230. package/packages/web/dist/client/assets/mindmap-definition-NLK3R4M7-CzA61XUf.js +96 -0
  231. package/packages/web/dist/client/assets/ordinal-BDEzSJ7C.js +1 -0
  232. package/packages/web/dist/client/assets/packet-AYTQ26CC-BO55e7cR.js +1 -0
  233. package/packages/web/dist/client/assets/path-fybaL0A-.js +1 -0
  234. package/packages/web/dist/client/assets/pegDiagram-GJSIUBJH-DeRiXYNd.js +1 -0
  235. package/packages/web/dist/client/assets/pie-WAS4IAKB-D9ibrQRe.js +1 -0
  236. package/packages/web/dist/client/assets/pieDiagram-5QR66LMP-BaBpPP33.js +39 -0
  237. package/packages/web/dist/client/assets/quadrantDiagram-O4NWA36T-DIfn4GjM.js +7 -0
  238. package/packages/web/dist/client/assets/radar-RG4KPBEZ-Dmn5-nJq.js +1 -0
  239. package/packages/web/dist/client/assets/railroad-74A4TZTK-ZCWNEd2Z.js +1 -0
  240. package/packages/web/dist/client/assets/railroad-abnf-HS5TGJTU-_swqhy0y.js +1 -0
  241. package/packages/web/dist/client/assets/railroad-ebnf-LZEXJU2U-CytbazPx.js +1 -0
  242. package/packages/web/dist/client/assets/railroad-peg-WCYAUIDC-Bg8QW17T.js +1 -0
  243. package/packages/web/dist/client/assets/railroadDiagram-XR7U4H2S-CDPZ39sy.js +1 -0
  244. package/packages/web/dist/client/assets/requirementDiagram-PLB6GJNP-CyCI8Yul.js +84 -0
  245. package/packages/web/dist/client/assets/rough.esm-Dy-Kn_BL.js +1 -0
  246. package/packages/web/dist/client/assets/sankeyDiagram-IPEJSGJF-p_XAPv72.js +40 -0
  247. package/packages/web/dist/client/assets/sequenceDiagram-PO4LG4MO-DmR6IP_o.js +169 -0
  248. package/packages/web/dist/client/assets/sizeCapture-INFHLROL-B0uUizjq.js +1 -0
  249. package/packages/web/dist/client/assets/src-gnv7pJ1z.js +1 -0
  250. package/packages/web/dist/client/assets/stateDiagram-v2-GCMORJYK-7ZTHl6ih.js +291 -0
  251. package/packages/web/dist/client/assets/swimlanes-2SLR337P-N6bj71pw.js +1 -0
  252. package/packages/web/dist/client/assets/swimlanesDiagram-TC7HE7FX-BJQ2CBsp.js +8 -0
  253. package/packages/web/dist/client/assets/timeline-definition-EJHVYXUP-GNez0kW8.js +120 -0
  254. package/packages/web/dist/client/assets/treeView-Q6P3EWNA-DyQZkboR.js +1 -0
  255. package/packages/web/dist/client/assets/treemap-WGGIJYW6-_J8Pw2TM.js +1 -0
  256. package/packages/web/dist/client/assets/usecaseDiagram-POWQR4AR-Bg-CzKjl.js +328 -0
  257. package/packages/web/dist/client/assets/vennDiagram-UO4OBE2U-DBQKHXhK.js +34 -0
  258. package/packages/web/dist/client/assets/wardley-WFR3VGLG-B5aAsQ3F.js +1 -0
  259. package/packages/web/dist/client/assets/wardleyDiagram-VNRHLVJA-BKUQ1LK6.js +78 -0
  260. package/packages/web/dist/client/assets/xychartDiagram-PMCCYNJV-BI2sVbks.js +7 -0
  261. package/packages/web/dist/client/index.html +2 -2
  262. package/packages/web/dist/client/assets/core-C5fJJN75.js +0 -3
  263. package/packages/web/dist/client/assets/index-C5D7B_8d.js +0 -45
  264. package/packages/web/dist/client/assets/index-OVLzpES-.css +0 -1
@@ -22,7 +22,7 @@ export class StreamCapture {
22
22
  return this.totalBytesAccum;
23
23
  }
24
24
 
25
- /** The first `n` bytes, read across however many chunks the kernel split them into. */
25
+ /** The first `n` bytes, across chunk boundaries. */
26
26
  lead(n: number): Uint8Array {
27
27
  if (this.fullBytes) {
28
28
  return this.fullBytes.subarray(0, n);
@@ -48,36 +48,29 @@ export class StreamCapture {
48
48
  return this.fullBytes;
49
49
  }
50
50
 
51
- /** Opaque bytes are not text: a half-image is worthless, so they are neither cut nor decoded. */
51
+ /** Opaque streams (images) are neither cut nor decoded. */
52
52
  snapshot(opaque = false): CapturedStream {
53
- if (this.totalBytesAccum === 0 || opaque) {
54
- return {
55
- text: "",
56
- totalBytes: this.totalBytesAccum,
57
- truncated: false,
58
- path: null,
59
- nextStart: null,
60
- };
53
+ const totalBytes = this.totalBytesAccum;
54
+ const untruncated = {
55
+ totalBytes,
56
+ truncated: false,
57
+ path: null,
58
+ nextStart: null,
59
+ };
60
+ if (totalBytes === 0 || opaque) {
61
+ return { text: "", ...untruncated };
61
62
  }
62
63
  const dec = new TextDecoder();
63
64
  const all = this.full();
64
- if (this.totalBytesAccum <= STREAM_HEAD_BYTES + STREAM_TAIL_BYTES) {
65
- return {
66
- text: dec.decode(all),
67
- totalBytes: this.totalBytesAccum,
68
- truncated: false,
69
- path: null,
70
- nextStart: null,
71
- };
65
+ if (totalBytes <= STREAM_HEAD_BYTES + STREAM_TAIL_BYTES) {
66
+ return { text: dec.decode(all), ...untruncated };
72
67
  }
73
68
  const headText = dec.decode(all.subarray(0, STREAM_HEAD_BYTES));
74
- const tailText = dec.decode(
75
- all.subarray(all.byteLength - STREAM_TAIL_BYTES)
76
- );
77
- const middle = this.totalBytesAccum - STREAM_HEAD_BYTES - STREAM_TAIL_BYTES;
69
+ const tailText = dec.decode(all.subarray(totalBytes - STREAM_TAIL_BYTES));
70
+ const middle = totalBytes - STREAM_HEAD_BYTES - STREAM_TAIL_BYTES;
78
71
  return {
79
72
  text: `${headText}\n... ${middle} bytes truncated ...\n${tailText}`,
80
- totalBytes: this.totalBytesAccum,
73
+ totalBytes,
81
74
  truncated: true,
82
75
  path: null,
83
76
  nextStart: Lines.continuationLine(headText),
@@ -10,7 +10,7 @@ import {
10
10
 
11
11
  const STREAMS = ["stdout", "stderr"] as const;
12
12
 
13
- export function stripTrailingNewline(s: string): string {
13
+ function stripTrailingNewline(s: string): string {
14
14
  return s.endsWith("\n") ? s.slice(0, -1) : s;
15
15
  }
16
16
 
@@ -68,7 +68,7 @@ export function formatResult(
68
68
  return lines.join("\n");
69
69
  }
70
70
 
71
- /** The words for a picture the content array will not carry; null when it will. */
71
+ /** Null when the image is shown in the content array. */
72
72
  function unshownImageLine(
73
73
  result: BashCommandResult,
74
74
  sniffed: ImageMimeType,
@@ -1,11 +1,7 @@
1
1
  import { Format } from "../../shared/Format";
2
2
  import { Images, type NormalisedImage } from "../../shared/Images";
3
3
 
4
- /**
5
- * The picture stdout printed, once a provider will take it. Bytes that sniffed
6
- * as an image but cannot be decoded are not an error here: the command ran, and
7
- * bash reports what it printed.
8
- */
4
+ /** Null when the bytes cannot be decoded; that is not a tool error. */
9
5
  export async function normaliseStdoutImage(
10
6
  bytes: Uint8Array
11
7
  ): Promise<NormalisedImage | null> {
@@ -16,12 +12,12 @@ export async function normaliseStdoutImage(
16
12
  }
17
13
  }
18
14
 
19
- /** `stdout is a 1200x800 png (240 KB)`, the subject every note about the picture shares. */
20
- export function imageSubject(image: NormalisedImage): string {
15
+ /** e.g. `stdout is a 1200x800 png (240 KB)`. */
16
+ function imageSubject(image: NormalisedImage): string {
21
17
  return `stdout is a ${image.width}x${image.height} ${Images.extensionOf(image.mimeType)} (${Format.bytes(image.bytes)})`;
22
18
  }
23
19
 
24
- /** What the model is told next to the picture, or instead of it when it has no eyes. */
20
+ /** Text sent with the image, or instead of it for a model without vision. */
25
21
  export function imageNote(image: NormalisedImage, vision: boolean): string {
26
22
  if (!vision) {
27
23
  return Images.noVisionNote("bash", imageSubject(image));
@@ -24,7 +24,7 @@ function installLifecycleHandlers(): void {
24
24
  }
25
25
  lifecycleHandlersInstalled = true;
26
26
 
27
- // Sweep escaped bash subtrees, then re-raise so the default handler still runs.
27
+ // Kill running commands, then re-raise so the default handler still runs.
28
28
  for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"] as const) {
29
29
  process.once(sig, () => {
30
30
  try {
@@ -12,7 +12,7 @@ export function bashView({ args, result }: BashViewInput): ToolView {
12
12
  return {
13
13
  label: "Bash",
14
14
  icon: "terminal",
15
- // `spans`, not `text`: a `text` title is split on newlines and rejoined with spaces.
15
+ // Not `text`: that would join a multi-line command onto one line.
16
16
  title: [
17
17
  {
18
18
  kind: "spans",
@@ -16,10 +16,8 @@ type Reader = ReadableStreamDefaultReader<Uint8Array>;
16
16
  const activePids = new Set<number>();
17
17
 
18
18
  const COMMAND_VAR = "PIM_BASH_COMMAND";
19
- // argv is what `pkill -f`/`pgrep -f` match, so a command naming its own target would
20
- // signal this shell before its target. The env hands it over out of argv's reach, and
21
- // the unset keeps it out of every child's environment too.
22
- // The raised oom_score_adj makes the command, not the agent or the desktop, the kernel's first victim.
19
+ // The command goes via env, not argv, so `pkill -f <target>` inside it can't match this shell.
20
+ // oom_score_adj makes the command the OOM killer's first victim, not pim.
23
21
  const RUNNER = `{ echo 500 > /proc/self/oom_score_adj; } 2>/dev/null; __pim_command=$${COMMAND_VAR}; unset ${COMMAND_VAR}; eval "$__pim_command"`;
24
22
 
25
23
  export function killAllActiveBashGroups(sig: NodeJS.Signals = "SIGTERM"): void {
@@ -59,11 +57,7 @@ async function spillIfTruncated(
59
57
  return stream.truncated ? SpillCache.write("bash", ext, cap.full()) : null;
60
58
  }
61
59
 
62
- /**
63
- * The picture stdout printed, or null. The verdict is the leading bytes',
64
- * before anything is decoded; the cap is the whole stream's, so an image too
65
- * big to send is text again and truncates like any other output.
66
- */
60
+ /** Sniffs the leading bytes only. Over the size cap, stdout is treated as text. */
67
61
  export function sniffStdoutImage(cap: StreamCapture): ImageMimeType | null {
68
62
  if (
69
63
  cap.totalBytes < Images.SNIFF_BYTES ||
@@ -104,7 +98,7 @@ export async function runBashCommand(
104
98
  const stderrCap = new StreamCapture();
105
99
  const scope = await MemoryCap.scope();
106
100
 
107
- // setsid gives the tree its own process group (pgid == proc.pid) so the whole tree can be signalled.
101
+ // setsid: own process group (pgid == pid), so the whole tree can be signalled.
108
102
  const proc = Bun.spawn({
109
103
  cmd: [...(scope?.argv ?? []), "setsid", "bash", "-lc", RUNNER],
110
104
  cwd,
@@ -119,7 +113,7 @@ export async function runBashCommand(
119
113
  let timedOut = false;
120
114
  let aborted = false;
121
115
 
122
- // Hold the readers: cancelling a locked stream through the stream itself throws.
116
+ // Keep the readers: cancelling a locked stream directly throws.
123
117
  const stdoutReader = getReader(
124
118
  proc.stdout as unknown as ReadableStream<Uint8Array>
125
119
  );
@@ -127,7 +121,7 @@ export async function runBashCommand(
127
121
  proc.stderr as unknown as ReadableStream<Uint8Array>
128
122
  );
129
123
 
130
- // Never block on EOF: a backgrounded child can hold the pipes open after bash exits.
124
+ // Not awaited: a backgrounded child can hold the pipes open after bash exits.
131
125
  const stdoutDrain = drain(stdoutReader, stdoutCap);
132
126
  const stderrDrain = drain(stderrReader, stderrCap);
133
127
 
@@ -181,7 +175,7 @@ export async function runBashCommand(
181
175
  exitCode = proc.exitCode ?? null;
182
176
  signalCode = (proc.signalCode as NodeJS.Signals | null | undefined) ?? null;
183
177
 
184
- // Bound the drain: a detached grandchild holding the pipe would outlive this call.
178
+ // Bounded: a detached grandchild may hold the pipe open forever.
185
179
  await Promise.race([
186
180
  Promise.all([stdoutDrain, stderrDrain]),
187
181
  Bun.sleep(DRAIN_GRACE_MS),
@@ -206,14 +200,14 @@ export async function runBashCommand(
206
200
  }
207
201
  }
208
202
 
209
- // A picture nothing will show is never decoded: a failed command reports its bytes instead.
203
+ // A failed command's image is never decoded.
210
204
  const sniffed = sniffStdoutImage(stdoutCap);
211
205
  const shows =
212
206
  sniffed !== null && !isErrorResult({ exitCode, timedOut, aborted });
213
207
  const stdout = stdoutCap.snapshot(sniffed !== null);
214
208
  const stderr = stderrCap.snapshot();
215
209
 
216
- // Under OOMPolicy=kill a limit hit is a SIGKILL of the whole scope; systemd's own Result for it is racy.
210
+ // OOMPolicy=kill + KillSignal=SIGKILL: an OOM ends in SIGKILL whoever wins; systemd's Result is racy.
217
211
  const memoryLimitHit =
218
212
  scope !== null && signalCode === "SIGKILL" && !timedOut && !aborted
219
213
  ? scope.limitBytes
@@ -37,18 +37,18 @@ export type BashCommandResult = {
37
37
  readonly signal: NodeJS.Signals | null;
38
38
  readonly stdout: CapturedStream;
39
39
  readonly stderr: CapturedStream;
40
- /** What stdout's leading bytes sniffed as, whether or not the picture could then be decoded. */
40
+ /** Set even when the image could not be decoded. */
41
41
  readonly stdoutSniffed: ImageMimeType | null;
42
- /** Set only when stdout sniffed as a picture and a provider will take it. */
42
+ /** Set only when the image decoded and will be shown. */
43
43
  readonly stdoutImage: NormalisedImage | null;
44
44
  readonly timedOut: boolean;
45
45
  readonly aborted: boolean;
46
- /** The limit in bytes, set when the capped command was SIGKILLed without pim sending it. */
46
+ /** The limit in bytes; set when the command was SIGKILLed by something other than pim. */
47
47
  readonly memoryLimitHit: number | null;
48
48
  readonly durationMs: number;
49
49
  };
50
50
 
51
- export type BashStreamDetails = {
51
+ type BashStreamDetails = {
52
52
  readonly totalBytes: number;
53
53
  readonly truncated: boolean;
54
54
  readonly path: string | null;
@@ -1,4 +1,3 @@
1
- import type { Stats } from "node:fs";
2
1
  import { realpath, stat } from "node:fs/promises";
3
2
  import {
4
3
  EditMatcher,
@@ -11,12 +10,12 @@ import { FsErrors } from "../../shared/FsErrors";
11
10
  import { Lines } from "../../shared/Lines";
12
11
  import type { RawEdit } from "./schema";
13
12
 
14
- export type NoopEdit = {
13
+ type NoopEdit = {
15
14
  readonly index: number;
16
15
  readonly range: string;
17
16
  };
18
17
 
19
- export type ResolvedEditMetadata = {
18
+ type ResolvedEditMetadata = {
20
19
  readonly index: number;
21
20
  readonly ranges: readonly string[];
22
21
  readonly strategy: EditMatchStrategy;
@@ -162,10 +161,10 @@ async function performEdit(
162
161
 
163
162
  const nextContent = EditMatcher.applyAll(originalContent, effectiveMutations);
164
163
 
165
- await writeFileAtomic(
164
+ await Fs.writeKeepingLinks(
166
165
  canonicalPath,
167
166
  hadBom ? `${Lines.utf8Bom}${nextContent}` : nextContent,
168
- metadata
167
+ { mode: Number(metadata.mode), nlink: metadata.nlink }
169
168
  );
170
169
 
171
170
  const original = Lines.splitWithTrailingNewline(originalContent);
@@ -256,7 +255,11 @@ function assertNoDuplicateEdits(edits: readonly ParsedEdit[]): void {
256
255
  const seen = new Map<string, number>();
257
256
 
258
257
  for (const [index, edit] of edits.entries()) {
259
- const key = duplicateKey(edit);
258
+ const key = JSON.stringify([
259
+ edit.oldString,
260
+ edit.newString,
261
+ edit.replaceAll,
262
+ ]);
260
263
  const previous = seen.get(key);
261
264
 
262
265
  if (previous !== undefined) {
@@ -269,22 +272,11 @@ function assertNoDuplicateEdits(edits: readonly ParsedEdit[]): void {
269
272
  }
270
273
  }
271
274
 
272
- function duplicateKey(edit: ParsedEdit): string {
273
- return JSON.stringify({
274
- oldString: edit.oldString,
275
- newString: edit.newString,
276
- replaceAll: edit.replaceAll,
277
- });
278
- }
279
-
280
275
  function sortMutations(mutations: readonly Mutation[]): readonly Mutation[] {
281
- return [...mutations].sort((left, right) => {
282
- if (left.range[0] !== right.range[0]) {
283
- return left.range[0] - right.range[0];
284
- }
285
-
286
- return left.range[1] - right.range[1];
287
- });
276
+ return [...mutations].sort(
277
+ (left, right) =>
278
+ left.range[0] - right.range[0] || left.range[1] - right.range[1]
279
+ );
288
280
  }
289
281
 
290
282
  function assertNoOverlaps(sorted: readonly Mutation[]): void {
@@ -320,17 +312,6 @@ function renderAllNoopError(noops: readonly NoopEdit[]): string {
320
312
  ].join("\n");
321
313
  }
322
314
 
323
- async function writeFileAtomic(
324
- canonicalPath: string,
325
- content: string,
326
- metadata: Stats
327
- ): Promise<void> {
328
- await Fs.writeKeepingLinks(canonicalPath, content, {
329
- mode: Number(metadata.mode),
330
- nlink: metadata.nlink,
331
- });
332
- }
333
-
334
315
  async function enqueue<T>(key: string, task: () => Promise<T>): Promise<T> {
335
316
  const previous = editQueues.get(key) ?? Promise.resolve();
336
317
  const queued = previous.then(task, task);
@@ -359,7 +340,7 @@ function joinHuman(items: readonly string[]): string {
359
340
  }
360
341
 
361
342
  if (items.length === 1) {
362
- return items[0] ?? "unknown";
343
+ return items[0]!;
363
344
  }
364
345
 
365
346
  return `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
@@ -6,12 +6,10 @@ export type GlobMatch = {
6
6
  readonly mtime: number;
7
7
  };
8
8
 
9
- export type GlobScanOptions = FileScanOptions;
10
-
11
9
  export async function findFiles(
12
10
  root: string,
13
11
  pattern: string,
14
- options: GlobScanOptions
12
+ options: FileScanOptions
15
13
  ): Promise<readonly GlobMatch[]> {
16
14
  const metadata = await FsErrors.statOrThrow(root);
17
15
 
@@ -39,15 +39,15 @@ export default function (pi: ExtensionAPI): void {
39
39
  throw new Error("Glob aborted before execution.");
40
40
  }
41
41
 
42
- const limit = headLimit ?? GLOB_HEAD_LIMIT_MAX;
43
- const resolvedPathFormat = pathFormat ?? DEFAULT_PATH_FORMAT;
44
- const absolutePath = Paths.resolve(path ?? ".", ctx.cwd);
45
- const matches = await findFiles(absolutePath, pattern, {
42
+ const scan = {
46
43
  exclude,
47
44
  includeDotfiles: includeDotfiles ?? false,
48
45
  includeIgnored: includeIgnored ?? false,
49
- });
50
- const outcome = renderFiles(matches, limit, {
46
+ };
47
+ const resolvedPathFormat = pathFormat ?? DEFAULT_PATH_FORMAT;
48
+ const absolutePath = Paths.resolve(path ?? ".", ctx.cwd);
49
+ const matches = await findFiles(absolutePath, pattern, scan);
50
+ const outcome = renderFiles(matches, headLimit ?? GLOB_HEAD_LIMIT_MAX, {
51
51
  cwd: ctx.cwd,
52
52
  pathFormat: resolvedPathFormat,
53
53
  });
@@ -67,9 +67,7 @@ export default function (pi: ExtensionAPI): void {
67
67
  details: {
68
68
  absolutePath,
69
69
  pattern,
70
- exclude,
71
- includeDotfiles: includeDotfiles ?? false,
72
- includeIgnored: includeIgnored ?? false,
70
+ ...scan,
73
71
  pathFormat: resolvedPathFormat,
74
72
  fileCount: matches.length,
75
73
  totalItems: outcome.totalItems,
@@ -11,9 +11,7 @@ import type {
11
11
  globSchema,
12
12
  } from "./schema";
13
13
 
14
- export type RenderOutcome = SearchList;
15
-
16
- export type RenderOptions = {
14
+ type RenderOptions = {
17
15
  readonly cwd: string;
18
16
  readonly pathFormat: GlobPathFormat;
19
17
  };
@@ -22,7 +20,7 @@ export function renderFiles(
22
20
  matches: readonly GlobMatch[],
23
21
  headLimit: number,
24
22
  options: RenderOptions
25
- ): RenderOutcome {
23
+ ): SearchList {
26
24
  return SearchRender.capList(
27
25
  matches.map((match) => SearchRender.formatPath(match.path, options)),
28
26
  headLimit
@@ -3,7 +3,7 @@ import { type Static, Type } from "typebox";
3
3
 
4
4
  export const GLOB_HEAD_LIMIT_MAX = 1000;
5
5
 
6
- export const GLOB_PATH_FORMATS = ["relative", "absolute"] as const;
6
+ const GLOB_PATH_FORMATS = ["relative", "absolute"] as const;
7
7
 
8
8
  export const globSchema = Type.Object({
9
9
  pattern: Type.String({
@@ -6,7 +6,7 @@ import { Pool } from "../../shared/Pool";
6
6
 
7
7
  const MATCH_CONCURRENCY = 16;
8
8
 
9
- export type GrepLine = {
9
+ type GrepLine = {
10
10
  readonly lineNumber: number;
11
11
  readonly text: string;
12
12
  };
@@ -24,18 +24,18 @@ export type GrepMatch = {
24
24
  readonly fileLines: readonly string[];
25
25
  };
26
26
 
27
- export type GrepMatcher = {
27
+ type GrepMatcher = {
28
28
  readonly regex: RegExp;
29
29
  readonly matchAcrossLines: boolean;
30
- /** Raw-byte needle for the literal fast path; undefined leaves the regex path unchanged. */
30
+ /** Byte needle for the literal fast path. */
31
31
  readonly literal: Buffer | undefined;
32
32
  };
33
33
 
34
- export type GrepScanOptions = FileScanOptions & {
34
+ type GrepScanOptions = FileScanOptions & {
35
35
  readonly retainFileLines?: boolean;
36
36
  };
37
37
 
38
- // ASCII-only: these stand for themselves in both a default-flag regex and raw UTF-8 bytes.
38
+ // ASCII chars that mean the same as regex source and as raw bytes.
39
39
  const PURE_LITERAL = /^[A-Za-z0-9_ \-/]+$/;
40
40
 
41
41
  function literalNeedle(
@@ -95,7 +95,7 @@ export async function findMatches(
95
95
  return scanned.filter((match) => match !== undefined);
96
96
  }
97
97
 
98
- // An entry may not be a readable regular file: an unreadable one is a non-match, never a failure.
98
+ // Unreadable entries are non-matches, not errors.
99
99
  async function matchFile(
100
100
  filePath: string,
101
101
  matcher: GrepMatcher,
@@ -110,7 +110,7 @@ async function matchFile(
110
110
  }
111
111
 
112
112
  if (matcher.literal !== undefined) {
113
- // Literal fast path: an ASCII needle's raw-byte hit/miss matches the decoded result.
113
+ // Skip decoding files that can't contain the needle.
114
114
  const bytes = Buffer.from(await file.arrayBuffer());
115
115
  if (bytes.indexOf(matcher.literal) < 0) {
116
116
  return undefined;
@@ -10,13 +10,13 @@ import type { ToolView, ViewBlock } from "../../view/ViewBlock";
10
10
  import type { GrepLineRange, GrepMatch } from "./grep";
11
11
  import type { GrepInput, GrepOutputMode, GrepPathFormat } from "./schema";
12
12
 
13
- export type RenderOutcome = SearchList & {
13
+ type RenderOutcome = SearchList & {
14
14
  readonly fileCount: number;
15
15
  readonly totalMatches: number;
16
16
  readonly itemNoun: string;
17
17
  };
18
18
 
19
- export type RenderOptions = {
19
+ type RenderOptions = {
20
20
  readonly cwd: string;
21
21
  readonly pathFormat: GrepPathFormat;
22
22
  readonly context: number;
@@ -60,7 +60,7 @@ export function renderMatches(
60
60
  };
61
61
  }
62
62
 
63
- export type TitleOptions = {
63
+ type TitleOptions = {
64
64
  readonly pattern: string | undefined;
65
65
  readonly path: string | undefined;
66
66
  readonly glob: string | undefined;
@@ -81,10 +81,9 @@ export type GrepViewDetails = {
81
81
  readonly fileCount?: number;
82
82
  };
83
83
 
84
- export type GrepViewInput = {
85
- /** Partially streamed while the call is in flight; every field is optional. */
84
+ type GrepViewInput = {
86
85
  readonly args: Partial<GrepInput>;
87
- /** The result body the tool already rendered for the model; empty in flight. */
86
+ /** Empty while in flight. */
88
87
  readonly body: string;
89
88
  readonly details?: GrepViewDetails;
90
89
  readonly cwd: string;
@@ -2,15 +2,11 @@ import { StringEnum } from "@earendil-works/pi-ai";
2
2
  import { type Static, Type } from "typebox";
3
3
 
4
4
  export const GREP_HEAD_LIMIT_MAX = 1000;
5
- export const GREP_CONTEXT_MAX = 20;
5
+ const GREP_CONTEXT_MAX = 20;
6
6
 
7
- export const GREP_OUTPUT_MODES = [
8
- "files_with_matches",
9
- "content",
10
- "count",
11
- ] as const;
7
+ const GREP_OUTPUT_MODES = ["files_with_matches", "content", "count"] as const;
12
8
 
13
- export const GREP_PATH_FORMATS = ["relative", "absolute"] as const;
9
+ const GREP_PATH_FORMATS = ["relative", "absolute"] as const;
14
10
 
15
11
  export const grepSchema = Type.Object({
16
12
  pattern: Type.String({
@@ -1,23 +1,17 @@
1
1
  import { PimSettings } from "../../shared/PimSettings";
2
2
  import type { ReadImageDetails } from "./schema";
3
3
 
4
- /** What the file looked like when its picture was last sent to the model. */
5
- export type ImageStamp = {
4
+ type ImageStamp = {
6
5
  readonly mtimeMs: number;
7
6
  readonly size: number;
8
7
  };
9
8
 
10
9
  type Sent = ImageStamp & { readonly details: ReadImageDetails };
11
10
 
12
- /**
13
- * The pictures already in this conversation, so a screenshot loop pays the
14
- * resize once. One instance per extension factory, which is one per session;
15
- * a resumed session starts empty and re-sends every image once.
16
- */
11
+ /** Images already sent this session, keyed by path. A resumed session starts empty. */
17
12
  export class ImageMemory {
18
13
  private readonly sent = new Map<string, Sent>();
19
14
 
20
- /** The settings flag is the escape hatch, so it is read only once a picture repeats. */
21
15
  async recall(
22
16
  path: string,
23
17
  stamp: ImageStamp
@@ -11,18 +11,18 @@ export type ImageReadOutcome = {
11
11
  readonly details: ReadImageDetails;
12
12
  };
13
13
 
14
- /** The picture is already in the transcript: the row still draws it, the model is only told so. */
14
+ /** Already sent earlier; the model only gets a note. */
15
15
  export type UnchangedImageReadOutcome = {
16
16
  readonly kind: "image-unchanged";
17
17
  readonly details: ReadImageDetails;
18
18
  };
19
19
 
20
- /** Bytes that are a picture, or a name that claims one — a claim that fails is a diagnostic, not text. */
20
+ /** True for image bytes or an image filename; a bad image then errors rather than reading as text. */
21
21
  export function isImageRead(head: Uint8Array, path: string): boolean {
22
22
  return Images.sniff(head) !== null || Images.looksLikeImageName(path);
23
23
  }
24
24
 
25
- /** Both refusals that must land before the file is pulled into memory. */
25
+ /** Checks to run before loading the file into memory. */
26
26
  export function assertImageReadable(
27
27
  path: string,
28
28
  sizeOnDisk: number,
@@ -68,20 +68,8 @@ export default function (pi: ExtensionAPI): void {
68
68
  });
69
69
  }
70
70
 
71
- return {
72
- content,
73
- details: {
74
- kind: "text",
75
- absolutePath,
76
- totalLines: outcome.totalLines,
77
- visibleStart: outcome.visibleStart,
78
- visibleEnd: outcome.visibleEnd,
79
- truncatedByByteCap: outcome.truncatedByByteCap,
80
- truncatedByEnd: outcome.truncatedByEnd,
81
- hadBom: outcome.hadBom,
82
- nextStart: outcome.nextStart,
83
- },
84
- };
71
+ const { body: _body, ...text } = outcome;
72
+ return { content, details: { ...text, absolutePath } };
85
73
  },
86
74
  toViewModel: readView,
87
75
  });
@@ -30,9 +30,8 @@ export type ReadOutcome =
30
30
  | ImageReadOutcome
31
31
  | UnchangedImageReadOutcome;
32
32
 
33
- export type ReadOptions = {
33
+ type ReadOptions = {
34
34
  readonly model?: VisionModel;
35
- /** The pictures this session has already sent; absent when the caller keeps none. */
36
35
  readonly memory?: ImageMemory;
37
36
  };
38
37
 
@@ -75,7 +74,7 @@ export async function readFile(
75
74
 
76
75
  const file = Bun.file(path);
77
76
 
78
- // One head serves both verdicts: the signature is in its first bytes, a NUL anywhere in it.
77
+ // Sniffs both the image signature and binary-ness.
79
78
  const head = await bytesOf(file.slice(0, Lines.binarySniffBytes), path);
80
79
 
81
80
  if (isImageRead(head, path)) {
@@ -113,7 +112,7 @@ async function bytesOf(file: Bun.BunFile, path: string): Promise<Uint8Array> {
113
112
  try {
114
113
  return await file.bytes();
115
114
  } catch (error) {
116
- rethrowFsError(error, path, "read");
115
+ rethrowFsError(error, path);
117
116
  }
118
117
  }
119
118
 
@@ -178,7 +177,7 @@ function renderText(
178
177
  };
179
178
  }
180
179
 
181
- function rethrowFsError(error: unknown, path: string, action: string): never {
180
+ function rethrowFsError(error: unknown, path: string): never {
182
181
  const code = FsErrors.code(error);
183
182
 
184
183
  if (code === "EACCES" || code === "EPERM") {
@@ -186,6 +185,6 @@ function rethrowFsError(error: unknown, path: string, action: string): never {
186
185
  }
187
186
 
188
187
  throw new Error(
189
- `Cannot ${action} ${path}: ${code ?? (error instanceof Error ? error.message : "unknown error")}.`
188
+ `Cannot read ${path}: ${code ?? (error instanceof Error ? error.message : "unknown error")}.`
190
189
  );
191
190
  }
@@ -44,7 +44,7 @@ function titleBlock(
44
44
  return { kind: "file", path, range: [input.start ?? 1, input.end] };
45
45
  }
46
46
 
47
- /** Legacy sessions predate the tag, so an untagged result is read for the range it may still carry. */
47
+ /** Legacy details have no `kind`, so check the fields. */
48
48
  function visibleRange(
49
49
  details: ReadDetails | undefined
50
50
  ): readonly [number, number] | undefined {