@psnext/slingcli 2.4.20260526-1 → 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 (227) 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 +73 -58
  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/package.json +6 -6
  100. package/slingshot/index.js +246 -244
  101. package/node_modules/koffi/CHANGELOG.md +0 -1093
  102. package/node_modules/koffi/LICENSE.txt +0 -22
  103. package/node_modules/koffi/README.md +0 -43
  104. package/node_modules/koffi/build/koffi/darwin_arm64/koffi.node +0 -0
  105. package/node_modules/koffi/build/koffi/darwin_x64/koffi.node +0 -0
  106. package/node_modules/koffi/build/koffi/freebsd_arm64/koffi.node +0 -0
  107. package/node_modules/koffi/build/koffi/freebsd_ia32/koffi.node +0 -0
  108. package/node_modules/koffi/build/koffi/freebsd_x64/koffi.node +0 -0
  109. package/node_modules/koffi/build/koffi/linux_arm64/koffi.node +0 -0
  110. package/node_modules/koffi/build/koffi/linux_armhf/koffi.node +0 -0
  111. package/node_modules/koffi/build/koffi/linux_ia32/koffi.node +0 -0
  112. package/node_modules/koffi/build/koffi/linux_loong64/koffi.node +0 -0
  113. package/node_modules/koffi/build/koffi/linux_riscv64d/koffi.node +0 -0
  114. package/node_modules/koffi/build/koffi/linux_x64/koffi.node +0 -0
  115. package/node_modules/koffi/build/koffi/musl_arm64/koffi.node +0 -0
  116. package/node_modules/koffi/build/koffi/musl_x64/koffi.node +0 -0
  117. package/node_modules/koffi/build/koffi/openbsd_ia32/koffi.node +0 -0
  118. package/node_modules/koffi/build/koffi/openbsd_x64/koffi.node +0 -0
  119. package/node_modules/koffi/build/koffi/win32_arm64/koffi.exp +0 -0
  120. package/node_modules/koffi/build/koffi/win32_arm64/koffi.lib +0 -0
  121. package/node_modules/koffi/build/koffi/win32_arm64/koffi.node +0 -0
  122. package/node_modules/koffi/build/koffi/win32_ia32/koffi.exp +0 -0
  123. package/node_modules/koffi/build/koffi/win32_ia32/koffi.lib +0 -0
  124. package/node_modules/koffi/build/koffi/win32_ia32/koffi.node +0 -0
  125. package/node_modules/koffi/build/koffi/win32_x64/koffi.exp +0 -0
  126. package/node_modules/koffi/build/koffi/win32_x64/koffi.lib +0 -0
  127. package/node_modules/koffi/build/koffi/win32_x64/koffi.node +0 -0
  128. package/node_modules/koffi/doc/benchmarks.md +0 -126
  129. package/node_modules/koffi/doc/callbacks.md +0 -210
  130. package/node_modules/koffi/doc/contribute.md +0 -148
  131. package/node_modules/koffi/doc/functions.md +0 -250
  132. package/node_modules/koffi/doc/index.md +0 -61
  133. package/node_modules/koffi/doc/input.md +0 -471
  134. package/node_modules/koffi/doc/migration.md +0 -159
  135. package/node_modules/koffi/doc/misc.md +0 -180
  136. package/node_modules/koffi/doc/output.md +0 -305
  137. package/node_modules/koffi/doc/packaging.md +0 -88
  138. package/node_modules/koffi/doc/platforms.md +0 -36
  139. package/node_modules/koffi/doc/pointers.md +0 -328
  140. package/node_modules/koffi/doc/start.md +0 -118
  141. package/node_modules/koffi/doc/unions.md +0 -186
  142. package/node_modules/koffi/doc/variables.md +0 -102
  143. package/node_modules/koffi/index.d.ts +0 -288
  144. package/node_modules/koffi/index.js +0 -634
  145. package/node_modules/koffi/indirect.js +0 -533
  146. package/node_modules/koffi/lib/native/base/base.cc +0 -11015
  147. package/node_modules/koffi/lib/native/base/base.hh +0 -6003
  148. package/node_modules/koffi/lib/native/base/crc.inc +0 -2214
  149. package/node_modules/koffi/lib/native/base/crc_gen.py +0 -72
  150. package/node_modules/koffi/lib/native/base/mimetypes.inc +0 -1248
  151. package/node_modules/koffi/lib/native/base/mimetypes_gen.py +0 -58
  152. package/node_modules/koffi/lib/native/base/tower.cc +0 -821
  153. package/node_modules/koffi/lib/native/base/tower.hh +0 -81
  154. package/node_modules/koffi/lib/native/base/unicode.inc +0 -408
  155. package/node_modules/koffi/lib/native/base/unicode_gen.py +0 -152
  156. package/node_modules/koffi/package.json +0 -38
  157. package/node_modules/koffi/src/cnoke/LICENSE.txt +0 -22
  158. package/node_modules/koffi/src/cnoke/README.md +0 -99
  159. package/node_modules/koffi/src/cnoke/assets/FindCNoke.cmake +0 -127
  160. package/node_modules/koffi/src/cnoke/assets/toolchains.json +0 -126
  161. package/node_modules/koffi/src/cnoke/assets/win_delay_hook.c +0 -36
  162. package/node_modules/koffi/src/cnoke/cnoke.js +0 -170
  163. package/node_modules/koffi/src/cnoke/package.json +0 -24
  164. package/node_modules/koffi/src/cnoke/src/builder.js +0 -511
  165. package/node_modules/koffi/src/cnoke/src/index.js +0 -10
  166. package/node_modules/koffi/src/cnoke/src/tools.js +0 -407
  167. package/node_modules/koffi/src/koffi/CMakeLists.txt +0 -182
  168. package/node_modules/koffi/src/koffi/src/abi_arm32.cc +0 -1018
  169. package/node_modules/koffi/src/koffi/src/abi_arm32_asm.S +0 -169
  170. package/node_modules/koffi/src/koffi/src/abi_arm64.cc +0 -1295
  171. package/node_modules/koffi/src/koffi/src/abi_arm64_asm.S +0 -195
  172. package/node_modules/koffi/src/koffi/src/abi_arm64_asm.asm +0 -174
  173. package/node_modules/koffi/src/koffi/src/abi_loong64.cc +0 -5
  174. package/node_modules/koffi/src/koffi/src/abi_loong64_asm.S +0 -204
  175. package/node_modules/koffi/src/koffi/src/abi_riscv64.cc +0 -915
  176. package/node_modules/koffi/src/koffi/src/abi_riscv64_asm.S +0 -203
  177. package/node_modules/koffi/src/koffi/src/abi_x64_sysv.cc +0 -939
  178. package/node_modules/koffi/src/koffi/src/abi_x64_sysv_asm.S +0 -231
  179. package/node_modules/koffi/src/koffi/src/abi_x64_win.cc +0 -715
  180. package/node_modules/koffi/src/koffi/src/abi_x64_win_asm.S +0 -166
  181. package/node_modules/koffi/src/koffi/src/abi_x64_win_asm.asm +0 -192
  182. package/node_modules/koffi/src/koffi/src/abi_x86.cc +0 -860
  183. package/node_modules/koffi/src/koffi/src/abi_x86_asm.S +0 -193
  184. package/node_modules/koffi/src/koffi/src/abi_x86_asm.asm +0 -177
  185. package/node_modules/koffi/src/koffi/src/call.cc +0 -1326
  186. package/node_modules/koffi/src/koffi/src/call.hh +0 -179
  187. package/node_modules/koffi/src/koffi/src/errno.inc +0 -462
  188. package/node_modules/koffi/src/koffi/src/ffi.cc +0 -2702
  189. package/node_modules/koffi/src/koffi/src/ffi.hh +0 -354
  190. package/node_modules/koffi/src/koffi/src/init.js +0 -105
  191. package/node_modules/koffi/src/koffi/src/parser.cc +0 -220
  192. package/node_modules/koffi/src/koffi/src/parser.hh +0 -54
  193. package/node_modules/koffi/src/koffi/src/util.cc +0 -1807
  194. package/node_modules/koffi/src/koffi/src/util.hh +0 -221
  195. package/node_modules/koffi/src/koffi/src/uv.cc +0 -193
  196. package/node_modules/koffi/src/koffi/src/uv.def +0 -10
  197. package/node_modules/koffi/src/koffi/src/uv.hh +0 -40
  198. package/node_modules/koffi/src/koffi/src/win32.cc +0 -198
  199. package/node_modules/koffi/src/koffi/src/win32.hh +0 -119
  200. package/node_modules/koffi/src/koffi/tools/write_trampolines.js +0 -77
  201. package/node_modules/koffi/vendor/node-addon-api/LICENSE.md +0 -9
  202. package/node_modules/koffi/vendor/node-addon-api/README.md +0 -95
  203. package/node_modules/koffi/vendor/node-addon-api/napi-inl.deprecated.h +0 -186
  204. package/node_modules/koffi/vendor/node-addon-api/napi-inl.h +0 -7033
  205. package/node_modules/koffi/vendor/node-addon-api/napi.h +0 -3309
  206. package/node_modules/koffi/vendor/node-api-headers/LICENSE +0 -21
  207. package/node_modules/koffi/vendor/node-api-headers/README.md +0 -95
  208. package/node_modules/koffi/vendor/node-api-headers/def/js_native_api.def +0 -125
  209. package/node_modules/koffi/vendor/node-api-headers/def/node_api.def +0 -157
  210. package/node_modules/koffi/vendor/node-api-headers/include/js_native_api.h +0 -591
  211. package/node_modules/koffi/vendor/node-api-headers/include/js_native_api_types.h +0 -210
  212. package/node_modules/koffi/vendor/node-api-headers/include/node_api.h +0 -265
  213. package/node_modules/koffi/vendor/node-api-headers/include/node_api_types.h +0 -58
  214. package/node_modules/koffi/vendor/node-api-headers/include/uv/aix.h +0 -32
  215. package/node_modules/koffi/vendor/node-api-headers/include/uv/bsd.h +0 -34
  216. package/node_modules/koffi/vendor/node-api-headers/include/uv/darwin.h +0 -61
  217. package/node_modules/koffi/vendor/node-api-headers/include/uv/errno.h +0 -483
  218. package/node_modules/koffi/vendor/node-api-headers/include/uv/linux.h +0 -34
  219. package/node_modules/koffi/vendor/node-api-headers/include/uv/os390.h +0 -33
  220. package/node_modules/koffi/vendor/node-api-headers/include/uv/posix.h +0 -31
  221. package/node_modules/koffi/vendor/node-api-headers/include/uv/sunos.h +0 -44
  222. package/node_modules/koffi/vendor/node-api-headers/include/uv/threadpool.h +0 -37
  223. package/node_modules/koffi/vendor/node-api-headers/include/uv/tree.h +0 -521
  224. package/node_modules/koffi/vendor/node-api-headers/include/uv/unix.h +0 -512
  225. package/node_modules/koffi/vendor/node-api-headers/include/uv/version.h +0 -43
  226. package/node_modules/koffi/vendor/node-api-headers/include/uv/win.h +0 -698
  227. 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) {