@buildautomaton/cli 0.1.33 → 0.1.35

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.
package/dist/index.js CHANGED
@@ -8000,8 +8000,8 @@ function getElementAtPath(obj, path41) {
8000
8000
  }
8001
8001
  function promiseAllObject(promisesObj) {
8002
8002
  const keys = Object.keys(promisesObj);
8003
- const promises3 = keys.map((key) => promisesObj[key]);
8004
- return Promise.all(promises3).then((results) => {
8003
+ const promises5 = keys.map((key) => promisesObj[key]);
8004
+ return Promise.all(promises5).then((results) => {
8005
8005
  const resolvedObj = {};
8006
8006
  for (let i = 0; i < keys.length; i++) {
8007
8007
  resolvedObj[keys[i]] = results[i];
@@ -22158,20 +22158,22 @@ function createWsBridge(options) {
22158
22158
  onOpen?.();
22159
22159
  });
22160
22160
  ws.on("message", (raw) => {
22161
- try {
22162
- let data;
22163
- if (typeof raw === "string") {
22164
- data = JSON.parse(raw);
22165
- } else if (Buffer.isBuffer(raw) || raw instanceof ArrayBuffer) {
22166
- const str = Buffer.isBuffer(raw) ? raw.toString("utf8") : Buffer.from(raw).toString("utf8");
22167
- data = JSON.parse(str);
22168
- } else {
22169
- data = raw;
22161
+ setImmediate(() => {
22162
+ try {
22163
+ let data;
22164
+ if (typeof raw === "string") {
22165
+ data = JSON.parse(raw);
22166
+ } else if (Buffer.isBuffer(raw) || raw instanceof ArrayBuffer) {
22167
+ const str = Buffer.isBuffer(raw) ? raw.toString("utf8") : Buffer.from(raw).toString("utf8");
22168
+ data = JSON.parse(str);
22169
+ } else {
22170
+ data = raw;
22171
+ }
22172
+ onMessage?.(data);
22173
+ } catch {
22174
+ onMessage?.(raw);
22170
22175
  }
22171
- onMessage?.(data);
22172
- } catch {
22173
- onMessage?.(raw);
22174
- }
22176
+ });
22175
22177
  });
22176
22178
  ws.on("close", (code, reason) => {
22177
22179
  disposeClientPing();
@@ -23962,7 +23964,7 @@ function installBridgeProcessResilience() {
23962
23964
  }
23963
23965
 
23964
23966
  // src/cli-version.ts
23965
- var CLI_VERSION = "0.1.33".length > 0 ? "0.1.33" : "0.0.0-dev";
23967
+ var CLI_VERSION = "0.1.35".length > 0 ? "0.1.35" : "0.0.0-dev";
23966
23968
 
23967
23969
  // src/connection/heartbeat/constants.ts
23968
23970
  var BRIDGE_APP_HEARTBEAT_INTERVAL_MS = 1e4;
@@ -25550,17 +25552,13 @@ function resolveSessionParentPathForAgentProcess(resolvedSessionParentPath) {
25550
25552
  return getBridgeRoot();
25551
25553
  }
25552
25554
 
25553
- // src/git/session-git-queue.ts
25554
- import { execFile as execFile7 } from "node:child_process";
25555
+ // src/git/snapshot/session-git-queue.ts
25555
25556
  import { readFile, stat } from "node:fs/promises";
25556
- import { promisify as promisify7 } from "node:util";
25557
25557
  import * as path15 from "node:path";
25558
25558
 
25559
- // src/git/pre-turn-snapshot.ts
25559
+ // src/git/snapshot/pre-turn-snapshot.ts
25560
25560
  import * as fs14 from "node:fs";
25561
25561
  import * as path14 from "node:path";
25562
- import { execFile as execFile6 } from "node:child_process";
25563
- import { promisify as promisify6 } from "node:util";
25564
25562
 
25565
25563
  // src/git/discover-repos.ts
25566
25564
  import * as fs13 from "node:fs";
@@ -30125,12 +30123,68 @@ function gitInstanceFactory(baseDir, options) {
30125
30123
  init_git_response_error();
30126
30124
  var simpleGit = gitInstanceFactory;
30127
30125
 
30126
+ // src/git/git-runtime.ts
30127
+ var activeGitChildProcesses = /* @__PURE__ */ new Set();
30128
+ function abortActiveGitChildProcesses() {
30129
+ for (const child of activeGitChildProcesses) {
30130
+ try {
30131
+ child.kill("SIGTERM");
30132
+ } catch {
30133
+ }
30134
+ }
30135
+ }
30136
+ var GitOperationAbortedError = class extends Error {
30137
+ constructor(message = "Git operation aborted") {
30138
+ super(message);
30139
+ this.name = "GitOperationAbortedError";
30140
+ }
30141
+ };
30142
+ function throwIfGitShutdownRequested() {
30143
+ if (isCliImmediateShutdownRequested()) {
30144
+ abortActiveGitChildProcesses();
30145
+ throw new GitOperationAbortedError();
30146
+ }
30147
+ }
30148
+ async function runGitTask(fn) {
30149
+ throwIfGitShutdownRequested();
30150
+ try {
30151
+ const result = await fn();
30152
+ throwIfGitShutdownRequested();
30153
+ return result;
30154
+ } catch (e) {
30155
+ if (isCliImmediateShutdownRequested()) {
30156
+ throw new GitOperationAbortedError();
30157
+ }
30158
+ throw e;
30159
+ }
30160
+ }
30161
+ async function yieldToEventLoop2() {
30162
+ throwIfGitShutdownRequested();
30163
+ await new Promise((resolve18) => {
30164
+ setImmediate(resolve18);
30165
+ });
30166
+ throwIfGitShutdownRequested();
30167
+ }
30168
+ async function forEachWithGitYield(items, fn) {
30169
+ for (let i = 0; i < items.length; i++) {
30170
+ if (i > 0 || items.length > 1) await yieldToEventLoop2();
30171
+ await fn(items[i], i);
30172
+ }
30173
+ }
30174
+
30128
30175
  // src/git/cli-simple-git.ts
30129
30176
  function cliSimpleGit(baseDir) {
30130
- const git = simpleGit({ baseDir });
30177
+ const git = simpleGit({
30178
+ baseDir,
30179
+ trimmed: true,
30180
+ spawnOptions: {}
30181
+ });
30131
30182
  git.outputHandler((command, stdout, stderr) => {
30132
30183
  const trace = isCliTrace();
30133
30184
  const onChunk = (label) => (chunk) => {
30185
+ if (isCliImmediateShutdownRequested()) {
30186
+ abortActiveGitChildProcesses();
30187
+ }
30134
30188
  const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
30135
30189
  const line = text.replace(/\s+$/, "");
30136
30190
  if (trace && line) {
@@ -30219,17 +30273,37 @@ async function discoverGitReposUnderRoot(rootPath) {
30219
30273
  return out;
30220
30274
  }
30221
30275
 
30222
- // src/git/pre-turn-snapshot.ts
30276
+ // src/git/git-exec.ts
30277
+ import { execFile as execFile6, spawn as spawn4 } from "node:child_process";
30278
+ import { promisify as promisify6 } from "node:util";
30223
30279
  var execFileAsync5 = promisify6(execFile6);
30280
+ async function execGitFile(args, options) {
30281
+ throwIfGitShutdownRequested();
30282
+ try {
30283
+ const result = await execFileAsync5("git", args, {
30284
+ maxBuffer: 10 * 1024 * 1024,
30285
+ ...options
30286
+ });
30287
+ throwIfGitShutdownRequested();
30288
+ return {
30289
+ stdout: String(result.stdout ?? ""),
30290
+ stderr: String(result.stderr ?? "")
30291
+ };
30292
+ } catch (e) {
30293
+ if (isCliImmediateShutdownRequested()) {
30294
+ throw new GitOperationAbortedError();
30295
+ }
30296
+ throw e;
30297
+ }
30298
+ }
30299
+
30300
+ // src/git/snapshot/pre-turn-snapshot.ts
30224
30301
  function snapshotsDirForCwd(agentCwd) {
30225
30302
  return path14.join(agentCwd, ".buildautomaton", "snapshots");
30226
30303
  }
30227
30304
  async function gitStashCreate(repoRoot, log2) {
30228
30305
  try {
30229
- const { stdout } = await execFileAsync5("git", ["stash", "create"], {
30230
- cwd: repoRoot,
30231
- maxBuffer: 10 * 1024 * 1024
30232
- });
30306
+ const { stdout } = await execGitFile(["stash", "create"], { cwd: repoRoot });
30233
30307
  return stdout.trim();
30234
30308
  } catch (e) {
30235
30309
  log2(
@@ -30240,7 +30314,7 @@ async function gitStashCreate(repoRoot, log2) {
30240
30314
  }
30241
30315
  async function gitRun(repoRoot, args, log2, label) {
30242
30316
  try {
30243
- await execFileAsync5("git", args, { cwd: repoRoot, maxBuffer: 10 * 1024 * 1024 });
30317
+ await execGitFile(args, { cwd: repoRoot });
30244
30318
  return { ok: true };
30245
30319
  } catch (e) {
30246
30320
  const msg = e instanceof Error ? e.message : String(e);
@@ -30274,10 +30348,10 @@ async function capturePreTurnSnapshot(options) {
30274
30348
  return { ok: false, error: "No git repos to snapshot" };
30275
30349
  }
30276
30350
  const repos = [];
30277
- for (const root of repoRoots) {
30351
+ await forEachWithGitYield(repoRoots, async (root) => {
30278
30352
  const stashSha = await gitStashCreate(root, log2);
30279
30353
  repos.push({ path: root, stashSha });
30280
- }
30354
+ });
30281
30355
  const dir = snapshotsDirForCwd(agentCwd);
30282
30356
  try {
30283
30357
  fs14.mkdirSync(dir, { recursive: true });
@@ -30312,17 +30386,25 @@ async function applyPreTurnSnapshot(filePath, log2) {
30312
30386
  if (!Array.isArray(data.repos)) {
30313
30387
  return { ok: false, error: "Invalid snapshot file" };
30314
30388
  }
30315
- for (const r of data.repos) {
30316
- if (!r.path) continue;
30389
+ let applyError = null;
30390
+ await forEachWithGitYield(data.repos, async (r) => {
30391
+ if (applyError || !r.path) return;
30317
30392
  const reset = await gitRun(r.path, ["reset", "--hard", "HEAD"], log2, "reset --hard");
30318
- if (!reset.ok) return reset;
30393
+ if (!reset.ok) {
30394
+ applyError = reset;
30395
+ return;
30396
+ }
30319
30397
  const clean = await gitRun(r.path, ["clean", "-fd"], log2, "clean -fd");
30320
- if (!clean.ok) return clean;
30398
+ if (!clean.ok) {
30399
+ applyError = clean;
30400
+ return;
30401
+ }
30321
30402
  if (r.stashSha) {
30322
30403
  const ap = await gitRun(r.path, ["stash", "apply", r.stashSha], log2, "stash apply");
30323
- if (!ap.ok) return ap;
30404
+ if (!ap.ok) applyError = ap;
30324
30405
  }
30325
- }
30406
+ });
30407
+ if (applyError?.ok === false) return applyError;
30326
30408
  log2(`[snapshot] Restored pre-turn state for ${data.runId.slice(0, 8)}\u2026`);
30327
30409
  return { ok: true };
30328
30410
  }
@@ -30330,8 +30412,7 @@ function snapshotFilePath(agentCwd, runId) {
30330
30412
  return path14.join(snapshotsDirForCwd(agentCwd), `${runId}.json`);
30331
30413
  }
30332
30414
 
30333
- // src/git/session-git-queue.ts
30334
- var execFileAsync6 = promisify7(execFile7);
30415
+ // src/git/snapshot/session-git-queue.ts
30335
30416
  var MAX_FULL_FILE_TEXT_BYTES = 512 * 1024;
30336
30417
  async function readWorkspaceFileAsUtf8(absPath) {
30337
30418
  try {
@@ -30360,35 +30441,28 @@ async function collectTurnGitDiffFromPreTurnSnapshot(options) {
30360
30441
  return;
30361
30442
  }
30362
30443
  const multiRepo = data.repos.length > 1;
30363
- for (const repo of data.repos) {
30364
- if (!repo.stashSha) continue;
30444
+ await forEachWithGitYield(data.repos, async (repo) => {
30445
+ if (!repo.stashSha) return;
30365
30446
  let namesRaw;
30366
30447
  try {
30367
- const { stdout } = await execFileAsync6("git", ["diff", "--name-only", repo.stashSha], {
30368
- cwd: repo.path,
30369
- maxBuffer: 10 * 1024 * 1024
30370
- });
30448
+ const { stdout } = await execGitFile(["diff", "--name-only", repo.stashSha], { cwd: repo.path });
30371
30449
  namesRaw = stdout;
30372
30450
  } catch (e) {
30373
30451
  log2(
30374
30452
  `[session-git-queue] Git diff --name-only failed in ${repo.path}: ${e instanceof Error ? e.message : String(e)}`
30375
30453
  );
30376
- continue;
30454
+ return;
30377
30455
  }
30378
30456
  const lines = namesRaw.split("\n").map((l) => l.trim()).filter(Boolean);
30379
30457
  const slug = path15.basename(repo.path).replace(/[^\w.-]+/g, "_") || "repo";
30380
- for (const rel of lines) {
30381
- if (rel.includes("..")) continue;
30458
+ await forEachWithGitYield(lines, async (rel) => {
30459
+ if (rel.includes("..")) return;
30382
30460
  try {
30383
- const { stdout: patchContent } = await execFileAsync6(
30384
- "git",
30461
+ const { stdout: patchContent } = await execGitFile(
30385
30462
  ["diff", "--no-color", repo.stashSha, "--", rel],
30386
- {
30387
- cwd: repo.path,
30388
- maxBuffer: 50 * 1024 * 1024
30389
- }
30463
+ { cwd: repo.path }
30390
30464
  );
30391
- if (!patchContent.trim()) continue;
30465
+ if (!patchContent.trim()) return;
30392
30466
  const displayPath = multiRepo ? `${slug}/${rel}` : rel;
30393
30467
  const workspaceFilePath = path15.join(repo.path, rel);
30394
30468
  const newText = await readWorkspaceFileAsUtf8(workspaceFilePath);
@@ -30405,8 +30479,8 @@ async function collectTurnGitDiffFromPreTurnSnapshot(options) {
30405
30479
  `[session-git-queue] Git diff failed for ${rel}: ${e instanceof Error ? e.message : String(e)}`
30406
30480
  );
30407
30481
  }
30408
- }
30409
- }
30482
+ });
30483
+ });
30410
30484
  }
30411
30485
 
30412
30486
  // src/agents/acp/put-summarize-change-summaries.ts
@@ -30774,9 +30848,9 @@ __export(claude_code_acp_client_exports, {
30774
30848
  });
30775
30849
 
30776
30850
  // src/agents/acp/clients/detect-command-on-path.ts
30777
- import { execFile as execFile8 } from "node:child_process";
30778
- import { promisify as promisify8 } from "node:util";
30779
- var execFileAsync7 = promisify8(execFile8);
30851
+ import { execFile as execFile7 } from "node:child_process";
30852
+ import { promisify as promisify7 } from "node:util";
30853
+ var execFileAsync6 = promisify7(execFile7);
30780
30854
  var COMMAND_ON_PATH_PROBE_TIMEOUT_MS = 750;
30781
30855
  async function execFileShutdownAware(file2, args, timeoutMs) {
30782
30856
  if (isCliImmediateShutdownRequested()) throw new Error("shutdown");
@@ -30786,7 +30860,7 @@ async function execFileShutdownAware(file2, args, timeoutMs) {
30786
30860
  }, 50);
30787
30861
  shutdownPoll.unref?.();
30788
30862
  try {
30789
- await execFileAsync7(file2, args, { timeout: timeoutMs, signal: ac.signal });
30863
+ await execFileAsync6(file2, args, { timeout: timeoutMs, signal: ac.signal });
30790
30864
  } finally {
30791
30865
  clearInterval(shutdownPoll);
30792
30866
  }
@@ -30866,7 +30940,7 @@ __export(cursor_acp_client_exports, {
30866
30940
  createCursorAcpClient: () => createCursorAcpClient,
30867
30941
  detectLocalAgentPresence: () => detectLocalAgentPresence3
30868
30942
  });
30869
- import { spawn as spawn4 } from "node:child_process";
30943
+ import { spawn as spawn5 } from "node:child_process";
30870
30944
  import * as readline from "node:readline";
30871
30945
 
30872
30946
  // src/agents/acp/format-session-update-kind-for-log.ts
@@ -30936,7 +31010,7 @@ async function createCursorAcpClient(options) {
30936
31010
  } = options;
30937
31011
  const dbgFs = process.env.BUILDAUTOMATON_DEBUG_ACP_FS === "1";
30938
31012
  const isWindows = process.platform === "win32";
30939
- const child = spawn4(command[0], command.slice(1), {
31013
+ const child = spawn5(command[0], command.slice(1), {
30940
31014
  cwd,
30941
31015
  stdio: ["pipe", "pipe", "pipe"],
30942
31016
  env: process.env,
@@ -32114,7 +32188,7 @@ async function ensureAcpClient(options) {
32114
32188
  if (!state.acpStartPromise) {
32115
32189
  let statOk = false;
32116
32190
  try {
32117
- const st = fs15.statSync(targetSessionParentPath);
32191
+ const st = await fs15.promises.stat(targetSessionParentPath);
32118
32192
  statOk = st.isDirectory();
32119
32193
  if (!statOk) {
32120
32194
  state.lastAcpStartError = `Agent cwd is not a directory: ${targetSessionParentPath}`;
@@ -32382,7 +32456,7 @@ import os8 from "node:os";
32382
32456
  import * as fs17 from "node:fs";
32383
32457
  import * as path20 from "node:path";
32384
32458
 
32385
- // src/git/worktree-add.ts
32459
+ // src/git/worktrees/worktree-add.ts
32386
32460
  async function gitWorktreeAddBranch(mainRepoPath, worktreePath, branch) {
32387
32461
  const mainGit = cliSimpleGit(mainRepoPath);
32388
32462
  await mainGit.raw(["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
@@ -32487,7 +32561,7 @@ async function prepareNewSessionWorktrees(options) {
32487
32561
  };
32488
32562
  }
32489
32563
 
32490
- // src/git/rename-branch.ts
32564
+ // src/git/branches/rename-branch.ts
32491
32565
  async function gitRenameCurrentBranch(repoDir, newName) {
32492
32566
  const g = cliSimpleGit(repoDir);
32493
32567
  await g.raw(["branch", "-m", newName]);
@@ -32511,10 +32585,10 @@ async function renameSessionWorktreeBranches(paths, newBranch, log2) {
32511
32585
  // src/worktrees/remove-session-worktrees.ts
32512
32586
  import * as fs20 from "node:fs";
32513
32587
 
32514
- // src/git/worktree-remove.ts
32588
+ // src/git/worktrees/worktree-remove.ts
32515
32589
  import * as fs19 from "node:fs";
32516
32590
 
32517
- // src/git/resolve-main-repo-from-git-file.ts
32591
+ // src/git/worktrees/resolve-main-repo-from-git-file.ts
32518
32592
  import * as fs18 from "node:fs";
32519
32593
  import * as path21 from "node:path";
32520
32594
  function resolveMainRepoFromWorktreeGitFile(wt) {
@@ -32528,7 +32602,7 @@ function resolveMainRepoFromWorktreeGitFile(wt) {
32528
32602
  return path21.dirname(gitDir);
32529
32603
  }
32530
32604
 
32531
- // src/git/worktree-remove.ts
32605
+ // src/git/worktrees/worktree-remove.ts
32532
32606
  async function gitWorktreeRemoveForce(worktreePath) {
32533
32607
  const mainRepo = resolveMainRepoFromWorktreeGitFile(worktreePath);
32534
32608
  if (mainRepo) {
@@ -32554,7 +32628,88 @@ async function removeSessionWorktrees(paths, log2) {
32554
32628
  }
32555
32629
  }
32556
32630
 
32557
- // src/git/working-directory/status/working-tree-status.ts
32631
+ // src/git/changes/lib/parse-git-status.ts
32632
+ function parseNameStatusLines(lines) {
32633
+ const m = /* @__PURE__ */ new Map();
32634
+ for (const line of lines) {
32635
+ if (!line.trim()) continue;
32636
+ const tabParts = line.split(" ");
32637
+ if (tabParts.length < 2) continue;
32638
+ const status = tabParts[0].trim();
32639
+ const code = status[0];
32640
+ if (code === "A") {
32641
+ m.set(tabParts[tabParts.length - 1], "added");
32642
+ } else if (code === "D") {
32643
+ m.set(tabParts[tabParts.length - 1], "removed");
32644
+ } else if (code === "R" || code === "C") {
32645
+ if (tabParts.length >= 3) m.set(tabParts[tabParts.length - 1], "modified");
32646
+ } else if (code === "M" || code === "U" || code === "T") {
32647
+ m.set(tabParts[tabParts.length - 1], "modified");
32648
+ }
32649
+ }
32650
+ return m;
32651
+ }
32652
+ function parseNumstatFirstLine(line) {
32653
+ const parts = line.split(" ");
32654
+ if (parts.length < 3) return null;
32655
+ const [a, d] = parts;
32656
+ const additions = a === "-" ? 0 : parseInt(String(a), 10) || 0;
32657
+ const deletions = d === "-" ? 0 : parseInt(String(d), 10) || 0;
32658
+ return { additions, deletions };
32659
+ }
32660
+ function parseNumstat(lines) {
32661
+ const m = /* @__PURE__ */ new Map();
32662
+ for (const line of lines) {
32663
+ if (!line.trim()) continue;
32664
+ const parts = line.split(" ");
32665
+ if (parts.length < 3) continue;
32666
+ const [a, d, p] = parts;
32667
+ const additions = a === "-" ? 0 : parseInt(String(a), 10) || 0;
32668
+ const deletions = d === "-" ? 0 : parseInt(String(d), 10) || 0;
32669
+ m.set(p, { additions, deletions });
32670
+ }
32671
+ return m;
32672
+ }
32673
+ async function numstatFromGitNoIndex(g, pathInRepo) {
32674
+ const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
32675
+ try {
32676
+ const out = await g.raw(["diff", "--numstat", "--no-index", "--", devNull, pathInRepo]);
32677
+ const first2 = String(out).split("\n").find((l) => l.trim()) ?? "";
32678
+ return parseNumstatFirstLine(first2);
32679
+ } catch {
32680
+ return null;
32681
+ }
32682
+ }
32683
+
32684
+ // src/git/changes/lib/working-tree-changed-path-count.ts
32685
+ function workingTreeChangedPathCount(nameStatusLines, numstatLines, untrackedLines) {
32686
+ const kindByPath = parseNameStatusLines(nameStatusLines);
32687
+ const numByPath = parseNumstat(numstatLines);
32688
+ const paths = /* @__PURE__ */ new Set([...kindByPath.keys(), ...numByPath.keys()]);
32689
+ for (const p of untrackedLines.map((s) => s.trim()).filter(Boolean)) {
32690
+ paths.add(p);
32691
+ }
32692
+ return paths.size;
32693
+ }
32694
+
32695
+ // src/git/changes/count-working-tree-changed-files.ts
32696
+ async function countWorkingTreeChangedFilesForRepo(repoGitCwd) {
32697
+ return runGitTask(async () => {
32698
+ const g = cliSimpleGit(repoGitCwd);
32699
+ const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
32700
+ g.raw(["diff", "--name-status", "HEAD"]).catch(() => ""),
32701
+ g.raw(["diff", "HEAD", "--numstat"]).catch(() => ""),
32702
+ g.raw(["ls-files", "--others", "--exclude-standard"]).catch(() => "")
32703
+ ]);
32704
+ return workingTreeChangedPathCount(
32705
+ String(nameStatusRaw).split("\n"),
32706
+ String(numstatRaw).split("\n"),
32707
+ String(untrackedRaw).split("\n")
32708
+ );
32709
+ });
32710
+ }
32711
+
32712
+ // src/git/commits/resolve-remote-tracking.ts
32558
32713
  async function tryConfigGet(g, key) {
32559
32714
  try {
32560
32715
  const out = await g.raw(["config", "--get", key]);
@@ -32637,30 +32792,6 @@ async function resolveBaseShaForUnpushedCommits(g) {
32637
32792
  if (!defaultRef) return null;
32638
32793
  return revParseSafe(g, defaultRef);
32639
32794
  }
32640
- function parseLogShaDateSubjectLines(raw) {
32641
- const out = [];
32642
- for (const line of String(raw).split("\n")) {
32643
- const l = line.trimEnd();
32644
- if (!l.trim()) continue;
32645
- const parts = l.split(" ");
32646
- if (parts.length < 3) continue;
32647
- const sha = parts[0].trim();
32648
- const committedAt = parts[1].trim();
32649
- const subject = parts.slice(2).join(" ").trim();
32650
- if (!/^[0-9a-f]{7,40}$/i.test(sha)) continue;
32651
- out.push({ sha, shortSha: sha.slice(0, 7), subject, committedAt });
32652
- }
32653
- return out;
32654
- }
32655
- async function gitLogNotReachableFromBase(g, baseSha, headSha) {
32656
- if (baseSha === headSha) return [];
32657
- try {
32658
- const logOut = await g.raw(["log", "--format=%H %cI %s", `${baseSha}..${headSha}`]);
32659
- return parseLogShaDateSubjectLines(logOut);
32660
- } catch {
32661
- return [];
32662
- }
32663
- }
32664
32795
  async function commitsAheadOfRemoteTracking(repoDir) {
32665
32796
  const g = cliSimpleGit(repoDir);
32666
32797
  const headSha = await revParseSafe(g, "HEAD");
@@ -32675,44 +32806,41 @@ async function commitsAheadOfRemoteTracking(repoDir) {
32675
32806
  return 0;
32676
32807
  }
32677
32808
  }
32809
+
32810
+ // src/git/status/working-tree-status.ts
32678
32811
  async function getRepoWorkingTreeStatus(repoDir) {
32679
- const g = cliSimpleGit(repoDir);
32680
- const st = await g.status();
32681
- const hasUncommittedChanges = (st.files?.length ?? 0) > 0;
32682
- const ahead = await commitsAheadOfRemoteTracking(repoDir);
32683
- return { hasUncommittedChanges, hasUnpushedCommits: ahead > 0 };
32684
- }
32685
- async function listUnpushedCommits(repoDir) {
32686
- const g = cliSimpleGit(repoDir);
32687
- const headSha = await revParseSafe(g, "HEAD");
32688
- if (!headSha) return [];
32689
- const baseSha = await resolveBaseShaForUnpushedCommits(g);
32690
- if (!baseSha) return [];
32691
- return gitLogNotReachableFromBase(g, baseSha, headSha);
32812
+ return runGitTask(async () => {
32813
+ const uncommittedFileCount = await countWorkingTreeChangedFilesForRepo(repoDir);
32814
+ const hasUncommittedChanges = uncommittedFileCount > 0;
32815
+ const ahead = await commitsAheadOfRemoteTracking(repoDir);
32816
+ return { hasUncommittedChanges, hasUnpushedCommits: ahead > 0, uncommittedFileCount };
32817
+ });
32692
32818
  }
32693
32819
  async function aggregateSessionPathsWorkingTreeStatus(paths) {
32694
32820
  let hasUncommittedChanges = false;
32695
32821
  let hasUnpushedCommits = false;
32696
- for (const p of paths) {
32822
+ let uncommittedFileCount = 0;
32823
+ await forEachWithGitYield(paths, async (p) => {
32697
32824
  const s = await getRepoWorkingTreeStatus(p);
32825
+ uncommittedFileCount += s.uncommittedFileCount;
32698
32826
  if (s.hasUncommittedChanges) hasUncommittedChanges = true;
32699
32827
  if (s.hasUnpushedCommits) hasUnpushedCommits = true;
32700
- }
32701
- return { hasUncommittedChanges, hasUnpushedCommits };
32828
+ });
32829
+ return { hasUncommittedChanges, hasUnpushedCommits, uncommittedFileCount };
32702
32830
  }
32703
32831
  async function pushAheadOfUpstreamForPaths(paths) {
32704
- for (const p of paths) {
32832
+ await forEachWithGitYield(paths, async (p) => {
32705
32833
  const g = cliSimpleGit(p);
32706
32834
  const ahead = await commitsAheadOfRemoteTracking(p);
32707
- if (ahead <= 0) continue;
32835
+ if (ahead <= 0) return;
32708
32836
  await g.push();
32709
- }
32837
+ });
32710
32838
  }
32711
32839
 
32712
- // src/git/working-directory/changes/types.ts
32840
+ // src/git/changes/types.ts
32713
32841
  var MAX_PATCH_CHARS = 35e4;
32714
32842
 
32715
- // src/git/working-directory/changes/repo-format.ts
32843
+ // src/git/changes/lib/repo-format.ts
32716
32844
  function posixJoinDirFile(dir, file2) {
32717
32845
  const d = dir === "." || dir === "" ? "" : dir.replace(/\\/g, "/").replace(/\/+$/, "");
32718
32846
  const f = file2.replace(/\\/g, "/").replace(/^\/+/, "");
@@ -32766,63 +32894,80 @@ function formatRemoteDisplayLabel(remoteUrl) {
32766
32894
  return `origin \xB7 ${hostPath}`;
32767
32895
  }
32768
32896
 
32769
- // src/git/working-directory/changes/get-working-tree-change-repo-details.ts
32897
+ // src/git/changes/get-working-tree-change-repo-details.ts
32770
32898
  import * as path23 from "node:path";
32771
32899
 
32772
- // src/git/working-directory/changes/parse-git-status.ts
32773
- function parseNameStatusLines(lines) {
32774
- const m = /* @__PURE__ */ new Map();
32775
- for (const line of lines) {
32776
- if (!line.trim()) continue;
32777
- const tabParts = line.split(" ");
32778
- if (tabParts.length < 2) continue;
32779
- const status = tabParts[0].trim();
32780
- const code = status[0];
32781
- if (code === "A") {
32782
- m.set(tabParts[tabParts.length - 1], "added");
32783
- } else if (code === "D") {
32784
- m.set(tabParts[tabParts.length - 1], "removed");
32785
- } else if (code === "R" || code === "C") {
32786
- if (tabParts.length >= 3) m.set(tabParts[tabParts.length - 1], "modified");
32787
- } else if (code === "M" || code === "U" || code === "T") {
32788
- m.set(tabParts[tabParts.length - 1], "modified");
32789
- }
32900
+ // src/git/commits/lib/parse-log-lines.ts
32901
+ function parseLogShaDateSubjectLines(raw) {
32902
+ const out = [];
32903
+ for (const line of String(raw).split("\n")) {
32904
+ const l = line.trimEnd();
32905
+ if (!l.trim()) continue;
32906
+ const parts = l.split(" ");
32907
+ if (parts.length < 3) continue;
32908
+ const sha = parts[0].trim();
32909
+ const committedAt = parts[1].trim();
32910
+ const subject = parts.slice(2).join(" ").trim();
32911
+ if (!/^[0-9a-f]{7,40}$/i.test(sha)) continue;
32912
+ out.push({ sha, shortSha: sha.slice(0, 7), subject, committedAt });
32790
32913
  }
32791
- return m;
32792
- }
32793
- function parseNumstatFirstLine(line) {
32794
- const parts = line.split(" ");
32795
- if (parts.length < 3) return null;
32796
- const [a, d] = parts;
32797
- const additions = a === "-" ? 0 : parseInt(String(a), 10) || 0;
32798
- const deletions = d === "-" ? 0 : parseInt(String(d), 10) || 0;
32799
- return { additions, deletions };
32914
+ return out;
32800
32915
  }
32801
- function parseNumstat(lines) {
32802
- const m = /* @__PURE__ */ new Map();
32803
- for (const line of lines) {
32804
- if (!line.trim()) continue;
32805
- const parts = line.split(" ");
32806
- if (parts.length < 3) continue;
32807
- const [a, d, p] = parts;
32808
- const additions = a === "-" ? 0 : parseInt(String(a), 10) || 0;
32809
- const deletions = d === "-" ? 0 : parseInt(String(d), 10) || 0;
32810
- m.set(p, { additions, deletions });
32916
+
32917
+ // src/git/commits/list-unpushed-commits.ts
32918
+ async function gitLogNotReachableFromBase(g, baseSha, headSha) {
32919
+ if (baseSha === headSha) return [];
32920
+ try {
32921
+ const logOut = await g.raw(["log", "--format=%H %cI %s", `${baseSha}..${headSha}`]);
32922
+ return parseLogShaDateSubjectLines(logOut);
32923
+ } catch {
32924
+ return [];
32811
32925
  }
32812
- return m;
32813
32926
  }
32814
- async function numstatFromGitNoIndex(g, pathInRepo) {
32815
- const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
32927
+ async function listUnpushedCommits(repoDir) {
32928
+ const g = cliSimpleGit(repoDir);
32929
+ const headSha = await revParseSafe(g, "HEAD");
32930
+ if (!headSha) return [];
32931
+ const baseSha = await resolveBaseShaForUnpushedCommits(g);
32932
+ if (!baseSha) return [];
32933
+ return gitLogNotReachableFromBase(g, baseSha, headSha);
32934
+ }
32935
+
32936
+ // src/git/commits/lib/sanitize-recent-commits-limit.ts
32937
+ var RECENT_COMMITS_LIMIT_MIN = 1;
32938
+ var RECENT_COMMITS_LIMIT_MAX = 50;
32939
+ var RECENT_COMMITS_LIMIT_DEFAULT = 2;
32940
+ function sanitizeRecentCommitsLimit(value) {
32941
+ const n = typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value.trim(), 10) : Number.NaN;
32942
+ if (!Number.isFinite(n)) return RECENT_COMMITS_LIMIT_DEFAULT;
32943
+ return Math.min(RECENT_COMMITS_LIMIT_MAX, Math.max(RECENT_COMMITS_LIMIT_MIN, Math.trunc(n)));
32944
+ }
32945
+
32946
+ // src/git/commits/list-recent-commits.ts
32947
+ async function listRecentCommits(repoDir, limitInput) {
32948
+ const limit = sanitizeRecentCommitsLimit(limitInput);
32949
+ const g = cliSimpleGit(repoDir);
32950
+ const headSha = await revParseSafe(g, "HEAD");
32951
+ if (!headSha) return { commits: [], hasMore: false };
32952
+ const unpushedSet = new Set((await listUnpushedCommits(repoDir)).map((c) => c.sha));
32816
32953
  try {
32817
- const out = await g.raw(["diff", "--numstat", "--no-index", "--", devNull, pathInRepo]);
32818
- const first2 = String(out).split("\n").find((l) => l.trim()) ?? "";
32819
- return parseNumstatFirstLine(first2);
32954
+ const logOut = await g.raw(["log", "--format=%H %cI %s", `-n`, String(limit), "HEAD"]);
32955
+ const commits = parseLogShaDateSubjectLines(logOut).map((c) => ({
32956
+ ...c,
32957
+ needsPush: unpushedSet.has(c.sha)
32958
+ }));
32959
+ let hasMore = false;
32960
+ if (commits.length === limit) {
32961
+ const nextOut = await g.raw(["log", "-n", "1", `--skip=${limit}`, "--format=%H", "HEAD"]);
32962
+ hasMore = /^[0-9a-f]{7,40}$/i.test(String(nextOut).trim());
32963
+ }
32964
+ return { commits, hasMore };
32820
32965
  } catch {
32821
- return null;
32966
+ return { commits: [], hasMore: false };
32822
32967
  }
32823
32968
  }
32824
32969
 
32825
- // src/git/working-directory/changes/patch-truncate.ts
32970
+ // src/git/changes/lib/patch-truncate.ts
32826
32971
  function truncatePatch(s) {
32827
32972
  if (s.length <= MAX_PATCH_CHARS) return s;
32828
32973
  return `${s.slice(0, MAX_PATCH_CHARS)}
@@ -32830,7 +32975,7 @@ function truncatePatch(s) {
32830
32975
  \u2026 (diff truncated)`;
32831
32976
  }
32832
32977
 
32833
- // src/git/working-directory/changes/list-changed-files-for-commit.ts
32978
+ // src/git/commits/list-changed-files-for-commit.ts
32834
32979
  var EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
32835
32980
  async function parentForCommitDiff(g, sha) {
32836
32981
  try {
@@ -32856,7 +33001,7 @@ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
32856
33001
  const paths = new Set([...kindByPath.keys(), ...numByPath.keys()].filter(Boolean));
32857
33002
  const rows = [];
32858
33003
  const normRel = repoRelPath === "." || repoRelPath === "" ? "." : repoRelPath;
32859
- for (const pathInRepo of paths) {
33004
+ await forEachWithGitYield([...paths], async (pathInRepo) => {
32860
33005
  const relLauncher = posixJoinDirFile(normRel, pathInRepo.replace(/\\/g, "/"));
32861
33006
  const nums = numByPath.get(pathInRepo);
32862
33007
  let additions = nums?.additions ?? 0;
@@ -32868,8 +33013,8 @@ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
32868
33013
  else change = "modified";
32869
33014
  }
32870
33015
  rows.push({ pathRelLauncher: relLauncher, additions, deletions, change });
32871
- }
32872
- for (const row of rows) {
33016
+ });
33017
+ await forEachWithGitYield(rows, async (row) => {
32873
33018
  let pathInRepo;
32874
33019
  if (normRel === ".") {
32875
33020
  pathInRepo = row.pathRelLauncher;
@@ -32881,16 +33026,16 @@ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
32881
33026
  const raw = await g.raw(["diff", "-U20000", range, "--", pathInRepo]).catch(() => "");
32882
33027
  const t = String(raw).trim();
32883
33028
  row.patchContent = t ? truncatePatch(t) : void 0;
32884
- }
33029
+ });
32885
33030
  rows.sort((a, b) => a.pathRelLauncher.localeCompare(b.pathRelLauncher));
32886
33031
  return rows;
32887
33032
  }
32888
33033
 
32889
- // src/git/working-directory/changes/list-changed-files-for-repo.ts
33034
+ // src/git/changes/list-changed-files-for-repo.ts
32890
33035
  import * as fs22 from "node:fs";
32891
33036
  import * as path22 from "node:path";
32892
33037
 
32893
- // src/git/working-directory/changes/count-lines.ts
33038
+ // src/git/changes/lib/count-lines.ts
32894
33039
  import { createReadStream } from "node:fs";
32895
33040
  import * as readline2 from "node:readline";
32896
33041
  async function countTextFileLines(filePath) {
@@ -32911,7 +33056,7 @@ async function countTextFileLines(filePath) {
32911
33056
  return lines;
32912
33057
  }
32913
33058
 
32914
- // src/git/working-directory/changes/hydrate-patch.ts
33059
+ // src/git/changes/hydrate-patch.ts
32915
33060
  import * as fs21 from "node:fs";
32916
33061
  var UNIFIED_HUNK_HEADER_RE = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
32917
33062
  var MAX_HYDRATE_LINES_PER_GAP = 8e3;
@@ -33027,26 +33172,16 @@ async function hydrateUnifiedPatchWithFileContext(patch, filePath, repoGitCwd, p
33027
33172
  return truncatePatch(out.join("\n"));
33028
33173
  }
33029
33174
 
33030
- // src/git/working-directory/changes/unified-diff-for-file.ts
33175
+ // src/git/changes/unified-diff-for-file.ts
33031
33176
  async function unifiedDiffForFile(repoCwd, pathInRepo, change) {
33032
33177
  const g = cliSimpleGit(repoCwd);
33033
- try {
33034
- let raw;
33035
- if (change === "added") {
33036
- const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
33037
- raw = await g.raw(["diff", "--no-index", "--", devNull, pathInRepo]);
33038
- } else {
33039
- raw = await g.raw(["diff", "HEAD", "--", pathInRepo]);
33040
- }
33041
- const t = String(raw).trim();
33042
- if (!t) return void 0;
33043
- return truncatePatch(t);
33044
- } catch {
33045
- return void 0;
33046
- }
33178
+ const args = change === "added" ? ["diff", "--no-color", "HEAD", "--", pathInRepo] : change === "removed" ? ["diff", "--no-color", "HEAD", "--", pathInRepo] : ["diff", "--no-color", "HEAD", "--", pathInRepo];
33179
+ const raw = await g.raw([...args]).catch(() => "");
33180
+ const t = String(raw).trim();
33181
+ return t ? truncatePatch(t) : void 0;
33047
33182
  }
33048
33183
 
33049
- // src/git/working-directory/changes/list-changed-files-for-repo.ts
33184
+ // src/git/changes/list-changed-files-for-repo.ts
33050
33185
  async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
33051
33186
  const g = cliSimpleGit(repoGitCwd);
33052
33187
  const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
@@ -33060,7 +33195,7 @@ async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
33060
33195
  const untracked = String(untrackedRaw).split("\n").map((s) => s.trim()).filter(Boolean);
33061
33196
  for (const p of untracked) paths.add(p);
33062
33197
  const rows = [];
33063
- for (const pathInRepo of paths) {
33198
+ await forEachWithGitYield([...paths], async (pathInRepo) => {
33064
33199
  const relLauncher = posixJoinDirFile(repoRelPath, pathInRepo.replace(/\\/g, "/"));
33065
33200
  const repoFilePath = path22.join(repoGitCwd, pathInRepo);
33066
33201
  const nums = numByPath.get(pathInRepo);
@@ -33090,9 +33225,9 @@ async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
33090
33225
  else change = "modified";
33091
33226
  }
33092
33227
  rows.push({ pathRelLauncher: relLauncher, additions, deletions, change });
33093
- }
33228
+ });
33094
33229
  const normRel = repoRelPath === "." || repoRelPath === "" ? "." : repoRelPath;
33095
- for (const row of rows) {
33230
+ await forEachWithGitYield(rows, async (row) => {
33096
33231
  let pathInRepo;
33097
33232
  if (normRel === ".") {
33098
33233
  pathInRepo = row.pathRelLauncher;
@@ -33107,11 +33242,11 @@ async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
33107
33242
  patch = await hydrateUnifiedPatchWithFileContext(patch, filePath, repoGitCwd, pathInRepo, row.change);
33108
33243
  }
33109
33244
  row.patchContent = patch;
33110
- }
33245
+ });
33111
33246
  return rows;
33112
33247
  }
33113
33248
 
33114
- // src/git/working-directory/changes/get-working-tree-change-repo-details.ts
33249
+ // src/git/changes/get-working-tree-change-repo-details.ts
33115
33250
  function normRepoRel(p) {
33116
33251
  const x = p.replace(/\\/g, "/").trim();
33117
33252
  return x === "" ? "." : x;
@@ -33130,7 +33265,9 @@ async function getWorkingTreeChangeRepoDetails(options) {
33130
33265
  throw new Error("commit sha is required for commit changes");
33131
33266
  }
33132
33267
  const basis = filter == null && basisInput.kind === "commit" ? { kind: "working" } : basisInput;
33133
- for (const target of options.commitTargetPaths) {
33268
+ for (let i = 0; i < options.commitTargetPaths.length; i++) {
33269
+ if (i > 0) await yieldToEventLoop2();
33270
+ const target = options.commitTargetPaths[i];
33134
33271
  const t = path23.resolve(target);
33135
33272
  if (!await isGitRepoDirectory(t)) continue;
33136
33273
  const g = cliSimpleGit(t);
@@ -33164,7 +33301,10 @@ async function getWorkingTreeChangeRepoDetails(options) {
33164
33301
  const files = basis.kind === "commit" ? await listChangedFilesForCommit(t, relForList, basis.sha.trim()) : await listChangedFilesForRepo(t, relForList);
33165
33302
  const st = await g.status();
33166
33303
  const hasUncommittedChanges = (st.files?.length ?? 0) > 0;
33167
- const unpushedCommits = await listUnpushedCommits(t);
33304
+ const [unpushedCommits, recentCommitList] = await Promise.all([
33305
+ listUnpushedCommits(t),
33306
+ listRecentCommits(t, options.recentCommitsLimit)
33307
+ ]);
33168
33308
  out.push({
33169
33309
  repoRelPath: norm,
33170
33310
  repoDisplayName,
@@ -33174,6 +33314,8 @@ async function getWorkingTreeChangeRepoDetails(options) {
33174
33314
  files,
33175
33315
  hasUncommittedChanges,
33176
33316
  unpushedCommits,
33317
+ recentCommits: recentCommitList.commits,
33318
+ recentCommitsHasMore: recentCommitList.hasMore,
33177
33319
  changesView: basis.kind === "commit" ? "commit" : "working",
33178
33320
  changesCommitSha: basis.kind === "commit" ? basis.sha.trim() : null
33179
33321
  });
@@ -33182,7 +33324,7 @@ async function getWorkingTreeChangeRepoDetails(options) {
33182
33324
  return out;
33183
33325
  }
33184
33326
 
33185
- // src/git/commit-and-push.ts
33327
+ // src/git/branches/commit-and-push.ts
33186
33328
  async function gitCommitAllIfDirty(repoDir, message, options) {
33187
33329
  const g = cliSimpleGit(repoDir);
33188
33330
  const st = await g.status();
@@ -33606,7 +33748,8 @@ var SessionWorktreeManager = class {
33606
33748
  sessionWorktreeRootPath: sessionWorkingTreeRelRoot,
33607
33749
  legacyRepoNestedSessionLayout: legacyNested,
33608
33750
  repoFilterRelPath: opts?.repoRelPath?.trim() ? opts.repoRelPath.trim() : null,
33609
- basis: opts?.basis
33751
+ basis: opts?.basis,
33752
+ recentCommitsLimit: opts?.recentCommitsLimit
33610
33753
  });
33611
33754
  }
33612
33755
  async pushSessionUpstream(sessionId) {
@@ -34136,7 +34279,7 @@ function pipedStdoutStderrFor(attemptStdio) {
34136
34279
  }
34137
34280
 
34138
34281
  // src/dev-servers/manager/shell-spawn/try-spawn-piped-via-sh.ts
34139
- import { spawn as spawn5 } from "node:child_process";
34282
+ import { spawn as spawn6 } from "node:child_process";
34140
34283
  function trySpawnPipedViaSh(command, env, cwd, signal) {
34141
34284
  const attempts = [
34142
34285
  { stdio: [devNullReadFd(), "pipe", "pipe"], endStdin: false },
@@ -34157,9 +34300,9 @@ function trySpawnPipedViaSh(command, env, cwd, signal) {
34157
34300
  if (process.platform === "win32") {
34158
34301
  opts.windowsHide = true;
34159
34302
  const com = process.env.ComSpec || "cmd.exe";
34160
- proc = spawn5(com, ["/d", "/s", "/c", command], opts);
34303
+ proc = spawn6(com, ["/d", "/s", "/c", command], opts);
34161
34304
  } else {
34162
- proc = spawn5("/bin/sh", ["-c", command], opts);
34305
+ proc = spawn6("/bin/sh", ["-c", command], opts);
34163
34306
  }
34164
34307
  if (attempt.endStdin) {
34165
34308
  proc.stdin?.end();
@@ -34179,7 +34322,7 @@ function trySpawnPipedViaSh(command, env, cwd, signal) {
34179
34322
  }
34180
34323
 
34181
34324
  // src/dev-servers/manager/shell-spawn/try-spawn-shell-true-piped.ts
34182
- import { spawn as spawn6 } from "node:child_process";
34325
+ import { spawn as spawn7 } from "node:child_process";
34183
34326
  function trySpawnShellTruePiped(command, env, cwd, devNullFd, signal) {
34184
34327
  try {
34185
34328
  const opts = {
@@ -34192,7 +34335,7 @@ function trySpawnShellTruePiped(command, env, cwd, devNullFd, signal) {
34192
34335
  if (process.platform === "win32") {
34193
34336
  opts.windowsHide = true;
34194
34337
  }
34195
- return spawn6(command, opts);
34338
+ return spawn7(command, opts);
34196
34339
  } catch (e) {
34197
34340
  if (isSpawnEbadf(e)) return null;
34198
34341
  throw e;
@@ -34200,7 +34343,7 @@ function trySpawnShellTruePiped(command, env, cwd, devNullFd, signal) {
34200
34343
  }
34201
34344
 
34202
34345
  // src/dev-servers/manager/shell-spawn/try-spawn-merged-log-file.ts
34203
- import { spawn as spawn7 } from "node:child_process";
34346
+ import { spawn as spawn8 } from "node:child_process";
34204
34347
  import fs28 from "node:fs";
34205
34348
  import { tmpdir } from "node:os";
34206
34349
  import path31 from "node:path";
@@ -34218,7 +34361,7 @@ function trySpawnMergedLogFile(command, env, cwd, signal) {
34218
34361
  try {
34219
34362
  let proc;
34220
34363
  if (process.platform === "win32") {
34221
- proc = spawn7(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", command], {
34364
+ proc = spawn8(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", command], {
34222
34365
  env,
34223
34366
  cwd,
34224
34367
  stdio,
@@ -34226,7 +34369,7 @@ function trySpawnMergedLogFile(command, env, cwd, signal) {
34226
34369
  ...signal ? { signal } : {}
34227
34370
  });
34228
34371
  } else {
34229
- proc = spawn7("/bin/sh", ["-c", command], { env, cwd, stdio, ...signal ? { signal } : {} });
34372
+ proc = spawn8("/bin/sh", ["-c", command], { env, cwd, stdio, ...signal ? { signal } : {} });
34230
34373
  }
34231
34374
  fs28.closeSync(logFd);
34232
34375
  return {
@@ -34247,7 +34390,7 @@ function trySpawnMergedLogFile(command, env, cwd, signal) {
34247
34390
  }
34248
34391
 
34249
34392
  // src/dev-servers/manager/shell-spawn/try-spawn-shell-script-log-redirect.ts
34250
- import { spawn as spawn8 } from "node:child_process";
34393
+ import { spawn as spawn9 } from "node:child_process";
34251
34394
  import fs29 from "node:fs";
34252
34395
  import { tmpdir as tmpdir2 } from "node:os";
34253
34396
  import path32 from "node:path";
@@ -34270,7 +34413,7 @@ cd ${shSingleQuote(cwd)}
34270
34413
  /bin/sh ${shSingleQuote(innerPath)} >>${shSingleQuote(logPath)} 2>&1
34271
34414
  `
34272
34415
  );
34273
- const proc = spawn8("/bin/sh", [runnerPath], {
34416
+ const proc = spawn9("/bin/sh", [runnerPath], {
34274
34417
  env,
34275
34418
  cwd: tmpRoot,
34276
34419
  stdio: "ignore",
@@ -34302,7 +34445,7 @@ CD /D ${q(cwd)}\r
34302
34445
  ${command} >> ${q(logPath)} 2>&1\r
34303
34446
  `
34304
34447
  );
34305
- const proc = spawn8(com, ["/d", "/s", "/c", q(runnerPath)], {
34448
+ const proc = spawn9(com, ["/d", "/s", "/c", q(runnerPath)], {
34306
34449
  env,
34307
34450
  cwd: tmpRoot,
34308
34451
  stdio: "ignore",
@@ -34323,7 +34466,7 @@ ${command} >> ${q(logPath)} 2>&1\r
34323
34466
  }
34324
34467
 
34325
34468
  // src/dev-servers/manager/shell-spawn/try-spawn-inherit.ts
34326
- import { spawn as spawn9 } from "node:child_process";
34469
+ import { spawn as spawn10 } from "node:child_process";
34327
34470
  function trySpawnInheritStdio(command, env, cwd, signal) {
34328
34471
  const opts = {
34329
34472
  env,
@@ -34335,9 +34478,9 @@ function trySpawnInheritStdio(command, env, cwd, signal) {
34335
34478
  if (process.platform === "win32") {
34336
34479
  opts.windowsHide = true;
34337
34480
  const com = process.env.ComSpec || "cmd.exe";
34338
- proc = spawn9(com, ["/d", "/s", "/c", command], opts);
34481
+ proc = spawn10(com, ["/d", "/s", "/c", command], opts);
34339
34482
  } else {
34340
- proc = spawn9("/bin/sh", ["-c", command], opts);
34483
+ proc = spawn10("/bin/sh", ["-c", command], opts);
34341
34484
  }
34342
34485
  return { proc, pipedStdoutStderr: false };
34343
34486
  }
@@ -34899,11 +35042,13 @@ function connectFirehose(options) {
34899
35042
  if (Buffer.isBuffer(raw) && tryConsumeBinaryProxyBody(raw, deps)) {
34900
35043
  return;
34901
35044
  }
34902
- try {
34903
- const text = Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw);
34904
- dispatchFirehoseJsonMessage(JSON.parse(text), deps);
34905
- } catch {
34906
- }
35045
+ setImmediate(() => {
35046
+ try {
35047
+ const text = Buffer.isBuffer(raw) ? raw.toString("utf8") : String(raw);
35048
+ dispatchFirehoseJsonMessage(JSON.parse(text), deps);
35049
+ } catch {
35050
+ }
35051
+ });
34907
35052
  });
34908
35053
  ws.on("close", (code, reason) => {
34909
35054
  disposeClientPing();
@@ -35419,7 +35564,9 @@ async function runLocalRevertBeforeQueuedPrompt(next, deps) {
35419
35564
  const tid = typeof pl.snapshotRevertTurnId === "string" && pl.snapshotRevertTurnId.trim() !== "" ? pl.snapshotRevertTurnId.trim() : next.turnId;
35420
35565
  const agentBase = deps.sessionWorktreeManager.getSessionWorktreeRootForSession(sid) ?? getBridgeRoot();
35421
35566
  const file2 = snapshotFilePath(agentBase, tid);
35422
- if (!fs31.existsSync(file2)) {
35567
+ try {
35568
+ await fs31.promises.access(file2, fs31.constants.F_OK);
35569
+ } catch {
35423
35570
  deps.log(
35424
35571
  `[Queue] requeued_with_revert: no pre-turn snapshot for ${tid.slice(0, 8)}\u2026; continuing without revert.`
35425
35572
  );
@@ -35593,12 +35740,12 @@ function createBridgePromptSenders(deps, getWs) {
35593
35740
  }
35594
35741
 
35595
35742
  // src/agents/acp/from-bridge/bridge-prompt-preamble.ts
35596
- import { execFile as execFile9 } from "node:child_process";
35597
- import { promisify as promisify9 } from "node:util";
35598
- var execFileAsync8 = promisify9(execFile9);
35743
+ import { execFile as execFile8 } from "node:child_process";
35744
+ import { promisify as promisify8 } from "node:util";
35745
+ var execFileAsync7 = promisify8(execFile8);
35599
35746
  async function readGitBranch(cwd) {
35600
35747
  try {
35601
- const { stdout } = await execFileAsync8("git", ["branch", "--show-current"], { cwd, maxBuffer: 64 * 1024 });
35748
+ const { stdout } = await execFileAsync7("git", ["branch", "--show-current"], { cwd, maxBuffer: 64 * 1024 });
35602
35749
  const b = stdout.trim();
35603
35750
  return b || null;
35604
35751
  } catch {
@@ -36308,7 +36455,8 @@ var handleSessionGitRequestMessage = (msg, deps) => {
36308
36455
  reply({
36309
36456
  ok: true,
36310
36457
  hasUncommittedChanges: r.hasUncommittedChanges,
36311
- hasUnpushedCommits: r.hasUnpushedCommits
36458
+ hasUnpushedCommits: r.hasUnpushedCommits,
36459
+ uncommittedFileCount: r.uncommittedFileCount
36312
36460
  });
36313
36461
  return;
36314
36462
  }
@@ -36322,7 +36470,12 @@ var handleSessionGitRequestMessage = (msg, deps) => {
36322
36470
  return;
36323
36471
  }
36324
36472
  }
36325
- const opts = repoRel && view === "commit" && commitSha ? { repoRelPath: repoRel, basis: { kind: "commit", sha: commitSha } } : repoRel ? { repoRelPath: repoRel, basis: { kind: "working" } } : void 0;
36473
+ const recentCommitsLimit = typeof msg.changesRecentCommitsLimit === "number" || typeof msg.changesRecentCommitsLimit === "string" ? msg.changesRecentCommitsLimit : void 0;
36474
+ const opts = repoRel && view === "commit" && commitSha ? {
36475
+ repoRelPath: repoRel,
36476
+ basis: { kind: "commit", sha: commitSha },
36477
+ recentCommitsLimit
36478
+ } : repoRel ? { repoRelPath: repoRel, basis: { kind: "working" }, recentCommitsLimit } : recentCommitsLimit !== void 0 ? { recentCommitsLimit } : void 0;
36326
36479
  const repos = await deps.sessionWorktreeManager.getSessionWorkingTreeChangeDetails(sessionId, opts);
36327
36480
  reply({
36328
36481
  ok: true,
@@ -36340,7 +36493,8 @@ var handleSessionGitRequestMessage = (msg, deps) => {
36340
36493
  reply({
36341
36494
  ok: true,
36342
36495
  hasUncommittedChanges: st2.hasUncommittedChanges,
36343
- hasUnpushedCommits: st2.hasUnpushedCommits
36496
+ hasUnpushedCommits: st2.hasUnpushedCommits,
36497
+ uncommittedFileCount: st2.uncommittedFileCount
36344
36498
  });
36345
36499
  return;
36346
36500
  }
@@ -36365,7 +36519,8 @@ var handleSessionGitRequestMessage = (msg, deps) => {
36365
36519
  reply({
36366
36520
  ok: true,
36367
36521
  hasUncommittedChanges: st.hasUncommittedChanges,
36368
- hasUnpushedCommits: st.hasUnpushedCommits
36522
+ hasUnpushedCommits: st.hasUnpushedCommits,
36523
+ uncommittedFileCount: st.uncommittedFileCount
36369
36524
  });
36370
36525
  } catch (e) {
36371
36526
  reply({ ok: false, error: e instanceof Error ? e.message : String(e) });
@@ -36408,7 +36563,9 @@ var handleRevertTurnSnapshotMessage = (msg, deps) => {
36408
36563
  if (!s) return;
36409
36564
  const agentBase = sessionWorktreeManager.getSessionWorktreeRootForSession(sessionId) ?? getBridgeRoot();
36410
36565
  const file2 = snapshotFilePath(agentBase, turnId);
36411
- if (!fs35.existsSync(file2)) {
36566
+ try {
36567
+ await fs35.promises.access(file2, fs35.constants.F_OK);
36568
+ } catch {
36412
36569
  sendWsMessage(s, {
36413
36570
  type: "revert_turn_snapshot_result",
36414
36571
  id,
@@ -36527,9 +36684,7 @@ function handleBridgeMessage(data, deps) {
36527
36684
  if (!deps.getWs()) return;
36528
36685
  const msg = parseApiToBridgeMessage(normalizeInboundBridgeWebSocketJson(data), deps.log);
36529
36686
  if (!msg) return;
36530
- setImmediate(() => {
36531
- dispatchBridgeMessage(msg, deps);
36532
- });
36687
+ dispatchBridgeMessage(msg, deps);
36533
36688
  }
36534
36689
 
36535
36690
  // src/auth/refresh-bridge-tokens.ts
@@ -37174,25 +37329,27 @@ async function createBridgeConnection(options) {
37174
37329
  }
37175
37330
  function sendAgentCapabilitiesToBridge(info) {
37176
37331
  if (!Array.isArray(info.configOptions) || info.configOptions.length === 0) return;
37177
- let changed = false;
37178
- try {
37179
- changed = withCliSqliteSync(
37180
- (db) => upsertCliAgentCapabilityCache(db, {
37181
- workspaceId,
37182
- agentType: info.agentType,
37183
- configOptions: info.configOptions
37184
- })
37185
- );
37186
- } catch (e) {
37187
- if (e instanceof CliSqliteInterrupted) return;
37188
- }
37189
- if (!changed) return;
37190
- const socket = getWs();
37191
- if (!socket || socket.readyState !== wrapper_default.OPEN) return;
37192
- sendWsMessage(socket, {
37193
- type: "agent_capabilities",
37194
- agentType: info.agentType,
37195
- configOptions: info.configOptions
37332
+ setImmediate(() => {
37333
+ let changed = false;
37334
+ try {
37335
+ changed = withCliSqliteSync(
37336
+ (db) => upsertCliAgentCapabilityCache(db, {
37337
+ workspaceId,
37338
+ agentType: info.agentType,
37339
+ configOptions: info.configOptions
37340
+ })
37341
+ );
37342
+ } catch (e) {
37343
+ if (e instanceof CliSqliteInterrupted) return;
37344
+ }
37345
+ if (!changed) return;
37346
+ const socket = getWs();
37347
+ if (!socket || socket.readyState !== wrapper_default.OPEN) return;
37348
+ sendWsMessage(socket, {
37349
+ type: "agent_capabilities",
37350
+ agentType: info.agentType,
37351
+ configOptions: info.configOptions
37352
+ });
37196
37353
  });
37197
37354
  }
37198
37355
  const worktreesRootPath = options.worktreesRootPath ?? defaultWorktreesRootPath();
@@ -37344,6 +37501,7 @@ async function runConnectedBridge(options, restartWithoutAuth) {
37344
37501
  let bridgeClose = null;
37345
37502
  const onSignal = (kind) => {
37346
37503
  requestCliImmediateShutdown();
37504
+ abortActiveGitChildProcesses();
37347
37505
  cleanupKeyCommand?.();
37348
37506
  logImmediate(
37349
37507
  kind === "interrupt" ? "Keyboard interrupt (Ctrl+C) \u2014 stopping\u2026" : "Stop requested \u2014 shutting down\u2026"