@buildautomaton/cli 0.1.49 → 0.1.51

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
@@ -4175,8 +4175,8 @@ var init_parseUtil = __esm({
4175
4175
  init_errors();
4176
4176
  init_en();
4177
4177
  makeIssue = (params) => {
4178
- const { data, path: path46, errorMaps, issueData } = params;
4179
- const fullPath = [...path46, ...issueData.path || []];
4178
+ const { data, path: path60, errorMaps, issueData } = params;
4179
+ const fullPath = [...path60, ...issueData.path || []];
4180
4180
  const fullIssue = {
4181
4181
  ...issueData,
4182
4182
  path: fullPath
@@ -4484,11 +4484,11 @@ var init_types = __esm({
4484
4484
  init_parseUtil();
4485
4485
  init_util();
4486
4486
  ParseInputLazyPath = class {
4487
- constructor(parent, value, path46, key) {
4487
+ constructor(parent, value, path60, key) {
4488
4488
  this._cachedPath = [];
4489
4489
  this.parent = parent;
4490
4490
  this.data = value;
4491
- this._path = path46;
4491
+ this._path = path60;
4492
4492
  this._key = key;
4493
4493
  }
4494
4494
  get path() {
@@ -8103,15 +8103,15 @@ function assignProp(target, prop, value) {
8103
8103
  configurable: true
8104
8104
  });
8105
8105
  }
8106
- function getElementAtPath(obj, path46) {
8107
- if (!path46)
8106
+ function getElementAtPath(obj, path60) {
8107
+ if (!path60)
8108
8108
  return obj;
8109
- return path46.reduce((acc, key) => acc?.[key], obj);
8109
+ return path60.reduce((acc, key) => acc?.[key], obj);
8110
8110
  }
8111
8111
  function promiseAllObject(promisesObj) {
8112
8112
  const keys = Object.keys(promisesObj);
8113
- const promises6 = keys.map((key) => promisesObj[key]);
8114
- return Promise.all(promises6).then((results) => {
8113
+ const promises11 = keys.map((key) => promisesObj[key]);
8114
+ return Promise.all(promises11).then((results) => {
8115
8115
  const resolvedObj = {};
8116
8116
  for (let i = 0; i < keys.length; i++) {
8117
8117
  resolvedObj[keys[i]] = results[i];
@@ -8355,11 +8355,11 @@ function aborted(x, startIndex = 0) {
8355
8355
  }
8356
8356
  return false;
8357
8357
  }
8358
- function prefixIssues(path46, issues) {
8358
+ function prefixIssues(path60, issues) {
8359
8359
  return issues.map((iss) => {
8360
8360
  var _a2;
8361
8361
  (_a2 = iss).path ?? (_a2.path = []);
8362
- iss.path.unshift(path46);
8362
+ iss.path.unshift(path60);
8363
8363
  return iss;
8364
8364
  });
8365
8365
  }
@@ -8548,7 +8548,7 @@ function treeifyError(error40, _mapper) {
8548
8548
  return issue2.message;
8549
8549
  };
8550
8550
  const result = { errors: [] };
8551
- const processError = (error41, path46 = []) => {
8551
+ const processError = (error41, path60 = []) => {
8552
8552
  var _a2, _b;
8553
8553
  for (const issue2 of error41.issues) {
8554
8554
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -8558,7 +8558,7 @@ function treeifyError(error40, _mapper) {
8558
8558
  } else if (issue2.code === "invalid_element") {
8559
8559
  processError({ issues: issue2.issues }, issue2.path);
8560
8560
  } else {
8561
- const fullpath = [...path46, ...issue2.path];
8561
+ const fullpath = [...path60, ...issue2.path];
8562
8562
  if (fullpath.length === 0) {
8563
8563
  result.errors.push(mapper(issue2));
8564
8564
  continue;
@@ -8588,9 +8588,9 @@ function treeifyError(error40, _mapper) {
8588
8588
  processError(error40);
8589
8589
  return result;
8590
8590
  }
8591
- function toDotPath(path46) {
8591
+ function toDotPath(path60) {
8592
8592
  const segs = [];
8593
- for (const seg of path46) {
8593
+ for (const seg of path60) {
8594
8594
  if (typeof seg === "number")
8595
8595
  segs.push(`[${seg}]`);
8596
8596
  else if (typeof seg === "symbol")
@@ -21053,8 +21053,8 @@ var init_acp = __esm({
21053
21053
  this.#requestHandler = requestHandler;
21054
21054
  this.#notificationHandler = notificationHandler;
21055
21055
  this.#stream = stream;
21056
- this.#closedPromise = new Promise((resolve20) => {
21057
- this.#abortController.signal.addEventListener("abort", () => resolve20());
21056
+ this.#closedPromise = new Promise((resolve32) => {
21057
+ this.#abortController.signal.addEventListener("abort", () => resolve32());
21058
21058
  });
21059
21059
  this.#receive();
21060
21060
  }
@@ -21203,8 +21203,8 @@ var init_acp = __esm({
21203
21203
  }
21204
21204
  async sendRequest(method, params) {
21205
21205
  const id = this.#nextRequestId++;
21206
- const responsePromise = new Promise((resolve20, reject) => {
21207
- this.#pendingResponses.set(id, { resolve: resolve20, reject });
21206
+ const responsePromise = new Promise((resolve32, reject) => {
21207
+ this.#pendingResponses.set(id, { resolve: resolve32, reject });
21208
21208
  });
21209
21209
  await this.#sendMessage({ jsonrpc: "2.0", id, method, params });
21210
21210
  return responsePromise;
@@ -22073,10 +22073,10 @@ var require_src2 = __commonJS({
22073
22073
  var fs_1 = __require("fs");
22074
22074
  var debug_1 = __importDefault(require_src());
22075
22075
  var log2 = debug_1.default("@kwsites/file-exists");
22076
- function check2(path46, isFile, isDirectory) {
22077
- log2(`checking %s`, path46);
22076
+ function check2(path60, isFile, isDirectory) {
22077
+ log2(`checking %s`, path60);
22078
22078
  try {
22079
- const stat2 = fs_1.statSync(path46);
22079
+ const stat2 = fs_1.statSync(path60);
22080
22080
  if (stat2.isFile() && isFile) {
22081
22081
  log2(`[OK] path represents a file`);
22082
22082
  return true;
@@ -22096,8 +22096,8 @@ var require_src2 = __commonJS({
22096
22096
  throw e;
22097
22097
  }
22098
22098
  }
22099
- function exists2(path46, type = exports.READABLE) {
22100
- return check2(path46, (type & exports.FILE) > 0, (type & exports.FOLDER) > 0);
22099
+ function exists2(path60, type = exports.READABLE) {
22100
+ return check2(path60, (type & exports.FILE) > 0, (type & exports.FOLDER) > 0);
22101
22101
  }
22102
22102
  exports.exists = exists2;
22103
22103
  exports.FILE = 1;
@@ -22446,10 +22446,10 @@ async function killChildProcessTreeGracefully(child, graceMs = ACP_PROCESS_TREE_
22446
22446
  }
22447
22447
  await killProcessTree(pid, "SIGTERM");
22448
22448
  if (graceMs <= 0) return;
22449
- const exited = new Promise((resolve20) => {
22450
- child.once("exit", () => resolve20());
22449
+ const exited = new Promise((resolve32) => {
22450
+ child.once("exit", () => resolve32());
22451
22451
  });
22452
- await Promise.race([exited, new Promise((resolve20) => setTimeout(resolve20, graceMs))]);
22452
+ await Promise.race([exited, new Promise((resolve32) => setTimeout(resolve32, graceMs))]);
22453
22453
  if (child.exitCode == null && child.signalCode == null) {
22454
22454
  await killProcessTree(pid, "SIGKILL");
22455
22455
  }
@@ -22466,8 +22466,8 @@ async function gracefulAcpSubprocessDisconnect(params) {
22466
22466
  await transport.closeSession?.(sessionId);
22467
22467
  } catch {
22468
22468
  }
22469
- await new Promise((resolve20) => {
22470
- const timer = setTimeout(resolve20, ACP_GRACEFUL_SESSION_CLOSE_WAIT_MS);
22469
+ await new Promise((resolve32) => {
22470
+ const timer = setTimeout(resolve32, ACP_GRACEFUL_SESSION_CLOSE_WAIT_MS);
22471
22471
  timer.unref?.();
22472
22472
  });
22473
22473
  }
@@ -23172,9 +23172,9 @@ function parseChangeSummaryJson(raw, allowedPaths, options) {
23172
23172
  const rawPath = typeof o.path === "string" ? o.path.trim() : "";
23173
23173
  const summary = typeof o.summary === "string" ? o.summary.trim() : "";
23174
23174
  if (!rawPath || !summary) continue;
23175
- const path46 = skip ? normalizeRepoRelativePath(rawPath) || rawPath : resolveChangeSummaryPathAgainstAllowed(rawPath, allowedPaths);
23176
- if (!path46) continue;
23177
- rows.push({ path: path46, summary: clampSummaryToAtMostTwoLines(summary) });
23175
+ const path60 = skip ? normalizeRepoRelativePath(rawPath) || rawPath : resolveChangeSummaryPathAgainstAllowed(rawPath, allowedPaths);
23176
+ if (!path60) continue;
23177
+ rows.push({ path: path60, summary: clampSummaryToAtMostTwoLines(summary) });
23178
23178
  }
23179
23179
  return rows;
23180
23180
  }
@@ -23392,7 +23392,7 @@ var GitRepoMetaSchema = external_exports.object({
23392
23392
  updatedAt: external_exports.string()
23393
23393
  });
23394
23394
 
23395
- // ../types/src/bridge-mcp-tool-display.ts
23395
+ // ../types/src/mcp/bridge-mcp-constants.ts
23396
23396
  var GET_INTEGRATION_CONTENT_TOOL = "get_integration_content";
23397
23397
  var LIST_PARENT_SESSION_PROMPTS_TOOL = "list_parent_session_prompts";
23398
23398
  var GET_PARENT_SESSION_TURN_TRANSCRIPT_TOOL = "get_parent_session_turn_transcript";
@@ -23628,11 +23628,11 @@ function createSdkStdioExtNotificationHandler(options) {
23628
23628
  // src/agents/acp/clients/sdk/sdk-stdio-permission-request-handshake.ts
23629
23629
  function awaitSdkStdioPermissionRequestHandshake(params) {
23630
23630
  const { requestId, paramsRecord, pending, onRequest } = params;
23631
- return new Promise((resolve20) => {
23632
- pending.set(requestId, { resolve: resolve20, params: paramsRecord });
23631
+ return new Promise((resolve32) => {
23632
+ pending.set(requestId, { resolve: resolve32, params: paramsRecord });
23633
23633
  if (onRequest == null) {
23634
23634
  pending.delete(requestId);
23635
- resolve20({ outcome: { outcome: "denied" } });
23635
+ resolve32({ outcome: { outcome: "denied" } });
23636
23636
  return;
23637
23637
  }
23638
23638
  try {
@@ -24176,10 +24176,10 @@ function createSdkStdioInitSettle(child) {
24176
24176
  forceAcpSubprocessDisconnect(child);
24177
24177
  reject(err);
24178
24178
  },
24179
- settleResolve(resolve20, handle) {
24179
+ settleResolve(resolve32, handle) {
24180
24180
  if (initSettled) return;
24181
24181
  initSettled = true;
24182
- resolve20(handle);
24182
+ resolve32(handle);
24183
24183
  }
24184
24184
  };
24185
24185
  }
@@ -24248,7 +24248,7 @@ async function createSdkStdioAcpClient(options) {
24248
24248
  onFileChange,
24249
24249
  stderrCapture
24250
24250
  });
24251
- return new Promise((resolve20, reject) => {
24251
+ return new Promise((resolve32, reject) => {
24252
24252
  const init = createSdkStdioInitSettle(child);
24253
24253
  child.on("error", (err) => {
24254
24254
  init.settleReject(reject, new Error(formatSpawnError(err, command[0])));
@@ -24273,7 +24273,7 @@ async function createSdkStdioAcpClient(options) {
24273
24273
  protocolVersion: PROTOCOL_VERSION2
24274
24274
  });
24275
24275
  init.settleResolve(
24276
- resolve20,
24276
+ resolve32,
24277
24277
  createSdkStdioHandle({
24278
24278
  child,
24279
24279
  sessionId: established.sessionId,
@@ -24381,7 +24381,7 @@ async function proxyToLocal(request) {
24381
24381
  };
24382
24382
  const maxAttempts = isIdempotentProxyMethod(request.method) ? LOCAL_PREVIEW_FETCH_RETRY_DELAYS_MS.length + 1 : 1;
24383
24383
  for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
24384
- const once = await new Promise((resolve20) => {
24384
+ const once = await new Promise((resolve32) => {
24385
24385
  const req = mod.request(opts, (res) => {
24386
24386
  const chunks = [];
24387
24387
  res.on("data", (c) => chunks.push(c));
@@ -24392,7 +24392,7 @@ async function proxyToLocal(request) {
24392
24392
  if (typeof v === "string") headers[k] = v;
24393
24393
  else if (Array.isArray(v) && v[0]) headers[k] = v[0];
24394
24394
  }
24395
- resolve20({
24395
+ resolve32({
24396
24396
  id: request.id,
24397
24397
  statusCode: res.statusCode ?? 0,
24398
24398
  headers,
@@ -24401,7 +24401,7 @@ async function proxyToLocal(request) {
24401
24401
  });
24402
24402
  });
24403
24403
  req.on("error", (err) => {
24404
- resolve20({
24404
+ resolve32({
24405
24405
  id: request.id,
24406
24406
  statusCode: 0,
24407
24407
  headers: {},
@@ -24483,8 +24483,8 @@ function randomSecret() {
24483
24483
  }
24484
24484
  return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
24485
24485
  }
24486
- async function requestPreviewApi(port, secret, method, path46, body) {
24487
- const url2 = `http://127.0.0.1:${port}${path46}`;
24486
+ async function requestPreviewApi(port, secret, method, path60, body) {
24487
+ const url2 = `http://127.0.0.1:${port}${path60}`;
24488
24488
  const headers = {
24489
24489
  [PREVIEW_SECRET_HEADER]: secret,
24490
24490
  "Content-Type": "application/json"
@@ -24496,7 +24496,7 @@ async function requestPreviewApi(port, secret, method, path46, body) {
24496
24496
  });
24497
24497
  const data = await res.json().catch(() => ({}));
24498
24498
  if (!res.ok) {
24499
- throw new Error(data?.error ?? `Preview API ${method} ${path46}: ${res.status}`);
24499
+ throw new Error(data?.error ?? `Preview API ${method} ${path60}: ${res.status}`);
24500
24500
  }
24501
24501
  return data;
24502
24502
  }
@@ -24711,7 +24711,7 @@ function installBridgeProcessResilience() {
24711
24711
  }
24712
24712
 
24713
24713
  // src/cli-version.ts
24714
- var CLI_VERSION = "0.1.49".length > 0 ? "0.1.49" : "0.0.0-dev";
24714
+ var CLI_VERSION = "0.1.51".length > 0 ? "0.1.51" : "0.0.0-dev";
24715
24715
 
24716
24716
  // src/connection/heartbeat/constants.ts
24717
24717
  var BRIDGE_APP_HEARTBEAT_INTERVAL_MS = 1e4;
@@ -25152,14 +25152,14 @@ var baseOpen = async (options) => {
25152
25152
  }
25153
25153
  const subprocess = childProcess.spawn(command, cliArguments, childProcessOptions);
25154
25154
  if (options.wait) {
25155
- return new Promise((resolve20, reject) => {
25155
+ return new Promise((resolve32, reject) => {
25156
25156
  subprocess.once("error", reject);
25157
25157
  subprocess.once("close", (exitCode) => {
25158
25158
  if (!options.allowNonzeroExitCode && exitCode > 0) {
25159
25159
  reject(new Error(`Exited with code ${exitCode}`));
25160
25160
  return;
25161
25161
  }
25162
- resolve20(subprocess);
25162
+ resolve32(subprocess);
25163
25163
  });
25164
25164
  });
25165
25165
  }
@@ -25703,8 +25703,8 @@ function runPendingAuth(options) {
25703
25703
  let hasOpenedBrowser = false;
25704
25704
  let resolved = false;
25705
25705
  let resolveAuth;
25706
- const authPromise = new Promise((resolve20) => {
25707
- resolveAuth = resolve20;
25706
+ const authPromise = new Promise((resolve32) => {
25707
+ resolveAuth = resolve32;
25708
25708
  });
25709
25709
  let reconnectAttempt = 0;
25710
25710
  const signInQuiet = createEmptyReconnectQuietSlot();
@@ -25855,7 +25855,7 @@ import sqliteWasm from "node-sqlite3-wasm";
25855
25855
 
25856
25856
  // src/runtime/yield-to-event-loop.ts
25857
25857
  function yieldToEventLoop() {
25858
- return new Promise((resolve20) => setImmediate(resolve20));
25858
+ return new Promise((resolve32) => setImmediate(resolve32));
25859
25859
  }
25860
25860
 
25861
25861
  // src/sqlite/cli-sqlite-paths.ts
@@ -27019,7 +27019,7 @@ async function createCursorAcpClient(options) {
27019
27019
  onFileChange,
27020
27020
  stderrCapture
27021
27021
  });
27022
- return new Promise((resolve20, reject) => {
27022
+ return new Promise((resolve32, reject) => {
27023
27023
  child.on("error", (err) => {
27024
27024
  killChildProcessTree(child, "SIGKILL");
27025
27025
  reject(new Error(formatSpawnError(err, command[0])));
@@ -27031,7 +27031,7 @@ async function createCursorAcpClient(options) {
27031
27031
  sessionCtx,
27032
27032
  incomingDeps: { dbgFs, sessionCtx, onSessionUpdate, onRequest }
27033
27033
  });
27034
- resolve20(
27034
+ resolve32(
27035
27035
  createCursorAcpHandle({
27036
27036
  child,
27037
27037
  sessionId: established.sessionId,
@@ -27969,7 +27969,7 @@ function computeAcpAgentKey(preferredAgentType, mode, agentConfig) {
27969
27969
  }
27970
27970
 
27971
27971
  // src/mcp/bridge-mcp-constants.ts
27972
- var BRIDGE_MCP_SERVER_NAME = "buildautomaton-bridge";
27972
+ var BRIDGE_MCP_SERVER_NAME2 = "buildautomaton-bridge";
27973
27973
 
27974
27974
  // src/mcp/resolve-bridge-mcp-server-path.ts
27975
27975
  import { dirname as dirname4, join as join2 } from "node:path";
@@ -27984,7 +27984,7 @@ function buildAcpMcpServers(bridgeAccessPort) {
27984
27984
  if (!Number.isFinite(bridgeAccessPort) || bridgeAccessPort <= 0) return [];
27985
27985
  return [
27986
27986
  {
27987
- name: BRIDGE_MCP_SERVER_NAME,
27987
+ name: BRIDGE_MCP_SERVER_NAME2,
27988
27988
  command: process.execPath,
27989
27989
  args: [resolveBridgeMcpServerScriptPath(), String(bridgeAccessPort)],
27990
27990
  env: []
@@ -28237,8 +28237,8 @@ function pathspec(...paths) {
28237
28237
  cache.set(key, paths);
28238
28238
  return key;
28239
28239
  }
28240
- function isPathSpec(path46) {
28241
- return path46 instanceof String && cache.has(path46);
28240
+ function isPathSpec(path60) {
28241
+ return path60 instanceof String && cache.has(path60);
28242
28242
  }
28243
28243
  function toPaths(pathSpec) {
28244
28244
  return cache.get(pathSpec) || [];
@@ -28327,8 +28327,8 @@ function toLinesWithContent(input = "", trimmed2 = true, separator = "\n") {
28327
28327
  function forEachLineWithContent(input, callback) {
28328
28328
  return toLinesWithContent(input, true).map((line) => callback(line));
28329
28329
  }
28330
- function folderExists(path46) {
28331
- return (0, import_file_exists.exists)(path46, import_file_exists.FOLDER);
28330
+ function folderExists(path60) {
28331
+ return (0, import_file_exists.exists)(path60, import_file_exists.FOLDER);
28332
28332
  }
28333
28333
  function append(target, item) {
28334
28334
  if (Array.isArray(target)) {
@@ -28732,8 +28732,8 @@ function checkIsRepoRootTask() {
28732
28732
  commands,
28733
28733
  format: "utf-8",
28734
28734
  onError,
28735
- parser(path46) {
28736
- return /^\.(git)?$/.test(path46.trim());
28735
+ parser(path60) {
28736
+ return /^\.(git)?$/.test(path60.trim());
28737
28737
  }
28738
28738
  };
28739
28739
  }
@@ -29167,11 +29167,11 @@ function parseGrep(grep) {
29167
29167
  const paths = /* @__PURE__ */ new Set();
29168
29168
  const results = {};
29169
29169
  forEachLineWithContent(grep, (input) => {
29170
- const [path46, line, preview] = input.split(NULL);
29171
- paths.add(path46);
29172
- (results[path46] = results[path46] || []).push({
29170
+ const [path60, line, preview] = input.split(NULL);
29171
+ paths.add(path60);
29172
+ (results[path60] = results[path60] || []).push({
29173
29173
  line: asNumber(line),
29174
- path: path46,
29174
+ path: path60,
29175
29175
  preview
29176
29176
  });
29177
29177
  });
@@ -29936,14 +29936,14 @@ var init_hash_object = __esm2({
29936
29936
  init_task();
29937
29937
  }
29938
29938
  });
29939
- function parseInit(bare, path46, text) {
29939
+ function parseInit(bare, path60, text) {
29940
29940
  const response = String(text).trim();
29941
29941
  let result;
29942
29942
  if (result = initResponseRegex.exec(response)) {
29943
- return new InitSummary(bare, path46, false, result[1]);
29943
+ return new InitSummary(bare, path60, false, result[1]);
29944
29944
  }
29945
29945
  if (result = reInitResponseRegex.exec(response)) {
29946
- return new InitSummary(bare, path46, true, result[1]);
29946
+ return new InitSummary(bare, path60, true, result[1]);
29947
29947
  }
29948
29948
  let gitDir = "";
29949
29949
  const tokens = response.split(" ");
@@ -29954,7 +29954,7 @@ function parseInit(bare, path46, text) {
29954
29954
  break;
29955
29955
  }
29956
29956
  }
29957
- return new InitSummary(bare, path46, /^re/i.test(response), gitDir);
29957
+ return new InitSummary(bare, path60, /^re/i.test(response), gitDir);
29958
29958
  }
29959
29959
  var InitSummary;
29960
29960
  var initResponseRegex;
@@ -29963,9 +29963,9 @@ var init_InitSummary = __esm2({
29963
29963
  "src/lib/responses/InitSummary.ts"() {
29964
29964
  "use strict";
29965
29965
  InitSummary = class {
29966
- constructor(bare, path46, existing, gitDir) {
29966
+ constructor(bare, path60, existing, gitDir) {
29967
29967
  this.bare = bare;
29968
- this.path = path46;
29968
+ this.path = path60;
29969
29969
  this.existing = existing;
29970
29970
  this.gitDir = gitDir;
29971
29971
  }
@@ -29977,7 +29977,7 @@ var init_InitSummary = __esm2({
29977
29977
  function hasBareCommand(command) {
29978
29978
  return command.includes(bareCommand);
29979
29979
  }
29980
- function initTask(bare = false, path46, customArgs) {
29980
+ function initTask(bare = false, path60, customArgs) {
29981
29981
  const commands = ["init", ...customArgs];
29982
29982
  if (bare && !hasBareCommand(commands)) {
29983
29983
  commands.splice(1, 0, bareCommand);
@@ -29986,7 +29986,7 @@ function initTask(bare = false, path46, customArgs) {
29986
29986
  commands,
29987
29987
  format: "utf-8",
29988
29988
  parser(text) {
29989
- return parseInit(commands.includes("--bare"), path46, text);
29989
+ return parseInit(commands.includes("--bare"), path60, text);
29990
29990
  }
29991
29991
  };
29992
29992
  }
@@ -30802,12 +30802,12 @@ var init_FileStatusSummary = __esm2({
30802
30802
  "use strict";
30803
30803
  fromPathRegex = /^(.+)\0(.+)$/;
30804
30804
  FileStatusSummary = class {
30805
- constructor(path46, index, working_dir) {
30806
- this.path = path46;
30805
+ constructor(path60, index, working_dir) {
30806
+ this.path = path60;
30807
30807
  this.index = index;
30808
30808
  this.working_dir = working_dir;
30809
30809
  if (index === "R" || working_dir === "R") {
30810
- const detail = fromPathRegex.exec(path46) || [null, path46, path46];
30810
+ const detail = fromPathRegex.exec(path60) || [null, path60, path60];
30811
30811
  this.from = detail[2] || "";
30812
30812
  this.path = detail[1] || "";
30813
30813
  }
@@ -30838,14 +30838,14 @@ function splitLine(result, lineStr) {
30838
30838
  default:
30839
30839
  return;
30840
30840
  }
30841
- function data(index, workingDir, path46) {
30841
+ function data(index, workingDir, path60) {
30842
30842
  const raw = `${index}${workingDir}`;
30843
30843
  const handler = parsers6.get(raw);
30844
30844
  if (handler) {
30845
- handler(result, path46);
30845
+ handler(result, path60);
30846
30846
  }
30847
30847
  if (raw !== "##" && raw !== "!!") {
30848
- result.files.push(new FileStatusSummary(path46, index, workingDir));
30848
+ result.files.push(new FileStatusSummary(path60, index, workingDir));
30849
30849
  }
30850
30850
  }
30851
30851
  }
@@ -31154,9 +31154,9 @@ var init_simple_git_api = __esm2({
31154
31154
  next
31155
31155
  );
31156
31156
  }
31157
- hashObject(path46, write) {
31157
+ hashObject(path60, write) {
31158
31158
  return this._runTask(
31159
- hashObjectTask(path46, write === true),
31159
+ hashObjectTask(path60, write === true),
31160
31160
  trailingFunctionArgument(arguments)
31161
31161
  );
31162
31162
  }
@@ -31509,8 +31509,8 @@ var init_branch = __esm2({
31509
31509
  }
31510
31510
  });
31511
31511
  function toPath(input) {
31512
- const path46 = input.trim().replace(/^["']|["']$/g, "");
31513
- return path46 && normalize3(path46);
31512
+ const path60 = input.trim().replace(/^["']|["']$/g, "");
31513
+ return path60 && normalize3(path60);
31514
31514
  }
31515
31515
  var parseCheckIgnore;
31516
31516
  var init_CheckIgnore = __esm2({
@@ -31824,8 +31824,8 @@ __export2(sub_module_exports, {
31824
31824
  subModuleTask: () => subModuleTask,
31825
31825
  updateSubModuleTask: () => updateSubModuleTask
31826
31826
  });
31827
- function addSubModuleTask(repo, path46) {
31828
- return subModuleTask(["add", repo, path46]);
31827
+ function addSubModuleTask(repo, path60) {
31828
+ return subModuleTask(["add", repo, path60]);
31829
31829
  }
31830
31830
  function initSubModuleTask(customArgs) {
31831
31831
  return subModuleTask(["init", ...customArgs]);
@@ -32158,8 +32158,8 @@ var require_git = __commonJS2({
32158
32158
  }
32159
32159
  return this._runTask(straightThroughStringTask2(command, this._trimmed), next);
32160
32160
  };
32161
- Git2.prototype.submoduleAdd = function(repo, path46, then) {
32162
- return this._runTask(addSubModuleTask2(repo, path46), trailingFunctionArgument2(arguments));
32161
+ Git2.prototype.submoduleAdd = function(repo, path60, then) {
32162
+ return this._runTask(addSubModuleTask2(repo, path60), trailingFunctionArgument2(arguments));
32163
32163
  };
32164
32164
  Git2.prototype.submoduleUpdate = function(args, then) {
32165
32165
  return this._runTask(
@@ -32796,14 +32796,14 @@ async function runGitTask(fn) {
32796
32796
  }
32797
32797
  async function yieldToEventLoop2() {
32798
32798
  throwIfGitShutdownRequested();
32799
- await new Promise((resolve20) => {
32800
- setImmediate(resolve20);
32799
+ await new Promise((resolve32) => {
32800
+ setImmediate(resolve32);
32801
32801
  });
32802
32802
  throwIfGitShutdownRequested();
32803
32803
  }
32804
32804
  async function forEachWithGitYield(items, fn) {
32805
32805
  for (let i = 0; i < items.length; i++) {
32806
- if (i > 0 || items.length > 1) await yieldToEventLoop2();
32806
+ await yieldToEventLoop2();
32807
32807
  await fn(items[i], i);
32808
32808
  }
32809
32809
  }
@@ -32837,11 +32837,8 @@ function cliSimpleGit(baseDir) {
32837
32837
  async function getRemoteOriginUrl(gitDir) {
32838
32838
  try {
32839
32839
  const git = cliSimpleGit(gitDir);
32840
- const remotes = await git.getRemotes(true);
32841
- const list = Array.isArray(remotes) ? remotes : [];
32842
- const origin = list.find((r) => r.name === "origin");
32843
- if (!origin?.refs?.fetch && !origin?.refs?.push) return "";
32844
- return (origin.refs.fetch ?? origin.refs.push ?? "").trim();
32840
+ const out = await git.raw(["config", "--get", "remote.origin.url"]);
32841
+ return String(out).trim();
32845
32842
  } catch {
32846
32843
  return "";
32847
32844
  }
@@ -32857,6 +32854,12 @@ async function isGitRepoDirectory(dirPath) {
32857
32854
  }
32858
32855
 
32859
32856
  // src/git/discover-repos.ts
32857
+ var GIT_DISCOVER_YIELD_EVERY = 32;
32858
+ async function yieldGitDiscoverWork(entryCount) {
32859
+ if (entryCount > 0 && entryCount % GIT_DISCOVER_YIELD_EVERY === 0) {
32860
+ await yieldToEventLoop();
32861
+ }
32862
+ }
32860
32863
  async function discoverGitRepos(cwd = getBridgeRoot()) {
32861
32864
  const result = [];
32862
32865
  const cwdResolved = path16.resolve(cwd);
@@ -32870,7 +32873,9 @@ async function discoverGitRepos(cwd = getBridgeRoot()) {
32870
32873
  } catch {
32871
32874
  return result;
32872
32875
  }
32873
- for (const ent of entries) {
32876
+ for (let i = 0; i < entries.length; i++) {
32877
+ await yieldGitDiscoverWork(i + 1);
32878
+ const ent = entries[i];
32874
32879
  if (!ent.isDirectory()) continue;
32875
32880
  const childPath = path16.join(cwdResolved, ent.name);
32876
32881
  if (await isGitRepoDirectory(childPath)) {
@@ -32883,6 +32888,7 @@ async function discoverGitRepos(cwd = getBridgeRoot()) {
32883
32888
  async function discoverGitReposUnderRoot(rootPath) {
32884
32889
  const root = path16.resolve(rootPath);
32885
32890
  const roots = [];
32891
+ let walkEntries = 0;
32886
32892
  async function walk(dir) {
32887
32893
  if (await isGitRepoDirectory(dir)) {
32888
32894
  roots.push(path16.resolve(dir));
@@ -32894,7 +32900,9 @@ async function discoverGitReposUnderRoot(rootPath) {
32894
32900
  } catch {
32895
32901
  return;
32896
32902
  }
32897
- for (const ent of entries) {
32903
+ for (let i = 0; i < entries.length; i++) {
32904
+ await yieldGitDiscoverWork(++walkEntries);
32905
+ const ent = entries[i];
32898
32906
  if (!ent.isDirectory() || ent.name === ".git") continue;
32899
32907
  await walk(path16.join(dir, ent.name));
32900
32908
  }
@@ -32902,7 +32910,9 @@ async function discoverGitReposUnderRoot(rootPath) {
32902
32910
  await walk(root);
32903
32911
  const uniq = [...new Set(roots)];
32904
32912
  const out = [];
32905
- for (const p of uniq) {
32913
+ for (let i = 0; i < uniq.length; i++) {
32914
+ await yieldGitDiscoverWork(i + 1);
32915
+ const p = uniq[i];
32906
32916
  const remoteUrl = await getRemoteOriginUrl(p);
32907
32917
  out.push({ absolutePath: p, remoteUrl });
32908
32918
  }
@@ -33122,9 +33132,9 @@ async function collectTurnGitDiffFromPreTurnSnapshot(options) {
33122
33132
  // src/agents/acp/put-summarize-change-summaries.ts
33123
33133
  async function putEncryptedChangeSummaryRows(params) {
33124
33134
  const base = params.apiBaseUrl.replace(/\/+$/, "");
33125
- const entries = params.rows.map(({ path: path46, summary }) => {
33135
+ const entries = params.rows.map(({ path: path60, summary }) => {
33126
33136
  const enc = params.e2ee.encryptFields({ summary }, ["summary"]);
33127
- return { path: path46, summary: JSON.stringify(enc) };
33137
+ return { path: path60, summary: JSON.stringify(enc) };
33128
33138
  });
33129
33139
  const res = await fetch(
33130
33140
  `${base}/internal/sessions/${encodeURIComponent(params.sessionId)}/follow-ups/summarize-changes`,
@@ -33570,7 +33580,7 @@ function buildIntegrationContentAgentPromptNote(sessionId) {
33570
33580
  "Use the get_integration_content MCP tool on the buildautomaton-bridge server to fetch the full content of each link.",
33571
33581
  `When calling the tool, pass sessionId "${sessionRef}" and the URL as url.`,
33572
33582
  "By default the tool links the fetched URL as this session external ticket when the session has none yet. Pass associateAsSessionTicket: false when fetching additional links for context only (after the primary ticket is linked, or when you do not want association).",
33573
- "If the tool reports unsupported_url, do not retry with the same URL. If it reports temporarily_unavailable, you may retry later."
33583
+ "If the tool reports unsupported_url, do not retry with the same URL. If it reports integration_reconnect_required, ask the user to reconnect the integration in workspace settings. If it reports temporarily_unavailable, you may retry later."
33574
33584
  ].join("\n");
33575
33585
  }
33576
33586
  function injectIntegrationContentAgentPromptNote(agentPrompt, sessionId) {
@@ -33605,7 +33615,7 @@ async function sendPromptToAgent(options) {
33605
33615
  runId,
33606
33616
  agentType,
33607
33617
  agentCwd,
33608
- sendResult: sendResult2,
33618
+ sendResult,
33609
33619
  sendSessionUpdate,
33610
33620
  log: log2,
33611
33621
  followUpCatalogPromptId,
@@ -33635,7 +33645,7 @@ async function sendPromptToAgent(options) {
33635
33645
  let sendOpts = {};
33636
33646
  if (attachments && attachments.length > 0) {
33637
33647
  if (!sessionId || !cloudApiBaseUrl || !getCloudAccessToken) {
33638
- sendResult2({
33648
+ sendResult({
33639
33649
  type: "prompt_result",
33640
33650
  id: promptId,
33641
33651
  ...sessionId ? { sessionId } : {},
@@ -33656,7 +33666,7 @@ async function sendPromptToAgent(options) {
33656
33666
  log: log2
33657
33667
  });
33658
33668
  if (!resolved.ok) {
33659
- sendResult2({
33669
+ sendResult({
33660
33670
  type: "prompt_result",
33661
33671
  id: promptId,
33662
33672
  ...sessionId ? { sessionId } : {},
@@ -33695,7 +33705,7 @@ async function sendPromptToAgent(options) {
33695
33705
  const errStr = typeof result.error === "string" ? result.error : void 0;
33696
33706
  const resultStop = typeof result.stopReason === "string" ? result.stopReason.trim() : "";
33697
33707
  const cancelledByAgent = resultStop.toLowerCase() === "cancelled" || errStr != null && isUserEndedSessionTurnErrorText(errStr);
33698
- sendResult2({
33708
+ sendResult({
33699
33709
  type: "prompt_result",
33700
33710
  id: promptId,
33701
33711
  ...sessionId ? { sessionId } : {},
@@ -33717,7 +33727,7 @@ async function sendPromptToAgent(options) {
33717
33727
  } catch (err) {
33718
33728
  const errMsg = err instanceof Error ? err.message : String(err);
33719
33729
  log2(`[Agent] Send failed: ${errMsg}`);
33720
- sendResult2({
33730
+ sendResult({
33721
33731
  type: "prompt_result",
33722
33732
  id: promptId,
33723
33733
  ...sessionId ? { sessionId } : {},
@@ -33751,7 +33761,7 @@ function handlePrompt(ctx, opts) {
33751
33761
  agentType,
33752
33762
  agentConfig,
33753
33763
  sessionParentPath,
33754
- sendResult: sendResult2,
33764
+ sendResult,
33755
33765
  sendSessionUpdate,
33756
33766
  followUpCatalogPromptId,
33757
33767
  sessionChangeSummaryFilePaths,
@@ -33768,7 +33778,7 @@ function handlePrompt(ctx, opts) {
33768
33778
  const activeRunId = runId;
33769
33779
  const acpAgentKey = computeAcpAgentKey(preferredForPrompt, mode, agentConfig ?? null);
33770
33780
  if (!acpAgentKey) {
33771
- sendResult2({
33781
+ sendResult({
33772
33782
  type: "prompt_result",
33773
33783
  id: promptId,
33774
33784
  ...sessionId ? { sessionId } : {},
@@ -33805,7 +33815,7 @@ function handlePrompt(ctx, opts) {
33805
33815
  const evaluated = Boolean(preferredForPrompt && errMsg.trim());
33806
33816
  const suggestsAuth = evaluated ? localAgentErrorSuggestsAuth(preferredForPrompt, errMsg) : false;
33807
33817
  const auth = suggestsAuth && preferredForPrompt ? { agentAuthRequired: true, agentType: preferredForPrompt } : {};
33808
- sendResult2({
33818
+ sendResult({
33809
33819
  type: "prompt_result",
33810
33820
  id: promptId,
33811
33821
  ...sessionId ? { sessionId } : {},
@@ -33825,7 +33835,7 @@ function handlePrompt(ctx, opts) {
33825
33835
  await handle.cancel?.();
33826
33836
  } catch {
33827
33837
  }
33828
- sendResult2({
33838
+ sendResult({
33829
33839
  type: "prompt_result",
33830
33840
  id: promptId,
33831
33841
  ...sessionId ? { sessionId } : {},
@@ -33846,7 +33856,7 @@ function handlePrompt(ctx, opts) {
33846
33856
  runId: activeRunId,
33847
33857
  agentType: preferredForPrompt,
33848
33858
  agentCwd: resolveSessionParentPathForAgentProcess(sessionParentPath),
33849
- sendResult: sendResult2,
33859
+ sendResult,
33850
33860
  sendSessionUpdate,
33851
33861
  log: ctx.log,
33852
33862
  followUpCatalogPromptId,
@@ -33902,6 +33912,496 @@ async function createAcpManager(options) {
33902
33912
  };
33903
33913
  }
33904
33914
 
33915
+ // src/worktrees/worktree-layout-file.ts
33916
+ import * as fs16 from "node:fs";
33917
+ import * as path19 from "node:path";
33918
+ import os7 from "node:os";
33919
+ var LAYOUT_FILENAME = "worktree-launcher-layout.json";
33920
+ function defaultWorktreeLayoutPath() {
33921
+ return path19.join(os7.homedir(), ".buildautomaton", LAYOUT_FILENAME);
33922
+ }
33923
+ function normalizeLoadedLayout(raw) {
33924
+ if (raw && typeof raw === "object" && "launcherCwds" in raw) {
33925
+ const j = raw;
33926
+ if (Array.isArray(j.launcherCwds)) return { launcherCwds: j.launcherCwds };
33927
+ }
33928
+ return { launcherCwds: [] };
33929
+ }
33930
+ function loadWorktreeLayout() {
33931
+ try {
33932
+ const p = defaultWorktreeLayoutPath();
33933
+ if (!fs16.existsSync(p)) return { launcherCwds: [] };
33934
+ const raw = JSON.parse(fs16.readFileSync(p, "utf8"));
33935
+ return normalizeLoadedLayout(raw);
33936
+ } catch {
33937
+ return { launcherCwds: [] };
33938
+ }
33939
+ }
33940
+ function saveWorktreeLayout(layout) {
33941
+ try {
33942
+ const dir = path19.dirname(defaultWorktreeLayoutPath());
33943
+ fs16.mkdirSync(dir, { recursive: true });
33944
+ fs16.writeFileSync(defaultWorktreeLayoutPath(), JSON.stringify(layout, null, 2), "utf8");
33945
+ } catch {
33946
+ }
33947
+ }
33948
+ function baseNameSafe(pathString) {
33949
+ return path19.basename(pathString).replace(/[^a-zA-Z0-9._-]+/g, "-") || "cwd";
33950
+ }
33951
+ function getLauncherDirNameIfPresent(layout, bridgeRootPath2) {
33952
+ const norm = path19.resolve(bridgeRootPath2);
33953
+ const existing = layout.launcherCwds.find((e) => path19.resolve(e.absolutePath) === norm);
33954
+ return existing?.dirName;
33955
+ }
33956
+ function allocateDirNameForLauncherCwd(layout, bridgeRootPath2) {
33957
+ const existing = getLauncherDirNameIfPresent(layout, bridgeRootPath2);
33958
+ if (existing) return existing;
33959
+ const norm = path19.resolve(bridgeRootPath2);
33960
+ const base = baseNameSafe(norm);
33961
+ const used = new Set(layout.launcherCwds.map((e) => e.dirName));
33962
+ let name = base;
33963
+ let n = 2;
33964
+ while (used.has(name)) {
33965
+ name = `${base}-${n}`;
33966
+ n += 1;
33967
+ }
33968
+ layout.launcherCwds.push({ absolutePath: norm, dirName: name });
33969
+ saveWorktreeLayout(layout);
33970
+ return name;
33971
+ }
33972
+
33973
+ // src/worktrees/manager/session-worktree-cache.ts
33974
+ import * as path20 from "node:path";
33975
+ var SessionWorktreeCache = class {
33976
+ sessionRepoCheckoutPaths = /* @__PURE__ */ new Map();
33977
+ sessionParentPathBySession = /* @__PURE__ */ new Map();
33978
+ sessionWorkingTreeRelRootBySession = /* @__PURE__ */ new Map();
33979
+ remember(sessionId, binding) {
33980
+ const paths = binding.repoCheckoutPaths.map((p) => path20.resolve(p));
33981
+ this.sessionRepoCheckoutPaths.set(sessionId, paths);
33982
+ this.sessionParentPathBySession.set(sessionId, path20.resolve(binding.sessionParentPath));
33983
+ this.sessionWorkingTreeRelRootBySession.set(sessionId, path20.resolve(binding.workingTreeRelRoot));
33984
+ }
33985
+ clearSession(sessionId) {
33986
+ const paths = this.sessionRepoCheckoutPaths.get(sessionId);
33987
+ this.sessionRepoCheckoutPaths.delete(sessionId);
33988
+ this.sessionParentPathBySession.delete(sessionId);
33989
+ this.sessionWorkingTreeRelRootBySession.delete(sessionId);
33990
+ return paths;
33991
+ }
33992
+ getSessionParentPath(sessionId) {
33993
+ return this.sessionParentPathBySession.get(sessionId);
33994
+ }
33995
+ getWorkingTreeRelRoot(sessionId) {
33996
+ return this.sessionWorkingTreeRelRootBySession.get(sessionId) ?? null;
33997
+ }
33998
+ hasSession(sessionId) {
33999
+ return this.sessionParentPathBySession.has(sessionId);
34000
+ }
34001
+ getRepoCheckoutPaths(sessionId) {
34002
+ const paths = this.sessionRepoCheckoutPaths.get(sessionId);
34003
+ return paths?.length ? [...paths] : void 0;
34004
+ }
34005
+ getRepoCheckoutPathsRef(sessionId) {
34006
+ const paths = this.sessionRepoCheckoutPaths.get(sessionId);
34007
+ return paths?.length ? paths : void 0;
34008
+ }
34009
+ isLegacyNestedLayout(sessionId) {
34010
+ const parent = this.sessionParentPathBySession.get(sessionId);
34011
+ const relRoot = this.sessionWorkingTreeRelRootBySession.get(sessionId);
34012
+ if (!parent || !relRoot) return false;
34013
+ return path20.resolve(parent) !== path20.resolve(relRoot);
34014
+ }
34015
+ };
34016
+
34017
+ // src/worktrees/manager/session-worktree-manager-context.ts
34018
+ function createSessionWorktreeManagerContext(options) {
34019
+ return {
34020
+ worktreesRootPath: options.worktreesRootPath,
34021
+ log: options.log,
34022
+ cache: new SessionWorktreeCache(),
34023
+ layout: loadWorktreeLayout()
34024
+ };
34025
+ }
34026
+
34027
+ // src/git/branches/commit-and-push.ts
34028
+ async function gitCommitAllIfDirty(repoDir, message, options) {
34029
+ const g = cliSimpleGit(repoDir);
34030
+ const st = await g.status();
34031
+ if (!st.files?.length) return;
34032
+ const branch = options.branch.trim();
34033
+ if (!branch) {
34034
+ throw new Error("Branch name is required");
34035
+ }
34036
+ const branches = await g.branchLocal();
34037
+ const localNames = new Set(branches.all.map((b) => b.replace(/^\*\s*/, "").trim()));
34038
+ if (!localNames.has(branch)) {
34039
+ await g.checkoutLocalBranch(branch);
34040
+ } else {
34041
+ await g.checkout(branch);
34042
+ }
34043
+ await g.add(".");
34044
+ await g.commit(message);
34045
+ if (options.push) {
34046
+ await g.push(["-u", "origin", branch]);
34047
+ }
34048
+ }
34049
+
34050
+ // src/worktrees/commit-session-worktrees.ts
34051
+ async function commitSessionWorktrees(options) {
34052
+ const { paths, branch, message, push } = options;
34053
+ try {
34054
+ for (const wt of paths) {
34055
+ await gitCommitAllIfDirty(wt, message, { push, branch });
34056
+ }
34057
+ return { ok: true };
34058
+ } catch (e) {
34059
+ const err = e instanceof Error ? e.message : String(e);
34060
+ return { ok: false, error: err };
34061
+ }
34062
+ }
34063
+
34064
+ // src/worktrees/manager/git/commit-session-worktree.ts
34065
+ async function commitSessionWorktree(cache2, params) {
34066
+ const paths = cache2.getRepoCheckoutPathsRef(params.sessionId);
34067
+ const targets = paths?.length ? paths : [getBridgeRoot()];
34068
+ return commitSessionWorktrees({
34069
+ paths: targets,
34070
+ branch: params.branch,
34071
+ message: params.message,
34072
+ push: params.push
34073
+ });
34074
+ }
34075
+
34076
+ // src/git/changes/parse/normalize-git-diff-path.ts
34077
+ function normalizeGitDiffPath(path60) {
34078
+ return path60.replace(/\\/g, "/").replace(/\/ +/g, "/").trim();
34079
+ }
34080
+
34081
+ // src/git/changes/parse/expand-git-rename-numstat-path.ts
34082
+ function expandGitRenameNumstatPath(rawPath) {
34083
+ const open2 = rawPath.indexOf("{");
34084
+ const arrow = rawPath.indexOf("=>");
34085
+ const close = rawPath.lastIndexOf("}");
34086
+ if (open2 === -1 || arrow === -1 || close === -1 || open2 > arrow || arrow > close) {
34087
+ return null;
34088
+ }
34089
+ const prefix = rawPath.slice(0, open2);
34090
+ const suffix = rawPath.slice(close + 1);
34091
+ const oldPart = rawPath.slice(open2 + 1, arrow).trim();
34092
+ const newPart = rawPath.slice(arrow + 2, close).trim();
34093
+ return {
34094
+ oldPath: normalizeGitDiffPath(`${prefix}${oldPart}${suffix}`),
34095
+ newPath: normalizeGitDiffPath(`${prefix}${newPart}${suffix}`)
34096
+ };
34097
+ }
34098
+ function isGitRenameNumstatPath(rawPath) {
34099
+ return expandGitRenameNumstatPath(rawPath) != null;
34100
+ }
34101
+
34102
+ // src/git/changes/parse/parse-name-status-entries.ts
34103
+ function parseNameStatusEntries(lines) {
34104
+ const entries = [];
34105
+ for (const line of lines) {
34106
+ if (!line.trim()) continue;
34107
+ const tabParts = line.split(" ");
34108
+ if (tabParts.length < 2) continue;
34109
+ const status = tabParts[0].trim();
34110
+ const code = status[0];
34111
+ if (code === "A") {
34112
+ entries.push({ pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]), change: "added" });
34113
+ } else if (code === "D") {
34114
+ entries.push({ pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]), change: "removed" });
34115
+ } else if (code === "R" || code === "C") {
34116
+ if (tabParts.length >= 3) {
34117
+ entries.push({
34118
+ pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]),
34119
+ oldPathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 2]),
34120
+ change: "moved"
34121
+ });
34122
+ }
34123
+ } else if (code === "M" || code === "U" || code === "T") {
34124
+ entries.push({ pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]), change: "modified" });
34125
+ }
34126
+ }
34127
+ return entries;
34128
+ }
34129
+ function parseNameStatusLines(lines) {
34130
+ const m = /* @__PURE__ */ new Map();
34131
+ for (const entry of parseNameStatusEntries(lines)) {
34132
+ m.set(entry.pathInRepo, entry.change);
34133
+ }
34134
+ return m;
34135
+ }
34136
+
34137
+ // src/git/changes/parse/parse-numstat-entries.ts
34138
+ function parseNumstatCounts(parts) {
34139
+ const [a, d] = parts;
34140
+ return {
34141
+ additions: a === "-" ? 0 : parseInt(String(a), 10) || 0,
34142
+ deletions: d === "-" ? 0 : parseInt(String(d), 10) || 0
34143
+ };
34144
+ }
34145
+ function parseNumstatLine(line) {
34146
+ const parts = line.split(" ");
34147
+ if (parts.length < 3) return null;
34148
+ const rawPath = parts[parts.length - 1];
34149
+ const { additions, deletions } = parseNumstatCounts(parts);
34150
+ const expanded = expandGitRenameNumstatPath(rawPath);
34151
+ const pathInRepo = normalizeGitDiffPath(expanded?.newPath ?? rawPath);
34152
+ return {
34153
+ pathInRepo,
34154
+ additions,
34155
+ deletions,
34156
+ ...expanded ? { oldPathInRepo: expanded.oldPath } : {}
34157
+ };
34158
+ }
34159
+ function parseNumstatEntries(lines) {
34160
+ const m = /* @__PURE__ */ new Map();
34161
+ for (const line of lines) {
34162
+ if (!line.trim()) continue;
34163
+ const parts = line.split(" ");
34164
+ if (parts.length < 3) continue;
34165
+ const rawPath = parts[parts.length - 1];
34166
+ const expanded = expandGitRenameNumstatPath(rawPath);
34167
+ const entry = parseNumstatLine(line);
34168
+ if (!entry) continue;
34169
+ const existing = m.get(entry.pathInRepo);
34170
+ if (!existing || expanded) {
34171
+ m.set(entry.pathInRepo, entry);
34172
+ }
34173
+ }
34174
+ return m;
34175
+ }
34176
+ function parseNumstat(lines) {
34177
+ const m = /* @__PURE__ */ new Map();
34178
+ for (const [path60, entry] of parseNumstatEntries(lines)) {
34179
+ m.set(path60, { additions: entry.additions, deletions: entry.deletions });
34180
+ }
34181
+ return m;
34182
+ }
34183
+
34184
+ // src/git/changes/parse/numstat-from-git-no-index.ts
34185
+ async function numstatFromGitNoIndex(g, pathInRepo) {
34186
+ const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
34187
+ try {
34188
+ const out = await g.raw(["diff", "--numstat", "--no-index", "--", devNull, pathInRepo]);
34189
+ const first2 = String(out).split("\n").find((l) => l.trim()) ?? "";
34190
+ const parsed = parseNumstatLine(first2);
34191
+ if (!parsed) return null;
34192
+ return { additions: parsed.additions, deletions: parsed.deletions };
34193
+ } catch {
34194
+ return null;
34195
+ }
34196
+ }
34197
+
34198
+ // src/git/changes/status/working-tree-changed-path-count.ts
34199
+ function workingTreeChangedPathCount(nameStatusLines, numstatLines, untrackedLines) {
34200
+ const kindByPath = parseNameStatusLines(nameStatusLines);
34201
+ const numByPath = parseNumstat(numstatLines);
34202
+ const paths = /* @__PURE__ */ new Set([...kindByPath.keys(), ...numByPath.keys()]);
34203
+ for (const p of untrackedLines.map((s) => s.trim()).filter(Boolean)) {
34204
+ paths.add(p);
34205
+ }
34206
+ return paths.size;
34207
+ }
34208
+
34209
+ // src/git/changes/count-working-tree-changed-files.ts
34210
+ async function countWorkingTreeChangedFilesForRepo(repoGitCwd) {
34211
+ return runGitTask(async () => {
34212
+ const g = cliSimpleGit(repoGitCwd);
34213
+ const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
34214
+ g.raw(["diff", "--name-status", "HEAD"]).catch(() => ""),
34215
+ g.raw(["diff", "HEAD", "--numstat"]).catch(() => ""),
34216
+ g.raw(["ls-files", "--others", "--exclude-standard"]).catch(() => "")
34217
+ ]);
34218
+ return workingTreeChangedPathCount(
34219
+ String(nameStatusRaw).split("\n"),
34220
+ String(numstatRaw).split("\n"),
34221
+ String(untrackedRaw).split("\n")
34222
+ );
34223
+ });
34224
+ }
34225
+
34226
+ // src/git/commits/remote-tracking/rev-parse-safe.ts
34227
+ async function revParseSafe(g, ref) {
34228
+ try {
34229
+ const v = (await g.raw(["rev-parse", "--verify", ref])).trim();
34230
+ return v || null;
34231
+ } catch {
34232
+ return null;
34233
+ }
34234
+ }
34235
+
34236
+ // src/git/commits/remote-tracking/git-config.ts
34237
+ async function tryConfigGet(g, key) {
34238
+ try {
34239
+ const out = await g.raw(["config", "--get", key]);
34240
+ const v = String(out).trim();
34241
+ return v || null;
34242
+ } catch {
34243
+ return null;
34244
+ }
34245
+ }
34246
+
34247
+ // src/git/commits/remote-tracking/branch-remote-config.ts
34248
+ async function readBranchRemoteConfig(g) {
34249
+ const branch = (await g.raw(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
34250
+ if (!branch || branch === "HEAD") {
34251
+ return { branch: null, remote: "origin", merge: null };
34252
+ }
34253
+ const [remote, merge2] = await Promise.all([
34254
+ tryConfigGet(g, `branch.${branch}.remote`),
34255
+ tryConfigGet(g, `branch.${branch}.merge`)
34256
+ ]);
34257
+ return { branch, remote: remote ?? "origin", merge: merge2 };
34258
+ }
34259
+ function trackingRefCandidates(config2) {
34260
+ const candidates = ["HEAD@{upstream}", "@{u}"];
34261
+ if (!config2.branch) return candidates;
34262
+ if (config2.merge) {
34263
+ candidates.push(
34264
+ `refs/remotes/${config2.remote}/${config2.merge.replace(/^refs\/heads\//, "")}`
34265
+ );
34266
+ }
34267
+ candidates.push(`refs/remotes/${config2.remote}/${config2.branch}`);
34268
+ return candidates;
34269
+ }
34270
+
34271
+ // src/git/commits/remote-tracking/first-resolved-ref.ts
34272
+ async function firstResolvedSha(g, candidates) {
34273
+ const resolved = await Promise.all(
34274
+ candidates.map(async (ref) => ({ ref, sha: await revParseSafe(g, ref) }))
34275
+ );
34276
+ return resolved.find((entry) => entry.sha)?.sha ?? null;
34277
+ }
34278
+
34279
+ // src/git/commits/remote-tracking/resolve-default-remote-branch-ref.ts
34280
+ var DEFAULT_REMOTE_BRANCH_NAMES = ["main", "master", "trunk", "develop"];
34281
+ function defaultRemoteBranchNameRefs(remote) {
34282
+ return DEFAULT_REMOTE_BRANCH_NAMES.map((name) => `refs/remotes/${remote}/${name}`);
34283
+ }
34284
+ async function resolveRemoteHeadSymbolicRef(g, remote) {
34285
+ const headSym = `refs/remotes/${remote}/HEAD`;
34286
+ try {
34287
+ const out = await g.raw(["symbolic-ref", "-q", "--verify", headSym]);
34288
+ const resolved = String(out).trim();
34289
+ return resolved.startsWith("refs/remotes/") ? resolved : null;
34290
+ } catch {
34291
+ return null;
34292
+ }
34293
+ }
34294
+ async function resolveDefaultRemoteBranchSha(g, remote) {
34295
+ const nameRefs = defaultRemoteBranchNameRefs(remote);
34296
+ const [symRef, ...nameShas] = await Promise.all([
34297
+ resolveRemoteHeadSymbolicRef(g, remote),
34298
+ ...nameRefs.map((ref) => revParseSafe(g, ref))
34299
+ ]);
34300
+ if (symRef) return revParseSafe(g, symRef);
34301
+ for (let i = 0; i < nameRefs.length; i++) {
34302
+ if (nameShas[i]) return nameShas[i];
34303
+ }
34304
+ return null;
34305
+ }
34306
+
34307
+ // src/git/commits/remote-tracking/resolve-base-sha-for-unpushed-commits.ts
34308
+ import * as path21 from "node:path";
34309
+ var BASE_SHA_CACHE_TTL_MS = 3e4;
34310
+ var baseShaCache = /* @__PURE__ */ new Map();
34311
+ async function resolveBaseShaForUnpushedCommitsUncached(g) {
34312
+ const config2 = await readBranchRemoteConfig(g);
34313
+ const trackingSha = await firstResolvedSha(g, trackingRefCandidates(config2));
34314
+ if (trackingSha) return trackingSha;
34315
+ return resolveDefaultRemoteBranchSha(g, config2.remote);
34316
+ }
34317
+ async function resolveBaseShaForUnpushedCommits(g, repoDir) {
34318
+ if (repoDir) {
34319
+ const key = path21.resolve(repoDir);
34320
+ const now = Date.now();
34321
+ const cached2 = baseShaCache.get(key);
34322
+ if (cached2 && cached2.expiresAt > now) return cached2.baseSha;
34323
+ const baseSha = await resolveBaseShaForUnpushedCommitsUncached(g);
34324
+ baseShaCache.set(key, { baseSha, expiresAt: now + BASE_SHA_CACHE_TTL_MS });
34325
+ return baseSha;
34326
+ }
34327
+ return resolveBaseShaForUnpushedCommitsUncached(g);
34328
+ }
34329
+
34330
+ // src/git/commits/remote-tracking/commits-ahead-of-remote-tracking.ts
34331
+ async function commitsAheadOfRemoteTracking(repoDir) {
34332
+ const g = cliSimpleGit(repoDir);
34333
+ const headSha = await revParseSafe(g, "HEAD");
34334
+ if (!headSha) return 0;
34335
+ const baseSha = await resolveBaseShaForUnpushedCommits(g, repoDir);
34336
+ if (!baseSha || baseSha === headSha) return 0;
34337
+ try {
34338
+ const out = await g.raw(["rev-list", "--count", `${baseSha}..${headSha}`]);
34339
+ const n = parseInt(String(out).trim(), 10);
34340
+ return Number.isNaN(n) ? 0 : n;
34341
+ } catch {
34342
+ return 0;
34343
+ }
34344
+ }
34345
+
34346
+ // src/git/status/working-tree-status.ts
34347
+ async function getRepoWorkingTreeStatus(repoDir) {
34348
+ return runGitTask(async () => {
34349
+ const uncommittedFileCount = await countWorkingTreeChangedFilesForRepo(repoDir);
34350
+ const hasUncommittedChanges = uncommittedFileCount > 0;
34351
+ const ahead = await commitsAheadOfRemoteTracking(repoDir);
34352
+ return { hasUncommittedChanges, hasUnpushedCommits: ahead > 0, uncommittedFileCount };
34353
+ });
34354
+ }
34355
+ async function aggregateSessionPathsWorkingTreeStatus(paths) {
34356
+ let hasUncommittedChanges = false;
34357
+ let hasUnpushedCommits = false;
34358
+ let uncommittedFileCount = 0;
34359
+ await forEachWithGitYield(paths, async (p) => {
34360
+ const s = await getRepoWorkingTreeStatus(p);
34361
+ uncommittedFileCount += s.uncommittedFileCount;
34362
+ if (s.hasUncommittedChanges) hasUncommittedChanges = true;
34363
+ if (s.hasUnpushedCommits) hasUnpushedCommits = true;
34364
+ });
34365
+ return { hasUncommittedChanges, hasUnpushedCommits, uncommittedFileCount };
34366
+ }
34367
+ async function pushAheadOfUpstreamForPaths(paths) {
34368
+ await forEachWithGitYield(paths, async (p) => {
34369
+ const g = cliSimpleGit(p);
34370
+ const ahead = await commitsAheadOfRemoteTracking(p);
34371
+ if (ahead <= 0) return;
34372
+ await g.push();
34373
+ });
34374
+ }
34375
+
34376
+ // src/worktrees/manager/resolve-commit-targets.ts
34377
+ function bridgeRootBinding(bridgeRoot) {
34378
+ return {
34379
+ sessionParentPath: bridgeRoot,
34380
+ workingTreeRelRoot: bridgeRoot,
34381
+ repoCheckoutPaths: [bridgeRoot]
34382
+ };
34383
+ }
34384
+ async function resolveCommitTargetsAsync(sessionId, cache2, discover) {
34385
+ const sid = sessionId.trim();
34386
+ const paths = cache2.getRepoCheckoutPathsRef(sid);
34387
+ if (paths?.length) return paths;
34388
+ const disc = await discover(sid);
34389
+ if (disc?.repoCheckoutPaths.length) {
34390
+ cache2.remember(sid, disc);
34391
+ return disc.repoCheckoutPaths;
34392
+ }
34393
+ const bridgeRoot = getBridgeRoot();
34394
+ cache2.remember(sid, bridgeRootBinding(bridgeRoot));
34395
+ return [bridgeRoot];
34396
+ }
34397
+
34398
+ // src/worktrees/manager/git/get-session-working-tree-status.ts
34399
+ async function getSessionWorkingTreeStatus(cache2, sessionId, discover) {
34400
+ return aggregateSessionPathsWorkingTreeStatus(
34401
+ await resolveCommitTargetsAsync(sessionId, cache2, discover)
34402
+ );
34403
+ }
34404
+
33905
34405
  // src/git/changes/types.ts
33906
34406
  var MAX_PATCH_CHARS = 35e4;
33907
34407
 
@@ -33959,106 +34459,14 @@ function formatRemoteDisplayLabel(remoteUrl) {
33959
34459
  return `origin \xB7 ${hostPath}`;
33960
34460
  }
33961
34461
 
33962
- // src/git/changes/get-working-tree-change-repo-details.ts
33963
- import * as path22 from "node:path";
34462
+ // src/git/changes/repo/get-working-tree-change-repo-details.ts
34463
+ import * as path28 from "node:path";
33964
34464
 
33965
- // src/git/commits/resolve-remote-tracking.ts
33966
- async function tryConfigGet(g, key) {
33967
- try {
33968
- const out = await g.raw(["config", "--get", key]);
33969
- const v = String(out).trim();
33970
- return v || null;
33971
- } catch {
33972
- return null;
33973
- }
33974
- }
33975
- async function revParseSafe(g, ref) {
33976
- try {
33977
- const v = (await g.raw(["rev-parse", ref])).trim();
33978
- return v || null;
33979
- } catch {
33980
- return null;
33981
- }
33982
- }
33983
- async function resolveRemoteTrackingRefForAhead(g) {
33984
- try {
33985
- await g.raw(["rev-parse", "--verify", "HEAD@{upstream}"]);
33986
- return "HEAD@{upstream}";
33987
- } catch {
33988
- try {
33989
- await g.raw(["rev-parse", "--verify", "@{u}"]);
33990
- return "@{u}";
33991
- } catch {
33992
- }
33993
- }
33994
- const branch = (await g.raw(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
33995
- if (!branch || branch === "HEAD") return null;
33996
- const remote = await tryConfigGet(g, `branch.${branch}.remote`) ?? "origin";
33997
- const merge2 = await tryConfigGet(g, `branch.${branch}.merge`);
33998
- if (merge2) {
33999
- const upstreamBranch = merge2.replace(/^refs\/heads\//, "");
34000
- const ref = `refs/remotes/${remote}/${upstreamBranch}`;
34001
- try {
34002
- await g.raw(["rev-parse", "--verify", ref]);
34003
- return ref;
34004
- } catch {
34005
- }
34006
- }
34007
- const fallbackRef = `refs/remotes/${remote}/${branch}`;
34008
- try {
34009
- await g.raw(["rev-parse", "--verify", fallbackRef]);
34010
- return fallbackRef;
34011
- } catch {
34012
- return null;
34013
- }
34014
- }
34015
- async function remoteForCurrentBranch(g) {
34016
- const branch = (await g.raw(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
34017
- if (!branch || branch === "HEAD") return "origin";
34018
- return await tryConfigGet(g, `branch.${branch}.remote`) ?? "origin";
34019
- }
34020
- async function resolveDefaultRemoteBranchRef(g, remote) {
34021
- const headSym = `refs/remotes/${remote}/HEAD`;
34022
- try {
34023
- const resolved = (await g.raw(["symbolic-ref", "-q", "--verify", headSym])).trim();
34024
- if (resolved.startsWith("refs/remotes/")) return resolved;
34025
- } catch {
34026
- }
34027
- for (const name of ["main", "master", "trunk", "develop"]) {
34028
- const r = `refs/remotes/${remote}/${name}`;
34029
- try {
34030
- await g.raw(["rev-parse", "--verify", r]);
34031
- return r;
34032
- } catch {
34033
- }
34034
- }
34035
- return null;
34036
- }
34037
- async function resolveBaseShaForUnpushedCommits(g) {
34038
- const trackingRef = await resolveRemoteTrackingRefForAhead(g);
34039
- if (trackingRef) {
34040
- const sha = await revParseSafe(g, trackingRef);
34041
- if (sha) return sha;
34042
- }
34043
- const remote = await remoteForCurrentBranch(g);
34044
- const defaultRef = await resolveDefaultRemoteBranchRef(g, remote);
34045
- if (!defaultRef) return null;
34046
- return revParseSafe(g, defaultRef);
34047
- }
34048
- async function commitsAheadOfRemoteTracking(repoDir) {
34049
- const g = cliSimpleGit(repoDir);
34050
- const headSha = await revParseSafe(g, "HEAD");
34051
- if (!headSha) return 0;
34052
- const baseSha = await resolveBaseShaForUnpushedCommits(g);
34053
- if (!baseSha || baseSha === headSha) return 0;
34054
- try {
34055
- const out = await g.raw(["rev-list", "--count", `${baseSha}..${headSha}`]);
34056
- const n = parseInt(String(out).trim(), 10);
34057
- return Number.isNaN(n) ? 0 : n;
34058
- } catch {
34059
- return 0;
34060
- }
34061
- }
34465
+ // src/git/changes/repo/build-working-tree-change-repo-detail.ts
34466
+ import * as path27 from "node:path";
34467
+
34468
+ // src/git/commits/list-unpushed-commits.ts
34469
+ import * as path22 from "node:path";
34062
34470
 
34063
34471
  // src/git/commits/lib/parse-log-lines.ts
34064
34472
  function parseLogShaDateSubjectLines(raw) {
@@ -34078,9 +34486,14 @@ function parseLogShaDateSubjectLines(raw) {
34078
34486
  }
34079
34487
 
34080
34488
  // src/git/commits/list-unpushed-commits.ts
34489
+ var UNPUSHED_CACHE_TTL_MS = 3e4;
34490
+ var unpushedCache = /* @__PURE__ */ new Map();
34081
34491
  async function gitLogNotReachableFromBase(g, baseSha, headSha) {
34082
34492
  if (baseSha === headSha) return [];
34083
34493
  try {
34494
+ const countOut = await g.raw(["rev-list", "--count", `${baseSha}..${headSha}`]);
34495
+ const count = parseInt(String(countOut).trim(), 10);
34496
+ if (!Number.isFinite(count) || count <= 0) return [];
34084
34497
  const logOut = await g.raw(["log", "--format=%H %cI %s", `${baseSha}..${headSha}`]);
34085
34498
  return parseLogShaDateSubjectLines(logOut);
34086
34499
  } catch {
@@ -34088,12 +34501,22 @@ async function gitLogNotReachableFromBase(g, baseSha, headSha) {
34088
34501
  }
34089
34502
  }
34090
34503
  async function listUnpushedCommits(repoDir) {
34504
+ const key = path22.resolve(repoDir);
34505
+ const now = Date.now();
34506
+ const cached2 = unpushedCache.get(key);
34507
+ if (cached2 && cached2.expiresAt > now) return cached2.commits;
34091
34508
  const g = cliSimpleGit(repoDir);
34092
- const headSha = await revParseSafe(g, "HEAD");
34093
- if (!headSha) return [];
34094
- const baseSha = await resolveBaseShaForUnpushedCommits(g);
34095
- if (!baseSha) return [];
34096
- return gitLogNotReachableFromBase(g, baseSha, headSha);
34509
+ const [headSha, baseSha] = await Promise.all([
34510
+ revParseSafe(g, "HEAD"),
34511
+ resolveBaseShaForUnpushedCommits(g, repoDir)
34512
+ ]);
34513
+ if (!headSha || !baseSha) {
34514
+ unpushedCache.set(key, { commits: [], expiresAt: now + UNPUSHED_CACHE_TTL_MS });
34515
+ return [];
34516
+ }
34517
+ const commits = await gitLogNotReachableFromBase(g, baseSha, headSha);
34518
+ unpushedCache.set(key, { commits, expiresAt: now + UNPUSHED_CACHE_TTL_MS });
34519
+ return commits;
34097
34520
  }
34098
34521
 
34099
34522
  // src/git/commits/lib/sanitize-recent-commits-limit.ts
@@ -34107,72 +34530,40 @@ function sanitizeRecentCommitsLimit(value) {
34107
34530
  }
34108
34531
 
34109
34532
  // src/git/commits/list-recent-commits.ts
34110
- async function listRecentCommits(repoDir, limitInput) {
34533
+ async function listRecentCommitRefs(repoDir, limitInput) {
34111
34534
  const limit = sanitizeRecentCommitsLimit(limitInput);
34112
34535
  const g = cliSimpleGit(repoDir);
34113
- const headSha = await revParseSafe(g, "HEAD");
34114
- if (!headSha) return { commits: [], hasMore: false };
34115
- const unpushedSet = new Set((await listUnpushedCommits(repoDir)).map((c) => c.sha));
34116
34536
  try {
34117
- const logOut = await g.raw(["log", "--format=%H %cI %s", `-n`, String(limit), "HEAD"]);
34118
- const commits = parseLogShaDateSubjectLines(logOut).map((c) => ({
34119
- ...c,
34120
- needsPush: unpushedSet.has(c.sha)
34121
- }));
34122
- let hasMore = false;
34123
- if (commits.length === limit) {
34124
- const nextOut = await g.raw(["log", "-n", "1", `--skip=${limit}`, "--format=%H", "HEAD"]);
34125
- hasMore = /^[0-9a-f]{7,40}$/i.test(String(nextOut).trim());
34126
- }
34537
+ const logOut = await g.raw(["log", "--format=%H %cI %s", `-n`, String(limit + 1), "HEAD"]);
34538
+ const parsed = parseLogShaDateSubjectLines(logOut);
34539
+ const hasMore = parsed.length > limit;
34540
+ const commits = parsed.slice(0, limit);
34127
34541
  return { commits, hasMore };
34128
34542
  } catch {
34129
34543
  return { commits: [], hasMore: false };
34130
34544
  }
34131
34545
  }
34132
34546
 
34133
- // src/git/changes/parse/normalize-git-diff-path.ts
34134
- function normalizeGitDiffPath(path46) {
34135
- return path46.replace(/\\/g, "/").replace(/\/ +/g, "/").trim();
34136
- }
34137
-
34138
- // src/git/changes/parse/expand-git-rename-numstat-path.ts
34139
- function expandGitRenameNumstatPath(rawPath) {
34140
- const open2 = rawPath.indexOf("{");
34141
- const arrow = rawPath.indexOf("=>");
34142
- const close = rawPath.lastIndexOf("}");
34143
- if (open2 === -1 || arrow === -1 || close === -1 || open2 > arrow || arrow > close) {
34144
- return null;
34145
- }
34146
- const prefix = rawPath.slice(0, open2);
34147
- const suffix = rawPath.slice(close + 1);
34148
- const oldPart = rawPath.slice(open2 + 1, arrow).trim();
34149
- const newPart = rawPath.slice(arrow + 2, close).trim();
34150
- return {
34151
- oldPath: normalizeGitDiffPath(`${prefix}${oldPart}${suffix}`),
34152
- newPath: normalizeGitDiffPath(`${prefix}${newPart}${suffix}`)
34153
- };
34154
- }
34155
- function isGitRenameNumstatPath(rawPath) {
34156
- return expandGitRenameNumstatPath(rawPath) != null;
34157
- }
34547
+ // src/git/changes/listing/list-changed-files-for-commit.ts
34548
+ import * as path24 from "node:path";
34158
34549
 
34159
34550
  // src/git/changes/rows/build-changed-file-row.ts
34160
34551
  function buildChangedFileRow(options) {
34161
34552
  const pathInRepo = normalizeGitDiffPath(options.pathInRepo);
34162
- const relLauncher = posixJoinDirFile(options.repoRelPath, pathInRepo);
34553
+ const workspaceRelPath = posixJoinDirFile(options.repoRelPath, pathInRepo);
34163
34554
  const additions = options.numEntry?.additions ?? 0;
34164
34555
  const deletions = options.numEntry?.deletions ?? 0;
34165
34556
  let change = options.nameEntry?.change ?? "modified";
34166
- let movedFromPathRelLauncher;
34557
+ let movedFromWorkspaceRelPath;
34167
34558
  let movedFromPathInRepo;
34168
34559
  const oldPathInRepo = options.nameEntry?.oldPathInRepo ?? options.numEntry?.oldPathInRepo;
34169
34560
  if (change === "moved" && oldPathInRepo) {
34170
34561
  movedFromPathInRepo = normalizeGitDiffPath(oldPathInRepo);
34171
- movedFromPathRelLauncher = posixJoinDirFile(options.repoRelPath, movedFromPathInRepo);
34562
+ movedFromWorkspaceRelPath = posixJoinDirFile(options.repoRelPath, movedFromPathInRepo);
34172
34563
  } else if (oldPathInRepo && change === "modified") {
34173
34564
  change = "moved";
34174
34565
  movedFromPathInRepo = normalizeGitDiffPath(oldPathInRepo);
34175
- movedFromPathRelLauncher = posixJoinDirFile(options.repoRelPath, movedFromPathInRepo);
34566
+ movedFromWorkspaceRelPath = posixJoinDirFile(options.repoRelPath, movedFromPathInRepo);
34176
34567
  }
34177
34568
  if (options.untracked && !options.nameEntry) {
34178
34569
  change = "added";
@@ -34182,11 +34573,11 @@ function buildChangedFileRow(options) {
34182
34573
  else change = "modified";
34183
34574
  }
34184
34575
  return {
34185
- pathRelLauncher: relLauncher,
34576
+ workspaceRelPath,
34186
34577
  additions,
34187
34578
  deletions,
34188
34579
  change,
34189
- ...movedFromPathRelLauncher ? { movedFromPathRelLauncher } : {},
34580
+ ...movedFromWorkspaceRelPath ? { movedFromWorkspaceRelPath } : {},
34190
34581
  ...movedFromPathInRepo ? { movedFromPathInRepo } : {}
34191
34582
  };
34192
34583
  }
@@ -34245,8 +34636,8 @@ async function unifiedDiffForFileInRange(repoCwd, range, pathInRepo, change, mov
34245
34636
  }
34246
34637
 
34247
34638
  // src/git/changes/total-lines/resolve-changed-file-total-lines.ts
34248
- import * as fs16 from "node:fs";
34249
- import * as path19 from "node:path";
34639
+ import * as fs17 from "node:fs";
34640
+ import * as path23 from "node:path";
34250
34641
 
34251
34642
  // src/git/changes/lines/count-lines.ts
34252
34643
  import { createReadStream } from "node:fs";
@@ -34292,13 +34683,13 @@ async function countLinesInGitRef(repoGitCwd, ref, pathInRepo) {
34292
34683
  }
34293
34684
  async function resolveWorkingTreeFileTotalLines(options) {
34294
34685
  if (options.isBinary) return null;
34295
- const filePath = path19.join(options.repoGitCwd, options.pathInRepo);
34686
+ const filePath = path23.join(options.repoGitCwd, options.pathInRepo);
34296
34687
  if (options.change === "removed") {
34297
34688
  return countLinesInGitRef(options.repoGitCwd, "HEAD", options.pathInRepo);
34298
34689
  }
34299
34690
  if (options.change === "moved" || options.change === "modified") {
34300
34691
  try {
34301
- const st = await fs16.promises.stat(filePath);
34692
+ const st = await fs17.promises.stat(filePath);
34302
34693
  if (!st.isFile()) return null;
34303
34694
  return await countTextFileLines(filePath);
34304
34695
  } catch {
@@ -34318,14 +34709,14 @@ async function resolveCommitFileTotalLines(options) {
34318
34709
  return null;
34319
34710
  }
34320
34711
 
34321
- // src/git/changes/listing/path-in-repo-from-launcher.ts
34322
- function pathInRepoFromLauncher(pathRelLauncher, repoRelPath) {
34712
+ // src/git/changes/listing/path-in-repo-from-workspace.ts
34713
+ function pathInRepoFromWorkspace(workspaceRelPath, repoRelPath) {
34323
34714
  const normRel = repoRelPath === "." || repoRelPath === "" ? "." : repoRelPath;
34324
- if (normRel === ".") return pathRelLauncher;
34325
- if (pathRelLauncher.startsWith(`${normRel}/`)) {
34326
- return pathRelLauncher.slice(normRel.length + 1);
34715
+ if (normRel === ".") return workspaceRelPath;
34716
+ if (workspaceRelPath.startsWith(`${normRel}/`)) {
34717
+ return workspaceRelPath.slice(normRel.length + 1);
34327
34718
  }
34328
- return pathRelLauncher;
34719
+ return workspaceRelPath;
34329
34720
  }
34330
34721
  function normalizeRepoRelPath(repoRelPath) {
34331
34722
  return repoRelPath === "." || repoRelPath === "" ? "." : repoRelPath;
@@ -34334,7 +34725,7 @@ function normalizeRepoRelPath(repoRelPath) {
34334
34725
  // src/git/changes/listing/enrich-commit-file-rows.ts
34335
34726
  async function enrichCommitFileRows(options) {
34336
34727
  await forEachWithGitYield(options.rows, async (row) => {
34337
- const pathInRepo = pathInRepoFromLauncher(row.pathRelLauncher, options.repoRelPath);
34728
+ const pathInRepo = pathInRepoFromWorkspace(row.workspaceRelPath, options.repoRelPath);
34338
34729
  row.patchContent = await unifiedDiffForFileInRange(
34339
34730
  options.repoGitCwd,
34340
34731
  options.range,
@@ -34353,102 +34744,6 @@ async function enrichCommitFileRows(options) {
34353
34744
  });
34354
34745
  }
34355
34746
 
34356
- // src/git/changes/parse/parse-name-status-entries.ts
34357
- function parseNameStatusEntries(lines) {
34358
- const entries = [];
34359
- for (const line of lines) {
34360
- if (!line.trim()) continue;
34361
- const tabParts = line.split(" ");
34362
- if (tabParts.length < 2) continue;
34363
- const status = tabParts[0].trim();
34364
- const code = status[0];
34365
- if (code === "A") {
34366
- entries.push({ pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]), change: "added" });
34367
- } else if (code === "D") {
34368
- entries.push({ pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]), change: "removed" });
34369
- } else if (code === "R" || code === "C") {
34370
- if (tabParts.length >= 3) {
34371
- entries.push({
34372
- pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]),
34373
- oldPathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 2]),
34374
- change: "moved"
34375
- });
34376
- }
34377
- } else if (code === "M" || code === "U" || code === "T") {
34378
- entries.push({ pathInRepo: normalizeGitDiffPath(tabParts[tabParts.length - 1]), change: "modified" });
34379
- }
34380
- }
34381
- return entries;
34382
- }
34383
- function parseNameStatusLines(lines) {
34384
- const m = /* @__PURE__ */ new Map();
34385
- for (const entry of parseNameStatusEntries(lines)) {
34386
- m.set(entry.pathInRepo, entry.change);
34387
- }
34388
- return m;
34389
- }
34390
-
34391
- // src/git/changes/parse/parse-numstat-entries.ts
34392
- function parseNumstatCounts(parts) {
34393
- const [a, d] = parts;
34394
- return {
34395
- additions: a === "-" ? 0 : parseInt(String(a), 10) || 0,
34396
- deletions: d === "-" ? 0 : parseInt(String(d), 10) || 0
34397
- };
34398
- }
34399
- function parseNumstatLine(line) {
34400
- const parts = line.split(" ");
34401
- if (parts.length < 3) return null;
34402
- const rawPath = parts[parts.length - 1];
34403
- const { additions, deletions } = parseNumstatCounts(parts);
34404
- const expanded = expandGitRenameNumstatPath(rawPath);
34405
- const pathInRepo = normalizeGitDiffPath(expanded?.newPath ?? rawPath);
34406
- return {
34407
- pathInRepo,
34408
- additions,
34409
- deletions,
34410
- ...expanded ? { oldPathInRepo: expanded.oldPath } : {}
34411
- };
34412
- }
34413
- function parseNumstatEntries(lines) {
34414
- const m = /* @__PURE__ */ new Map();
34415
- for (const line of lines) {
34416
- if (!line.trim()) continue;
34417
- const parts = line.split(" ");
34418
- if (parts.length < 3) continue;
34419
- const rawPath = parts[parts.length - 1];
34420
- const expanded = expandGitRenameNumstatPath(rawPath);
34421
- const entry = parseNumstatLine(line);
34422
- if (!entry) continue;
34423
- const existing = m.get(entry.pathInRepo);
34424
- if (!existing || expanded) {
34425
- m.set(entry.pathInRepo, entry);
34426
- }
34427
- }
34428
- return m;
34429
- }
34430
- function parseNumstat(lines) {
34431
- const m = /* @__PURE__ */ new Map();
34432
- for (const [path46, entry] of parseNumstatEntries(lines)) {
34433
- m.set(path46, { additions: entry.additions, deletions: entry.deletions });
34434
- }
34435
- return m;
34436
- }
34437
-
34438
- // src/git/changes/parse/numstat-from-git-no-index.ts
34439
- async function numstatFromGitNoIndex(g, pathInRepo) {
34440
- const devNull = process.platform === "win32" ? "NUL" : "/dev/null";
34441
- try {
34442
- const out = await g.raw(["diff", "--numstat", "--no-index", "--", devNull, pathInRepo]);
34443
- const first2 = String(out).split("\n").find((l) => l.trim()) ?? "";
34444
- const parsed = parseNumstatLine(first2);
34445
- if (!parsed) return null;
34446
- return { additions: parsed.additions, deletions: parsed.deletions };
34447
- } catch {
34448
- return null;
34449
- }
34450
- }
34451
-
34452
34747
  // src/git/changes/rows/collect-moved-source-paths.ts
34453
34748
  function collectMovedSourcePaths(nameEntries, numByPath) {
34454
34749
  const sources = /* @__PURE__ */ new Set();
@@ -34528,8 +34823,8 @@ function pickPreferredChangedFileRow(a, b) {
34528
34823
  const aHasPatch = Boolean(a.patchContent?.trim());
34529
34824
  const bHasPatch = Boolean(b.patchContent?.trim());
34530
34825
  if (aHasPatch !== bHasPatch) return aHasPatch ? a : b;
34531
- const aHasMoveMeta = Boolean(a.movedFromPathInRepo || a.movedFromPathRelLauncher);
34532
- const bHasMoveMeta = Boolean(b.movedFromPathInRepo || b.movedFromPathRelLauncher);
34826
+ const aHasMoveMeta = Boolean(a.movedFromPathInRepo || a.movedFromWorkspaceRelPath);
34827
+ const bHasMoveMeta = Boolean(b.movedFromPathInRepo || b.movedFromWorkspaceRelPath);
34533
34828
  if (aHasMoveMeta !== bHasMoveMeta) return aHasMoveMeta ? a : b;
34534
34829
  return a;
34535
34830
  }
@@ -34538,30 +34833,39 @@ function pickPreferredChangedFileRow(a, b) {
34538
34833
  function dedupeChangedFileRows(rows) {
34539
34834
  const byPath = /* @__PURE__ */ new Map();
34540
34835
  for (const row of rows) {
34541
- const key = normalizeGitDiffPath(row.pathRelLauncher);
34542
- const normalizedRow = key === row.pathRelLauncher ? row : { ...row, pathRelLauncher: key };
34836
+ const key = normalizeGitDiffPath(row.workspaceRelPath);
34837
+ const normalizedRow = key === row.workspaceRelPath ? row : { ...row, workspaceRelPath: key };
34543
34838
  const existing = byPath.get(key);
34544
34839
  if (!existing) {
34545
34840
  byPath.set(key, normalizedRow);
34546
34841
  continue;
34547
34842
  }
34548
34843
  const preferRow = pickPreferredChangedFileRow(existing, normalizedRow);
34549
- byPath.set(key, { ...preferRow, pathRelLauncher: key });
34844
+ byPath.set(key, { ...preferRow, workspaceRelPath: key });
34550
34845
  }
34551
34846
  return [...byPath.values()];
34552
34847
  }
34553
34848
 
34554
34849
  // src/git/changes/listing/list-changed-files-for-commit.ts
34555
- async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
34850
+ var COMMIT_FILES_CACHE_TTL_MS = 5 * 6e4;
34851
+ var commitFilesCache = /* @__PURE__ */ new Map();
34852
+ function commitFilesCacheKey(repoGitCwd, commitSha) {
34853
+ return `${path24.resolve(repoGitCwd)}:${commitSha}`;
34854
+ }
34855
+ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha, options = {}) {
34856
+ const cacheKey = commitFilesCacheKey(repoGitCwd, commitSha);
34857
+ if (!options.includeFilePatches) {
34858
+ const now = Date.now();
34859
+ const cached2 = commitFilesCache.get(cacheKey);
34860
+ if (cached2 && cached2.expiresAt > now) return cached2.rows;
34861
+ }
34556
34862
  const g = cliSimpleGit(repoGitCwd);
34557
- const parent = await parentForCommitDiff(g, commitSha);
34558
- const range = `${parent}..${commitSha}`;
34863
+ const normRel = normalizeRepoRelPath(repoRelPath);
34559
34864
  const [nameStatusRaw, numstatRaw] = await Promise.all([
34560
- g.raw(["diff", ...RENAME_DIFF_ARGS, "--name-status", range]).catch(() => ""),
34561
- g.raw(["diff", ...RENAME_DIFF_ARGS, "--numstat", range]).catch(() => "")
34865
+ g.raw(["diff-tree", "--no-commit-id", "--name-status", "-r", ...RENAME_DIFF_ARGS, commitSha]).catch(() => ""),
34866
+ g.raw(["show", "--numstat", "--format=format:", commitSha]).catch(() => "")
34562
34867
  ]);
34563
34868
  const parsed = parseDiffOutput(String(nameStatusRaw), String(numstatRaw));
34564
- const normRel = normalizeRepoRelPath(repoRelPath);
34565
34869
  const paths = collectChangedPaths({
34566
34870
  nameEntries: parsed.nameEntries,
34567
34871
  numByPath: parsed.numByPath
@@ -34574,29 +34878,39 @@ async function listChangedFilesForCommit(repoGitCwd, repoRelPath, commitSha) {
34574
34878
  movedSourcePaths: parsed.movedSourcePaths
34575
34879
  });
34576
34880
  rows = dedupeChangedFileRows(rows);
34577
- await enrichCommitFileRows({
34578
- rows,
34579
- repoGitCwd,
34580
- repoRelPath: normRel,
34581
- range,
34582
- commitSha,
34583
- parentSha: parent,
34584
- binaryByPath: parsed.binaryByPath
34585
- });
34881
+ if (options.includeFilePatches) {
34882
+ const parent = await parentForCommitDiff(g, commitSha);
34883
+ const range = `${parent}..${commitSha}`;
34884
+ await enrichCommitFileRows({
34885
+ rows,
34886
+ repoGitCwd,
34887
+ repoRelPath: normRel,
34888
+ range,
34889
+ commitSha,
34890
+ parentSha: parent,
34891
+ binaryByPath: parsed.binaryByPath
34892
+ });
34893
+ }
34586
34894
  const finalRows = dedupeChangedFileRows(rows);
34587
- finalRows.sort((a, b) => a.pathRelLauncher.localeCompare(b.pathRelLauncher));
34895
+ finalRows.sort((a, b) => a.workspaceRelPath.localeCompare(b.workspaceRelPath));
34896
+ if (!options.includeFilePatches) {
34897
+ commitFilesCache.set(cacheKey, {
34898
+ rows: finalRows,
34899
+ expiresAt: Date.now() + COMMIT_FILES_CACHE_TTL_MS
34900
+ });
34901
+ }
34588
34902
  return finalRows;
34589
34903
  }
34590
34904
 
34591
34905
  // src/git/changes/listing/apply-untracked-file-stats.ts
34592
- import * as fs17 from "node:fs";
34593
- import * as path20 from "node:path";
34906
+ import * as fs18 from "node:fs";
34907
+ import * as path25 from "node:path";
34594
34908
  function createUntrackedStatsApplier(options) {
34595
34909
  return async (row, pathInRepo) => {
34596
34910
  if (!options.untrackedSet.has(pathInRepo)) return;
34597
34911
  if (options.nameByPath.has(pathInRepo)) return;
34598
34912
  if (row.change === "moved") return;
34599
- const repoFilePath = path20.join(options.repoGitCwd, pathInRepo);
34913
+ const repoFilePath = path25.join(options.repoGitCwd, pathInRepo);
34600
34914
  const fromGit = await numstatFromGitNoIndex(options.g, pathInRepo);
34601
34915
  if (fromGit) {
34602
34916
  row.additions = fromGit.additions;
@@ -34604,7 +34918,7 @@ function createUntrackedStatsApplier(options) {
34604
34918
  return;
34605
34919
  }
34606
34920
  try {
34607
- const st = await fs17.promises.stat(repoFilePath);
34921
+ const st = await fs18.promises.stat(repoFilePath);
34608
34922
  row.additions = st.isFile() ? await countTextFileLines(repoFilePath) : 0;
34609
34923
  } catch {
34610
34924
  row.additions = 0;
@@ -34614,13 +34928,75 @@ function createUntrackedStatsApplier(options) {
34614
34928
  }
34615
34929
 
34616
34930
  // src/git/changes/listing/enrich-working-tree-file-rows.ts
34617
- import * as path21 from "node:path";
34931
+ import * as path26 from "node:path";
34618
34932
 
34619
- // src/git/changes/patch/hydrate-patch.ts
34620
- import * as fs18 from "node:fs";
34933
+ // src/git/changes/patch/hydrate/append-injected-context-lines.ts
34934
+ async function appendInjectedContextLines(out, inject, maybeYield, injectedTotal) {
34935
+ for (const t of inject) {
34936
+ out.push(` ${t}`);
34937
+ injectedTotal.value++;
34938
+ await maybeYield();
34939
+ }
34940
+ }
34941
+
34942
+ // src/git/changes/patch/hydrate/constants.ts
34621
34943
  var UNIFIED_HUNK_HEADER_RE = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
34622
34944
  var MAX_HYDRATE_LINES_PER_GAP = 8e3;
34623
34945
  var MAX_HYDRATE_LINES_PER_FILE = 8e4;
34946
+ var HYDRATE_YIELD_EVERY_LINES = 512;
34947
+
34948
+ // src/git/changes/patch/hydrate/collect-gap-inject-lines.ts
34949
+ async function collectGapInjectLines(options) {
34950
+ if (options.injectedTotal >= MAX_HYDRATE_LINES_PER_FILE) return null;
34951
+ const remaining = MAX_HYDRATE_LINES_PER_FILE - options.injectedTotal;
34952
+ if (options.gapNewEnd >= options.gapNewStart && options.change !== "removed") {
34953
+ const nNew = options.gapNewEnd - options.gapNewStart + 1;
34954
+ if (options.gapOldEnd < options.gapOldStart || options.gapOldEnd - options.gapOldStart + 1 === nNew) {
34955
+ const cap = Math.min(nNew, MAX_HYDRATE_LINES_PER_GAP, remaining);
34956
+ const dl = await options.diskLines();
34957
+ if (dl && cap > 0) {
34958
+ return dl.slice(options.gapNewStart - 1, options.gapNewStart - 1 + cap);
34959
+ }
34960
+ }
34961
+ return null;
34962
+ }
34963
+ if (options.gapOldEnd >= options.gapOldStart && options.change === "removed") {
34964
+ const nOld = options.gapOldEnd - options.gapOldStart + 1;
34965
+ const cap = Math.min(nOld, MAX_HYDRATE_LINES_PER_GAP, remaining);
34966
+ const bl = await options.blobLines();
34967
+ if (bl && cap > 0) {
34968
+ return bl.slice(options.gapOldStart - 1, options.gapOldStart - 1 + cap);
34969
+ }
34970
+ }
34971
+ return null;
34972
+ }
34973
+
34974
+ // src/git/changes/patch/hydrate/consume-unified-hunk-body.ts
34975
+ async function consumeUnifiedHunkBody(options) {
34976
+ let i = options.startIndex;
34977
+ let oldConsumed = 0;
34978
+ let newConsumed = 0;
34979
+ while (i < options.all.length) {
34980
+ await options.maybeYield();
34981
+ const bl = options.all[i];
34982
+ if (UNIFIED_HUNK_HEADER_RE.test(bl)) break;
34983
+ options.out.push(bl);
34984
+ i++;
34985
+ if (bl.startsWith("\\")) continue;
34986
+ const ch = bl[0];
34987
+ if (ch === " ") {
34988
+ oldConsumed++;
34989
+ newConsumed++;
34990
+ } else if (ch === "-") {
34991
+ oldConsumed++;
34992
+ } else if (ch === "+") {
34993
+ newConsumed++;
34994
+ }
34995
+ }
34996
+ return { nextIndex: i, oldConsumed, newConsumed };
34997
+ }
34998
+
34999
+ // src/git/changes/patch/hydrate/read-git-blob-lines.ts
34624
35000
  async function readGitBlobLines(repoCwd, pathInRepo) {
34625
35001
  try {
34626
35002
  const rel = pathInRepo.replace(/\\/g, "/");
@@ -34630,35 +35006,66 @@ async function readGitBlobLines(repoCwd, pathInRepo) {
34630
35006
  return null;
34631
35007
  }
34632
35008
  }
35009
+
35010
+ // src/git/changes/patch/hydrate/read-worktree-file-lines.ts
35011
+ import * as fs19 from "node:fs";
34633
35012
  async function readWorktreeFileLines(filePath) {
34634
35013
  try {
34635
- const raw = await fs18.promises.readFile(filePath, "utf8");
35014
+ const raw = await fs19.promises.readFile(filePath, "utf8");
34636
35015
  return raw.split(/\r?\n/);
34637
35016
  } catch {
34638
35017
  return null;
34639
35018
  }
34640
35019
  }
34641
- async function hydrateUnifiedPatchWithFileContext(patch, filePath, repoGitCwd, pathInRepo, change) {
34642
- if (!patch.trim() || patch.includes("Binary files")) return patch;
34643
- const all = patch.split("\n");
34644
- const out = [];
34645
- let prevOldEnd = 0;
34646
- let prevNewEnd = 0;
34647
- let injectedTotal = 0;
34648
- let i = 0;
35020
+
35021
+ // src/git/changes/patch/hydrate/create-hydrate-source-lines.ts
35022
+ function createHydrateSourceLines(options) {
34649
35023
  let blobCache;
34650
35024
  let diskCache;
34651
35025
  const blobLines = async () => {
34652
35026
  if (blobCache !== void 0) return blobCache;
34653
- blobCache = await readGitBlobLines(repoGitCwd, pathInRepo);
35027
+ blobCache = await readGitBlobLines(options.repoGitCwd, options.pathInRepo);
34654
35028
  return blobCache;
34655
35029
  };
34656
35030
  const diskLines = async () => {
34657
35031
  if (diskCache !== void 0) return diskCache;
34658
- diskCache = await readWorktreeFileLines(filePath);
35032
+ diskCache = await readWorktreeFileLines(options.filePath);
34659
35033
  return diskCache;
34660
35034
  };
35035
+ return { blobLines, diskLines };
35036
+ }
35037
+
35038
+ // src/git/changes/patch/hydrate/create-hydrate-yield.ts
35039
+ function createHydrateYield() {
35040
+ let processedLines = 0;
35041
+ return async () => {
35042
+ processedLines++;
35043
+ if (processedLines % HYDRATE_YIELD_EVERY_LINES === 0) {
35044
+ await yieldToEventLoop();
35045
+ }
35046
+ };
35047
+ }
35048
+
35049
+ // src/git/changes/patch/hydrate/update-hunk-line-ends.ts
35050
+ function updateHunkLineEnds(options) {
35051
+ const prevOldEnd = options.oldStart > 0 ? options.oldStart + options.oldConsumed - 1 : 0;
35052
+ const prevNewEnd = options.newStart > 0 ? options.newStart + options.newConsumed - 1 : 0;
35053
+ return { prevOldEnd, prevNewEnd };
35054
+ }
35055
+
35056
+ // src/git/changes/patch/hydrate/hydrate-unified-patch-with-file-context.ts
35057
+ async function hydrateUnifiedPatchWithFileContext(patch, filePath, repoGitCwd, pathInRepo, change) {
35058
+ if (!patch.trim() || patch.includes("Binary files")) return patch;
35059
+ const all = patch.split("\n");
35060
+ const out = [];
35061
+ let prevOldEnd = 0;
35062
+ let prevNewEnd = 0;
35063
+ const injectedTotal = { value: 0 };
35064
+ let i = 0;
35065
+ const maybeYield = createHydrateYield();
35066
+ const { blobLines, diskLines } = createHydrateSourceLines({ repoGitCwd, pathInRepo, filePath });
34661
35067
  while (i < all.length) {
35068
+ await maybeYield();
34662
35069
  const line = all[i];
34663
35070
  const hm = line.match(UNIFIED_HUNK_HEADER_RE);
34664
35071
  if (!hm) {
@@ -34668,66 +35075,31 @@ async function hydrateUnifiedPatchWithFileContext(patch, filePath, repoGitCwd, p
34668
35075
  }
34669
35076
  const oldStart = parseInt(hm[1], 10) || 0;
34670
35077
  const newStart = parseInt(hm[3], 10) || 0;
34671
- const gapOldStart = prevOldEnd + 1;
34672
- const gapOldEnd = oldStart - 1;
34673
- const gapNewStart = prevNewEnd + 1;
34674
- const gapNewEnd = newStart - 1;
34675
- if (injectedTotal < MAX_HYDRATE_LINES_PER_FILE) {
34676
- let inject = null;
34677
- if (gapNewEnd >= gapNewStart && change !== "removed") {
34678
- const nNew = gapNewEnd - gapNewStart + 1;
34679
- if (gapOldEnd < gapOldStart || gapOldEnd - gapOldStart + 1 === nNew) {
34680
- const cap = Math.min(nNew, MAX_HYDRATE_LINES_PER_GAP, MAX_HYDRATE_LINES_PER_FILE - injectedTotal);
34681
- const dl = await diskLines();
34682
- if (dl && cap > 0) {
34683
- inject = dl.slice(gapNewStart - 1, gapNewStart - 1 + cap);
34684
- }
34685
- }
34686
- } else if (gapOldEnd >= gapOldStart && change === "removed") {
34687
- const nOld = gapOldEnd - gapOldStart + 1;
34688
- const cap = Math.min(nOld, MAX_HYDRATE_LINES_PER_GAP, MAX_HYDRATE_LINES_PER_FILE - injectedTotal);
34689
- const bl = await blobLines();
34690
- if (bl && cap > 0) {
34691
- inject = bl.slice(gapOldStart - 1, gapOldStart - 1 + cap);
34692
- }
34693
- }
34694
- if (inject && inject.length > 0) {
34695
- for (const t of inject) {
34696
- out.push(` ${t}`);
34697
- injectedTotal++;
34698
- }
34699
- }
35078
+ const inject = await collectGapInjectLines({
35079
+ change,
35080
+ gapOldStart: prevOldEnd + 1,
35081
+ gapOldEnd: oldStart - 1,
35082
+ gapNewStart: prevNewEnd + 1,
35083
+ gapNewEnd: newStart - 1,
35084
+ injectedTotal: injectedTotal.value,
35085
+ blobLines,
35086
+ diskLines
35087
+ });
35088
+ if (inject && inject.length > 0) {
35089
+ await appendInjectedContextLines(out, inject, maybeYield, injectedTotal);
34700
35090
  }
34701
35091
  out.push(line);
34702
35092
  i++;
34703
- let oldConsumed = 0;
34704
- let newConsumed = 0;
34705
- while (i < all.length) {
34706
- const bl = all[i];
34707
- if (UNIFIED_HUNK_HEADER_RE.test(bl)) break;
34708
- out.push(bl);
34709
- i++;
34710
- if (bl.startsWith("\\")) continue;
34711
- const ch = bl[0];
34712
- if (ch === " ") {
34713
- oldConsumed++;
34714
- newConsumed++;
34715
- } else if (ch === "-") {
34716
- oldConsumed++;
34717
- } else if (ch === "+") {
34718
- newConsumed++;
34719
- }
34720
- }
34721
- if (oldStart > 0) {
34722
- prevOldEnd = oldStart + oldConsumed - 1;
34723
- } else {
34724
- prevOldEnd = 0;
34725
- }
34726
- if (newStart > 0) {
34727
- prevNewEnd = newStart + newConsumed - 1;
34728
- } else {
34729
- prevNewEnd = 0;
34730
- }
35093
+ const body = await consumeUnifiedHunkBody({ all, startIndex: i, out, maybeYield });
35094
+ i = body.nextIndex;
35095
+ const ends = updateHunkLineEnds({
35096
+ oldStart,
35097
+ newStart,
35098
+ oldConsumed: body.oldConsumed,
35099
+ newConsumed: body.newConsumed
35100
+ });
35101
+ prevOldEnd = ends.prevOldEnd;
35102
+ prevNewEnd = ends.prevNewEnd;
34731
35103
  }
34732
35104
  return truncatePatch(out.join("\n"));
34733
35105
  }
@@ -34735,8 +35107,8 @@ async function hydrateUnifiedPatchWithFileContext(patch, filePath, repoGitCwd, p
34735
35107
  // src/git/changes/listing/enrich-working-tree-file-rows.ts
34736
35108
  async function enrichWorkingTreeFileRows(options) {
34737
35109
  await forEachWithGitYield(options.rows, async (row) => {
34738
- const pathInRepo = pathInRepoFromLauncher(row.pathRelLauncher, options.repoRelPath);
34739
- const filePath = path21.join(options.repoGitCwd, pathInRepo);
35110
+ const pathInRepo = pathInRepoFromWorkspace(row.workspaceRelPath, options.repoRelPath);
35111
+ const filePath = path26.join(options.repoGitCwd, pathInRepo);
34740
35112
  const hydrateKind = row.change === "moved" ? "modified" : row.change;
34741
35113
  let patch = await unifiedDiffForFile(options.repoGitCwd, pathInRepo, row.change, row.movedFromPathInRepo);
34742
35114
  if (patch) {
@@ -34759,7 +35131,7 @@ async function enrichWorkingTreeFileRows(options) {
34759
35131
  }
34760
35132
 
34761
35133
  // src/git/changes/listing/list-changed-files-for-repo.ts
34762
- async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
35134
+ async function listChangedFilesForRepo(repoGitCwd, repoRelPath, options = {}) {
34763
35135
  const g = cliSimpleGit(repoGitCwd);
34764
35136
  const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
34765
35137
  g.raw(["diff", ...RENAME_DIFF_ARGS, "--name-status", "HEAD"]).catch(() => ""),
@@ -34789,23 +35161,101 @@ async function listChangedFilesForRepo(repoGitCwd, repoRelPath) {
34789
35161
  })
34790
35162
  });
34791
35163
  rows = dedupeChangedFileRows(rows);
34792
- await enrichWorkingTreeFileRows({
34793
- rows,
34794
- repoGitCwd,
34795
- repoRelPath,
34796
- binaryByPath: parsed.binaryByPath
34797
- });
35164
+ if (options.includeFilePatches) {
35165
+ await enrichWorkingTreeFileRows({
35166
+ rows,
35167
+ repoGitCwd,
35168
+ repoRelPath,
35169
+ binaryByPath: parsed.binaryByPath
35170
+ });
35171
+ }
34798
35172
  return dedupeChangedFileRows(rows);
34799
35173
  }
34800
35174
 
34801
- // src/git/changes/get-working-tree-change-repo-details.ts
35175
+ // src/git/changes/repo/norm-repo-rel-path.ts
34802
35176
  function normRepoRel(p) {
34803
35177
  const x = p.replace(/\\/g, "/").trim();
34804
35178
  return x === "" ? "." : x;
34805
35179
  }
35180
+
35181
+ // src/git/changes/repo/build-working-tree-change-repo-detail.ts
35182
+ async function loadRecentAndUnpushedCommits(repoDir, recentCommitsLimit) {
35183
+ const [unpushedCommits, recentCommitList] = await Promise.all([
35184
+ listUnpushedCommits(repoDir),
35185
+ listRecentCommitRefs(repoDir, recentCommitsLimit)
35186
+ ]);
35187
+ const unpushedSet = new Set(unpushedCommits.map((c) => c.sha));
35188
+ return {
35189
+ unpushedCommits,
35190
+ recentCommits: recentCommitList.commits.map((c) => ({
35191
+ ...c,
35192
+ needsPush: unpushedSet.has(c.sha)
35193
+ })),
35194
+ recentCommitsHasMore: recentCommitList.hasMore
35195
+ };
35196
+ }
35197
+ async function buildWorkingTreeChangeRepoDetail(options) {
35198
+ const t = path27.resolve(options.targetPath);
35199
+ if (!await isGitRepoDirectory(t)) return null;
35200
+ const g = cliSimpleGit(t);
35201
+ const isCommitView = options.basis.kind === "commit";
35202
+ const sessionWtRoot = options.sessionWorktreeRootPath ? path27.resolve(options.sessionWorktreeRootPath) : null;
35203
+ const legacyNested = options.legacyRepoNestedSessionLayout;
35204
+ await yieldToEventLoop2();
35205
+ const branchPromise = g.raw(["rev-parse", "--abbrev-ref", "HEAD"]).then((out) => out.trim() || "HEAD").catch(() => "HEAD");
35206
+ const remoteUrlPromise = getRemoteOriginUrl(t);
35207
+ const topLevelPromise = sessionWtRoot ? Promise.resolve(t) : g.raw(["rev-parse", "--show-toplevel"]).then((out) => out.trim()).catch(() => t);
35208
+ const [branch, remoteUrl, topOrT] = await Promise.all([
35209
+ branchPromise,
35210
+ remoteUrlPromise,
35211
+ topLevelPromise
35212
+ ]);
35213
+ const remoteDisplay = formatRemoteDisplayLabel(remoteUrl);
35214
+ let repoRelPath;
35215
+ if (sessionWtRoot) {
35216
+ const anchor = legacyNested ? path27.dirname(t) : t;
35217
+ const relNorm = path27.relative(sessionWtRoot, anchor);
35218
+ repoRelPath = relNorm === "" ? "." : relNorm.replace(/\\/g, "/");
35219
+ } else {
35220
+ const top = topOrT;
35221
+ const rel = path27.relative(options.bridgeRoot, path27.resolve(top)).replace(/\\/g, "/") || ".";
35222
+ repoRelPath = rel.startsWith("..") ? path27.basename(path27.resolve(top)) : rel;
35223
+ }
35224
+ const norm = normRepoRel(repoRelPath === "" ? "." : repoRelPath);
35225
+ if (options.repoFilterRelPath && norm !== options.repoFilterRelPath) return null;
35226
+ const repoDisplayName = formatRepoShortTitle(remoteUrl, norm === "." ? "." : norm);
35227
+ const relForList = norm === "." ? "." : norm;
35228
+ const commitSha = isCommitView ? options.basis.sha.trim() : null;
35229
+ const filesPromise = isCommitView && commitSha ? listChangedFilesForCommit(t, relForList, commitSha, {
35230
+ includeFilePatches: options.includeFilePatches
35231
+ }) : listChangedFilesForRepo(t, relForList, { includeFilePatches: options.includeFilePatches });
35232
+ const statusPromise = isCommitView ? Promise.resolve(false) : g.status().then((st) => (st.files?.length ?? 0) > 0);
35233
+ const commitsPromise = isCommitView ? Promise.resolve(null) : loadRecentAndUnpushedCommits(t, options.recentCommitsLimit);
35234
+ const [files, hasUncommittedChanges, commitData] = await Promise.all([
35235
+ filesPromise,
35236
+ statusPromise,
35237
+ commitsPromise
35238
+ ]);
35239
+ return {
35240
+ repoRelPath: norm,
35241
+ repoDisplayName,
35242
+ branch,
35243
+ remoteUrl,
35244
+ remoteDisplay,
35245
+ files,
35246
+ hasUncommittedChanges,
35247
+ unpushedCommits: commitData?.unpushedCommits ?? [],
35248
+ recentCommits: commitData?.recentCommits ?? [],
35249
+ recentCommitsHasMore: commitData?.recentCommitsHasMore ?? false,
35250
+ changesView: isCommitView ? "commit" : "working",
35251
+ changesCommitSha: commitSha
35252
+ };
35253
+ }
35254
+
35255
+ // src/git/changes/repo/get-working-tree-change-repo-details.ts
34806
35256
  async function getWorkingTreeChangeRepoDetails(options) {
34807
- const bridgeRoot = path22.resolve(getBridgeRoot());
34808
- const sessionWtRoot = options.sessionWorktreeRootPath ? path22.resolve(options.sessionWorktreeRootPath) : null;
35257
+ const bridgeRoot = path28.resolve(getBridgeRoot());
35258
+ const sessionWtRoot = options.sessionWorktreeRootPath ? path28.resolve(options.sessionWorktreeRootPath) : null;
34809
35259
  const legacyNested = options.legacyRepoNestedSessionLayout === true;
34810
35260
  const out = [];
34811
35261
  const filter = options.repoFilterRelPath != null ? normRepoRel(options.repoFilterRelPath) : null;
@@ -34818,152 +35268,167 @@ async function getWorkingTreeChangeRepoDetails(options) {
34818
35268
  }
34819
35269
  const basis = filter == null && basisInput.kind === "commit" ? { kind: "working" } : basisInput;
34820
35270
  for (let i = 0; i < options.commitTargetPaths.length; i++) {
34821
- if (i > 0) await yieldToEventLoop2();
34822
- const target = options.commitTargetPaths[i];
34823
- const t = path22.resolve(target);
34824
- if (!await isGitRepoDirectory(t)) continue;
34825
- const g = cliSimpleGit(t);
34826
- let branch = "HEAD";
34827
- try {
34828
- branch = (await g.raw(["rev-parse", "--abbrev-ref", "HEAD"])).trim() || "HEAD";
34829
- } catch {
34830
- branch = "HEAD";
34831
- }
34832
- const remoteUrl = await getRemoteOriginUrl(t);
34833
- const remoteDisplay = formatRemoteDisplayLabel(remoteUrl);
34834
- let repoRelPath;
34835
- if (sessionWtRoot) {
34836
- const anchor = legacyNested ? path22.dirname(t) : t;
34837
- const relNorm = path22.relative(sessionWtRoot, anchor);
34838
- repoRelPath = relNorm === "" ? "." : relNorm.replace(/\\/g, "/");
34839
- } else {
34840
- let top = t;
34841
- try {
34842
- top = (await g.raw(["rev-parse", "--show-toplevel"])).trim();
34843
- } catch {
34844
- top = t;
34845
- }
34846
- const rel = path22.relative(bridgeRoot, path22.resolve(top)).replace(/\\/g, "/") || ".";
34847
- repoRelPath = rel.startsWith("..") ? path22.basename(path22.resolve(top)) : rel;
34848
- }
34849
- const norm = normRepoRel(repoRelPath === "" ? "." : repoRelPath);
34850
- if (filter && norm !== filter) continue;
34851
- const repoDisplayName = formatRepoShortTitle(remoteUrl, norm === "." ? "." : norm);
34852
- const relForList = norm === "." ? "." : norm;
34853
- const files = basis.kind === "commit" ? await listChangedFilesForCommit(t, relForList, basis.sha.trim()) : await listChangedFilesForRepo(t, relForList);
34854
- const st = await g.status();
34855
- const hasUncommittedChanges = (st.files?.length ?? 0) > 0;
34856
- const [unpushedCommits, recentCommitList] = await Promise.all([
34857
- listUnpushedCommits(t),
34858
- listRecentCommits(t, options.recentCommitsLimit)
34859
- ]);
34860
- out.push({
34861
- repoRelPath: norm,
34862
- repoDisplayName,
34863
- branch,
34864
- remoteUrl,
34865
- remoteDisplay,
34866
- files,
34867
- hasUncommittedChanges,
34868
- unpushedCommits,
34869
- recentCommits: recentCommitList.commits,
34870
- recentCommitsHasMore: recentCommitList.hasMore,
34871
- changesView: basis.kind === "commit" ? "commit" : "working",
34872
- changesCommitSha: basis.kind === "commit" ? basis.sha.trim() : null
34873
- });
35271
+ await yieldToEventLoop2();
35272
+ const targetPath = options.commitTargetPaths[i];
35273
+ const detail = await buildWorkingTreeChangeRepoDetail({
35274
+ targetPath,
35275
+ bridgeRoot,
35276
+ sessionWorktreeRootPath: sessionWtRoot,
35277
+ legacyRepoNestedSessionLayout: legacyNested,
35278
+ repoFilterRelPath: filter,
35279
+ basis,
35280
+ recentCommitsLimit: options.recentCommitsLimit,
35281
+ includeFilePatches: options.includeFilePatches
35282
+ });
35283
+ if (!detail) continue;
35284
+ out.push(detail);
34874
35285
  if (filter) return out;
34875
35286
  }
34876
35287
  return out;
34877
35288
  }
34878
35289
 
34879
- // src/git/changes/status/working-tree-changed-path-count.ts
34880
- function workingTreeChangedPathCount(nameStatusLines, numstatLines, untrackedLines) {
34881
- const kindByPath = parseNameStatusLines(nameStatusLines);
34882
- const numByPath = parseNumstat(numstatLines);
34883
- const paths = /* @__PURE__ */ new Set([...kindByPath.keys(), ...numByPath.keys()]);
34884
- for (const p of untrackedLines.map((s) => s.trim()).filter(Boolean)) {
34885
- paths.add(p);
34886
- }
34887
- return paths.size;
34888
- }
34889
-
34890
- // src/git/changes/count-working-tree-changed-files.ts
34891
- async function countWorkingTreeChangedFilesForRepo(repoGitCwd) {
34892
- return runGitTask(async () => {
35290
+ // src/git/changes/repo/get-working-tree-change-file-patch.ts
35291
+ import * as path29 from "node:path";
35292
+ async function getWorkingTreeChangeFilePatch(options) {
35293
+ const repoGitCwd = path29.resolve(options.repoGitCwd);
35294
+ if (!await isGitRepoDirectory(repoGitCwd)) {
35295
+ throw new Error("Not a git repository");
35296
+ }
35297
+ const normRel = options.repoRelPath === "." || options.repoRelPath === "" ? "." : options.repoRelPath;
35298
+ const pathInRepo = pathInRepoFromWorkspace(options.workspaceRelPath, normRel);
35299
+ const movedFromPathInRepo = options.change === "moved" && options.movedFromWorkspaceRelPath ? pathInRepoFromWorkspace(options.movedFromWorkspaceRelPath, normRel) : void 0;
35300
+ if (options.basis.kind === "commit") {
35301
+ const commitSha = options.basis.sha.trim();
35302
+ if (!commitSha) throw new Error("commit sha is required for commit file patch");
34893
35303
  const g = cliSimpleGit(repoGitCwd);
34894
- const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
34895
- g.raw(["diff", "--name-status", "HEAD"]).catch(() => ""),
34896
- g.raw(["diff", "HEAD", "--numstat"]).catch(() => ""),
34897
- g.raw(["ls-files", "--others", "--exclude-standard"]).catch(() => "")
34898
- ]);
34899
- return workingTreeChangedPathCount(
34900
- String(nameStatusRaw).split("\n"),
34901
- String(numstatRaw).split("\n"),
34902
- String(untrackedRaw).split("\n")
35304
+ const parentSha = await parentForCommitDiff(g, commitSha);
35305
+ const range = `${parentSha}..${commitSha}`;
35306
+ const patchContent2 = await unifiedDiffForFileInRange(
35307
+ repoGitCwd,
35308
+ range,
35309
+ pathInRepo,
35310
+ options.change,
35311
+ movedFromPathInRepo
34903
35312
  );
35313
+ const isBinary = Boolean(patchContent2?.includes("Binary files"));
35314
+ const totalLines2 = await resolveCommitFileTotalLines({
35315
+ repoGitCwd,
35316
+ pathInRepo,
35317
+ change: options.change,
35318
+ commitSha,
35319
+ parentSha,
35320
+ isBinary
35321
+ });
35322
+ return { patchContent: patchContent2, totalLines: totalLines2 };
35323
+ }
35324
+ const filePath = path29.join(repoGitCwd, pathInRepo);
35325
+ const hydrateKind = options.change === "moved" ? "modified" : options.change;
35326
+ let patchContent = await unifiedDiffForFile(
35327
+ repoGitCwd,
35328
+ pathInRepo,
35329
+ options.change,
35330
+ movedFromPathInRepo
35331
+ );
35332
+ if (patchContent) {
35333
+ patchContent = await hydrateUnifiedPatchWithFileContext(
35334
+ patchContent,
35335
+ filePath,
35336
+ repoGitCwd,
35337
+ pathInRepo,
35338
+ hydrateKind
35339
+ );
35340
+ }
35341
+ const totalLines = await resolveWorkingTreeFileTotalLines({
35342
+ repoGitCwd,
35343
+ pathInRepo,
35344
+ change: options.change,
35345
+ isBinary: Boolean(patchContent?.includes("Binary files"))
34904
35346
  });
35347
+ return { patchContent, totalLines };
34905
35348
  }
34906
35349
 
34907
- // src/git/status/working-tree-status.ts
34908
- async function getRepoWorkingTreeStatus(repoDir) {
34909
- return runGitTask(async () => {
34910
- const uncommittedFileCount = await countWorkingTreeChangedFilesForRepo(repoDir);
34911
- const hasUncommittedChanges = uncommittedFileCount > 0;
34912
- const ahead = await commitsAheadOfRemoteTracking(repoDir);
34913
- return { hasUncommittedChanges, hasUnpushedCommits: ahead > 0, uncommittedFileCount };
35350
+ // src/worktrees/manager/git/get-session-working-tree-change-details.ts
35351
+ async function getSessionWorkingTreeChangeDetails(cache2, sessionId, discover, opts) {
35352
+ const targets = await resolveCommitTargetsAsync(sessionId, cache2, discover);
35353
+ return getWorkingTreeChangeRepoDetails({
35354
+ commitTargetPaths: targets,
35355
+ sessionWorktreeRootPath: cache2.getWorkingTreeRelRoot(sessionId),
35356
+ legacyRepoNestedSessionLayout: cache2.isLegacyNestedLayout(sessionId),
35357
+ repoFilterRelPath: opts?.repoRelPath?.trim() ? normRepoRel(opts.repoRelPath.trim()) : null,
35358
+ basis: opts?.basis,
35359
+ recentCommitsLimit: opts?.recentCommitsLimit,
35360
+ includeFilePatches: false
34914
35361
  });
34915
35362
  }
34916
- async function aggregateSessionPathsWorkingTreeStatus(paths) {
34917
- let hasUncommittedChanges = false;
34918
- let hasUnpushedCommits = false;
34919
- let uncommittedFileCount = 0;
34920
- await forEachWithGitYield(paths, async (p) => {
34921
- const s = await getRepoWorkingTreeStatus(p);
34922
- uncommittedFileCount += s.uncommittedFileCount;
34923
- if (s.hasUncommittedChanges) hasUncommittedChanges = true;
34924
- if (s.hasUnpushedCommits) hasUnpushedCommits = true;
34925
- });
34926
- return { hasUncommittedChanges, hasUnpushedCommits, uncommittedFileCount };
34927
- }
34928
- async function pushAheadOfUpstreamForPaths(paths) {
34929
- await forEachWithGitYield(paths, async (p) => {
34930
- const g = cliSimpleGit(p);
34931
- const ahead = await commitsAheadOfRemoteTracking(p);
34932
- if (ahead <= 0) return;
34933
- await g.push();
34934
- });
35363
+
35364
+ // src/worktrees/manager/git/parse-working-tree-change-kind.ts
35365
+ function parseWorkingTreeChangeKind(value) {
35366
+ if (value === "added" || value === "removed" || value === "modified" || value === "moved") return value;
35367
+ return null;
34935
35368
  }
34936
35369
 
34937
- // src/git/branches/commit-and-push.ts
34938
- async function gitCommitAllIfDirty(repoDir, message, options) {
34939
- const g = cliSimpleGit(repoDir);
34940
- const st = await g.status();
34941
- if (!st.files?.length) return;
34942
- const branch = options.branch.trim();
34943
- if (!branch) {
34944
- throw new Error("Branch name is required");
34945
- }
34946
- const branches = await g.branchLocal();
34947
- const localNames = new Set(branches.all.map((b) => b.replace(/^\*\s*/, "").trim()));
34948
- if (!localNames.has(branch)) {
34949
- await g.checkoutLocalBranch(branch);
35370
+ // src/worktrees/manager/git/get-session-working-tree-change-file-patch.ts
35371
+ import * as path31 from "node:path";
35372
+
35373
+ // src/git/changes/repo/resolve-checkout-repo-rel-path.ts
35374
+ import * as path30 from "node:path";
35375
+ async function resolveCheckoutRepoRelPath(options) {
35376
+ const t = path30.resolve(options.checkoutPath);
35377
+ const g = cliSimpleGit(t);
35378
+ const sessionWtRoot = options.sessionWorktreeRootPath ? path30.resolve(options.sessionWorktreeRootPath) : null;
35379
+ const legacyNested = options.legacyRepoNestedSessionLayout;
35380
+ let repoRelPath;
35381
+ if (sessionWtRoot) {
35382
+ const anchor = legacyNested ? path30.dirname(t) : t;
35383
+ const relNorm = path30.relative(sessionWtRoot, anchor);
35384
+ repoRelPath = relNorm === "" ? "." : relNorm.replace(/\\/g, "/");
34950
35385
  } else {
34951
- await g.checkout(branch);
35386
+ let top = t;
35387
+ try {
35388
+ await yieldToEventLoop2();
35389
+ top = (await g.raw(["rev-parse", "--show-toplevel"])).trim();
35390
+ } catch {
35391
+ top = t;
35392
+ }
35393
+ const rel = path30.relative(options.bridgeRoot, path30.resolve(top)).replace(/\\/g, "/") || ".";
35394
+ repoRelPath = rel.startsWith("..") ? path30.basename(path30.resolve(top)) : rel;
34952
35395
  }
34953
- await g.add(".");
34954
- await g.commit(message);
34955
- if (options.push) {
34956
- await g.push(["-u", "origin", branch]);
35396
+ return normRepoRel(repoRelPath === "" ? "." : repoRelPath);
35397
+ }
35398
+
35399
+ // src/worktrees/manager/git/get-session-working-tree-change-file-patch.ts
35400
+ async function getSessionWorkingTreeChangeFilePatch(cache2, sessionId, discover, opts) {
35401
+ const targets = await resolveCommitTargetsAsync(sessionId, cache2, discover);
35402
+ const bridgeRoot = path31.resolve(getBridgeRoot());
35403
+ const sessionWtRoot = cache2.getWorkingTreeRelRoot(sessionId) ? path31.resolve(cache2.getWorkingTreeRelRoot(sessionId)) : null;
35404
+ const legacyNested = cache2.isLegacyNestedLayout(sessionId);
35405
+ const filter = normRepoRel(opts.repoRelPath.trim());
35406
+ for (const targetPath of targets) {
35407
+ const checkoutPath = path31.resolve(targetPath);
35408
+ if (!await isGitRepoDirectory(checkoutPath)) continue;
35409
+ const repoRelPath = await resolveCheckoutRepoRelPath({
35410
+ checkoutPath,
35411
+ bridgeRoot,
35412
+ sessionWorktreeRootPath: sessionWtRoot,
35413
+ legacyRepoNestedSessionLayout: legacyNested
35414
+ });
35415
+ if (repoRelPath !== filter) continue;
35416
+ return getWorkingTreeChangeFilePatch({
35417
+ repoGitCwd: checkoutPath,
35418
+ repoRelPath: filter,
35419
+ workspaceRelPath: opts.workspaceRelPath.trim(),
35420
+ change: opts.change,
35421
+ movedFromWorkspaceRelPath: opts.movedFromWorkspaceRelPath?.trim() || null,
35422
+ basis: opts.basis
35423
+ });
34957
35424
  }
35425
+ throw new Error(`Repository not found: ${filter}`);
34958
35426
  }
34959
35427
 
34960
- // src/worktrees/commit-session-worktrees.ts
34961
- async function commitSessionWorktrees(options) {
34962
- const { paths, branch, message, push } = options;
35428
+ // src/worktrees/manager/git/push-session-upstream.ts
35429
+ async function pushSessionUpstream(cache2, sessionId, discover) {
34963
35430
  try {
34964
- for (const wt of paths) {
34965
- await gitCommitAllIfDirty(wt, message, { push, branch });
34966
- }
35431
+ await pushAheadOfUpstreamForPaths(await resolveCommitTargetsAsync(sessionId, cache2, discover));
34967
35432
  return { ok: true };
34968
35433
  } catch (e) {
34969
35434
  const err = e instanceof Error ? e.message : String(e);
@@ -34972,23 +35437,23 @@ async function commitSessionWorktrees(options) {
34972
35437
  }
34973
35438
 
34974
35439
  // src/worktrees/remove-session-worktrees.ts
34975
- import * as fs21 from "node:fs";
35440
+ import * as fs22 from "node:fs";
34976
35441
 
34977
35442
  // src/git/worktrees/worktree-remove.ts
34978
- import * as fs20 from "node:fs";
35443
+ import * as fs21 from "node:fs";
34979
35444
 
34980
35445
  // src/git/worktrees/resolve-main-repo-from-git-file.ts
34981
- import * as fs19 from "node:fs";
34982
- import * as path23 from "node:path";
35446
+ import * as fs20 from "node:fs";
35447
+ import * as path32 from "node:path";
34983
35448
  function resolveMainRepoFromWorktreeGitFile(wt) {
34984
- const gitDirFile = path23.join(wt, ".git");
34985
- if (!fs19.existsSync(gitDirFile) || !fs19.statSync(gitDirFile).isFile()) return "";
34986
- const first2 = fs19.readFileSync(gitDirFile, "utf8").trim();
35449
+ const gitDirFile = path32.join(wt, ".git");
35450
+ if (!fs20.existsSync(gitDirFile) || !fs20.statSync(gitDirFile).isFile()) return "";
35451
+ const first2 = fs20.readFileSync(gitDirFile, "utf8").trim();
34987
35452
  const m = first2.match(/^gitdir:\s*(.+)$/im);
34988
35453
  if (!m) return "";
34989
- const gitWorktreePath = path23.resolve(wt, m[1].trim());
34990
- const gitDir = path23.dirname(path23.dirname(gitWorktreePath));
34991
- return path23.dirname(gitDir);
35454
+ const gitWorktreePath = path32.resolve(wt, m[1].trim());
35455
+ const gitDir = path32.dirname(path32.dirname(gitWorktreePath));
35456
+ return path32.dirname(gitDir);
34992
35457
  }
34993
35458
 
34994
35459
  // src/git/worktrees/worktree-remove.ts
@@ -34997,7 +35462,7 @@ async function gitWorktreeRemoveForce(worktreePath) {
34997
35462
  if (mainRepo) {
34998
35463
  await cliSimpleGit(mainRepo).raw(["worktree", "remove", "--force", worktreePath]);
34999
35464
  } else {
35000
- fs20.rmSync(worktreePath, { recursive: true, force: true });
35465
+ fs21.rmSync(worktreePath, { recursive: true, force: true });
35001
35466
  }
35002
35467
  }
35003
35468
 
@@ -35010,13 +35475,20 @@ async function removeSessionWorktrees(paths, log2) {
35010
35475
  } catch (e) {
35011
35476
  log2(`[worktrees] Remove failed for ${wt}: ${e instanceof Error ? e.message : String(e)}`);
35012
35477
  try {
35013
- fs21.rmSync(wt, { recursive: true, force: true });
35478
+ fs22.rmSync(wt, { recursive: true, force: true });
35014
35479
  } catch {
35015
35480
  }
35016
35481
  }
35017
35482
  }
35018
35483
  }
35019
35484
 
35485
+ // src/worktrees/manager/git/remove-session-worktree-checkouts.ts
35486
+ async function removeSessionWorktreeCheckouts(cache2, sessionId, log2) {
35487
+ const paths = cache2.clearSession(sessionId);
35488
+ if (!paths?.length) return;
35489
+ await removeSessionWorktrees(paths, log2);
35490
+ }
35491
+
35020
35492
  // src/git/branches/rename-branch.ts
35021
35493
  async function gitRenameCurrentBranch(repoDir, newName) {
35022
35494
  const g = cliSimpleGit(repoDir);
@@ -35038,239 +35510,310 @@ async function renameSessionWorktreeBranches(paths, newBranch, log2) {
35038
35510
  }
35039
35511
  }
35040
35512
 
35041
- // src/worktrees/worktree-layout-file.ts
35042
- import * as fs22 from "node:fs";
35043
- import * as path24 from "node:path";
35044
- import os7 from "node:os";
35045
- var LAYOUT_FILENAME = "worktree-launcher-layout.json";
35046
- function defaultWorktreeLayoutPath() {
35047
- return path24.join(os7.homedir(), ".buildautomaton", LAYOUT_FILENAME);
35048
- }
35049
- function normalizeLoadedLayout(raw) {
35050
- if (raw && typeof raw === "object" && "launcherCwds" in raw) {
35051
- const j = raw;
35052
- if (Array.isArray(j.launcherCwds)) return { launcherCwds: j.launcherCwds };
35053
- }
35054
- return { launcherCwds: [] };
35055
- }
35056
- function loadWorktreeLayout() {
35057
- try {
35058
- const p = defaultWorktreeLayoutPath();
35059
- if (!fs22.existsSync(p)) return { launcherCwds: [] };
35060
- const raw = JSON.parse(fs22.readFileSync(p, "utf8"));
35061
- return normalizeLoadedLayout(raw);
35062
- } catch {
35063
- return { launcherCwds: [] };
35064
- }
35065
- }
35066
- function saveWorktreeLayout(layout) {
35067
- try {
35068
- const dir = path24.dirname(defaultWorktreeLayoutPath());
35069
- fs22.mkdirSync(dir, { recursive: true });
35070
- fs22.writeFileSync(defaultWorktreeLayoutPath(), JSON.stringify(layout, null, 2), "utf8");
35071
- } catch {
35072
- }
35073
- }
35074
- function baseNameSafe(pathString) {
35075
- return path24.basename(pathString).replace(/[^a-zA-Z0-9._-]+/g, "-") || "cwd";
35076
- }
35077
- function getLauncherDirNameIfPresent(layout, bridgeRootPath2) {
35078
- const norm = path24.resolve(bridgeRootPath2);
35079
- const existing = layout.launcherCwds.find((e) => path24.resolve(e.absolutePath) === norm);
35080
- return existing?.dirName;
35513
+ // src/worktrees/manager/git/rename-session-worktree-branch.ts
35514
+ async function renameSessionWorktreeBranch(cache2, sessionId, newBranch, log2) {
35515
+ const paths = cache2.getRepoCheckoutPathsRef(sessionId);
35516
+ if (!paths?.length) return;
35517
+ await renameSessionWorktreeBranches(paths, newBranch, log2);
35081
35518
  }
35082
- function allocateDirNameForLauncherCwd(layout, bridgeRootPath2) {
35083
- const existing = getLauncherDirNameIfPresent(layout, bridgeRootPath2);
35084
- if (existing) return existing;
35085
- const norm = path24.resolve(bridgeRootPath2);
35086
- const base = baseNameSafe(norm);
35087
- const used = new Set(layout.launcherCwds.map((e) => e.dirName));
35088
- let name = base;
35089
- let n = 2;
35090
- while (used.has(name)) {
35091
- name = `${base}-${n}`;
35092
- n += 1;
35093
- }
35094
- layout.launcherCwds.push({ absolutePath: norm, dirName: name });
35095
- saveWorktreeLayout(layout);
35096
- return name;
35519
+
35520
+ // src/worktrees/discovery/discover-session-worktree-on-disk.ts
35521
+ import * as fs26 from "node:fs";
35522
+ import * as path37 from "node:path";
35523
+
35524
+ // src/worktrees/discovery/collect-worktree-paths.ts
35525
+ import * as fs24 from "node:fs";
35526
+ import * as path34 from "node:path";
35527
+
35528
+ // src/worktrees/discovery/disk-walk-constants.ts
35529
+ var DISK_WALK_YIELD_EVERY = 64;
35530
+ var LEGACY_WALK_MAX_DEPTH = 12;
35531
+ var SKIP_DISK_WALK_DIR_NAMES = /* @__PURE__ */ new Set([
35532
+ "node_modules",
35533
+ "bower_components",
35534
+ "vendor",
35535
+ "Pods",
35536
+ "Carthage",
35537
+ "DerivedData",
35538
+ ".yarn",
35539
+ ".pnpm-store",
35540
+ "dist",
35541
+ "build",
35542
+ ".next",
35543
+ "coverage",
35544
+ ".turbo",
35545
+ ".cache",
35546
+ "target",
35547
+ "__pycache__"
35548
+ ]);
35549
+ function shouldSkipDiskWalkEntry(name) {
35550
+ return name.startsWith(".") || SKIP_DISK_WALK_DIR_NAMES.has(name);
35097
35551
  }
35098
35552
 
35099
- // src/worktrees/discover-session-worktree-on-disk.ts
35553
+ // src/worktrees/discovery/disk-walk-utils.ts
35100
35554
  import * as fs23 from "node:fs";
35101
- import * as path25 from "node:path";
35102
- function isGitDir(dirPath) {
35555
+ import * as path33 from "node:path";
35556
+ async function yieldDuringDiskWalk(state) {
35557
+ state.entries++;
35558
+ if (state.entries % DISK_WALK_YIELD_EVERY !== 0) return true;
35559
+ if (isCliImmediateShutdownRequested()) return false;
35560
+ await yieldToEventLoop();
35561
+ return !isCliImmediateShutdownRequested();
35562
+ }
35563
+ async function isGitDir(dirPath) {
35103
35564
  try {
35104
- return fs23.existsSync(path25.join(dirPath, ".git"));
35565
+ await fs23.promises.access(path33.join(dirPath, ".git"));
35566
+ return true;
35105
35567
  } catch {
35106
35568
  return false;
35107
35569
  }
35108
35570
  }
35109
- function collectGitRepoRootsUnderDirectory(rootPath) {
35571
+
35572
+ // src/worktrees/discovery/collect-worktree-paths.ts
35573
+ async function collectGitRepoRootsUnderDirectory(rootPath) {
35110
35574
  const out = [];
35111
- const walk = (dir) => {
35112
- if (isGitDir(dir)) {
35113
- out.push(path25.resolve(dir));
35575
+ const state = { entries: 0 };
35576
+ const walk = async (dir) => {
35577
+ if (!await yieldDuringDiskWalk(state)) return;
35578
+ if (await isGitDir(dir)) {
35579
+ out.push(path34.resolve(dir));
35114
35580
  return;
35115
35581
  }
35116
35582
  let entries;
35117
35583
  try {
35118
- entries = fs23.readdirSync(dir, { withFileTypes: true });
35584
+ entries = await fs24.promises.readdir(dir, { withFileTypes: true });
35119
35585
  } catch {
35120
35586
  return;
35121
35587
  }
35122
35588
  for (const e of entries) {
35123
- if (e.name.startsWith(".")) continue;
35124
- const full = path25.join(dir, e.name);
35589
+ if (shouldSkipDiskWalkEntry(e.name)) continue;
35590
+ const full = path34.join(dir, e.name);
35125
35591
  if (!e.isDirectory()) continue;
35126
- walk(full);
35592
+ await walk(full);
35127
35593
  }
35128
35594
  };
35129
- walk(path25.resolve(rootPath));
35130
- return [...new Set(out)];
35595
+ await walk(path34.resolve(rootPath));
35596
+ return { paths: [...new Set(out)], entriesVisited: state.entries };
35131
35597
  }
35132
- function collectWorktreeRootsNamed(root, sessionId, maxDepth) {
35598
+ async function collectWorktreeRootsNamed(root, sessionId, maxDepth = LEGACY_WALK_MAX_DEPTH) {
35133
35599
  const out = [];
35134
- const walk = (dir, depth) => {
35600
+ const state = { entries: 0 };
35601
+ const walk = async (dir, depth) => {
35135
35602
  if (depth > maxDepth) return;
35603
+ if (!await yieldDuringDiskWalk(state)) return;
35136
35604
  let entries;
35137
35605
  try {
35138
- entries = fs23.readdirSync(dir, { withFileTypes: true });
35606
+ entries = await fs24.promises.readdir(dir, { withFileTypes: true });
35139
35607
  } catch {
35140
35608
  return;
35141
35609
  }
35142
35610
  for (const e of entries) {
35143
- if (e.name.startsWith(".")) continue;
35144
- const full = path25.join(dir, e.name);
35611
+ if (shouldSkipDiskWalkEntry(e.name)) continue;
35612
+ const full = path34.join(dir, e.name);
35145
35613
  if (!e.isDirectory()) continue;
35146
35614
  if (e.name === sessionId) {
35147
- if (isGitDir(full)) out.push(path25.resolve(full));
35148
- } else {
35149
- walk(full, depth + 1);
35615
+ if (await isGitDir(full)) out.push(path34.resolve(full));
35616
+ continue;
35150
35617
  }
35618
+ if (await isGitDir(full)) continue;
35619
+ await walk(full, depth + 1);
35151
35620
  }
35152
35621
  };
35153
- walk(root, 0);
35154
- return out;
35622
+ await walk(root, 0);
35623
+ return { paths: out, entriesVisited: state.entries };
35624
+ }
35625
+
35626
+ // src/worktrees/discovery/layout-keys-for-bridge-root.ts
35627
+ import * as path35 from "node:path";
35628
+ function layoutKeysForBridgeRoot(layout, bridgeRoot, keysOnDisk) {
35629
+ const bridge = path35.resolve(bridgeRoot);
35630
+ const preferred = getLauncherDirNameIfPresent(layout, bridgeRoot);
35631
+ const relevant = layout.launcherCwds.filter((e) => {
35632
+ const entry = path35.resolve(e.absolutePath);
35633
+ return bridge === entry || bridge.startsWith(entry + path35.sep) || entry.startsWith(bridge + path35.sep);
35634
+ }).sort((a, b) => path35.resolve(b.absolutePath).length - path35.resolve(a.absolutePath).length).map((e) => e.dirName).filter((name) => keysOnDisk.includes(name));
35635
+ const ordered = [];
35636
+ const seen = /* @__PURE__ */ new Set();
35637
+ const add = (k) => {
35638
+ if (seen.has(k) || !keysOnDisk.includes(k)) return;
35639
+ seen.add(k);
35640
+ ordered.push(k);
35641
+ };
35642
+ if (preferred) add(preferred);
35643
+ for (const k of relevant) add(k);
35644
+ if (relevant.length > 0) {
35645
+ return ordered;
35646
+ }
35647
+ for (const k of keysOnDisk) add(k);
35648
+ return ordered;
35155
35649
  }
35156
- function tryBindingFromSessionDirectory(sessionDir) {
35650
+
35651
+ // src/worktrees/discovery/try-binding-from-session-directory.ts
35652
+ import * as fs25 from "node:fs";
35653
+ import * as path36 from "node:path";
35654
+ async function tryBindingFromSessionDirectory(sessionDir) {
35157
35655
  let st;
35158
35656
  try {
35159
- st = fs23.statSync(sessionDir);
35657
+ st = await fs25.promises.stat(sessionDir);
35160
35658
  } catch {
35161
35659
  return null;
35162
35660
  }
35163
35661
  if (!st.isDirectory()) return null;
35164
- const worktreePaths = collectGitRepoRootsUnderDirectory(sessionDir);
35662
+ const { paths: worktreePaths } = await collectGitRepoRootsUnderDirectory(sessionDir);
35165
35663
  if (worktreePaths.length === 0) return null;
35166
- const abs = path25.resolve(sessionDir);
35664
+ const abs = path36.resolve(sessionDir);
35167
35665
  return {
35168
35666
  sessionParentPath: abs,
35169
35667
  workingTreeRelRoot: abs,
35170
35668
  repoCheckoutPaths: worktreePaths
35171
35669
  };
35172
35670
  }
35173
- function discoverLegacyBindingAscendingFromCheckout(sessionId, checkoutPath) {
35671
+
35672
+ // src/worktrees/discovery/discover-session-worktree-on-disk.ts
35673
+ async function discoverSessionWorktreeOnDisk(options) {
35674
+ const { sessionId, worktreesRootPath, layout, bridgeRoot } = options;
35174
35675
  const sid = sessionId.trim();
35175
35676
  if (!sid) return null;
35176
- const hintR = path25.resolve(checkoutPath);
35177
- let best = null;
35178
- let cur = path25.dirname(hintR);
35179
- for (let i = 0; i < 40; i++) {
35180
- const paths = collectWorktreeRootsNamed(cur, sid, 24);
35181
- if (paths.some((p) => path25.resolve(p) === hintR)) {
35182
- const isolated = resolveIsolatedSessionParentPathFromCheckouts(paths) ?? path25.resolve(paths[0]);
35183
- best = {
35184
- sessionParentPath: path25.resolve(isolated),
35185
- workingTreeRelRoot: path25.resolve(cur),
35186
- repoCheckoutPaths: paths.map((p) => path25.resolve(p))
35187
- };
35188
- }
35189
- const next = path25.dirname(cur);
35190
- if (next === cur) break;
35191
- cur = next;
35677
+ try {
35678
+ await fs26.promises.access(worktreesRootPath);
35679
+ } catch {
35680
+ return null;
35192
35681
  }
35193
- return best;
35194
- }
35195
- function discoverSessionWorktreeOnDisk(options) {
35196
- const { sessionId, worktreesRootPath, layout, bridgeRoot } = options;
35197
- if (!sessionId.trim() || !fs23.existsSync(worktreesRootPath)) return null;
35198
35682
  const preferredKey = getLauncherDirNameIfPresent(layout, bridgeRoot);
35199
- const keys = [];
35200
- if (preferredKey) keys.push(preferredKey);
35683
+ const keysOnDisk = [];
35684
+ if (preferredKey) keysOnDisk.push(preferredKey);
35201
35685
  try {
35202
- for (const name of fs23.readdirSync(worktreesRootPath)) {
35686
+ for (const name of await fs26.promises.readdir(worktreesRootPath)) {
35203
35687
  if (name.startsWith(".")) continue;
35204
- const p = path25.join(worktreesRootPath, name);
35205
- if (!fs23.statSync(p).isDirectory()) continue;
35206
- if (name !== preferredKey) keys.push(name);
35688
+ const p = path37.join(worktreesRootPath, name);
35689
+ let st;
35690
+ try {
35691
+ st = await fs26.promises.stat(p);
35692
+ } catch {
35693
+ continue;
35694
+ }
35695
+ if (!st.isDirectory()) continue;
35696
+ if (!keysOnDisk.includes(name)) keysOnDisk.push(name);
35207
35697
  }
35208
35698
  } catch {
35209
35699
  return null;
35210
35700
  }
35701
+ const keys = layoutKeysForBridgeRoot(layout, bridgeRoot, keysOnDisk);
35211
35702
  for (const key of keys) {
35212
- const layoutRoot = path25.join(worktreesRootPath, key);
35213
- if (!fs23.existsSync(layoutRoot) || !fs23.statSync(layoutRoot).isDirectory()) continue;
35214
- const sessionDir = path25.join(layoutRoot, sessionId);
35215
- const nested = tryBindingFromSessionDirectory(sessionDir);
35703
+ if (isCliImmediateShutdownRequested()) return null;
35704
+ await yieldToEventLoop();
35705
+ const layoutRoot = path37.join(worktreesRootPath, key);
35706
+ let layoutSt;
35707
+ try {
35708
+ layoutSt = await fs26.promises.stat(layoutRoot);
35709
+ } catch {
35710
+ continue;
35711
+ }
35712
+ if (!layoutSt.isDirectory()) continue;
35713
+ const sessionDir = path37.join(layoutRoot, sid);
35714
+ const nested = await tryBindingFromSessionDirectory(sessionDir);
35216
35715
  if (nested) return nested;
35217
- const legacyPaths = collectWorktreeRootsNamed(layoutRoot, sessionId, 24);
35218
- if (legacyPaths.length > 0) {
35219
- const isolated = resolveIsolatedSessionParentPathFromCheckouts(legacyPaths) ?? path25.resolve(legacyPaths[0]);
35716
+ const legacy = await collectWorktreeRootsNamed(layoutRoot, sid);
35717
+ if (legacy.paths.length > 0) {
35718
+ const isolated = resolveIsolatedSessionParentPathFromCheckouts(legacy.paths) ?? path37.resolve(legacy.paths[0]);
35220
35719
  return {
35221
- sessionParentPath: path25.resolve(isolated),
35222
- workingTreeRelRoot: path25.resolve(layoutRoot),
35223
- repoCheckoutPaths: legacyPaths.map((p) => path25.resolve(p))
35720
+ sessionParentPath: path37.resolve(isolated),
35721
+ workingTreeRelRoot: path37.resolve(layoutRoot),
35722
+ repoCheckoutPaths: legacy.paths.map((p) => path37.resolve(p))
35224
35723
  };
35225
35724
  }
35226
35725
  }
35227
35726
  return null;
35228
35727
  }
35229
- function discoverSessionWorktreesUnderSessionWorktreeRoot(sessionWorktreeRootPathOrHint, sessionId) {
35728
+
35729
+ // src/worktrees/discovery/discover-session-worktrees-under-session-worktree-root.ts
35730
+ import * as fs27 from "node:fs";
35731
+ import * as path39 from "node:path";
35732
+
35733
+ // src/worktrees/discovery/discover-legacy-binding-ascending-from-checkout.ts
35734
+ import * as path38 from "node:path";
35735
+ async function discoverLegacyBindingAscendingFromCheckout(sessionId, checkoutPath) {
35736
+ const sid = sessionId.trim();
35737
+ if (!sid) return null;
35738
+ const hintR = path38.resolve(checkoutPath);
35739
+ let best = null;
35740
+ let cur = path38.dirname(hintR);
35741
+ for (let i = 0; i < 40; i++) {
35742
+ if (isCliImmediateShutdownRequested()) return best;
35743
+ await yieldToEventLoop();
35744
+ const paths = await collectWorktreeRootsNamed(cur, sid);
35745
+ if (paths.paths.some((p) => path38.resolve(p) === hintR)) {
35746
+ const isolated = resolveIsolatedSessionParentPathFromCheckouts(paths.paths) ?? path38.resolve(paths.paths[0]);
35747
+ best = {
35748
+ sessionParentPath: path38.resolve(isolated),
35749
+ workingTreeRelRoot: path38.resolve(cur),
35750
+ repoCheckoutPaths: paths.paths.map((p) => path38.resolve(p))
35751
+ };
35752
+ }
35753
+ const next = path38.dirname(cur);
35754
+ if (next === cur) break;
35755
+ cur = next;
35756
+ }
35757
+ return best;
35758
+ }
35759
+
35760
+ // src/worktrees/discovery/discover-session-worktrees-under-session-worktree-root.ts
35761
+ async function discoverSessionWorktreesUnderSessionWorktreeRoot(sessionWorktreeRootPathOrHint, sessionId) {
35230
35762
  const sid = sessionId.trim();
35231
35763
  if (!sid) return null;
35232
- const hint = path25.resolve(sessionWorktreeRootPathOrHint);
35233
- const underHint = tryBindingFromSessionDirectory(path25.join(hint, sid));
35764
+ const hint = path39.resolve(sessionWorktreeRootPathOrHint);
35765
+ const underHint = await tryBindingFromSessionDirectory(path39.join(hint, sid));
35234
35766
  if (underHint) return underHint;
35235
- const direct = tryBindingFromSessionDirectory(hint);
35767
+ const direct = await tryBindingFromSessionDirectory(hint);
35236
35768
  if (direct) {
35237
- if (path25.basename(hint) === sid && isGitDir(hint)) {
35238
- const legacyFromCheckout = discoverLegacyBindingAscendingFromCheckout(sid, hint);
35769
+ if (path39.basename(hint) === sid && await isGitDir(hint)) {
35770
+ const legacyFromCheckout = await discoverLegacyBindingAscendingFromCheckout(sid, hint);
35239
35771
  if (legacyFromCheckout && legacyFromCheckout.repoCheckoutPaths.length > direct.repoCheckoutPaths.length) {
35240
35772
  return legacyFromCheckout;
35241
35773
  }
35242
35774
  }
35243
35775
  return direct;
35244
35776
  }
35245
- if (path25.basename(hint) === sid && isGitDir(hint)) {
35246
- const legacyFromCheckout = discoverLegacyBindingAscendingFromCheckout(sid, hint);
35777
+ if (path39.basename(hint) === sid && await isGitDir(hint)) {
35778
+ const legacyFromCheckout = await discoverLegacyBindingAscendingFromCheckout(sid, hint);
35247
35779
  if (legacyFromCheckout) return legacyFromCheckout;
35248
35780
  }
35249
35781
  let st;
35250
35782
  try {
35251
- st = fs23.statSync(hint);
35783
+ st = await fs27.promises.stat(hint);
35252
35784
  } catch {
35253
35785
  return null;
35254
35786
  }
35255
35787
  if (!st.isDirectory()) return null;
35256
- const legacyPaths = collectWorktreeRootsNamed(hint, sid, 24);
35257
- if (legacyPaths.length === 0) return null;
35258
- const isolated = resolveIsolatedSessionParentPathFromCheckouts(legacyPaths) ?? path25.resolve(legacyPaths[0]);
35788
+ const legacyPaths = await collectWorktreeRootsNamed(hint, sid);
35789
+ if (legacyPaths.paths.length === 0) return null;
35790
+ const isolated = resolveIsolatedSessionParentPathFromCheckouts(legacyPaths.paths) ?? path39.resolve(legacyPaths.paths[0]);
35259
35791
  return {
35260
- sessionParentPath: path25.resolve(isolated),
35792
+ sessionParentPath: path39.resolve(isolated),
35261
35793
  workingTreeRelRoot: hint,
35262
- repoCheckoutPaths: legacyPaths.map((p) => path25.resolve(p))
35794
+ repoCheckoutPaths: legacyPaths.paths.map((p) => path39.resolve(p))
35263
35795
  };
35264
35796
  }
35265
35797
 
35266
35798
  // src/worktrees/manager/discover-session-binding.ts
35267
- function discoverSessionBinding(params) {
35268
- return discoverSessionWorktreeOnDisk({
35269
- sessionId: params.sessionId,
35799
+ var discoverInflightBySessionId = /* @__PURE__ */ new Map();
35800
+ function discoverSessionBindingAsync(params) {
35801
+ const sid = params.sessionId.trim();
35802
+ if (!sid) return Promise.resolve(null);
35803
+ const existing = discoverInflightBySessionId.get(sid);
35804
+ if (existing) return existing;
35805
+ const promise2 = discoverSessionWorktreeOnDisk({
35806
+ sessionId: sid,
35270
35807
  worktreesRootPath: params.worktreesRootPath,
35271
35808
  layout: params.layout,
35272
35809
  bridgeRoot: getBridgeRoot()
35810
+ }).finally(() => {
35811
+ if (discoverInflightBySessionId.get(sid) === promise2) {
35812
+ discoverInflightBySessionId.delete(sid);
35813
+ }
35273
35814
  });
35815
+ discoverInflightBySessionId.set(sid, promise2);
35816
+ return promise2;
35274
35817
  }
35275
35818
 
35276
35819
  // src/worktrees/manager/resolve-isolated-session-parent-path.ts
@@ -35283,12 +35826,17 @@ function resolveIsolatedSessionParentPath(sessionId, cache2, ensureRepoCheckoutP
35283
35826
  if (!paths?.length) return null;
35284
35827
  return resolveIsolatedSessionParentPathFromCheckouts(paths);
35285
35828
  }
35286
- function ensureRepoCheckoutPathsForSession(sessionId, cache2, discover) {
35829
+ function ensureRepoCheckoutPathsForSession(sessionId, cache2) {
35830
+ if (!sessionId?.trim()) return void 0;
35831
+ const cached2 = cache2.getRepoCheckoutPaths(sessionId.trim());
35832
+ return cached2?.length ? [...cached2] : void 0;
35833
+ }
35834
+ async function ensureRepoCheckoutPathsForSessionAsync(sessionId, cache2, discover) {
35287
35835
  if (!sessionId?.trim()) return void 0;
35288
35836
  const sid = sessionId.trim();
35289
35837
  const cached2 = cache2.getRepoCheckoutPaths(sid);
35290
35838
  if (cached2?.length) return cached2;
35291
- const disc = discover(sid);
35839
+ const disc = await discover(sid);
35292
35840
  if (disc?.repoCheckoutPaths.length) {
35293
35841
  cache2.remember(sid, disc);
35294
35842
  return [...disc.repoCheckoutPaths];
@@ -35296,18 +35844,6 @@ function ensureRepoCheckoutPathsForSession(sessionId, cache2, discover) {
35296
35844
  return void 0;
35297
35845
  }
35298
35846
 
35299
- // src/worktrees/manager/resolve-commit-targets.ts
35300
- function resolveCommitTargets(sessionId, cache2, discover) {
35301
- const paths = cache2.getRepoCheckoutPathsRef(sessionId);
35302
- if (paths?.length) return paths;
35303
- const disc = discover(sessionId);
35304
- if (disc?.repoCheckoutPaths.length) {
35305
- cache2.remember(sessionId, disc);
35306
- return disc.repoCheckoutPaths;
35307
- }
35308
- return [getBridgeRoot()];
35309
- }
35310
-
35311
35847
  // src/worktrees/manager/parse-session-parent.ts
35312
35848
  function parseSessionParent(v) {
35313
35849
  if (v === "bridge_root" || v === "worktrees_root") return v;
@@ -35316,8 +35852,8 @@ function parseSessionParent(v) {
35316
35852
  }
35317
35853
 
35318
35854
  // src/worktrees/prepare-new-session-worktrees.ts
35319
- import * as fs24 from "node:fs";
35320
- import * as path26 from "node:path";
35855
+ import * as fs28 from "node:fs";
35856
+ import * as path40 from "node:path";
35321
35857
 
35322
35858
  // src/git/worktrees/worktree-add.ts
35323
35859
  async function gitWorktreeAddBranch(mainRepoPath, worktreePath, branch, baseRef = "HEAD") {
@@ -35339,10 +35875,10 @@ function resolveBaseRefForRepo(relNorm, baseBranches) {
35339
35875
  }
35340
35876
  async function prepareNewSessionWorktrees(options) {
35341
35877
  const { worktreesRootPath, bridgeRoot, sessionId, layout, log: log2, worktreeBaseBranches } = options;
35342
- const bridgeResolved = path26.resolve(bridgeRoot);
35878
+ const bridgeResolved = path40.resolve(bridgeRoot);
35343
35879
  const cwdKey = allocateDirNameForLauncherCwd(layout, bridgeResolved);
35344
- const bridgeKeyDir = path26.join(worktreesRootPath, cwdKey);
35345
- const sessionDir = path26.join(bridgeKeyDir, sessionId);
35880
+ const bridgeKeyDir = path40.join(worktreesRootPath, cwdKey);
35881
+ const sessionDir = path40.join(bridgeKeyDir, sessionId);
35346
35882
  const repos = await discoverGitReposUnderRoot(bridgeResolved);
35347
35883
  if (repos.length === 0) {
35348
35884
  log2("[worktrees] No Git repositories under bridge root; skipping worktree creation.");
@@ -35350,14 +35886,14 @@ async function prepareNewSessionWorktrees(options) {
35350
35886
  }
35351
35887
  const branch = `session-${sessionId}`;
35352
35888
  const worktreePaths = [];
35353
- fs24.mkdirSync(sessionDir, { recursive: true });
35889
+ fs28.mkdirSync(sessionDir, { recursive: true });
35354
35890
  for (const repo of repos) {
35355
- let rel = path26.relative(bridgeResolved, repo.absolutePath);
35356
- if (rel.startsWith("..") || path26.isAbsolute(rel)) continue;
35891
+ let rel = path40.relative(bridgeResolved, repo.absolutePath);
35892
+ if (rel.startsWith("..") || path40.isAbsolute(rel)) continue;
35357
35893
  const relNorm = normalizeRepoRelPath2(rel === "" ? "." : rel);
35358
- const wtPath = relNorm === "." ? sessionDir : path26.join(sessionDir, relNorm);
35894
+ const wtPath = relNorm === "." ? sessionDir : path40.join(sessionDir, relNorm);
35359
35895
  if (relNorm !== ".") {
35360
- fs24.mkdirSync(path26.dirname(wtPath), { recursive: true });
35896
+ fs28.mkdirSync(path40.dirname(wtPath), { recursive: true });
35361
35897
  }
35362
35898
  const baseRef = resolveBaseRefForRepo(relNorm, worktreeBaseBranches);
35363
35899
  try {
@@ -35398,10 +35934,10 @@ async function prepareAndRememberSessionWorktrees(params) {
35398
35934
  }
35399
35935
 
35400
35936
  // src/worktrees/manager/resolve-existing-session-parent-path.ts
35401
- function resolveExistingSessionParentPath(sessionId, cache2, discover) {
35937
+ async function resolveExistingSessionParentPath(sessionId, cache2, discover) {
35402
35938
  const cached2 = cache2.getSessionParentPath(sessionId);
35403
35939
  if (cached2) return cached2;
35404
- const disc = discover();
35940
+ const disc = await discover();
35405
35941
  if (disc) {
35406
35942
  cache2.remember(sessionId, disc);
35407
35943
  return cache2.getSessionParentPath(sessionId);
@@ -35410,9 +35946,9 @@ function resolveExistingSessionParentPath(sessionId, cache2, discover) {
35410
35946
  }
35411
35947
 
35412
35948
  // src/worktrees/manager/resolve-explicit-session-parent-path.ts
35413
- import * as path27 from "node:path";
35414
- function resolveExplicitSessionParentPath(params) {
35415
- const resolved = path27.resolve(params.parentPathRaw);
35949
+ import * as path41 from "node:path";
35950
+ async function resolveExplicitSessionParentPath(params) {
35951
+ const resolved = path41.resolve(params.parentPathRaw);
35416
35952
  if (parseSessionParent(params.sessionParent) !== "worktrees_root") {
35417
35953
  return resolved;
35418
35954
  }
@@ -35420,15 +35956,16 @@ function resolveExplicitSessionParentPath(params) {
35420
35956
  params.cache.remember(params.sessionId, binding);
35421
35957
  return params.cache.getSessionParentPath(params.sessionId) ?? resolved;
35422
35958
  };
35423
- const diskFirst = params.discover();
35959
+ const diskFirst = await params.discover();
35424
35960
  if (diskFirst) return rememberAndReturn(diskFirst);
35425
- const fromRoot = discoverSessionWorktreesUnderSessionWorktreeRoot(resolved, params.sessionId);
35961
+ const fromRoot = await discoverSessionWorktreesUnderSessionWorktreeRoot(resolved, params.sessionId);
35426
35962
  if (fromRoot) return rememberAndReturn(fromRoot);
35427
35963
  let cur = resolved;
35428
35964
  for (let i = 0; i < 16; i++) {
35429
- const tryRoot = discoverSessionWorktreesUnderSessionWorktreeRoot(cur, params.sessionId);
35965
+ await yieldToEventLoop();
35966
+ const tryRoot = await discoverSessionWorktreesUnderSessionWorktreeRoot(cur, params.sessionId);
35430
35967
  if (tryRoot) return rememberAndReturn(tryRoot);
35431
- const next = path27.dirname(cur);
35968
+ const next = path41.dirname(cur);
35432
35969
  if (next === cur) break;
35433
35970
  cur = next;
35434
35971
  }
@@ -35479,173 +36016,117 @@ async function resolveSessionParentPathForPrompt(params) {
35479
36016
  });
35480
36017
  }
35481
36018
 
35482
- // src/worktrees/manager/session-worktree-cache.ts
35483
- import * as path28 from "node:path";
35484
- var SessionWorktreeCache = class {
35485
- sessionRepoCheckoutPaths = /* @__PURE__ */ new Map();
35486
- sessionParentPathBySession = /* @__PURE__ */ new Map();
35487
- sessionWorkingTreeRelRootBySession = /* @__PURE__ */ new Map();
35488
- remember(sessionId, binding) {
35489
- const paths = binding.repoCheckoutPaths.map((p) => path28.resolve(p));
35490
- this.sessionRepoCheckoutPaths.set(sessionId, paths);
35491
- this.sessionParentPathBySession.set(sessionId, path28.resolve(binding.sessionParentPath));
35492
- this.sessionWorkingTreeRelRootBySession.set(sessionId, path28.resolve(binding.workingTreeRelRoot));
35493
- }
35494
- clearSession(sessionId) {
35495
- const paths = this.sessionRepoCheckoutPaths.get(sessionId);
35496
- this.sessionRepoCheckoutPaths.delete(sessionId);
35497
- this.sessionParentPathBySession.delete(sessionId);
35498
- this.sessionWorkingTreeRelRootBySession.delete(sessionId);
35499
- return paths;
35500
- }
35501
- getSessionParentPath(sessionId) {
35502
- return this.sessionParentPathBySession.get(sessionId);
35503
- }
35504
- getWorkingTreeRelRoot(sessionId) {
35505
- return this.sessionWorkingTreeRelRootBySession.get(sessionId) ?? null;
35506
- }
35507
- hasSession(sessionId) {
35508
- return this.sessionParentPathBySession.has(sessionId);
35509
- }
35510
- getRepoCheckoutPaths(sessionId) {
35511
- const paths = this.sessionRepoCheckoutPaths.get(sessionId);
35512
- return paths?.length ? [...paths] : void 0;
35513
- }
35514
- getRepoCheckoutPathsRef(sessionId) {
35515
- const paths = this.sessionRepoCheckoutPaths.get(sessionId);
35516
- return paths?.length ? paths : void 0;
35517
- }
35518
- isLegacyNestedLayout(sessionId) {
35519
- const parent = this.sessionParentPathBySession.get(sessionId);
35520
- const relRoot = this.sessionWorkingTreeRelRootBySession.get(sessionId);
35521
- if (!parent || !relRoot) return false;
35522
- return path28.resolve(parent) !== path28.resolve(relRoot);
35523
- }
35524
- };
36019
+ // src/worktrees/manager/session-worktree-paths.ts
36020
+ function createSessionWorktreePathsApi(ctx) {
36021
+ const discoverAsync = (sessionId) => discoverSessionBindingAsync({
36022
+ sessionId,
36023
+ worktreesRootPath: ctx.worktreesRootPath,
36024
+ layout: ctx.layout
36025
+ });
36026
+ return {
36027
+ discoverAsync,
36028
+ getIsolatedSessionParentPathForSession(sessionId) {
36029
+ return resolveIsolatedSessionParentPath(
36030
+ sessionId,
36031
+ ctx.cache,
36032
+ (sid) => ensureRepoCheckoutPathsForSession(sid, ctx.cache)
36033
+ );
36034
+ },
36035
+ resolveSessionParentPathForPrompt(sessionId, opts) {
36036
+ return resolveSessionParentPathForPrompt({
36037
+ sessionId,
36038
+ cache: ctx.cache,
36039
+ worktreesRootPath: ctx.worktreesRootPath,
36040
+ layout: ctx.layout,
36041
+ log: ctx.log,
36042
+ discover: (sid) => discoverAsync(sid),
36043
+ opts
36044
+ });
36045
+ },
36046
+ usesWorktreeSession(sessionId) {
36047
+ if (!sessionId) return false;
36048
+ return ctx.cache.hasSession(sessionId);
36049
+ },
36050
+ getRepoCheckoutPathsForSession(sessionId) {
36051
+ if (!sessionId) return void 0;
36052
+ return ctx.cache.getRepoCheckoutPaths(sessionId);
36053
+ },
36054
+ ensureRepoCheckoutPathsForSession(sessionId) {
36055
+ return ensureRepoCheckoutPathsForSession(sessionId, ctx.cache);
36056
+ },
36057
+ async ensureRepoCheckoutPathsForSessionAsync(sessionId) {
36058
+ return ensureRepoCheckoutPathsForSessionAsync(sessionId, ctx.cache, (sid) => discoverAsync(sid));
36059
+ },
36060
+ getSessionWorktreeRootForSession(sessionId) {
36061
+ return resolveIsolatedSessionParentPath(
36062
+ sessionId,
36063
+ ctx.cache,
36064
+ (sid) => ensureRepoCheckoutPathsForSession(sid, ctx.cache)
36065
+ );
36066
+ }
36067
+ };
36068
+ }
36069
+
36070
+ // src/worktrees/manager/session-worktree-git.ts
36071
+ function createSessionWorktreeGitApi(ctx) {
36072
+ const paths = createSessionWorktreePathsApi(ctx);
36073
+ return {
36074
+ async renameSessionBranch(sessionId, newBranch) {
36075
+ await renameSessionWorktreeBranch(ctx.cache, sessionId, newBranch, ctx.log);
36076
+ },
36077
+ async removeSessionWorktrees(sessionId) {
36078
+ await removeSessionWorktreeCheckouts(ctx.cache, sessionId, ctx.log);
36079
+ },
36080
+ async commitSession(params) {
36081
+ return commitSessionWorktree(ctx.cache, params);
36082
+ },
36083
+ async getSessionWorkingTreeStatus(sessionId) {
36084
+ return getSessionWorkingTreeStatus(ctx.cache, sessionId, (sid) => paths.discoverAsync(sid));
36085
+ },
36086
+ async getSessionWorkingTreeChangeDetails(sessionId, opts) {
36087
+ return getSessionWorkingTreeChangeDetails(ctx.cache, sessionId, (sid) => paths.discoverAsync(sid), opts);
36088
+ },
36089
+ async getSessionWorkingTreeChangeFilePatch(sessionId, opts) {
36090
+ return getSessionWorkingTreeChangeFilePatch(ctx.cache, sessionId, (sid) => paths.discoverAsync(sid), opts);
36091
+ },
36092
+ async pushSessionUpstream(sessionId) {
36093
+ return pushSessionUpstream(ctx.cache, sessionId, (sid) => paths.discoverAsync(sid));
36094
+ }
36095
+ };
36096
+ }
35525
36097
 
35526
- // src/worktrees/manager/manager.ts
36098
+ // src/worktrees/manager/session-worktree-manager.ts
35527
36099
  var SessionWorktreeManager = class {
35528
- worktreesRootPath;
35529
- log;
35530
- cache = new SessionWorktreeCache();
35531
- layout;
35532
36100
  constructor(options) {
35533
- this.worktreesRootPath = options.worktreesRootPath;
35534
- this.log = options.log;
35535
- this.layout = loadWorktreeLayout();
35536
- }
35537
- discover(sessionId) {
35538
- return discoverSessionBinding({
35539
- sessionId,
35540
- worktreesRootPath: this.worktreesRootPath,
35541
- layout: this.layout
35542
- });
35543
- }
35544
- getIsolatedSessionParentPathForSession(sessionId) {
35545
- return resolveIsolatedSessionParentPath(
35546
- sessionId,
35547
- this.cache,
35548
- (sid) => this.ensureRepoCheckoutPathsForSession(sid)
35549
- );
35550
- }
35551
- resolveSessionParentPathForPrompt(sessionId, opts) {
35552
- return resolveSessionParentPathForPrompt({
35553
- sessionId,
35554
- cache: this.cache,
35555
- worktreesRootPath: this.worktreesRootPath,
35556
- layout: this.layout,
35557
- log: this.log,
35558
- discover: (sid) => this.discover(sid),
35559
- opts
35560
- });
35561
- }
35562
- async renameSessionBranch(sessionId, newBranch) {
35563
- const paths = this.cache.getRepoCheckoutPathsRef(sessionId);
35564
- if (!paths?.length) return;
35565
- await renameSessionWorktreeBranches(paths, newBranch, this.log);
35566
- }
35567
- usesWorktreeSession(sessionId) {
35568
- if (!sessionId) return false;
35569
- return this.cache.hasSession(sessionId);
35570
- }
35571
- getRepoCheckoutPathsForSession(sessionId) {
35572
- if (!sessionId) return void 0;
35573
- return this.cache.getRepoCheckoutPaths(sessionId);
35574
- }
35575
- ensureRepoCheckoutPathsForSession(sessionId) {
35576
- return ensureRepoCheckoutPathsForSession(sessionId, this.cache, (sid) => this.discover(sid));
35577
- }
35578
- getSessionWorktreeRootForSession(sessionId) {
35579
- return this.getIsolatedSessionParentPathForSession(sessionId);
35580
- }
35581
- async removeSessionWorktrees(sessionId) {
35582
- const paths = this.cache.clearSession(sessionId);
35583
- if (!paths?.length) return;
35584
- await removeSessionWorktrees(paths, this.log);
35585
- }
35586
- async commitSession(params) {
35587
- const paths = this.cache.getRepoCheckoutPathsRef(params.sessionId);
35588
- const targets = paths?.length ? paths : [getBridgeRoot()];
35589
- return commitSessionWorktrees({
35590
- paths: targets,
35591
- branch: params.branch,
35592
- message: params.message,
35593
- push: params.push
35594
- });
35595
- }
35596
- async getSessionWorkingTreeStatus(sessionId) {
35597
- return aggregateSessionPathsWorkingTreeStatus(
35598
- resolveCommitTargets(sessionId, this.cache, (sid) => this.discover(sid))
35599
- );
35600
- }
35601
- async getSessionWorkingTreeChangeDetails(sessionId, opts) {
35602
- const targets = resolveCommitTargets(sessionId, this.cache, (sid) => this.discover(sid));
35603
- return getWorkingTreeChangeRepoDetails({
35604
- commitTargetPaths: targets,
35605
- sessionWorktreeRootPath: this.cache.getWorkingTreeRelRoot(sessionId),
35606
- legacyRepoNestedSessionLayout: this.cache.isLegacyNestedLayout(sessionId),
35607
- repoFilterRelPath: opts?.repoRelPath?.trim() ? opts.repoRelPath.trim() : null,
35608
- basis: opts?.basis,
35609
- recentCommitsLimit: opts?.recentCommitsLimit
35610
- });
35611
- }
35612
- async pushSessionUpstream(sessionId) {
35613
- try {
35614
- await pushAheadOfUpstreamForPaths(
35615
- resolveCommitTargets(sessionId, this.cache, (sid) => this.discover(sid))
35616
- );
35617
- return { ok: true };
35618
- } catch (e) {
35619
- const err = e instanceof Error ? e.message : String(e);
35620
- return { ok: false, error: err };
35621
- }
36101
+ const ctx = createSessionWorktreeManagerContext(options);
36102
+ Object.assign(this, createSessionWorktreePathsApi(ctx), createSessionWorktreeGitApi(ctx));
35622
36103
  }
35623
36104
  };
35624
36105
 
35625
36106
  // src/worktrees/manager/default-worktrees-root-path.ts
35626
- import * as path29 from "node:path";
36107
+ import * as path42 from "node:path";
35627
36108
  import os8 from "node:os";
35628
36109
  function defaultWorktreesRootPath() {
35629
- return path29.join(os8.homedir(), ".buildautomaton", "worktrees");
36110
+ return path42.join(os8.homedir(), ".buildautomaton", "worktrees");
35630
36111
  }
35631
36112
 
35632
36113
  // src/files/watch-file-index.ts
35633
36114
  import { watch } from "node:fs";
35634
- import path34 from "node:path";
36115
+ import path47 from "node:path";
35635
36116
 
35636
36117
  // src/files/index/paths.ts
35637
- import path30 from "node:path";
36118
+ import path43 from "node:path";
35638
36119
  import crypto2 from "node:crypto";
35639
36120
  function getCwdHashForFileIndex(resolvedCwd) {
35640
- return crypto2.createHash("sha256").update(path30.resolve(resolvedCwd)).digest("hex").slice(0, INDEX_HASH_LEN);
36121
+ return crypto2.createHash("sha256").update(path43.resolve(resolvedCwd)).digest("hex").slice(0, INDEX_HASH_LEN);
35641
36122
  }
35642
36123
 
35643
36124
  // src/files/index/build-file-index.ts
35644
- import path32 from "node:path";
36125
+ import path45 from "node:path";
35645
36126
 
35646
36127
  // src/files/index/walk-workspace-tree.ts
35647
- import fs25 from "node:fs";
35648
- import path31 from "node:path";
36128
+ import fs29 from "node:fs";
36129
+ import path44 from "node:path";
35649
36130
  var DEPENDENCY_INSTALL_DIR_NAMES = /* @__PURE__ */ new Set([
35650
36131
  "node_modules",
35651
36132
  "bower_components",
@@ -35662,7 +36143,7 @@ function shouldSkipWorkspaceWalkEntry(name) {
35662
36143
  async function walkWorkspaceTreeAsync(dir, baseDir, onFile, state) {
35663
36144
  let names;
35664
36145
  try {
35665
- names = await fs25.promises.readdir(dir);
36146
+ names = await fs29.promises.readdir(dir);
35666
36147
  } catch {
35667
36148
  return;
35668
36149
  }
@@ -35674,18 +36155,18 @@ async function walkWorkspaceTreeAsync(dir, baseDir, onFile, state) {
35674
36155
  if (isCliImmediateShutdownRequested()) throw new CliSqliteInterrupted();
35675
36156
  }
35676
36157
  state.n++;
35677
- const full = path31.join(dir, name);
36158
+ const full = path44.join(dir, name);
35678
36159
  let stat2;
35679
36160
  try {
35680
- stat2 = await fs25.promises.stat(full);
36161
+ stat2 = await fs29.promises.stat(full);
35681
36162
  } catch {
35682
36163
  continue;
35683
36164
  }
35684
- const relative6 = path31.relative(baseDir, full).replace(/\\/g, "/");
36165
+ const relative7 = path44.relative(baseDir, full).replace(/\\/g, "/");
35685
36166
  if (stat2.isDirectory()) {
35686
36167
  await walkWorkspaceTreeAsync(full, baseDir, onFile, state);
35687
36168
  } else if (stat2.isFile()) {
35688
- onFile(relative6);
36169
+ onFile(relative7);
35689
36170
  }
35690
36171
  }
35691
36172
  }
@@ -35694,7 +36175,7 @@ function createWalkYieldState() {
35694
36175
  }
35695
36176
 
35696
36177
  // src/files/index/file-index-sqlite-lock.ts
35697
- import fs26 from "node:fs";
36178
+ import fs30 from "node:fs";
35698
36179
  function isSqliteCorruptError(e) {
35699
36180
  const msg = e instanceof Error ? e.message : String(e);
35700
36181
  return msg.includes("malformed") || msg.includes("database disk image is malformed") || msg.includes("corrupt");
@@ -35708,7 +36189,7 @@ function withFileIndexSqliteLock(fn) {
35708
36189
  if (!isSqliteCorruptError(e)) throw e;
35709
36190
  closeAllCliSqliteConnections();
35710
36191
  try {
35711
- fs26.unlinkSync(getCliSqlitePath());
36192
+ fs30.unlinkSync(getCliSqlitePath());
35712
36193
  } catch {
35713
36194
  }
35714
36195
  chain = Promise.resolve();
@@ -35783,7 +36264,7 @@ async function collectWorkspacePathsAsync(resolved) {
35783
36264
  }
35784
36265
  async function buildFileIndexAsync(cwd) {
35785
36266
  return withFileIndexSqliteLock(async () => {
35786
- const resolved = path32.resolve(cwd);
36267
+ const resolved = path45.resolve(cwd);
35787
36268
  await yieldToEventLoop();
35788
36269
  assertNotShutdown();
35789
36270
  const paths = await collectWorkspacePathsAsync(resolved);
@@ -35795,7 +36276,7 @@ async function buildFileIndexAsync(cwd) {
35795
36276
  }
35796
36277
 
35797
36278
  // src/files/index/ensure-file-index.ts
35798
- import path33 from "node:path";
36279
+ import path46 from "node:path";
35799
36280
 
35800
36281
  // src/files/index/search-file-index.ts
35801
36282
  function escapeLikePattern(fragment) {
@@ -35847,7 +36328,7 @@ async function searchBridgeFilePathsAsync(resolvedCwd, query, limit = 100) {
35847
36328
 
35848
36329
  // src/files/index/ensure-file-index.ts
35849
36330
  async function ensureFileIndexAsync(cwd) {
35850
- const resolved = path33.resolve(cwd);
36331
+ const resolved = path46.resolve(cwd);
35851
36332
  if (await bridgeFileIndexIsPopulated(resolved)) {
35852
36333
  return { fromCache: true, pathCount: await bridgeFileIndexPathCount(resolved) };
35853
36334
  }
@@ -35891,7 +36372,7 @@ function createFsWatcher(resolved, schedule) {
35891
36372
  }
35892
36373
  }
35893
36374
  function startFileIndexWatcher(cwd = getBridgeRoot()) {
35894
- const resolved = path34.resolve(cwd);
36375
+ const resolved = path47.resolve(cwd);
35895
36376
  void buildFileIndexAsync(resolved).catch((e) => {
35896
36377
  if (e instanceof CliSqliteInterrupted) return;
35897
36378
  console.error("[file-index] Initial index build failed:", e);
@@ -35921,7 +36402,7 @@ function startFileIndexWatcher(cwd = getBridgeRoot()) {
35921
36402
  }
35922
36403
 
35923
36404
  // src/connection/create-bridge-connection.ts
35924
- import * as path45 from "node:path";
36405
+ import * as path59 from "node:path";
35925
36406
 
35926
36407
  // src/dev-servers/manager/dev-server-manager.ts
35927
36408
  import { rm as rm2 } from "node:fs/promises";
@@ -35943,15 +36424,15 @@ function sendDevServerStatus(getWs, serverId, status, options) {
35943
36424
 
35944
36425
  // src/dev-servers/process/terminate-child-process.ts
35945
36426
  async function sigtermAndWaitForExit(proc, graceMs, log2, shortId) {
35946
- const exited = new Promise((resolve20) => {
35947
- proc.once("exit", () => resolve20());
36427
+ const exited = new Promise((resolve32) => {
36428
+ proc.once("exit", () => resolve32());
35948
36429
  });
35949
36430
  log2(`[dev-server] Sending SIGTERM to ${shortId} (pid=${proc.pid ?? "?"}).`);
35950
36431
  try {
35951
36432
  proc.kill("SIGTERM");
35952
36433
  } catch {
35953
36434
  }
35954
- await Promise.race([exited, new Promise((resolve20) => setTimeout(resolve20, graceMs))]);
36435
+ await Promise.race([exited, new Promise((resolve32) => setTimeout(resolve32, graceMs))]);
35955
36436
  }
35956
36437
  function forceKillChild(proc, log2, shortId, graceMs) {
35957
36438
  log2(
@@ -35965,7 +36446,7 @@ function forceKillChild(proc, log2, shortId, graceMs) {
35965
36446
  }
35966
36447
 
35967
36448
  // src/dev-servers/process/wire-dev-server-child-process.ts
35968
- import fs27 from "node:fs";
36449
+ import fs31 from "node:fs";
35969
36450
 
35970
36451
  // src/dev-servers/manager/forward-pipe.ts
35971
36452
  function forwardChildPipe(childReadable, terminal, onData) {
@@ -36001,7 +36482,7 @@ function wireDevServerChildProcess(d) {
36001
36482
  d.setPollInterval(void 0);
36002
36483
  return;
36003
36484
  }
36004
- fs27.readFile(d.mergedLogPath, (err, buf) => {
36485
+ fs31.readFile(d.mergedLogPath, (err, buf) => {
36005
36486
  if (err || (d.getSpawnGeneration() ?? 0) !== d.scheduledGen) return;
36006
36487
  if (buf.length <= d.mergedReadPos.value) return;
36007
36488
  const chunk = Buffer.from(buf.subarray(d.mergedReadPos.value));
@@ -36039,7 +36520,7 @@ ${errTail}` : ""}`);
36039
36520
  d.sendStatus(code === 0 || code == null ? "stopped" : "error", detail, tails);
36040
36521
  };
36041
36522
  if (mergedPath) {
36042
- fs27.readFile(mergedPath, (err, buf) => {
36523
+ fs31.readFile(mergedPath, (err, buf) => {
36043
36524
  if (!err && buf.length > d.mergedReadPos.value) {
36044
36525
  const chunk = Buffer.from(buf.subarray(d.mergedReadPos.value));
36045
36526
  if (chunk.length > 0) {
@@ -36141,13 +36622,13 @@ function parseDevServerDefs(servers) {
36141
36622
  }
36142
36623
 
36143
36624
  // src/dev-servers/manager/shell-spawn/utils.ts
36144
- import fs28 from "node:fs";
36625
+ import fs32 from "node:fs";
36145
36626
  function isSpawnEbadf(e) {
36146
36627
  return typeof e === "object" && e !== null && "code" in e && e.code === "EBADF";
36147
36628
  }
36148
36629
  function rmDirQuiet(dir) {
36149
36630
  try {
36150
- fs28.rmSync(dir, { recursive: true, force: true });
36631
+ fs32.rmSync(dir, { recursive: true, force: true });
36151
36632
  } catch {
36152
36633
  }
36153
36634
  }
@@ -36155,7 +36636,7 @@ var cachedDevNullReadFd;
36155
36636
  function devNullReadFd() {
36156
36637
  if (cachedDevNullReadFd === void 0) {
36157
36638
  const devPath = process.platform === "win32" ? "nul" : "/dev/null";
36158
- cachedDevNullReadFd = fs28.openSync(devPath, "r");
36639
+ cachedDevNullReadFd = fs32.openSync(devPath, "r");
36159
36640
  }
36160
36641
  return cachedDevNullReadFd;
36161
36642
  }
@@ -36229,15 +36710,15 @@ function trySpawnShellTruePiped(command, env, cwd, devNullFd, signal) {
36229
36710
 
36230
36711
  // src/dev-servers/manager/shell-spawn/try-spawn-merged-log-file.ts
36231
36712
  import { spawn as spawn8 } from "node:child_process";
36232
- import fs29 from "node:fs";
36713
+ import fs33 from "node:fs";
36233
36714
  import { tmpdir } from "node:os";
36234
- import path35 from "node:path";
36715
+ import path48 from "node:path";
36235
36716
  function trySpawnMergedLogFile(command, env, cwd, signal) {
36236
- const tmpRoot = fs29.mkdtempSync(path35.join(tmpdir(), "ba-devsrv-log-"));
36237
- const logPath = path35.join(tmpRoot, "combined.log");
36717
+ const tmpRoot = fs33.mkdtempSync(path48.join(tmpdir(), "ba-devsrv-log-"));
36718
+ const logPath = path48.join(tmpRoot, "combined.log");
36238
36719
  let logFd;
36239
36720
  try {
36240
- logFd = fs29.openSync(logPath, "a");
36721
+ logFd = fs33.openSync(logPath, "a");
36241
36722
  } catch {
36242
36723
  rmDirQuiet(tmpRoot);
36243
36724
  return null;
@@ -36256,7 +36737,7 @@ function trySpawnMergedLogFile(command, env, cwd, signal) {
36256
36737
  } else {
36257
36738
  proc = spawn8("/bin/sh", ["-c", command], { env, cwd, stdio, ...signal ? { signal } : {} });
36258
36739
  }
36259
- fs29.closeSync(logFd);
36740
+ fs33.closeSync(logFd);
36260
36741
  return {
36261
36742
  proc,
36262
36743
  pipedStdoutStderr: true,
@@ -36265,7 +36746,7 @@ function trySpawnMergedLogFile(command, env, cwd, signal) {
36265
36746
  };
36266
36747
  } catch (e) {
36267
36748
  try {
36268
- fs29.closeSync(logFd);
36749
+ fs33.closeSync(logFd);
36269
36750
  } catch {
36270
36751
  }
36271
36752
  rmDirQuiet(tmpRoot);
@@ -36276,22 +36757,22 @@ function trySpawnMergedLogFile(command, env, cwd, signal) {
36276
36757
 
36277
36758
  // src/dev-servers/manager/shell-spawn/try-spawn-shell-script-log-redirect.ts
36278
36759
  import { spawn as spawn9 } from "node:child_process";
36279
- import fs30 from "node:fs";
36760
+ import fs34 from "node:fs";
36280
36761
  import { tmpdir as tmpdir2 } from "node:os";
36281
- import path36 from "node:path";
36762
+ import path49 from "node:path";
36282
36763
  function shSingleQuote(s) {
36283
36764
  return `'${s.replace(/'/g, `'\\''`)}'`;
36284
36765
  }
36285
36766
  function trySpawnShellScriptLogRedirectUnix(command, env, cwd, signal) {
36286
- const tmpRoot = fs30.mkdtempSync(path36.join(tmpdir2(), "ba-devsrv-sh-"));
36287
- const logPath = path36.join(tmpRoot, "combined.log");
36288
- const innerPath = path36.join(tmpRoot, "_cmd.sh");
36289
- const runnerPath = path36.join(tmpRoot, "_run.sh");
36767
+ const tmpRoot = fs34.mkdtempSync(path49.join(tmpdir2(), "ba-devsrv-sh-"));
36768
+ const logPath = path49.join(tmpRoot, "combined.log");
36769
+ const innerPath = path49.join(tmpRoot, "_cmd.sh");
36770
+ const runnerPath = path49.join(tmpRoot, "_run.sh");
36290
36771
  try {
36291
- fs30.writeFileSync(innerPath, `#!/bin/sh
36772
+ fs34.writeFileSync(innerPath, `#!/bin/sh
36292
36773
  ${command}
36293
36774
  `);
36294
- fs30.writeFileSync(
36775
+ fs34.writeFileSync(
36295
36776
  runnerPath,
36296
36777
  `#!/bin/sh
36297
36778
  cd ${shSingleQuote(cwd)}
@@ -36317,13 +36798,13 @@ cd ${shSingleQuote(cwd)}
36317
36798
  }
36318
36799
  }
36319
36800
  function trySpawnShellScriptLogRedirectWin(command, env, cwd, signal) {
36320
- const tmpRoot = fs30.mkdtempSync(path36.join(tmpdir2(), "ba-devsrv-sh-"));
36321
- const logPath = path36.join(tmpRoot, "combined.log");
36322
- const runnerPath = path36.join(tmpRoot, "_run.bat");
36801
+ const tmpRoot = fs34.mkdtempSync(path49.join(tmpdir2(), "ba-devsrv-sh-"));
36802
+ const logPath = path49.join(tmpRoot, "combined.log");
36803
+ const runnerPath = path49.join(tmpRoot, "_run.bat");
36323
36804
  const q = (p) => `"${p.replace(/"/g, '""')}"`;
36324
36805
  const com = process.env.ComSpec || "cmd.exe";
36325
36806
  try {
36326
- fs30.writeFileSync(
36807
+ fs34.writeFileSync(
36327
36808
  runnerPath,
36328
36809
  `@ECHO OFF\r
36329
36810
  CD /D ${q(cwd)}\r
@@ -37091,61 +37572,84 @@ function createOnBridgeIdentified(opts) {
37091
37572
  };
37092
37573
  }
37093
37574
 
37094
- // src/skills/discover-local-agent-skills.ts
37095
- import fs31 from "node:fs";
37096
- import path37 from "node:path";
37575
+ // src/skills/discovery/skill-discovery-roots.ts
37097
37576
  var SKILL_DISCOVERY_ROOTS = [".agents/skills", ".claude/skills", ".cursor/skills", "skills"];
37098
- function discoverLocalSkills(cwd) {
37577
+ async function yieldSkillDiscoveryWork(entryCount) {
37578
+ if (entryCount > 0 && entryCount % INDEX_WORK_YIELD_EVERY === 0) {
37579
+ await yieldToEventLoop();
37580
+ }
37581
+ }
37582
+
37583
+ // src/skills/discovery/discover-local-skills.ts
37584
+ import fs35 from "node:fs";
37585
+ import path50 from "node:path";
37586
+ function collectSkillFromDir(rel, base, name, seenKeys, out) {
37587
+ const dir = path50.join(base, name);
37588
+ try {
37589
+ if (!fs35.statSync(dir).isDirectory()) return;
37590
+ } catch {
37591
+ return;
37592
+ }
37593
+ const skillMd = path50.join(dir, "SKILL.md");
37594
+ if (!fs35.existsSync(skillMd)) return;
37595
+ const key = `${rel}/${name}`;
37596
+ if (seenKeys.has(key)) return;
37597
+ seenKeys.add(key);
37598
+ out.push({ skillKey: name, path: `${rel}/${name}`.replace(/\\/g, "/") });
37599
+ }
37600
+ async function discoverLocalSkillsAsync(cwd) {
37099
37601
  const out = [];
37100
37602
  const seenKeys = /* @__PURE__ */ new Set();
37603
+ let work = 0;
37101
37604
  for (const rel of SKILL_DISCOVERY_ROOTS) {
37102
- const base = path37.join(cwd, rel);
37103
- if (!fs31.existsSync(base) || !fs31.statSync(base).isDirectory()) continue;
37605
+ await yieldSkillDiscoveryWork(++work);
37606
+ const base = path50.join(cwd, rel);
37607
+ if (!fs35.existsSync(base) || !fs35.statSync(base).isDirectory()) continue;
37104
37608
  let entries = [];
37105
37609
  try {
37106
- entries = fs31.readdirSync(base);
37610
+ entries = fs35.readdirSync(base);
37107
37611
  } catch {
37108
37612
  continue;
37109
37613
  }
37110
- for (const name of entries) {
37111
- const dir = path37.join(base, name);
37112
- try {
37113
- if (!fs31.statSync(dir).isDirectory()) continue;
37114
- } catch {
37115
- continue;
37116
- }
37117
- const skillMd = path37.join(dir, "SKILL.md");
37118
- if (!fs31.existsSync(skillMd)) continue;
37119
- const key = `${rel}/${name}`;
37120
- if (seenKeys.has(key)) continue;
37121
- seenKeys.add(key);
37122
- out.push({ skillKey: name, path: `${rel}/${name}`.replace(/\\/g, "/") });
37614
+ for (let i = 0; i < entries.length; i++) {
37615
+ await yieldSkillDiscoveryWork(++work);
37616
+ collectSkillFromDir(rel, base, entries[i], seenKeys, out);
37123
37617
  }
37124
37618
  }
37125
37619
  return out;
37126
37620
  }
37127
- function discoverSkillLayoutRoots(cwd) {
37621
+
37622
+ // src/skills/discovery/discover-skill-layout-roots.ts
37623
+ import fs36 from "node:fs";
37624
+ import path51 from "node:path";
37625
+ function collectLayoutSkill(rel, base, name, skills2) {
37626
+ const dir = path51.join(base, name);
37627
+ try {
37628
+ if (!fs36.statSync(dir).isDirectory()) return;
37629
+ } catch {
37630
+ return;
37631
+ }
37632
+ if (!fs36.existsSync(path51.join(dir, "SKILL.md"))) return;
37633
+ const relPath = `${rel}/${name}`.replace(/\\/g, "/");
37634
+ skills2.push({ name, relPath });
37635
+ }
37636
+ async function discoverSkillLayoutRootsAsync(cwd) {
37128
37637
  const roots = [];
37638
+ let work = 0;
37129
37639
  for (const rel of SKILL_DISCOVERY_ROOTS) {
37130
- const base = path37.join(cwd, rel);
37131
- if (!fs31.existsSync(base) || !fs31.statSync(base).isDirectory()) continue;
37640
+ await yieldSkillDiscoveryWork(++work);
37641
+ const base = path51.join(cwd, rel);
37642
+ if (!fs36.existsSync(base) || !fs36.statSync(base).isDirectory()) continue;
37132
37643
  let entries = [];
37133
37644
  try {
37134
- entries = fs31.readdirSync(base);
37645
+ entries = fs36.readdirSync(base);
37135
37646
  } catch {
37136
37647
  continue;
37137
37648
  }
37138
37649
  const skills2 = [];
37139
- for (const name of entries) {
37140
- const dir = path37.join(base, name);
37141
- try {
37142
- if (!fs31.statSync(dir).isDirectory()) continue;
37143
- } catch {
37144
- continue;
37145
- }
37146
- if (!fs31.existsSync(path37.join(dir, "SKILL.md"))) continue;
37147
- const relPath = `${rel}/${name}`.replace(/\\/g, "/");
37148
- skills2.push({ name, relPath });
37650
+ for (let i = 0; i < entries.length; i++) {
37651
+ await yieldSkillDiscoveryWork(++work);
37652
+ collectLayoutSkill(rel, base, entries[i], skills2);
37149
37653
  }
37150
37654
  if (skills2.length > 0) {
37151
37655
  roots.push({ path: rel.replace(/\\/g, "/"), skills: skills2 });
@@ -37187,16 +37691,18 @@ async function detectLocalAgentTypes() {
37187
37691
  function createSendLocalSkillsReport(getWs, logFn) {
37188
37692
  return () => {
37189
37693
  setImmediate(() => {
37190
- try {
37191
- const socket = getWs();
37192
- if (!socket || socket.readyState !== wrapper_default.OPEN) return;
37193
- const skills2 = discoverLocalSkills(getBridgeRoot());
37194
- sendWsMessage(socket, { type: "local_skills", skills: skills2 });
37195
- } catch (e) {
37196
- logFn(
37197
- `[Bridge service] Local skills report failed: ${e instanceof Error ? e.message : String(e)}`
37198
- );
37199
- }
37694
+ void (async () => {
37695
+ try {
37696
+ const socket = getWs();
37697
+ if (!socket || socket.readyState !== wrapper_default.OPEN) return;
37698
+ const skills2 = await discoverLocalSkillsAsync(getBridgeRoot());
37699
+ sendWsMessage(socket, { type: "local_skills", skills: skills2 });
37700
+ } catch (e) {
37701
+ logFn(
37702
+ `[Bridge service] Local skills report failed: ${e instanceof Error ? e.message : String(e)}`
37703
+ );
37704
+ }
37705
+ })();
37200
37706
  });
37201
37707
  };
37202
37708
  }
@@ -37499,7 +38005,7 @@ function syncRunningTurnQueueKeys(turns, queueKey) {
37499
38005
  }
37500
38006
 
37501
38007
  // src/prompt-turn-queue/runner/run-local-revert-before-queued-prompt.ts
37502
- import fs32 from "node:fs";
38008
+ import fs37 from "node:fs";
37503
38009
  async function runLocalRevertBeforeQueuedPrompt(next, deps) {
37504
38010
  if (next.serverState !== "requeued_with_revert") return true;
37505
38011
  const sid = next.sessionId;
@@ -37508,7 +38014,7 @@ async function runLocalRevertBeforeQueuedPrompt(next, deps) {
37508
38014
  const agentBase = deps.sessionWorktreeManager.getSessionWorktreeRootForSession(sid) ?? getBridgeRoot();
37509
38015
  const file2 = snapshotFilePath(agentBase, tid);
37510
38016
  try {
37511
- await fs32.promises.access(file2, fs32.constants.F_OK);
38017
+ await fs37.promises.access(file2, fs37.constants.F_OK);
37512
38018
  } catch {
37513
38019
  deps.log(
37514
38020
  `[Queue] requeued_with_revert: no pre-turn snapshot for ${tid.slice(0, 8)}\u2026; continuing without revert.`
@@ -37623,7 +38129,7 @@ function createBridgePromptSenders(deps, getWs) {
37623
38129
  sendWsMessage(s, wire);
37624
38130
  return true;
37625
38131
  };
37626
- const sendResult2 = (result) => {
38132
+ const sendResult = (result) => {
37627
38133
  const skipEncryptForChangeSummaryFollowUp = result.type === "prompt_result" && result.followUpCatalogPromptId === BUILTIN_SESSION_CHANGE_SUMMARY_FOLLOW_UP_CATALOG_PROMPT_ID;
37628
38134
  const encryptedFields = result.type === "prompt_result" && !skipEncryptForChangeSummaryFollowUp ? ["output", "error"] : [];
37629
38135
  sendBridgeMessage(result, encryptedFields);
@@ -37656,7 +38162,7 @@ function createBridgePromptSenders(deps, getWs) {
37656
38162
  ]) : payload;
37657
38163
  sendWsMessage(s, wire);
37658
38164
  };
37659
- return { sendBridgeMessage, sendResult: sendResult2, sendSessionUpdate };
38165
+ return { sendBridgeMessage, sendResult, sendSessionUpdate };
37660
38166
  }
37661
38167
 
37662
38168
  // src/agents/acp/from-bridge/handle-bridge-prompt/parse-bridge-attachments.ts
@@ -37760,7 +38266,7 @@ async function readGitBranch(cwd) {
37760
38266
  async function runBridgePromptPreamble(params) {
37761
38267
  const { getWs, log: log2, sessionWorktreeManager, sessionId, runId, effectiveCwd } = params;
37762
38268
  const s = getWs();
37763
- const repoCheckoutPaths = sessionWorktreeManager.ensureRepoCheckoutPathsForSession(sessionId);
38269
+ const repoCheckoutPaths = await sessionWorktreeManager.ensureRepoCheckoutPathsForSessionAsync(sessionId);
37764
38270
  const repoRoots = await resolveSnapshotRepoRoots({
37765
38271
  worktreePaths: repoCheckoutPaths,
37766
38272
  fallbackCwd: effectiveCwd,
@@ -37796,9 +38302,9 @@ function parseChangeSummarySnapshots(raw) {
37796
38302
  for (const item of raw) {
37797
38303
  if (!item || typeof item !== "object") continue;
37798
38304
  const o = item;
37799
- const path46 = typeof o.path === "string" && o.path.trim() !== "" ? o.path.trim() : "";
37800
- if (!path46) continue;
37801
- const row = { path: path46 };
38305
+ const path60 = typeof o.path === "string" && o.path.trim() !== "" ? o.path.trim() : "";
38306
+ if (!path60) continue;
38307
+ const row = { path: path60 };
37802
38308
  if (typeof o.patchContent === "string") row.patchContent = o.patchContent;
37803
38309
  if (typeof o.oldText === "string") row.oldText = o.oldText;
37804
38310
  if (typeof o.newText === "string") row.newText = o.newText;
@@ -37832,7 +38338,7 @@ async function runPreambleAndPrompt(params) {
37832
38338
  agentId,
37833
38339
  agentConfig,
37834
38340
  resolvedCwd,
37835
- senders: { sendResult: sendResult2, sendSessionUpdate }
38341
+ senders: { sendResult, sendSessionUpdate }
37836
38342
  } = params;
37837
38343
  const effectiveCwd = resolveSessionParentPathForAgentProcess(resolvedCwd);
37838
38344
  await runBridgePromptPreamble({
@@ -37870,7 +38376,7 @@ async function runPreambleAndPrompt(params) {
37870
38376
  agentId,
37871
38377
  agentConfig,
37872
38378
  sessionParentPath: effectiveCwd,
37873
- sendResult: sendResult2,
38379
+ sendResult,
37874
38380
  sendSessionUpdate,
37875
38381
  followUpCatalogPromptId,
37876
38382
  sessionChangeSummaryFilePaths: pathsForUpload,
@@ -38012,14 +38518,14 @@ var handleSkillCallMessage = (msg, { getWs, log: log2 }) => {
38012
38518
  };
38013
38519
 
38014
38520
  // src/files/list-dir/index.ts
38015
- import fs34 from "node:fs";
38521
+ import fs39 from "node:fs";
38016
38522
 
38017
38523
  // src/files/ensure-under-cwd.ts
38018
- import path38 from "node:path";
38524
+ import path52 from "node:path";
38019
38525
  function ensureUnderCwd(relativePath, cwd = getBridgeRoot()) {
38020
- const normalized = path38.normalize(relativePath).replace(/^(\.\/)+/, "");
38021
- const resolved = path38.resolve(cwd, normalized);
38022
- if (!resolved.startsWith(cwd + path38.sep) && resolved !== cwd) {
38526
+ const normalized = path52.normalize(relativePath).replace(/^(\.\/)+/, "");
38527
+ const resolved = path52.resolve(cwd, normalized);
38528
+ if (!resolved.startsWith(cwd + path52.sep) && resolved !== cwd) {
38023
38529
  return null;
38024
38530
  }
38025
38531
  return resolved;
@@ -38029,15 +38535,15 @@ function ensureUnderCwd(relativePath, cwd = getBridgeRoot()) {
38029
38535
  var LIST_DIR_YIELD_EVERY = 256;
38030
38536
 
38031
38537
  // src/files/list-dir/map-dir-entry.ts
38032
- import path39 from "node:path";
38033
- import fs33 from "node:fs";
38538
+ import path53 from "node:path";
38539
+ import fs38 from "node:fs";
38034
38540
  async function mapDirEntry(d, relativePath, resolved) {
38035
- const entryPath = path39.join(relativePath || ".", d.name).replace(/\\/g, "/");
38036
- const fullPath = path39.join(resolved, d.name);
38541
+ const entryPath = path53.join(relativePath || ".", d.name).replace(/\\/g, "/");
38542
+ const fullPath = path53.join(resolved, d.name);
38037
38543
  let isDir = d.isDirectory();
38038
38544
  if (d.isSymbolicLink()) {
38039
38545
  try {
38040
- const targetStat = await fs33.promises.stat(fullPath);
38546
+ const targetStat = await fs38.promises.stat(fullPath);
38041
38547
  isDir = targetStat.isDirectory();
38042
38548
  } catch {
38043
38549
  isDir = false;
@@ -38067,7 +38573,7 @@ async function listDirAsync(relativePath, sessionParentPath = getBridgeRoot()) {
38067
38573
  return { error: "Path is outside working directory" };
38068
38574
  }
38069
38575
  try {
38070
- const names = await fs34.promises.readdir(resolved, { withFileTypes: true });
38576
+ const names = await fs39.promises.readdir(resolved, { withFileTypes: true });
38071
38577
  const entries = [];
38072
38578
  for (let i = 0; i < names.length; i++) {
38073
38579
  if (i > 0 && i % LIST_DIR_YIELD_EVERY === 0) {
@@ -38087,18 +38593,18 @@ var LINE_CHUNK_SIZE = 64 * 1024;
38087
38593
  var READ_RANGE_YIELD_EVERY_BYTES = 256 * 1024;
38088
38594
 
38089
38595
  // src/files/read-file/resolve-file-path.ts
38090
- import fs35 from "node:fs";
38596
+ import fs40 from "node:fs";
38091
38597
  async function resolveFilePathAsync(relativePath, sessionParentPath = getBridgeRoot()) {
38092
38598
  const resolved = ensureUnderCwd(relativePath, sessionParentPath);
38093
38599
  if (!resolved) return { error: "Path is outside working directory" };
38094
38600
  let real;
38095
38601
  try {
38096
- real = await fs35.promises.realpath(resolved);
38602
+ real = await fs40.promises.realpath(resolved);
38097
38603
  } catch {
38098
38604
  real = resolved;
38099
38605
  }
38100
38606
  try {
38101
- const stat2 = await fs35.promises.stat(real);
38607
+ const stat2 = await fs40.promises.stat(real);
38102
38608
  if (!stat2.isFile()) return { error: "Not a file" };
38103
38609
  return real;
38104
38610
  } catch (err) {
@@ -38107,11 +38613,11 @@ async function resolveFilePathAsync(relativePath, sessionParentPath = getBridgeR
38107
38613
  }
38108
38614
 
38109
38615
  // src/files/read-file/read-file-range-async.ts
38110
- import fs36 from "node:fs";
38616
+ import fs41 from "node:fs";
38111
38617
  import { StringDecoder } from "node:string_decoder";
38112
38618
  async function readFileRangeAsync(filePath, startLine, endLine, lineOffsetIn, lineChunkSize = LINE_CHUNK_SIZE) {
38113
- const fileSize = (await fs36.promises.stat(filePath)).size;
38114
- const fd = await fs36.promises.open(filePath, "r");
38619
+ const fileSize = (await fs41.promises.stat(filePath)).size;
38620
+ const fd = await fs41.promises.open(filePath, "r");
38115
38621
  const bufSize = 64 * 1024;
38116
38622
  const buf = Buffer.alloc(bufSize);
38117
38623
  const decoder = new StringDecoder("utf8");
@@ -38273,11 +38779,11 @@ async function readFileRangeAsync(filePath, startLine, endLine, lineOffsetIn, li
38273
38779
  }
38274
38780
 
38275
38781
  // src/files/read-file/read-file-buffer-full-async.ts
38276
- import fs37 from "node:fs";
38782
+ import fs42 from "node:fs";
38277
38783
  var READ_CHUNK_BYTES = 256 * 1024;
38278
38784
  async function readFileBufferFullAsync(filePath) {
38279
- const stat2 = await fs37.promises.stat(filePath);
38280
- const fd = await fs37.promises.open(filePath, "r");
38785
+ const stat2 = await fs42.promises.stat(filePath);
38786
+ const fd = await fs42.promises.open(filePath, "r");
38281
38787
  const chunks = [];
38282
38788
  let position = 0;
38283
38789
  let bytesSinceYield = 0;
@@ -38373,10 +38879,10 @@ async function readFileAsync(relativePath, startLine, endLine, lineOffset, lineC
38373
38879
  }
38374
38880
 
38375
38881
  // src/files/resolve-file-browser-session-parent.ts
38376
- function resolveFileBrowserSessionParent(sessionWorktreeManager, sessionId) {
38882
+ async function resolveFileBrowserSessionParent(sessionWorktreeManager, sessionId) {
38377
38883
  const sid = sessionId?.trim();
38378
38884
  if (sid) {
38379
- sessionWorktreeManager.ensureRepoCheckoutPathsForSession(sid);
38885
+ await sessionWorktreeManager.ensureRepoCheckoutPathsForSessionAsync(sid);
38380
38886
  const worktreeRoot = sessionWorktreeManager.getSessionWorktreeRootForSession(sid);
38381
38887
  if (worktreeRoot) return worktreeRoot;
38382
38888
  }
@@ -38384,14 +38890,14 @@ function resolveFileBrowserSessionParent(sessionWorktreeManager, sessionId) {
38384
38890
  }
38385
38891
 
38386
38892
  // src/files/handle-file-browser-search.ts
38387
- import path40 from "node:path";
38893
+ import path54 from "node:path";
38388
38894
  var SEARCH_LIMIT = 100;
38389
38895
  function handleFileBrowserSearch(msg, socket, e2ee, sessionWorktreeManager) {
38390
38896
  void (async () => {
38391
38897
  await yieldToEventLoop();
38392
38898
  const q = typeof msg.q === "string" ? msg.q : "";
38393
- const sessionParentPath = path40.resolve(
38394
- sessionWorktreeManager != null ? resolveFileBrowserSessionParent(sessionWorktreeManager, msg.sessionId) : getBridgeRoot()
38899
+ const sessionParentPath = path54.resolve(
38900
+ sessionWorktreeManager != null ? await resolveFileBrowserSessionParent(sessionWorktreeManager, msg.sessionId) : getBridgeRoot()
38395
38901
  );
38396
38902
  if (!await bridgeFileIndexIsPopulated(sessionParentPath)) {
38397
38903
  const payload2 = {
@@ -38429,7 +38935,7 @@ function handleFileBrowserRequest(msg, socket, e2ee, sessionWorktreeManager) {
38429
38935
  void (async () => {
38430
38936
  const reqPath = msg.path.replace(/^\/+/, "") || ".";
38431
38937
  const op = msg.op === "read" ? "read" : "list";
38432
- const sessionParentPath = sessionWorktreeManager != null ? resolveFileBrowserSessionParent(sessionWorktreeManager, msg.sessionId) : void 0;
38938
+ const sessionParentPath = sessionWorktreeManager != null ? await resolveFileBrowserSessionParent(sessionWorktreeManager, msg.sessionId) : void 0;
38433
38939
  if (op === "list") {
38434
38940
  const result = await listDirAsync(reqPath, sessionParentPath);
38435
38941
  if ("error" in result) {
@@ -38500,37 +39006,56 @@ function handleFileBrowserSearchMessage(msg, { getWs, e2ee, sessionWorktreeManag
38500
39006
 
38501
39007
  // src/routing/handlers/skill-layout-request.ts
38502
39008
  function handleSkillLayoutRequest(msg, deps) {
38503
- const socket = deps.getWs();
38504
39009
  const id = typeof msg.id === "string" ? msg.id : "";
38505
- const roots = discoverSkillLayoutRoots(getBridgeRoot());
38506
- if (socket) {
38507
- sendWsMessage(socket, { type: "skill_layout_response", id, roots });
38508
- }
39010
+ void (async () => {
39011
+ const socket = deps.getWs();
39012
+ const roots = await discoverSkillLayoutRootsAsync(getBridgeRoot());
39013
+ if (socket) {
39014
+ sendWsMessage(socket, { type: "skill_layout_response", id, roots });
39015
+ }
39016
+ })();
38509
39017
  }
38510
39018
 
38511
- // src/skills/install-remote-skills.ts
38512
- import fs38 from "node:fs";
38513
- import path41 from "node:path";
38514
- function installRemoteSkills(cwd, targetDir, items) {
39019
+ // src/skills/install/is-valid-install-item.ts
39020
+ function isValidRemoteSkillInstallItem(item) {
39021
+ if (item === null || typeof item !== "object") return false;
39022
+ const o = item;
39023
+ return typeof o.sourceId === "string" && typeof o.skillName === "string" && typeof o.versionHash === "string" && Array.isArray(o.files);
39024
+ }
39025
+
39026
+ // src/skills/install/install-remote-skills-async.ts
39027
+ import fs43 from "node:fs";
39028
+ import path55 from "node:path";
39029
+
39030
+ // src/skills/install/constants.ts
39031
+ var INSTALL_SKILLS_YIELD_EVERY = 16;
39032
+
39033
+ // src/skills/install/install-remote-skills-async.ts
39034
+ async function writeInstallFileAsync(skillDir, f, filesWritten) {
39035
+ if (typeof f.path !== "string" || !f.text && !f.base64) return;
39036
+ if (++filesWritten.count % INSTALL_SKILLS_YIELD_EVERY === 0) {
39037
+ await yieldToEventLoop();
39038
+ }
39039
+ const dest = path55.join(skillDir, f.path);
39040
+ await fs43.promises.mkdir(path55.dirname(dest), { recursive: true });
39041
+ if (f.text !== void 0) {
39042
+ await fs43.promises.writeFile(dest, f.text, "utf8");
39043
+ } else if (f.base64) {
39044
+ await fs43.promises.writeFile(dest, Buffer.from(f.base64, "base64"));
39045
+ }
39046
+ }
39047
+ async function installRemoteSkillsAsync(cwd, targetDir, items) {
38515
39048
  const installed2 = [];
38516
39049
  if (!Array.isArray(items)) {
38517
39050
  return { success: false, error: "Invalid items" };
38518
39051
  }
39052
+ const filesWritten = { count: 0 };
38519
39053
  try {
38520
39054
  for (const item of items) {
38521
- if (typeof item.sourceId !== "string" || typeof item.skillName !== "string" || typeof item.versionHash !== "string" || !Array.isArray(item.files)) {
38522
- continue;
38523
- }
38524
- const skillDir = path41.join(cwd, targetDir, item.skillName);
39055
+ if (!isValidRemoteSkillInstallItem(item)) continue;
39056
+ const skillDir = path55.join(cwd, targetDir, item.skillName);
38525
39057
  for (const f of item.files) {
38526
- if (typeof f.path !== "string" || !f.text && !f.base64) continue;
38527
- const dest = path41.join(skillDir, f.path);
38528
- fs38.mkdirSync(path41.dirname(dest), { recursive: true });
38529
- if (f.text !== void 0) {
38530
- fs38.writeFileSync(dest, f.text, "utf8");
38531
- } else if (f.base64) {
38532
- fs38.writeFileSync(dest, Buffer.from(f.base64, "base64"));
38533
- }
39058
+ await writeInstallFileAsync(skillDir, f, filesWritten);
38534
39059
  }
38535
39060
  installed2.push({
38536
39061
  sourceId: item.sourceId,
@@ -38546,118 +39071,241 @@ function installRemoteSkills(cwd, targetDir, items) {
38546
39071
 
38547
39072
  // src/routing/handlers/install-skills.ts
38548
39073
  var handleInstallSkillsMessage = (msg, deps) => {
38549
- const socket = deps.getWs();
38550
39074
  const id = typeof msg.id === "string" ? msg.id : "";
38551
39075
  const targetDir = typeof msg.targetDir === "string" && msg.targetDir.trim() ? msg.targetDir.trim() : ".agents/skills";
38552
39076
  const rawItems = msg.items;
38553
39077
  const cwd = getBridgeRoot();
38554
- const result = installRemoteSkills(cwd, targetDir, rawItems);
38555
- if (!result.success) {
38556
- const err = result.error ?? "Invalid items";
38557
- deps.log(`[Bridge service] Install skills failed: ${err}`);
39078
+ void (async () => {
39079
+ const socket = deps.getWs();
39080
+ const result = await installRemoteSkillsAsync(cwd, targetDir, rawItems);
39081
+ if (!result.success) {
39082
+ const err = result.error ?? "Invalid items";
39083
+ deps.log(`[Bridge service] Install skills failed: ${err}`);
39084
+ if (socket) {
39085
+ sendWsMessage(socket, { type: "install_skills_result", id, success: false, error: err });
39086
+ }
39087
+ return;
39088
+ }
38558
39089
  if (socket) {
38559
- sendWsMessage(socket, { type: "install_skills_result", id, success: false, error: err });
39090
+ sendWsMessage(socket, { type: "install_skills_result", id, success: true, installed: result.installed });
38560
39091
  }
38561
- return;
38562
- }
38563
- if (socket) {
38564
- sendWsMessage(socket, { type: "install_skills_result", id, success: true, installed: result.installed });
38565
- }
39092
+ })();
38566
39093
  };
38567
39094
 
38568
39095
  // src/routing/handlers/refresh-local-skills.ts
38569
39096
  var handleRefreshLocalSkills = (_msg, deps) => {
38570
- deps.sendLocalSkillsReport?.();
39097
+ setImmediate(() => {
39098
+ deps.sendLocalSkillsReport?.();
39099
+ });
38571
39100
  };
38572
39101
 
38573
- // src/routing/handlers/session-git-request.ts
38574
- function sendResult(ws, id, payload, e2ee, encryptedFields = []) {
39102
+ // src/routing/handlers/git/handle-session-git-commit.ts
39103
+ async function handleSessionGitCommitAction(deps, sessionId, msg, reply) {
39104
+ const branch = typeof msg.branch === "string" ? msg.branch : "";
39105
+ const message = typeof msg.message === "string" ? msg.message : "";
39106
+ const pushAfterCommit = msg.pushAfterCommit === true;
39107
+ if (!branch.trim() || !message.trim()) {
39108
+ reply({ ok: false, error: "branch and message are required for commit" });
39109
+ return;
39110
+ }
39111
+ const commitRes = await deps.sessionWorktreeManager.commitSession({
39112
+ sessionId,
39113
+ branch: branch.trim(),
39114
+ message: message.trim(),
39115
+ push: pushAfterCommit
39116
+ });
39117
+ if (!commitRes.ok) {
39118
+ reply({ ok: false, error: commitRes.error ?? "Commit failed" });
39119
+ return;
39120
+ }
39121
+ const st = await deps.sessionWorktreeManager.getSessionWorkingTreeStatus(sessionId);
39122
+ reply({
39123
+ ok: true,
39124
+ hasUncommittedChanges: st.hasUncommittedChanges,
39125
+ hasUnpushedCommits: st.hasUnpushedCommits,
39126
+ uncommittedFileCount: st.uncommittedFileCount
39127
+ });
39128
+ }
39129
+
39130
+ // src/routing/handlers/git/session-git-changes-params.ts
39131
+ function readString(value) {
39132
+ return typeof value === "string" ? value.trim() : "";
39133
+ }
39134
+ function readChangesObject(msg) {
39135
+ const changes = msg.changes;
39136
+ if (!changes || typeof changes !== "object" || Array.isArray(changes)) return null;
39137
+ return changes;
39138
+ }
39139
+ function parseSessionGitChangesParams(msg) {
39140
+ const nested = readChangesObject(msg);
39141
+ const file2 = nested?.file && typeof nested.file === "object" && !Array.isArray(nested.file) ? nested.file : null;
39142
+ const repoRelPath = readString(nested?.repoRelPath) || readString(msg.changesRepoRelPath);
39143
+ const viewRaw = nested?.view ?? msg.changesView;
39144
+ const view = viewRaw === "commit" ? "commit" : "working";
39145
+ const commitSha = readString(nested?.commitSha) || readString(msg.changesCommitSha);
39146
+ const recentCommitsLimit = nested?.recentCommitsLimit ?? msg.changesRecentCommitsLimit;
39147
+ return {
39148
+ repoRelPath,
39149
+ view,
39150
+ commitSha,
39151
+ recentCommitsLimit: typeof recentCommitsLimit === "number" || typeof recentCommitsLimit === "string" ? recentCommitsLimit : void 0,
39152
+ fileWorkspaceRelPath: readString(file2?.workspaceRelPath),
39153
+ fileChange: readString(file2?.change),
39154
+ fileMovedFromWorkspaceRelPath: readString(file2?.movedFromWorkspaceRelPath)
39155
+ };
39156
+ }
39157
+
39158
+ // src/routing/handlers/git/handle-session-git-get-change-file-patch.ts
39159
+ async function handleSessionGitGetChangeFilePatchAction(deps, msg, reply) {
39160
+ const changes = parseSessionGitChangesParams(msg);
39161
+ const { repoRelPath: repoRel, view, commitSha, fileWorkspaceRelPath, fileChange, fileMovedFromWorkspaceRelPath } = changes;
39162
+ const change = parseWorkingTreeChangeKind(fileChange);
39163
+ if (!repoRel || !fileWorkspaceRelPath || !change) {
39164
+ reply({
39165
+ ok: false,
39166
+ error: "changes.repoRelPath, changes.file.workspaceRelPath, and changes.file.change are required"
39167
+ });
39168
+ return;
39169
+ }
39170
+ if (view === "commit" && !commitSha) {
39171
+ reply({ ok: false, error: "changes.commitSha is required for commit file patch" });
39172
+ return;
39173
+ }
39174
+ const patch = await deps.sessionWorktreeManager.getSessionWorkingTreeChangeFilePatch(msg.sessionId, {
39175
+ repoRelPath: repoRel,
39176
+ workspaceRelPath: fileWorkspaceRelPath,
39177
+ change,
39178
+ movedFromWorkspaceRelPath: fileMovedFromWorkspaceRelPath || null,
39179
+ basis: view === "commit" ? { kind: "commit", sha: commitSha } : { kind: "working" }
39180
+ });
39181
+ reply(
39182
+ {
39183
+ ok: true,
39184
+ patchContent: patch.patchContent ?? null,
39185
+ totalLines: patch.totalLines
39186
+ },
39187
+ ["patchContent"]
39188
+ );
39189
+ }
39190
+
39191
+ // src/routing/handlers/git/coalesce-list-changes.ts
39192
+ var listChangesInflight = /* @__PURE__ */ new Map();
39193
+ function normalizeListChangesRepoRel(repoRelPath) {
39194
+ if (!repoRelPath?.trim()) return "";
39195
+ return normRepoRel(repoRelPath.trim());
39196
+ }
39197
+ function listChangesInflightKey(sessionId, opts) {
39198
+ const basis = opts?.basis;
39199
+ return [
39200
+ sessionId,
39201
+ normalizeListChangesRepoRel(opts?.repoRelPath),
39202
+ basis?.kind === "commit" ? `commit:${basis.sha}` : "working",
39203
+ String(opts?.recentCommitsLimit ?? "")
39204
+ ].join("|");
39205
+ }
39206
+ function getSessionWorkingTreeChangeDetailsCoalesced(sessionWorktreeManager, sessionId, opts) {
39207
+ const key = listChangesInflightKey(sessionId, opts);
39208
+ const existing = listChangesInflight.get(key);
39209
+ if (existing) return existing;
39210
+ const promise2 = sessionWorktreeManager.getSessionWorkingTreeChangeDetails(sessionId, opts).finally(() => {
39211
+ if (listChangesInflight.get(key) === promise2) listChangesInflight.delete(key);
39212
+ });
39213
+ listChangesInflight.set(key, promise2);
39214
+ return promise2;
39215
+ }
39216
+
39217
+ // src/routing/handlers/git/handle-session-git-list-changes.ts
39218
+ async function handleSessionGitListChangesAction(deps, msg, reply) {
39219
+ const changes = parseSessionGitChangesParams(msg);
39220
+ const { repoRelPath: repoRel, view, commitSha, recentCommitsLimit } = changes;
39221
+ if (view === "commit") {
39222
+ if (!repoRel || !commitSha) {
39223
+ reply({
39224
+ ok: false,
39225
+ error: "changes.repoRelPath and changes.commitSha are required for commit view"
39226
+ });
39227
+ return;
39228
+ }
39229
+ }
39230
+ const opts = repoRel && view === "commit" && commitSha ? {
39231
+ repoRelPath: repoRel,
39232
+ basis: { kind: "commit", sha: commitSha },
39233
+ recentCommitsLimit
39234
+ } : repoRel ? { repoRelPath: repoRel, basis: { kind: "working" }, recentCommitsLimit } : recentCommitsLimit !== void 0 ? { recentCommitsLimit } : void 0;
39235
+ const repos = await getSessionWorkingTreeChangeDetailsCoalesced(
39236
+ deps.sessionWorktreeManager,
39237
+ msg.sessionId,
39238
+ opts
39239
+ );
39240
+ reply({ ok: true, repos }, ["repos"]);
39241
+ }
39242
+
39243
+ // src/routing/handlers/git/handle-session-git-push.ts
39244
+ async function handleSessionGitPushAction(deps, sessionId, reply) {
39245
+ const pushRes = await deps.sessionWorktreeManager.pushSessionUpstream(sessionId);
39246
+ if (!pushRes.ok) {
39247
+ reply({ ok: false, error: pushRes.error ?? "Push failed" });
39248
+ return;
39249
+ }
39250
+ const st = await deps.sessionWorktreeManager.getSessionWorkingTreeStatus(sessionId);
39251
+ reply({
39252
+ ok: true,
39253
+ hasUncommittedChanges: st.hasUncommittedChanges,
39254
+ hasUnpushedCommits: st.hasUnpushedCommits,
39255
+ uncommittedFileCount: st.uncommittedFileCount
39256
+ });
39257
+ }
39258
+
39259
+ // src/routing/handlers/git/handle-session-git-status.ts
39260
+ async function handleSessionGitStatusAction(deps, sessionId, reply) {
39261
+ const r = await deps.sessionWorktreeManager.getSessionWorkingTreeStatus(sessionId);
39262
+ reply({
39263
+ ok: true,
39264
+ hasUncommittedChanges: r.hasUncommittedChanges,
39265
+ hasUnpushedCommits: r.hasUnpushedCommits,
39266
+ uncommittedFileCount: r.uncommittedFileCount
39267
+ });
39268
+ }
39269
+
39270
+ // src/routing/handlers/git/send-session-git-result.ts
39271
+ function sendSessionGitResult(ws, id, payload, e2ee, encryptedFields = []) {
38575
39272
  if (!ws) return;
38576
39273
  const message = { type: "session_git_result", id, ...payload };
38577
- sendWsMessage(ws, e2ee && encryptedFields.length > 0 ? e2ee.encryptFields(message, encryptedFields) : message);
39274
+ let wire = message;
39275
+ if (e2ee && encryptedFields.length > 0) {
39276
+ wire = e2ee.encryptFields(message, encryptedFields);
39277
+ }
39278
+ sendWsMessage(ws, wire);
38578
39279
  }
39280
+
39281
+ // src/routing/handlers/git/session-git-request.ts
38579
39282
  var handleSessionGitRequestMessage = (msg, deps) => {
38580
39283
  if (typeof msg.id !== "string") return;
38581
39284
  const sessionId = typeof msg.sessionId === "string" ? msg.sessionId : "";
38582
39285
  const action = msg.action;
38583
- if (!sessionId || action !== "status" && action !== "push" && action !== "commit" && action !== "list_changes")
39286
+ if (!sessionId || action !== "status" && action !== "push" && action !== "commit" && action !== "list_changes" && action !== "file_diff")
38584
39287
  return;
38585
39288
  void (async () => {
38586
39289
  const ws = deps.getWs();
38587
- const reply = (payload, encryptedFields = []) => sendResult(ws, msg.id, payload, deps.e2ee, encryptedFields);
39290
+ const reply = (payload, encryptedFields = []) => sendSessionGitResult(ws, msg.id, payload, deps.e2ee, encryptedFields);
38588
39291
  try {
38589
39292
  if (action === "status") {
38590
- const r = await deps.sessionWorktreeManager.getSessionWorkingTreeStatus(sessionId);
38591
- reply({
38592
- ok: true,
38593
- hasUncommittedChanges: r.hasUncommittedChanges,
38594
- hasUnpushedCommits: r.hasUnpushedCommits,
38595
- uncommittedFileCount: r.uncommittedFileCount
38596
- });
39293
+ await handleSessionGitStatusAction(deps, sessionId, reply);
38597
39294
  return;
38598
39295
  }
38599
39296
  if (action === "list_changes") {
38600
- const repoRel = typeof msg.changesRepoRelPath === "string" ? msg.changesRepoRelPath.trim() : "";
38601
- const view = msg.changesView === "commit" ? "commit" : "working";
38602
- const commitSha = typeof msg.changesCommitSha === "string" ? msg.changesCommitSha.trim() : "";
38603
- if (view === "commit") {
38604
- if (!repoRel || !commitSha) {
38605
- reply({ ok: false, error: "changesRepoRelPath and changesCommitSha are required for commit view" });
38606
- return;
38607
- }
38608
- }
38609
- const recentCommitsLimit = typeof msg.changesRecentCommitsLimit === "number" || typeof msg.changesRecentCommitsLimit === "string" ? msg.changesRecentCommitsLimit : void 0;
38610
- const opts = repoRel && view === "commit" && commitSha ? {
38611
- repoRelPath: repoRel,
38612
- basis: { kind: "commit", sha: commitSha },
38613
- recentCommitsLimit
38614
- } : repoRel ? { repoRelPath: repoRel, basis: { kind: "working" }, recentCommitsLimit } : recentCommitsLimit !== void 0 ? { recentCommitsLimit } : void 0;
38615
- const repos = await deps.sessionWorktreeManager.getSessionWorkingTreeChangeDetails(sessionId, opts);
38616
- reply({
38617
- ok: true,
38618
- repos
38619
- }, ["repos"]);
39297
+ await handleSessionGitListChangesAction(deps, { ...msg, sessionId }, reply);
38620
39298
  return;
38621
39299
  }
38622
- if (action === "push") {
38623
- const pushRes = await deps.sessionWorktreeManager.pushSessionUpstream(sessionId);
38624
- if (!pushRes.ok) {
38625
- reply({ ok: false, error: pushRes.error ?? "Push failed" });
38626
- return;
38627
- }
38628
- const st2 = await deps.sessionWorktreeManager.getSessionWorkingTreeStatus(sessionId);
38629
- reply({
38630
- ok: true,
38631
- hasUncommittedChanges: st2.hasUncommittedChanges,
38632
- hasUnpushedCommits: st2.hasUnpushedCommits,
38633
- uncommittedFileCount: st2.uncommittedFileCount
38634
- });
38635
- return;
38636
- }
38637
- const branch = typeof msg.branch === "string" ? msg.branch : "";
38638
- const message = typeof msg.message === "string" ? msg.message : "";
38639
- const pushAfterCommit = msg.pushAfterCommit === true;
38640
- if (!branch.trim() || !message.trim()) {
38641
- reply({ ok: false, error: "branch and message are required for commit" });
39300
+ if (action === "file_diff") {
39301
+ await handleSessionGitGetChangeFilePatchAction(deps, { ...msg, sessionId }, reply);
38642
39302
  return;
38643
39303
  }
38644
- const commitRes = await deps.sessionWorktreeManager.commitSession({
38645
- sessionId,
38646
- branch: branch.trim(),
38647
- message: message.trim(),
38648
- push: pushAfterCommit
38649
- });
38650
- if (!commitRes.ok) {
38651
- reply({ ok: false, error: commitRes.error ?? "Commit failed" });
39304
+ if (action === "push") {
39305
+ await handleSessionGitPushAction(deps, sessionId, reply);
38652
39306
  return;
38653
39307
  }
38654
- const st = await deps.sessionWorktreeManager.getSessionWorkingTreeStatus(sessionId);
38655
- reply({
38656
- ok: true,
38657
- hasUncommittedChanges: st.hasUncommittedChanges,
38658
- hasUnpushedCommits: st.hasUnpushedCommits,
38659
- uncommittedFileCount: st.uncommittedFileCount
38660
- });
39308
+ await handleSessionGitCommitAction(deps, sessionId, msg, reply);
38661
39309
  } catch (e) {
38662
39310
  reply({ ok: false, error: e instanceof Error ? e.message : String(e) });
38663
39311
  }
@@ -38687,7 +39335,7 @@ var handleSessionDiscardedMessage = (msg, deps) => {
38687
39335
  };
38688
39336
 
38689
39337
  // src/routing/handlers/revert-turn-snapshot.ts
38690
- import * as fs39 from "node:fs";
39338
+ import * as fs44 from "node:fs";
38691
39339
  var handleRevertTurnSnapshotMessage = (msg, deps) => {
38692
39340
  const id = typeof msg.id === "string" ? msg.id : "";
38693
39341
  const sessionId = typeof msg.sessionId === "string" ? msg.sessionId : "";
@@ -38700,7 +39348,7 @@ var handleRevertTurnSnapshotMessage = (msg, deps) => {
38700
39348
  const agentBase = sessionWorktreeManager.getSessionWorktreeRootForSession(sessionId) ?? getBridgeRoot();
38701
39349
  const file2 = snapshotFilePath(agentBase, turnId);
38702
39350
  try {
38703
- await fs39.promises.access(file2, fs39.constants.F_OK);
39351
+ await fs44.promises.access(file2, fs44.constants.F_OK);
38704
39352
  } catch {
38705
39353
  sendWsMessage(s, {
38706
39354
  type: "revert_turn_snapshot_result",
@@ -38738,11 +39386,13 @@ var handleDevServerControl = (msg, deps) => {
38738
39386
  // src/routing/handlers/dev-servers-config.ts
38739
39387
  var handleDevServersConfig = (msg, deps) => {
38740
39388
  const devServers = msg.devServers;
38741
- deps.devServerManager?.applyConfig(devServers ?? []);
39389
+ setImmediate(() => {
39390
+ deps.devServerManager?.applyConfig(devServers ?? []);
39391
+ });
38742
39392
  };
38743
39393
 
38744
39394
  // src/git/bridge-git-context.ts
38745
- import * as path42 from "node:path";
39395
+ import * as path56 from "node:path";
38746
39396
 
38747
39397
  // src/git/branches/get-current-branch.ts
38748
39398
  async function getCurrentBranch(repoPath) {
@@ -38792,12 +39442,12 @@ async function listRepoBranchRefs(repoPath) {
38792
39442
  // src/git/bridge-git-context.ts
38793
39443
  function folderNameForRelPath(relPath, bridgeRoot) {
38794
39444
  if (relPath === "." || relPath === "") {
38795
- return path42.basename(path42.resolve(bridgeRoot)) || "repo";
39445
+ return path56.basename(path56.resolve(bridgeRoot)) || "repo";
38796
39446
  }
38797
- return path42.basename(relPath) || relPath;
39447
+ return path56.basename(relPath) || relPath;
38798
39448
  }
38799
39449
  async function discoverGitReposForBridgeContext(bridgeRoot) {
38800
- const root = path42.resolve(bridgeRoot);
39450
+ const root = path56.resolve(bridgeRoot);
38801
39451
  if (await isGitRepoDirectory(root)) {
38802
39452
  const remoteUrl = await getRemoteOriginUrl(root);
38803
39453
  return [{ absolutePath: root, remoteUrl }];
@@ -38805,17 +39455,19 @@ async function discoverGitReposForBridgeContext(bridgeRoot) {
38805
39455
  const [deep, shallow] = await Promise.all([discoverGitReposUnderRoot(root), discoverGitRepos(root)]);
38806
39456
  const byPath = /* @__PURE__ */ new Map();
38807
39457
  for (const repo of [...deep, ...shallow]) {
38808
- byPath.set(path42.resolve(repo.absolutePath), repo);
39458
+ byPath.set(path56.resolve(repo.absolutePath), repo);
38809
39459
  }
38810
39460
  return [...byPath.values()];
38811
39461
  }
38812
39462
  async function getBridgeGitContext(bridgeRoot = getBridgeRoot()) {
38813
- const bridgeResolved = path42.resolve(bridgeRoot);
39463
+ const bridgeResolved = path56.resolve(bridgeRoot);
38814
39464
  const repos = await discoverGitReposForBridgeContext(bridgeResolved);
38815
39465
  const rows = [];
38816
- for (const repo of repos) {
38817
- let rel = path42.relative(bridgeResolved, repo.absolutePath);
38818
- if (rel.startsWith("..") || path42.isAbsolute(rel)) continue;
39466
+ for (let i = 0; i < repos.length; i++) {
39467
+ if (i > 0) await yieldToEventLoop();
39468
+ const repo = repos[i];
39469
+ let rel = path56.relative(bridgeResolved, repo.absolutePath);
39470
+ if (rel.startsWith("..") || path56.isAbsolute(rel)) continue;
38819
39471
  const relPath = rel === "" ? "." : rel.replace(/\\/g, "/");
38820
39472
  const currentBranch = await getCurrentBranch(repo.absolutePath);
38821
39473
  const remoteUrl = repo.remoteUrl.trim() || null;
@@ -38830,11 +39482,11 @@ async function getBridgeGitContext(bridgeRoot = getBridgeRoot()) {
38830
39482
  return rows;
38831
39483
  }
38832
39484
  async function listRepoBranchesForBridge(repoRelPath, bridgeRoot = getBridgeRoot()) {
38833
- const bridgeResolved = path42.resolve(bridgeRoot);
39485
+ const bridgeResolved = path56.resolve(bridgeRoot);
38834
39486
  const rel = repoRelPath.trim() === "." ? "" : repoRelPath.trim().replace(/\\/g, "/");
38835
- const repoPath = rel === "" ? bridgeResolved : path42.join(bridgeResolved, rel);
38836
- const resolved = path42.resolve(repoPath);
38837
- if (!resolved.startsWith(bridgeResolved + path42.sep) && resolved !== bridgeResolved) {
39487
+ const repoPath = rel === "" ? bridgeResolved : path56.join(bridgeResolved, rel);
39488
+ const resolved = path56.resolve(repoPath);
39489
+ if (!resolved.startsWith(bridgeResolved + path56.sep) && resolved !== bridgeResolved) {
38838
39490
  return [];
38839
39491
  }
38840
39492
  return listRepoBranchRefs(resolved);
@@ -39418,10 +40070,10 @@ function listCliAgentCapabilityCacheForWorkspace(db, workspaceId) {
39418
40070
  }
39419
40071
 
39420
40072
  // src/agents/capabilities/warmup-agent-capabilities-on-connect.ts
39421
- import * as path44 from "node:path";
40073
+ import * as path58 from "node:path";
39422
40074
 
39423
40075
  // src/agents/capabilities/probe-one-agent-type-for-capabilities.ts
39424
- import * as path43 from "node:path";
40076
+ import * as path57 from "node:path";
39425
40077
  async function probeOneAgentTypeForCapabilities(params) {
39426
40078
  const { agentType, cwd, workspaceId, log: log2, reportAgentCapabilities, bridgeReport = true, shouldContinue } = params;
39427
40079
  const canContinue = () => shouldContinue?.() !== false;
@@ -39459,7 +40111,7 @@ async function probeOneAgentTypeForCapabilities(params) {
39459
40111
  if (!canContinue()) return false;
39460
40112
  handle = await resolved.createClient({
39461
40113
  command: resolved.command,
39462
- cwd: path43.resolve(cwd),
40114
+ cwd: path57.resolve(cwd),
39463
40115
  backendAgentType: agentType,
39464
40116
  sessionMode: "agent",
39465
40117
  persistedAcpSessionId: null,
@@ -39537,7 +40189,7 @@ async function warmupAgentCapabilitiesOnConnect(params) {
39537
40189
  const { workspaceId, log: log2, getWs } = params;
39538
40190
  const isCurrent = beginAgentCapabilityWarmupRun();
39539
40191
  if (!isCurrent()) return;
39540
- const cwd = path44.resolve(getBridgeRoot());
40192
+ const cwd = path58.resolve(getBridgeRoot());
39541
40193
  async function sendBatchFromCache() {
39542
40194
  if (!isCurrent()) return;
39543
40195
  const socket = getWs();
@@ -39883,18 +40535,18 @@ import * as http from "node:http";
39883
40535
 
39884
40536
  // src/mcp/bridge-access/read-json-body.ts
39885
40537
  function readJsonBody(req) {
39886
- return new Promise((resolve20, reject) => {
40538
+ return new Promise((resolve32, reject) => {
39887
40539
  const chunks = [];
39888
40540
  req.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
39889
40541
  req.on("end", () => {
39890
40542
  try {
39891
40543
  const raw = Buffer.concat(chunks).toString("utf8").trim();
39892
40544
  if (!raw) {
39893
- resolve20({});
40545
+ resolve32({});
39894
40546
  return;
39895
40547
  }
39896
40548
  const parsed = JSON.parse(raw);
39897
- resolve20(parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {});
40549
+ resolve32(parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {});
39898
40550
  } catch (e) {
39899
40551
  reject(e);
39900
40552
  }
@@ -39952,7 +40604,7 @@ function createBridgeAccessRequestHandler(registry2) {
39952
40604
  // src/mcp/bridge-access/start-server.ts
39953
40605
  function startBridgeAccessServer(registry2) {
39954
40606
  const server = http.createServer(createBridgeAccessRequestHandler(registry2));
39955
- return new Promise((resolve20, reject) => {
40607
+ return new Promise((resolve32, reject) => {
39956
40608
  server.once("error", reject);
39957
40609
  server.listen(0, "127.0.0.1", () => {
39958
40610
  const addr = server.address();
@@ -39960,7 +40612,7 @@ function startBridgeAccessServer(registry2) {
39960
40612
  reject(new Error("Bridge access server did not bind"));
39961
40613
  return;
39962
40614
  }
39963
- resolve20({
40615
+ resolve32({
39964
40616
  port: addr.port,
39965
40617
  close: () => new Promise((closeResolve, closeReject) => {
39966
40618
  server.close((err) => err ? closeReject(err) : closeResolve());
@@ -40100,8 +40752,8 @@ async function createBridgeConnection(options) {
40100
40752
  getCloudAccessToken: () => tokens.accessToken
40101
40753
  };
40102
40754
  const identifyReportedPaths = {
40103
- bridgeRootPath: path45.resolve(getBridgeRoot()),
40104
- worktreesRootPath: path45.resolve(worktreesRootPath)
40755
+ bridgeRootPath: path59.resolve(getBridgeRoot()),
40756
+ worktreesRootPath: path59.resolve(worktreesRootPath)
40105
40757
  };
40106
40758
  const { connect } = createMainBridgeWebSocketLifecycle({
40107
40759
  state,