@getpaseo/server 0.1.58 → 0.1.60

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 (276) hide show
  1. package/dist/scripts/dev-runner.js +26 -7
  2. package/dist/scripts/dev-runner.js.map +1 -1
  3. package/dist/server/client/daemon-client-runtime-metrics.d.ts +39 -0
  4. package/dist/server/client/daemon-client-runtime-metrics.d.ts.map +1 -0
  5. package/dist/server/client/daemon-client-runtime-metrics.js +173 -0
  6. package/dist/server/client/daemon-client-runtime-metrics.js.map +1 -0
  7. package/dist/server/client/daemon-client.d.ts +58 -9
  8. package/dist/server/client/daemon-client.d.ts.map +1 -1
  9. package/dist/server/client/daemon-client.js +151 -10
  10. package/dist/server/client/daemon-client.js.map +1 -1
  11. package/dist/server/server/agent/agent-manager.d.ts +55 -48
  12. package/dist/server/server/agent/agent-manager.d.ts.map +1 -1
  13. package/dist/server/server/agent/agent-manager.js +541 -331
  14. package/dist/server/server/agent/agent-manager.js.map +1 -1
  15. package/dist/server/server/agent/agent-metadata-generator.d.ts +3 -2
  16. package/dist/server/server/agent/agent-metadata-generator.d.ts.map +1 -1
  17. package/dist/server/server/agent/agent-metadata-generator.js +31 -16
  18. package/dist/server/server/agent/agent-metadata-generator.js.map +1 -1
  19. package/dist/server/server/agent/agent-projections.d.ts +2 -1
  20. package/dist/server/server/agent/agent-projections.d.ts.map +1 -1
  21. package/dist/server/server/agent/agent-projections.js +29 -1
  22. package/dist/server/server/agent/agent-projections.js.map +1 -1
  23. package/dist/server/server/agent/agent-sdk-types.d.ts +9 -5
  24. package/dist/server/server/agent/agent-sdk-types.d.ts.map +1 -1
  25. package/dist/server/server/agent/agent-sdk-types.js.map +1 -1
  26. package/dist/server/server/agent/agent-storage.d.ts +76 -69
  27. package/dist/server/server/agent/agent-storage.d.ts.map +1 -1
  28. package/dist/server/server/agent/agent-storage.js +13 -6
  29. package/dist/server/server/agent/agent-storage.js.map +1 -1
  30. package/dist/server/server/agent/agent-stream-coalescer.d.ts +41 -0
  31. package/dist/server/server/agent/agent-stream-coalescer.d.ts.map +1 -0
  32. package/dist/server/server/agent/agent-stream-coalescer.js +166 -0
  33. package/dist/server/server/agent/agent-stream-coalescer.js.map +1 -0
  34. package/dist/server/server/agent/agent-timeline-store-types.d.ts +54 -0
  35. package/dist/server/server/agent/agent-timeline-store-types.d.ts.map +1 -0
  36. package/dist/server/server/agent/agent-timeline-store-types.js +2 -0
  37. package/dist/server/server/agent/agent-timeline-store-types.js.map +1 -0
  38. package/dist/server/server/agent/agent-timeline-store.d.ts +32 -0
  39. package/dist/server/server/agent/agent-timeline-store.d.ts.map +1 -0
  40. package/dist/server/server/agent/agent-timeline-store.js +245 -0
  41. package/dist/server/server/agent/agent-timeline-store.js.map +1 -0
  42. package/dist/server/server/agent/mcp-server.d.ts +12 -1
  43. package/dist/server/server/agent/mcp-server.d.ts.map +1 -1
  44. package/dist/server/server/agent/mcp-server.js +276 -65
  45. package/dist/server/server/agent/mcp-server.js.map +1 -1
  46. package/dist/server/server/agent/mcp-shared.d.ts +196 -152
  47. package/dist/server/server/agent/mcp-shared.d.ts.map +1 -1
  48. package/dist/server/server/agent/mcp-shared.js +40 -42
  49. package/dist/server/server/agent/mcp-shared.js.map +1 -1
  50. package/dist/server/server/agent/model-resolver.d.ts.map +1 -1
  51. package/dist/server/server/agent/model-resolver.js +3 -1
  52. package/dist/server/server/agent/model-resolver.js.map +1 -1
  53. package/dist/server/server/agent/prompt-attachments.d.ts +6 -0
  54. package/dist/server/server/agent/prompt-attachments.d.ts.map +1 -0
  55. package/dist/server/server/agent/prompt-attachments.js +31 -0
  56. package/dist/server/server/agent/prompt-attachments.js.map +1 -0
  57. package/dist/server/server/agent/provider-launch-config.d.ts +12 -10
  58. package/dist/server/server/agent/provider-launch-config.d.ts.map +1 -1
  59. package/dist/server/server/agent/provider-launch-config.js +34 -0
  60. package/dist/server/server/agent/provider-launch-config.js.map +1 -1
  61. package/dist/server/server/agent/provider-manifest.d.ts +1 -0
  62. package/dist/server/server/agent/provider-manifest.d.ts.map +1 -1
  63. package/dist/server/server/agent/provider-manifest.js +22 -1
  64. package/dist/server/server/agent/provider-manifest.js.map +1 -1
  65. package/dist/server/server/agent/provider-registry.d.ts +5 -2
  66. package/dist/server/server/agent/provider-registry.d.ts.map +1 -1
  67. package/dist/server/server/agent/provider-registry.js +20 -9
  68. package/dist/server/server/agent/provider-registry.js.map +1 -1
  69. package/dist/server/server/agent/provider-snapshot-manager.d.ts +17 -5
  70. package/dist/server/server/agent/provider-snapshot-manager.d.ts.map +1 -1
  71. package/dist/server/server/agent/provider-snapshot-manager.js +150 -61
  72. package/dist/server/server/agent/provider-snapshot-manager.js.map +1 -1
  73. package/dist/server/server/agent/providers/acp-agent.d.ts +8 -4
  74. package/dist/server/server/agent/providers/acp-agent.d.ts.map +1 -1
  75. package/dist/server/server/agent/providers/acp-agent.js +73 -8
  76. package/dist/server/server/agent/providers/acp-agent.js.map +1 -1
  77. package/dist/server/server/agent/providers/claude/claude-models.d.ts.map +1 -1
  78. package/dist/server/server/agent/providers/claude/claude-models.js +21 -0
  79. package/dist/server/server/agent/providers/claude/claude-models.js.map +1 -1
  80. package/dist/server/server/agent/providers/claude/task-notification-tool-call.d.ts +2 -2
  81. package/dist/server/server/agent/providers/claude-agent.d.ts +1 -1
  82. package/dist/server/server/agent/providers/claude-agent.d.ts.map +1 -1
  83. package/dist/server/server/agent/providers/claude-agent.js +15 -8
  84. package/dist/server/server/agent/providers/claude-agent.js.map +1 -1
  85. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts +37 -4
  86. package/dist/server/server/agent/providers/codex-app-server-agent.d.ts.map +1 -1
  87. package/dist/server/server/agent/providers/codex-app-server-agent.js +61 -31
  88. package/dist/server/server/agent/providers/codex-app-server-agent.js.map +1 -1
  89. package/dist/server/server/agent/providers/copilot-acp-agent.d.ts.map +1 -1
  90. package/dist/server/server/agent/providers/copilot-acp-agent.js +3 -2
  91. package/dist/server/server/agent/providers/copilot-acp-agent.js.map +1 -1
  92. package/dist/server/server/agent/providers/mock-load-test-agent.d.ts +64 -0
  93. package/dist/server/server/agent/providers/mock-load-test-agent.d.ts.map +1 -0
  94. package/dist/server/server/agent/providers/mock-load-test-agent.js +585 -0
  95. package/dist/server/server/agent/providers/mock-load-test-agent.js.map +1 -0
  96. package/dist/server/server/agent/providers/opencode-agent.d.ts +19 -4
  97. package/dist/server/server/agent/providers/opencode-agent.d.ts.map +1 -1
  98. package/dist/server/server/agent/providers/opencode-agent.js +227 -118
  99. package/dist/server/server/agent/providers/opencode-agent.js.map +1 -1
  100. package/dist/server/server/agent/providers/pi-direct-agent.d.ts +69 -0
  101. package/dist/server/server/agent/providers/pi-direct-agent.d.ts.map +1 -0
  102. package/dist/server/server/agent/providers/pi-direct-agent.js +1177 -0
  103. package/dist/server/server/agent/providers/pi-direct-agent.js.map +1 -0
  104. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts +7 -4
  105. package/dist/server/server/agent/providers/tool-call-detail-primitives.d.ts.map +1 -1
  106. package/dist/server/server/agent-attention-policy.d.ts +13 -13
  107. package/dist/server/server/agent-attention-policy.d.ts.map +1 -1
  108. package/dist/server/server/agent-attention-policy.js +20 -36
  109. package/dist/server/server/agent-attention-policy.js.map +1 -1
  110. package/dist/server/server/bootstrap.d.ts +6 -0
  111. package/dist/server/server/bootstrap.d.ts.map +1 -1
  112. package/dist/server/server/bootstrap.js +113 -11
  113. package/dist/server/server/bootstrap.js.map +1 -1
  114. package/dist/server/server/chat/chat-rpc-schemas.d.ts +44 -44
  115. package/dist/server/server/chat/chat-types.d.ts +6 -6
  116. package/dist/server/server/checkout-diff-manager.d.ts +0 -1
  117. package/dist/server/server/checkout-diff-manager.d.ts.map +1 -1
  118. package/dist/server/server/checkout-diff-manager.js +6 -4
  119. package/dist/server/server/checkout-diff-manager.js.map +1 -1
  120. package/dist/server/server/config.d.ts.map +1 -1
  121. package/dist/server/server/config.js +1 -0
  122. package/dist/server/server/config.js.map +1 -1
  123. package/dist/server/server/file-explorer/service.d.ts.map +1 -1
  124. package/dist/server/server/file-explorer/service.js +2 -1
  125. package/dist/server/server/file-explorer/service.js.map +1 -1
  126. package/dist/server/server/loop/rpc-schemas.d.ts +392 -392
  127. package/dist/server/server/loop-service.d.ts +52 -52
  128. package/dist/server/server/paseo-worktree-archive-service.d.ts +41 -0
  129. package/dist/server/server/paseo-worktree-archive-service.d.ts.map +1 -0
  130. package/dist/server/server/paseo-worktree-archive-service.js +137 -0
  131. package/dist/server/server/paseo-worktree-archive-service.js.map +1 -0
  132. package/dist/server/server/paseo-worktree-service.d.ts +24 -0
  133. package/dist/server/server/paseo-worktree-service.d.ts.map +1 -0
  134. package/dist/server/server/paseo-worktree-service.js +94 -0
  135. package/dist/server/server/paseo-worktree-service.js.map +1 -0
  136. package/dist/server/server/path-utils.d.ts +1 -0
  137. package/dist/server/server/path-utils.d.ts.map +1 -1
  138. package/dist/server/server/path-utils.js +9 -0
  139. package/dist/server/server/path-utils.js.map +1 -1
  140. package/dist/server/server/persisted-config.d.ts +73 -73
  141. package/dist/server/server/persistence-hooks.d.ts.map +1 -1
  142. package/dist/server/server/persistence-hooks.js +3 -0
  143. package/dist/server/server/persistence-hooks.js.map +1 -1
  144. package/dist/server/server/resolve-worktree-creation-intent.d.ts +30 -0
  145. package/dist/server/server/resolve-worktree-creation-intent.d.ts.map +1 -0
  146. package/dist/server/server/resolve-worktree-creation-intent.js +163 -0
  147. package/dist/server/server/resolve-worktree-creation-intent.js.map +1 -0
  148. package/dist/server/server/schedule/rpc-schemas.d.ts +192 -192
  149. package/dist/server/server/schedule/service.d.ts +1 -1
  150. package/dist/server/server/schedule/service.d.ts.map +1 -1
  151. package/dist/server/server/schedule/types.d.ts +44 -44
  152. package/dist/server/server/script-health-monitor.d.ts +39 -0
  153. package/dist/server/server/script-health-monitor.d.ts.map +1 -0
  154. package/dist/server/server/script-health-monitor.js +158 -0
  155. package/dist/server/server/script-health-monitor.js.map +1 -0
  156. package/dist/server/server/script-proxy.d.ts +40 -0
  157. package/dist/server/server/script-proxy.d.ts.map +1 -0
  158. package/dist/server/server/script-proxy.js +245 -0
  159. package/dist/server/server/script-proxy.js.map +1 -0
  160. package/dist/server/server/script-route-branch-handler.d.ts +10 -0
  161. package/dist/server/server/script-route-branch-handler.d.ts.map +1 -0
  162. package/dist/server/server/script-route-branch-handler.js +45 -0
  163. package/dist/server/server/script-route-branch-handler.js.map +1 -0
  164. package/dist/server/server/script-status-projection.d.ts +29 -0
  165. package/dist/server/server/script-status-projection.d.ts.map +1 -0
  166. package/dist/server/server/script-status-projection.js +133 -0
  167. package/dist/server/server/script-status-projection.js.map +1 -0
  168. package/dist/server/server/session.d.ts +77 -13
  169. package/dist/server/server/session.d.ts.map +1 -1
  170. package/dist/server/server/session.js +1290 -548
  171. package/dist/server/server/session.js.map +1 -1
  172. package/dist/server/server/websocket-server.d.ts +27 -3
  173. package/dist/server/server/websocket-server.d.ts.map +1 -1
  174. package/dist/server/server/websocket-server.js +112 -29
  175. package/dist/server/server/websocket-server.js.map +1 -1
  176. package/dist/server/server/workspace-archive-service.d.ts +8 -0
  177. package/dist/server/server/workspace-archive-service.d.ts.map +1 -0
  178. package/dist/server/server/workspace-archive-service.js +17 -0
  179. package/dist/server/server/workspace-archive-service.js.map +1 -0
  180. package/dist/server/server/workspace-git-metadata.d.ts +24 -0
  181. package/dist/server/server/workspace-git-metadata.d.ts.map +1 -0
  182. package/dist/server/server/workspace-git-metadata.js +78 -0
  183. package/dist/server/server/workspace-git-metadata.js.map +1 -0
  184. package/dist/server/server/workspace-git-service.d.ts +104 -5
  185. package/dist/server/server/workspace-git-service.d.ts.map +1 -1
  186. package/dist/server/server/workspace-git-service.js +442 -56
  187. package/dist/server/server/workspace-git-service.js.map +1 -1
  188. package/dist/server/server/workspace-reconciliation-service.d.ts +54 -0
  189. package/dist/server/server/workspace-reconciliation-service.d.ts.map +1 -0
  190. package/dist/server/server/workspace-reconciliation-service.js +176 -0
  191. package/dist/server/server/workspace-reconciliation-service.js.map +1 -0
  192. package/dist/server/server/workspace-registry-bootstrap.d.ts.map +1 -1
  193. package/dist/server/server/workspace-registry-bootstrap.js +4 -3
  194. package/dist/server/server/workspace-registry-bootstrap.js.map +1 -1
  195. package/dist/server/server/workspace-registry.d.ts +8 -8
  196. package/dist/server/server/workspace-registry.test-helpers.d.ts +37 -0
  197. package/dist/server/server/workspace-registry.test-helpers.d.ts.map +1 -0
  198. package/dist/server/server/workspace-registry.test-helpers.js +121 -0
  199. package/dist/server/server/workspace-registry.test-helpers.js.map +1 -0
  200. package/dist/server/server/workspace-script-runtime-store.d.ts +28 -0
  201. package/dist/server/server/workspace-script-runtime-store.d.ts.map +1 -0
  202. package/dist/server/server/workspace-script-runtime-store.js +78 -0
  203. package/dist/server/server/workspace-script-runtime-store.js.map +1 -0
  204. package/dist/server/server/workspace-service-env.d.ts +17 -0
  205. package/dist/server/server/workspace-service-env.d.ts.map +1 -0
  206. package/dist/server/server/workspace-service-env.js +80 -0
  207. package/dist/server/server/workspace-service-env.js.map +1 -0
  208. package/dist/server/server/workspace-service-port-registry.d.ts +19 -0
  209. package/dist/server/server/workspace-service-port-registry.d.ts.map +1 -0
  210. package/dist/server/server/workspace-service-port-registry.js +59 -0
  211. package/dist/server/server/workspace-service-port-registry.js.map +1 -0
  212. package/dist/server/server/worktree-bootstrap.d.ts +55 -10
  213. package/dist/server/server/worktree-bootstrap.d.ts.map +1 -1
  214. package/dist/server/server/worktree-bootstrap.js +290 -112
  215. package/dist/server/server/worktree-bootstrap.js.map +1 -1
  216. package/dist/server/server/worktree-core.d.ts +25 -0
  217. package/dist/server/server/worktree-core.d.ts.map +1 -0
  218. package/dist/server/server/worktree-core.js +75 -0
  219. package/dist/server/server/worktree-core.js.map +1 -0
  220. package/dist/server/server/worktree-errors.d.ts +12 -0
  221. package/dist/server/server/worktree-errors.d.ts.map +1 -0
  222. package/dist/server/server/worktree-errors.js +31 -0
  223. package/dist/server/server/worktree-errors.js.map +1 -0
  224. package/dist/server/server/worktree-session.d.ts +56 -70
  225. package/dist/server/server/worktree-session.d.ts.map +1 -1
  226. package/dist/server/server/worktree-session.js +176 -251
  227. package/dist/server/server/worktree-session.js.map +1 -1
  228. package/dist/server/services/github-service.d.ts +225 -0
  229. package/dist/server/services/github-service.d.ts.map +1 -0
  230. package/dist/server/services/github-service.js +1381 -0
  231. package/dist/server/services/github-service.js.map +1 -0
  232. package/dist/server/shared/messages.d.ts +29408 -12268
  233. package/dist/server/shared/messages.d.ts.map +1 -1
  234. package/dist/server/shared/messages.js +391 -65
  235. package/dist/server/shared/messages.js.map +1 -1
  236. package/dist/server/terminal/shell-integration/zsh/.zshenv +17 -0
  237. package/dist/server/terminal/shell-integration/zsh/paseo-integration.zsh +32 -0
  238. package/dist/server/terminal/terminal-manager.d.ts +9 -0
  239. package/dist/server/terminal/terminal-manager.d.ts.map +1 -1
  240. package/dist/server/terminal/terminal-manager.js +27 -0
  241. package/dist/server/terminal/terminal-manager.js.map +1 -1
  242. package/dist/server/terminal/terminal-output-coalescer.d.ts +30 -0
  243. package/dist/server/terminal/terminal-output-coalescer.d.ts.map +1 -0
  244. package/dist/server/terminal/terminal-output-coalescer.js +55 -0
  245. package/dist/server/terminal/terminal-output-coalescer.js.map +1 -0
  246. package/dist/server/terminal/terminal.d.ts +32 -1
  247. package/dist/server/terminal/terminal.d.ts.map +1 -1
  248. package/dist/server/terminal/terminal.js +397 -17
  249. package/dist/server/terminal/terminal.js.map +1 -1
  250. package/dist/server/utils/checkout-git.d.ts +63 -10
  251. package/dist/server/utils/checkout-git.d.ts.map +1 -1
  252. package/dist/server/utils/checkout-git.js +321 -229
  253. package/dist/server/utils/checkout-git.js.map +1 -1
  254. package/dist/server/utils/promise-timeout.d.ts +9 -0
  255. package/dist/server/utils/promise-timeout.d.ts.map +1 -0
  256. package/dist/server/utils/promise-timeout.js +25 -0
  257. package/dist/server/utils/promise-timeout.js.map +1 -0
  258. package/dist/server/utils/script-hostname.d.ts +8 -0
  259. package/dist/server/utils/script-hostname.d.ts.map +1 -0
  260. package/dist/server/utils/script-hostname.js +14 -0
  261. package/dist/server/utils/script-hostname.js.map +1 -0
  262. package/dist/server/utils/string-command-shell.d.ts +10 -0
  263. package/dist/server/utils/string-command-shell.d.ts.map +1 -0
  264. package/dist/server/utils/string-command-shell.js +21 -0
  265. package/dist/server/utils/string-command-shell.js.map +1 -0
  266. package/dist/server/utils/worktree.d.ts +54 -7
  267. package/dist/server/utils/worktree.d.ts.map +1 -1
  268. package/dist/server/utils/worktree.js +434 -129
  269. package/dist/server/utils/worktree.js.map +1 -1
  270. package/dist/src/terminal/shell-integration/zsh/.zshenv +17 -0
  271. package/dist/src/terminal/shell-integration/zsh/paseo-integration.zsh +32 -0
  272. package/package.json +11 -14
  273. package/dist/server/server/agent/providers/pi-acp-agent.d.ts +0 -28
  274. package/dist/server/server/agent/providers/pi-acp-agent.d.ts.map +0 -1
  275. package/dist/server/server/agent/providers/pi-acp-agent.js +0 -302
  276. package/dist/server/server/agent/providers/pi-acp-agent.js.map +0 -1
@@ -1,12 +1,11 @@
1
1
  import { resolve, dirname, basename } from "path";
2
2
  import { existsSync, realpathSync } from "fs";
3
- import { open as openFile, stat as statFile } from "fs/promises";
3
+ import { open as openFile, readFile, stat as statFile } from "fs/promises";
4
4
  import { TTLCache } from "@isaacs/ttlcache";
5
5
  import { parseAndHighlightDiff } from "../server/utils/diff-highlighter.js";
6
- import { findExecutable } from "./executable.js";
6
+ import { GitHubAuthenticationError, GitHubCliMissingError, createGitHubService, resolveGitHubRepo, } from "../services/github-service.js";
7
7
  import { parseGitRevParsePath, resolveGitRevParsePath } from "./git-rev-parse-path.js";
8
8
  import { runGitCommand } from "./run-git-command.js";
9
- import { execCommand } from "./spawn.js";
10
9
  import { isPaseoOwnedWorktreeCwd } from "./worktree.js";
11
10
  import { requirePaseoWorktreeBaseRefName } from "./worktree-metadata.js";
12
11
  const READ_ONLY_GIT_ENV = {
@@ -15,10 +14,14 @@ const READ_ONLY_GIT_ENV = {
15
14
  };
16
15
  const DEFAULT_PULL_REQUEST_STATUS_CACHE_TTL_MS = 30000;
17
16
  const PULL_REQUEST_STATUS_CACHE_MAX = 1000;
17
+ const DEFAULT_SHORTSTAT_CACHE_TTL_MS = 15000;
18
+ const SHORTSTAT_CACHE_MAX = 1000;
18
19
  let pullRequestStatusCacheTtlMs = DEFAULT_PULL_REQUEST_STATUS_CACHE_TTL_MS;
19
20
  let pullRequestStatusCache = createPullRequestStatusCache(pullRequestStatusCacheTtlMs);
20
21
  const pullRequestStatusInFlight = new Map();
21
- let cachedGhPath = undefined;
22
+ let shortstatCacheTtlMs = DEFAULT_SHORTSTAT_CACHE_TTL_MS;
23
+ let shortstatCache = createShortstatCache(shortstatCacheTtlMs);
24
+ const shortstatInFlight = new Map();
22
25
  function createPullRequestStatusCache(ttlMs) {
23
26
  return new TTLCache({
24
27
  ttl: ttlMs,
@@ -26,9 +29,19 @@ function createPullRequestStatusCache(ttlMs) {
26
29
  checkAgeOnGet: true,
27
30
  });
28
31
  }
32
+ function createShortstatCache(ttlMs) {
33
+ return new TTLCache({
34
+ ttl: ttlMs,
35
+ max: SHORTSTAT_CACHE_MAX,
36
+ checkAgeOnGet: true,
37
+ });
38
+ }
29
39
  function getPullRequestStatusCacheKey(cwd) {
30
40
  return resolve(cwd);
31
41
  }
42
+ function getShortstatCacheKey(cwd) {
43
+ return resolve(cwd);
44
+ }
32
45
  export function __resetPullRequestStatusCacheForTests() {
33
46
  pullRequestStatusCache.clear();
34
47
  pullRequestStatusCache.cancelTimer();
@@ -43,11 +56,22 @@ export function __setPullRequestStatusCacheTtlForTests(ttlMs) {
43
56
  pullRequestStatusCache = createPullRequestStatusCache(ttlMs);
44
57
  pullRequestStatusInFlight.clear();
45
58
  }
46
- export function __resetGhPathCacheForTests() {
47
- cachedGhPath = undefined;
59
+ export function __resetCheckoutShortstatCacheForTests() {
60
+ shortstatCache.clear();
61
+ shortstatCache.cancelTimer();
62
+ shortstatCacheTtlMs = DEFAULT_SHORTSTAT_CACHE_TTL_MS;
63
+ shortstatCache = createShortstatCache(shortstatCacheTtlMs);
64
+ shortstatInFlight.clear();
65
+ }
66
+ export function __setCheckoutShortstatCacheTtlForTests(ttlMs) {
67
+ shortstatCache.clear();
68
+ shortstatCache.cancelTimer();
69
+ shortstatCacheTtlMs = ttlMs;
70
+ shortstatCache = createShortstatCache(ttlMs);
71
+ shortstatInFlight.clear();
48
72
  }
49
- export function __setGhPathForTests(path) {
50
- cachedGhPath = path;
73
+ function getCheckoutDiffRefArgs(refs) {
74
+ return [refs.baseRef, ...(refs.targetRef ? [refs.targetRef] : [])];
51
75
  }
52
76
  function normalizeBranchSuggestionName(raw) {
53
77
  const trimmed = raw.trim();
@@ -126,7 +150,8 @@ export async function listBranchSuggestions(cwd, options) {
126
150
  continue;
127
151
  const existing = branchMeta.get(normalized);
128
152
  branchMeta.set(normalized, {
129
- isLocal: true,
153
+ hasLocal: true,
154
+ hasRemote: existing?.hasRemote ?? false,
130
155
  committerDate: Math.max(ref.committerDate, existing?.committerDate ?? 0),
131
156
  });
132
157
  }
@@ -136,11 +161,16 @@ export async function listBranchSuggestions(cwd, options) {
136
161
  continue;
137
162
  const existing = branchMeta.get(normalized);
138
163
  if (!existing) {
139
- branchMeta.set(normalized, { isLocal: false, committerDate: ref.committerDate });
164
+ branchMeta.set(normalized, {
165
+ hasLocal: false,
166
+ hasRemote: true,
167
+ committerDate: ref.committerDate,
168
+ });
140
169
  }
141
170
  else {
142
171
  branchMeta.set(normalized, {
143
172
  ...existing,
173
+ hasRemote: true,
144
174
  committerDate: Math.max(ref.committerDate, existing.committerDate),
145
175
  });
146
176
  }
@@ -150,13 +180,71 @@ export async function listBranchSuggestions(cwd, options) {
150
180
  return [];
151
181
  }
152
182
  const ordered = sortBranchSuggestions(filteredNames, branchMeta, query);
153
- return ordered.slice(0, limit);
183
+ return ordered.slice(0, limit).map((name) => {
184
+ const meta = branchMeta.get(name);
185
+ return {
186
+ name,
187
+ committerDate: meta?.committerDate ?? 0,
188
+ hasLocal: meta?.hasLocal ?? false,
189
+ hasRemote: meta?.hasRemote ?? false,
190
+ };
191
+ });
154
192
  }
155
- async function listCheckoutFileChanges(cwd, ref, ignoreWhitespace = false) {
193
+ export async function resolveBranchCheckout(cwd, name) {
194
+ await requireGitRepo(cwd);
195
+ const normalized = normalizeBranchSuggestionName(name);
196
+ if (!normalized) {
197
+ return { kind: "not-found" };
198
+ }
199
+ const localRef = `refs/heads/${normalized}`;
200
+ const localResult = await runGitCommand(["rev-parse", "--verify", "--quiet", localRef], {
201
+ cwd,
202
+ env: READ_ONLY_GIT_ENV,
203
+ acceptExitCodes: [0, 1],
204
+ });
205
+ const hasLocal = localResult.exitCode === 0;
206
+ if (hasLocal) {
207
+ return { kind: "local", name: normalized };
208
+ }
209
+ const remoteRef = `origin/${normalized}`;
210
+ const remoteRefPath = `refs/remotes/${remoteRef}`;
211
+ const remoteResult = await runGitCommand(["rev-parse", "--verify", "--quiet", remoteRefPath], {
212
+ cwd,
213
+ env: READ_ONLY_GIT_ENV,
214
+ acceptExitCodes: [0, 1],
215
+ });
216
+ const hasRemote = remoteResult.exitCode === 0;
217
+ if (hasRemote) {
218
+ return { kind: "remote-only", name: normalized, remoteRef };
219
+ }
220
+ return { kind: "not-found" };
221
+ }
222
+ export async function checkoutResolvedBranch(input) {
223
+ const { cwd, resolution } = input;
224
+ switch (resolution.kind) {
225
+ case "local": {
226
+ const { stdout } = await runGitCommand(["rev-parse", "--abbrev-ref", "HEAD"], { cwd });
227
+ const current = stdout.trim();
228
+ if (current === resolution.name) {
229
+ return { source: "local" };
230
+ }
231
+ await runGitCommand(["checkout", resolution.name], { cwd });
232
+ return { source: "local" };
233
+ }
234
+ case "remote-only":
235
+ await runGitCommand(["checkout", "-b", resolution.name, "--track", resolution.remoteRef], {
236
+ cwd,
237
+ });
238
+ return { source: "remote" };
239
+ case "not-found":
240
+ throw new Error(`Branch not found: ${input.requestedBranch ?? "unknown"}`);
241
+ }
242
+ }
243
+ async function listCheckoutFileChanges(cwd, refs, ignoreWhitespace = false) {
156
244
  const changes = [];
157
245
  const { stdout: nameStatusOut } = await runGitCommand(buildGitDiffArgs({
158
246
  ignoreWhitespace,
159
- extra: ["--name-status", ref],
247
+ extra: ["--name-status", ...getCheckoutDiffRefArgs(refs)],
160
248
  }), { cwd, env: READ_ONLY_GIT_ENV });
161
249
  for (const line of nameStatusOut
162
250
  .split("\n")
@@ -192,21 +280,23 @@ async function listCheckoutFileChanges(cwd, ref, ignoreWhitespace = false) {
192
280
  isDeleted: code === "D",
193
281
  });
194
282
  }
195
- const { stdout: untrackedOut } = await runGitCommand(["ls-files", "--others", "--exclude-standard"], {
196
- cwd,
197
- env: READ_ONLY_GIT_ENV,
198
- });
199
- for (const file of untrackedOut
200
- .split("\n")
201
- .map((l) => l.trim())
202
- .filter(Boolean)) {
203
- changes.push({
204
- path: file,
205
- status: "U",
206
- isNew: true,
207
- isDeleted: false,
208
- isUntracked: true,
283
+ if (refs.includeUntracked) {
284
+ const { stdout: untrackedOut } = await runGitCommand(["ls-files", "--others", "--exclude-standard"], {
285
+ cwd,
286
+ env: READ_ONLY_GIT_ENV,
209
287
  });
288
+ for (const file of untrackedOut
289
+ .split("\n")
290
+ .map((l) => l.trim())
291
+ .filter(Boolean)) {
292
+ changes.push({
293
+ path: file,
294
+ status: "U",
295
+ isNew: true,
296
+ isDeleted: false,
297
+ isUntracked: true,
298
+ });
299
+ }
210
300
  }
211
301
  // Deduplicate by path (prefer tracked status over untracked marker if both appear).
212
302
  const byPath = new Map();
@@ -264,10 +354,10 @@ function buildGitDiffArgs(args) {
264
354
  }
265
355
  const TRACKED_DIFF_NUMSTAT_MAX_BYTES = 2 * 1024 * 1024; // 2MB
266
356
  const TRACKED_MAX_CHANGED_LINES = 40000;
267
- async function getTrackedNumstatByPath(cwd, ref, ignoreWhitespace = false) {
357
+ async function getTrackedNumstatByPath(cwd, refs, ignoreWhitespace = false) {
268
358
  const result = await runGitCommand(buildGitDiffArgs({
269
359
  ignoreWhitespace,
270
- extra: ["--numstat", ref],
360
+ extra: ["--numstat", ...getCheckoutDiffRefArgs(refs)],
271
361
  }), {
272
362
  cwd,
273
363
  env: READ_ONLY_GIT_ENV,
@@ -358,12 +448,34 @@ export async function getCurrentBranch(cwd) {
358
448
  env: READ_ONLY_GIT_ENV,
359
449
  });
360
450
  const branch = stdout.trim();
451
+ if (branch === "HEAD") {
452
+ return await getRebaseHeadBranch(cwd);
453
+ }
361
454
  return branch.length > 0 ? branch : null;
362
455
  }
363
456
  catch {
364
457
  return null;
365
458
  }
366
459
  }
460
+ async function getRebaseHeadBranch(cwd) {
461
+ for (const path of ["rebase-merge/head-name", "rebase-apply/head-name"]) {
462
+ try {
463
+ const { stdout } = await runGitCommand(["rev-parse", "--git-path", path], {
464
+ cwd,
465
+ env: READ_ONLY_GIT_ENV,
466
+ });
467
+ const headName = (await readFile(resolve(cwd, stdout.trim()), "utf8")).trim();
468
+ if (headName.startsWith("refs/heads/")) {
469
+ return headName.slice("refs/heads/".length) || null;
470
+ }
471
+ return headName || null;
472
+ }
473
+ catch {
474
+ // Try the other rebase backend.
475
+ }
476
+ }
477
+ return null;
478
+ }
367
479
  async function getWorktreeRoot(cwd) {
368
480
  try {
369
481
  const { stdout } = await runGitCommand(["rev-parse", "--show-toplevel"], {
@@ -598,21 +710,47 @@ async function resolveBaseRef(repoRoot) {
598
710
  return resolveRepositoryDefaultBranch(repoRoot);
599
711
  }
600
712
  function normalizeLocalBranchRefName(input) {
601
- return input.startsWith("origin/") ? input.slice("origin/".length) : input;
713
+ if (input.startsWith("refs/remotes/origin/")) {
714
+ return input.slice("refs/remotes/origin/".length);
715
+ }
716
+ if (input.startsWith("refs/heads/")) {
717
+ return input.slice("refs/heads/".length);
718
+ }
719
+ if (input.startsWith("origin/")) {
720
+ return input.slice("origin/".length);
721
+ }
722
+ return input;
723
+ }
724
+ function normalizeComparisonBaseRefName(input) {
725
+ const localName = normalizeLocalBranchRefName(input);
726
+ return { localName, originRef: `origin/${localName}` };
602
727
  }
603
728
  async function doesGitRefExist(cwd, fullRef) {
604
- try {
605
- await runGitCommand(["show-ref", "--verify", "--quiet", fullRef], {
606
- cwd,
607
- env: READ_ONLY_GIT_ENV,
608
- });
609
- return true;
729
+ const result = await runGitCommand(["show-ref", "--verify", "--quiet", fullRef], {
730
+ cwd,
731
+ env: READ_ONLY_GIT_ENV,
732
+ acceptExitCodes: [0, 1],
733
+ });
734
+ return result.exitCode === 0;
735
+ }
736
+ async function resolveBestComparisonBaseRef(cwd, baseRef) {
737
+ const normalized = normalizeComparisonBaseRefName(baseRef);
738
+ const [hasLocal, hasOrigin] = await Promise.all([
739
+ doesGitRefExist(cwd, `refs/heads/${normalized.localName}`),
740
+ doesGitRefExist(cwd, `refs/remotes/origin/${normalized.localName}`),
741
+ ]);
742
+ if (hasOrigin) {
743
+ return normalized.originRef;
610
744
  }
611
- catch {
612
- return false;
745
+ if (hasLocal) {
746
+ return normalized.localName;
613
747
  }
748
+ const refName = baseRef.startsWith("origin/") || baseRef.startsWith("refs/remotes/origin/")
749
+ ? normalized.originRef
750
+ : normalized.localName;
751
+ throw new Error(`Base branch not found locally or on origin: ${refName}`);
614
752
  }
615
- async function resolveBestComparisonBaseRef(cwd, normalizedBaseRef) {
753
+ async function resolveMostAheadBaseRef(cwd, normalizedBaseRef) {
616
754
  const [hasLocal, hasOrigin] = await Promise.all([
617
755
  doesGitRefExist(cwd, `refs/heads/${normalizedBaseRef}`),
618
756
  doesGitRefExist(cwd, `refs/remotes/origin/${normalizedBaseRef}`),
@@ -626,18 +764,15 @@ async function resolveBestComparisonBaseRef(cwd, normalizedBaseRef) {
626
764
  if (!hasLocal && !hasOrigin) {
627
765
  throw new Error(`Base branch not found locally or on origin: ${normalizedBaseRef}`);
628
766
  }
629
- // Both exist: choose the ref with more unique commits compared to the other.
630
- try {
631
- const { stdout } = await runGitCommand(["rev-list", "--left-right", "--count", `${normalizedBaseRef}...origin/${normalizedBaseRef}`], { cwd, env: READ_ONLY_GIT_ENV });
632
- const [localOnlyRaw, originOnlyRaw] = stdout.trim().split(/\s+/);
633
- const localOnly = Number.parseInt(localOnlyRaw ?? "0", 10);
634
- const originOnly = Number.parseInt(originOnlyRaw ?? "0", 10);
635
- if (!Number.isNaN(localOnly) && !Number.isNaN(originOnly) && originOnly > localOnly) {
636
- return `origin/${normalizedBaseRef}`;
637
- }
767
+ const { stdout } = await runGitCommand(["rev-list", "--left-right", "--count", `${normalizedBaseRef}...origin/${normalizedBaseRef}`], { cwd, env: READ_ONLY_GIT_ENV });
768
+ const [localOnlyRaw, originOnlyRaw] = stdout.trim().split(/\s+/);
769
+ const localOnly = Number.parseInt(localOnlyRaw ?? "0", 10);
770
+ const originOnly = Number.parseInt(originOnlyRaw ?? "0", 10);
771
+ if (Number.isNaN(localOnly) || Number.isNaN(originOnly)) {
772
+ return normalizedBaseRef;
638
773
  }
639
- catch {
640
- // ignore and fall back to local
774
+ if (originOnly > localOnly) {
775
+ return `origin/${normalizedBaseRef}`;
641
776
  }
642
777
  return normalizedBaseRef;
643
778
  }
@@ -646,7 +781,7 @@ async function getAheadBehind(cwd, baseRef, currentBranch) {
646
781
  if (!normalizedBaseRef || !currentBranch || normalizedBaseRef === currentBranch) {
647
782
  return null;
648
783
  }
649
- const comparisonBaseRef = await resolveBestComparisonBaseRef(cwd, normalizedBaseRef);
784
+ const comparisonBaseRef = await resolveBestComparisonBaseRef(cwd, baseRef);
650
785
  const { stdout } = await runGitCommand(["rev-list", "--left-right", "--count", `${comparisonBaseRef}...${currentBranch}`], { cwd, env: READ_ONLY_GIT_ENV });
651
786
  const [behindRaw, aheadRaw] = stdout.trim().split(/\s+/);
652
787
  const behind = Number.parseInt(behindRaw ?? "0", 10);
@@ -852,7 +987,27 @@ export async function getCheckoutStatus(cwd, context) {
852
987
  isPaseoOwnedWorktree: false,
853
988
  };
854
989
  }
855
- export async function getCheckoutShortstat(cwd, context) {
990
+ function parseCheckoutShortstat(text) {
991
+ const trimmed = text.trim();
992
+ if (!trimmed) {
993
+ return null;
994
+ }
995
+ let additions = 0;
996
+ let deletions = 0;
997
+ const addMatch = trimmed.match(/(\d+)\s+insertion/);
998
+ if (addMatch) {
999
+ additions = Number.parseInt(addMatch[1], 10);
1000
+ }
1001
+ const delMatch = trimmed.match(/(\d+)\s+deletion/);
1002
+ if (delMatch) {
1003
+ deletions = Number.parseInt(delMatch[1], 10);
1004
+ }
1005
+ if (additions === 0 && deletions === 0) {
1006
+ return null;
1007
+ }
1008
+ return { additions, deletions };
1009
+ }
1010
+ async function getCheckoutShortstatUncached(cwd, context) {
856
1011
  try {
857
1012
  await requireGitRepo(cwd);
858
1013
  }
@@ -862,70 +1017,91 @@ export async function getCheckoutShortstat(cwd, context) {
862
1017
  const configured = await getConfiguredBaseRefForCwd(cwd, context);
863
1018
  const localBaseRef = configured.baseRef ?? (await resolveBaseRef(cwd));
864
1019
  const currentBranch = await getCurrentBranch(cwd);
865
- let diffTarget;
1020
+ let comparisonRef;
866
1021
  if (currentBranch && localBaseRef && currentBranch !== localBaseRef) {
867
- // Feature branch: diff against the merge-base with the base branch
868
- const comparisonBaseRef = await resolveBestComparisonBaseRef(cwd, normalizeLocalBranchRefName(localBaseRef));
869
1022
  try {
870
- const { stdout } = await runGitCommand(["merge-base", "HEAD", comparisonBaseRef], {
871
- cwd,
872
- env: READ_ONLY_GIT_ENV,
873
- });
874
- const mergeBase = stdout.trim();
875
- if (!mergeBase) {
876
- return null;
877
- }
878
- diffTarget = mergeBase;
1023
+ comparisonRef = await resolveBestComparisonBaseRef(cwd, localBaseRef);
879
1024
  }
880
1025
  catch {
881
1026
  return null;
882
1027
  }
883
1028
  }
884
1029
  else if (currentBranch) {
885
- // On the base branch (or no base ref configured): diff against remote tracking branch
886
1030
  const hasOrigin = await doesGitRefExist(cwd, `refs/remotes/origin/${currentBranch}`);
887
1031
  if (!hasOrigin) {
888
1032
  return null;
889
1033
  }
890
- diffTarget = `origin/${currentBranch}`;
1034
+ comparisonRef = `origin/${currentBranch}`;
891
1035
  }
892
1036
  else {
893
1037
  return null;
894
1038
  }
895
1039
  try {
896
- // Omit HEAD so the diff includes uncommitted (staged + unstaged) changes
897
- const { stdout } = await runGitCommand(["diff", "--shortstat", diffTarget], {
1040
+ const { stdout: mergeBaseOut } = await runGitCommand(["merge-base", "HEAD", comparisonRef], {
898
1041
  cwd,
899
1042
  env: READ_ONLY_GIT_ENV,
900
1043
  });
901
- const text = stdout.trim();
902
- if (!text) {
1044
+ const mergeBase = mergeBaseOut.trim();
1045
+ if (!mergeBase) {
903
1046
  return null;
904
1047
  }
905
- let additions = 0;
906
- let deletions = 0;
907
- const addMatch = text.match(/(\d+)\s+insertion/);
908
- if (addMatch) {
909
- additions = Number.parseInt(addMatch[1], 10);
910
- }
911
- const delMatch = text.match(/(\d+)\s+deletion/);
912
- if (delMatch) {
913
- deletions = Number.parseInt(delMatch[1], 10);
914
- }
915
- if (additions === 0 && deletions === 0) {
916
- return null;
917
- }
918
- return { additions, deletions };
1048
+ const { stdout } = await runGitCommand(["diff", "--shortstat", mergeBase], {
1049
+ cwd,
1050
+ env: READ_ONLY_GIT_ENV,
1051
+ });
1052
+ return parseCheckoutShortstat(stdout);
919
1053
  }
920
1054
  catch {
921
1055
  return null;
922
1056
  }
923
1057
  }
1058
+ function getOrLoadCheckoutShortstat(cwd, context, options) {
1059
+ const cacheKey = getShortstatCacheKey(cwd);
1060
+ if (!options?.force) {
1061
+ const cached = shortstatCache.get(cacheKey);
1062
+ if (cached !== undefined) {
1063
+ return Promise.resolve(cached);
1064
+ }
1065
+ const existing = shortstatInFlight.get(cacheKey);
1066
+ if (existing) {
1067
+ return existing;
1068
+ }
1069
+ }
1070
+ const load = getCheckoutShortstatUncached(cwd, context)
1071
+ .then((shortstat) => {
1072
+ shortstatCache.set(cacheKey, shortstat);
1073
+ return shortstat;
1074
+ })
1075
+ .finally(() => {
1076
+ shortstatInFlight.delete(cacheKey);
1077
+ });
1078
+ shortstatInFlight.set(cacheKey, load);
1079
+ return load;
1080
+ }
1081
+ export async function getCheckoutShortstat(cwd, context, options) {
1082
+ return getOrLoadCheckoutShortstat(cwd, context, options);
1083
+ }
1084
+ export function getCachedCheckoutShortstat(cwd) {
1085
+ return shortstatCache.get(getShortstatCacheKey(cwd));
1086
+ }
1087
+ export function warmCheckoutShortstatInBackground(cwd, context, onComplete) {
1088
+ const cacheKey = getShortstatCacheKey(cwd);
1089
+ if (shortstatCache.get(cacheKey) !== undefined || shortstatInFlight.has(cacheKey)) {
1090
+ return;
1091
+ }
1092
+ void getOrLoadCheckoutShortstat(cwd, context)
1093
+ .then(() => {
1094
+ onComplete?.();
1095
+ })
1096
+ .catch(() => {
1097
+ // Non-critical: keep listing path resilient even if git commands fail.
1098
+ });
1099
+ }
924
1100
  export async function getCheckoutDiff(cwd, compare, context) {
925
1101
  await requireGitRepo(cwd);
926
- let refForDiff;
1102
+ let refsForDiff;
927
1103
  if (compare.mode === "uncommitted") {
928
- refForDiff = "HEAD";
1104
+ refsForDiff = { baseRef: "HEAD", includeUntracked: true };
929
1105
  }
930
1106
  else {
931
1107
  const configured = await getConfiguredBaseRefForCwd(cwd, context);
@@ -936,12 +1112,15 @@ export async function getCheckoutDiff(cwd, compare, context) {
936
1112
  if (configured.isPaseoOwnedWorktree && compare.baseRef && compare.baseRef !== baseRef) {
937
1113
  throw new Error(`Base ref mismatch: expected ${baseRef}, got ${compare.baseRef}`);
938
1114
  }
939
- const normalizedBaseRef = normalizeLocalBranchRefName(baseRef);
940
- const bestBaseRef = await resolveBestComparisonBaseRef(cwd, normalizedBaseRef);
941
- refForDiff = (await tryResolveMergeBase(cwd, bestBaseRef)) ?? bestBaseRef;
1115
+ const bestBaseRef = await resolveBestComparisonBaseRef(cwd, baseRef);
1116
+ refsForDiff = {
1117
+ baseRef: (await tryResolveMergeBase(cwd, bestBaseRef)) ?? bestBaseRef,
1118
+ targetRef: "HEAD",
1119
+ includeUntracked: false,
1120
+ };
942
1121
  }
943
1122
  const ignoreWhitespace = compare.ignoreWhitespace === true;
944
- const changes = await listCheckoutFileChanges(cwd, refForDiff, ignoreWhitespace);
1123
+ const changes = await listCheckoutFileChanges(cwd, refsForDiff, ignoreWhitespace);
945
1124
  changes.sort((a, b) => {
946
1125
  if (a.path === b.path)
947
1126
  return 0;
@@ -971,7 +1150,7 @@ export async function getCheckoutDiff(cwd, compare, context) {
971
1150
  const untrackedChanges = changes.filter((change) => change.isUntracked === true);
972
1151
  const trackedChangeByPath = new Map(trackedChanges.map((change) => [change.path, change]));
973
1152
  const trackedNumstatByPath = trackedChanges.length > 0
974
- ? await getTrackedNumstatByPath(cwd, refForDiff, ignoreWhitespace)
1153
+ ? await getTrackedNumstatByPath(cwd, refsForDiff, ignoreWhitespace)
975
1154
  : new Map();
976
1155
  const trackedDiffPaths = [];
977
1156
  const trackedPlaceholderByPath = new Map();
@@ -992,7 +1171,7 @@ export async function getCheckoutDiff(cwd, compare, context) {
992
1171
  if (trackedDiffPaths.length > 0) {
993
1172
  const trackedDiffResult = await runGitCommand(buildGitDiffArgs({
994
1173
  ignoreWhitespace,
995
- extra: [refForDiff, "--", ...trackedDiffPaths],
1174
+ extra: [...getCheckoutDiffRefArgs(refsForDiff), "--", ...trackedDiffPaths],
996
1175
  }), {
997
1176
  cwd,
998
1177
  env: READ_ONLY_GIT_ENV,
@@ -1021,7 +1200,13 @@ export async function getCheckoutDiff(cwd, compare, context) {
1021
1200
  return null;
1022
1201
  }
1023
1202
  const refPath = change.oldPath ?? change.path;
1024
- return readGitFileContentAtRef(cwd, refForDiff, refPath);
1203
+ return readGitFileContentAtRef(cwd, refsForDiff.baseRef, refPath);
1204
+ },
1205
+ getNewFileContent: async (file) => {
1206
+ if (!refsForDiff.targetRef) {
1207
+ return null;
1208
+ }
1209
+ return readGitFileContentAtRef(cwd, refsForDiff.targetRef, file.path);
1025
1210
  },
1026
1211
  })
1027
1212
  : [];
@@ -1051,7 +1236,10 @@ export async function getCheckoutDiff(cwd, compare, context) {
1051
1236
  // `git diff -w --name-status` can still report a modified path even when the
1052
1237
  // whitespace-filtered patch and numstat are both empty. Skip emitting a
1053
1238
  // structured placeholder in that case so whitespace-only edits truly disappear.
1054
- if (ignoreWhitespace && !trackedDiffTruncated && stat === null) {
1239
+ if (ignoreWhitespace &&
1240
+ !trackedDiffTruncated &&
1241
+ change.status.startsWith("M") &&
1242
+ (!stat || (!stat.isBinary && stat.additions === 0 && stat.deletions === 0))) {
1055
1243
  continue;
1056
1244
  }
1057
1245
  structured.push({
@@ -1153,10 +1341,10 @@ export async function mergeToBase(cwd, options = {}, context) {
1153
1341
  }
1154
1342
  let normalizedBaseRef = baseRef;
1155
1343
  normalizedBaseRef = normalizeLocalBranchRefName(normalizedBaseRef);
1344
+ const currentWorktreeRoot = (await getWorktreeRoot(cwd)) ?? cwd;
1156
1345
  if (normalizedBaseRef === currentBranch) {
1157
- return;
1346
+ return currentWorktreeRoot;
1158
1347
  }
1159
- const currentWorktreeRoot = (await getWorktreeRoot(cwd)) ?? cwd;
1160
1348
  const baseWorktree = await getWorktreePathForBranch(cwd, normalizedBaseRef);
1161
1349
  const operationCwd = baseWorktree ?? currentWorktreeRoot;
1162
1350
  const isSameCheckout = resolve(operationCwd) === resolve(currentWorktreeRoot);
@@ -1246,6 +1434,7 @@ export async function mergeToBase(cwd, options = {}, context) {
1246
1434
  }
1247
1435
  }
1248
1436
  }
1437
+ return operationCwd;
1249
1438
  }
1250
1439
  export async function mergeFromBase(cwd, options = {}, context) {
1251
1440
  await requireGitRepo(cwd);
@@ -1272,7 +1461,7 @@ export async function mergeFromBase(cwd, options = {}, context) {
1272
1461
  }
1273
1462
  }
1274
1463
  const normalizedBaseRef = normalizeLocalBranchRefName(baseRef);
1275
- const bestBaseRef = await resolveBestComparisonBaseRef(cwd, normalizedBaseRef);
1464
+ const bestBaseRef = await resolveMostAheadBaseRef(cwd, normalizedBaseRef);
1276
1465
  if (bestBaseRef === currentBranch) {
1277
1466
  return;
1278
1467
  }
@@ -1331,7 +1520,7 @@ export async function mergeFromBase(cwd, options = {}, context) {
1331
1520
  throw error;
1332
1521
  }
1333
1522
  }
1334
- export async function pullCurrentBranch(cwd) {
1523
+ export async function pullCurrentBranch(cwd, github) {
1335
1524
  await requireGitRepo(cwd);
1336
1525
  const currentBranch = await getCurrentBranch(cwd);
1337
1526
  if (!currentBranch || currentBranch === "HEAD") {
@@ -1343,13 +1532,14 @@ export async function pullCurrentBranch(cwd) {
1343
1532
  }
1344
1533
  try {
1345
1534
  await runGitCommand(["pull"], { cwd, timeout: 120000 });
1535
+ github?.invalidate({ cwd });
1346
1536
  }
1347
1537
  catch (error) {
1348
1538
  await abortGitPullConflictState(cwd);
1349
1539
  throw error;
1350
1540
  }
1351
1541
  }
1352
- export async function pushCurrentBranch(cwd) {
1542
+ export async function pushCurrentBranch(cwd, github) {
1353
1543
  await requireGitRepo(cwd);
1354
1544
  const currentBranch = await getCurrentBranch(cwd);
1355
1545
  if (!currentBranch || currentBranch === "HEAD") {
@@ -1360,80 +1550,11 @@ export async function pushCurrentBranch(cwd) {
1360
1550
  throw new Error("Remote 'origin' is not configured.");
1361
1551
  }
1362
1552
  await runGitCommand(["push", "-u", "origin", currentBranch], { cwd, timeout: 120000 });
1553
+ github?.invalidate({ cwd });
1363
1554
  }
1364
- export async function resolveGhPath() {
1365
- if (cachedGhPath === undefined) {
1366
- cachedGhPath = await findExecutable("gh");
1367
- }
1368
- if (cachedGhPath === null) {
1369
- throw new Error("GitHub CLI (gh) is not installed or not in PATH");
1370
- }
1371
- return cachedGhPath;
1372
- }
1373
- function getCommandErrorText(error) {
1374
- if (!(error instanceof Error)) {
1375
- return String(error);
1376
- }
1377
- const stderr = typeof error?.stderr === "string" ? error.stderr : "";
1378
- const stdout = typeof error?.stdout === "string" ? error.stdout : "";
1379
- return `${error.message}\n${stderr}\n${stdout}`.toLowerCase();
1380
- }
1381
- function isGhAuthError(error) {
1382
- const text = getCommandErrorText(error);
1383
- return (text.includes("gh auth login") ||
1384
- text.includes("not logged into any github hosts") ||
1385
- text.includes("authentication failed") ||
1386
- text.includes("authentication required") ||
1387
- text.includes("bad credentials") ||
1388
- text.includes("http 401"));
1389
- }
1390
- async function resolveGitHubRepo(cwd) {
1391
- try {
1392
- const { stdout } = await runGitCommand(["config", "--get", "remote.origin.url"], {
1393
- cwd,
1394
- env: READ_ONLY_GIT_ENV,
1395
- });
1396
- const url = stdout.trim();
1397
- if (!url) {
1398
- return null;
1399
- }
1400
- let cleaned = url;
1401
- if (cleaned.startsWith("git@github.com:")) {
1402
- cleaned = cleaned.slice("git@github.com:".length);
1403
- }
1404
- else if (cleaned.startsWith("https://github.com/")) {
1405
- cleaned = cleaned.slice("https://github.com/".length);
1406
- }
1407
- else if (cleaned.startsWith("http://github.com/")) {
1408
- cleaned = cleaned.slice("http://github.com/".length);
1409
- }
1410
- else {
1411
- const marker = "github.com/";
1412
- const index = cleaned.indexOf(marker);
1413
- if (index !== -1) {
1414
- cleaned = cleaned.slice(index + marker.length);
1415
- }
1416
- else {
1417
- return null;
1418
- }
1419
- }
1420
- if (cleaned.endsWith(".git")) {
1421
- cleaned = cleaned.slice(0, -".git".length);
1422
- }
1423
- if (!cleaned.includes("/")) {
1424
- return null;
1425
- }
1426
- return cleaned;
1427
- }
1428
- catch {
1429
- // ignore
1430
- }
1431
- return null;
1432
- }
1433
- export async function createPullRequest(cwd, options) {
1555
+ export async function createPullRequest(cwd, options, github = createGitHubService(), workspaceGitService) {
1434
1556
  await requireGitRepo(cwd);
1435
- const ghPath = await resolveGhPath();
1436
- const repo = await resolveGitHubRepo(cwd);
1557
+ const repo = await resolveGitHubRepo(cwd, { workspaceGitService });
1437
1558
  if (!repo) {
1438
1559
  throw new Error("Unable to determine GitHub repo from git remote");
1439
1560
  }
@@ -1451,31 +1572,30 @@ export async function createPullRequest(cwd, options) {
1451
1572
  throw new Error(`Base ref mismatch: expected ${base}, got ${options.base}`);
1452
1573
  }
1453
1574
  await runGitCommand(["push", "-u", "origin", head], { cwd, timeout: 120000 });
1454
- const ghEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
1455
- const args = ["api", "-X", "POST", `repos/${repo}/pulls`, "-f", `title=${options.title}`];
1456
- args.push("-f", `head=${head}`);
1457
- args.push("-f", `base=${normalizedBase}`);
1458
- if (options.body) {
1459
- args.push("-f", `body=${options.body}`);
1460
- }
1461
- const { stdout } = await execCommand(ghPath, args, { cwd, env: ghEnv });
1462
- const parsed = JSON.parse(stdout.trim());
1463
- if (!parsed?.url || !parsed?.number) {
1464
- throw new Error("GitHub CLI did not return PR url/number");
1465
- }
1466
- return { url: parsed.url, number: parsed.number };
1575
+ const result = await github.createPullRequest({
1576
+ cwd,
1577
+ repo,
1578
+ title: options.title,
1579
+ body: options.body,
1580
+ head,
1581
+ base: normalizedBase,
1582
+ });
1583
+ github.invalidate({ cwd });
1584
+ return result;
1467
1585
  }
1468
- export async function getPullRequestStatus(cwd) {
1586
+ export async function getPullRequestStatus(cwd, github = createGitHubService(), options) {
1469
1587
  const cacheKey = getPullRequestStatusCacheKey(cwd);
1470
- const cached = pullRequestStatusCache.get(cacheKey);
1471
- if (cached) {
1472
- return cached;
1473
- }
1474
- const existing = pullRequestStatusInFlight.get(cacheKey);
1475
- if (existing) {
1476
- return existing;
1588
+ if (!options?.force) {
1589
+ const cached = pullRequestStatusCache.get(cacheKey);
1590
+ if (cached) {
1591
+ return cached;
1592
+ }
1593
+ const existing = pullRequestStatusInFlight.get(cacheKey);
1594
+ if (existing) {
1595
+ return existing;
1596
+ }
1477
1597
  }
1478
- const lookup = getPullRequestStatusUncached(cwd)
1598
+ const lookup = getPullRequestStatusUncached(cwd, github, options)
1479
1599
  .then((status) => {
1480
1600
  pullRequestStatusCache.set(cacheKey, status);
1481
1601
  return status;
@@ -1486,7 +1606,7 @@ export async function getPullRequestStatus(cwd) {
1486
1606
  pullRequestStatusInFlight.set(cacheKey, lookup);
1487
1607
  return lookup;
1488
1608
  }
1489
- async function getPullRequestStatusUncached(cwd) {
1609
+ async function getPullRequestStatusUncached(cwd, github, options) {
1490
1610
  await requireGitRepo(cwd);
1491
1611
  const head = await getCurrentBranch(cwd);
1492
1612
  if (!head) {
@@ -1495,49 +1615,21 @@ async function getPullRequestStatusUncached(cwd) {
1495
1615
  githubFeaturesEnabled: false,
1496
1616
  };
1497
1617
  }
1498
- let ghPath;
1499
- try {
1500
- ghPath = await resolveGhPath();
1501
- }
1502
- catch {
1503
- return {
1504
- status: null,
1505
- githubFeaturesEnabled: false,
1506
- };
1507
- }
1508
1618
  try {
1509
- const { stdout } = await execCommand(ghPath, ["pr", "view", "--json", "url,title,state,baseRefName,headRefName,mergedAt"], { cwd, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } });
1510
- const pr = JSON.parse(stdout.trim());
1511
- if (!pr || typeof pr !== "object" || !pr.url || !pr.title) {
1512
- return { status: null, githubFeaturesEnabled: true };
1513
- }
1514
- const mergedAt = typeof pr.mergedAt === "string" && pr.mergedAt.trim().length > 0 ? pr.mergedAt : null;
1515
- const state = mergedAt !== null
1516
- ? "merged"
1517
- : typeof pr.state === "string" && pr.state.trim().length > 0
1518
- ? pr.state.toLowerCase()
1519
- : "";
1619
+ const status = await github.getCurrentPullRequestStatus({
1620
+ cwd,
1621
+ headRef: head,
1622
+ reason: options?.reason,
1623
+ });
1520
1624
  return {
1521
- status: {
1522
- url: pr.url,
1523
- title: pr.title,
1524
- state,
1525
- baseRefName: pr.baseRefName ?? "",
1526
- headRefName: pr.headRefName ?? head,
1527
- isMerged: mergedAt !== null,
1528
- },
1625
+ status,
1529
1626
  githubFeaturesEnabled: true,
1530
1627
  };
1531
1628
  }
1532
1629
  catch (error) {
1533
- if (isGhAuthError(error)) {
1630
+ if (error instanceof GitHubCliMissingError || error instanceof GitHubAuthenticationError) {
1534
1631
  return { status: null, githubFeaturesEnabled: false };
1535
1632
  }
1536
- // gh pr view exits non-zero when no PR exists for the branch
1537
- const message = error instanceof Error ? error.message : String(error);
1538
- if (message.includes("no pull requests found") || message.includes("Could not resolve")) {
1539
- return { status: null, githubFeaturesEnabled: true };
1540
- }
1541
1633
  throw error;
1542
1634
  }
1543
1635
  }