@buildautomaton/cli 0.1.33 → 0.1.34

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
@@ -23962,7 +23962,7 @@ function installBridgeProcessResilience() {
23962
23962
  }
23963
23963
 
23964
23964
  // src/cli-version.ts
23965
- var CLI_VERSION = "0.1.33".length > 0 ? "0.1.33" : "0.0.0-dev";
23965
+ var CLI_VERSION = "0.1.34".length > 0 ? "0.1.34" : "0.0.0-dev";
23966
23966
 
23967
23967
  // src/connection/heartbeat/constants.ts
23968
23968
  var BRIDGE_APP_HEARTBEAT_INTERVAL_MS = 1e4;
@@ -25550,17 +25550,13 @@ function resolveSessionParentPathForAgentProcess(resolvedSessionParentPath) {
25550
25550
  return getBridgeRoot();
25551
25551
  }
25552
25552
 
25553
- // src/git/session-git-queue.ts
25554
- import { execFile as execFile7 } from "node:child_process";
25553
+ // src/git/snapshot/session-git-queue.ts
25555
25554
  import { readFile, stat } from "node:fs/promises";
25556
- import { promisify as promisify7 } from "node:util";
25557
25555
  import * as path15 from "node:path";
25558
25556
 
25559
- // src/git/pre-turn-snapshot.ts
25557
+ // src/git/snapshot/pre-turn-snapshot.ts
25560
25558
  import * as fs14 from "node:fs";
25561
25559
  import * as path14 from "node:path";
25562
- import { execFile as execFile6 } from "node:child_process";
25563
- import { promisify as promisify6 } from "node:util";
25564
25560
 
25565
25561
  // src/git/discover-repos.ts
25566
25562
  import * as fs13 from "node:fs";
@@ -30125,12 +30121,68 @@ function gitInstanceFactory(baseDir, options) {
30125
30121
  init_git_response_error();
30126
30122
  var simpleGit = gitInstanceFactory;
30127
30123
 
30124
+ // src/git/git-runtime.ts
30125
+ var activeGitChildProcesses = /* @__PURE__ */ new Set();
30126
+ function abortActiveGitChildProcesses() {
30127
+ for (const child of activeGitChildProcesses) {
30128
+ try {
30129
+ child.kill("SIGTERM");
30130
+ } catch {
30131
+ }
30132
+ }
30133
+ }
30134
+ var GitOperationAbortedError = class extends Error {
30135
+ constructor(message = "Git operation aborted") {
30136
+ super(message);
30137
+ this.name = "GitOperationAbortedError";
30138
+ }
30139
+ };
30140
+ function throwIfGitShutdownRequested() {
30141
+ if (isCliImmediateShutdownRequested()) {
30142
+ abortActiveGitChildProcesses();
30143
+ throw new GitOperationAbortedError();
30144
+ }
30145
+ }
30146
+ async function runGitTask(fn) {
30147
+ throwIfGitShutdownRequested();
30148
+ try {
30149
+ const result = await fn();
30150
+ throwIfGitShutdownRequested();
30151
+ return result;
30152
+ } catch (e) {
30153
+ if (isCliImmediateShutdownRequested()) {
30154
+ throw new GitOperationAbortedError();
30155
+ }
30156
+ throw e;
30157
+ }
30158
+ }
30159
+ async function yieldToEventLoop2() {
30160
+ throwIfGitShutdownRequested();
30161
+ await new Promise((resolve18) => {
30162
+ setImmediate(resolve18);
30163
+ });
30164
+ throwIfGitShutdownRequested();
30165
+ }
30166
+ async function forEachWithGitYield(items, fn) {
30167
+ for (let i = 0; i < items.length; i++) {
30168
+ if (i > 0 || items.length > 1) await yieldToEventLoop2();
30169
+ await fn(items[i], i);
30170
+ }
30171
+ }
30172
+
30128
30173
  // src/git/cli-simple-git.ts
30129
30174
  function cliSimpleGit(baseDir) {
30130
- const git = simpleGit({ baseDir });
30175
+ const git = simpleGit({
30176
+ baseDir,
30177
+ trimmed: true,
30178
+ spawnOptions: {}
30179
+ });
30131
30180
  git.outputHandler((command, stdout, stderr) => {
30132
30181
  const trace = isCliTrace();
30133
30182
  const onChunk = (label) => (chunk) => {
30183
+ if (isCliImmediateShutdownRequested()) {
30184
+ abortActiveGitChildProcesses();
30185
+ }
30134
30186
  const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
30135
30187
  const line = text.replace(/\s+$/, "");
30136
30188
  if (trace && line) {
@@ -30219,17 +30271,37 @@ async function discoverGitReposUnderRoot(rootPath) {
30219
30271
  return out;
30220
30272
  }
30221
30273
 
30222
- // src/git/pre-turn-snapshot.ts
30274
+ // src/git/git-exec.ts
30275
+ import { execFile as execFile6, spawn as spawn4 } from "node:child_process";
30276
+ import { promisify as promisify6 } from "node:util";
30223
30277
  var execFileAsync5 = promisify6(execFile6);
30278
+ async function execGitFile(args, options) {
30279
+ throwIfGitShutdownRequested();
30280
+ try {
30281
+ const result = await execFileAsync5("git", args, {
30282
+ maxBuffer: 10 * 1024 * 1024,
30283
+ ...options
30284
+ });
30285
+ throwIfGitShutdownRequested();
30286
+ return {
30287
+ stdout: String(result.stdout ?? ""),
30288
+ stderr: String(result.stderr ?? "")
30289
+ };
30290
+ } catch (e) {
30291
+ if (isCliImmediateShutdownRequested()) {
30292
+ throw new GitOperationAbortedError();
30293
+ }
30294
+ throw e;
30295
+ }
30296
+ }
30297
+
30298
+ // src/git/snapshot/pre-turn-snapshot.ts
30224
30299
  function snapshotsDirForCwd(agentCwd) {
30225
30300
  return path14.join(agentCwd, ".buildautomaton", "snapshots");
30226
30301
  }
30227
30302
  async function gitStashCreate(repoRoot, log2) {
30228
30303
  try {
30229
- const { stdout } = await execFileAsync5("git", ["stash", "create"], {
30230
- cwd: repoRoot,
30231
- maxBuffer: 10 * 1024 * 1024
30232
- });
30304
+ const { stdout } = await execGitFile(["stash", "create"], { cwd: repoRoot });
30233
30305
  return stdout.trim();
30234
30306
  } catch (e) {
30235
30307
  log2(
@@ -30240,7 +30312,7 @@ async function gitStashCreate(repoRoot, log2) {
30240
30312
  }
30241
30313
  async function gitRun(repoRoot, args, log2, label) {
30242
30314
  try {
30243
- await execFileAsync5("git", args, { cwd: repoRoot, maxBuffer: 10 * 1024 * 1024 });
30315
+ await execGitFile(args, { cwd: repoRoot });
30244
30316
  return { ok: true };
30245
30317
  } catch (e) {
30246
30318
  const msg = e instanceof Error ? e.message : String(e);
@@ -30274,10 +30346,10 @@ async function capturePreTurnSnapshot(options) {
30274
30346
  return { ok: false, error: "No git repos to snapshot" };
30275
30347
  }
30276
30348
  const repos = [];
30277
- for (const root of repoRoots) {
30349
+ await forEachWithGitYield(repoRoots, async (root) => {
30278
30350
  const stashSha = await gitStashCreate(root, log2);
30279
30351
  repos.push({ path: root, stashSha });
30280
- }
30352
+ });
30281
30353
  const dir = snapshotsDirForCwd(agentCwd);
30282
30354
  try {
30283
30355
  fs14.mkdirSync(dir, { recursive: true });
@@ -30312,17 +30384,25 @@ async function applyPreTurnSnapshot(filePath, log2) {
30312
30384
  if (!Array.isArray(data.repos)) {
30313
30385
  return { ok: false, error: "Invalid snapshot file" };
30314
30386
  }
30315
- for (const r of data.repos) {
30316
- if (!r.path) continue;
30387
+ let applyError = null;
30388
+ await forEachWithGitYield(data.repos, async (r) => {
30389
+ if (applyError || !r.path) return;
30317
30390
  const reset = await gitRun(r.path, ["reset", "--hard", "HEAD"], log2, "reset --hard");
30318
- if (!reset.ok) return reset;
30391
+ if (!reset.ok) {
30392
+ applyError = reset;
30393
+ return;
30394
+ }
30319
30395
  const clean = await gitRun(r.path, ["clean", "-fd"], log2, "clean -fd");
30320
- if (!clean.ok) return clean;
30396
+ if (!clean.ok) {
30397
+ applyError = clean;
30398
+ return;
30399
+ }
30321
30400
  if (r.stashSha) {
30322
30401
  const ap = await gitRun(r.path, ["stash", "apply", r.stashSha], log2, "stash apply");
30323
- if (!ap.ok) return ap;
30402
+ if (!ap.ok) applyError = ap;
30324
30403
  }
30325
- }
30404
+ });
30405
+ if (applyError?.ok === false) return applyError;
30326
30406
  log2(`[snapshot] Restored pre-turn state for ${data.runId.slice(0, 8)}\u2026`);
30327
30407
  return { ok: true };
30328
30408
  }
@@ -30330,8 +30410,7 @@ function snapshotFilePath(agentCwd, runId) {
30330
30410
  return path14.join(snapshotsDirForCwd(agentCwd), `${runId}.json`);
30331
30411
  }
30332
30412
 
30333
- // src/git/session-git-queue.ts
30334
- var execFileAsync6 = promisify7(execFile7);
30413
+ // src/git/snapshot/session-git-queue.ts
30335
30414
  var MAX_FULL_FILE_TEXT_BYTES = 512 * 1024;
30336
30415
  async function readWorkspaceFileAsUtf8(absPath) {
30337
30416
  try {
@@ -30360,35 +30439,28 @@ async function collectTurnGitDiffFromPreTurnSnapshot(options) {
30360
30439
  return;
30361
30440
  }
30362
30441
  const multiRepo = data.repos.length > 1;
30363
- for (const repo of data.repos) {
30364
- if (!repo.stashSha) continue;
30442
+ await forEachWithGitYield(data.repos, async (repo) => {
30443
+ if (!repo.stashSha) return;
30365
30444
  let namesRaw;
30366
30445
  try {
30367
- const { stdout } = await execFileAsync6("git", ["diff", "--name-only", repo.stashSha], {
30368
- cwd: repo.path,
30369
- maxBuffer: 10 * 1024 * 1024
30370
- });
30446
+ const { stdout } = await execGitFile(["diff", "--name-only", repo.stashSha], { cwd: repo.path });
30371
30447
  namesRaw = stdout;
30372
30448
  } catch (e) {
30373
30449
  log2(
30374
30450
  `[session-git-queue] Git diff --name-only failed in ${repo.path}: ${e instanceof Error ? e.message : String(e)}`
30375
30451
  );
30376
- continue;
30452
+ return;
30377
30453
  }
30378
30454
  const lines = namesRaw.split("\n").map((l) => l.trim()).filter(Boolean);
30379
30455
  const slug = path15.basename(repo.path).replace(/[^\w.-]+/g, "_") || "repo";
30380
- for (const rel of lines) {
30381
- if (rel.includes("..")) continue;
30456
+ await forEachWithGitYield(lines, async (rel) => {
30457
+ if (rel.includes("..")) return;
30382
30458
  try {
30383
- const { stdout: patchContent } = await execFileAsync6(
30384
- "git",
30459
+ const { stdout: patchContent } = await execGitFile(
30385
30460
  ["diff", "--no-color", repo.stashSha, "--", rel],
30386
- {
30387
- cwd: repo.path,
30388
- maxBuffer: 50 * 1024 * 1024
30389
- }
30461
+ { cwd: repo.path }
30390
30462
  );
30391
- if (!patchContent.trim()) continue;
30463
+ if (!patchContent.trim()) return;
30392
30464
  const displayPath = multiRepo ? `${slug}/${rel}` : rel;
30393
30465
  const workspaceFilePath = path15.join(repo.path, rel);
30394
30466
  const newText = await readWorkspaceFileAsUtf8(workspaceFilePath);
@@ -30405,8 +30477,8 @@ async function collectTurnGitDiffFromPreTurnSnapshot(options) {
30405
30477
  `[session-git-queue] Git diff failed for ${rel}: ${e instanceof Error ? e.message : String(e)}`
30406
30478
  );
30407
30479
  }
30408
- }
30409
- }
30480
+ });
30481
+ });
30410
30482
  }
30411
30483
 
30412
30484
  // src/agents/acp/put-summarize-change-summaries.ts
@@ -30774,9 +30846,9 @@ __export(claude_code_acp_client_exports, {
30774
30846
  });
30775
30847
 
30776
30848
  // 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);
30849
+ import { execFile as execFile7 } from "node:child_process";
30850
+ import { promisify as promisify7 } from "node:util";
30851
+ var execFileAsync6 = promisify7(execFile7);
30780
30852
  var COMMAND_ON_PATH_PROBE_TIMEOUT_MS = 750;
30781
30853
  async function execFileShutdownAware(file2, args, timeoutMs) {
30782
30854
  if (isCliImmediateShutdownRequested()) throw new Error("shutdown");
@@ -30786,7 +30858,7 @@ async function execFileShutdownAware(file2, args, timeoutMs) {
30786
30858
  }, 50);
30787
30859
  shutdownPoll.unref?.();
30788
30860
  try {
30789
- await execFileAsync7(file2, args, { timeout: timeoutMs, signal: ac.signal });
30861
+ await execFileAsync6(file2, args, { timeout: timeoutMs, signal: ac.signal });
30790
30862
  } finally {
30791
30863
  clearInterval(shutdownPoll);
30792
30864
  }
@@ -30866,7 +30938,7 @@ __export(cursor_acp_client_exports, {
30866
30938
  createCursorAcpClient: () => createCursorAcpClient,
30867
30939
  detectLocalAgentPresence: () => detectLocalAgentPresence3
30868
30940
  });
30869
- import { spawn as spawn4 } from "node:child_process";
30941
+ import { spawn as spawn5 } from "node:child_process";
30870
30942
  import * as readline from "node:readline";
30871
30943
 
30872
30944
  // src/agents/acp/format-session-update-kind-for-log.ts
@@ -30936,7 +31008,7 @@ async function createCursorAcpClient(options) {
30936
31008
  } = options;
30937
31009
  const dbgFs = process.env.BUILDAUTOMATON_DEBUG_ACP_FS === "1";
30938
31010
  const isWindows = process.platform === "win32";
30939
- const child = spawn4(command[0], command.slice(1), {
31011
+ const child = spawn5(command[0], command.slice(1), {
30940
31012
  cwd,
30941
31013
  stdio: ["pipe", "pipe", "pipe"],
30942
31014
  env: process.env,
@@ -32382,7 +32454,7 @@ import os8 from "node:os";
32382
32454
  import * as fs17 from "node:fs";
32383
32455
  import * as path20 from "node:path";
32384
32456
 
32385
- // src/git/worktree-add.ts
32457
+ // src/git/worktrees/worktree-add.ts
32386
32458
  async function gitWorktreeAddBranch(mainRepoPath, worktreePath, branch) {
32387
32459
  const mainGit = cliSimpleGit(mainRepoPath);
32388
32460
  await mainGit.raw(["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
@@ -32487,7 +32559,7 @@ async function prepareNewSessionWorktrees(options) {
32487
32559
  };
32488
32560
  }
32489
32561
 
32490
- // src/git/rename-branch.ts
32562
+ // src/git/branches/rename-branch.ts
32491
32563
  async function gitRenameCurrentBranch(repoDir, newName) {
32492
32564
  const g = cliSimpleGit(repoDir);
32493
32565
  await g.raw(["branch", "-m", newName]);
@@ -32511,10 +32583,10 @@ async function renameSessionWorktreeBranches(paths, newBranch, log2) {
32511
32583
  // src/worktrees/remove-session-worktrees.ts
32512
32584
  import * as fs20 from "node:fs";
32513
32585
 
32514
- // src/git/worktree-remove.ts
32586
+ // src/git/worktrees/worktree-remove.ts
32515
32587
  import * as fs19 from "node:fs";
32516
32588
 
32517
- // src/git/resolve-main-repo-from-git-file.ts
32589
+ // src/git/worktrees/resolve-main-repo-from-git-file.ts
32518
32590
  import * as fs18 from "node:fs";
32519
32591
  import * as path21 from "node:path";
32520
32592
  function resolveMainRepoFromWorktreeGitFile(wt) {
@@ -32528,7 +32600,7 @@ function resolveMainRepoFromWorktreeGitFile(wt) {
32528
32600
  return path21.dirname(gitDir);
32529
32601
  }
32530
32602
 
32531
- // src/git/worktree-remove.ts
32603
+ // src/git/worktrees/worktree-remove.ts
32532
32604
  async function gitWorktreeRemoveForce(worktreePath) {
32533
32605
  const mainRepo = resolveMainRepoFromWorktreeGitFile(worktreePath);
32534
32606
  if (mainRepo) {
@@ -32554,7 +32626,88 @@ async function removeSessionWorktrees(paths, log2) {
32554
32626
  }
32555
32627
  }
32556
32628
 
32557
- // src/git/working-directory/status/working-tree-status.ts
32629
+ // src/git/changes/lib/parse-git-status.ts
32630
+ function parseNameStatusLines(lines) {
32631
+ const m = /* @__PURE__ */ new Map();
32632
+ for (const line of lines) {
32633
+ if (!line.trim()) continue;
32634
+ const tabParts = line.split(" ");
32635
+ if (tabParts.length < 2) continue;
32636
+ const status = tabParts[0].trim();
32637
+ const code = status[0];
32638
+ if (code === "A") {
32639
+ m.set(tabParts[tabParts.length - 1], "added");
32640
+ } else if (code === "D") {
32641
+ m.set(tabParts[tabParts.length - 1], "removed");
32642
+ } else if (code === "R" || code === "C") {
32643
+ if (tabParts.length >= 3) m.set(tabParts[tabParts.length - 1], "modified");
32644
+ } else if (code === "M" || code === "U" || code === "T") {
32645
+ m.set(tabParts[tabParts.length - 1], "modified");
32646
+ }
32647
+ }
32648
+ return m;
32649
+ }
32650
+ function parseNumstatFirstLine(line) {
32651
+ const parts = line.split(" ");
32652
+ if (parts.length < 3) return null;
32653
+ const [a, d] = parts;
32654
+ const additions = a === "-" ? 0 : parseInt(String(a), 10) || 0;
32655
+ const deletions = d === "-" ? 0 : parseInt(String(d), 10) || 0;
32656
+ return { additions, deletions };
32657
+ }
32658
+ function parseNumstat(lines) {
32659
+ const m = /* @__PURE__ */ new Map();
32660
+ for (const line of lines) {
32661
+ if (!line.trim()) continue;
32662
+ const parts = line.split(" ");
32663
+ if (parts.length < 3) continue;
32664
+ const [a, d, p] = parts;
32665
+ const additions = a === "-" ? 0 : parseInt(String(a), 10) || 0;
32666
+ const deletions = d === "-" ? 0 : parseInt(String(d), 10) || 0;
32667
+ m.set(p, { additions, deletions });
32668
+ }
32669
+ return m;
32670
+ }
32671
+ async function numstatFromGitNoIndex(g, pathInRepo) {
32672
+ const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
32673
+ try {
32674
+ const out = await g.raw(["diff", "--numstat", "--no-index", "--", devNull, pathInRepo]);
32675
+ const first2 = String(out).split("\n").find((l) => l.trim()) ?? "";
32676
+ return parseNumstatFirstLine(first2);
32677
+ } catch {
32678
+ return null;
32679
+ }
32680
+ }
32681
+
32682
+ // src/git/changes/lib/working-tree-changed-path-count.ts
32683
+ function workingTreeChangedPathCount(nameStatusLines, numstatLines, untrackedLines) {
32684
+ const kindByPath = parseNameStatusLines(nameStatusLines);
32685
+ const numByPath = parseNumstat(numstatLines);
32686
+ const paths = /* @__PURE__ */ new Set([...kindByPath.keys(), ...numByPath.keys()]);
32687
+ for (const p of untrackedLines.map((s) => s.trim()).filter(Boolean)) {
32688
+ paths.add(p);
32689
+ }
32690
+ return paths.size;
32691
+ }
32692
+
32693
+ // src/git/changes/count-working-tree-changed-files.ts
32694
+ async function countWorkingTreeChangedFilesForRepo(repoGitCwd) {
32695
+ return runGitTask(async () => {
32696
+ const g = cliSimpleGit(repoGitCwd);
32697
+ const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
32698
+ g.raw(["diff", "--name-status", "HEAD"]).catch(() => ""),
32699
+ g.raw(["diff", "HEAD", "--numstat"]).catch(() => ""),
32700
+ g.raw(["ls-files", "--others", "--exclude-standard"]).catch(() => "")
32701
+ ]);
32702
+ return workingTreeChangedPathCount(
32703
+ String(nameStatusRaw).split("\n"),
32704
+ String(numstatRaw).split("\n"),
32705
+ String(untrackedRaw).split("\n")
32706
+ );
32707
+ });
32708
+ }
32709
+
32710
+ // src/git/commits/resolve-remote-tracking.ts
32558
32711
  async function tryConfigGet(g, key) {
32559
32712
  try {
32560
32713
  const out = await g.raw(["config", "--get", key]);
@@ -32637,30 +32790,6 @@ async function resolveBaseShaForUnpushedCommits(g) {
32637
32790
  if (!defaultRef) return null;
32638
32791
  return revParseSafe(g, defaultRef);
32639
32792
  }
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
32793
  async function commitsAheadOfRemoteTracking(repoDir) {
32665
32794
  const g = cliSimpleGit(repoDir);
32666
32795
  const headSha = await revParseSafe(g, "HEAD");
@@ -32675,44 +32804,41 @@ async function commitsAheadOfRemoteTracking(repoDir) {
32675
32804
  return 0;
32676
32805
  }
32677
32806
  }
32807
+
32808
+ // src/git/status/working-tree-status.ts
32678
32809
  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);
32810
+ return runGitTask(async () => {
32811
+ const uncommittedFileCount = await countWorkingTreeChangedFilesForRepo(repoDir);
32812
+ const hasUncommittedChanges = uncommittedFileCount > 0;
32813
+ const ahead = await commitsAheadOfRemoteTracking(repoDir);
32814
+ return { hasUncommittedChanges, hasUnpushedCommits: ahead > 0, uncommittedFileCount };
32815
+ });
32692
32816
  }
32693
32817
  async function aggregateSessionPathsWorkingTreeStatus(paths) {
32694
32818
  let hasUncommittedChanges = false;
32695
32819
  let hasUnpushedCommits = false;
32696
- for (const p of paths) {
32820
+ let uncommittedFileCount = 0;
32821
+ await forEachWithGitYield(paths, async (p) => {
32697
32822
  const s = await getRepoWorkingTreeStatus(p);
32823
+ uncommittedFileCount += s.uncommittedFileCount;
32698
32824
  if (s.hasUncommittedChanges) hasUncommittedChanges = true;
32699
32825
  if (s.hasUnpushedCommits) hasUnpushedCommits = true;
32700
- }
32701
- return { hasUncommittedChanges, hasUnpushedCommits };
32826
+ });
32827
+ return { hasUncommittedChanges, hasUnpushedCommits, uncommittedFileCount };
32702
32828
  }
32703
32829
  async function pushAheadOfUpstreamForPaths(paths) {
32704
- for (const p of paths) {
32830
+ await forEachWithGitYield(paths, async (p) => {
32705
32831
  const g = cliSimpleGit(p);
32706
32832
  const ahead = await commitsAheadOfRemoteTracking(p);
32707
- if (ahead <= 0) continue;
32833
+ if (ahead <= 0) return;
32708
32834
  await g.push();
32709
- }
32835
+ });
32710
32836
  }
32711
32837
 
32712
- // src/git/working-directory/changes/types.ts
32838
+ // src/git/changes/types.ts
32713
32839
  var MAX_PATCH_CHARS = 35e4;
32714
32840
 
32715
- // src/git/working-directory/changes/repo-format.ts
32841
+ // src/git/changes/lib/repo-format.ts
32716
32842
  function posixJoinDirFile(dir, file2) {
32717
32843
  const d = dir === "." || dir === "" ? "" : dir.replace(/\\/g, "/").replace(/\/+$/, "");
32718
32844
  const f = file2.replace(/\\/g, "/").replace(/^\/+/, "");
@@ -32766,63 +32892,80 @@ function formatRemoteDisplayLabel(remoteUrl) {
32766
32892
  return `origin \xB7 ${hostPath}`;
32767
32893
  }
32768
32894
 
32769
- // src/git/working-directory/changes/get-working-tree-change-repo-details.ts
32895
+ // src/git/changes/get-working-tree-change-repo-details.ts
32770
32896
  import * as path23 from "node:path";
32771
32897
 
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
- }
32898
+ // src/git/commits/lib/parse-log-lines.ts
32899
+ function parseLogShaDateSubjectLines(raw) {
32900
+ const out = [];
32901
+ for (const line of String(raw).split("\n")) {
32902
+ const l = line.trimEnd();
32903
+ if (!l.trim()) continue;
32904
+ const parts = l.split(" ");
32905
+ if (parts.length < 3) continue;
32906
+ const sha = parts[0].trim();
32907
+ const committedAt = parts[1].trim();
32908
+ const subject = parts.slice(2).join(" ").trim();
32909
+ if (!/^[0-9a-f]{7,40}$/i.test(sha)) continue;
32910
+ out.push({ sha, shortSha: sha.slice(0, 7), subject, committedAt });
32790
32911
  }
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 };
32912
+ return out;
32800
32913
  }
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 });
32914
+
32915
+ // src/git/commits/list-unpushed-commits.ts
32916
+ async function gitLogNotReachableFromBase(g, baseSha, headSha) {
32917
+ if (baseSha === headSha) return [];
32918
+ try {
32919
+ const logOut = await g.raw(["log", "--format=%H %cI %s", `${baseSha}..${headSha}`]);
32920
+ return parseLogShaDateSubjectLines(logOut);
32921
+ } catch {
32922
+ return [];
32811
32923
  }
32812
- return m;
32813
32924
  }
32814
- async function numstatFromGitNoIndex(g, pathInRepo) {
32815
- const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
32925
+ async function listUnpushedCommits(repoDir) {
32926
+ const g = cliSimpleGit(repoDir);
32927
+ const headSha = await revParseSafe(g, "HEAD");
32928
+ if (!headSha) return [];
32929
+ const baseSha = await resolveBaseShaForUnpushedCommits(g);
32930
+ if (!baseSha) return [];
32931
+ return gitLogNotReachableFromBase(g, baseSha, headSha);
32932
+ }
32933
+
32934
+ // src/git/commits/lib/sanitize-recent-commits-limit.ts
32935
+ var RECENT_COMMITS_LIMIT_MIN = 1;
32936
+ var RECENT_COMMITS_LIMIT_MAX = 50;
32937
+ var RECENT_COMMITS_LIMIT_DEFAULT = 2;
32938
+ function sanitizeRecentCommitsLimit(value) {
32939
+ const n = typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value.trim(), 10) : Number.NaN;
32940
+ if (!Number.isFinite(n)) return RECENT_COMMITS_LIMIT_DEFAULT;
32941
+ return Math.min(RECENT_COMMITS_LIMIT_MAX, Math.max(RECENT_COMMITS_LIMIT_MIN, Math.trunc(n)));
32942
+ }
32943
+
32944
+ // src/git/commits/list-recent-commits.ts
32945
+ async function listRecentCommits(repoDir, limitInput) {
32946
+ const limit = sanitizeRecentCommitsLimit(limitInput);
32947
+ const g = cliSimpleGit(repoDir);
32948
+ const headSha = await revParseSafe(g, "HEAD");
32949
+ if (!headSha) return { commits: [], hasMore: false };
32950
+ const unpushedSet = new Set((await listUnpushedCommits(repoDir)).map((c) => c.sha));
32816
32951
  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);
32952
+ const logOut = await g.raw(["log", "--format=%H %cI %s", `-n`, String(limit), "HEAD"]);
32953
+ const commits = parseLogShaDateSubjectLines(logOut).map((c) => ({
32954
+ ...c,
32955
+ needsPush: unpushedSet.has(c.sha)
32956
+ }));
32957
+ let hasMore = false;
32958
+ if (commits.length === limit) {
32959
+ const nextOut = await g.raw(["log", "-n", "1", `--skip=${limit}`, "--format=%H", "HEAD"]);
32960
+ hasMore = /^[0-9a-f]{7,40}$/i.test(String(nextOut).trim());
32961
+ }
32962
+ return { commits, hasMore };
32820
32963
  } catch {
32821
- return null;
32964
+ return { commits: [], hasMore: false };
32822
32965
  }
32823
32966
  }
32824
32967
 
32825
- // src/git/working-directory/changes/patch-truncate.ts
32968
+ // src/git/changes/lib/patch-truncate.ts
32826
32969
  function truncatePatch(s) {
32827
32970
  if (s.length <= MAX_PATCH_CHARS) return s;
32828
32971
  return `${s.slice(0, MAX_PATCH_CHARS)}
@@ -32830,7 +32973,7 @@ function truncatePatch(s) {
32830
32973
  \u2026 (diff truncated)`;
32831
32974
  }
32832
32975
 
32833
- // src/git/working-directory/changes/list-changed-files-for-commit.ts
32976
+ // src/git/commits/list-changed-files-for-commit.ts
32834
32977
  var EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
32835
32978
  async function parentForCommitDiff(g, sha) {
32836
32979
  try {
@@ -32856,7 +32999,7 @@ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
32856
32999
  const paths = new Set([...kindByPath.keys(), ...numByPath.keys()].filter(Boolean));
32857
33000
  const rows = [];
32858
33001
  const normRel = repoRelPath === "." || repoRelPath === "" ? "." : repoRelPath;
32859
- for (const pathInRepo of paths) {
33002
+ await forEachWithGitYield([...paths], async (pathInRepo) => {
32860
33003
  const relLauncher = posixJoinDirFile(normRel, pathInRepo.replace(/\\/g, "/"));
32861
33004
  const nums = numByPath.get(pathInRepo);
32862
33005
  let additions = nums?.additions ?? 0;
@@ -32868,8 +33011,8 @@ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
32868
33011
  else change = "modified";
32869
33012
  }
32870
33013
  rows.push({ pathRelLauncher: relLauncher, additions, deletions, change });
32871
- }
32872
- for (const row of rows) {
33014
+ });
33015
+ await forEachWithGitYield(rows, async (row) => {
32873
33016
  let pathInRepo;
32874
33017
  if (normRel === ".") {
32875
33018
  pathInRepo = row.pathRelLauncher;
@@ -32881,16 +33024,16 @@ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
32881
33024
  const raw = await g.raw(["diff", "-U20000", range, "--", pathInRepo]).catch(() => "");
32882
33025
  const t = String(raw).trim();
32883
33026
  row.patchContent = t ? truncatePatch(t) : void 0;
32884
- }
33027
+ });
32885
33028
  rows.sort((a, b) => a.pathRelLauncher.localeCompare(b.pathRelLauncher));
32886
33029
  return rows;
32887
33030
  }
32888
33031
 
32889
- // src/git/working-directory/changes/list-changed-files-for-repo.ts
33032
+ // src/git/changes/list-changed-files-for-repo.ts
32890
33033
  import * as fs22 from "node:fs";
32891
33034
  import * as path22 from "node:path";
32892
33035
 
32893
- // src/git/working-directory/changes/count-lines.ts
33036
+ // src/git/changes/lib/count-lines.ts
32894
33037
  import { createReadStream } from "node:fs";
32895
33038
  import * as readline2 from "node:readline";
32896
33039
  async function countTextFileLines(filePath) {
@@ -32911,7 +33054,7 @@ async function countTextFileLines(filePath) {
32911
33054
  return lines;
32912
33055
  }
32913
33056
 
32914
- // src/git/working-directory/changes/hydrate-patch.ts
33057
+ // src/git/changes/hydrate-patch.ts
32915
33058
  import * as fs21 from "node:fs";
32916
33059
  var UNIFIED_HUNK_HEADER_RE = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
32917
33060
  var MAX_HYDRATE_LINES_PER_GAP = 8e3;
@@ -33027,26 +33170,16 @@ async function hydrateUnifiedPatchWithFileContext(patch, filePath, repoGitCwd, p
33027
33170
  return truncatePatch(out.join("\n"));
33028
33171
  }
33029
33172
 
33030
- // src/git/working-directory/changes/unified-diff-for-file.ts
33173
+ // src/git/changes/unified-diff-for-file.ts
33031
33174
  async function unifiedDiffForFile(repoCwd, pathInRepo, change) {
33032
33175
  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
- }
33176
+ const args = change === "added" ? ["diff", "--no-color", "HEAD", "--", pathInRepo] : change === "removed" ? ["diff", "--no-color", "HEAD", "--", pathInRepo] : ["diff", "--no-color", "HEAD", "--", pathInRepo];
33177
+ const raw = await g.raw([...args]).catch(() => "");
33178
+ const t = String(raw).trim();
33179
+ return t ? truncatePatch(t) : void 0;
33047
33180
  }
33048
33181
 
33049
- // src/git/working-directory/changes/list-changed-files-for-repo.ts
33182
+ // src/git/changes/list-changed-files-for-repo.ts
33050
33183
  async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
33051
33184
  const g = cliSimpleGit(repoGitCwd);
33052
33185
  const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
@@ -33060,7 +33193,7 @@ async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
33060
33193
  const untracked = String(untrackedRaw).split("\n").map((s) => s.trim()).filter(Boolean);
33061
33194
  for (const p of untracked) paths.add(p);
33062
33195
  const rows = [];
33063
- for (const pathInRepo of paths) {
33196
+ await forEachWithGitYield([...paths], async (pathInRepo) => {
33064
33197
  const relLauncher = posixJoinDirFile(repoRelPath, pathInRepo.replace(/\\/g, "/"));
33065
33198
  const repoFilePath = path22.join(repoGitCwd, pathInRepo);
33066
33199
  const nums = numByPath.get(pathInRepo);
@@ -33090,9 +33223,9 @@ async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
33090
33223
  else change = "modified";
33091
33224
  }
33092
33225
  rows.push({ pathRelLauncher: relLauncher, additions, deletions, change });
33093
- }
33226
+ });
33094
33227
  const normRel = repoRelPath === "." || repoRelPath === "" ? "." : repoRelPath;
33095
- for (const row of rows) {
33228
+ await forEachWithGitYield(rows, async (row) => {
33096
33229
  let pathInRepo;
33097
33230
  if (normRel === ".") {
33098
33231
  pathInRepo = row.pathRelLauncher;
@@ -33107,11 +33240,11 @@ async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
33107
33240
  patch = await hydrateUnifiedPatchWithFileContext(patch, filePath, repoGitCwd, pathInRepo, row.change);
33108
33241
  }
33109
33242
  row.patchContent = patch;
33110
- }
33243
+ });
33111
33244
  return rows;
33112
33245
  }
33113
33246
 
33114
- // src/git/working-directory/changes/get-working-tree-change-repo-details.ts
33247
+ // src/git/changes/get-working-tree-change-repo-details.ts
33115
33248
  function normRepoRel(p) {
33116
33249
  const x = p.replace(/\\/g, "/").trim();
33117
33250
  return x === "" ? "." : x;
@@ -33130,7 +33263,9 @@ async function getWorkingTreeChangeRepoDetails(options) {
33130
33263
  throw new Error("commit sha is required for commit changes");
33131
33264
  }
33132
33265
  const basis = filter == null && basisInput.kind === "commit" ? { kind: "working" } : basisInput;
33133
- for (const target of options.commitTargetPaths) {
33266
+ for (let i = 0; i < options.commitTargetPaths.length; i++) {
33267
+ if (i > 0) await yieldToEventLoop2();
33268
+ const target = options.commitTargetPaths[i];
33134
33269
  const t = path23.resolve(target);
33135
33270
  if (!await isGitRepoDirectory(t)) continue;
33136
33271
  const g = cliSimpleGit(t);
@@ -33164,7 +33299,10 @@ async function getWorkingTreeChangeRepoDetails(options) {
33164
33299
  const files = basis.kind === "commit" ? await listChangedFilesForCommit(t, relForList, basis.sha.trim()) : await listChangedFilesForRepo(t, relForList);
33165
33300
  const st = await g.status();
33166
33301
  const hasUncommittedChanges = (st.files?.length ?? 0) > 0;
33167
- const unpushedCommits = await listUnpushedCommits(t);
33302
+ const [unpushedCommits, recentCommitList] = await Promise.all([
33303
+ listUnpushedCommits(t),
33304
+ listRecentCommits(t, options.recentCommitsLimit)
33305
+ ]);
33168
33306
  out.push({
33169
33307
  repoRelPath: norm,
33170
33308
  repoDisplayName,
@@ -33174,6 +33312,8 @@ async function getWorkingTreeChangeRepoDetails(options) {
33174
33312
  files,
33175
33313
  hasUncommittedChanges,
33176
33314
  unpushedCommits,
33315
+ recentCommits: recentCommitList.commits,
33316
+ recentCommitsHasMore: recentCommitList.hasMore,
33177
33317
  changesView: basis.kind === "commit" ? "commit" : "working",
33178
33318
  changesCommitSha: basis.kind === "commit" ? basis.sha.trim() : null
33179
33319
  });
@@ -33182,7 +33322,7 @@ async function getWorkingTreeChangeRepoDetails(options) {
33182
33322
  return out;
33183
33323
  }
33184
33324
 
33185
- // src/git/commit-and-push.ts
33325
+ // src/git/branches/commit-and-push.ts
33186
33326
  async function gitCommitAllIfDirty(repoDir, message, options) {
33187
33327
  const g = cliSimpleGit(repoDir);
33188
33328
  const st = await g.status();
@@ -33606,7 +33746,8 @@ var SessionWorktreeManager = class {
33606
33746
  sessionWorktreeRootPath: sessionWorkingTreeRelRoot,
33607
33747
  legacyRepoNestedSessionLayout: legacyNested,
33608
33748
  repoFilterRelPath: opts?.repoRelPath?.trim() ? opts.repoRelPath.trim() : null,
33609
- basis: opts?.basis
33749
+ basis: opts?.basis,
33750
+ recentCommitsLimit: opts?.recentCommitsLimit
33610
33751
  });
33611
33752
  }
33612
33753
  async pushSessionUpstream(sessionId) {
@@ -34136,7 +34277,7 @@ function pipedStdoutStderrFor(attemptStdio) {
34136
34277
  }
34137
34278
 
34138
34279
  // src/dev-servers/manager/shell-spawn/try-spawn-piped-via-sh.ts
34139
- import { spawn as spawn5 } from "node:child_process";
34280
+ import { spawn as spawn6 } from "node:child_process";
34140
34281
  function trySpawnPipedViaSh(command, env, cwd, signal) {
34141
34282
  const attempts = [
34142
34283
  { stdio: [devNullReadFd(), "pipe", "pipe"], endStdin: false },
@@ -34157,9 +34298,9 @@ function trySpawnPipedViaSh(command, env, cwd, signal) {
34157
34298
  if (process.platform === "win32") {
34158
34299
  opts.windowsHide = true;
34159
34300
  const com = process.env.ComSpec || "cmd.exe";
34160
- proc = spawn5(com, ["/d", "/s", "/c", command], opts);
34301
+ proc = spawn6(com, ["/d", "/s", "/c", command], opts);
34161
34302
  } else {
34162
- proc = spawn5("/bin/sh", ["-c", command], opts);
34303
+ proc = spawn6("/bin/sh", ["-c", command], opts);
34163
34304
  }
34164
34305
  if (attempt.endStdin) {
34165
34306
  proc.stdin?.end();
@@ -34179,7 +34320,7 @@ function trySpawnPipedViaSh(command, env, cwd, signal) {
34179
34320
  }
34180
34321
 
34181
34322
  // src/dev-servers/manager/shell-spawn/try-spawn-shell-true-piped.ts
34182
- import { spawn as spawn6 } from "node:child_process";
34323
+ import { spawn as spawn7 } from "node:child_process";
34183
34324
  function trySpawnShellTruePiped(command, env, cwd, devNullFd, signal) {
34184
34325
  try {
34185
34326
  const opts = {
@@ -34192,7 +34333,7 @@ function trySpawnShellTruePiped(command, env, cwd, devNullFd, signal) {
34192
34333
  if (process.platform === "win32") {
34193
34334
  opts.windowsHide = true;
34194
34335
  }
34195
- return spawn6(command, opts);
34336
+ return spawn7(command, opts);
34196
34337
  } catch (e) {
34197
34338
  if (isSpawnEbadf(e)) return null;
34198
34339
  throw e;
@@ -34200,7 +34341,7 @@ function trySpawnShellTruePiped(command, env, cwd, devNullFd, signal) {
34200
34341
  }
34201
34342
 
34202
34343
  // src/dev-servers/manager/shell-spawn/try-spawn-merged-log-file.ts
34203
- import { spawn as spawn7 } from "node:child_process";
34344
+ import { spawn as spawn8 } from "node:child_process";
34204
34345
  import fs28 from "node:fs";
34205
34346
  import { tmpdir } from "node:os";
34206
34347
  import path31 from "node:path";
@@ -34218,7 +34359,7 @@ function trySpawnMergedLogFile(command, env, cwd, signal) {
34218
34359
  try {
34219
34360
  let proc;
34220
34361
  if (process.platform === "win32") {
34221
- proc = spawn7(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", command], {
34362
+ proc = spawn8(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", command], {
34222
34363
  env,
34223
34364
  cwd,
34224
34365
  stdio,
@@ -34226,7 +34367,7 @@ function trySpawnMergedLogFile(command, env, cwd, signal) {
34226
34367
  ...signal ? { signal } : {}
34227
34368
  });
34228
34369
  } else {
34229
- proc = spawn7("/bin/sh", ["-c", command], { env, cwd, stdio, ...signal ? { signal } : {} });
34370
+ proc = spawn8("/bin/sh", ["-c", command], { env, cwd, stdio, ...signal ? { signal } : {} });
34230
34371
  }
34231
34372
  fs28.closeSync(logFd);
34232
34373
  return {
@@ -34247,7 +34388,7 @@ function trySpawnMergedLogFile(command, env, cwd, signal) {
34247
34388
  }
34248
34389
 
34249
34390
  // src/dev-servers/manager/shell-spawn/try-spawn-shell-script-log-redirect.ts
34250
- import { spawn as spawn8 } from "node:child_process";
34391
+ import { spawn as spawn9 } from "node:child_process";
34251
34392
  import fs29 from "node:fs";
34252
34393
  import { tmpdir as tmpdir2 } from "node:os";
34253
34394
  import path32 from "node:path";
@@ -34270,7 +34411,7 @@ cd ${shSingleQuote(cwd)}
34270
34411
  /bin/sh ${shSingleQuote(innerPath)} >>${shSingleQuote(logPath)} 2>&1
34271
34412
  `
34272
34413
  );
34273
- const proc = spawn8("/bin/sh", [runnerPath], {
34414
+ const proc = spawn9("/bin/sh", [runnerPath], {
34274
34415
  env,
34275
34416
  cwd: tmpRoot,
34276
34417
  stdio: "ignore",
@@ -34302,7 +34443,7 @@ CD /D ${q(cwd)}\r
34302
34443
  ${command} >> ${q(logPath)} 2>&1\r
34303
34444
  `
34304
34445
  );
34305
- const proc = spawn8(com, ["/d", "/s", "/c", q(runnerPath)], {
34446
+ const proc = spawn9(com, ["/d", "/s", "/c", q(runnerPath)], {
34306
34447
  env,
34307
34448
  cwd: tmpRoot,
34308
34449
  stdio: "ignore",
@@ -34323,7 +34464,7 @@ ${command} >> ${q(logPath)} 2>&1\r
34323
34464
  }
34324
34465
 
34325
34466
  // src/dev-servers/manager/shell-spawn/try-spawn-inherit.ts
34326
- import { spawn as spawn9 } from "node:child_process";
34467
+ import { spawn as spawn10 } from "node:child_process";
34327
34468
  function trySpawnInheritStdio(command, env, cwd, signal) {
34328
34469
  const opts = {
34329
34470
  env,
@@ -34335,9 +34476,9 @@ function trySpawnInheritStdio(command, env, cwd, signal) {
34335
34476
  if (process.platform === "win32") {
34336
34477
  opts.windowsHide = true;
34337
34478
  const com = process.env.ComSpec || "cmd.exe";
34338
- proc = spawn9(com, ["/d", "/s", "/c", command], opts);
34479
+ proc = spawn10(com, ["/d", "/s", "/c", command], opts);
34339
34480
  } else {
34340
- proc = spawn9("/bin/sh", ["-c", command], opts);
34481
+ proc = spawn10("/bin/sh", ["-c", command], opts);
34341
34482
  }
34342
34483
  return { proc, pipedStdoutStderr: false };
34343
34484
  }
@@ -35593,12 +35734,12 @@ function createBridgePromptSenders(deps, getWs) {
35593
35734
  }
35594
35735
 
35595
35736
  // 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);
35737
+ import { execFile as execFile8 } from "node:child_process";
35738
+ import { promisify as promisify8 } from "node:util";
35739
+ var execFileAsync7 = promisify8(execFile8);
35599
35740
  async function readGitBranch(cwd) {
35600
35741
  try {
35601
- const { stdout } = await execFileAsync8("git", ["branch", "--show-current"], { cwd, maxBuffer: 64 * 1024 });
35742
+ const { stdout } = await execFileAsync7("git", ["branch", "--show-current"], { cwd, maxBuffer: 64 * 1024 });
35602
35743
  const b = stdout.trim();
35603
35744
  return b || null;
35604
35745
  } catch {
@@ -36308,7 +36449,8 @@ var handleSessionGitRequestMessage = (msg, deps) => {
36308
36449
  reply({
36309
36450
  ok: true,
36310
36451
  hasUncommittedChanges: r.hasUncommittedChanges,
36311
- hasUnpushedCommits: r.hasUnpushedCommits
36452
+ hasUnpushedCommits: r.hasUnpushedCommits,
36453
+ uncommittedFileCount: r.uncommittedFileCount
36312
36454
  });
36313
36455
  return;
36314
36456
  }
@@ -36322,7 +36464,12 @@ var handleSessionGitRequestMessage = (msg, deps) => {
36322
36464
  return;
36323
36465
  }
36324
36466
  }
36325
- const opts = repoRel && view === "commit" && commitSha ? { repoRelPath: repoRel, basis: { kind: "commit", sha: commitSha } } : repoRel ? { repoRelPath: repoRel, basis: { kind: "working" } } : void 0;
36467
+ const recentCommitsLimit = typeof msg.changesRecentCommitsLimit === "number" || typeof msg.changesRecentCommitsLimit === "string" ? msg.changesRecentCommitsLimit : void 0;
36468
+ const opts = repoRel && view === "commit" && commitSha ? {
36469
+ repoRelPath: repoRel,
36470
+ basis: { kind: "commit", sha: commitSha },
36471
+ recentCommitsLimit
36472
+ } : repoRel ? { repoRelPath: repoRel, basis: { kind: "working" }, recentCommitsLimit } : recentCommitsLimit !== void 0 ? { recentCommitsLimit } : void 0;
36326
36473
  const repos = await deps.sessionWorktreeManager.getSessionWorkingTreeChangeDetails(sessionId, opts);
36327
36474
  reply({
36328
36475
  ok: true,
@@ -36340,7 +36487,8 @@ var handleSessionGitRequestMessage = (msg, deps) => {
36340
36487
  reply({
36341
36488
  ok: true,
36342
36489
  hasUncommittedChanges: st2.hasUncommittedChanges,
36343
- hasUnpushedCommits: st2.hasUnpushedCommits
36490
+ hasUnpushedCommits: st2.hasUnpushedCommits,
36491
+ uncommittedFileCount: st2.uncommittedFileCount
36344
36492
  });
36345
36493
  return;
36346
36494
  }
@@ -36365,7 +36513,8 @@ var handleSessionGitRequestMessage = (msg, deps) => {
36365
36513
  reply({
36366
36514
  ok: true,
36367
36515
  hasUncommittedChanges: st.hasUncommittedChanges,
36368
- hasUnpushedCommits: st.hasUnpushedCommits
36516
+ hasUnpushedCommits: st.hasUnpushedCommits,
36517
+ uncommittedFileCount: st.uncommittedFileCount
36369
36518
  });
36370
36519
  } catch (e) {
36371
36520
  reply({ ok: false, error: e instanceof Error ? e.message : String(e) });
@@ -37344,6 +37493,7 @@ async function runConnectedBridge(options, restartWithoutAuth) {
37344
37493
  let bridgeClose = null;
37345
37494
  const onSignal = (kind) => {
37346
37495
  requestCliImmediateShutdown();
37496
+ abortActiveGitChildProcesses();
37347
37497
  cleanupKeyCommand?.();
37348
37498
  logImmediate(
37349
37499
  kind === "interrupt" ? "Keyboard interrupt (Ctrl+C) \u2014 stopping\u2026" : "Stop requested \u2014 shutting down\u2026"