@pi-in-go/pigpen-ahp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/CREDITS.md +27 -0
  2. package/LICENSE +23 -0
  3. package/README.md +148 -0
  4. package/extensions/ahp/access_options_test.go +116 -0
  5. package/extensions/ahp/extension.go +54 -0
  6. package/extensions/ahp/go.mod +12 -0
  7. package/extensions/ahp/go.sum +2 -0
  8. package/extensions/ahp/go.work +8 -0
  9. package/extensions/ahp/internal/channels/chat.go +110 -0
  10. package/extensions/ahp/internal/channels/root.go +32 -0
  11. package/extensions/ahp/internal/channels/session.go +128 -0
  12. package/extensions/ahp/internal/channels/session_test.go +108 -0
  13. package/extensions/ahp/internal/compose/compose.go +153 -0
  14. package/extensions/ahp/internal/compose/compose_test.go +120 -0
  15. package/extensions/ahp/internal/gaps/gaps_test.go +88 -0
  16. package/extensions/ahp/internal/host/handshake_test.go +206 -0
  17. package/extensions/ahp/internal/host/host.go +1305 -0
  18. package/extensions/ahp/internal/host/reconnect_test.go +197 -0
  19. package/extensions/ahp/internal/host/replaywindow_test.go +31 -0
  20. package/extensions/ahp/internal/host/schema_test.go +79 -0
  21. package/extensions/ahp/internal/host/store.go +291 -0
  22. package/extensions/ahp/internal/host/store_update_test.go +85 -0
  23. package/extensions/ahp/internal/host/subscriptions_test.go +160 -0
  24. package/extensions/ahp/internal/host/surface_test.go +95 -0
  25. package/extensions/ahp/internal/host/workarounds.go +458 -0
  26. package/extensions/ahp/internal/host/workarounds_test.go +96 -0
  27. package/extensions/ahp/internal/live/live.go +328 -0
  28. package/extensions/ahp/internal/live/live_test.go +91 -0
  29. package/extensions/ahp/internal/live/store.go +113 -0
  30. package/extensions/ahp/internal/mapper/activity.go +199 -0
  31. package/extensions/ahp/internal/mapper/activity_test.go +188 -0
  32. package/extensions/ahp/internal/mapper/bench_test.go +38 -0
  33. package/extensions/ahp/internal/mapper/event_mapper_test.go +380 -0
  34. package/extensions/ahp/internal/mapper/fixtures_test.go +387 -0
  35. package/extensions/ahp/internal/mapper/helpers_test.go +251 -0
  36. package/extensions/ahp/internal/mapper/js.go +131 -0
  37. package/extensions/ahp/internal/mapper/mapper.go +600 -0
  38. package/extensions/ahp/internal/mapper/message_input_test.go +196 -0
  39. package/extensions/ahp/internal/mapper/messageinput.go +272 -0
  40. package/extensions/ahp/internal/mapper/schema_helpers_test.go +5 -0
  41. package/extensions/ahp/internal/mapper/testdata/LICENSE-pi-ahp +21 -0
  42. package/extensions/ahp/internal/mapper/testdata/fixtures/abort.json +711 -0
  43. package/extensions/ahp/internal/mapper/testdata/fixtures/bash-long-output.json +1312 -0
  44. package/extensions/ahp/internal/mapper/testdata/fixtures/compaction.json +6285 -0
  45. package/extensions/ahp/internal/mapper/testdata/fixtures/parallel-tools.json +2622 -0
  46. package/extensions/ahp/internal/mapper/testdata/fixtures/plain-text.json +1856 -0
  47. package/extensions/ahp/internal/mapper/testdata/fixtures/single-tool.json +1128 -0
  48. package/extensions/ahp/internal/mapper/testdata/fixtures/steering.json +3926 -0
  49. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-bash.json +2372 -0
  50. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-edit.json +3983 -0
  51. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-error.json +2850 -0
  52. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-find.json +1162 -0
  53. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-grep.json +2783 -0
  54. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-loop.json +1693 -0
  55. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-ls.json +2427 -0
  56. package/extensions/ahp/internal/mapper/testdata/fixtures/tool-write.json +1271 -0
  57. package/extensions/ahp/internal/mapper/title.go +34 -0
  58. package/extensions/ahp/internal/mapper/usermsg.go +87 -0
  59. package/extensions/ahp/internal/pi/activeturn_test.go +296 -0
  60. package/extensions/ahp/internal/pi/backend.go +64 -0
  61. package/extensions/ahp/internal/pi/catalogue.go +370 -0
  62. package/extensions/ahp/internal/pi/catalogue_test.go +271 -0
  63. package/extensions/ahp/internal/pi/chatdriver.go +533 -0
  64. package/extensions/ahp/internal/pi/chatdriver_test.go +416 -0
  65. package/extensions/ahp/internal/pi/clientactions.go +212 -0
  66. package/extensions/ahp/internal/pi/clientactions_test.go +273 -0
  67. package/extensions/ahp/internal/pi/completions.go +230 -0
  68. package/extensions/ahp/internal/pi/completions_test.go +272 -0
  69. package/extensions/ahp/internal/pi/config_test.go +269 -0
  70. package/extensions/ahp/internal/pi/deletesession.go +69 -0
  71. package/extensions/ahp/internal/pi/deletesession_test.go +153 -0
  72. package/extensions/ahp/internal/pi/disposal_test.go +362 -0
  73. package/extensions/ahp/internal/pi/fixture_test.go +308 -0
  74. package/extensions/ahp/internal/pi/foreign_test.go +122 -0
  75. package/extensions/ahp/internal/pi/harness_test.go +83 -0
  76. package/extensions/ahp/internal/pi/history.go +269 -0
  77. package/extensions/ahp/internal/pi/hydrated_test.go +171 -0
  78. package/extensions/ahp/internal/pi/hydratedlifecycle_test.go +196 -0
  79. package/extensions/ahp/internal/pi/hydration_test.go +209 -0
  80. package/extensions/ahp/internal/pi/hydrator.go +212 -0
  81. package/extensions/ahp/internal/pi/imageinput.go +224 -0
  82. package/extensions/ahp/internal/pi/imageinput_test.go +57 -0
  83. package/extensions/ahp/internal/pi/lifecycle_test.go +381 -0
  84. package/extensions/ahp/internal/pi/models.go +212 -0
  85. package/extensions/ahp/internal/pi/models_test.go +128 -0
  86. package/extensions/ahp/internal/pi/paging.go +95 -0
  87. package/extensions/ahp/internal/pi/pagingtruncate_test.go +323 -0
  88. package/extensions/ahp/internal/pi/projecttrust.go +222 -0
  89. package/extensions/ahp/internal/pi/registry.go +924 -0
  90. package/extensions/ahp/internal/pi/restart_test.go +185 -0
  91. package/extensions/ahp/internal/pi/services.go +70 -0
  92. package/extensions/ahp/internal/pi/sessionconfig.go +64 -0
  93. package/extensions/ahp/internal/pi/sessionfiles_test.go +74 -0
  94. package/extensions/ahp/internal/pi/sessionstore.go +83 -0
  95. package/extensions/ahp/internal/pi/summary_test.go +426 -0
  96. package/extensions/ahp/internal/pi/workarounds_wire_test.go +208 -0
  97. package/extensions/ahp/internal/pisession/fromentries_test.go +32 -0
  98. package/extensions/ahp/internal/pisession/json.go +31 -0
  99. package/extensions/ahp/internal/pisession/pisession.go +580 -0
  100. package/extensions/ahp/internal/settings/access.go +63 -0
  101. package/extensions/ahp/internal/settings/access_test.go +52 -0
  102. package/extensions/ahp/internal/settings/settings.go +117 -0
  103. package/extensions/ahp/internal/settings/settings_test.go +104 -0
  104. package/extensions/ahp/internal/svc/etag_other.go +15 -0
  105. package/extensions/ahp/internal/svc/etag_unix.go +34 -0
  106. package/extensions/ahp/internal/svc/glob.go +197 -0
  107. package/extensions/ahp/internal/svc/mime.go +37 -0
  108. package/extensions/ahp/internal/svc/paths.go +108 -0
  109. package/extensions/ahp/internal/svc/pty.go +64 -0
  110. package/extensions/ahp/internal/svc/pty_darwin.go +42 -0
  111. package/extensions/ahp/internal/svc/pty_linux.go +36 -0
  112. package/extensions/ahp/internal/svc/pty_other.go +9 -0
  113. package/extensions/ahp/internal/svc/pty_test.go +114 -0
  114. package/extensions/ahp/internal/svc/pty_unix.go +147 -0
  115. package/extensions/ahp/internal/svc/resource.go +495 -0
  116. package/extensions/ahp/internal/svc/resource_test.go +501 -0
  117. package/extensions/ahp/internal/svc/stat_bsd.go +9 -0
  118. package/extensions/ahp/internal/svc/stat_linux.go +9 -0
  119. package/extensions/ahp/internal/svc/terminal.go +454 -0
  120. package/extensions/ahp/internal/svc/terminal_test.go +512 -0
  121. package/extensions/ahp/internal/svc/watch.go +590 -0
  122. package/extensions/ahp/internal/svc/watch_test.go +627 -0
  123. package/extensions/ahp/internal/svc/watchevents_test.go +452 -0
  124. package/extensions/ahp/internal/svc/watchpolicy.go +81 -0
  125. package/extensions/ahp/internal/svc/watchpolicy_test.go +151 -0
  126. package/extensions/ahp/internal/svc/watchracy_test.go +61 -0
  127. package/extensions/ahp/internal/testkit/schema/LICENSE-agent-host-protocol +21 -0
  128. package/extensions/ahp/internal/testkit/schema/actions.schema.json +9208 -0
  129. package/extensions/ahp/internal/testkit/schema/commands.schema.json +10862 -0
  130. package/extensions/ahp/internal/testkit/schema/errors.schema.json +10928 -0
  131. package/extensions/ahp/internal/testkit/schema/notifications.schema.json +6667 -0
  132. package/extensions/ahp/internal/testkit/schema/state.schema.json +6392 -0
  133. package/extensions/ahp/internal/testkit/schema.go +360 -0
  134. package/extensions/ahp/internal/testkit/testkit.go +367 -0
  135. package/extensions/ahp/internal/twin/twin.go +27 -0
  136. package/extensions/ahp/internal/wire/dispatchable.go +106 -0
  137. package/extensions/ahp/internal/wire/dispatchable_list.go +15 -0
  138. package/extensions/ahp/internal/wire/helpers_test.go +5 -0
  139. package/extensions/ahp/internal/wire/wire.go +218 -0
  140. package/extensions/ahp/internal/wire/wire_test.go +123 -0
  141. package/extensions/ahp/internal/ws/helpers_test.go +7 -0
  142. package/extensions/ahp/internal/ws/vscode_test.go +198 -0
  143. package/extensions/ahp/internal/ws/ws.go +674 -0
  144. package/extensions/ahp/internal/ws/ws_test.go +324 -0
  145. package/extensions/ahp/realpig_test.go +589 -0
  146. package/extensions/ahp/runtime.go +311 -0
  147. package/extensions/ahp/runtime_test.go +110 -0
  148. package/extensions/ahp/testdata/upstream-tests.json +3833 -0
  149. package/extensions/ahp/third_party/agent-host-protocol-go/LICENSE +21 -0
  150. package/extensions/ahp/third_party/agent-host-protocol-go/NOTICE-PIGPEN.md +18 -0
  151. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/client.go +1011 -0
  152. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/error.go +111 -0
  153. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/multi_host_state_mirror.go +239 -0
  154. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/reducers.go +1939 -0
  155. package/extensions/ahp/third_party/agent-host-protocol-go/ahp/transport.go +176 -0
  156. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/actions.generated.go +2447 -0
  157. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/commands.generated.go +1546 -0
  158. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/common.go +205 -0
  159. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/errors.generated.go +65 -0
  160. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/messages.generated.go +138 -0
  161. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/notifications.generated.go +264 -0
  162. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/state.generated.go +6171 -0
  163. package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/version.generated.go +31 -0
  164. package/extensions/ahp/third_party/agent-host-protocol-go/go.mod +3 -0
  165. package/extensions/ahp/twins_test.go +91 -0
  166. package/package.json +40 -0
  167. package/proof/PORT.md +121 -0
  168. package/proof/mutations.json +152 -0
  169. package/proof/oracle/LICENSE +21 -0
  170. package/proof/oracle/README.md +120 -0
  171. package/proof/oracle/UPSTREAM.md +7 -0
  172. package/proof/oracle/package.json +68 -0
  173. package/proof/oracle/src/bin/cli.ts +66 -0
  174. package/proof/oracle/src/bin/tunnel.ts +128 -0
  175. package/proof/oracle/src/channels/chat.ts +134 -0
  176. package/proof/oracle/src/channels/root.ts +39 -0
  177. package/proof/oracle/src/channels/session.ts +118 -0
  178. package/proof/oracle/src/channels/terminal.ts +11 -0
  179. package/proof/oracle/src/core/channels.ts +108 -0
  180. package/proof/oracle/src/core/client-workarounds.ts +358 -0
  181. package/proof/oracle/src/core/connection.ts +41 -0
  182. package/proof/oracle/src/core/host.ts +882 -0
  183. package/proof/oracle/src/core/sequencer.ts +75 -0
  184. package/proof/oracle/src/core/state-store.ts +162 -0
  185. package/proof/oracle/src/core/uri.ts +25 -0
  186. package/proof/oracle/src/host/direct-settings.ts +110 -0
  187. package/proof/oracle/src/host/pi-host.ts +220 -0
  188. package/proof/oracle/src/host/serve.ts +71 -0
  189. package/proof/oracle/src/host/terminal-service.ts +346 -0
  190. package/proof/oracle/src/pi/activity.ts +171 -0
  191. package/proof/oracle/src/pi/changeset-service.ts +646 -0
  192. package/proof/oracle/src/pi/changeset-uri.ts +64 -0
  193. package/proof/oracle/src/pi/chat-driver.ts +529 -0
  194. package/proof/oracle/src/pi/completions.ts +160 -0
  195. package/proof/oracle/src/pi/delete-session.ts +53 -0
  196. package/proof/oracle/src/pi/event-mapper.ts +648 -0
  197. package/proof/oracle/src/pi/git-changes.ts +605 -0
  198. package/proof/oracle/src/pi/history.ts +305 -0
  199. package/proof/oracle/src/pi/image-input.ts +56 -0
  200. package/proof/oracle/src/pi/image-mime.ts +8 -0
  201. package/proof/oracle/src/pi/in-process-backend.ts +155 -0
  202. package/proof/oracle/src/pi/message-input.ts +212 -0
  203. package/proof/oracle/src/pi/models.ts +124 -0
  204. package/proof/oracle/src/pi/project-trust.ts +66 -0
  205. package/proof/oracle/src/pi/provider.ts +2 -0
  206. package/proof/oracle/src/pi/resource-paths.ts +80 -0
  207. package/proof/oracle/src/pi/resource-service.ts +356 -0
  208. package/proof/oracle/src/pi/resource-watch-policy.ts +35 -0
  209. package/proof/oracle/src/pi/resource-watch.ts +357 -0
  210. package/proof/oracle/src/pi/session-catalogue.ts +318 -0
  211. package/proof/oracle/src/pi/session-config.ts +84 -0
  212. package/proof/oracle/src/pi/session-history.ts +75 -0
  213. package/proof/oracle/src/pi/session-hydrator.ts +219 -0
  214. package/proof/oracle/src/pi/session-registry.ts +815 -0
  215. package/proof/oracle/src/pi/session-storage.ts +29 -0
  216. package/proof/oracle/src/pi/session-title.ts +18 -0
  217. package/proof/oracle/src/pi/turn-paging.ts +94 -0
  218. package/proof/oracle/src/pi/user-message.ts +66 -0
  219. package/proof/oracle/src/protocol/errors.ts +44 -0
  220. package/proof/oracle/src/protocol/jsonrpc.ts +89 -0
  221. package/proof/oracle/src/protocol/version.ts +39 -0
  222. package/proof/oracle/src/transport/websocket.ts +136 -0
  223. package/proof/oracle/src/tunnel/devtunnel.ts +293 -0
  224. package/proof/oracle/src/tunnel/discovery.ts +37 -0
  225. package/proof/oracle/test/active-turn-reconnect.test.ts +274 -0
  226. package/proof/oracle/test/activity.test.ts +171 -0
  227. package/proof/oracle/test/changeset-lifecycle.test.serial.ts +303 -0
  228. package/proof/oracle/test/changeset-uri.test.ts +37 -0
  229. package/proof/oracle/test/changeset.test.serial.ts +487 -0
  230. package/proof/oracle/test/chat-driver.test.ts +691 -0
  231. package/proof/oracle/test/client-actions.test.ts +409 -0
  232. package/proof/oracle/test/client-workarounds.test.ts +320 -0
  233. package/proof/oracle/test/completions.test.ts +323 -0
  234. package/proof/oracle/test/delete-session.test.ts +142 -0
  235. package/proof/oracle/test/direct-settings.test.ts +81 -0
  236. package/proof/oracle/test/event-mapper.test.ts +619 -0
  237. package/proof/oracle/test/fetch-turns.test.ts +207 -0
  238. package/proof/oracle/test/fixtures/abort.json +711 -0
  239. package/proof/oracle/test/fixtures/bash-long-output.json +1312 -0
  240. package/proof/oracle/test/fixtures/compaction.json +6285 -0
  241. package/proof/oracle/test/fixtures/parallel-tools.json +2622 -0
  242. package/proof/oracle/test/fixtures/plain-text.json +1856 -0
  243. package/proof/oracle/test/fixtures/single-tool.json +1128 -0
  244. package/proof/oracle/test/fixtures/steering.json +3926 -0
  245. package/proof/oracle/test/fixtures/tool-bash.json +2372 -0
  246. package/proof/oracle/test/fixtures/tool-edit.json +3983 -0
  247. package/proof/oracle/test/fixtures/tool-error.json +2850 -0
  248. package/proof/oracle/test/fixtures/tool-find.json +1162 -0
  249. package/proof/oracle/test/fixtures/tool-grep.json +2783 -0
  250. package/proof/oracle/test/fixtures/tool-loop.json +1693 -0
  251. package/proof/oracle/test/fixtures/tool-ls.json +2427 -0
  252. package/proof/oracle/test/fixtures/tool-write.json +1271 -0
  253. package/proof/oracle/test/handshake.test.ts +275 -0
  254. package/proof/oracle/test/harness.ts +153 -0
  255. package/proof/oracle/test/hydrated-session-lifecycle.test.ts +211 -0
  256. package/proof/oracle/test/image-input.test.ts +28 -0
  257. package/proof/oracle/test/image-session.test.ts +69 -0
  258. package/proof/oracle/test/live-turn.test.ts +304 -0
  259. package/proof/oracle/test/mapper-fixtures.test.ts +293 -0
  260. package/proof/oracle/test/message-input.test.ts +181 -0
  261. package/proof/oracle/test/model-discovery.test.ts +170 -0
  262. package/proof/oracle/test/models.test.ts +109 -0
  263. package/proof/oracle/test/pi-host.test.ts +91 -0
  264. package/proof/oracle/test/pi-replay.test.ts +141 -0
  265. package/proof/oracle/test/project-trust.test.ts +125 -0
  266. package/proof/oracle/test/protocol-surface.test.ts +121 -0
  267. package/proof/oracle/test/pty.test.ts +100 -0
  268. package/proof/oracle/test/reconnect.test.ts +389 -0
  269. package/proof/oracle/test/resource-watch-policy.test.ts +78 -0
  270. package/proof/oracle/test/resource-watch.test.serial.ts +536 -0
  271. package/proof/oracle/test/resource.test.ts +499 -0
  272. package/proof/oracle/test/schema.test.ts +88 -0
  273. package/proof/oracle/test/session-catalogue.test.ts +314 -0
  274. package/proof/oracle/test/session-config.test.ts +224 -0
  275. package/proof/oracle/test/session-disposal.test.ts +364 -0
  276. package/proof/oracle/test/session-hydration.test.ts +194 -0
  277. package/proof/oracle/test/session-lifecycle.test.ts +416 -0
  278. package/proof/oracle/test/session-storage.test.serial.ts +32 -0
  279. package/proof/oracle/test/session-summary.test.ts +418 -0
  280. package/proof/oracle/test/subscriptions.test.ts +198 -0
  281. package/proof/oracle/test/support/assertions.ts +32 -0
  282. package/proof/oracle/test/support/async.ts +26 -0
  283. package/proof/oracle/test/support/hydrated-session.ts +205 -0
  284. package/proof/oracle/test/support/images.ts +8 -0
  285. package/proof/oracle/test/support/recorded-fixtures.ts +60 -0
  286. package/proof/oracle/test/support/recorded-scenarios.ts +145 -0
  287. package/proof/oracle/test/support/replay.ts +194 -0
  288. package/proof/oracle/test/support/schema.ts +221 -0
  289. package/proof/oracle/test/support/session-files.ts +12 -0
  290. package/proof/oracle/test/support/session-storage.ts +22 -0
  291. package/proof/oracle/test/support/upstream.ts +25 -0
  292. package/proof/oracle/test/support/watch-events.ts +108 -0
  293. package/proof/oracle/test/terminal-service.test.ts +495 -0
  294. package/proof/oracle/test/truncate.test.ts +230 -0
  295. package/proof/oracle/test/tunnel.test.ts +258 -0
  296. package/proof/oracle/test/upstream-workarounds.test.ts +37 -0
  297. package/proof/oracle/test/uri.test.ts +24 -0
  298. package/proof/oracle/test/watch-events.test.ts +118 -0
  299. package/proof/oracle/tsconfig.json +32 -0
  300. package/proof/proof-piglet/piglet.yaml +18 -0
  301. package/proof/tools/fakellm/go.mod +3 -0
  302. package/proof/tools/fakellm/main.go +109 -0
  303. package/proof/tools/gen-dispatchable.py +30 -0
  304. package/provenance.json +28 -0
@@ -0,0 +1,605 @@
1
+ /** Computes Git-backed FileEdit state without modifying the repository or index. */
2
+
3
+ import { isUtf8 } from "node:buffer";
4
+ import { execFile } from "node:child_process";
5
+ import { constants, type Stats } from "node:fs";
6
+ import { type FileHandle, lstat, open, realpath } from "node:fs/promises";
7
+ import { isAbsolute, relative, resolve, sep } from "node:path";
8
+ import { pathToFileURL } from "node:url";
9
+ import type { ChangesetFile, FileEdit } from "@microsoft/agent-host-protocol";
10
+ import type { PiChangesetKind } from "./changeset-uri.ts";
11
+
12
+ const GIT_BLOB_SCHEME = "git-blob:";
13
+ const MAX_GIT_OUTPUT = 32 * 1024 * 1024;
14
+ const MAX_UNTRACKED_LINE_COUNT_BYTES = 8 * 1024 * 1024;
15
+ const MAX_TOTAL_UNTRACKED_LINE_COUNT_BYTES = 32 * 1024 * 1024;
16
+ const GIT_TIMEOUT_MS = 30_000;
17
+
18
+ const GIT_CONTEXT_OVERRIDES = new Set([
19
+ "GIT_ALTERNATE_OBJECT_DIRECTORIES",
20
+ "GIT_COMMON_DIR",
21
+ "GIT_CONFIG_COUNT",
22
+ "GIT_CONFIG_PARAMETERS",
23
+ "GIT_DIR",
24
+ "GIT_INDEX_FILE",
25
+ "GIT_NAMESPACE",
26
+ "GIT_OBJECT_DIRECTORY",
27
+ "GIT_PREFIX",
28
+ "GIT_QUARANTINE_PATH",
29
+ "GIT_SHALLOW_FILE",
30
+ "GIT_WORK_TREE",
31
+ ]);
32
+
33
+ export interface GitWorkspace {
34
+ readonly cwd: string;
35
+ readonly repositoryRoot: string;
36
+ readonly gitDirectory: string;
37
+ readonly commonGitDirectory: string;
38
+ readonly pathspec: string;
39
+ readonly head?: string;
40
+ readonly parent?: string;
41
+ }
42
+
43
+ export interface GitBlobRef {
44
+ readonly sessionId: string;
45
+ readonly oid: string;
46
+ readonly path: string;
47
+ }
48
+
49
+ interface ParsedGitChange {
50
+ readonly oldPath?: string;
51
+ readonly newPath?: string;
52
+ readonly oldOid?: string;
53
+ readonly newOid?: string;
54
+ readonly added?: number;
55
+ readonly removed?: number;
56
+ }
57
+
58
+ export interface GitChangesBackend {
59
+ inspect(cwd: string, signal?: AbortSignal): Promise<GitWorkspace | undefined>;
60
+ compute(
61
+ workspace: GitWorkspace,
62
+ sessionId: string,
63
+ kind: PiChangesetKind,
64
+ signal?: AbortSignal,
65
+ ): Promise<ChangesetFile[]>;
66
+ ignoredDirectories(workspace: GitWorkspace, signal?: AbortSignal): Promise<string[]>;
67
+ pathForBlob(workspace: GitWorkspace, blob: GitBlobRef): string | undefined;
68
+ readBlob(workspace: GitWorkspace, blob: GitBlobRef, signal?: AbortSignal): Promise<Buffer>;
69
+ }
70
+
71
+ class GitCommandError extends Error {}
72
+
73
+ function gitEnvironment(): NodeJS.ProcessEnv {
74
+ const environment = { ...process.env };
75
+ for (const key of Object.keys(environment)) {
76
+ if (GIT_CONTEXT_OVERRIDES.has(key) || key.startsWith("GIT_CONFIG_KEY_") || key.startsWith("GIT_CONFIG_VALUE_")) {
77
+ delete environment[key];
78
+ }
79
+ }
80
+ return {
81
+ ...environment,
82
+ GIT_NO_LAZY_FETCH: "1",
83
+ GIT_OPTIONAL_LOCKS: "0",
84
+ GIT_PAGER: "cat",
85
+ GIT_TERMINAL_PROMPT: "0",
86
+ LC_ALL: "C",
87
+ };
88
+ }
89
+
90
+ function bufferOf(value: string | Buffer): Buffer {
91
+ return Buffer.isBuffer(value) ? value : Buffer.from(value);
92
+ }
93
+
94
+ function runGit(
95
+ cwd: string,
96
+ args: readonly string[],
97
+ options: { readonly allowFailure?: boolean; readonly signal?: AbortSignal } = {},
98
+ ): Promise<Buffer | undefined> {
99
+ return new Promise((resolveCommand, rejectCommand) => {
100
+ execFile(
101
+ "git",
102
+ ["--literal-pathspecs", "-c", "core.filemode=false", "-c", "core.fsmonitor=false", ...args],
103
+ {
104
+ cwd,
105
+ encoding: "buffer",
106
+ env: gitEnvironment(),
107
+ maxBuffer: MAX_GIT_OUTPUT,
108
+ timeout: GIT_TIMEOUT_MS,
109
+ ...(options.signal ? { signal: options.signal } : {}),
110
+ },
111
+ (error, stdout, stderr) => {
112
+ if (!error) {
113
+ resolveCommand(bufferOf(stdout));
114
+ return;
115
+ }
116
+ if (options.signal?.aborted) {
117
+ rejectCommand(options.signal.reason ?? error);
118
+ return;
119
+ }
120
+ const code = (error as NodeJS.ErrnoException).code;
121
+ if (options.allowFailure && (code === "ENOENT" || typeof code === "number")) {
122
+ resolveCommand(undefined);
123
+ return;
124
+ }
125
+ if (code === "ENOENT") {
126
+ rejectCommand(new GitCommandError("git executable was not found"));
127
+ return;
128
+ }
129
+ const detail = bufferOf(stderr).toString("utf8").trim();
130
+ rejectCommand(new GitCommandError(detail || error.message));
131
+ },
132
+ );
133
+ });
134
+ }
135
+
136
+ function utf8(output: Buffer, source: string): string {
137
+ if (!isUtf8(output)) throw new GitCommandError(`${source} returned a non-UTF-8 path`);
138
+ return output.toString("utf8");
139
+ }
140
+
141
+ function outputLine(output: Buffer | undefined): string | undefined {
142
+ if (!output) return undefined;
143
+ const value = utf8(output, "git").replace(/\r?\n$/u, "");
144
+ return value ? value : undefined;
145
+ }
146
+
147
+ function nulPaths(output: Buffer | undefined, source: string): string[] {
148
+ if (!output || output.length === 0) return [];
149
+ const value = utf8(output, source);
150
+ if (!value.endsWith("\0")) throw new GitCommandError(`${source} returned an unterminated path list`);
151
+ return value.slice(0, -1).split("\0");
152
+ }
153
+
154
+ function validOid(value: string | undefined): value is string {
155
+ return value !== undefined && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u.test(value);
156
+ }
157
+
158
+ function blobOid(mode: string, oid: string | undefined): string | undefined {
159
+ return /^100[0-7]{3}$/u.test(mode) && validOid(oid) && !/^0+$/u.test(oid) ? oid : undefined;
160
+ }
161
+
162
+ function gitPathToAbsolute(repositoryRoot: string, path: string): string | undefined {
163
+ if (
164
+ !path ||
165
+ path.startsWith("/") ||
166
+ path.split("/").some((segment) => segment === "" || segment === "." || segment === "..")
167
+ ) {
168
+ return undefined;
169
+ }
170
+ const absolute = resolve(repositoryRoot, ...path.split("/"));
171
+ const rel = relative(repositoryRoot, absolute);
172
+ return rel !== ".." && !rel.startsWith(`..${sep}`) ? absolute : undefined;
173
+ }
174
+
175
+ function isInside(root: string, candidate: string): boolean {
176
+ const rel = relative(root, candidate);
177
+ return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
178
+ }
179
+
180
+ function numberField(value: string | undefined): number | undefined {
181
+ if (value === "-") return undefined;
182
+ if (!value || !/^\d+$/u.test(value)) throw new GitCommandError("unexpected git numstat output");
183
+ const parsed = Number(value);
184
+ if (!Number.isSafeInteger(parsed)) throw new GitCommandError("git numstat exceeds the supported range");
185
+ return parsed;
186
+ }
187
+
188
+ /** Parses the combined NUL-delimited `git diff --raw --numstat` stream. */
189
+ function parseGitChanges(output: Buffer): ParsedGitChange[] {
190
+ if (output.length === 0) return [];
191
+ const value = utf8(output, "git diff");
192
+ if (!value.endsWith("\0")) throw new GitCommandError("git diff returned an unterminated record stream");
193
+ const segments = value.split("\0");
194
+ const changes: Omit<ParsedGitChange, "added" | "removed">[] = [];
195
+ const stats = new Map<string, { readonly added?: number; readonly removed?: number }>();
196
+
197
+ for (let index = 0; index < segments.length; ) {
198
+ const segment = segments[index++];
199
+ if (!segment) continue;
200
+ if (segment.startsWith(":")) {
201
+ const fields = segment.slice(1).split(" ");
202
+ const oldMode = fields[0];
203
+ const newMode = fields[1];
204
+ const oldOid = fields[2];
205
+ const newOid = fields[3];
206
+ const status = fields[4]?.[0];
207
+ const firstPath = segments[index++];
208
+ if (!firstPath || !oldMode || !newMode || !status || !"ADMR".includes(status)) {
209
+ throw new GitCommandError("unexpected git raw diff output");
210
+ }
211
+ const secondPath = status === "R" ? segments[index++] : undefined;
212
+ if (status === "R" && !secondPath) throw new GitCommandError("git rename is missing its destination");
213
+ const oldBlob = blobOid(oldMode, oldOid);
214
+ const newBlob = blobOid(newMode, newOid);
215
+ if (
216
+ (oldMode !== "000000" && !/^100[0-7]{3}$/u.test(oldMode)) ||
217
+ (newMode !== "000000" && !/^100[0-7]{3}$/u.test(newMode))
218
+ ) {
219
+ continue;
220
+ }
221
+ switch (status) {
222
+ case "A":
223
+ changes.push({ newPath: firstPath, ...(newBlob ? { newOid: newBlob } : {}) });
224
+ break;
225
+ case "D":
226
+ changes.push({ oldPath: firstPath, ...(oldBlob ? { oldOid: oldBlob } : {}) });
227
+ break;
228
+ case "M":
229
+ changes.push({
230
+ oldPath: firstPath,
231
+ newPath: firstPath,
232
+ ...(oldBlob ? { oldOid: oldBlob } : {}),
233
+ ...(newBlob ? { newOid: newBlob } : {}),
234
+ });
235
+ break;
236
+ case "R":
237
+ if (secondPath) {
238
+ changes.push({
239
+ oldPath: firstPath,
240
+ newPath: secondPath,
241
+ ...(oldBlob ? { oldOid: oldBlob } : {}),
242
+ ...(newBlob ? { newOid: newBlob } : {}),
243
+ });
244
+ }
245
+ break;
246
+ }
247
+ continue;
248
+ }
249
+
250
+ const firstTab = segment.indexOf("\t");
251
+ const secondTab = firstTab < 0 ? -1 : segment.indexOf("\t", firstTab + 1);
252
+ if (firstTab < 0 || secondTab < 0) throw new GitCommandError("unexpected git numstat output");
253
+ const addedField = segment.slice(0, firstTab);
254
+ const removedField = segment.slice(firstTab + 1, secondTab);
255
+ const inlinePath = segment.slice(secondTab + 1);
256
+ let oldPath: string | undefined;
257
+ let newPath: string | undefined;
258
+ if (inlinePath) {
259
+ oldPath = inlinePath;
260
+ newPath = inlinePath;
261
+ } else {
262
+ oldPath = segments[index++];
263
+ newPath = segments[index++];
264
+ if (!oldPath || !newPath) throw new GitCommandError("git rename numstat is missing a path");
265
+ }
266
+ const key = newPath ?? oldPath;
267
+ if (key) {
268
+ const added = numberField(addedField);
269
+ const removed = numberField(removedField);
270
+ stats.set(key, {
271
+ ...(added !== undefined ? { added } : {}),
272
+ ...(removed !== undefined ? { removed } : {}),
273
+ });
274
+ }
275
+ }
276
+
277
+ return changes.map((change) => {
278
+ const counts = stats.get(change.newPath ?? change.oldPath ?? "");
279
+ if (!counts) throw new GitCommandError("git diff is missing numstat data");
280
+ return { ...change, ...counts };
281
+ });
282
+ }
283
+
284
+ function lineCount(buffer: Buffer): number | undefined {
285
+ if (!isUtf8(buffer) || buffer.includes(0)) return undefined;
286
+ if (buffer.length === 0) return 0;
287
+ let lines = 0;
288
+ for (const byte of buffer) {
289
+ if (byte === 0x0a) lines += 1;
290
+ }
291
+ return buffer.at(-1) === 0x0a ? lines : lines + 1;
292
+ }
293
+
294
+ interface BoundedRegularFile {
295
+ readonly regular: boolean;
296
+ readonly buffer?: Buffer;
297
+ readonly bytes: number;
298
+ }
299
+
300
+ async function boundedRegularFile(path: string, budget: number): Promise<BoundedRegularFile | undefined> {
301
+ let initial: Stats;
302
+ try {
303
+ initial = await lstat(path);
304
+ } catch (error) {
305
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
306
+ throw error;
307
+ }
308
+ if (!initial.isFile()) return { regular: false, bytes: 0 };
309
+ if (budget <= 0 || initial.size > MAX_UNTRACKED_LINE_COUNT_BYTES || initial.size > budget) {
310
+ return { regular: true, bytes: 0 };
311
+ }
312
+
313
+ const noFollow = process.platform === "win32" ? 0 : constants.O_NOFOLLOW;
314
+ let handle: FileHandle;
315
+ try {
316
+ handle = await open(path, constants.O_RDONLY | constants.O_NONBLOCK | noFollow);
317
+ } catch (error) {
318
+ const code = (error as NodeJS.ErrnoException).code;
319
+ if (code === "ELOOP" || code === "ENOENT" || code === "ENXIO") return undefined;
320
+ throw error;
321
+ }
322
+ try {
323
+ const info = await handle.stat();
324
+ if (!info.isFile()) return { regular: false, bytes: 0 };
325
+ if (info.size > MAX_UNTRACKED_LINE_COUNT_BYTES || info.size > budget) return { regular: true, bytes: 0 };
326
+ const buffer = Buffer.alloc(info.size);
327
+ let offset = 0;
328
+ while (offset < buffer.length) {
329
+ const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
330
+ if (bytesRead === 0) break;
331
+ offset += bytesRead;
332
+ }
333
+ return { regular: true, buffer: buffer.subarray(0, offset), bytes: info.size };
334
+ } finally {
335
+ await handle.close();
336
+ }
337
+ }
338
+
339
+ function diffCounts(change: ParsedGitChange): FileEdit["diff"] {
340
+ return change.added === undefined && change.removed === undefined
341
+ ? undefined
342
+ : {
343
+ ...(change.added !== undefined ? { added: change.added } : {}),
344
+ ...(change.removed !== undefined ? { removed: change.removed } : {}),
345
+ };
346
+ }
347
+
348
+ function gitBlobUri(sessionId: string, oid: string, path: string, displayPath: string): string {
349
+ const metadata = Buffer.from(JSON.stringify({ sessionId, oid, path } satisfies GitBlobRef)).toString("base64url");
350
+ // Keep the query one opaque, unreserved token so URI parse/serialize round-trips preserve it.
351
+ return `${GIT_BLOB_SCHEME}${pathToFileURL(displayPath).pathname}?${metadata}`;
352
+ }
353
+
354
+ export function parseGitBlobUri(uri: string): GitBlobRef | undefined {
355
+ let parsed: URL;
356
+ try {
357
+ parsed = new URL(uri);
358
+ } catch {
359
+ return undefined;
360
+ }
361
+ if (parsed.protocol !== GIT_BLOB_SCHEME || parsed.hash || !parsed.search) return undefined;
362
+ const encoded = parsed.search.slice(1);
363
+ if (!/^[A-Za-z0-9_-]+$/u.test(encoded)) return undefined;
364
+ try {
365
+ const value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8")) as Partial<GitBlobRef>;
366
+ return typeof value.sessionId === "string" &&
367
+ value.sessionId.length > 0 &&
368
+ validOid(value.oid) &&
369
+ typeof value.path === "string"
370
+ ? { sessionId: value.sessionId, oid: value.oid, path: value.path }
371
+ : undefined;
372
+ } catch {
373
+ return undefined;
374
+ }
375
+ }
376
+
377
+ export class GitChanges implements GitChangesBackend {
378
+ async inspect(cwd: string, signal?: AbortSignal): Promise<GitWorkspace | undefined> {
379
+ const rootOutput = await runGit(cwd, ["rev-parse", "--show-toplevel"], {
380
+ allowFailure: true,
381
+ ...(signal ? { signal } : {}),
382
+ });
383
+ const root = outputLine(rootOutput);
384
+ if (!root) return undefined;
385
+
386
+ const [canonicalCwd, canonicalRoot] = await Promise.all([realpath(cwd), realpath(root)]);
387
+ if (!isInside(canonicalRoot, canonicalCwd)) return undefined;
388
+ const pathspec = relative(canonicalRoot, canonicalCwd).split(sep).join("/") || ".";
389
+ const [headOutput, gitDirectoryOutput, commonGitDirectoryOutput] = await Promise.all([
390
+ runGit(canonicalRoot, ["rev-parse", "--verify", "HEAD"], {
391
+ allowFailure: true,
392
+ ...(signal ? { signal } : {}),
393
+ }),
394
+ runGit(canonicalRoot, ["rev-parse", "--absolute-git-dir"], signal ? { signal } : {}),
395
+ runGit(canonicalRoot, ["rev-parse", "--git-common-dir"], signal ? { signal } : {}),
396
+ ]);
397
+ const headValue = outputLine(headOutput);
398
+ const head = validOid(headValue) ? headValue : undefined;
399
+ const parentValue = head
400
+ ? outputLine(
401
+ await runGit(canonicalRoot, ["rev-parse", "--verify", "HEAD^1"], {
402
+ allowFailure: true,
403
+ ...(signal ? { signal } : {}),
404
+ }),
405
+ )
406
+ : undefined;
407
+ const parent = validOid(parentValue) ? parentValue : undefined;
408
+ const gitDirectory = outputLine(gitDirectoryOutput);
409
+ const commonGitDirectory = outputLine(commonGitDirectoryOutput);
410
+ if (!gitDirectory || !commonGitDirectory) throw new GitCommandError("git did not report its metadata directory");
411
+ return {
412
+ cwd: canonicalCwd,
413
+ repositoryRoot: canonicalRoot,
414
+ gitDirectory: await realpath(resolve(canonicalRoot, gitDirectory)),
415
+ commonGitDirectory: await realpath(resolve(canonicalRoot, commonGitDirectory)),
416
+ pathspec,
417
+ ...(head ? { head } : {}),
418
+ ...(parent ? { parent } : {}),
419
+ };
420
+ }
421
+
422
+ async compute(
423
+ workspace: GitWorkspace,
424
+ sessionId: string,
425
+ kind: PiChangesetKind,
426
+ signal?: AbortSignal,
427
+ ): Promise<ChangesetFile[]> {
428
+ if (kind === "latest-commit") {
429
+ if (!workspace.head) return [];
430
+ const args = workspace.parent
431
+ ? [
432
+ "diff",
433
+ "--no-ext-diff",
434
+ "--no-textconv",
435
+ "--raw",
436
+ "--numstat",
437
+ "--no-abbrev",
438
+ "--diff-filter=ADMR",
439
+ "--find-renames",
440
+ "-z",
441
+ workspace.parent,
442
+ workspace.head,
443
+ "--",
444
+ workspace.pathspec,
445
+ ]
446
+ : [
447
+ "diff-tree",
448
+ "--no-ext-diff",
449
+ "--no-textconv",
450
+ "--root",
451
+ "--no-commit-id",
452
+ "-r",
453
+ "--raw",
454
+ "--numstat",
455
+ "--no-abbrev",
456
+ "--diff-filter=ADMR",
457
+ "--find-renames",
458
+ "-z",
459
+ workspace.head,
460
+ "--",
461
+ workspace.pathspec,
462
+ ];
463
+ const output = await runGit(workspace.repositoryRoot, args, signal ? { signal } : {});
464
+ return this.#toFiles(workspace, sessionId, "latest-commit", parseGitChanges(output ?? Buffer.alloc(0)));
465
+ }
466
+
467
+ const changes: ParsedGitChange[] = [];
468
+ if (workspace.head) {
469
+ const output = await runGit(
470
+ workspace.repositoryRoot,
471
+ [
472
+ "diff",
473
+ "--no-ext-diff",
474
+ "--no-textconv",
475
+ "--raw",
476
+ "--numstat",
477
+ "--no-abbrev",
478
+ "--diff-filter=ADMR",
479
+ "--find-renames",
480
+ "-z",
481
+ workspace.head,
482
+ "--",
483
+ workspace.pathspec,
484
+ ],
485
+ signal ? { signal } : {},
486
+ );
487
+ changes.push(...parseGitChanges(output ?? Buffer.alloc(0)));
488
+ }
489
+ const untracked = await runGit(
490
+ workspace.repositoryRoot,
491
+ [
492
+ "ls-files",
493
+ ...(workspace.head ? [] : ["--cached"]),
494
+ "--others",
495
+ "--exclude-standard",
496
+ "-z",
497
+ "--",
498
+ workspace.pathspec,
499
+ ],
500
+ signal ? { signal } : {},
501
+ );
502
+ const known = new Set(changes.map((change) => change.newPath ?? change.oldPath));
503
+ let remainingLineCountBytes = MAX_TOTAL_UNTRACKED_LINE_COUNT_BYTES;
504
+ for (const path of nulPaths(untracked, "git ls-files")) {
505
+ if (known.has(path)) continue;
506
+ const absolute = gitPathToAbsolute(workspace.repositoryRoot, path);
507
+ if (!absolute || !isInside(workspace.cwd, absolute)) continue;
508
+ const content = await boundedRegularFile(absolute, remainingLineCountBytes);
509
+ if (!content?.regular) continue;
510
+ const added = content.buffer ? lineCount(content.buffer) : undefined;
511
+ remainingLineCountBytes -= content.bytes;
512
+ changes.push({ newPath: path, ...(added !== undefined ? { added, removed: 0 } : {}) });
513
+ }
514
+ return this.#toFiles(workspace, sessionId, "uncommitted", changes);
515
+ }
516
+
517
+ async ignoredDirectories(workspace: GitWorkspace, signal?: AbortSignal): Promise<string[]> {
518
+ const output = await runGit(
519
+ workspace.repositoryRoot,
520
+ ["ls-files", "--others", "--ignored", "--exclude-standard", "--directory", "-z", "--", workspace.pathspec],
521
+ signal ? { signal } : {},
522
+ );
523
+ const directories: string[] = [];
524
+ for (const path of nulPaths(output, "git ls-files --ignored")) {
525
+ const absolute = gitPathToAbsolute(workspace.repositoryRoot, path.replace(/\/$/u, ""));
526
+ if (absolute && isInside(workspace.cwd, absolute)) directories.push(absolute);
527
+ }
528
+ return directories;
529
+ }
530
+
531
+ pathForBlob(workspace: GitWorkspace, blob: GitBlobRef): string | undefined {
532
+ const absolute = gitPathToAbsolute(workspace.repositoryRoot, blob.path);
533
+ return absolute && isInside(workspace.cwd, absolute) ? absolute : undefined;
534
+ }
535
+
536
+ async readBlob(workspace: GitWorkspace, blob: GitBlobRef, signal?: AbortSignal): Promise<Buffer> {
537
+ if (!validOid(blob.oid) || !this.pathForBlob(workspace, blob)) {
538
+ throw new GitCommandError("invalid git blob reference");
539
+ }
540
+ const output = await runGit(workspace.repositoryRoot, ["cat-file", "blob", blob.oid], signal ? { signal } : {});
541
+ if (!output) throw new GitCommandError("git blob was not found");
542
+ return output;
543
+ }
544
+
545
+ #toFiles(
546
+ workspace: GitWorkspace,
547
+ sessionId: string,
548
+ kind: PiChangesetKind,
549
+ changes: readonly ParsedGitChange[],
550
+ ): ChangesetFile[] {
551
+ const files: ChangesetFile[] = [];
552
+ for (const change of changes) {
553
+ const oldAbsolute = change.oldPath ? gitPathToAbsolute(workspace.repositoryRoot, change.oldPath) : undefined;
554
+ const newAbsolute = change.newPath ? gitPathToAbsolute(workspace.repositoryRoot, change.newPath) : undefined;
555
+ const oldPath = oldAbsolute && isInside(workspace.cwd, oldAbsolute) ? change.oldPath : undefined;
556
+ const newPath = newAbsolute && isInside(workspace.cwd, newAbsolute) ? change.newPath : undefined;
557
+ if ((!oldPath || !oldAbsolute) && (!newPath || !newAbsolute)) continue;
558
+
559
+ let before: NonNullable<FileEdit["before"]> | undefined;
560
+ let after: NonNullable<FileEdit["after"]> | undefined;
561
+ if (kind === "latest-commit") {
562
+ if ((oldPath && !change.oldOid) || (newPath && !change.newOid)) {
563
+ throw new GitCommandError("git diff did not provide a committed blob id");
564
+ }
565
+ const afterBlob =
566
+ newPath && newAbsolute && change.newOid
567
+ ? gitBlobUri(sessionId, change.newOid, newPath, newAbsolute)
568
+ : undefined;
569
+ const beforeBlob =
570
+ oldPath && oldAbsolute && change.oldOid
571
+ ? gitBlobUri(sessionId, change.oldOid, oldPath, oldAbsolute)
572
+ : undefined;
573
+ if (beforeBlob) {
574
+ // For an in-place edit both sides share one file identity. Using the
575
+ // committed after-side URI also keeps clients from substituting the
576
+ // mutable working-tree file for this immutable view.
577
+ before = { uri: oldPath === newPath && afterBlob ? afterBlob : beforeBlob, content: { uri: beforeBlob } };
578
+ }
579
+ if (afterBlob) after = { uri: afterBlob, content: { uri: afterBlob } };
580
+ } else if (kind === "uncommitted") {
581
+ if (oldPath && !change.oldOid) throw new GitCommandError("git diff did not provide a baseline blob id");
582
+ if (oldPath && oldAbsolute && change.oldOid) {
583
+ before = {
584
+ uri: pathToFileURL(oldAbsolute).toString(),
585
+ content: { uri: gitBlobUri(sessionId, change.oldOid, oldPath, oldAbsolute) },
586
+ };
587
+ }
588
+ if (newPath && newAbsolute) {
589
+ const uri = pathToFileURL(newAbsolute).toString();
590
+ after = { uri, content: { uri } };
591
+ }
592
+ }
593
+ if (!before && !after) continue;
594
+ const diff = diffCounts(change);
595
+ const edit: FileEdit = {
596
+ ...(before ? { before } : {}),
597
+ ...(after ? { after } : {}),
598
+ ...(diff ? { diff } : {}),
599
+ };
600
+ const identity = (newPath ? newAbsolute : undefined) ?? (oldPath ? oldAbsolute : undefined);
601
+ if (identity) files.push({ id: pathToFileURL(identity).toString(), edit });
602
+ }
603
+ return files.sort((left, right) => left.id.localeCompare(right.id));
604
+ }
605
+ }