@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/INSTALL.md +55 -55
- package/LICENSE +21 -21
- package/README.md +199 -200
- package/cordis.patch.yml +12 -12
- package/legacy-preset/agent.cordis.yml +11 -11
- package/legacy-preset/preset.yml +4 -4
- package/lib/bin.mjs +1 -1
- package/lib/bin.mjs.map +1 -1
- package/lib/client.d.ts +5 -11
- package/lib/client.js +1044 -1147
- package/lib/client.js.map +1 -1
- package/lib/{events-oovRTmX7.mjs → events-B498uofA.mjs} +2 -3
- package/lib/events-B498uofA.mjs.map +1 -0
- package/lib/index.d.mts +0 -1
- package/lib/index.mjs +254 -237
- package/lib/index.mjs.map +1 -1
- package/lib/{presenters-BVWj7u0a.mjs → presenters-CRFcjLSC.mjs} +2 -2
- package/lib/presenters-CRFcjLSC.mjs.map +1 -0
- package/lib/{preset-installer-CuosP3sA.mjs → preset-installer-C4TTNV9L.mjs} +2 -2
- package/lib/preset-installer-C4TTNV9L.mjs.map +1 -0
- package/lib/preset-route.mjs +2 -2
- package/lib/preset-route.mjs.map +1 -1
- package/package.json +222 -197
- package/preset/claude/agent.cordis.yml +11 -11
- package/preset/claude/preset.yml +4 -4
- package/lib/events-oovRTmX7.mjs.map +0 -1
- package/lib/presenters-BVWj7u0a.mjs.map +0 -1
- package/lib/preset-installer-CuosP3sA.mjs.map +0 -1
package/lib/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
import { a as projectClaudeCommands, i as CLAUDE_COMMANDS_SERVICE, r as dynamicPresenterDefinition, t as CLAUDE_PRESENTER_NAMES } from "./presenters-
|
|
3
|
-
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-
|
|
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$
|
|
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$
|
|
1839
|
-
stderr: { maxBytes: MAX_OUTPUT_BYTES$
|
|
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: "
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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
|
|
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$
|
|
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$
|
|
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$
|
|
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,
|
|
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
|
|
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:
|
|
4945
|
-
|
|
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$
|
|
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$
|
|
5630
|
-
stderr: { maxBytes: MAX_OUTPUT_BYTES$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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$
|
|
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$
|
|
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(
|
|
9441
|
-
const repositorySetup = new RepositorySetupService(
|
|
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:
|
|
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(
|
|
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,
|
|
9562
|
+
registerClaudeDoctorRoutes(webCtx, subprocess, supervisor, supervisorConfig, resolutionError);
|
|
9558
9563
|
const desktopActions = webCtx.get("desktopActions");
|
|
9559
|
-
registerClaudeUpdateRoutes(webCtx,
|
|
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(
|
|
9572
|
-
|
|
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(
|
|
9597
|
+
const promptAssist = new PromptAssistService(subprocess, () => supervisorConfig.executablePath);
|
|
9581
9598
|
registerClaudePromptNameRoute(webCtx, promptAssist);
|
|
9582
9599
|
registerClaudePromptRefineRoute(webCtx, promptAssist);
|
|
9583
|
-
registerPullRequestFeedbackRoute(webCtx, new PullRequestFeedbackService(
|
|
9584
|
-
registerAskRoute(webCtx, new AskService(
|
|
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(
|
|
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
|