@psnext/slingcli 2.4.20260525-4 → 2.4.20260527-2

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 (243) hide show
  1. package/bin/sling.js +4 -1
  2. package/node_modules/@aws-sdk/core/package.json +2 -2
  3. package/node_modules/@aws-sdk/credential-provider-env/package.json +2 -2
  4. package/node_modules/@aws-sdk/credential-provider-http/dist-cjs/fromHttp/fromHttp.js +12 -6
  5. package/node_modules/@aws-sdk/credential-provider-http/dist-es/fromHttp/fromHttp.js +12 -6
  6. package/node_modules/@aws-sdk/credential-provider-http/package.json +3 -2
  7. package/node_modules/@aws-sdk/credential-provider-ini/package.json +9 -9
  8. package/node_modules/@aws-sdk/credential-provider-login/package.json +3 -3
  9. package/node_modules/@aws-sdk/credential-provider-node/package.json +7 -7
  10. package/node_modules/@aws-sdk/credential-provider-process/package.json +2 -2
  11. package/node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers/package.json +3 -3
  12. package/node_modules/@aws-sdk/credential-provider-sso/package.json +4 -4
  13. package/node_modules/@aws-sdk/credential-provider-web-identity/package.json +3 -3
  14. package/node_modules/@aws-sdk/middleware-websocket/package.json +2 -2
  15. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/cognito-identity/index.js +1 -1
  16. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/signin/index.js +1 -1
  17. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso/index.js +1 -1
  18. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sso-oidc/index.js +1 -1
  19. package/node_modules/@aws-sdk/nested-clients/dist-cjs/submodules/sts/index.js +1 -1
  20. package/node_modules/@aws-sdk/nested-clients/package.json +3 -3
  21. package/node_modules/@aws-sdk/signature-v4-multi-region/package.json +1 -2
  22. package/node_modules/@aws-sdk/xml-builder/package.json +2 -2
  23. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.js +18 -24
  24. package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
  25. package/node_modules/@earendil-works/pi-ai/dist/cli.js +14 -0
  26. package/node_modules/@earendil-works/pi-ai/dist/models.generated.js +78 -63
  27. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.js +2 -1
  28. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.js +8 -22
  29. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.js +1 -1
  30. package/node_modules/@earendil-works/pi-ai/dist/providers/images/openrouter.js +1 -1
  31. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex-responses.js +60 -40
  32. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-completions.js +1 -1
  33. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-responses.js +1 -1
  34. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/device-code.js +55 -0
  35. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/github-copilot.js +45 -69
  36. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/index.js +1 -0
  37. package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js +5 -2
  38. package/node_modules/@earendil-works/pi-ai/package.json +4 -3
  39. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/file-processor.js +2 -3
  40. package/node_modules/@earendil-works/pi-coding-agent/dist/config.js +18 -12
  41. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session-runtime.js +2 -1
  42. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session-services.js +3 -2
  43. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js +8 -2
  44. package/node_modules/@earendil-works/pi-coding-agent/dist/core/auth-storage.js +2 -1
  45. package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/compaction.js +18 -24
  46. package/node_modules/@earendil-works/pi-coding-agent/dist/core/export-html/index.js +8 -6
  47. package/node_modules/@earendil-works/pi-coding-agent/dist/core/export-html/template.js +6 -3
  48. package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js +12 -29
  49. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-registry.js +5 -1
  50. package/node_modules/@earendil-works/pi-coding-agent/dist/core/output-guard.js +52 -22
  51. package/node_modules/@earendil-works/pi-coding-agent/dist/core/package-manager.js +63 -36
  52. package/node_modules/@earendil-works/pi-coding-agent/dist/core/prompt-templates.js +6 -20
  53. package/node_modules/@earendil-works/pi-coding-agent/dist/core/resource-loader.js +38 -31
  54. package/node_modules/@earendil-works/pi-coding-agent/dist/core/sdk.js +9 -4
  55. package/node_modules/@earendil-works/pi-coding-agent/dist/core/session-manager.js +32 -24
  56. package/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js +6 -13
  57. package/node_modules/@earendil-works/pi-coding-agent/dist/core/skills.js +8 -22
  58. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/bash.js +54 -53
  59. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/edit-diff.js +8 -1
  60. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/edit.js +44 -81
  61. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/file-mutation-queue.js +27 -12
  62. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/find.js +2 -3
  63. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/grep.js +3 -3
  64. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/ls.js +5 -5
  65. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/output-accumulator.js +9 -3
  66. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/path-utils.js +39 -21
  67. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/read.js +9 -8
  68. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/truncate.js +12 -2
  69. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/write.js +20 -35
  70. package/node_modules/@earendil-works/pi-coding-agent/dist/main.js +5 -6
  71. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/config-selector.js +1 -1
  72. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/footer.js +14 -5
  73. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/login-dialog.js +29 -4
  74. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/user-message.js +1 -1
  75. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js +4 -4
  76. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/theme/theme.js +37 -28
  77. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-client.js +64 -7
  78. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-mode.js +15 -3
  79. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/clipboard-native.js +14 -8
  80. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/image-resize-core.js +124 -0
  81. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/image-resize-worker.js +31 -0
  82. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/image-resize.js +75 -115
  83. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/paths.js +33 -7
  84. package/node_modules/@earendil-works/pi-coding-agent/npm-shrinkwrap.json +13 -26
  85. package/node_modules/@earendil-works/pi-coding-agent/package.json +6 -6
  86. package/node_modules/@earendil-works/pi-tui/dist/components/editor.js +40 -55
  87. package/node_modules/@earendil-works/pi-tui/dist/components/input.js +2 -2
  88. package/node_modules/@earendil-works/pi-tui/dist/components/markdown.js +12 -2
  89. package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.js +53 -0
  90. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.js +4 -1
  91. package/node_modules/@earendil-works/pi-tui/dist/terminal.js +38 -16
  92. package/node_modules/@earendil-works/pi-tui/dist/utils.js +19 -12
  93. package/node_modules/@earendil-works/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-modifiers.node +0 -0
  94. package/node_modules/@earendil-works/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
  95. package/node_modules/@earendil-works/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
  96. package/node_modules/@earendil-works/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
  97. package/node_modules/@earendil-works/pi-tui/package.json +3 -5
  98. package/node_modules/@smithy/node-http-handler/package.json +2 -2
  99. package/node_modules/undici/lib/api/api-request.js +1 -1
  100. package/node_modules/undici/lib/cache/sqlite-cache-store.js +3 -3
  101. package/node_modules/undici/lib/core/connect.js +16 -0
  102. package/node_modules/undici/lib/core/request.js +17 -2
  103. package/node_modules/undici/lib/core/socks5-client.js +10 -5
  104. package/node_modules/undici/lib/core/socks5-utils.js +17 -22
  105. package/node_modules/undici/lib/dispatcher/client-h1.js +64 -20
  106. package/node_modules/undici/lib/dispatcher/client.js +6 -2
  107. package/node_modules/undici/lib/dispatcher/h2c-client.js +1 -1
  108. package/node_modules/undici/lib/mock/mock-call-history.js +15 -15
  109. package/node_modules/undici/lib/util/cache.js +8 -7
  110. package/node_modules/undici/lib/web/fetch/formdata-parser.js +17 -6
  111. package/node_modules/undici/lib/web/fetch/index.js +5 -2
  112. package/node_modules/undici/lib/web/webidl/index.js +5 -5
  113. package/node_modules/undici/lib/web/websocket/stream/websocketstream.js +1 -7
  114. package/node_modules/undici/package.json +1 -1
  115. package/package.json +6 -6
  116. package/slingshot/index.js +269 -266
  117. package/node_modules/koffi/CHANGELOG.md +0 -1093
  118. package/node_modules/koffi/LICENSE.txt +0 -22
  119. package/node_modules/koffi/README.md +0 -43
  120. package/node_modules/koffi/build/koffi/darwin_arm64/koffi.node +0 -0
  121. package/node_modules/koffi/build/koffi/darwin_x64/koffi.node +0 -0
  122. package/node_modules/koffi/build/koffi/freebsd_arm64/koffi.node +0 -0
  123. package/node_modules/koffi/build/koffi/freebsd_ia32/koffi.node +0 -0
  124. package/node_modules/koffi/build/koffi/freebsd_x64/koffi.node +0 -0
  125. package/node_modules/koffi/build/koffi/linux_arm64/koffi.node +0 -0
  126. package/node_modules/koffi/build/koffi/linux_armhf/koffi.node +0 -0
  127. package/node_modules/koffi/build/koffi/linux_ia32/koffi.node +0 -0
  128. package/node_modules/koffi/build/koffi/linux_loong64/koffi.node +0 -0
  129. package/node_modules/koffi/build/koffi/linux_riscv64d/koffi.node +0 -0
  130. package/node_modules/koffi/build/koffi/linux_x64/koffi.node +0 -0
  131. package/node_modules/koffi/build/koffi/musl_arm64/koffi.node +0 -0
  132. package/node_modules/koffi/build/koffi/musl_x64/koffi.node +0 -0
  133. package/node_modules/koffi/build/koffi/openbsd_ia32/koffi.node +0 -0
  134. package/node_modules/koffi/build/koffi/openbsd_x64/koffi.node +0 -0
  135. package/node_modules/koffi/build/koffi/win32_arm64/koffi.exp +0 -0
  136. package/node_modules/koffi/build/koffi/win32_arm64/koffi.lib +0 -0
  137. package/node_modules/koffi/build/koffi/win32_arm64/koffi.node +0 -0
  138. package/node_modules/koffi/build/koffi/win32_ia32/koffi.exp +0 -0
  139. package/node_modules/koffi/build/koffi/win32_ia32/koffi.lib +0 -0
  140. package/node_modules/koffi/build/koffi/win32_ia32/koffi.node +0 -0
  141. package/node_modules/koffi/build/koffi/win32_x64/koffi.exp +0 -0
  142. package/node_modules/koffi/build/koffi/win32_x64/koffi.lib +0 -0
  143. package/node_modules/koffi/build/koffi/win32_x64/koffi.node +0 -0
  144. package/node_modules/koffi/doc/benchmarks.md +0 -126
  145. package/node_modules/koffi/doc/callbacks.md +0 -210
  146. package/node_modules/koffi/doc/contribute.md +0 -148
  147. package/node_modules/koffi/doc/functions.md +0 -250
  148. package/node_modules/koffi/doc/index.md +0 -61
  149. package/node_modules/koffi/doc/input.md +0 -471
  150. package/node_modules/koffi/doc/migration.md +0 -159
  151. package/node_modules/koffi/doc/misc.md +0 -180
  152. package/node_modules/koffi/doc/output.md +0 -305
  153. package/node_modules/koffi/doc/packaging.md +0 -88
  154. package/node_modules/koffi/doc/platforms.md +0 -36
  155. package/node_modules/koffi/doc/pointers.md +0 -328
  156. package/node_modules/koffi/doc/start.md +0 -118
  157. package/node_modules/koffi/doc/unions.md +0 -186
  158. package/node_modules/koffi/doc/variables.md +0 -102
  159. package/node_modules/koffi/index.d.ts +0 -288
  160. package/node_modules/koffi/index.js +0 -634
  161. package/node_modules/koffi/indirect.js +0 -533
  162. package/node_modules/koffi/lib/native/base/base.cc +0 -11015
  163. package/node_modules/koffi/lib/native/base/base.hh +0 -6003
  164. package/node_modules/koffi/lib/native/base/crc.inc +0 -2214
  165. package/node_modules/koffi/lib/native/base/crc_gen.py +0 -72
  166. package/node_modules/koffi/lib/native/base/mimetypes.inc +0 -1248
  167. package/node_modules/koffi/lib/native/base/mimetypes_gen.py +0 -58
  168. package/node_modules/koffi/lib/native/base/tower.cc +0 -821
  169. package/node_modules/koffi/lib/native/base/tower.hh +0 -81
  170. package/node_modules/koffi/lib/native/base/unicode.inc +0 -408
  171. package/node_modules/koffi/lib/native/base/unicode_gen.py +0 -152
  172. package/node_modules/koffi/package.json +0 -38
  173. package/node_modules/koffi/src/cnoke/LICENSE.txt +0 -22
  174. package/node_modules/koffi/src/cnoke/README.md +0 -99
  175. package/node_modules/koffi/src/cnoke/assets/FindCNoke.cmake +0 -127
  176. package/node_modules/koffi/src/cnoke/assets/toolchains.json +0 -126
  177. package/node_modules/koffi/src/cnoke/assets/win_delay_hook.c +0 -36
  178. package/node_modules/koffi/src/cnoke/cnoke.js +0 -170
  179. package/node_modules/koffi/src/cnoke/package.json +0 -24
  180. package/node_modules/koffi/src/cnoke/src/builder.js +0 -511
  181. package/node_modules/koffi/src/cnoke/src/index.js +0 -10
  182. package/node_modules/koffi/src/cnoke/src/tools.js +0 -407
  183. package/node_modules/koffi/src/koffi/CMakeLists.txt +0 -182
  184. package/node_modules/koffi/src/koffi/src/abi_arm32.cc +0 -1018
  185. package/node_modules/koffi/src/koffi/src/abi_arm32_asm.S +0 -169
  186. package/node_modules/koffi/src/koffi/src/abi_arm64.cc +0 -1295
  187. package/node_modules/koffi/src/koffi/src/abi_arm64_asm.S +0 -195
  188. package/node_modules/koffi/src/koffi/src/abi_arm64_asm.asm +0 -174
  189. package/node_modules/koffi/src/koffi/src/abi_loong64.cc +0 -5
  190. package/node_modules/koffi/src/koffi/src/abi_loong64_asm.S +0 -204
  191. package/node_modules/koffi/src/koffi/src/abi_riscv64.cc +0 -915
  192. package/node_modules/koffi/src/koffi/src/abi_riscv64_asm.S +0 -203
  193. package/node_modules/koffi/src/koffi/src/abi_x64_sysv.cc +0 -939
  194. package/node_modules/koffi/src/koffi/src/abi_x64_sysv_asm.S +0 -231
  195. package/node_modules/koffi/src/koffi/src/abi_x64_win.cc +0 -715
  196. package/node_modules/koffi/src/koffi/src/abi_x64_win_asm.S +0 -166
  197. package/node_modules/koffi/src/koffi/src/abi_x64_win_asm.asm +0 -192
  198. package/node_modules/koffi/src/koffi/src/abi_x86.cc +0 -860
  199. package/node_modules/koffi/src/koffi/src/abi_x86_asm.S +0 -193
  200. package/node_modules/koffi/src/koffi/src/abi_x86_asm.asm +0 -177
  201. package/node_modules/koffi/src/koffi/src/call.cc +0 -1326
  202. package/node_modules/koffi/src/koffi/src/call.hh +0 -179
  203. package/node_modules/koffi/src/koffi/src/errno.inc +0 -462
  204. package/node_modules/koffi/src/koffi/src/ffi.cc +0 -2702
  205. package/node_modules/koffi/src/koffi/src/ffi.hh +0 -354
  206. package/node_modules/koffi/src/koffi/src/init.js +0 -105
  207. package/node_modules/koffi/src/koffi/src/parser.cc +0 -220
  208. package/node_modules/koffi/src/koffi/src/parser.hh +0 -54
  209. package/node_modules/koffi/src/koffi/src/util.cc +0 -1807
  210. package/node_modules/koffi/src/koffi/src/util.hh +0 -221
  211. package/node_modules/koffi/src/koffi/src/uv.cc +0 -193
  212. package/node_modules/koffi/src/koffi/src/uv.def +0 -10
  213. package/node_modules/koffi/src/koffi/src/uv.hh +0 -40
  214. package/node_modules/koffi/src/koffi/src/win32.cc +0 -198
  215. package/node_modules/koffi/src/koffi/src/win32.hh +0 -119
  216. package/node_modules/koffi/src/koffi/tools/write_trampolines.js +0 -77
  217. package/node_modules/koffi/vendor/node-addon-api/LICENSE.md +0 -9
  218. package/node_modules/koffi/vendor/node-addon-api/README.md +0 -95
  219. package/node_modules/koffi/vendor/node-addon-api/napi-inl.deprecated.h +0 -186
  220. package/node_modules/koffi/vendor/node-addon-api/napi-inl.h +0 -7033
  221. package/node_modules/koffi/vendor/node-addon-api/napi.h +0 -3309
  222. package/node_modules/koffi/vendor/node-api-headers/LICENSE +0 -21
  223. package/node_modules/koffi/vendor/node-api-headers/README.md +0 -95
  224. package/node_modules/koffi/vendor/node-api-headers/def/js_native_api.def +0 -125
  225. package/node_modules/koffi/vendor/node-api-headers/def/node_api.def +0 -157
  226. package/node_modules/koffi/vendor/node-api-headers/include/js_native_api.h +0 -591
  227. package/node_modules/koffi/vendor/node-api-headers/include/js_native_api_types.h +0 -210
  228. package/node_modules/koffi/vendor/node-api-headers/include/node_api.h +0 -265
  229. package/node_modules/koffi/vendor/node-api-headers/include/node_api_types.h +0 -58
  230. package/node_modules/koffi/vendor/node-api-headers/include/uv/aix.h +0 -32
  231. package/node_modules/koffi/vendor/node-api-headers/include/uv/bsd.h +0 -34
  232. package/node_modules/koffi/vendor/node-api-headers/include/uv/darwin.h +0 -61
  233. package/node_modules/koffi/vendor/node-api-headers/include/uv/errno.h +0 -483
  234. package/node_modules/koffi/vendor/node-api-headers/include/uv/linux.h +0 -34
  235. package/node_modules/koffi/vendor/node-api-headers/include/uv/os390.h +0 -33
  236. package/node_modules/koffi/vendor/node-api-headers/include/uv/posix.h +0 -31
  237. package/node_modules/koffi/vendor/node-api-headers/include/uv/sunos.h +0 -44
  238. package/node_modules/koffi/vendor/node-api-headers/include/uv/threadpool.h +0 -37
  239. package/node_modules/koffi/vendor/node-api-headers/include/uv/tree.h +0 -521
  240. package/node_modules/koffi/vendor/node-api-headers/include/uv/unix.h +0 -512
  241. package/node_modules/koffi/vendor/node-api-headers/include/uv/version.h +0 -43
  242. package/node_modules/koffi/vendor/node-api-headers/include/uv/win.h +0 -698
  243. package/node_modules/koffi/vendor/node-api-headers/include/uv.h +0 -1990
@@ -3,7 +3,7 @@ import { constants } from "fs";
3
3
  import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises";
4
4
  import { Type } from "typebox";
5
5
  import { renderDiff } from "../../modes/interactive/components/diff.js";
6
- import { applyEditsToNormalizedContent, computeEditsDiff, detectLineEnding, generateDiffString, normalizeToLF, restoreLineEndings, stripBom, } from "./edit-diff.js";
6
+ import { applyEditsToNormalizedContent, computeEditsDiff, detectLineEnding, generateDiffString, generateUnifiedPatch, normalizeToLF, restoreLineEndings, stripBom, } from "./edit-diff.js";
7
7
  import { withFileMutationQueue } from "./file-mutation-queue.js";
8
8
  import { resolveToCwd } from "./path-utils.js";
9
9
  import { invalidArgText, shortenPath, str } from "./render-utils.js";
@@ -177,88 +177,51 @@ export function createEditToolDefinition(cwd, options) {
177
177
  async execute(_toolCallId, input, signal, _onUpdate, _ctx) {
178
178
  const { path, edits } = validateEditInput(input);
179
179
  const absolutePath = resolveToCwd(path, cwd);
180
- return withFileMutationQueue(absolutePath, () => new Promise((resolve, reject) => {
181
- // Check if already aborted.
182
- if (signal?.aborted) {
183
- reject(new Error("Operation aborted"));
184
- return;
185
- }
186
- let aborted = false;
187
- // Set up abort handler.
188
- const onAbort = () => {
189
- aborted = true;
190
- reject(new Error("Operation aborted"));
180
+ return withFileMutationQueue(absolutePath, async () => {
181
+ // Do not reject from an abort event listener here: that would release the
182
+ // mutation queue while an in-flight filesystem operation may still finish.
183
+ // Checking signal.aborted after each await observes the same aborts while
184
+ // keeping the queue locked until the current operation has settled.
185
+ const throwIfAborted = () => {
186
+ if (signal?.aborted)
187
+ throw new Error("Operation aborted");
191
188
  };
192
- if (signal) {
193
- signal.addEventListener("abort", onAbort, { once: true });
189
+ throwIfAborted();
190
+ // Check if file exists.
191
+ try {
192
+ await ops.access(absolutePath);
194
193
  }
195
- // Perform the edit operation.
196
- void (async () => {
197
- try {
198
- // Check if file exists.
199
- try {
200
- await ops.access(absolutePath);
201
- }
202
- catch (error) {
203
- const errorMessage = error instanceof Error && "code" in error ? `Error code: ${error.code}` : String(error);
204
- if (signal) {
205
- signal.removeEventListener("abort", onAbort);
206
- }
207
- reject(new Error(`Could not edit file: ${path}. ${errorMessage}.`));
208
- return;
209
- }
210
- // Check if aborted before reading.
211
- if (aborted) {
212
- return;
213
- }
214
- // Read the file.
215
- const buffer = await ops.readFile(absolutePath);
216
- const rawContent = buffer.toString("utf-8");
217
- // Check if aborted after reading.
218
- if (aborted) {
219
- return;
220
- }
221
- // Strip BOM before matching. The model will not include an invisible BOM in oldText.
222
- const { bom, text: content } = stripBom(rawContent);
223
- const originalEnding = detectLineEnding(content);
224
- const normalizedContent = normalizeToLF(content);
225
- const { baseContent, newContent } = applyEditsToNormalizedContent(normalizedContent, edits, path);
226
- // Check if aborted before writing.
227
- if (aborted) {
228
- return;
229
- }
230
- const finalContent = bom + restoreLineEndings(newContent, originalEnding);
231
- await ops.writeFile(absolutePath, finalContent);
232
- // Check if aborted after writing.
233
- if (aborted) {
234
- return;
235
- }
236
- // Clean up abort handler.
237
- if (signal) {
238
- signal.removeEventListener("abort", onAbort);
239
- }
240
- const diffResult = generateDiffString(baseContent, newContent);
241
- resolve({
242
- content: [
243
- {
244
- type: "text",
245
- text: `Successfully replaced ${edits.length} block(s) in ${path}.`,
246
- },
247
- ],
248
- details: { diff: diffResult.diff, firstChangedLine: diffResult.firstChangedLine },
249
- });
250
- }
251
- catch (error) {
252
- // Clean up abort handler.
253
- if (signal) {
254
- signal.removeEventListener("abort", onAbort);
255
- }
256
- if (!aborted) {
257
- reject(error instanceof Error ? error : new Error(String(error)));
258
- }
259
- }
260
- })();
261
- }));
194
+ catch (error) {
195
+ throwIfAborted();
196
+ const errorMessage = error instanceof Error && "code" in error ? `Error code: ${error.code}` : String(error);
197
+ throw new Error(`Could not edit file: ${path}. ${errorMessage}.`);
198
+ }
199
+ throwIfAborted();
200
+ // Read the file.
201
+ const buffer = await ops.readFile(absolutePath);
202
+ const rawContent = buffer.toString("utf-8");
203
+ throwIfAborted();
204
+ // Strip BOM before matching. The model will not include an invisible BOM in oldText.
205
+ const { bom, text: content } = stripBom(rawContent);
206
+ const originalEnding = detectLineEnding(content);
207
+ const normalizedContent = normalizeToLF(content);
208
+ const { baseContent, newContent } = applyEditsToNormalizedContent(normalizedContent, edits, path);
209
+ throwIfAborted();
210
+ const finalContent = bom + restoreLineEndings(newContent, originalEnding);
211
+ await ops.writeFile(absolutePath, finalContent);
212
+ throwIfAborted();
213
+ const diffResult = generateDiffString(baseContent, newContent);
214
+ const patch = generateUnifiedPatch(path, baseContent, newContent);
215
+ return {
216
+ content: [
217
+ {
218
+ type: "text",
219
+ text: `Successfully replaced ${edits.length} block(s) in ${path}.`,
220
+ },
221
+ ],
222
+ details: { diff: diffResult.diff, patch, firstChangedLine: diffResult.firstChangedLine },
223
+ };
224
+ });
262
225
  },
263
226
  renderCall(args, theme, context) {
264
227
  const component = getEditCallRenderComponent(context.state, context.lastComponent);
@@ -1,13 +1,23 @@
1
- import { realpathSync } from "node:fs";
1
+ import { realpath } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
3
  const fileMutationQueues = new Map();
4
- function getMutationQueueKey(filePath) {
4
+ let registrationQueue = Promise.resolve();
5
+ function isMissingPathError(error) {
6
+ return (typeof error === "object" &&
7
+ error !== null &&
8
+ "code" in error &&
9
+ (error.code === "ENOENT" || error.code === "ENOTDIR"));
10
+ }
11
+ async function getMutationQueueKey(filePath) {
5
12
  const resolvedPath = resolve(filePath);
6
13
  try {
7
- return realpathSync.native(resolvedPath);
14
+ return await realpath(resolvedPath);
8
15
  }
9
- catch {
10
- return resolvedPath;
16
+ catch (error) {
17
+ if (isMissingPathError(error)) {
18
+ return resolvedPath;
19
+ }
20
+ throw error;
11
21
  }
12
22
  }
13
23
  /**
@@ -15,14 +25,19 @@ function getMutationQueueKey(filePath) {
15
25
  * Operations for different files still run in parallel.
16
26
  */
17
27
  export async function withFileMutationQueue(filePath, fn) {
18
- const key = getMutationQueueKey(filePath);
19
- const currentQueue = fileMutationQueues.get(key) ?? Promise.resolve();
20
- let releaseNext;
21
- const nextQueue = new Promise((resolveQueue) => {
22
- releaseNext = resolveQueue;
28
+ const registration = registrationQueue.then(async () => {
29
+ const key = await getMutationQueueKey(filePath);
30
+ const currentQueue = fileMutationQueues.get(key) ?? Promise.resolve();
31
+ let releaseNext;
32
+ const nextQueue = new Promise((resolveQueue) => {
33
+ releaseNext = resolveQueue;
34
+ });
35
+ const chainedQueue = currentQueue.then(() => nextQueue);
36
+ fileMutationQueues.set(key, chainedQueue);
37
+ return { key, currentQueue, chainedQueue, releaseNext };
23
38
  });
24
- const chainedQueue = currentQueue.then(() => nextQueue);
25
- fileMutationQueues.set(key, chainedQueue);
39
+ registrationQueue = registration.then(() => undefined, () => undefined);
40
+ const { key, currentQueue, chainedQueue, releaseNext } = await registration;
26
41
  await currentQueue;
27
42
  try {
28
43
  return await fn();
@@ -1,12 +1,11 @@
1
1
  import { createInterface } from "node:readline";
2
2
  import { Text } from "@earendil-works/pi-tui";
3
3
  import { spawn } from "child_process";
4
- import { existsSync } from "fs";
5
4
  import path from "path";
6
5
  import { Type } from "typebox";
7
6
  import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
8
7
  import { ensureTool } from "../../utils/tools-manager.js";
9
- import { resolveToCwd } from "./path-utils.js";
8
+ import { pathExists, resolveToCwd } from "./path-utils.js";
10
9
  import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js";
11
10
  import { wrapToolDefinition } from "./tool-definition-wrapper.js";
12
11
  import { DEFAULT_MAX_BYTES, formatSize, truncateHead } from "./truncate.js";
@@ -22,7 +21,7 @@ const findSchema = Type.Object({
22
21
  });
23
22
  const DEFAULT_LIMIT = 1000;
24
23
  const defaultFindOperations = {
25
- exists: existsSync,
24
+ exists: pathExists,
26
25
  // This is a placeholder. Actual fd execution happens in execute() when no custom glob is provided.
27
26
  glob: () => [],
28
27
  };
@@ -1,7 +1,7 @@
1
+ import { readFile as fsReadFile, stat as fsStat } from "node:fs/promises";
1
2
  import { createInterface } from "node:readline";
2
3
  import { Text } from "@earendil-works/pi-tui";
3
4
  import { spawn } from "child_process";
4
- import { readFileSync, statSync } from "fs";
5
5
  import path from "path";
6
6
  import { Type } from "typebox";
7
7
  import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
@@ -21,8 +21,8 @@ const grepSchema = Type.Object({
21
21
  });
22
22
  const DEFAULT_LIMIT = 100;
23
23
  const defaultGrepOperations = {
24
- isDirectory: (p) => statSync(p).isDirectory(),
25
- readFile: (p) => readFileSync(p, "utf-8"),
24
+ isDirectory: async (p) => (await fsStat(p)).isDirectory(),
25
+ readFile: (p) => fsReadFile(p, "utf-8"),
26
26
  };
27
27
  function formatGrepCall(args, theme) {
28
28
  const pattern = str(args?.pattern);
@@ -1,9 +1,9 @@
1
+ import { readdir as fsReaddir, stat as fsStat } from "node:fs/promises";
1
2
  import { Text } from "@earendil-works/pi-tui";
2
- import { existsSync, readdirSync, statSync } from "fs";
3
3
  import nodePath from "path";
4
4
  import { Type } from "typebox";
5
5
  import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
6
- import { resolveToCwd } from "./path-utils.js";
6
+ import { pathExists, resolveToCwd } from "./path-utils.js";
7
7
  import { getTextOutput, invalidArgText, shortenPath, str } from "./render-utils.js";
8
8
  import { wrapToolDefinition } from "./tool-definition-wrapper.js";
9
9
  import { DEFAULT_MAX_BYTES, formatSize, truncateHead } from "./truncate.js";
@@ -13,9 +13,9 @@ const lsSchema = Type.Object({
13
13
  });
14
14
  const DEFAULT_LIMIT = 500;
15
15
  const defaultLsOperations = {
16
- exists: existsSync,
17
- stat: statSync,
18
- readdir: readdirSync,
16
+ exists: pathExists,
17
+ stat: fsStat,
18
+ readdir: fsReaddir,
19
19
  };
20
20
  function formatLsCall(args, theme) {
21
21
  const rawPath = str(args?.path);
@@ -29,8 +29,10 @@ export class OutputAccumulator {
29
29
  tailStartsAtLineBoundary = true;
30
30
  totalRawBytes = 0;
31
31
  totalDecodedBytes = 0;
32
- totalLines = 1;
32
+ completedLines = 0;
33
+ totalLines = 0;
33
34
  currentLineBytes = 0;
35
+ hasOpenLine = false;
34
36
  finished = false;
35
37
  tempFilePath;
36
38
  tempFileStream;
@@ -133,11 +135,15 @@ export class OutputAccumulator {
133
135
  }
134
136
  if (newlines === 0) {
135
137
  this.currentLineBytes += bytes;
138
+ this.hasOpenLine = true;
136
139
  }
137
140
  else {
138
- this.totalLines += newlines;
139
- this.currentLineBytes = byteLength(text.slice(lastNewline + 1));
141
+ this.completedLines += newlines;
142
+ const tail = text.slice(lastNewline + 1);
143
+ this.currentLineBytes = byteLength(tail);
144
+ this.hasOpenLine = tail.length > 0;
140
145
  }
146
+ this.totalLines = this.completedLines + (this.hasOpenLine ? 1 : 0);
141
147
  }
142
148
  trimTail() {
143
149
  const buffer = Buffer.from(this.tailText, "utf-8");
@@ -1,11 +1,7 @@
1
1
  import { accessSync, constants } from "node:fs";
2
- import * as os from "node:os";
3
- import { isAbsolute, resolve as resolvePath } from "node:path";
4
- const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
2
+ import { access } from "node:fs/promises";
3
+ import { normalizePath, resolvePath } from "../../utils/paths.js";
5
4
  const NARROW_NO_BREAK_SPACE = "\u202F";
6
- function normalizeUnicodeSpaces(str) {
7
- return str.replace(UNICODE_SPACES, " ");
8
- }
9
5
  function tryMacOSScreenshotPath(filePath) {
10
6
  return filePath.replace(/ (AM|PM)\./gi, `${NARROW_NO_BREAK_SPACE}$1.`);
11
7
  }
@@ -27,29 +23,24 @@ function fileExists(filePath) {
27
23
  return false;
28
24
  }
29
25
  }
30
- function normalizeAtPrefix(filePath) {
31
- return filePath.startsWith("@") ? filePath.slice(1) : filePath;
32
- }
33
- export function expandPath(filePath) {
34
- const normalized = normalizeUnicodeSpaces(normalizeAtPrefix(filePath));
35
- if (normalized === "~") {
36
- return os.homedir();
26
+ export async function pathExists(filePath) {
27
+ try {
28
+ await access(filePath, constants.F_OK);
29
+ return true;
37
30
  }
38
- if (normalized.startsWith("~/")) {
39
- return os.homedir() + normalized.slice(1);
31
+ catch {
32
+ return false;
40
33
  }
41
- return normalized;
34
+ }
35
+ export function expandPath(filePath) {
36
+ return normalizePath(filePath, { normalizeUnicodeSpaces: true, stripAtPrefix: true });
42
37
  }
43
38
  /**
44
39
  * Resolve a path relative to the given cwd.
45
40
  * Handles ~ expansion and absolute paths.
46
41
  */
47
42
  export function resolveToCwd(filePath, cwd) {
48
- const expanded = expandPath(filePath);
49
- if (isAbsolute(expanded)) {
50
- return expanded;
51
- }
52
- return resolvePath(cwd, expanded);
43
+ return resolvePath(filePath, cwd, { normalizeUnicodeSpaces: true, stripAtPrefix: true });
53
44
  }
54
45
  export function resolveReadPath(filePath, cwd) {
55
46
  const resolved = resolveToCwd(filePath, cwd);
@@ -78,4 +69,31 @@ export function resolveReadPath(filePath, cwd) {
78
69
  }
79
70
  return resolved;
80
71
  }
72
+ export async function resolveReadPathAsync(filePath, cwd) {
73
+ const resolved = resolveToCwd(filePath, cwd);
74
+ if (await pathExists(resolved)) {
75
+ return resolved;
76
+ }
77
+ // Try macOS AM/PM variant (narrow no-break space before AM/PM)
78
+ const amPmVariant = tryMacOSScreenshotPath(resolved);
79
+ if (amPmVariant !== resolved && (await pathExists(amPmVariant))) {
80
+ return amPmVariant;
81
+ }
82
+ // Try NFD variant (macOS stores filenames in NFD form)
83
+ const nfdVariant = tryNFDVariant(resolved);
84
+ if (nfdVariant !== resolved && (await pathExists(nfdVariant))) {
85
+ return nfdVariant;
86
+ }
87
+ // Try curly quote variant (macOS uses U+2019 in screenshot names)
88
+ const curlyVariant = tryCurlyQuoteVariant(resolved);
89
+ if (curlyVariant !== resolved && (await pathExists(curlyVariant))) {
90
+ return curlyVariant;
91
+ }
92
+ // Try combined NFD + curly quote (for French macOS screenshots like "Capture d'écran")
93
+ const nfdCurlyVariant = tryCurlyQuoteVariant(nfdVariant);
94
+ if (nfdCurlyVariant !== resolved && (await pathExists(nfdCurlyVariant))) {
95
+ return nfdCurlyVariant;
96
+ }
97
+ return resolved;
98
+ }
81
99
  //# sourceMappingURL=path-utils.js.map
@@ -9,7 +9,7 @@ import { getLanguageFromPath, highlightCode } from "../../modes/interactive/them
9
9
  import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js";
10
10
  import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js";
11
11
  import { formatPathRelativeToCwdOrAbsolute } from "../../utils/paths.js";
12
- import { resolveReadPath } from "./path-utils.js";
12
+ import { resolveReadPathAsync, resolveToCwd } from "./path-utils.js";
13
13
  import { getTextOutput, invalidArgText, replaceTabs, shortenPath, str } from "./render-utils.js";
14
14
  import { wrapToolDefinition } from "./tool-definition-wrapper.js";
15
15
  import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead } from "./truncate.js";
@@ -73,7 +73,7 @@ function getCompactReadClassification(args, cwd) {
73
73
  const rawPath = str(args?.file_path ?? args?.path);
74
74
  if (!rawPath)
75
75
  return undefined;
76
- const absolutePath = resolveReadPath(rawPath, cwd);
76
+ const absolutePath = resolveToCwd(rawPath, cwd);
77
77
  const fileName = basename(absolutePath);
78
78
  if (fileName === "SKILL.md") {
79
79
  return { kind: "skill", label: basename(dirname(absolutePath)) || fileName };
@@ -100,8 +100,8 @@ function formatCompactReadCall(classification, args, theme) {
100
100
  formatReadLineRange(args, theme) +
101
101
  expandHint);
102
102
  }
103
- function formatReadResult(args, result, options, theme, showImages, cwd, isError) {
104
- if (!options.expanded && !isError && getCompactReadClassification(args, cwd)) {
103
+ function formatReadResult(args, result, options, theme, showImages, _cwd, isError) {
104
+ if (!options.expanded && !isError) {
105
105
  return "";
106
106
  }
107
107
  const rawPath = str(args?.file_path ?? args?.path);
@@ -141,7 +141,6 @@ export function createReadToolDefinition(cwd, options) {
141
141
  promptGuidelines: ["Use read to examine files instead of cat or sed."],
142
142
  parameters: readSchema,
143
143
  async execute(_toolCallId, { path, offset, limit }, signal, _onUpdate, ctx) {
144
- const absolutePath = resolveReadPath(path, cwd);
145
144
  return new Promise((resolve, reject) => {
146
145
  if (signal?.aborted) {
147
146
  reject(new Error("Operation aborted"));
@@ -155,6 +154,9 @@ export function createReadToolDefinition(cwd, options) {
155
154
  signal?.addEventListener("abort", onAbort, { once: true });
156
155
  (async () => {
157
156
  try {
157
+ const absolutePath = await resolveReadPathAsync(path, cwd);
158
+ if (aborted)
159
+ return;
158
160
  // Check if file exists and is readable.
159
161
  await ops.access(absolutePath);
160
162
  if (aborted)
@@ -166,10 +168,9 @@ export function createReadToolDefinition(cwd, options) {
166
168
  if (mimeType) {
167
169
  // Read image as binary.
168
170
  const buffer = await ops.readFile(absolutePath);
169
- const base64 = buffer.toString("base64");
170
171
  if (autoResizeImages) {
171
172
  // Resize image if needed before sending it back to the model.
172
- const resized = await resizeImage({ type: "image", data: base64, mimeType });
173
+ const resized = await resizeImage(buffer, mimeType);
173
174
  if (!resized) {
174
175
  let textNote = `Read image file [${mimeType}]\n[Image omitted: could not be resized below the inline image size limit.]`;
175
176
  if (nonVisionImageNote)
@@ -195,7 +196,7 @@ export function createReadToolDefinition(cwd, options) {
195
196
  textNote += `\n${nonVisionImageNote}`;
196
197
  content = [
197
198
  { type: "text", text: textNote },
198
- { type: "image", data: base64, mimeType },
199
+ { type: "image", data: buffer.toString("base64"), mimeType },
199
200
  ];
200
201
  }
201
202
  }
@@ -10,6 +10,16 @@
10
10
  export const DEFAULT_MAX_LINES = 2000;
11
11
  export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB
12
12
  export const GREP_MAX_LINE_LENGTH = 500; // Max chars per grep match line
13
+ function splitLinesForCounting(content) {
14
+ if (content.length === 0) {
15
+ return [];
16
+ }
17
+ const lines = content.split("\n");
18
+ if (content.endsWith("\n")) {
19
+ lines.pop();
20
+ }
21
+ return lines;
22
+ }
13
23
  /**
14
24
  * Format bytes as human-readable size.
15
25
  */
@@ -35,7 +45,7 @@ export function truncateHead(content, options = {}) {
35
45
  const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
36
46
  const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
37
47
  const totalBytes = Buffer.byteLength(content, "utf-8");
38
- const lines = content.split("\n");
48
+ const lines = splitLinesForCounting(content);
39
49
  const totalLines = lines.length;
40
50
  // Check if no truncation needed
41
51
  if (totalLines <= maxLines && totalBytes <= maxBytes) {
@@ -114,7 +124,7 @@ export function truncateTail(content, options = {}) {
114
124
  const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
115
125
  const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
116
126
  const totalBytes = Buffer.byteLength(content, "utf-8");
117
- const lines = content.split("\n");
127
+ const lines = splitLinesForCounting(content);
118
128
  const totalLines = lines.length;
119
129
  // Check if no truncation needed
120
130
  if (totalLines <= maxLines && totalBytes <= maxBytes) {
@@ -141,42 +141,27 @@ export function createWriteToolDefinition(cwd, options) {
141
141
  async execute(_toolCallId, { path, content }, signal, _onUpdate, _ctx) {
142
142
  const absolutePath = resolveToCwd(path, cwd);
143
143
  const dir = dirname(absolutePath);
144
- return withFileMutationQueue(absolutePath, () => new Promise((resolve, reject) => {
145
- if (signal?.aborted) {
146
- reject(new Error("Operation aborted"));
147
- return;
148
- }
149
- let aborted = false;
150
- const onAbort = () => {
151
- aborted = true;
152
- reject(new Error("Operation aborted"));
144
+ return withFileMutationQueue(absolutePath, async () => {
145
+ // Do not reject from an abort event listener here: that would release the
146
+ // mutation queue while an in-flight filesystem operation may still finish.
147
+ // Checking signal.aborted after each await observes the same aborts while
148
+ // keeping the queue locked until the current operation has settled.
149
+ const throwIfAborted = () => {
150
+ if (signal?.aborted)
151
+ throw new Error("Operation aborted");
153
152
  };
154
- signal?.addEventListener("abort", onAbort, { once: true });
155
- (async () => {
156
- try {
157
- // Create parent directories if needed.
158
- await ops.mkdir(dir);
159
- if (aborted)
160
- return;
161
- // Write the file contents.
162
- await ops.writeFile(absolutePath, content);
163
- if (aborted)
164
- return;
165
- signal?.removeEventListener("abort", onAbort);
166
- resolve({
167
- content: [
168
- { type: "text", text: `Successfully wrote ${content.length} bytes to ${path}` },
169
- ],
170
- details: undefined,
171
- });
172
- }
173
- catch (error) {
174
- signal?.removeEventListener("abort", onAbort);
175
- if (!aborted)
176
- reject(error);
177
- }
178
- })();
179
- }));
153
+ throwIfAborted();
154
+ // Create parent directories if needed.
155
+ await ops.mkdir(dir);
156
+ throwIfAborted();
157
+ // Write the file contents.
158
+ await ops.writeFile(absolutePath, content);
159
+ throwIfAborted();
160
+ return {
161
+ content: [{ type: "text", text: `Successfully wrote ${content.length} bytes to ${path}` }],
162
+ details: undefined,
163
+ };
164
+ });
180
165
  },
181
166
  renderCall(args, theme, context) {
182
167
  const renderArgs = args;
@@ -4,7 +4,6 @@
4
4
  * This file handles CLI argument parsing and translates them into
5
5
  * createAgentSession() options. The SDK does the heavy lifting.
6
6
  */
7
- import { resolve } from "node:path";
8
7
  import { createInterface } from "node:readline";
9
8
  import { modelsAreEqual } from "@earendil-works/pi-ai";
10
9
  import { ProcessTerminal, setKeybindings, TUI } from "@earendil-works/pi-tui";
@@ -33,7 +32,7 @@ import { InteractiveMode, runPrintMode, runRpcMode } from "./modes/index.js";
33
32
  import { ExtensionSelectorComponent } from "./modes/interactive/components/extension-selector.js";
34
33
  import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js";
35
34
  import { handleConfigCommand, handlePackageCommand } from "./package-manager-cli.js";
36
- import { isLocalPath } from "./utils/paths.js";
35
+ import { isLocalPath, normalizePath, resolvePath } from "./utils/paths.js";
37
36
  import { cleanupWindowsSelfUpdateQuarantine } from "./utils/windows-self-update.js";
38
37
  /**
39
38
  * Read all content from piped stdin.
@@ -106,9 +105,9 @@ async function prepareInitialMessage(parsed, autoResizeImages, stdinContent) {
106
105
  * If it looks like a path, use as-is. Otherwise try to match as session ID prefix.
107
106
  */
108
107
  async function resolveSessionPath(sessionArg, cwd, sessionDir) {
109
- // If it looks like a file path, use as-is
108
+ // If it looks like a file path, resolve it before handing it to the session manager.
110
109
  if (sessionArg.includes("/") || sessionArg.includes("\\") || sessionArg.endsWith(".jsonl")) {
111
- return { type: "path", path: sessionArg };
110
+ return { type: "path", path: resolvePath(sessionArg, cwd) };
112
111
  }
113
112
  // Try to match as session ID in current project first
114
113
  const localSessions = await SessionManager.list(cwd, sessionDir);
@@ -296,7 +295,7 @@ function buildSessionOptions(parsed, scopedModels, hasExistingSession, modelRegi
296
295
  return { options, cliThinkingFromModel, diagnostics };
297
296
  }
298
297
  function resolveCliPaths(cwd, paths) {
299
- return paths?.map((value) => (isLocalPath(value) ? resolve(cwd, value) : value));
298
+ return paths?.map((value) => (isLocalPath(value) ? resolvePath(value, cwd) : value));
300
299
  }
301
300
  async function promptForMissingSessionCwd(issue, settingsManager) {
302
301
  initTheme(settingsManager.getTheme());
@@ -387,7 +386,7 @@ export async function main(args, options) {
387
386
  // the target session cwd is known. The startup-cwd settings manager is used only for
388
387
  // sessionDir lookup during session selection.
389
388
  const envSessionDir = process.env[ENV_SESSION_DIR];
390
- const sessionDir = parsed.sessionDir ??
389
+ const sessionDir = (parsed.sessionDir ? normalizePath(parsed.sessionDir) : undefined) ??
391
390
  (envSessionDir ? expandTildePath(envSessionDir) : undefined) ??
392
391
  startupSettingsManager.getSessionDir();
393
392
  let sessionManager = await createSessionManager(parsed, cwd, sessionDir, startupSettingsManager);
@@ -462,7 +462,7 @@ class ResourceList {
462
462
  }
463
463
  getResourcePattern(item) {
464
464
  const scope = item.metadata.scope;
465
- const baseDir = this.getTopLevelBaseDir(scope);
465
+ const baseDir = item.metadata.baseDir ?? this.getTopLevelBaseDir(scope);
466
466
  return relative(baseDir, item.path);
467
467
  }
468
468
  getPackageResourcePattern(item) {
@@ -1,3 +1,4 @@
1
+ import { isAbsolute, relative, resolve, sep } from "node:path";
1
2
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
3
  import { theme } from "../theme/theme.js";
3
4
  /**
@@ -25,6 +26,18 @@ function formatTokens(count) {
25
26
  return `${(count / 1000000).toFixed(1)}M`;
26
27
  return `${Math.round(count / 1000000)}M`;
27
28
  }
29
+ export function formatCwdForFooter(cwd, home) {
30
+ if (!home)
31
+ return cwd;
32
+ const resolvedCwd = resolve(cwd);
33
+ const resolvedHome = resolve(home);
34
+ const relativeToHome = relative(resolvedHome, resolvedCwd);
35
+ const isInsideHome = relativeToHome === "" ||
36
+ (relativeToHome !== ".." && !relativeToHome.startsWith(`..${sep}`) && !isAbsolute(relativeToHome));
37
+ if (!isInsideHome)
38
+ return cwd;
39
+ return relativeToHome === "" ? "~" : `~${sep}${relativeToHome}`;
40
+ }
28
41
  /**
29
42
  * Footer component that shows pwd, token stats, and context usage.
30
43
  * Computes token/context stats from session, gets git branch and extension statuses from provider.
@@ -81,11 +94,7 @@ export class FooterComponent {
81
94
  const contextPercentValue = contextUsage?.percent ?? 0;
82
95
  const contextPercent = contextUsage?.percent !== null ? contextPercentValue.toFixed(1) : "?";
83
96
  // Replace home directory with ~
84
- let pwd = this.session.sessionManager.getCwd();
85
- const home = process.env.HOME || process.env.USERPROFILE;
86
- if (home && pwd.startsWith(home)) {
87
- pwd = `~${pwd.slice(home.length)}`;
88
- }
97
+ let pwd = formatCwdForFooter(this.session.sessionManager.getCwd(), process.env.HOME || process.env.USERPROFILE);
89
98
  // Add git branch if available
90
99
  const branch = this.footerData.getGitBranch();
91
100
  if (branch) {