@norman-else/dsh-claude 0.1.45 → 0.1.47

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/lib/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { A as CLAUDE_REPOSITORY_FEEDBACK_PATH, B as DEFAULT_CLAUDE_RENDER_MODE, C as CLAUDE_PROJECTION_PATH, D as CLAUDE_PROSE_MODES, E as CLAUDE_PROMPT_REFINE_PATH, F as CLAUDE_REWIND_PATH, G as isClaudeRenderMode, H as TASK_TOOL_NAMES, I as CLAUDE_UPDATE_CHECK_PATH, L as CLAUDE_UPDATE_PATH, M as CLAUDE_REPOSITORY_SETUP_PATH, N as CLAUDE_REPOSITORY_STATUS_PATH, O as CLAUDE_RENDER_MODES, P as CLAUDE_REVIEW_COMMENT_PATH, R as CLAUDE_USAGE_PATH, S as CLAUDE_PLAN_FEEDBACK_PATH, T as CLAUDE_PROMPT_NAME_PATH, U as isClaudeAlertMode, W as isClaudeProseMode, _ as CLAUDE_CODE_PROVIDER_IDS, a as latestClaudeTasks, b as CLAUDE_GLOBAL_SETTINGS_PATH, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_ALERT_MODES, g as CLAUDE_CODE_PROVIDER, h as CLAUDE_CODE_PRESET_ID, i as latestClaudeSessionBinding, j as CLAUDE_REPOSITORY_FILE_PATH, k as CLAUDE_REPOSITORY_ACTION_PATH, l as redactText, m as CLAUDE_CLIENT_DIAGNOSTICS_PATH, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_ASK_PATH, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as CLAUDE_DOCTOR_PATH, w as CLAUDE_PROMPTS_PATH, x as CLAUDE_JIRA_PATH, y as CLAUDE_EDITOR_OPEN_PATH, z as DEFAULT_CLAUDE_PROSE_MODE } from "./events-oovRTmX7.mjs";
2
- import { a as projectClaudeCommands, i as CLAUDE_COMMANDS_SERVICE, r as dynamicPresenterDefinition, t as CLAUDE_PRESENTER_NAMES } from "./presenters-BVWj7u0a.mjs";
3
- import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-CuosP3sA.mjs";
1
+ import { A as CLAUDE_REPOSITORY_FILE_PATH, C as CLAUDE_PROMPTS_PATH, D as CLAUDE_RENDER_MODES, E as CLAUDE_PROSE_MODES, F as CLAUDE_UPDATE_CHECK_PATH, H as isClaudeAlertMode, I as CLAUDE_UPDATE_PATH, L as CLAUDE_USAGE_PATH, M as CLAUDE_REPOSITORY_STATUS_PATH, N as CLAUDE_REVIEW_COMMENT_PATH, O as CLAUDE_REPOSITORY_ACTION_PATH, P as CLAUDE_REWIND_PATH, R as DEFAULT_CLAUDE_PROSE_MODE, S as CLAUDE_PROJECTION_PATH, T as CLAUDE_PROMPT_REFINE_PATH, U as isClaudeProseMode, V as TASK_TOOL_NAMES, W as isClaudeRenderMode, _ as CLAUDE_CODE_PROVIDER_IDS, a as latestClaudeTasks, b as CLAUDE_JIRA_PATH, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_ALERT_MODES, g as CLAUDE_CODE_PROVIDER, h as CLAUDE_CODE_PRESET_ID, i as latestClaudeSessionBinding, j as CLAUDE_REPOSITORY_SETUP_PATH, k as CLAUDE_REPOSITORY_FEEDBACK_PATH, l as redactText, m as CLAUDE_CLIENT_DIAGNOSTICS_PATH, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_ASK_PATH, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as CLAUDE_DOCTOR_PATH, w as CLAUDE_PROMPT_NAME_PATH, x as CLAUDE_PLAN_FEEDBACK_PATH, y as CLAUDE_GLOBAL_SETTINGS_PATH, z as DEFAULT_CLAUDE_RENDER_MODE } from "./events-B498uofA.mjs";
2
+ import { a as projectClaudeCommands, i as CLAUDE_COMMANDS_SERVICE, r as dynamicPresenterDefinition, t as CLAUDE_PRESENTER_NAMES } from "./presenters-CRFcjLSC.mjs";
3
+ import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-C4TTNV9L.mjs";
4
4
  import z from "@deepseek-ai/schemastery";
5
5
  import { createHash, randomUUID } from "node:crypto";
6
6
  import { chmod, mkdir, opendir, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
@@ -1810,7 +1810,7 @@ function createManagedClaudeSpawner(runtime, executablePath, observe) {
1810
1810
  * honours `.gitignore`, so build output and `node_modules` are neither
1811
1811
  * captured nor removed.
1812
1812
  */
1813
- const MAX_OUTPUT_BYTES$5 = 65536;
1813
+ const MAX_OUTPUT_BYTES$4 = 65536;
1814
1814
  /** `add -A` walks the whole checkout; a large repository needs more than the
1815
1815
  * status probes' five seconds, and this sits in front of every turn. */
1816
1816
  const GIT_TIMEOUT_MS$4 = 3e4;
@@ -1835,8 +1835,8 @@ async function run$1(runtime, git, args, cwd, env = {}) {
1835
1835
  cwd,
1836
1836
  stdio: {
1837
1837
  stdin: "ignore",
1838
- stdout: { maxBytes: MAX_OUTPUT_BYTES$5 },
1839
- stderr: { maxBytes: MAX_OUTPUT_BYTES$5 }
1838
+ stdout: { maxBytes: MAX_OUTPUT_BYTES$4 },
1839
+ stderr: { maxBytes: MAX_OUTPUT_BYTES$4 }
1840
1840
  },
1841
1841
  graceMs: 1e3,
1842
1842
  signal: AbortSignal.timeout(GIT_TIMEOUT_MS$4),
@@ -1923,6 +1923,18 @@ const CLAUDE_MODE_BY_SANDBOX = {
1923
1923
  "workspace-write": "acceptEdits",
1924
1924
  "danger-full-access": "bypassPermissions"
1925
1925
  };
1926
+ /** Appended to the Claude Code system prompt on every session.
1927
+ *
1928
+ * The plan panel opens on exactly one signal: an `ExitPlanMode` call, whose
1929
+ * argument is the plan (see `planText` in permission.ts). Claude Code's own
1930
+ * plan mode says to make that call once the plan file is written, but
1931
+ * user-installed skills routinely say the opposite — "confirm the design in
1932
+ * prose before proceeding" — and a turn that ends on that question hands DSH
1933
+ * nothing to show: no record, no button, no panel, until the user types
1934
+ * "continue" and Claude makes the call it skipped. This line settles the
1935
+ * conflict in favour of the handoff, so the plan reaches the panel the moment
1936
+ * it is done rather than one message later. */
1937
+ const PLAN_MODE_HANDOFF_PROMPT = "When you are in plan mode and the plan is written, end the turn by calling ExitPlanMode with the plan. Do not end a plan-mode turn by asking the user for confirmation in prose: the user reads and approves the plan through ExitPlanMode, and a turn that stops short of that call shows them nothing.";
1926
1938
  /** Fold DSH's native access selector into Claude Code's closest permission mode. */
1927
1939
  function claudePermissionMode(events) {
1928
1940
  for (let index = events.length - 1; index >= 0; index -= 1) {
@@ -2072,8 +2084,7 @@ var ClaudeSupervisor = class {
2072
2084
  cwd: entry.cwd,
2073
2085
  model: entry.model,
2074
2086
  ...entry.thinkingMode === void 0 ? {} : { thinkingMode: entry.thinkingMode },
2075
- lastUsedAt: entry.lastUsedAt,
2076
- ...entry.process === void 0 ? {} : { pid: entry.process.handle.pid }
2087
+ lastUsedAt: entry.lastUsedAt
2077
2088
  }));
2078
2089
  }
2079
2090
  supportedCommands(agent, model = this.#config.defaultModel) {
@@ -2596,7 +2607,8 @@ var ClaudeSupervisor = class {
2596
2607
  ],
2597
2608
  systemPrompt: {
2598
2609
  type: "preset",
2599
- preset: "claude_code"
2610
+ preset: "claude_code",
2611
+ append: PLAN_MODE_HANDOFF_PROMPT
2600
2612
  },
2601
2613
  tools: {
2602
2614
  type: "preset",
@@ -2796,7 +2808,7 @@ var ClaudeSupervisor = class {
2796
2808
  case "unknown":
2797
2809
  await this.#appendActivity(active, {
2798
2810
  kind: message.kind === "status" ? "status" : "warning",
2799
- phase: "updated",
2811
+ phase: "completed",
2800
2812
  title: message.title,
2801
2813
  ..."summary" in message ? { summary: message.summary } : {},
2802
2814
  ..."detail" in message ? { detail: message.detail } : {}
@@ -4198,7 +4210,7 @@ function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolution
4198
4210
  }
4199
4211
  //#endregion
4200
4212
  //#region src/projection-routes.ts
4201
- const MAX_SESSION_ID_CHARS$7 = 1024;
4213
+ const MAX_SESSION_ID_CHARS$6 = 1024;
4202
4214
  /** Slow-moving metadata refresh and stream heartbeat cadence; deliberately off
4203
4215
  * the transcript hot path so git/gh latency never delays visible text. */
4204
4216
  const META_REFRESH_MS = 5e3;
@@ -4206,7 +4218,7 @@ const META_REFRESH_MS = 5e3;
4206
4218
  * so this cannot collide with one. */
4207
4219
  const MULTI_SEGMENT = "multi";
4208
4220
  function validSessionId(value) {
4209
- return value.length > 0 && value.length <= MAX_SESSION_ID_CHARS$7;
4221
+ return value.length > 0 && value.length <= MAX_SESSION_ID_CHARS$6;
4210
4222
  }
4211
4223
  function targetFromUrl(url) {
4212
4224
  const prefix = `${CLAUDE_PROJECTION_PATH}/`;
@@ -4244,6 +4256,7 @@ function envelope(projection, meta) {
4244
4256
  ...projection.contextUsage === void 0 ? {} : { contextUsage: projection.contextUsage },
4245
4257
  ...projection.tasks === void 0 ? {} : { tasks: projection.tasks },
4246
4258
  ...meta.repository === void 0 ? {} : { repository: meta.repository },
4259
+ ...meta.repositories === void 0 ? {} : { repositories: meta.repositories },
4247
4260
  reviewComments: meta.reviewComments,
4248
4261
  ...projection.rewind === void 0 ? {} : { rewind: { ranges: projection.rewind.ranges } }
4249
4262
  };
@@ -4259,7 +4272,7 @@ function envelope(projection, meta) {
4259
4272
  * session spent it in proportion to how many Claude sessions existed — which
4260
4273
  * is what left the settings panel unable to get a connection at all. One
4261
4274
  * carrier is one connection, whatever the session count. */
4262
- function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSession = () => [], repositoryForSession = async () => void 0, reviewCommentsForSession = () => []) {
4275
+ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSession = () => [], repositoryForSession = async () => void 0, reviewCommentsForSession = () => [], extraRepositoriesForSession = async () => []) {
4263
4276
  const info = (message) => {
4264
4277
  ctx.logger?.info?.(message);
4265
4278
  };
@@ -4272,12 +4285,14 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
4272
4285
  reviewComments: owned ? reviewCommentsForSession(sessionId) : []
4273
4286
  };
4274
4287
  };
4275
- const assembleMeta = async (sessionId) => {
4288
+ const assembleMeta = async (sessionId, activities) => {
4276
4289
  const meta = localMeta(sessionId);
4277
- const repository = meta.owned ? await repositoryForSession(sessionId) : void 0;
4290
+ if (!meta.owned) return meta;
4291
+ const [repository, repositories] = await Promise.all([repositoryForSession(sessionId), extraRepositoriesForSession(sessionId, activities ?? (await sidecar.read(sessionId)).activities)]);
4278
4292
  return {
4279
4293
  ...meta,
4280
- ...repository === void 0 ? {} : { repository }
4294
+ ...repository === void 0 ? {} : { repository },
4295
+ ...repositories.length === 0 ? {} : { repositories }
4281
4296
  };
4282
4297
  };
4283
4298
  const streamMulti = async (res, io, sessionIds) => {
@@ -4309,6 +4324,7 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
4309
4324
  owned: meta.owned,
4310
4325
  commands: meta.commands,
4311
4326
  ...meta.repository === void 0 ? {} : { repository: meta.repository },
4327
+ ...meta.repositories === void 0 ? {} : { repositories: meta.repositories },
4312
4328
  reviewComments: meta.reviewComments
4313
4329
  });
4314
4330
  };
@@ -4322,7 +4338,7 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
4322
4338
  seq: sidecar.sequence(sessionId),
4323
4339
  ...envelope(projection, meta)
4324
4340
  });
4325
- const probed = await assembleMeta(sessionId);
4341
+ const probed = await assembleMeta(sessionId, projection.activities);
4326
4342
  if (closed || JSON.stringify(probed) === JSON.stringify(metas.get(sessionId))) return;
4327
4343
  writeMeta(sessionId, probed);
4328
4344
  };
@@ -4435,7 +4451,8 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
4435
4451
  if (target.kind === "multi") return await streamMulti(res, io, target.sessionIds);
4436
4452
  info(`dsh-claude: projection poll for ${target.sessionId.slice(0, 64)}`);
4437
4453
  try {
4438
- const body = envelope(await sidecar.read(target.sessionId), await assembleMeta(target.sessionId));
4454
+ const projection = await sidecar.read(target.sessionId);
4455
+ const body = envelope(projection, await assembleMeta(target.sessionId, projection.activities));
4439
4456
  res.writeHead(200, {
4440
4457
  "content-type": "application/json; charset=utf-8",
4441
4458
  "cache-control": "no-store",
@@ -4531,8 +4548,13 @@ async function diffFuncnameArgs() {
4531
4548
  }
4532
4549
  //#endregion
4533
4550
  //#region src/repository-status.ts
4534
- const MAX_OUTPUT_BYTES$4 = 65536;
4551
+ const MAX_OUTPUT_BYTES$3 = 65536;
4552
+ /** What the diff panel receives: whole files packed under this budget. */
4535
4553
  const MAX_DIFF_BYTES = 262144;
4554
+ /** What git may hand back before packing; beyond this the diff is truncated. */
4555
+ const MAX_RAW_DIFF_BYTES = 8388608;
4556
+ /** One file's share of the panel; a lockfile or bundle past this is elided by name. */
4557
+ const MAX_FILE_PATCH_BYTES = 65536;
4536
4558
  const MAX_FILE_BYTES = 8388608;
4537
4559
  const MAX_UNTRACKED_DIFFS = 50;
4538
4560
  const GIT_TIMEOUT_MS$3 = 5e3;
@@ -4552,7 +4574,7 @@ async function collect$3(handle) {
4552
4574
  lossy: stdout?.lossy === true
4553
4575
  };
4554
4576
  }
4555
- async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$4) {
4577
+ async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$3) {
4556
4578
  const signal = AbortSignal.timeout(timeoutMs);
4557
4579
  return collect$3(runtime.spawn({
4558
4580
  argv: [executable, ...args],
@@ -4560,7 +4582,7 @@ async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUT
4560
4582
  stdio: {
4561
4583
  stdin: "ignore",
4562
4584
  stdout: { maxBytes },
4563
- stderr: { maxBytes: MAX_OUTPUT_BYTES$4 }
4585
+ stderr: { maxBytes: MAX_OUTPUT_BYTES$3 }
4564
4586
  },
4565
4587
  graceMs: 1e3,
4566
4588
  signal,
@@ -4639,6 +4661,31 @@ async function detectRepositoryOperation(gitDir) {
4639
4661
  };
4640
4662
  }
4641
4663
  }
4664
+ /**
4665
+ * Pack a unified diff into a byte budget one whole file at a time.
4666
+ *
4667
+ * A single oversized file (a lockfile, a bundle) used to void the entire
4668
+ * patch, which read as "no changes" in the panel. Files are kept in git's
4669
+ * order while they fit; a file past its own cap or past what is left of the
4670
+ * budget is skipped and named, so the panel still shows every other change.
4671
+ */
4672
+ function packPatchByFile(patch, budget = MAX_DIFF_BYTES, perFile = MAX_FILE_PATCH_BYTES) {
4673
+ let packed = "";
4674
+ const elided = [];
4675
+ for (const section of patch.split(/(?=^diff --git )/mu)) {
4676
+ if (section.length === 0) continue;
4677
+ if (section.length <= perFile && packed.length + section.length <= budget) {
4678
+ packed += section;
4679
+ continue;
4680
+ }
4681
+ const header = /^diff --git a\/(?<a>.*?) b\/(?<b>.*)$/mu.exec(section);
4682
+ elided.push(header?.groups?.["b"] ?? header?.groups?.["a"] ?? "?");
4683
+ }
4684
+ return {
4685
+ patch: packed,
4686
+ elided
4687
+ };
4688
+ }
4642
4689
  function parseDiffNumstat(value) {
4643
4690
  let additions = 0;
4644
4691
  let deletions = 0;
@@ -4735,6 +4782,7 @@ var RepositoryStatusService = class {
4735
4782
  #lastReady = /* @__PURE__ */ new Map();
4736
4783
  #gitExecutable;
4737
4784
  #ghExecutable;
4785
+ #roots = /* @__PURE__ */ new Map();
4738
4786
  constructor(runtime, cacheTtlMs = CACHE_TTL_MS) {
4739
4787
  this.#runtime = runtime;
4740
4788
  this.#cacheTtlMs = cacheTtlMs;
@@ -4757,7 +4805,27 @@ var RepositoryStatusService = class {
4757
4805
  this.#cache.delete(cwd);
4758
4806
  this.#lastReady.delete(cwd);
4759
4807
  }
4808
+ /** The repository holding `directory`, or undefined outside any. A root
4809
+ * does not move, so a settled answer is kept for the service's lifetime;
4810
+ * a probe that could not run is not an answer and is asked again. */
4811
+ rootOf(directory) {
4812
+ const known = this.#roots.get(directory);
4813
+ if (known !== void 0) return known;
4814
+ const value = (async () => {
4815
+ const top = await run(this.#runtime, await this.#git(), [
4816
+ "rev-parse",
4817
+ "--path-format=absolute",
4818
+ "--show-toplevel"
4819
+ ], directory, GIT_TIMEOUT_MS$3);
4820
+ return top.exitCode === 0 ? resolve(top.stdout.trim()) : void 0;
4821
+ })().catch(() => {
4822
+ this.#roots.delete(directory);
4823
+ });
4824
+ this.#roots.set(directory, value);
4825
+ return value;
4826
+ }
4760
4827
  dispose() {
4828
+ this.#roots.clear();
4761
4829
  this.#cache.clear();
4762
4830
  this.#lastReady.clear();
4763
4831
  }
@@ -4930,19 +4998,20 @@ var RepositoryStatusService = class {
4930
4998
  "--unified=3",
4931
4999
  base,
4932
5000
  "--"
4933
- ], cwd, GIT_TIMEOUT_MS$3, MAX_DIFF_BYTES);
5001
+ ], cwd, GIT_TIMEOUT_MS$3, MAX_RAW_DIFF_BYTES);
4934
5002
  if (patch.exitCode !== 0) return {
4935
5003
  ...summary,
4936
5004
  truncated: true
4937
5005
  };
4938
5006
  const untracked = await this.#untrackedDiff(cwd, git, signal);
4939
- const combinedPatch = `${patch.stdout}${untracked.patch}`;
5007
+ const packed = packPatchByFile(`${patch.stdout}${untracked.patch}`);
4940
5008
  return {
4941
5009
  additions: summary.additions + untracked.additions,
4942
5010
  deletions: summary.deletions,
4943
5011
  files: summary.files + untracked.files,
4944
- ...patch.lossy ? {} : { patch: combinedPatch.slice(0, MAX_DIFF_BYTES) },
4945
- truncated: patch.lossy || untracked.truncated || combinedPatch.length > MAX_DIFF_BYTES
5012
+ ...patch.lossy ? {} : { patch: packed.patch },
5013
+ ...packed.elided.length === 0 ? {} : { elided: packed.elided },
5014
+ truncated: patch.lossy || untracked.truncated
4946
5015
  };
4947
5016
  } catch {
4948
5017
  return;
@@ -5106,7 +5175,7 @@ async function summarizeBranchSlug(executablePath, intent, factory = query) {
5106
5175
  }
5107
5176
  //#endregion
5108
5177
  //#region src/repository-setup.ts
5109
- const MAX_OUTPUT_BYTES$3 = 131072;
5178
+ const MAX_OUTPUT_BYTES$2 = 131072;
5110
5179
  const GIT_TIMEOUT_MS$2 = 1e4;
5111
5180
  const GIT_FETCH_TIMEOUT_MS = 6e4;
5112
5181
  const MAX_PATH_CHARS$2 = 4096;
@@ -5626,8 +5695,8 @@ var RepositorySetupService = class {
5626
5695
  cwd,
5627
5696
  stdio: {
5628
5697
  stdin: "ignore",
5629
- stdout: { maxBytes: MAX_OUTPUT_BYTES$3 },
5630
- stderr: { maxBytes: MAX_OUTPUT_BYTES$3 }
5698
+ stdout: { maxBytes: MAX_OUTPUT_BYTES$2 },
5699
+ stderr: { maxBytes: MAX_OUTPUT_BYTES$2 }
5631
5700
  },
5632
5701
  graceMs: 1e3,
5633
5702
  signal: AbortSignal.timeout(timeoutMs),
@@ -5668,7 +5737,7 @@ var RepositorySetupService = class {
5668
5737
  };
5669
5738
  //#endregion
5670
5739
  //#region src/repository-actions.ts
5671
- const MAX_OUTPUT_BYTES$2 = 262144;
5740
+ const MAX_OUTPUT_BYTES$1 = 262144;
5672
5741
  const MAX_PATCH_CHARS = 65536;
5673
5742
  const MAX_MESSAGE_CHARS = 512;
5674
5743
  const MAX_PR_TEXT_CHARS = 8192;
@@ -5841,7 +5910,8 @@ var RepositoryActionService = class {
5841
5910
  const merged = await this.#run(gh, [
5842
5911
  "pr",
5843
5912
  "merge",
5844
- `--${method}`
5913
+ `--${method}`,
5914
+ ...request.admin === true ? ["--admin"] : []
5845
5915
  ], before.root, REMOTE_TIMEOUT_MS);
5846
5916
  if (merged.exitCode !== 0 || merged.lossy) {
5847
5917
  const reason = merged.stderr.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0).at(-1);
@@ -6093,7 +6163,7 @@ var RepositoryActionService = class {
6093
6163
  "--",
6094
6164
  ":(exclude)WARP.md",
6095
6165
  ":(exclude)**/WARP.md"
6096
- ], root, GIT_TIMEOUT_MS$1, MAX_OUTPUT_BYTES$2),
6166
+ ], root, GIT_TIMEOUT_MS$1, MAX_OUTPUT_BYTES$1),
6097
6167
  this.#run(git, [
6098
6168
  ...funcname,
6099
6169
  "diff",
@@ -6103,7 +6173,7 @@ var RepositoryActionService = class {
6103
6173
  "--",
6104
6174
  ":(exclude)WARP.md",
6105
6175
  ":(exclude)**/WARP.md"
6106
- ], root, GIT_TIMEOUT_MS$1, MAX_OUTPUT_BYTES$2)
6176
+ ], root, GIT_TIMEOUT_MS$1, MAX_OUTPUT_BYTES$1)
6107
6177
  ]);
6108
6178
  if (branchResult.exitCode !== 0) throw new RepositoryActionError("detached-head", "A detached HEAD cannot be committed from this panel.");
6109
6179
  if (headResult.exitCode !== 0 || statusResult.exitCode !== 0 || statusResult.lossy) throw new RepositoryActionError("repository-unavailable", "Repository state is unavailable.");
@@ -6197,14 +6267,14 @@ var RepositoryActionService = class {
6197
6267
  if (result.exitCode !== 0 || result.lossy) throw new RepositoryActionError(code, message);
6198
6268
  return result;
6199
6269
  }
6200
- #run(executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$2) {
6270
+ #run(executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$1) {
6201
6271
  return collect$1(this.#runtime.spawn({
6202
6272
  argv: [executable, ...args],
6203
6273
  cwd,
6204
6274
  stdio: {
6205
6275
  stdin: "ignore",
6206
6276
  stdout: { maxBytes },
6207
- stderr: { maxBytes: MAX_OUTPUT_BYTES$2 }
6277
+ stderr: { maxBytes: MAX_OUTPUT_BYTES$1 }
6208
6278
  },
6209
6279
  graceMs: 1e3,
6210
6280
  signal: AbortSignal.timeout(timeoutMs),
@@ -6282,7 +6352,7 @@ async function streamSetup(res, service, input) {
6282
6352
  * The prefix is registered as a stream because the setup POST holds its
6283
6353
  * connection open for the whole worktree build; the short sibling paths ride
6284
6354
  * the same registration and answer with `json` before releasing it. */
6285
- function registerRepositorySetupRoute(ctx, service, sweep) {
6355
+ function registerRepositorySetupRoute(ctx, service, sweep, cleaned) {
6286
6356
  registerPluginRoute(ctx, {
6287
6357
  mode: "stream",
6288
6358
  kind: "prefix",
@@ -6314,7 +6384,10 @@ function registerRepositorySetupRoute(ctx, service, sweep) {
6314
6384
  if (pathname === `/plugins/dsh-claude/repository/setup/cleanup`) {
6315
6385
  if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
6316
6386
  const input = await readJson$6(io);
6317
- return json(res, 200, await service.cleanupMerged(string$2(input, "path"), string$2(input, "baseBranch")));
6387
+ const path = string$2(input, "path");
6388
+ const result = await service.cleanupMerged(path, string$2(input, "baseBranch"));
6389
+ cleaned?.(path);
6390
+ return json(res, 200, result);
6318
6391
  }
6319
6392
  if (pathname === `/plugins/dsh-claude/repository/setup/sweep`) {
6320
6393
  if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
@@ -6342,7 +6415,8 @@ function registerRepositorySetupRoute(ctx, service, sweep) {
6342
6415
  //#endregion
6343
6416
  //#region src/repository-action-routes.ts
6344
6417
  const MAX_BODY_BYTES$6 = 16384;
6345
- const MAX_SESSION_ID_CHARS$6 = 1024;
6418
+ const MAX_SESSION_ID_CHARS$5 = 1024;
6419
+ const MAX_ROOT_CHARS = 4096;
6346
6420
  const ACTIONS = /* @__PURE__ */ new Set([
6347
6421
  "commit",
6348
6422
  "commit-push",
@@ -6378,9 +6452,13 @@ async function readJson$5(io) {
6378
6452
  }
6379
6453
  function sessionId$1(url) {
6380
6454
  const value = url.searchParams.get("sessionId");
6381
- if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$6) throw new RepositoryActionError("invalid-session", "The session is invalid.");
6455
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$5) throw new RepositoryActionError("invalid-session", "The session is invalid.");
6382
6456
  return value;
6383
6457
  }
6458
+ function requestedRoot(url) {
6459
+ const value = url.searchParams.get("root");
6460
+ return value === null || value.length === 0 || value.length > MAX_ROOT_CHARS ? void 0 : value;
6461
+ }
6384
6462
  function string$1(input, key) {
6385
6463
  const value = input[key];
6386
6464
  if (typeof value !== "string") throw new RepositoryActionError("invalid-request", `The ${key} field is required.`);
@@ -6409,6 +6487,9 @@ function actionRequest(input) {
6409
6487
  ...input.push === void 0 ? {} : typeof input.push === "boolean" ? { push: input.push } : (() => {
6410
6488
  throw new RepositoryActionError("invalid-request", "The push field must be a boolean.");
6411
6489
  })(),
6490
+ ...input.admin === void 0 ? {} : typeof input.admin === "boolean" ? { admin: input.admin } : (() => {
6491
+ throw new RepositoryActionError("invalid-request", "The admin field must be a boolean.");
6492
+ })(),
6412
6493
  ...input.mergeMethod === void 0 ? {} : input.mergeMethod === "merge" || input.mergeMethod === "squash" || input.mergeMethod === "rebase" ? { mergeMethod: input.mergeMethod } : (() => {
6413
6494
  throw new RepositoryActionError("invalid-request", "The mergeMethod field is invalid.");
6414
6495
  })()
@@ -6428,7 +6509,7 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
6428
6509
  handler: async (io) => {
6429
6510
  const url = io.url;
6430
6511
  try {
6431
- const cwd = cwdForSession(sessionId$1(url));
6512
+ const cwd = cwdForSession(sessionId$1(url), requestedRoot(url));
6432
6513
  if (cwd === void 0) throw new RepositoryActionError("session-unavailable", "The Claude session is unavailable.");
6433
6514
  if (url.pathname === `/plugins/dsh-claude/repository/action/preview`) {
6434
6515
  if (io.method !== "GET") return {
@@ -6490,187 +6571,6 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
6490
6571
  });
6491
6572
  }
6492
6573
  //#endregion
6493
- //#region src/editor-open.ts
6494
- const MAX_OUTPUT_BYTES$1 = 8192;
6495
- /** Long enough for a launcher shim to fail loudly, short enough that the
6496
- * request returns while the IDE is still booting. On Windows and Linux the
6497
- * IDE binary IS the launched process, so still running past this is success. */
6498
- const SETTLE_MS = 1500;
6499
- const EDITOR_IDS = /* @__PURE__ */ new Set(["cursor", "idea"]);
6500
- /** Launch commands tried in order, first success wins. macOS keeps `open -a`
6501
- * behind the CLI shim because both shims are opt-in installs there, while
6502
- * `open -a` finds the bundle wherever Toolbox or the DMG dropped it. */
6503
- const LAUNCHERS = {
6504
- cursor: {
6505
- darwin: [["cursor"], [
6506
- "open",
6507
- "-a",
6508
- "Cursor"
6509
- ]],
6510
- win32: [["cursor"]],
6511
- linux: [["cursor"]]
6512
- },
6513
- idea: {
6514
- darwin: [
6515
- ["idea"],
6516
- [
6517
- "open",
6518
- "-a",
6519
- "IntelliJ IDEA"
6520
- ],
6521
- [
6522
- "open",
6523
- "-a",
6524
- "IntelliJ IDEA CE"
6525
- ]
6526
- ],
6527
- win32: [["idea"], ["idea64.exe"]],
6528
- linux: [["idea"], ["idea.sh"]]
6529
- }
6530
- };
6531
- /** cmd.exe re-interprets these, and a mis-parsed path opens the wrong project
6532
- * rather than failing. Refuse instead of guessing. */
6533
- const WINDOWS_UNSAFE = /["&|<>^%]/u;
6534
- var EditorOpenError = class extends Error {
6535
- code;
6536
- constructor(code, message) {
6537
- super(message);
6538
- this.name = "EditorOpenError";
6539
- this.code = code;
6540
- }
6541
- };
6542
- /** Open a session's working directory in a desktop editor. */
6543
- var EditorOpenService = class {
6544
- #runtime;
6545
- #platform;
6546
- #settleMs;
6547
- constructor(runtime, platform = process.platform, settleMs = SETTLE_MS) {
6548
- this.#runtime = runtime;
6549
- this.#platform = platform;
6550
- this.#settleMs = settleMs;
6551
- }
6552
- async open(cwd, editor) {
6553
- if (this.#platform === "win32" && WINDOWS_UNSAFE.test(cwd)) throw new EditorOpenError("unsupported-path", "The project path cannot be opened through the Windows shell.");
6554
- const candidates = LAUNCHERS[editor][this.#platform] ?? LAUNCHERS[editor].linux ?? [];
6555
- let found = false;
6556
- for (const candidate of candidates) {
6557
- const argv = await this.#argv(candidate, cwd);
6558
- if (argv === void 0) continue;
6559
- found = true;
6560
- if (await this.#launch(argv, cwd)) return;
6561
- }
6562
- throw found ? new EditorOpenError("launch-failed", "The editor refused to open the project.") : new EditorOpenError("editor-unavailable", "The editor was not found on PATH.");
6563
- }
6564
- async #argv(candidate, cwd) {
6565
- const [program, ...rest] = candidate;
6566
- if (program === void 0) return void 0;
6567
- if (this.#platform === "win32") return [
6568
- "cmd.exe",
6569
- "/d",
6570
- "/s",
6571
- "/c",
6572
- program,
6573
- ...rest,
6574
- cwd
6575
- ];
6576
- try {
6577
- return [
6578
- await this.#runtime.resolveExecutable(program),
6579
- ...rest,
6580
- cwd
6581
- ];
6582
- } catch {
6583
- return;
6584
- }
6585
- }
6586
- /** True once the editor is launched: either the shim exited cleanly or the
6587
- * process is still alive past the settle window. */
6588
- async #launch(argv, cwd) {
6589
- let handle;
6590
- try {
6591
- handle = this.#runtime.spawn({
6592
- argv,
6593
- cwd,
6594
- stdio: {
6595
- stdin: "ignore",
6596
- stdout: { maxBytes: MAX_OUTPUT_BYTES$1 },
6597
- stderr: { maxBytes: MAX_OUTPUT_BYTES$1 }
6598
- },
6599
- graceMs: 1e3,
6600
- env: {}
6601
- });
6602
- } catch {
6603
- return false;
6604
- }
6605
- return await Promise.race([handle.done.then((outcome) => outcome.exitCode === 0), new Promise((resolve) => {
6606
- setTimeout(() => resolve(true), this.#settleMs).unref?.();
6607
- })]);
6608
- }
6609
- };
6610
- //#endregion
6611
- //#region src/editor-open-routes.ts
6612
- const MAX_SESSION_ID_CHARS$5 = 1024;
6613
- /** Open the session's working directory in a desktop editor. Query-only: the
6614
- * request carries two enum-ish values, so there is no body to parse. */
6615
- function registerEditorOpenRoute(ctx, service, cwdForSession) {
6616
- registerPluginRoute(ctx, {
6617
- mode: "unary",
6618
- kind: "exact",
6619
- path: CLAUDE_EDITOR_OPEN_PATH,
6620
- methods: ["POST"],
6621
- budget: "fast",
6622
- handler: async (io) => {
6623
- const params = io.url.searchParams;
6624
- const id = params.get("sessionId");
6625
- const editor = params.get("editor");
6626
- if (id === null || id.length === 0 || id.length > MAX_SESSION_ID_CHARS$5) return {
6627
- status: 400,
6628
- value: {
6629
- error: "invalid-session",
6630
- message: "The session is invalid."
6631
- }
6632
- };
6633
- if (editor === null || !EDITOR_IDS.has(editor)) return {
6634
- status: 400,
6635
- value: {
6636
- error: "invalid-editor",
6637
- message: "The editor is invalid."
6638
- }
6639
- };
6640
- const cwd = cwdForSession(id);
6641
- if (cwd === void 0) return {
6642
- status: 409,
6643
- value: {
6644
- error: "session-unavailable",
6645
- message: "The Claude session is unavailable."
6646
- }
6647
- };
6648
- try {
6649
- await service.open(cwd, editor);
6650
- return {
6651
- status: 200,
6652
- value: { opened: true }
6653
- };
6654
- } catch (error) {
6655
- if (error instanceof EditorOpenError) return {
6656
- status: 409,
6657
- value: {
6658
- error: error.code,
6659
- message: error.message
6660
- }
6661
- };
6662
- return {
6663
- status: 500,
6664
- value: {
6665
- error: "editor-open-unavailable",
6666
- message: "The editor could not be launched."
6667
- }
6668
- };
6669
- }
6670
- }
6671
- });
6672
- }
6673
- //#endregion
6674
6574
  //#region src/prompts.ts
6675
6575
  const MAX_PROMPT_FILES = 256;
6676
6576
  const MAX_PROMPT_BYTES = 16384;
@@ -7467,7 +7367,7 @@ function registerPullRequestFeedbackRoute(ctx, service, cwdForSession) {
7467
7367
  const reads = io.method === "GET";
7468
7368
  const writes = io.method === "POST";
7469
7369
  try {
7470
- const cwd = cwdForSession(sessionId(url));
7370
+ const cwd = cwdForSession(sessionId(url), url.searchParams.get("root") ?? void 0);
7471
7371
  if (cwd === void 0) throw new PullRequestFeedbackError("session-unavailable", "The Claude session is unavailable.");
7472
7372
  if (url.pathname === `/plugins/dsh-claude/repository/feedback/comments`) {
7473
7373
  if (!reads) return {
@@ -8606,6 +8506,60 @@ function registerClaudeRewindRoute(ctx, sidecar, access) {
8606
8506
  });
8607
8507
  }
8608
8508
  //#endregion
8509
+ //#region src/touched-repositories.ts
8510
+ /** Which other repositories a session has written into.
8511
+ *
8512
+ * A session is pinned to one checkout, but Claude writes wherever it is
8513
+ * pointed: a monorepo sibling, a dependency checked out next door. Those
8514
+ * edits never showed up anywhere -- no diff, no pull request -- because every
8515
+ * repository read keyed off the session's own cwd. The file tools' arguments
8516
+ * are already on the activity log, so the extra roots are derived from there
8517
+ * rather than tracked as new state. */
8518
+ const FILE_TOOLS = /* @__PURE__ */ new Set([
8519
+ "Edit",
8520
+ "MultiEdit",
8521
+ "Write",
8522
+ "NotebookEdit"
8523
+ ]);
8524
+ /** The activity detail is a redacted JSON string that may be cut short, so
8525
+ * this matches the one key rather than parsing the document. */
8526
+ const PATH_KEY = /"(?:file_path|notebook_path)"\s*:\s*"((?:[^"\\]|\\.)*)"/u;
8527
+ /** Absolute paths Claude wrote through its file tools, first-seen order. */
8528
+ function touchedFilePaths(activities) {
8529
+ const paths = /* @__PURE__ */ new Set();
8530
+ for (const activity of activities) {
8531
+ if (activity.kind !== "tool-call" && activity.kind !== "subagent" || activity.toolName === void 0 || !FILE_TOOLS.has(activity.toolName) || activity.detail === void 0) continue;
8532
+ const escaped = PATH_KEY.exec(activity.detail)?.[1];
8533
+ if (escaped === void 0) continue;
8534
+ try {
8535
+ const path = JSON.parse(`"${escaped}"`);
8536
+ if (isAbsolute(path)) paths.add(path);
8537
+ } catch {}
8538
+ }
8539
+ return [...paths];
8540
+ }
8541
+ /** Repository roots behind the touched paths, minus the session's own, in
8542
+ * first-seen order. `rootOf` answers undefined outside any repository. */
8543
+ async function touchedRepositoryRoots(paths, sessionRoot, rootOf, max) {
8544
+ const roots = [];
8545
+ const directories = new Set(paths.map((path) => dirname(path)));
8546
+ for (const directory of directories) {
8547
+ const root = await rootOf(directory);
8548
+ if (root === void 0 || root === sessionRoot || roots.includes(root)) continue;
8549
+ roots.push(root);
8550
+ if (roots.length >= max) break;
8551
+ }
8552
+ return roots;
8553
+ }
8554
+ /** Whether a linked checkout still has anything to show. A cleaned-up
8555
+ * worktree is gone, and a checkout cleaned up in place is back on base with
8556
+ * nothing pending -- either way its bar comes down. A clean checkout with an
8557
+ * open or merged pull request, or unpushed work, stays. */
8558
+ function linkedRepositoryShown(status) {
8559
+ if (status.status !== "ready") return false;
8560
+ return status.dirty === true || status.upstream === false || (status.ahead ?? 0) > 0 || status.pullRequest !== void 0;
8561
+ }
8562
+ //#endregion
8609
8563
  //#region src/update-routes.ts
8610
8564
  const PLUGIN_PACKAGE_NAME = "@norman-else/dsh-claude";
8611
8565
  const MAX_MANIFEST_BYTES = 262144;
@@ -8875,6 +8829,54 @@ function registerClaudeUpdateRoutes(ctx, runtime, deps = {}) {
8875
8829
  });
8876
8830
  }
8877
8831
  //#endregion
8832
+ //#region src/windows-job-runner.ts
8833
+ const RUN_AS_NODE = "ELECTRON_RUN_AS_NODE";
8834
+ /**
8835
+ * Make the Host's Windows Job runner start as Node when the Host itself is an
8836
+ * Electron utility process.
8837
+ *
8838
+ * Since DSH Desktop 2.0.7 the Host runs in `utilityProcess.fork` and, on
8839
+ * Windows, `subprocess-local` launches every ordinary target through a helper
8840
+ * it spawns as `[process.execPath, runner.js]`. Inside Electron that path is
8841
+ * `DSH Desktop.exe`, and the fork carries no `ELECTRON_RUN_AS_NODE`, so the
8842
+ * helper starts as a second GUI instance, yields to the running one, and exits
8843
+ * 0 without ever reporting -- the Host then fails the spawn with "Windows Job
8844
+ * runner exited with exit code 0 before proving its managed range empty".
8845
+ *
8846
+ * The runner's environment is read from `process.env` synchronously inside
8847
+ * `spawn()`, so the flag is set for exactly that call and removed after. The
8848
+ * target's own environment is built the same way, so the spec tells the Host
8849
+ * to drop the flag there: a target must not inherit a variable that turns any
8850
+ * Electron app it launches into a bare Node process.
8851
+ */
8852
+ function withElectronNodeRunner(runtime, options = {}) {
8853
+ const platform = options.platform ?? process.platform;
8854
+ const electron = options.electron ?? process.versions.electron !== void 0;
8855
+ if (platform !== "win32" || !electron) return runtime;
8856
+ return new Proxy(runtime, { get(target, property, receiver) {
8857
+ if (property !== "spawn") {
8858
+ const value = Reflect.get(target, property, receiver);
8859
+ return typeof value === "function" ? value.bind(target) : value;
8860
+ }
8861
+ return (spec) => {
8862
+ const withoutFlag = {
8863
+ ...spec,
8864
+ env: {
8865
+ ...spec.env,
8866
+ [RUN_AS_NODE]: void 0
8867
+ }
8868
+ };
8869
+ if (process.env[RUN_AS_NODE] !== void 0) return target.spawn(withoutFlag);
8870
+ process.env[RUN_AS_NODE] = "1";
8871
+ try {
8872
+ return target.spawn(withoutFlag);
8873
+ } finally {
8874
+ delete process.env[RUN_AS_NODE];
8875
+ }
8876
+ };
8877
+ } });
8878
+ }
8879
+ //#endregion
8878
8880
  //#region src/plan-usage-routes.ts
8879
8881
  const EMPTY = {
8880
8882
  available: false,
@@ -9314,6 +9316,8 @@ const CLAUDE_SCOPE_UNAVAILABLE_MESSAGE = "agent command scope unavailable (prese
9314
9316
  const CATALOG_RETRY_MS = 5e3;
9315
9317
  const SCOPE_RETRY_MS = 500;
9316
9318
  const MAX_CATALOG_RETRIES = 3;
9319
+ /** Bounded: each extra checkout costs a git chain and a `gh pr view` per sweep. */
9320
+ const MAX_EXTRA_REPOSITORIES = 8;
9317
9321
  const MAX_SCOPE_RETRIES = 24;
9318
9322
  function mountClaudeMetadata(ctx, supervisor, agent, model, sidecar, publishCommands = () => {}, resolveCommands = () => ctx.agentPresets.serviceFor(agent, CLAUDE_COMMANDS_SERVICE)) {
9319
9323
  if (ctx.agentPresets.composedPreset(agent.ctx) !== "claude") return void 0;
@@ -9420,6 +9424,7 @@ async function installManagedPresetCompatibility(logger, install = ensureManaged
9420
9424
  }
9421
9425
  }
9422
9426
  async function apply(ctx, config) {
9427
+ const subprocess = withElectronNodeRunner(ctx.subprocess);
9423
9428
  await installManagedPresetCompatibility(ctx.logger);
9424
9429
  const defaultLimits = {
9425
9430
  idleTimeoutMs: config.idleTimeoutMs ?? 18e5,
@@ -9437,15 +9442,15 @@ async function apply(ctx, config) {
9437
9442
  };
9438
9443
  await applySettingsOverrides();
9439
9444
  const sidecar = new ClaudeSidecarRepository();
9440
- const repositoryStatus = new RepositoryStatusService(ctx.subprocess);
9441
- const repositorySetup = new RepositorySetupService(ctx.subprocess, {
9445
+ const repositoryStatus = new RepositoryStatusService(subprocess);
9446
+ const repositorySetup = new RepositorySetupService(subprocess, {
9442
9447
  branchPrefix: () => readWorktreeBranchPrefix(),
9443
9448
  summarizeBranch: (intent) => summarizeBranchSlug(supervisorConfig.executablePath, intent)
9444
9449
  });
9445
9450
  const reviewComments = new ReviewCommentStore();
9446
9451
  const commandCatalogs = /* @__PURE__ */ new Map();
9447
9452
  const supervisor = new ClaudeSupervisor({
9448
- runtime: ctx.subprocess,
9453
+ runtime: subprocess,
9449
9454
  approval: ctx.approval,
9450
9455
  userQuestions: ctx.userQuestions,
9451
9456
  config: supervisorConfig,
@@ -9454,7 +9459,7 @@ async function apply(ctx, config) {
9454
9459
  });
9455
9460
  let resolutionError;
9456
9461
  try {
9457
- supervisorConfig.executablePath = (await resolveClaudeExecutable(ctx.subprocess, config.executablePath === void 0 || config.executablePath.length === 0 ? void 0 : config.executablePath)).path;
9462
+ supervisorConfig.executablePath = (await resolveClaudeExecutable(subprocess, config.executablePath === void 0 || config.executablePath.length === 0 ? void 0 : config.executablePath)).path;
9458
9463
  ctx.llm.registerAdapter([...CLAUDE_CODE_PROVIDER_IDS], createClaudeCodeAdapter(supervisor, ctx.agents, ctx.attachments, (agent) => ctx.agentPresets.composedPreset(agent.ctx), (sessionId) => reviewComments.drain(sessionId), () => readRenderMode(), (request) => summarizeSessionTitle(supervisorConfig.executablePath, request), () => probeClaudeModels(supervisorConfig.executablePath)));
9459
9464
  ctx.effect(() => {
9460
9465
  const mounted = /* @__PURE__ */ new Map();
@@ -9554,9 +9559,9 @@ async function apply(ctx, config) {
9554
9559
  ctx.effect(() => () => repositoryStatus.dispose(), "dsh-claude: repository status cache");
9555
9560
  ctx.inject(["webServer"], (webCtx) => {
9556
9561
  registerClaudeClientDiagnosticsRoute(webCtx);
9557
- registerClaudeDoctorRoutes(webCtx, webCtx.subprocess, supervisor, supervisorConfig, resolutionError);
9562
+ registerClaudeDoctorRoutes(webCtx, subprocess, supervisor, supervisorConfig, resolutionError);
9558
9563
  const desktopActions = webCtx.get("desktopActions");
9559
- registerClaudeUpdateRoutes(webCtx, webCtx.subprocess, { ...typeof desktopActions?.requestRestart === "function" ? { requestRestart: desktopActions.requestRestart.bind(desktopActions) } : {} });
9564
+ registerClaudeUpdateRoutes(webCtx, subprocess, { ...typeof desktopActions?.requestRestart === "function" ? { requestRestart: desktopActions.requestRestart.bind(desktopActions) } : {} });
9560
9565
  registerClaudeGlobalSettingsRoute(webCtx, {
9561
9566
  defaultLimits,
9562
9567
  onUpdated: async () => {
@@ -9564,24 +9569,36 @@ async function apply(ctx, config) {
9564
9569
  supervisor.limitsChanged();
9565
9570
  }
9566
9571
  });
9567
- registerRepositorySetupRoute(webCtx, repositorySetup, () => sweepWorktrees?.());
9572
+ registerRepositorySetupRoute(webCtx, repositorySetup, () => sweepWorktrees?.(), (path) => repositoryStatus.invalidate(path));
9568
9573
  registerRepositoryStatusRoute(webCtx, repositoryStatus);
9569
9574
  registerRepositoryFileRoute(webCtx, repositoryStatus);
9570
9575
  registerJiraRoute(webCtx, new JiraService());
9571
- const repositoryActions = new RepositoryActionService(webCtx.subprocess, supervisorConfig.executablePath, (cwd) => repositoryStatus.invalidate(cwd));
9572
- const cwdForClaudeSession = (sessionId) => {
9576
+ const repositoryActions = new RepositoryActionService(subprocess, supervisorConfig.executablePath, (cwd) => repositoryStatus.invalidate(cwd));
9577
+ /** Roots the latest projection probe vouched for, per session: the only
9578
+ * checkouts a route may act on besides the session's own. */
9579
+ const extraRoots = /* @__PURE__ */ new Map();
9580
+ const cwdForClaudeSession = (sessionId, root) => {
9573
9581
  const agent = webCtx.agents.get(sessionId);
9574
9582
  if (agent === void 0 || webCtx.agentPresets.composedPreset(agent.ctx) !== "claude") return void 0;
9575
- return agent.session.header.cwd;
9583
+ if (root === void 0) return agent.session.header.cwd;
9584
+ return extraRoots.get(sessionId)?.includes(root) === true ? root : void 0;
9585
+ };
9586
+ const extraRepositoriesForClaudeSession = async (sessionId, activities) => {
9587
+ const cwd = cwdForClaudeSession(sessionId);
9588
+ if (cwd === void 0) return [];
9589
+ const own = await repositoryStatus.rootOf(cwd);
9590
+ const roots = await touchedRepositoryRoots(touchedFilePaths(activities), own ?? cwd, (directory) => repositoryStatus.rootOf(directory), MAX_EXTRA_REPOSITORIES);
9591
+ const statuses = (await Promise.all(roots.map((root) => repositoryStatus.inspect(root)))).filter(linkedRepositoryShown);
9592
+ extraRoots.set(sessionId, statuses.map((status) => status.root ?? status.cwd));
9593
+ return statuses;
9576
9594
  };
9577
9595
  registerRepositoryActionRoute(webCtx, repositoryActions, cwdForClaudeSession);
9578
- registerEditorOpenRoute(webCtx, new EditorOpenService(webCtx.subprocess), cwdForClaudeSession);
9579
9596
  registerClaudePromptsRoute(webCtx);
9580
- const promptAssist = new PromptAssistService(webCtx.subprocess, () => supervisorConfig.executablePath);
9597
+ const promptAssist = new PromptAssistService(subprocess, () => supervisorConfig.executablePath);
9581
9598
  registerClaudePromptNameRoute(webCtx, promptAssist);
9582
9599
  registerClaudePromptRefineRoute(webCtx, promptAssist);
9583
- registerPullRequestFeedbackRoute(webCtx, new PullRequestFeedbackService(webCtx.subprocess), cwdForClaudeSession);
9584
- registerAskRoute(webCtx, new AskService(webCtx.subprocess, supervisorConfig.executablePath), cwdForClaudeSession, (sessionId) => {
9600
+ registerPullRequestFeedbackRoute(webCtx, new PullRequestFeedbackService(subprocess), cwdForClaudeSession);
9601
+ registerAskRoute(webCtx, new AskService(subprocess, supervisorConfig.executablePath), cwdForClaudeSession, (sessionId) => {
9585
9602
  const snapshot = supervisor.snapshots().find((item) => item.sessionId === sessionId);
9586
9603
  return snapshot === void 0 ? void 0 : {
9587
9604
  model: claudeModelValue(snapshot.model),
@@ -9603,7 +9620,7 @@ async function apply(ctx, config) {
9603
9620
  reset: (sessionId) => supervisor.disposeSession(sessionId),
9604
9621
  restoreFiles: async (sessionId, tree) => {
9605
9622
  const cwd = webCtx.agents.get(sessionId)?.session.header.cwd;
9606
- return cwd === void 0 ? false : restoreWorktreeTree(ctx.subprocess, cwd, tree);
9623
+ return cwd === void 0 ? false : restoreWorktreeTree(subprocess, cwd, tree);
9607
9624
  }
9608
9625
  });
9609
9626
  registerPlanUsageRoute(webCtx, (fetchedAt) => probePlanUsage(supervisorConfig.executablePath, fetchedAt));
@@ -9612,7 +9629,7 @@ async function apply(ctx, config) {
9612
9629
  if (agent === void 0 || webCtx.agentPresets.composedPreset(agent.ctx) !== "claude") return void 0;
9613
9630
  const cwd = agent.session.header.cwd;
9614
9631
  return cwd === void 0 ? void 0 : repositoryStatus.inspect(cwd);
9615
- }, (sessionId) => reviewComments.list(sessionId));
9632
+ }, (sessionId) => reviewComments.list(sessionId), extraRepositoriesForClaudeSession);
9616
9633
  });
9617
9634
  }
9618
9635
  //#endregion