@sideboard-ai/core 0.1.67 → 0.1.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/{agents-TS3EI2YI.js → agents-JBD7IAGK.js} +6 -6
  2. package/dist/{agents-OTBZVTZO.js → agents-PE3RHQS5.js} +6 -6
  3. package/dist/{chunk-K7LM4CLD.js → chunk-47RBLBRK.js} +4 -4
  4. package/dist/{chunk-B4PRRKMA.js → chunk-6Q7H6SMX.js} +4 -4
  5. package/dist/{chunk-FGDJOJ2L.js → chunk-AE5VBOFE.js} +1 -1
  6. package/dist/{chunk-DB3I75FI.js → chunk-BTL7EMGT.js} +4 -4
  7. package/dist/{chunk-4UIYN56C.js → chunk-D4DPQ552.js} +1 -1
  8. package/dist/{chunk-WAFIF22N.js → chunk-DPDGMMGI.js} +2 -2
  9. package/dist/{chunk-4BUCGNZS.js → chunk-EEBDVLDK.js} +22 -20
  10. package/dist/{chunk-57WWNMFP.js → chunk-N5LMEP65.js} +1 -1
  11. package/dist/{chunk-JWZNK3CQ.js → chunk-OICHV4DH.js} +2 -2
  12. package/dist/{chunk-5HTTCYWA.js → chunk-QQC2D4MY.js} +2 -2
  13. package/dist/{chunk-LW5CMQGZ.js → chunk-REKGFVUX.js} +22 -20
  14. package/dist/{chunk-7ZOQY4A7.js → chunk-ROXEENBT.js} +2 -2
  15. package/dist/{chunk-M5V4QIWF.js → chunk-VROPG6QF.js} +1 -1
  16. package/dist/{chunk-ZRFAZVGL.js → chunk-VX6ACNXS.js} +1 -1
  17. package/dist/{connected-teams-RUNMOCG5.js → connected-teams-35EIRJCP.js} +2 -2
  18. package/dist/{connected-teams-OMDHTL6A.js → connected-teams-ZRS53OAT.js} +2 -2
  19. package/dist/{coordinator-prompt-3BSMKZL4.js → coordinator-prompt-HD6IMCKS.js} +3 -3
  20. package/dist/{coordinator-prompt-6HMTOX4G.js → coordinator-prompt-YZYZ5P6P.js} +3 -3
  21. package/dist/{global-workspace-QS7A23CB.js → global-workspace-7P5NG3JZ.js} +4 -4
  22. package/dist/{global-workspace-YZ2NJVLL.js → global-workspace-Z6GF7D2K.js} +4 -4
  23. package/dist/index.cjs +367 -158
  24. package/dist/index.d.cts +27 -1
  25. package/dist/index.d.ts +27 -1
  26. package/dist/index.js +364 -157
  27. package/dist/mcp/run-stdio.cjs +367 -158
  28. package/dist/mcp/run-stdio.js +363 -156
  29. package/dist/{run-Y2MI3EVV.js → run-A4RHY7HH.js} +1 -1
  30. package/dist/{run-JLD2Y52J.js → run-V2WZUMJA.js} +1 -1
  31. package/dist/{workspaces-EBFW7I7S.js → workspaces-BWJNGEE6.js} +5 -5
  32. package/dist/{workspaces-AIUYUXB7.js → workspaces-O6EZGKQK.js} +5 -5
  33. package/dist/{worktree-WMDJW2BL.js → worktree-VDXLUOWR.js} +2 -2
  34. package/dist/{worktree-7NBPIVFZ.js → worktree-ZPSKEZFE.js} +2 -2
  35. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -2295,7 +2295,7 @@ async function git(args, cwd, opts) {
2295
2295
  prefix.push("-c", `${key}=${value}`);
2296
2296
  }
2297
2297
  }
2298
- return run("git", [...prefix, ...args], {
2298
+ return run("git", ["--no-pager", ...prefix, ...args], {
2299
2299
  cwd,
2300
2300
  reject: opts?.reject,
2301
2301
  timeoutMs: opts?.timeoutMs,
@@ -2798,7 +2798,7 @@ async function originGhRepoEnv(cwd) {
2798
2798
  const slug = await resolveGithubRepoSlug(cwd);
2799
2799
  return slug ? { GH_REPO: slug } : {};
2800
2800
  }
2801
- async function resolveDefaultBranch(repoPath) {
2801
+ async function resolveDefaultBranch(repoPath, opts) {
2802
2802
  const viaOrigin = await git(
2803
2803
  ["symbolic-ref", "refs/remotes/origin/HEAD"],
2804
2804
  repoPath,
@@ -2807,22 +2807,24 @@ async function resolveDefaultBranch(repoPath) {
2807
2807
  if (viaOrigin.exitCode === 0 && viaOrigin.stdout.trim()) {
2808
2808
  return viaOrigin.stdout.trim().replace(/^refs\/remotes\/origin\//, "");
2809
2809
  }
2810
- const slug = await resolveGithubRepoSlug(repoPath);
2811
- const viaGh = await gh(
2812
- [
2813
- "repo",
2814
- "view",
2815
- ...slug ? ["--repo", slug] : [],
2816
- "--json",
2817
- "defaultBranchRef",
2818
- "--jq",
2819
- ".defaultBranchRef.name"
2820
- ],
2821
- repoPath,
2822
- { reject: false }
2823
- );
2824
- if (viaGh.exitCode === 0 && viaGh.stdout.trim()) {
2825
- return viaGh.stdout.trim();
2810
+ if (opts?.network !== false) {
2811
+ const slug = await resolveGithubRepoSlug(repoPath);
2812
+ const viaGh = await gh(
2813
+ [
2814
+ "repo",
2815
+ "view",
2816
+ ...slug ? ["--repo", slug] : [],
2817
+ "--json",
2818
+ "defaultBranchRef",
2819
+ "--jq",
2820
+ ".defaultBranchRef.name"
2821
+ ],
2822
+ repoPath,
2823
+ { reject: false, timeoutMs: 8e3 }
2824
+ );
2825
+ if (viaGh.exitCode === 0 && viaGh.stdout.trim()) {
2826
+ return viaGh.stdout.trim();
2827
+ }
2826
2828
  }
2827
2829
  for (const candidate of ["main", "master"]) {
2828
2830
  const check = await git(
@@ -3474,11 +3476,11 @@ async function listWorktrees(repoPath) {
3474
3476
  return entries;
3475
3477
  }
3476
3478
  async function isDirty(worktreePath) {
3477
- const { stdout } = await git(["status", "--porcelain", "-uall"], worktreePath);
3479
+ const { stdout } = await git(["status", "--porcelain"], worktreePath);
3478
3480
  for (const line of stdout.split("\n")) {
3479
3481
  if (!line.trim()) continue;
3480
3482
  const rel = porcelainStatusPath(line);
3481
- if (!rel || isSideboardScratchPath(rel)) continue;
3483
+ if (!rel || isSideboardScratchPath(rel) || rel === ".sideboard") continue;
3482
3484
  return true;
3483
3485
  }
3484
3486
  return false;
@@ -9181,24 +9183,25 @@ function capPatch(patch, maxHunk) {
9181
9183
 
9182
9184
  \u2026 truncated (${patch.length - maxHunk} more chars)`;
9183
9185
  }
9186
+ var mergeBaseCache = /* @__PURE__ */ new Map();
9187
+ var MERGE_BASE_TTL_MS = 45e3;
9184
9188
  async function resolveMergeBase(worktreePath, base, repoPath) {
9185
9189
  const baseRef = await resolveDiffBaseRef(worktreePath, base, repoPath);
9190
+ const cacheKey = `${worktreePath}\0${baseRef}`;
9191
+ const hit = mergeBaseCache.get(cacheKey);
9192
+ if (hit && Date.now() - hit.at < MERGE_BASE_TTL_MS) return hit.value;
9186
9193
  const { stdout } = await git(["merge-base", baseRef, "HEAD"], worktreePath, {
9187
9194
  reject: false
9188
9195
  });
9189
- const mb = stdout.trim();
9190
- return mb || baseRef;
9196
+ const mb = stdout.trim() || baseRef;
9197
+ mergeBaseCache.set(cacheKey, { at: Date.now(), value: mb });
9198
+ return mb;
9191
9199
  }
9192
9200
  async function countUnpushedCommits(worktreePath) {
9193
9201
  const head = await git(["rev-parse", "--abbrev-ref", "HEAD"], worktreePath, {
9194
9202
  reject: false
9195
9203
  });
9196
9204
  const branch = head.stdout.trim();
9197
- if (branch && branch !== "HEAD") {
9198
- await git(["fetch", "origin", branch, "--quiet"], worktreePath, {
9199
- reject: false
9200
- });
9201
- }
9202
9205
  const upstream = await git(
9203
9206
  ["rev-list", "--count", "@{upstream}..HEAD"],
9204
9207
  worktreePath,
@@ -9231,11 +9234,37 @@ function splitCombinedDiff(stdout) {
9231
9234
  }
9232
9235
  return map;
9233
9236
  }
9237
+ function syntheticAddPatch(path, content, maxHunk) {
9238
+ const lines = content.split("\n");
9239
+ const body = lines.map((l) => `+${l}`).join("\n");
9240
+ const header = `diff --git a/${path} b/${path}
9241
+ new file mode 100644
9242
+ --- /dev/null
9243
+ +++ b/${path}
9244
+ @@ -0,0 +1,${lines.length} @@
9245
+ `;
9246
+ return {
9247
+ path,
9248
+ status: "A",
9249
+ patch: capPatch(`${header}${body}`, maxHunk),
9250
+ additions: lines.length,
9251
+ deletions: 0
9252
+ };
9253
+ }
9234
9254
  async function untrackedPatch(worktreePath, path, maxHunk) {
9255
+ const abs = (0, import_node_path17.join)(worktreePath, path);
9256
+ try {
9257
+ const st = (0, import_node_fs20.statSync)(abs);
9258
+ if (st.isFile() && st.size > maxHunk) {
9259
+ const buf = (0, import_node_fs20.readFileSync)(abs).subarray(0, maxHunk);
9260
+ return syntheticAddPatch(path, buf.toString("utf8"), maxHunk);
9261
+ }
9262
+ } catch {
9263
+ }
9235
9264
  const { stdout: patch } = await git(
9236
- ["diff", "--no-index", "--", "/dev/null", path],
9265
+ ["diff", "--no-ext-diff", "--no-color", "--no-index", "--", "/dev/null", path],
9237
9266
  worktreePath,
9238
- { reject: false }
9267
+ { reject: false, timeoutMs: 4e3 }
9239
9268
  );
9240
9269
  const normalized = patch.replace(/^diff --git a\/.+? b\/.+?$/m, `diff --git a/${path} b/${path}`).replace(/^--- .*$/m, "--- /dev/null").replace(/^\+\+\+ .*$/m, `+++ b/${path}`);
9241
9270
  const addLines = normalized.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
@@ -9247,22 +9276,97 @@ async function untrackedPatch(worktreePath, path, maxHunk) {
9247
9276
  deletions: 0
9248
9277
  };
9249
9278
  }
9250
- async function listUntrackedPaths(worktreePath) {
9251
- const { stdout } = await git(
9252
- ["ls-files", "-z", "--others", "--exclude-standard"],
9253
- worktreePath,
9254
- { reject: false }
9255
- );
9279
+ async function listUntrackedPaths(worktreePath, path) {
9280
+ const args = ["ls-files", "-z", "--others", "--exclude-standard"];
9281
+ if (path) args.push("--", path);
9282
+ const { stdout } = await git(args, worktreePath, { reject: false });
9256
9283
  return stdout.split("\0").filter(Boolean).filter((p) => !isSideboardScratchPath(p));
9257
9284
  }
9258
- async function listUntrackedFiles(worktreePath, maxHunk) {
9259
- const paths = await listUntrackedPaths(worktreePath);
9260
- if (paths.length === 0) return [];
9261
- return Promise.all(paths.map((path) => untrackedPatch(worktreePath, path, maxHunk)));
9285
+ async function listUntrackedFiles(worktreePath, maxHunk, paths) {
9286
+ const list = paths ?? await listUntrackedPaths(worktreePath);
9287
+ if (list.length === 0) return [];
9288
+ return Promise.all(list.map((path) => untrackedPatch(worktreePath, path, maxHunk)));
9289
+ }
9290
+ function untrackedStubs(paths) {
9291
+ return paths.map((path) => ({
9292
+ path,
9293
+ status: "A",
9294
+ patch: "",
9295
+ additions: 0,
9296
+ deletions: 0
9297
+ }));
9298
+ }
9299
+ function pathspec(path) {
9300
+ return path ? ["--", path] : [];
9301
+ }
9302
+ function gitDiff(args, cwd, opts) {
9303
+ return git(["diff", "--no-ext-diff", "--no-color", ...args], cwd, {
9304
+ reject: false,
9305
+ timeoutMs: opts?.timeoutMs
9306
+ });
9307
+ }
9308
+ function fileFromPatch(path, patch, maxHunk) {
9309
+ let status = "M";
9310
+ if (/^new file mode /m.test(patch) || /^--- \/dev\/null/m.test(patch)) {
9311
+ status = "A";
9312
+ } else if (/^deleted file mode /m.test(patch) || /^\+\+\+ \/dev\/null/m.test(patch)) {
9313
+ status = "D";
9314
+ } else if (/^rename from /m.test(patch)) {
9315
+ status = "R";
9316
+ }
9317
+ const additions = patch.split("\n").filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
9318
+ const deletions = patch.split("\n").filter((l) => l.startsWith("-") && !l.startsWith("---")).length;
9319
+ return {
9320
+ path,
9321
+ status,
9322
+ patch: capPatch(patch, maxHunk),
9323
+ additions,
9324
+ deletions
9325
+ };
9326
+ }
9327
+ async function gitDiffParts(worktreePath, diffArgs, includePatches, path) {
9328
+ const extra = pathspec(path);
9329
+ if (includePatches) {
9330
+ const diff = await gitDiff([...diffArgs, ...extra], worktreePath, {
9331
+ timeoutMs: 4e3
9332
+ });
9333
+ return { nameStatus: "", numstat: "", combinedDiff: diff.stdout };
9334
+ }
9335
+ const [ns, num2] = await Promise.all([
9336
+ gitDiff(["--name-status", ...diffArgs, ...extra], worktreePath),
9337
+ gitDiff(["--numstat", ...diffArgs, ...extra], worktreePath)
9338
+ ]);
9339
+ return {
9340
+ nameStatus: ns.stdout,
9341
+ numstat: num2.stdout,
9342
+ combinedDiff: ""
9343
+ };
9344
+ }
9345
+ function formatStat(files) {
9346
+ if (files.length === 0) return "(no changes)";
9347
+ const additions = files.reduce((n2, f) => n2 + (f.additions ?? 0), 0);
9348
+ const deletions = files.reduce((n2, f) => n2 + (f.deletions ?? 0), 0);
9349
+ const n = files.length;
9350
+ return `${n} file${n === 1 ? "" : "s"} changed, ${additions} insertions(+), ${deletions} deletions(-)`;
9351
+ }
9352
+ function emptyScopeStats() {
9353
+ const z2 = emptyStat();
9354
+ return {
9355
+ commits: z2,
9356
+ uncommitted: z2,
9357
+ staged: z2,
9358
+ unstaged: z2,
9359
+ last_turn: z2
9360
+ };
9262
9361
  }
9263
9362
  function filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk) {
9264
- const counts = parseNumstat(numstat);
9265
9363
  const patches = splitCombinedDiff(combinedDiff);
9364
+ if (!nameStatus.trim() && patches.size > 0) {
9365
+ return [...patches.entries()].map(
9366
+ ([path, patch]) => fileFromPatch(path, patch, maxHunk)
9367
+ );
9368
+ }
9369
+ const counts = parseNumstat(numstat);
9266
9370
  const files = [];
9267
9371
  for (const { status, path } of parseNameStatus(nameStatus)) {
9268
9372
  const n = counts.get(path);
@@ -9284,11 +9388,11 @@ async function computeScopeStats(worktreePath, mergeBase, lastTurnBase, untracke
9284
9388
  { stdout: unstagedNum },
9285
9389
  lastTurnNum
9286
9390
  ] = await Promise.all([
9287
- mergeBase ? git(["diff", "--numstat", mergeBase], worktreePath, { reject: false }) : Promise.resolve({ stdout: "" }),
9288
- git(["diff", "--numstat", "HEAD"], worktreePath, { reject: false }),
9289
- git(["diff", "--numstat", "--cached"], worktreePath, { reject: false }),
9290
- git(["diff", "--numstat"], worktreePath, { reject: false }),
9291
- lastTurnBase ? git(["diff", "--numstat", lastTurnBase], worktreePath, { reject: false }) : Promise.resolve({ stdout: "" })
9391
+ mergeBase ? gitDiff(["--numstat", mergeBase], worktreePath) : Promise.resolve({ stdout: "" }),
9392
+ gitDiff(["--numstat", "HEAD"], worktreePath),
9393
+ gitDiff(["--numstat", "--cached"], worktreePath),
9394
+ gitDiff(["--numstat"], worktreePath),
9395
+ lastTurnBase ? gitDiff(["--numstat", lastTurnBase], worktreePath) : Promise.resolve({ stdout: "" })
9292
9396
  ]);
9293
9397
  return {
9294
9398
  commits: mergeBase ? statFromNumstat(commitsNum, untrackedCount) : emptyStat(),
@@ -9307,7 +9411,7 @@ async function listBranchCommits(worktreePath, repoPath, opts) {
9307
9411
  if (head.exitCode !== 0) return [];
9308
9412
  let base = opts?.base;
9309
9413
  try {
9310
- base = base ?? await resolveDefaultBranch(repoPath);
9414
+ base = base ?? await resolveDefaultBranch(repoPath, { network: false });
9311
9415
  } catch {
9312
9416
  base = "HEAD";
9313
9417
  }
@@ -9337,7 +9441,121 @@ async function listBranchCommits(worktreePath, repoPath, opts) {
9337
9441
  }
9338
9442
  return commits;
9339
9443
  }
9444
+ var SHA_RE = /^[0-9a-f]{7,40}$/i;
9445
+ function emptyPathDiff(scope, base, hasLastTurnBase, commitSha = null) {
9446
+ return {
9447
+ scope,
9448
+ commitSha: scope === "commits" ? commitSha : null,
9449
+ base,
9450
+ files: [],
9451
+ stat: formatStat([]),
9452
+ dirty: false,
9453
+ unpushed: 0,
9454
+ hasLastTurnBase,
9455
+ commits: [],
9456
+ scopeStats: emptyScopeStats()
9457
+ };
9458
+ }
9459
+ async function getPathDiff(worktreePath, repoPath, opts) {
9460
+ const { path, scope, commitSha, lastTurnBase, hasLastTurnBase, includePatches, maxHunk } = opts;
9461
+ if (scope === "last_turn" && !lastTurnBase) {
9462
+ return emptyPathDiff(scope, "last turn", false);
9463
+ }
9464
+ let diffArgs;
9465
+ let labelBase;
9466
+ if (scope === "staged") {
9467
+ diffArgs = ["--cached"];
9468
+ labelBase = "staged";
9469
+ } else if (scope === "unstaged") {
9470
+ diffArgs = [];
9471
+ labelBase = "unstaged";
9472
+ } else if (scope === "uncommitted") {
9473
+ diffArgs = ["HEAD"];
9474
+ labelBase = "HEAD";
9475
+ } else if (scope === "last_turn") {
9476
+ diffArgs = [lastTurnBase];
9477
+ labelBase = "last turn";
9478
+ } else if (scope === "commits" && commitSha) {
9479
+ diffArgs = [`${commitSha}^!`];
9480
+ labelBase = commitSha.slice(0, 7);
9481
+ } else if (scope === "commits" && opts.base && SHA_RE.test(opts.base)) {
9482
+ diffArgs = [opts.base];
9483
+ labelBase = opts.base.slice(0, 7);
9484
+ } else if (scope === "commits") {
9485
+ let base = opts.base;
9486
+ try {
9487
+ base = base ?? await resolveDefaultBranch(repoPath, { network: false });
9488
+ } catch {
9489
+ base = "HEAD";
9490
+ }
9491
+ const mergeBase = await resolveMergeBase(worktreePath, base, repoPath);
9492
+ diffArgs = [mergeBase];
9493
+ labelBase = mergeBase;
9494
+ } else {
9495
+ diffArgs = ["HEAD"];
9496
+ labelBase = "HEAD";
9497
+ }
9498
+ const diff = await gitDiff(
9499
+ includePatches ? [...diffArgs, "--", path] : ["--name-status", ...diffArgs, "--", path],
9500
+ worktreePath,
9501
+ { timeoutMs: 4e3 }
9502
+ );
9503
+ const filesMap = /* @__PURE__ */ new Map();
9504
+ if (includePatches) {
9505
+ const parsed = filesFromDiff("", "", diff.stdout, maxHunk);
9506
+ for (const file of parsed) filesMap.set(file.path, file);
9507
+ if (diff.stdout.trim() && !filesMap.has(path)) {
9508
+ filesMap.set(path, fileFromPatch(path, diff.stdout, maxHunk));
9509
+ }
9510
+ } else {
9511
+ for (const file of filesFromDiff(diff.stdout, "", "", maxHunk)) {
9512
+ filesMap.set(file.path, file);
9513
+ }
9514
+ }
9515
+ if (filesMap.size === 0) {
9516
+ const maybe = await listUntrackedPaths(worktreePath, path);
9517
+ if (maybe.length > 0) {
9518
+ const extra = includePatches ? await listUntrackedFiles(worktreePath, maxHunk, maybe) : untrackedStubs(maybe);
9519
+ for (const file of extra) filesMap.set(file.path, file);
9520
+ }
9521
+ }
9522
+ const files = [...filesMap.values()].sort((a, b) => a.path.localeCompare(b.path));
9523
+ return {
9524
+ scope,
9525
+ commitSha: scope === "commits" ? commitSha : null,
9526
+ base: labelBase,
9527
+ files,
9528
+ stat: formatStat(files),
9529
+ dirty: files.length > 0,
9530
+ unpushed: 0,
9531
+ hasLastTurnBase,
9532
+ commits: [],
9533
+ scopeStats: emptyScopeStats(),
9534
+ mergeBase: SHA_RE.test(diffArgs[0] ?? "") ? diffArgs[0] : null
9535
+ };
9536
+ }
9340
9537
  async function getDiff(worktreePath, repoPath, opts) {
9538
+ const maxHunk = opts?.maxHunkChars ?? 8e3;
9539
+ const scope = opts?.scope ?? "commits";
9540
+ const lastTurnBase = opts?.lastTurnBase?.trim() || null;
9541
+ const hasLastTurnBase = Boolean(lastTurnBase);
9542
+ const commitSha = opts?.commitSha?.trim() || null;
9543
+ const includePatches = opts?.includePatches ?? true;
9544
+ const pathFilter = opts?.path?.trim() || null;
9545
+ const fileOnly = Boolean(pathFilter);
9546
+ const includeMeta = (opts?.includeMeta ?? true) && !fileOnly;
9547
+ if (fileOnly && pathFilter && !includeMeta) {
9548
+ return getPathDiff(worktreePath, repoPath, {
9549
+ path: pathFilter,
9550
+ scope,
9551
+ commitSha,
9552
+ lastTurnBase,
9553
+ hasLastTurnBase,
9554
+ base: opts?.base,
9555
+ includePatches,
9556
+ maxHunk
9557
+ });
9558
+ }
9341
9559
  const status = await inspectGitWorktree(worktreePath);
9342
9560
  if (status === "missing_worktree") {
9343
9561
  throw new Error("Worktree not found");
@@ -9345,18 +9563,19 @@ async function getDiff(worktreePath, repoPath, opts) {
9345
9563
  if (status === "not_git") {
9346
9564
  throw new Error("Not a Git repository");
9347
9565
  }
9348
- const maxHunk = opts?.maxHunkChars ?? 8e3;
9349
- const scope = opts?.scope ?? "commits";
9350
- const lastTurnBase = opts?.lastTurnBase?.trim() || null;
9351
- const hasLastTurnBase = Boolean(lastTurnBase);
9352
- const commitSha = opts?.commitSha?.trim() || null;
9353
- const head = await git(["rev-parse", "--verify", "HEAD"], worktreePath, {
9566
+ const wantUntracked = opts?.includeUntracked !== void 0 ? opts.includeUntracked : !fileOnly && (scope === "commits" && !commitSha || scope === "uncommitted" || scope === "unstaged" || scope === "last_turn");
9567
+ const needsMergeBase = scope === "commits" && !commitSha || includeMeta;
9568
+ const headP = git(["rev-parse", "--verify", "HEAD"], worktreePath, {
9354
9569
  reject: false
9355
9570
  });
9571
+ const untrackedP = wantUntracked ? listUntrackedPaths(worktreePath, pathFilter) : Promise.resolve([]);
9572
+ const head = await headP;
9356
9573
  const hasHead = head.exitCode === 0;
9357
- const untrackedPaths = await listUntrackedPaths(worktreePath);
9358
9574
  if (!hasHead) {
9359
- const files2 = scope === "staged" ? [] : (await listUntrackedFiles(worktreePath, maxHunk)).sort(
9575
+ const untrackedPaths2 = await untrackedP;
9576
+ const files2 = scope === "staged" ? [] : includePatches ? (await listUntrackedFiles(worktreePath, maxHunk, untrackedPaths2)).sort(
9577
+ (a, b) => a.path.localeCompare(b.path)
9578
+ ) : untrackedStubs(untrackedPaths2).sort(
9360
9579
  (a, b) => a.path.localeCompare(b.path)
9361
9580
  );
9362
9581
  const untrackedStat = {
@@ -9370,7 +9589,7 @@ async function getDiff(worktreePath, repoPath, opts) {
9370
9589
  commitSha: null,
9371
9590
  base: "(no commits)",
9372
9591
  files: files2,
9373
- stat: files2.length ? files2.map((f) => ` ${f.path} | untracked`).join("\n") : "(no changes)",
9592
+ stat: formatStat(files2),
9374
9593
  dirty: files2.length > 0,
9375
9594
  unpushed: 0,
9376
9595
  hasLastTurnBase,
@@ -9384,137 +9603,122 @@ async function getDiff(worktreePath, repoPath, opts) {
9384
9603
  }
9385
9604
  };
9386
9605
  }
9387
- let base = opts?.base;
9388
- try {
9389
- base = base ?? await resolveDefaultBranch(repoPath);
9390
- } catch {
9391
- base = "HEAD";
9606
+ let mergeBase = "HEAD";
9607
+ let baseLabel = "HEAD";
9608
+ if (needsMergeBase) {
9609
+ let base = opts?.base;
9610
+ try {
9611
+ base = base ?? await resolveDefaultBranch(repoPath, { network: false });
9612
+ } catch {
9613
+ base = "HEAD";
9614
+ }
9615
+ mergeBase = await resolveMergeBase(worktreePath, base, repoPath);
9616
+ baseLabel = await resolveDiffBaseRef(worktreePath, base, repoPath);
9392
9617
  }
9393
- base = await resolveDiffBaseRef(worktreePath, base, repoPath);
9394
- if (base.startsWith("origin/")) {
9395
- const short = base.slice("origin/".length);
9396
- await git(["fetch", "origin", short, "--quiet"], worktreePath, {
9397
- reject: false
9398
- });
9399
- base = await resolveDiffBaseRef(worktreePath, short, repoPath);
9618
+ const includeUntracked = wantUntracked;
9619
+ const metaPromise = includeMeta ? Promise.all([
9620
+ computeScopeStats(
9621
+ worktreePath,
9622
+ mergeBase,
9623
+ lastTurnBase,
9624
+ // Cheap: don't wait on the untracked walk just to pad the badge.
9625
+ 0
9626
+ ),
9627
+ listBranchCommits(worktreePath, repoPath, { base: baseLabel }),
9628
+ isDirty(worktreePath),
9629
+ countUnpushedCommits(worktreePath)
9630
+ ]).then(([scopeStats, commits, dirty, unpushed]) => ({
9631
+ scopeStats,
9632
+ commits,
9633
+ dirty,
9634
+ unpushed
9635
+ })) : Promise.resolve({
9636
+ scopeStats: emptyScopeStats(),
9637
+ commits: [],
9638
+ dirty: false,
9639
+ unpushed: 0
9640
+ });
9641
+ if (scope === "last_turn" && !lastTurnBase) {
9642
+ const meta2 = await metaPromise;
9643
+ return {
9644
+ scope,
9645
+ commitSha: null,
9646
+ base: "last turn",
9647
+ files: [],
9648
+ stat: "(no last agent turn)",
9649
+ dirty: meta2.dirty,
9650
+ unpushed: meta2.unpushed,
9651
+ hasLastTurnBase: false,
9652
+ commits: meta2.commits,
9653
+ scopeStats: meta2.scopeStats
9654
+ };
9400
9655
  }
9401
- const mergeBase = await resolveMergeBase(worktreePath, base, repoPath);
9402
- const scopeStats = await computeScopeStats(
9403
- worktreePath,
9404
- mergeBase,
9405
- lastTurnBase,
9406
- untrackedPaths.length
9407
- );
9408
- const includeUntracked = scope === "commits" && !commitSha || scope === "uncommitted" || scope === "unstaged" || scope === "last_turn";
9409
- let nameStatus = "";
9410
- let numstat = "";
9411
- let combinedDiff = "";
9412
- let labelBase = base;
9656
+ let diffArgs;
9657
+ let labelBase = baseLabel;
9413
9658
  if (scope === "staged") {
9414
- const [ns, num2, diff] = await Promise.all([
9415
- git(["diff", "--name-status", "--cached"], worktreePath, { reject: false }),
9416
- git(["diff", "--numstat", "--cached"], worktreePath, { reject: false }),
9417
- git(["diff", "--cached"], worktreePath, { reject: false })
9418
- ]);
9419
- nameStatus = ns.stdout;
9420
- numstat = num2.stdout;
9421
- combinedDiff = diff.stdout;
9659
+ diffArgs = ["--cached"];
9422
9660
  labelBase = "staged";
9423
9661
  } else if (scope === "unstaged") {
9424
- const [ns, num2, diff] = await Promise.all([
9425
- git(["diff", "--name-status"], worktreePath, { reject: false }),
9426
- git(["diff", "--numstat"], worktreePath, { reject: false }),
9427
- git(["diff"], worktreePath, { reject: false })
9428
- ]);
9429
- nameStatus = ns.stdout;
9430
- numstat = num2.stdout;
9431
- combinedDiff = diff.stdout;
9662
+ diffArgs = [];
9432
9663
  labelBase = "unstaged";
9433
9664
  } else if (scope === "uncommitted") {
9434
- const [ns, num2, diff] = await Promise.all([
9435
- git(["diff", "--name-status", "HEAD"], worktreePath, { reject: false }),
9436
- git(["diff", "--numstat", "HEAD"], worktreePath, { reject: false }),
9437
- git(["diff", "HEAD"], worktreePath, { reject: false })
9438
- ]);
9439
- nameStatus = ns.stdout;
9440
- numstat = num2.stdout;
9441
- combinedDiff = diff.stdout;
9665
+ diffArgs = ["HEAD"];
9442
9666
  labelBase = "HEAD";
9443
9667
  } else if (scope === "last_turn") {
9444
- if (!lastTurnBase) {
9445
- const commits2 = await listBranchCommits(worktreePath, repoPath, { base });
9446
- return {
9447
- scope,
9448
- commitSha: null,
9449
- base: "last turn",
9450
- files: [],
9451
- stat: "(no last agent turn)",
9452
- dirty: await isDirty(worktreePath),
9453
- unpushed: await countUnpushedCommits(worktreePath),
9454
- hasLastTurnBase: false,
9455
- commits: commits2,
9456
- scopeStats
9457
- };
9458
- }
9459
- const [ns, num2, diff] = await Promise.all([
9460
- git(["diff", "--name-status", lastTurnBase], worktreePath, { reject: false }),
9461
- git(["diff", "--numstat", lastTurnBase], worktreePath, { reject: false }),
9462
- git(["diff", lastTurnBase], worktreePath, { reject: false })
9463
- ]);
9464
- nameStatus = ns.stdout;
9465
- numstat = num2.stdout;
9466
- combinedDiff = diff.stdout;
9668
+ diffArgs = [lastTurnBase];
9467
9669
  labelBase = "last turn";
9468
9670
  } else if (scope === "commits" && commitSha) {
9469
- const range = `${commitSha}^!`;
9470
- const [ns, num2, diff] = await Promise.all([
9471
- git(["diff", "--name-status", range], worktreePath, { reject: false }),
9472
- git(["diff", "--numstat", range], worktreePath, { reject: false }),
9473
- git(["diff", range], worktreePath, { reject: false })
9474
- ]);
9475
- nameStatus = ns.stdout;
9476
- numstat = num2.stdout;
9477
- combinedDiff = diff.stdout;
9671
+ diffArgs = [`${commitSha}^!`];
9478
9672
  labelBase = commitSha.slice(0, 7);
9479
9673
  } else {
9480
- const [ns, num2, diff] = await Promise.all([
9481
- git(["diff", "--name-status", mergeBase], worktreePath, { reject: false }),
9482
- git(["diff", "--numstat", mergeBase], worktreePath, { reject: false }),
9483
- git(["diff", mergeBase], worktreePath, { reject: false })
9484
- ]);
9485
- nameStatus = ns.stdout;
9486
- numstat = num2.stdout;
9487
- combinedDiff = diff.stdout;
9488
- labelBase = base;
9674
+ diffArgs = [mergeBase];
9675
+ labelBase = baseLabel;
9489
9676
  }
9490
- const commits = await listBranchCommits(worktreePath, repoPath, { base });
9677
+ const partsPromise = gitDiffParts(
9678
+ worktreePath,
9679
+ diffArgs,
9680
+ includePatches,
9681
+ pathFilter
9682
+ );
9683
+ const [parts, meta, untrackedPaths] = await Promise.all([
9684
+ partsPromise,
9685
+ metaPromise,
9686
+ untrackedP
9687
+ ]);
9491
9688
  const filesMap = /* @__PURE__ */ new Map();
9492
- for (const file of filesFromDiff(nameStatus, numstat, combinedDiff, maxHunk)) {
9689
+ for (const file of filesFromDiff(
9690
+ parts.nameStatus,
9691
+ parts.numstat,
9692
+ parts.combinedDiff,
9693
+ maxHunk
9694
+ )) {
9493
9695
  filesMap.set(file.path, file);
9494
9696
  }
9495
9697
  if (includeUntracked) {
9496
- const untrackedFiles = await listUntrackedFiles(worktreePath, maxHunk);
9497
- for (const file of untrackedFiles) {
9698
+ const extra = includePatches ? await listUntrackedFiles(worktreePath, maxHunk, untrackedPaths) : untrackedStubs(untrackedPaths);
9699
+ for (const file of extra) {
9498
9700
  if (!filesMap.has(file.path)) filesMap.set(file.path, file);
9499
9701
  }
9702
+ } else if (fileOnly && pathFilter && !filesMap.has(pathFilter)) {
9703
+ const maybe = await listUntrackedPaths(worktreePath, pathFilter);
9704
+ if (maybe.length > 0) {
9705
+ const extra = includePatches ? await listUntrackedFiles(worktreePath, maxHunk, maybe) : untrackedStubs(maybe);
9706
+ for (const file of extra) filesMap.set(file.path, file);
9707
+ }
9500
9708
  }
9501
9709
  const files = [...filesMap.values()].sort((a, b) => a.path.localeCompare(b.path));
9502
- const { stdout: statOut } = await git(
9503
- scope === "staged" ? ["diff", "--stat", "--cached"] : scope === "unstaged" ? ["diff", "--stat"] : scope === "uncommitted" ? ["diff", "--stat", "HEAD"] : scope === "last_turn" && lastTurnBase ? ["diff", "--stat", lastTurnBase] : scope === "commits" && commitSha ? ["diff", "--stat", `${commitSha}^!`] : ["diff", "--stat", mergeBase],
9504
- worktreePath,
9505
- { reject: false }
9506
- );
9507
9710
  return {
9508
9711
  scope,
9509
9712
  commitSha: scope === "commits" ? commitSha : null,
9510
9713
  base: labelBase,
9511
9714
  files,
9512
- stat: statOut.trim() || (files.length ? `${files.length} file(s)` : "(no changes)"),
9513
- dirty: await isDirty(worktreePath),
9514
- unpushed: await countUnpushedCommits(worktreePath),
9715
+ stat: formatStat(files),
9716
+ dirty: fileOnly || !includeMeta ? files.length > 0 : meta.dirty,
9717
+ unpushed: meta.unpushed,
9515
9718
  hasLastTurnBase,
9516
- commits,
9517
- scopeStats
9719
+ commits: meta.commits,
9720
+ scopeStats: meta.scopeStats,
9721
+ mergeBase: needsMergeBase ? mergeBase : null
9518
9722
  };
9519
9723
  }
9520
9724
  async function listWorktreeFiles(worktreePath, opts) {
@@ -13210,6 +13414,11 @@ var Orchestrator = class {
13210
13414
  return getDiff(thread.worktreePath, thread.repoPath, {
13211
13415
  scope: opts?.scope,
13212
13416
  commitSha: opts?.commitSha,
13417
+ base: opts?.base,
13418
+ includePatches: opts?.includePatches,
13419
+ includeMeta: opts?.includeMeta,
13420
+ includeUntracked: opts?.includeUntracked,
13421
+ path: opts?.path,
13213
13422
  lastTurnBase: this.turnBaselines.get(thread.id) ?? null
13214
13423
  });
13215
13424
  }