@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/cli.js CHANGED
@@ -25064,7 +25064,7 @@ var {
25064
25064
  } = import_index.default;
25065
25065
 
25066
25066
  // src/cli-version.ts
25067
- var CLI_VERSION = "0.1.33".length > 0 ? "0.1.33" : "0.0.0-dev";
25067
+ var CLI_VERSION = "0.1.34".length > 0 ? "0.1.34" : "0.0.0-dev";
25068
25068
 
25069
25069
  // src/cli/defaults.ts
25070
25070
  var DEFAULT_API_URL = process.env.BUILDAUTOMATON_API_URL ?? "https://api.buildautomaton.com";
@@ -27783,17 +27783,13 @@ function getAgentModelFromAgentConfig(config2) {
27783
27783
  return t !== "" ? t : null;
27784
27784
  }
27785
27785
 
27786
- // src/git/session-git-queue.ts
27787
- import { execFile as execFile7 } from "node:child_process";
27786
+ // src/git/snapshot/session-git-queue.ts
27788
27787
  import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
27789
- import { promisify as promisify7 } from "node:util";
27790
27788
  import * as path15 from "node:path";
27791
27789
 
27792
- // src/git/pre-turn-snapshot.ts
27790
+ // src/git/snapshot/pre-turn-snapshot.ts
27793
27791
  import * as fs15 from "node:fs";
27794
27792
  import * as path14 from "node:path";
27795
- import { execFile as execFile6 } from "node:child_process";
27796
- import { promisify as promisify6 } from "node:util";
27797
27793
 
27798
27794
  // src/git/discover-repos.ts
27799
27795
  import * as fs14 from "node:fs";
@@ -32358,12 +32354,68 @@ function gitInstanceFactory(baseDir, options) {
32358
32354
  init_git_response_error();
32359
32355
  var simpleGit = gitInstanceFactory;
32360
32356
 
32357
+ // src/git/git-runtime.ts
32358
+ var activeGitChildProcesses = /* @__PURE__ */ new Set();
32359
+ function abortActiveGitChildProcesses() {
32360
+ for (const child of activeGitChildProcesses) {
32361
+ try {
32362
+ child.kill("SIGTERM");
32363
+ } catch {
32364
+ }
32365
+ }
32366
+ }
32367
+ var GitOperationAbortedError = class extends Error {
32368
+ constructor(message = "Git operation aborted") {
32369
+ super(message);
32370
+ this.name = "GitOperationAbortedError";
32371
+ }
32372
+ };
32373
+ function throwIfGitShutdownRequested() {
32374
+ if (isCliImmediateShutdownRequested()) {
32375
+ abortActiveGitChildProcesses();
32376
+ throw new GitOperationAbortedError();
32377
+ }
32378
+ }
32379
+ async function runGitTask(fn) {
32380
+ throwIfGitShutdownRequested();
32381
+ try {
32382
+ const result = await fn();
32383
+ throwIfGitShutdownRequested();
32384
+ return result;
32385
+ } catch (e) {
32386
+ if (isCliImmediateShutdownRequested()) {
32387
+ throw new GitOperationAbortedError();
32388
+ }
32389
+ throw e;
32390
+ }
32391
+ }
32392
+ async function yieldToEventLoop2() {
32393
+ throwIfGitShutdownRequested();
32394
+ await new Promise((resolve20) => {
32395
+ setImmediate(resolve20);
32396
+ });
32397
+ throwIfGitShutdownRequested();
32398
+ }
32399
+ async function forEachWithGitYield(items, fn) {
32400
+ for (let i = 0; i < items.length; i++) {
32401
+ if (i > 0 || items.length > 1) await yieldToEventLoop2();
32402
+ await fn(items[i], i);
32403
+ }
32404
+ }
32405
+
32361
32406
  // src/git/cli-simple-git.ts
32362
32407
  function cliSimpleGit(baseDir) {
32363
- const git = simpleGit({ baseDir });
32408
+ const git = simpleGit({
32409
+ baseDir,
32410
+ trimmed: true,
32411
+ spawnOptions: {}
32412
+ });
32364
32413
  git.outputHandler((command, stdout, stderr) => {
32365
32414
  const trace = isCliTrace();
32366
32415
  const onChunk = (label) => (chunk) => {
32416
+ if (isCliImmediateShutdownRequested()) {
32417
+ abortActiveGitChildProcesses();
32418
+ }
32367
32419
  const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
32368
32420
  const line = text.replace(/\s+$/, "");
32369
32421
  if (trace && line) {
@@ -32452,17 +32504,37 @@ async function discoverGitReposUnderRoot(rootPath) {
32452
32504
  return out;
32453
32505
  }
32454
32506
 
32455
- // src/git/pre-turn-snapshot.ts
32507
+ // src/git/git-exec.ts
32508
+ import { execFile as execFile6, spawn as spawn2 } from "node:child_process";
32509
+ import { promisify as promisify6 } from "node:util";
32456
32510
  var execFileAsync5 = promisify6(execFile6);
32511
+ async function execGitFile(args, options) {
32512
+ throwIfGitShutdownRequested();
32513
+ try {
32514
+ const result = await execFileAsync5("git", args, {
32515
+ maxBuffer: 10 * 1024 * 1024,
32516
+ ...options
32517
+ });
32518
+ throwIfGitShutdownRequested();
32519
+ return {
32520
+ stdout: String(result.stdout ?? ""),
32521
+ stderr: String(result.stderr ?? "")
32522
+ };
32523
+ } catch (e) {
32524
+ if (isCliImmediateShutdownRequested()) {
32525
+ throw new GitOperationAbortedError();
32526
+ }
32527
+ throw e;
32528
+ }
32529
+ }
32530
+
32531
+ // src/git/snapshot/pre-turn-snapshot.ts
32457
32532
  function snapshotsDirForCwd(agentCwd) {
32458
32533
  return path14.join(agentCwd, ".buildautomaton", "snapshots");
32459
32534
  }
32460
32535
  async function gitStashCreate(repoRoot, log2) {
32461
32536
  try {
32462
- const { stdout } = await execFileAsync5("git", ["stash", "create"], {
32463
- cwd: repoRoot,
32464
- maxBuffer: 10 * 1024 * 1024
32465
- });
32537
+ const { stdout } = await execGitFile(["stash", "create"], { cwd: repoRoot });
32466
32538
  return stdout.trim();
32467
32539
  } catch (e) {
32468
32540
  log2(
@@ -32473,7 +32545,7 @@ async function gitStashCreate(repoRoot, log2) {
32473
32545
  }
32474
32546
  async function gitRun(repoRoot, args, log2, label) {
32475
32547
  try {
32476
- await execFileAsync5("git", args, { cwd: repoRoot, maxBuffer: 10 * 1024 * 1024 });
32548
+ await execGitFile(args, { cwd: repoRoot });
32477
32549
  return { ok: true };
32478
32550
  } catch (e) {
32479
32551
  const msg = e instanceof Error ? e.message : String(e);
@@ -32507,10 +32579,10 @@ async function capturePreTurnSnapshot(options) {
32507
32579
  return { ok: false, error: "No git repos to snapshot" };
32508
32580
  }
32509
32581
  const repos = [];
32510
- for (const root of repoRoots) {
32582
+ await forEachWithGitYield(repoRoots, async (root) => {
32511
32583
  const stashSha = await gitStashCreate(root, log2);
32512
32584
  repos.push({ path: root, stashSha });
32513
- }
32585
+ });
32514
32586
  const dir = snapshotsDirForCwd(agentCwd);
32515
32587
  try {
32516
32588
  fs15.mkdirSync(dir, { recursive: true });
@@ -32545,17 +32617,25 @@ async function applyPreTurnSnapshot(filePath, log2) {
32545
32617
  if (!Array.isArray(data.repos)) {
32546
32618
  return { ok: false, error: "Invalid snapshot file" };
32547
32619
  }
32548
- for (const r of data.repos) {
32549
- if (!r.path) continue;
32620
+ let applyError = null;
32621
+ await forEachWithGitYield(data.repos, async (r) => {
32622
+ if (applyError || !r.path) return;
32550
32623
  const reset = await gitRun(r.path, ["reset", "--hard", "HEAD"], log2, "reset --hard");
32551
- if (!reset.ok) return reset;
32624
+ if (!reset.ok) {
32625
+ applyError = reset;
32626
+ return;
32627
+ }
32552
32628
  const clean = await gitRun(r.path, ["clean", "-fd"], log2, "clean -fd");
32553
- if (!clean.ok) return clean;
32629
+ if (!clean.ok) {
32630
+ applyError = clean;
32631
+ return;
32632
+ }
32554
32633
  if (r.stashSha) {
32555
32634
  const ap = await gitRun(r.path, ["stash", "apply", r.stashSha], log2, "stash apply");
32556
- if (!ap.ok) return ap;
32635
+ if (!ap.ok) applyError = ap;
32557
32636
  }
32558
- }
32637
+ });
32638
+ if (applyError?.ok === false) return applyError;
32559
32639
  log2(`[snapshot] Restored pre-turn state for ${data.runId.slice(0, 8)}\u2026`);
32560
32640
  return { ok: true };
32561
32641
  }
@@ -32563,8 +32643,7 @@ function snapshotFilePath(agentCwd, runId) {
32563
32643
  return path14.join(snapshotsDirForCwd(agentCwd), `${runId}.json`);
32564
32644
  }
32565
32645
 
32566
- // src/git/session-git-queue.ts
32567
- var execFileAsync6 = promisify7(execFile7);
32646
+ // src/git/snapshot/session-git-queue.ts
32568
32647
  var MAX_FULL_FILE_TEXT_BYTES = 512 * 1024;
32569
32648
  async function readWorkspaceFileAsUtf8(absPath) {
32570
32649
  try {
@@ -32593,35 +32672,28 @@ async function collectTurnGitDiffFromPreTurnSnapshot(options) {
32593
32672
  return;
32594
32673
  }
32595
32674
  const multiRepo = data.repos.length > 1;
32596
- for (const repo of data.repos) {
32597
- if (!repo.stashSha) continue;
32675
+ await forEachWithGitYield(data.repos, async (repo) => {
32676
+ if (!repo.stashSha) return;
32598
32677
  let namesRaw;
32599
32678
  try {
32600
- const { stdout } = await execFileAsync6("git", ["diff", "--name-only", repo.stashSha], {
32601
- cwd: repo.path,
32602
- maxBuffer: 10 * 1024 * 1024
32603
- });
32679
+ const { stdout } = await execGitFile(["diff", "--name-only", repo.stashSha], { cwd: repo.path });
32604
32680
  namesRaw = stdout;
32605
32681
  } catch (e) {
32606
32682
  log2(
32607
32683
  `[session-git-queue] Git diff --name-only failed in ${repo.path}: ${e instanceof Error ? e.message : String(e)}`
32608
32684
  );
32609
- continue;
32685
+ return;
32610
32686
  }
32611
32687
  const lines = namesRaw.split("\n").map((l) => l.trim()).filter(Boolean);
32612
32688
  const slug = path15.basename(repo.path).replace(/[^\w.-]+/g, "_") || "repo";
32613
- for (const rel of lines) {
32614
- if (rel.includes("..")) continue;
32689
+ await forEachWithGitYield(lines, async (rel) => {
32690
+ if (rel.includes("..")) return;
32615
32691
  try {
32616
- const { stdout: patchContent } = await execFileAsync6(
32617
- "git",
32692
+ const { stdout: patchContent } = await execGitFile(
32618
32693
  ["diff", "--no-color", repo.stashSha, "--", rel],
32619
- {
32620
- cwd: repo.path,
32621
- maxBuffer: 50 * 1024 * 1024
32622
- }
32694
+ { cwd: repo.path }
32623
32695
  );
32624
- if (!patchContent.trim()) continue;
32696
+ if (!patchContent.trim()) return;
32625
32697
  const displayPath = multiRepo ? `${slug}/${rel}` : rel;
32626
32698
  const workspaceFilePath = path15.join(repo.path, rel);
32627
32699
  const newText = await readWorkspaceFileAsUtf8(workspaceFilePath);
@@ -32638,8 +32710,8 @@ async function collectTurnGitDiffFromPreTurnSnapshot(options) {
32638
32710
  `[session-git-queue] Git diff failed for ${rel}: ${e instanceof Error ? e.message : String(e)}`
32639
32711
  );
32640
32712
  }
32641
- }
32642
- }
32713
+ });
32714
+ });
32643
32715
  }
32644
32716
 
32645
32717
  // src/agents/acp/put-summarize-change-summaries.ts
@@ -32981,9 +33053,9 @@ __export(claude_code_acp_client_exports, {
32981
33053
  });
32982
33054
 
32983
33055
  // src/agents/acp/clients/detect-command-on-path.ts
32984
- import { execFile as execFile8 } from "node:child_process";
32985
- import { promisify as promisify8 } from "node:util";
32986
- var execFileAsync7 = promisify8(execFile8);
33056
+ import { execFile as execFile7 } from "node:child_process";
33057
+ import { promisify as promisify7 } from "node:util";
33058
+ var execFileAsync6 = promisify7(execFile7);
32987
33059
  var COMMAND_ON_PATH_PROBE_TIMEOUT_MS = 750;
32988
33060
  async function execFileShutdownAware(file2, args, timeoutMs) {
32989
33061
  if (isCliImmediateShutdownRequested()) throw new Error("shutdown");
@@ -32993,7 +33065,7 @@ async function execFileShutdownAware(file2, args, timeoutMs) {
32993
33065
  }, 50);
32994
33066
  shutdownPoll.unref?.();
32995
33067
  try {
32996
- await execFileAsync7(file2, args, { timeout: timeoutMs, signal: ac.signal });
33068
+ await execFileAsync6(file2, args, { timeout: timeoutMs, signal: ac.signal });
32997
33069
  } finally {
32998
33070
  clearInterval(shutdownPoll);
32999
33071
  }
@@ -33018,7 +33090,7 @@ async function execProbeShutdownAware(file2, args, timeoutMs) {
33018
33090
  }
33019
33091
 
33020
33092
  // src/agents/acp/clients/sdk/sdk-stdio-acp-client.ts
33021
- import { spawn as spawn2 } from "node:child_process";
33093
+ import { spawn as spawn3 } from "node:child_process";
33022
33094
  import { Readable, Writable } from "node:stream";
33023
33095
 
33024
33096
  // src/agents/acp/clients/agent-stderr-capture.ts
@@ -33613,7 +33685,7 @@ async function createSdkStdioAcpClient(options) {
33613
33685
  getActiveConfigOptions
33614
33686
  } = options;
33615
33687
  const isWindows = process.platform === "win32";
33616
- const child = spawn2(command[0], command.slice(1), {
33688
+ const child = spawn3(command[0], command.slice(1), {
33617
33689
  cwd,
33618
33690
  stdio: ["pipe", "pipe", "pipe"],
33619
33691
  env: process.env,
@@ -33826,7 +33898,7 @@ __export(cursor_acp_client_exports, {
33826
33898
  createCursorAcpClient: () => createCursorAcpClient,
33827
33899
  detectLocalAgentPresence: () => detectLocalAgentPresence3
33828
33900
  });
33829
- import { spawn as spawn3 } from "node:child_process";
33901
+ import { spawn as spawn4 } from "node:child_process";
33830
33902
  import * as readline from "node:readline";
33831
33903
 
33832
33904
  // src/agents/acp/format-session-update-kind-for-log.ts
@@ -33896,7 +33968,7 @@ async function createCursorAcpClient(options) {
33896
33968
  } = options;
33897
33969
  const dbgFs = process.env.BUILDAUTOMATON_DEBUG_ACP_FS === "1";
33898
33970
  const isWindows = process.platform === "win32";
33899
- const child = spawn3(command[0], command.slice(1), {
33971
+ const child = spawn4(command[0], command.slice(1), {
33900
33972
  cwd,
33901
33973
  stdio: ["pipe", "pipe", "pipe"],
33902
33974
  env: process.env,
@@ -35342,7 +35414,7 @@ import os8 from "node:os";
35342
35414
  import * as fs18 from "node:fs";
35343
35415
  import * as path21 from "node:path";
35344
35416
 
35345
- // src/git/worktree-add.ts
35417
+ // src/git/worktrees/worktree-add.ts
35346
35418
  async function gitWorktreeAddBranch(mainRepoPath, worktreePath, branch) {
35347
35419
  const mainGit = cliSimpleGit(mainRepoPath);
35348
35420
  await mainGit.raw(["worktree", "add", "-b", branch, worktreePath, "HEAD"]);
@@ -35447,7 +35519,7 @@ async function prepareNewSessionWorktrees(options) {
35447
35519
  };
35448
35520
  }
35449
35521
 
35450
- // src/git/rename-branch.ts
35522
+ // src/git/branches/rename-branch.ts
35451
35523
  async function gitRenameCurrentBranch(repoDir, newName) {
35452
35524
  const g = cliSimpleGit(repoDir);
35453
35525
  await g.raw(["branch", "-m", newName]);
@@ -35471,10 +35543,10 @@ async function renameSessionWorktreeBranches(paths, newBranch, log2) {
35471
35543
  // src/worktrees/remove-session-worktrees.ts
35472
35544
  import * as fs21 from "node:fs";
35473
35545
 
35474
- // src/git/worktree-remove.ts
35546
+ // src/git/worktrees/worktree-remove.ts
35475
35547
  import * as fs20 from "node:fs";
35476
35548
 
35477
- // src/git/resolve-main-repo-from-git-file.ts
35549
+ // src/git/worktrees/resolve-main-repo-from-git-file.ts
35478
35550
  import * as fs19 from "node:fs";
35479
35551
  import * as path22 from "node:path";
35480
35552
  function resolveMainRepoFromWorktreeGitFile(wt) {
@@ -35488,7 +35560,7 @@ function resolveMainRepoFromWorktreeGitFile(wt) {
35488
35560
  return path22.dirname(gitDir);
35489
35561
  }
35490
35562
 
35491
- // src/git/worktree-remove.ts
35563
+ // src/git/worktrees/worktree-remove.ts
35492
35564
  async function gitWorktreeRemoveForce(worktreePath) {
35493
35565
  const mainRepo = resolveMainRepoFromWorktreeGitFile(worktreePath);
35494
35566
  if (mainRepo) {
@@ -35514,7 +35586,88 @@ async function removeSessionWorktrees(paths, log2) {
35514
35586
  }
35515
35587
  }
35516
35588
 
35517
- // src/git/working-directory/status/working-tree-status.ts
35589
+ // src/git/changes/lib/parse-git-status.ts
35590
+ function parseNameStatusLines(lines) {
35591
+ const m = /* @__PURE__ */ new Map();
35592
+ for (const line of lines) {
35593
+ if (!line.trim()) continue;
35594
+ const tabParts = line.split(" ");
35595
+ if (tabParts.length < 2) continue;
35596
+ const status = tabParts[0].trim();
35597
+ const code = status[0];
35598
+ if (code === "A") {
35599
+ m.set(tabParts[tabParts.length - 1], "added");
35600
+ } else if (code === "D") {
35601
+ m.set(tabParts[tabParts.length - 1], "removed");
35602
+ } else if (code === "R" || code === "C") {
35603
+ if (tabParts.length >= 3) m.set(tabParts[tabParts.length - 1], "modified");
35604
+ } else if (code === "M" || code === "U" || code === "T") {
35605
+ m.set(tabParts[tabParts.length - 1], "modified");
35606
+ }
35607
+ }
35608
+ return m;
35609
+ }
35610
+ function parseNumstatFirstLine(line) {
35611
+ const parts = line.split(" ");
35612
+ if (parts.length < 3) return null;
35613
+ const [a, d] = parts;
35614
+ const additions = a === "-" ? 0 : parseInt(String(a), 10) || 0;
35615
+ const deletions = d === "-" ? 0 : parseInt(String(d), 10) || 0;
35616
+ return { additions, deletions };
35617
+ }
35618
+ function parseNumstat(lines) {
35619
+ const m = /* @__PURE__ */ new Map();
35620
+ for (const line of lines) {
35621
+ if (!line.trim()) continue;
35622
+ const parts = line.split(" ");
35623
+ if (parts.length < 3) continue;
35624
+ const [a, d, p] = parts;
35625
+ const additions = a === "-" ? 0 : parseInt(String(a), 10) || 0;
35626
+ const deletions = d === "-" ? 0 : parseInt(String(d), 10) || 0;
35627
+ m.set(p, { additions, deletions });
35628
+ }
35629
+ return m;
35630
+ }
35631
+ async function numstatFromGitNoIndex(g, pathInRepo) {
35632
+ const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
35633
+ try {
35634
+ const out = await g.raw(["diff", "--numstat", "--no-index", "--", devNull, pathInRepo]);
35635
+ const first2 = String(out).split("\n").find((l) => l.trim()) ?? "";
35636
+ return parseNumstatFirstLine(first2);
35637
+ } catch {
35638
+ return null;
35639
+ }
35640
+ }
35641
+
35642
+ // src/git/changes/lib/working-tree-changed-path-count.ts
35643
+ function workingTreeChangedPathCount(nameStatusLines, numstatLines, untrackedLines) {
35644
+ const kindByPath = parseNameStatusLines(nameStatusLines);
35645
+ const numByPath = parseNumstat(numstatLines);
35646
+ const paths = /* @__PURE__ */ new Set([...kindByPath.keys(), ...numByPath.keys()]);
35647
+ for (const p of untrackedLines.map((s) => s.trim()).filter(Boolean)) {
35648
+ paths.add(p);
35649
+ }
35650
+ return paths.size;
35651
+ }
35652
+
35653
+ // src/git/changes/count-working-tree-changed-files.ts
35654
+ async function countWorkingTreeChangedFilesForRepo(repoGitCwd) {
35655
+ return runGitTask(async () => {
35656
+ const g = cliSimpleGit(repoGitCwd);
35657
+ const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
35658
+ g.raw(["diff", "--name-status", "HEAD"]).catch(() => ""),
35659
+ g.raw(["diff", "HEAD", "--numstat"]).catch(() => ""),
35660
+ g.raw(["ls-files", "--others", "--exclude-standard"]).catch(() => "")
35661
+ ]);
35662
+ return workingTreeChangedPathCount(
35663
+ String(nameStatusRaw).split("\n"),
35664
+ String(numstatRaw).split("\n"),
35665
+ String(untrackedRaw).split("\n")
35666
+ );
35667
+ });
35668
+ }
35669
+
35670
+ // src/git/commits/resolve-remote-tracking.ts
35518
35671
  async function tryConfigGet(g, key) {
35519
35672
  try {
35520
35673
  const out = await g.raw(["config", "--get", key]);
@@ -35597,30 +35750,6 @@ async function resolveBaseShaForUnpushedCommits(g) {
35597
35750
  if (!defaultRef) return null;
35598
35751
  return revParseSafe(g, defaultRef);
35599
35752
  }
35600
- function parseLogShaDateSubjectLines(raw) {
35601
- const out = [];
35602
- for (const line of String(raw).split("\n")) {
35603
- const l = line.trimEnd();
35604
- if (!l.trim()) continue;
35605
- const parts = l.split(" ");
35606
- if (parts.length < 3) continue;
35607
- const sha = parts[0].trim();
35608
- const committedAt = parts[1].trim();
35609
- const subject = parts.slice(2).join(" ").trim();
35610
- if (!/^[0-9a-f]{7,40}$/i.test(sha)) continue;
35611
- out.push({ sha, shortSha: sha.slice(0, 7), subject, committedAt });
35612
- }
35613
- return out;
35614
- }
35615
- async function gitLogNotReachableFromBase(g, baseSha, headSha) {
35616
- if (baseSha === headSha) return [];
35617
- try {
35618
- const logOut = await g.raw(["log", "--format=%H %cI %s", `${baseSha}..${headSha}`]);
35619
- return parseLogShaDateSubjectLines(logOut);
35620
- } catch {
35621
- return [];
35622
- }
35623
- }
35624
35753
  async function commitsAheadOfRemoteTracking(repoDir) {
35625
35754
  const g = cliSimpleGit(repoDir);
35626
35755
  const headSha = await revParseSafe(g, "HEAD");
@@ -35635,44 +35764,41 @@ async function commitsAheadOfRemoteTracking(repoDir) {
35635
35764
  return 0;
35636
35765
  }
35637
35766
  }
35767
+
35768
+ // src/git/status/working-tree-status.ts
35638
35769
  async function getRepoWorkingTreeStatus(repoDir) {
35639
- const g = cliSimpleGit(repoDir);
35640
- const st = await g.status();
35641
- const hasUncommittedChanges = (st.files?.length ?? 0) > 0;
35642
- const ahead = await commitsAheadOfRemoteTracking(repoDir);
35643
- return { hasUncommittedChanges, hasUnpushedCommits: ahead > 0 };
35644
- }
35645
- async function listUnpushedCommits(repoDir) {
35646
- const g = cliSimpleGit(repoDir);
35647
- const headSha = await revParseSafe(g, "HEAD");
35648
- if (!headSha) return [];
35649
- const baseSha = await resolveBaseShaForUnpushedCommits(g);
35650
- if (!baseSha) return [];
35651
- return gitLogNotReachableFromBase(g, baseSha, headSha);
35770
+ return runGitTask(async () => {
35771
+ const uncommittedFileCount = await countWorkingTreeChangedFilesForRepo(repoDir);
35772
+ const hasUncommittedChanges = uncommittedFileCount > 0;
35773
+ const ahead = await commitsAheadOfRemoteTracking(repoDir);
35774
+ return { hasUncommittedChanges, hasUnpushedCommits: ahead > 0, uncommittedFileCount };
35775
+ });
35652
35776
  }
35653
35777
  async function aggregateSessionPathsWorkingTreeStatus(paths) {
35654
35778
  let hasUncommittedChanges = false;
35655
35779
  let hasUnpushedCommits = false;
35656
- for (const p of paths) {
35780
+ let uncommittedFileCount = 0;
35781
+ await forEachWithGitYield(paths, async (p) => {
35657
35782
  const s = await getRepoWorkingTreeStatus(p);
35783
+ uncommittedFileCount += s.uncommittedFileCount;
35658
35784
  if (s.hasUncommittedChanges) hasUncommittedChanges = true;
35659
35785
  if (s.hasUnpushedCommits) hasUnpushedCommits = true;
35660
- }
35661
- return { hasUncommittedChanges, hasUnpushedCommits };
35786
+ });
35787
+ return { hasUncommittedChanges, hasUnpushedCommits, uncommittedFileCount };
35662
35788
  }
35663
35789
  async function pushAheadOfUpstreamForPaths(paths) {
35664
- for (const p of paths) {
35790
+ await forEachWithGitYield(paths, async (p) => {
35665
35791
  const g = cliSimpleGit(p);
35666
35792
  const ahead = await commitsAheadOfRemoteTracking(p);
35667
- if (ahead <= 0) continue;
35793
+ if (ahead <= 0) return;
35668
35794
  await g.push();
35669
- }
35795
+ });
35670
35796
  }
35671
35797
 
35672
- // src/git/working-directory/changes/types.ts
35798
+ // src/git/changes/types.ts
35673
35799
  var MAX_PATCH_CHARS = 35e4;
35674
35800
 
35675
- // src/git/working-directory/changes/repo-format.ts
35801
+ // src/git/changes/lib/repo-format.ts
35676
35802
  function posixJoinDirFile(dir, file2) {
35677
35803
  const d = dir === "." || dir === "" ? "" : dir.replace(/\\/g, "/").replace(/\/+$/, "");
35678
35804
  const f = file2.replace(/\\/g, "/").replace(/^\/+/, "");
@@ -35726,63 +35852,80 @@ function formatRemoteDisplayLabel(remoteUrl) {
35726
35852
  return `origin \xB7 ${hostPath}`;
35727
35853
  }
35728
35854
 
35729
- // src/git/working-directory/changes/get-working-tree-change-repo-details.ts
35855
+ // src/git/changes/get-working-tree-change-repo-details.ts
35730
35856
  import * as path24 from "node:path";
35731
35857
 
35732
- // src/git/working-directory/changes/parse-git-status.ts
35733
- function parseNameStatusLines(lines) {
35734
- const m = /* @__PURE__ */ new Map();
35735
- for (const line of lines) {
35736
- if (!line.trim()) continue;
35737
- const tabParts = line.split(" ");
35738
- if (tabParts.length < 2) continue;
35739
- const status = tabParts[0].trim();
35740
- const code = status[0];
35741
- if (code === "A") {
35742
- m.set(tabParts[tabParts.length - 1], "added");
35743
- } else if (code === "D") {
35744
- m.set(tabParts[tabParts.length - 1], "removed");
35745
- } else if (code === "R" || code === "C") {
35746
- if (tabParts.length >= 3) m.set(tabParts[tabParts.length - 1], "modified");
35747
- } else if (code === "M" || code === "U" || code === "T") {
35748
- m.set(tabParts[tabParts.length - 1], "modified");
35749
- }
35858
+ // src/git/commits/lib/parse-log-lines.ts
35859
+ function parseLogShaDateSubjectLines(raw) {
35860
+ const out = [];
35861
+ for (const line of String(raw).split("\n")) {
35862
+ const l = line.trimEnd();
35863
+ if (!l.trim()) continue;
35864
+ const parts = l.split(" ");
35865
+ if (parts.length < 3) continue;
35866
+ const sha = parts[0].trim();
35867
+ const committedAt = parts[1].trim();
35868
+ const subject = parts.slice(2).join(" ").trim();
35869
+ if (!/^[0-9a-f]{7,40}$/i.test(sha)) continue;
35870
+ out.push({ sha, shortSha: sha.slice(0, 7), subject, committedAt });
35750
35871
  }
35751
- return m;
35752
- }
35753
- function parseNumstatFirstLine(line) {
35754
- const parts = line.split(" ");
35755
- if (parts.length < 3) return null;
35756
- const [a, d] = parts;
35757
- const additions = a === "-" ? 0 : parseInt(String(a), 10) || 0;
35758
- const deletions = d === "-" ? 0 : parseInt(String(d), 10) || 0;
35759
- return { additions, deletions };
35872
+ return out;
35760
35873
  }
35761
- function parseNumstat(lines) {
35762
- const m = /* @__PURE__ */ new Map();
35763
- for (const line of lines) {
35764
- if (!line.trim()) continue;
35765
- const parts = line.split(" ");
35766
- if (parts.length < 3) continue;
35767
- const [a, d, p] = parts;
35768
- const additions = a === "-" ? 0 : parseInt(String(a), 10) || 0;
35769
- const deletions = d === "-" ? 0 : parseInt(String(d), 10) || 0;
35770
- m.set(p, { additions, deletions });
35874
+
35875
+ // src/git/commits/list-unpushed-commits.ts
35876
+ async function gitLogNotReachableFromBase(g, baseSha, headSha) {
35877
+ if (baseSha === headSha) return [];
35878
+ try {
35879
+ const logOut = await g.raw(["log", "--format=%H %cI %s", `${baseSha}..${headSha}`]);
35880
+ return parseLogShaDateSubjectLines(logOut);
35881
+ } catch {
35882
+ return [];
35771
35883
  }
35772
- return m;
35773
35884
  }
35774
- async function numstatFromGitNoIndex(g, pathInRepo) {
35775
- const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
35885
+ async function listUnpushedCommits(repoDir) {
35886
+ const g = cliSimpleGit(repoDir);
35887
+ const headSha = await revParseSafe(g, "HEAD");
35888
+ if (!headSha) return [];
35889
+ const baseSha = await resolveBaseShaForUnpushedCommits(g);
35890
+ if (!baseSha) return [];
35891
+ return gitLogNotReachableFromBase(g, baseSha, headSha);
35892
+ }
35893
+
35894
+ // src/git/commits/lib/sanitize-recent-commits-limit.ts
35895
+ var RECENT_COMMITS_LIMIT_MIN = 1;
35896
+ var RECENT_COMMITS_LIMIT_MAX = 50;
35897
+ var RECENT_COMMITS_LIMIT_DEFAULT = 2;
35898
+ function sanitizeRecentCommitsLimit(value) {
35899
+ const n = typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value.trim(), 10) : Number.NaN;
35900
+ if (!Number.isFinite(n)) return RECENT_COMMITS_LIMIT_DEFAULT;
35901
+ return Math.min(RECENT_COMMITS_LIMIT_MAX, Math.max(RECENT_COMMITS_LIMIT_MIN, Math.trunc(n)));
35902
+ }
35903
+
35904
+ // src/git/commits/list-recent-commits.ts
35905
+ async function listRecentCommits(repoDir, limitInput) {
35906
+ const limit = sanitizeRecentCommitsLimit(limitInput);
35907
+ const g = cliSimpleGit(repoDir);
35908
+ const headSha = await revParseSafe(g, "HEAD");
35909
+ if (!headSha) return { commits: [], hasMore: false };
35910
+ const unpushedSet = new Set((await listUnpushedCommits(repoDir)).map((c) => c.sha));
35776
35911
  try {
35777
- const out = await g.raw(["diff", "--numstat", "--no-index", "--", devNull, pathInRepo]);
35778
- const first2 = String(out).split("\n").find((l) => l.trim()) ?? "";
35779
- return parseNumstatFirstLine(first2);
35912
+ const logOut = await g.raw(["log", "--format=%H %cI %s", `-n`, String(limit), "HEAD"]);
35913
+ const commits = parseLogShaDateSubjectLines(logOut).map((c) => ({
35914
+ ...c,
35915
+ needsPush: unpushedSet.has(c.sha)
35916
+ }));
35917
+ let hasMore = false;
35918
+ if (commits.length === limit) {
35919
+ const nextOut = await g.raw(["log", "-n", "1", `--skip=${limit}`, "--format=%H", "HEAD"]);
35920
+ hasMore = /^[0-9a-f]{7,40}$/i.test(String(nextOut).trim());
35921
+ }
35922
+ return { commits, hasMore };
35780
35923
  } catch {
35781
- return null;
35924
+ return { commits: [], hasMore: false };
35782
35925
  }
35783
35926
  }
35784
35927
 
35785
- // src/git/working-directory/changes/patch-truncate.ts
35928
+ // src/git/changes/lib/patch-truncate.ts
35786
35929
  function truncatePatch(s) {
35787
35930
  if (s.length <= MAX_PATCH_CHARS) return s;
35788
35931
  return `${s.slice(0, MAX_PATCH_CHARS)}
@@ -35790,7 +35933,7 @@ function truncatePatch(s) {
35790
35933
  \u2026 (diff truncated)`;
35791
35934
  }
35792
35935
 
35793
- // src/git/working-directory/changes/list-changed-files-for-commit.ts
35936
+ // src/git/commits/list-changed-files-for-commit.ts
35794
35937
  var EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
35795
35938
  async function parentForCommitDiff(g, sha) {
35796
35939
  try {
@@ -35816,7 +35959,7 @@ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
35816
35959
  const paths = new Set([...kindByPath.keys(), ...numByPath.keys()].filter(Boolean));
35817
35960
  const rows = [];
35818
35961
  const normRel = repoRelPath === "." || repoRelPath === "" ? "." : repoRelPath;
35819
- for (const pathInRepo of paths) {
35962
+ await forEachWithGitYield([...paths], async (pathInRepo) => {
35820
35963
  const relLauncher = posixJoinDirFile(normRel, pathInRepo.replace(/\\/g, "/"));
35821
35964
  const nums = numByPath.get(pathInRepo);
35822
35965
  let additions = nums?.additions ?? 0;
@@ -35828,8 +35971,8 @@ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
35828
35971
  else change = "modified";
35829
35972
  }
35830
35973
  rows.push({ pathRelLauncher: relLauncher, additions, deletions, change });
35831
- }
35832
- for (const row of rows) {
35974
+ });
35975
+ await forEachWithGitYield(rows, async (row) => {
35833
35976
  let pathInRepo;
35834
35977
  if (normRel === ".") {
35835
35978
  pathInRepo = row.pathRelLauncher;
@@ -35841,16 +35984,16 @@ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
35841
35984
  const raw = await g.raw(["diff", "-U20000", range, "--", pathInRepo]).catch(() => "");
35842
35985
  const t = String(raw).trim();
35843
35986
  row.patchContent = t ? truncatePatch(t) : void 0;
35844
- }
35987
+ });
35845
35988
  rows.sort((a, b) => a.pathRelLauncher.localeCompare(b.pathRelLauncher));
35846
35989
  return rows;
35847
35990
  }
35848
35991
 
35849
- // src/git/working-directory/changes/list-changed-files-for-repo.ts
35992
+ // src/git/changes/list-changed-files-for-repo.ts
35850
35993
  import * as fs23 from "node:fs";
35851
35994
  import * as path23 from "node:path";
35852
35995
 
35853
- // src/git/working-directory/changes/count-lines.ts
35996
+ // src/git/changes/lib/count-lines.ts
35854
35997
  import { createReadStream } from "node:fs";
35855
35998
  import * as readline2 from "node:readline";
35856
35999
  async function countTextFileLines(filePath) {
@@ -35871,7 +36014,7 @@ async function countTextFileLines(filePath) {
35871
36014
  return lines;
35872
36015
  }
35873
36016
 
35874
- // src/git/working-directory/changes/hydrate-patch.ts
36017
+ // src/git/changes/hydrate-patch.ts
35875
36018
  import * as fs22 from "node:fs";
35876
36019
  var UNIFIED_HUNK_HEADER_RE = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
35877
36020
  var MAX_HYDRATE_LINES_PER_GAP = 8e3;
@@ -35987,26 +36130,16 @@ async function hydrateUnifiedPatchWithFileContext(patch, filePath, repoGitCwd, p
35987
36130
  return truncatePatch(out.join("\n"));
35988
36131
  }
35989
36132
 
35990
- // src/git/working-directory/changes/unified-diff-for-file.ts
36133
+ // src/git/changes/unified-diff-for-file.ts
35991
36134
  async function unifiedDiffForFile(repoCwd, pathInRepo, change) {
35992
36135
  const g = cliSimpleGit(repoCwd);
35993
- try {
35994
- let raw;
35995
- if (change === "added") {
35996
- const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
35997
- raw = await g.raw(["diff", "--no-index", "--", devNull, pathInRepo]);
35998
- } else {
35999
- raw = await g.raw(["diff", "HEAD", "--", pathInRepo]);
36000
- }
36001
- const t = String(raw).trim();
36002
- if (!t) return void 0;
36003
- return truncatePatch(t);
36004
- } catch {
36005
- return void 0;
36006
- }
36136
+ const args = change === "added" ? ["diff", "--no-color", "HEAD", "--", pathInRepo] : change === "removed" ? ["diff", "--no-color", "HEAD", "--", pathInRepo] : ["diff", "--no-color", "HEAD", "--", pathInRepo];
36137
+ const raw = await g.raw([...args]).catch(() => "");
36138
+ const t = String(raw).trim();
36139
+ return t ? truncatePatch(t) : void 0;
36007
36140
  }
36008
36141
 
36009
- // src/git/working-directory/changes/list-changed-files-for-repo.ts
36142
+ // src/git/changes/list-changed-files-for-repo.ts
36010
36143
  async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
36011
36144
  const g = cliSimpleGit(repoGitCwd);
36012
36145
  const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
@@ -36020,7 +36153,7 @@ async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
36020
36153
  const untracked = String(untrackedRaw).split("\n").map((s) => s.trim()).filter(Boolean);
36021
36154
  for (const p of untracked) paths.add(p);
36022
36155
  const rows = [];
36023
- for (const pathInRepo of paths) {
36156
+ await forEachWithGitYield([...paths], async (pathInRepo) => {
36024
36157
  const relLauncher = posixJoinDirFile(repoRelPath, pathInRepo.replace(/\\/g, "/"));
36025
36158
  const repoFilePath = path23.join(repoGitCwd, pathInRepo);
36026
36159
  const nums = numByPath.get(pathInRepo);
@@ -36050,9 +36183,9 @@ async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
36050
36183
  else change = "modified";
36051
36184
  }
36052
36185
  rows.push({ pathRelLauncher: relLauncher, additions, deletions, change });
36053
- }
36186
+ });
36054
36187
  const normRel = repoRelPath === "." || repoRelPath === "" ? "." : repoRelPath;
36055
- for (const row of rows) {
36188
+ await forEachWithGitYield(rows, async (row) => {
36056
36189
  let pathInRepo;
36057
36190
  if (normRel === ".") {
36058
36191
  pathInRepo = row.pathRelLauncher;
@@ -36067,11 +36200,11 @@ async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
36067
36200
  patch = await hydrateUnifiedPatchWithFileContext(patch, filePath, repoGitCwd, pathInRepo, row.change);
36068
36201
  }
36069
36202
  row.patchContent = patch;
36070
- }
36203
+ });
36071
36204
  return rows;
36072
36205
  }
36073
36206
 
36074
- // src/git/working-directory/changes/get-working-tree-change-repo-details.ts
36207
+ // src/git/changes/get-working-tree-change-repo-details.ts
36075
36208
  function normRepoRel(p) {
36076
36209
  const x = p.replace(/\\/g, "/").trim();
36077
36210
  return x === "" ? "." : x;
@@ -36090,7 +36223,9 @@ async function getWorkingTreeChangeRepoDetails(options) {
36090
36223
  throw new Error("commit sha is required for commit changes");
36091
36224
  }
36092
36225
  const basis = filter == null && basisInput.kind === "commit" ? { kind: "working" } : basisInput;
36093
- for (const target of options.commitTargetPaths) {
36226
+ for (let i = 0; i < options.commitTargetPaths.length; i++) {
36227
+ if (i > 0) await yieldToEventLoop2();
36228
+ const target = options.commitTargetPaths[i];
36094
36229
  const t = path24.resolve(target);
36095
36230
  if (!await isGitRepoDirectory(t)) continue;
36096
36231
  const g = cliSimpleGit(t);
@@ -36124,7 +36259,10 @@ async function getWorkingTreeChangeRepoDetails(options) {
36124
36259
  const files = basis.kind === "commit" ? await listChangedFilesForCommit(t, relForList, basis.sha.trim()) : await listChangedFilesForRepo(t, relForList);
36125
36260
  const st = await g.status();
36126
36261
  const hasUncommittedChanges = (st.files?.length ?? 0) > 0;
36127
- const unpushedCommits = await listUnpushedCommits(t);
36262
+ const [unpushedCommits, recentCommitList] = await Promise.all([
36263
+ listUnpushedCommits(t),
36264
+ listRecentCommits(t, options.recentCommitsLimit)
36265
+ ]);
36128
36266
  out.push({
36129
36267
  repoRelPath: norm,
36130
36268
  repoDisplayName,
@@ -36134,6 +36272,8 @@ async function getWorkingTreeChangeRepoDetails(options) {
36134
36272
  files,
36135
36273
  hasUncommittedChanges,
36136
36274
  unpushedCommits,
36275
+ recentCommits: recentCommitList.commits,
36276
+ recentCommitsHasMore: recentCommitList.hasMore,
36137
36277
  changesView: basis.kind === "commit" ? "commit" : "working",
36138
36278
  changesCommitSha: basis.kind === "commit" ? basis.sha.trim() : null
36139
36279
  });
@@ -36142,7 +36282,7 @@ async function getWorkingTreeChangeRepoDetails(options) {
36142
36282
  return out;
36143
36283
  }
36144
36284
 
36145
- // src/git/commit-and-push.ts
36285
+ // src/git/branches/commit-and-push.ts
36146
36286
  async function gitCommitAllIfDirty(repoDir, message, options) {
36147
36287
  const g = cliSimpleGit(repoDir);
36148
36288
  const st = await g.status();
@@ -36566,7 +36706,8 @@ var SessionWorktreeManager = class {
36566
36706
  sessionWorktreeRootPath: sessionWorkingTreeRelRoot,
36567
36707
  legacyRepoNestedSessionLayout: legacyNested,
36568
36708
  repoFilterRelPath: opts?.repoRelPath?.trim() ? opts.repoRelPath.trim() : null,
36569
- basis: opts?.basis
36709
+ basis: opts?.basis,
36710
+ recentCommitsLimit: opts?.recentCommitsLimit
36570
36711
  });
36571
36712
  }
36572
36713
  async pushSessionUpstream(sessionId) {
@@ -37096,7 +37237,7 @@ function pipedStdoutStderrFor(attemptStdio) {
37096
37237
  }
37097
37238
 
37098
37239
  // src/dev-servers/manager/shell-spawn/try-spawn-piped-via-sh.ts
37099
- import { spawn as spawn4 } from "node:child_process";
37240
+ import { spawn as spawn5 } from "node:child_process";
37100
37241
  function trySpawnPipedViaSh(command, env, cwd, signal) {
37101
37242
  const attempts = [
37102
37243
  { stdio: [devNullReadFd(), "pipe", "pipe"], endStdin: false },
@@ -37117,9 +37258,9 @@ function trySpawnPipedViaSh(command, env, cwd, signal) {
37117
37258
  if (process.platform === "win32") {
37118
37259
  opts.windowsHide = true;
37119
37260
  const com = process.env.ComSpec || "cmd.exe";
37120
- proc = spawn4(com, ["/d", "/s", "/c", command], opts);
37261
+ proc = spawn5(com, ["/d", "/s", "/c", command], opts);
37121
37262
  } else {
37122
- proc = spawn4("/bin/sh", ["-c", command], opts);
37263
+ proc = spawn5("/bin/sh", ["-c", command], opts);
37123
37264
  }
37124
37265
  if (attempt.endStdin) {
37125
37266
  proc.stdin?.end();
@@ -37139,7 +37280,7 @@ function trySpawnPipedViaSh(command, env, cwd, signal) {
37139
37280
  }
37140
37281
 
37141
37282
  // src/dev-servers/manager/shell-spawn/try-spawn-shell-true-piped.ts
37142
- import { spawn as spawn5 } from "node:child_process";
37283
+ import { spawn as spawn6 } from "node:child_process";
37143
37284
  function trySpawnShellTruePiped(command, env, cwd, devNullFd, signal) {
37144
37285
  try {
37145
37286
  const opts = {
@@ -37152,7 +37293,7 @@ function trySpawnShellTruePiped(command, env, cwd, devNullFd, signal) {
37152
37293
  if (process.platform === "win32") {
37153
37294
  opts.windowsHide = true;
37154
37295
  }
37155
- return spawn5(command, opts);
37296
+ return spawn6(command, opts);
37156
37297
  } catch (e) {
37157
37298
  if (isSpawnEbadf(e)) return null;
37158
37299
  throw e;
@@ -37160,7 +37301,7 @@ function trySpawnShellTruePiped(command, env, cwd, devNullFd, signal) {
37160
37301
  }
37161
37302
 
37162
37303
  // src/dev-servers/manager/shell-spawn/try-spawn-merged-log-file.ts
37163
- import { spawn as spawn6 } from "node:child_process";
37304
+ import { spawn as spawn7 } from "node:child_process";
37164
37305
  import fs29 from "node:fs";
37165
37306
  import { tmpdir } from "node:os";
37166
37307
  import path32 from "node:path";
@@ -37178,7 +37319,7 @@ function trySpawnMergedLogFile(command, env, cwd, signal) {
37178
37319
  try {
37179
37320
  let proc;
37180
37321
  if (process.platform === "win32") {
37181
- proc = spawn6(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", command], {
37322
+ proc = spawn7(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", command], {
37182
37323
  env,
37183
37324
  cwd,
37184
37325
  stdio,
@@ -37186,7 +37327,7 @@ function trySpawnMergedLogFile(command, env, cwd, signal) {
37186
37327
  ...signal ? { signal } : {}
37187
37328
  });
37188
37329
  } else {
37189
- proc = spawn6("/bin/sh", ["-c", command], { env, cwd, stdio, ...signal ? { signal } : {} });
37330
+ proc = spawn7("/bin/sh", ["-c", command], { env, cwd, stdio, ...signal ? { signal } : {} });
37190
37331
  }
37191
37332
  fs29.closeSync(logFd);
37192
37333
  return {
@@ -37207,7 +37348,7 @@ function trySpawnMergedLogFile(command, env, cwd, signal) {
37207
37348
  }
37208
37349
 
37209
37350
  // src/dev-servers/manager/shell-spawn/try-spawn-shell-script-log-redirect.ts
37210
- import { spawn as spawn7 } from "node:child_process";
37351
+ import { spawn as spawn8 } from "node:child_process";
37211
37352
  import fs30 from "node:fs";
37212
37353
  import { tmpdir as tmpdir2 } from "node:os";
37213
37354
  import path33 from "node:path";
@@ -37230,7 +37371,7 @@ cd ${shSingleQuote(cwd)}
37230
37371
  /bin/sh ${shSingleQuote(innerPath)} >>${shSingleQuote(logPath)} 2>&1
37231
37372
  `
37232
37373
  );
37233
- const proc = spawn7("/bin/sh", [runnerPath], {
37374
+ const proc = spawn8("/bin/sh", [runnerPath], {
37234
37375
  env,
37235
37376
  cwd: tmpRoot,
37236
37377
  stdio: "ignore",
@@ -37262,7 +37403,7 @@ CD /D ${q(cwd)}\r
37262
37403
  ${command} >> ${q(logPath)} 2>&1\r
37263
37404
  `
37264
37405
  );
37265
- const proc = spawn7(com, ["/d", "/s", "/c", q(runnerPath)], {
37406
+ const proc = spawn8(com, ["/d", "/s", "/c", q(runnerPath)], {
37266
37407
  env,
37267
37408
  cwd: tmpRoot,
37268
37409
  stdio: "ignore",
@@ -37283,7 +37424,7 @@ ${command} >> ${q(logPath)} 2>&1\r
37283
37424
  }
37284
37425
 
37285
37426
  // src/dev-servers/manager/shell-spawn/try-spawn-inherit.ts
37286
- import { spawn as spawn8 } from "node:child_process";
37427
+ import { spawn as spawn9 } from "node:child_process";
37287
37428
  function trySpawnInheritStdio(command, env, cwd, signal) {
37288
37429
  const opts = {
37289
37430
  env,
@@ -37295,9 +37436,9 @@ function trySpawnInheritStdio(command, env, cwd, signal) {
37295
37436
  if (process.platform === "win32") {
37296
37437
  opts.windowsHide = true;
37297
37438
  const com = process.env.ComSpec || "cmd.exe";
37298
- proc = spawn8(com, ["/d", "/s", "/c", command], opts);
37439
+ proc = spawn9(com, ["/d", "/s", "/c", command], opts);
37299
37440
  } else {
37300
- proc = spawn8("/bin/sh", ["-c", command], opts);
37441
+ proc = spawn9("/bin/sh", ["-c", command], opts);
37301
37442
  }
37302
37443
  return { proc, pipedStdoutStderr: false };
37303
37444
  }
@@ -38721,12 +38862,12 @@ function createBridgePromptSenders(deps, getWs) {
38721
38862
  }
38722
38863
 
38723
38864
  // src/agents/acp/from-bridge/bridge-prompt-preamble.ts
38724
- import { execFile as execFile9 } from "node:child_process";
38725
- import { promisify as promisify9 } from "node:util";
38726
- var execFileAsync8 = promisify9(execFile9);
38865
+ import { execFile as execFile8 } from "node:child_process";
38866
+ import { promisify as promisify8 } from "node:util";
38867
+ var execFileAsync7 = promisify8(execFile8);
38727
38868
  async function readGitBranch(cwd) {
38728
38869
  try {
38729
- const { stdout } = await execFileAsync8("git", ["branch", "--show-current"], { cwd, maxBuffer: 64 * 1024 });
38870
+ const { stdout } = await execFileAsync7("git", ["branch", "--show-current"], { cwd, maxBuffer: 64 * 1024 });
38730
38871
  const b = stdout.trim();
38731
38872
  return b || null;
38732
38873
  } catch {
@@ -38974,7 +39115,7 @@ var handleSessionRequestResponseMessage = (msg, deps) => {
38974
39115
  };
38975
39116
 
38976
39117
  // src/skills/preview.ts
38977
- import { spawn as spawn9 } from "node:child_process";
39118
+ import { spawn as spawn10 } from "node:child_process";
38978
39119
  var PREVIEW_API_BASE_PATH = "/__preview";
38979
39120
  var PREVIEW_SECRET_HEADER = "X-Preview-Secret";
38980
39121
  var DEFAULT_PORT = 3e3;
@@ -39053,7 +39194,7 @@ var previewSkill = {
39053
39194
  const parts = command.split(/\s+/);
39054
39195
  const exe = parts[0];
39055
39196
  const args = parts.slice(1);
39056
- previewProcess = spawn9(isWindows && exe === "npm" ? "npm.cmd" : exe, args, {
39197
+ previewProcess = spawn10(isWindows && exe === "npm" ? "npm.cmd" : exe, args, {
39057
39198
  cwd: getBridgeRoot(),
39058
39199
  stdio: ["ignore", "pipe", "pipe"],
39059
39200
  env: {
@@ -39610,7 +39751,8 @@ var handleSessionGitRequestMessage = (msg, deps) => {
39610
39751
  reply({
39611
39752
  ok: true,
39612
39753
  hasUncommittedChanges: r.hasUncommittedChanges,
39613
- hasUnpushedCommits: r.hasUnpushedCommits
39754
+ hasUnpushedCommits: r.hasUnpushedCommits,
39755
+ uncommittedFileCount: r.uncommittedFileCount
39614
39756
  });
39615
39757
  return;
39616
39758
  }
@@ -39624,7 +39766,12 @@ var handleSessionGitRequestMessage = (msg, deps) => {
39624
39766
  return;
39625
39767
  }
39626
39768
  }
39627
- const opts = repoRel && view === "commit" && commitSha ? { repoRelPath: repoRel, basis: { kind: "commit", sha: commitSha } } : repoRel ? { repoRelPath: repoRel, basis: { kind: "working" } } : void 0;
39769
+ const recentCommitsLimit = typeof msg.changesRecentCommitsLimit === "number" || typeof msg.changesRecentCommitsLimit === "string" ? msg.changesRecentCommitsLimit : void 0;
39770
+ const opts = repoRel && view === "commit" && commitSha ? {
39771
+ repoRelPath: repoRel,
39772
+ basis: { kind: "commit", sha: commitSha },
39773
+ recentCommitsLimit
39774
+ } : repoRel ? { repoRelPath: repoRel, basis: { kind: "working" }, recentCommitsLimit } : recentCommitsLimit !== void 0 ? { recentCommitsLimit } : void 0;
39628
39775
  const repos = await deps.sessionWorktreeManager.getSessionWorkingTreeChangeDetails(sessionId, opts);
39629
39776
  reply({
39630
39777
  ok: true,
@@ -39642,7 +39789,8 @@ var handleSessionGitRequestMessage = (msg, deps) => {
39642
39789
  reply({
39643
39790
  ok: true,
39644
39791
  hasUncommittedChanges: st2.hasUncommittedChanges,
39645
- hasUnpushedCommits: st2.hasUnpushedCommits
39792
+ hasUnpushedCommits: st2.hasUnpushedCommits,
39793
+ uncommittedFileCount: st2.uncommittedFileCount
39646
39794
  });
39647
39795
  return;
39648
39796
  }
@@ -39667,7 +39815,8 @@ var handleSessionGitRequestMessage = (msg, deps) => {
39667
39815
  reply({
39668
39816
  ok: true,
39669
39817
  hasUncommittedChanges: st.hasUncommittedChanges,
39670
- hasUnpushedCommits: st.hasUnpushedCommits
39818
+ hasUnpushedCommits: st.hasUnpushedCommits,
39819
+ uncommittedFileCount: st.uncommittedFileCount
39671
39820
  });
39672
39821
  } catch (e) {
39673
39822
  reply({ ok: false, error: e instanceof Error ? e.message : String(e) });
@@ -40567,6 +40716,7 @@ async function runConnectedBridge(options, restartWithoutAuth) {
40567
40716
  let bridgeClose = null;
40568
40717
  const onSignal = (kind) => {
40569
40718
  requestCliImmediateShutdown();
40719
+ abortActiveGitChildProcesses();
40570
40720
  cleanupKeyCommand?.();
40571
40721
  logImmediate(
40572
40722
  kind === "interrupt" ? "Keyboard interrupt (Ctrl+C) \u2014 stopping\u2026" : "Stop requested \u2014 shutting down\u2026"