@norman-else/dsh-claude 0.1.46 → 0.1.48
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/README.md +1 -2
- package/lib/bin.mjs +1 -1
- package/lib/client.d.ts +4 -14
- package/lib/client.js +702 -1198
- 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 +454 -274
- package/lib/index.mjs.map +1 -1
- package/lib/{presenters-BVWj7u0a.mjs → presenters-CRFcjLSC.mjs} +2 -2
- package/lib/{presenters-BVWj7u0a.mjs.map → presenters-CRFcjLSC.mjs.map} +1 -1
- package/lib/{preset-installer-CuosP3sA.mjs → preset-installer-C4TTNV9L.mjs} +2 -2
- package/lib/{preset-installer-CuosP3sA.mjs.map → preset-installer-C4TTNV9L.mjs.map} +1 -1
- package/lib/preset-route.mjs +2 -2
- package/package.json +121 -96
- package/lib/events-oovRTmX7.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",
|
|
@@ -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}/`;
|
|
@@ -4536,15 +4548,22 @@ async function diffFuncnameArgs() {
|
|
|
4536
4548
|
}
|
|
4537
4549
|
//#endregion
|
|
4538
4550
|
//#region src/repository-status.ts
|
|
4539
|
-
const MAX_OUTPUT_BYTES$
|
|
4551
|
+
const MAX_OUTPUT_BYTES$3 = 65536;
|
|
4552
|
+
/** What the diff panel receives: whole files packed under this budget. */
|
|
4540
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;
|
|
4541
4558
|
const MAX_FILE_BYTES = 8388608;
|
|
4542
4559
|
const MAX_UNTRACKED_DIFFS = 50;
|
|
4543
4560
|
const GIT_TIMEOUT_MS$3 = 5e3;
|
|
4544
4561
|
const GH_TIMEOUT_MS$1 = 8e3;
|
|
4545
4562
|
const CACHE_TTL_MS = 5e3;
|
|
4563
|
+
const PULL_REQUEST_TTL_FACTOR = 12;
|
|
4546
4564
|
const MAX_TEXT_CHARS = 1024;
|
|
4547
4565
|
const MAX_CONFLICT_PATHS = 100;
|
|
4566
|
+
const PULL_REQUEST_FIELDS = "number,title,url,state,isDraft,reviewDecision,mergeStateStatus,mergedAt,statusCheckRollup,author,createdAt,baseRefName,headRefName,additions,deletions,changedFiles";
|
|
4548
4567
|
function bounded(value) {
|
|
4549
4568
|
return value.trim().slice(0, MAX_TEXT_CHARS);
|
|
4550
4569
|
}
|
|
@@ -4557,7 +4576,7 @@ async function collect$3(handle) {
|
|
|
4557
4576
|
lossy: stdout?.lossy === true
|
|
4558
4577
|
};
|
|
4559
4578
|
}
|
|
4560
|
-
async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$
|
|
4579
|
+
async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$3) {
|
|
4561
4580
|
const signal = AbortSignal.timeout(timeoutMs);
|
|
4562
4581
|
return collect$3(runtime.spawn({
|
|
4563
4582
|
argv: [executable, ...args],
|
|
@@ -4565,7 +4584,7 @@ async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUT
|
|
|
4565
4584
|
stdio: {
|
|
4566
4585
|
stdin: "ignore",
|
|
4567
4586
|
stdout: { maxBytes },
|
|
4568
|
-
stderr: { maxBytes: MAX_OUTPUT_BYTES$
|
|
4587
|
+
stderr: { maxBytes: MAX_OUTPUT_BYTES$3 }
|
|
4569
4588
|
},
|
|
4570
4589
|
graceMs: 1e3,
|
|
4571
4590
|
signal,
|
|
@@ -4644,6 +4663,31 @@ async function detectRepositoryOperation(gitDir) {
|
|
|
4644
4663
|
};
|
|
4645
4664
|
}
|
|
4646
4665
|
}
|
|
4666
|
+
/**
|
|
4667
|
+
* Pack a unified diff into a byte budget one whole file at a time.
|
|
4668
|
+
*
|
|
4669
|
+
* A single oversized file (a lockfile, a bundle) used to void the entire
|
|
4670
|
+
* patch, which read as "no changes" in the panel. Files are kept in git's
|
|
4671
|
+
* order while they fit; a file past its own cap or past what is left of the
|
|
4672
|
+
* budget is skipped and named, so the panel still shows every other change.
|
|
4673
|
+
*/
|
|
4674
|
+
function packPatchByFile(patch, budget = MAX_DIFF_BYTES, perFile = MAX_FILE_PATCH_BYTES) {
|
|
4675
|
+
let packed = "";
|
|
4676
|
+
const elided = [];
|
|
4677
|
+
for (const section of patch.split(/(?=^diff --git )/mu)) {
|
|
4678
|
+
if (section.length === 0) continue;
|
|
4679
|
+
if (section.length <= perFile && packed.length + section.length <= budget) {
|
|
4680
|
+
packed += section;
|
|
4681
|
+
continue;
|
|
4682
|
+
}
|
|
4683
|
+
const header = /^diff --git a\/(?<a>.*?) b\/(?<b>.*)$/mu.exec(section);
|
|
4684
|
+
elided.push(header?.groups?.["b"] ?? header?.groups?.["a"] ?? "?");
|
|
4685
|
+
}
|
|
4686
|
+
return {
|
|
4687
|
+
patch: packed,
|
|
4688
|
+
elided
|
|
4689
|
+
};
|
|
4690
|
+
}
|
|
4647
4691
|
function parseDiffNumstat(value) {
|
|
4648
4692
|
let additions = 0;
|
|
4649
4693
|
let deletions = 0;
|
|
@@ -4719,6 +4763,7 @@ function parsePullRequest(value) {
|
|
|
4719
4763
|
review: reviewState(input.reviewDecision),
|
|
4720
4764
|
checks: aggregateChecks(input.statusCheckRollup),
|
|
4721
4765
|
...typeof input.mergeStateStatus === "string" ? { mergeState: bounded(input.mergeStateStatus) } : {},
|
|
4766
|
+
...typeof input.headRefName === "string" && input.headRefName.length > 0 ? { headBranch: bounded(input.headRefName) } : {},
|
|
4722
4767
|
...typeof record$11(input.author)?.login === "string" ? { author: bounded(String(record$11(input.author)?.login)) } : {},
|
|
4723
4768
|
...typeof input.createdAt === "string" && Number.isFinite(Date.parse(input.createdAt)) ? { createdAt: new Date(input.createdAt).toISOString() } : {},
|
|
4724
4769
|
...typeof input.mergedAt === "string" && Number.isFinite(Date.parse(input.mergedAt)) ? { mergedAt: new Date(input.mergedAt).toISOString() } : {},
|
|
@@ -4759,6 +4804,75 @@ var RepositoryStatusService = class {
|
|
|
4759
4804
|
value.catch(() => this.#cache.delete(cwd));
|
|
4760
4805
|
return value;
|
|
4761
4806
|
}
|
|
4807
|
+
/** One pull request by number, as the checkout-shaped status the panels
|
|
4808
|
+
* read: a session that opened a pull request in another repository and
|
|
4809
|
+
* then moved that checkout back to its base branch still has the pull
|
|
4810
|
+
* request. No root, no diff; `cwd` is only where gh runs. Same cache and
|
|
4811
|
+
* stabilisation as `inspect`, keyed off the pull request. */
|
|
4812
|
+
inspectPullRequest(cwd, repository, number) {
|
|
4813
|
+
const key = `gh:${repository}#${number}`;
|
|
4814
|
+
const current = this.#cache.get(key);
|
|
4815
|
+
if (current !== void 0 && current.expiresAt > Date.now()) return current.value;
|
|
4816
|
+
const value = (async () => {
|
|
4817
|
+
const gh = await this.#gh();
|
|
4818
|
+
if (gh === void 0) return {
|
|
4819
|
+
status: "unavailable",
|
|
4820
|
+
cwd
|
|
4821
|
+
};
|
|
4822
|
+
const result = await run(this.#runtime, gh, [
|
|
4823
|
+
"pr",
|
|
4824
|
+
"view",
|
|
4825
|
+
String(number),
|
|
4826
|
+
"--repo",
|
|
4827
|
+
repository,
|
|
4828
|
+
"--json",
|
|
4829
|
+
PULL_REQUEST_FIELDS
|
|
4830
|
+
], cwd, GH_TIMEOUT_MS$1);
|
|
4831
|
+
const raw = result.exitCode === 0 ? JSON.parse(result.stdout) : void 0;
|
|
4832
|
+
const pullRequest = parsePullRequest(raw);
|
|
4833
|
+
if (pullRequest === void 0) return {
|
|
4834
|
+
status: "unavailable",
|
|
4835
|
+
cwd
|
|
4836
|
+
};
|
|
4837
|
+
const counts = record$11(raw);
|
|
4838
|
+
const count = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
4839
|
+
const patch = await run(this.#runtime, gh, [
|
|
4840
|
+
"pr",
|
|
4841
|
+
"diff",
|
|
4842
|
+
String(number),
|
|
4843
|
+
"--repo",
|
|
4844
|
+
repository
|
|
4845
|
+
], cwd, GH_TIMEOUT_MS$1, MAX_RAW_DIFF_BYTES);
|
|
4846
|
+
const packed = patch.exitCode === 0 && !patch.lossy ? packPatchByFile(patch.stdout) : void 0;
|
|
4847
|
+
return {
|
|
4848
|
+
status: "ready",
|
|
4849
|
+
cwd,
|
|
4850
|
+
remote: repository,
|
|
4851
|
+
...pullRequest.headBranch === void 0 ? {} : { branch: pullRequest.headBranch },
|
|
4852
|
+
detached: false,
|
|
4853
|
+
worktree: false,
|
|
4854
|
+
dirty: false,
|
|
4855
|
+
pullRequest,
|
|
4856
|
+
pullRequestOnly: true,
|
|
4857
|
+
diff: {
|
|
4858
|
+
additions: count(counts?.additions),
|
|
4859
|
+
deletions: count(counts?.deletions),
|
|
4860
|
+
files: count(counts?.changedFiles),
|
|
4861
|
+
...packed === void 0 ? {} : { patch: packed.patch },
|
|
4862
|
+
...packed === void 0 || packed.elided.length === 0 ? {} : { elided: packed.elided },
|
|
4863
|
+
truncated: packed === void 0
|
|
4864
|
+
}
|
|
4865
|
+
};
|
|
4866
|
+
})().then((next) => this.#stabilize(key, next), () => ({
|
|
4867
|
+
status: "unavailable",
|
|
4868
|
+
cwd
|
|
4869
|
+
}));
|
|
4870
|
+
this.#cache.set(key, {
|
|
4871
|
+
expiresAt: Date.now() + this.#cacheTtlMs * PULL_REQUEST_TTL_FACTOR,
|
|
4872
|
+
value
|
|
4873
|
+
});
|
|
4874
|
+
return value;
|
|
4875
|
+
}
|
|
4762
4876
|
invalidate(cwd) {
|
|
4763
4877
|
this.#cache.delete(cwd);
|
|
4764
4878
|
this.#lastReady.delete(cwd);
|
|
@@ -4956,19 +5070,20 @@ var RepositoryStatusService = class {
|
|
|
4956
5070
|
"--unified=3",
|
|
4957
5071
|
base,
|
|
4958
5072
|
"--"
|
|
4959
|
-
], cwd, GIT_TIMEOUT_MS$3,
|
|
5073
|
+
], cwd, GIT_TIMEOUT_MS$3, MAX_RAW_DIFF_BYTES);
|
|
4960
5074
|
if (patch.exitCode !== 0) return {
|
|
4961
5075
|
...summary,
|
|
4962
5076
|
truncated: true
|
|
4963
5077
|
};
|
|
4964
5078
|
const untracked = await this.#untrackedDiff(cwd, git, signal);
|
|
4965
|
-
const
|
|
5079
|
+
const packed = packPatchByFile(`${patch.stdout}${untracked.patch}`);
|
|
4966
5080
|
return {
|
|
4967
5081
|
additions: summary.additions + untracked.additions,
|
|
4968
5082
|
deletions: summary.deletions,
|
|
4969
5083
|
files: summary.files + untracked.files,
|
|
4970
|
-
...patch.lossy ? {} : { patch:
|
|
4971
|
-
|
|
5084
|
+
...patch.lossy ? {} : { patch: packed.patch },
|
|
5085
|
+
...packed.elided.length === 0 ? {} : { elided: packed.elided },
|
|
5086
|
+
truncated: patch.lossy || untracked.truncated
|
|
4972
5087
|
};
|
|
4973
5088
|
} catch {
|
|
4974
5089
|
return;
|
|
@@ -5037,7 +5152,7 @@ var RepositoryStatusService = class {
|
|
|
5037
5152
|
"--repo",
|
|
5038
5153
|
repository,
|
|
5039
5154
|
"--json",
|
|
5040
|
-
|
|
5155
|
+
PULL_REQUEST_FIELDS
|
|
5041
5156
|
], cwd, GH_TIMEOUT_MS$1);
|
|
5042
5157
|
if (result.exitCode !== 0) return void 0;
|
|
5043
5158
|
return parsePullRequest(JSON.parse(result.stdout));
|
|
@@ -5132,7 +5247,7 @@ async function summarizeBranchSlug(executablePath, intent, factory = query) {
|
|
|
5132
5247
|
}
|
|
5133
5248
|
//#endregion
|
|
5134
5249
|
//#region src/repository-setup.ts
|
|
5135
|
-
const MAX_OUTPUT_BYTES$
|
|
5250
|
+
const MAX_OUTPUT_BYTES$2 = 131072;
|
|
5136
5251
|
const GIT_TIMEOUT_MS$2 = 1e4;
|
|
5137
5252
|
const GIT_FETCH_TIMEOUT_MS = 6e4;
|
|
5138
5253
|
const MAX_PATH_CHARS$2 = 4096;
|
|
@@ -5295,9 +5410,16 @@ var RepositorySetupService = class {
|
|
|
5295
5410
|
/** Tear down a merged branch: remove a plugin worktree (and its lease and
|
|
5296
5411
|
* branch), or switch a plain checkout back to the base branch and delete
|
|
5297
5412
|
* the merged branch. Refuses dirty trees. */
|
|
5298
|
-
|
|
5413
|
+
/** `branch` names the merged branch when the checkout is no longer on it:
|
|
5414
|
+
* a session that opened a pull request in another clone switched that
|
|
5415
|
+
* clone back to base itself, and only the local branch is left to delete.
|
|
5416
|
+
* `requirePushed` is for a branch no pull request vouches for: it is only
|
|
5417
|
+
* removed once every commit on it is reachable from some remote, so a
|
|
5418
|
+
* worktree that never got as far as a pull request cannot take work with it. */
|
|
5419
|
+
async cleanupMerged(pathValue, baseBranch, branch, requirePushed = false) {
|
|
5299
5420
|
const path = safePath(pathValue);
|
|
5300
|
-
const base = safeBranch(baseBranch);
|
|
5421
|
+
const base = baseBranch === void 0 ? void 0 : safeBranch(baseBranch);
|
|
5422
|
+
const named = branch === void 0 ? void 0 : safeBranch(branch);
|
|
5301
5423
|
const git = await this.#git();
|
|
5302
5424
|
const status = await this.#run(git, [
|
|
5303
5425
|
"status",
|
|
@@ -5307,36 +5429,56 @@ var RepositorySetupService = class {
|
|
|
5307
5429
|
if (status.exitCode !== 0 || status.lossy) throw new RepositorySetupError("repository-unavailable", "The repository state is unavailable.");
|
|
5308
5430
|
if (status.stdout.trim().length > 0) throw new RepositorySetupError("dirty-workspace", "Commit or stash workspace changes before cleaning up.");
|
|
5309
5431
|
const lease = (await this.#readLeases()).find((item) => comparablePath(item.path) === comparablePath(path));
|
|
5310
|
-
if (lease !== void 0)
|
|
5311
|
-
if (
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5316
|
-
|
|
5317
|
-
|
|
5318
|
-
"
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
|
|
5330
|
-
|
|
5432
|
+
if (lease !== void 0) {
|
|
5433
|
+
if (requirePushed) await this.#requirePushed(git, lease.root, lease.branch);
|
|
5434
|
+
return this.#serialize(async () => {
|
|
5435
|
+
if ((await this.#run(git, [
|
|
5436
|
+
"worktree",
|
|
5437
|
+
"remove",
|
|
5438
|
+
"--",
|
|
5439
|
+
lease.path
|
|
5440
|
+
], lease.root)).exitCode !== 0) throw new RepositorySetupError("worktree-remove-failed", "Git could not remove the worktree.");
|
|
5441
|
+
await this.#run(git, [
|
|
5442
|
+
"branch",
|
|
5443
|
+
"-D",
|
|
5444
|
+
"--",
|
|
5445
|
+
lease.branch
|
|
5446
|
+
], lease.root).catch(() => void 0);
|
|
5447
|
+
const current = await this.#readLeases();
|
|
5448
|
+
await this.#writeLeases(current.filter((item) => item.id !== lease.id));
|
|
5449
|
+
return {
|
|
5450
|
+
mode: "worktree",
|
|
5451
|
+
root: lease.root,
|
|
5452
|
+
branch: lease.branch
|
|
5453
|
+
};
|
|
5454
|
+
});
|
|
5455
|
+
}
|
|
5331
5456
|
const root = await this.#repositoryRoot(git, path);
|
|
5457
|
+
if (base === void 0) throw new RepositorySetupError("nothing-to-clean", "A plain checkout needs the base branch to return to.");
|
|
5332
5458
|
const head = await this.#run(git, [
|
|
5333
5459
|
"symbolic-ref",
|
|
5334
5460
|
"--quiet",
|
|
5335
5461
|
"--short",
|
|
5336
5462
|
"HEAD"
|
|
5337
5463
|
], root);
|
|
5338
|
-
const
|
|
5339
|
-
if (
|
|
5464
|
+
const current = head.exitCode === 0 ? head.stdout.trim() : "";
|
|
5465
|
+
if (current.length === 0 || current === base) {
|
|
5466
|
+
if (named === void 0 || named === base) throw new RepositorySetupError("nothing-to-clean", "The checkout is already on the base branch.");
|
|
5467
|
+
if (requirePushed) await this.#requirePushed(git, root, named);
|
|
5468
|
+
await this.#run(git, [
|
|
5469
|
+
"branch",
|
|
5470
|
+
"-D",
|
|
5471
|
+
"--",
|
|
5472
|
+
named
|
|
5473
|
+
], root).catch(() => void 0);
|
|
5474
|
+
await this.#run(git, ["pull", "--ff-only"], root, GIT_FETCH_TIMEOUT_MS).catch(() => void 0);
|
|
5475
|
+
return {
|
|
5476
|
+
mode: "checkout",
|
|
5477
|
+
root,
|
|
5478
|
+
branch: named
|
|
5479
|
+
};
|
|
5480
|
+
}
|
|
5481
|
+
if (requirePushed) await this.#requirePushed(git, root, current);
|
|
5340
5482
|
if ((await this.#run(git, [
|
|
5341
5483
|
"switch",
|
|
5342
5484
|
"--",
|
|
@@ -5346,13 +5488,13 @@ var RepositorySetupService = class {
|
|
|
5346
5488
|
"branch",
|
|
5347
5489
|
"-D",
|
|
5348
5490
|
"--",
|
|
5349
|
-
|
|
5491
|
+
current
|
|
5350
5492
|
], root).catch(() => void 0);
|
|
5351
5493
|
await this.#run(git, ["pull", "--ff-only"], root, GIT_FETCH_TIMEOUT_MS).catch(() => void 0);
|
|
5352
5494
|
return {
|
|
5353
5495
|
mode: "checkout",
|
|
5354
5496
|
root,
|
|
5355
|
-
branch
|
|
5497
|
+
branch: current
|
|
5356
5498
|
};
|
|
5357
5499
|
}
|
|
5358
5500
|
bindLease(leaseId, sessionId) {
|
|
@@ -5629,6 +5771,17 @@ var RepositorySetupService = class {
|
|
|
5629
5771
|
}
|
|
5630
5772
|
return `${prefix}/${slug(baseBranch, "branch")}-${stamp}-${suffix}`;
|
|
5631
5773
|
}
|
|
5774
|
+
/** Refuse to delete a branch holding commits no remote has. */
|
|
5775
|
+
async #requirePushed(git, root, branch) {
|
|
5776
|
+
const count = await this.#run(git, [
|
|
5777
|
+
"rev-list",
|
|
5778
|
+
"--count",
|
|
5779
|
+
branch,
|
|
5780
|
+
"--not",
|
|
5781
|
+
"--remotes"
|
|
5782
|
+
], root);
|
|
5783
|
+
if (count.exitCode !== 0 || Number(count.stdout.trim()) > 0) throw new RepositorySetupError("unpushed-commits", "The branch has commits that are not on any remote; push or discard them first.");
|
|
5784
|
+
}
|
|
5632
5785
|
async #repositoryRoot(git, cwd) {
|
|
5633
5786
|
const result = await this.#run(git, [
|
|
5634
5787
|
"rev-parse",
|
|
@@ -5652,8 +5805,8 @@ var RepositorySetupService = class {
|
|
|
5652
5805
|
cwd,
|
|
5653
5806
|
stdio: {
|
|
5654
5807
|
stdin: "ignore",
|
|
5655
|
-
stdout: { maxBytes: MAX_OUTPUT_BYTES$
|
|
5656
|
-
stderr: { maxBytes: MAX_OUTPUT_BYTES$
|
|
5808
|
+
stdout: { maxBytes: MAX_OUTPUT_BYTES$2 },
|
|
5809
|
+
stderr: { maxBytes: MAX_OUTPUT_BYTES$2 }
|
|
5657
5810
|
},
|
|
5658
5811
|
graceMs: 1e3,
|
|
5659
5812
|
signal: AbortSignal.timeout(timeoutMs),
|
|
@@ -5694,7 +5847,7 @@ var RepositorySetupService = class {
|
|
|
5694
5847
|
};
|
|
5695
5848
|
//#endregion
|
|
5696
5849
|
//#region src/repository-actions.ts
|
|
5697
|
-
const MAX_OUTPUT_BYTES$
|
|
5850
|
+
const MAX_OUTPUT_BYTES$1 = 262144;
|
|
5698
5851
|
const MAX_PATCH_CHARS = 65536;
|
|
5699
5852
|
const MAX_MESSAGE_CHARS = 512;
|
|
5700
5853
|
const MAX_PR_TEXT_CHARS = 8192;
|
|
@@ -5867,6 +6020,7 @@ var RepositoryActionService = class {
|
|
|
5867
6020
|
const merged = await this.#run(gh, [
|
|
5868
6021
|
"pr",
|
|
5869
6022
|
"merge",
|
|
6023
|
+
...request.pullNumber === void 0 ? [] : [String(request.pullNumber)],
|
|
5870
6024
|
`--${method}`,
|
|
5871
6025
|
...request.admin === true ? ["--admin"] : []
|
|
5872
6026
|
], before.root, REMOTE_TIMEOUT_MS);
|
|
@@ -6120,7 +6274,7 @@ var RepositoryActionService = class {
|
|
|
6120
6274
|
"--",
|
|
6121
6275
|
":(exclude)WARP.md",
|
|
6122
6276
|
":(exclude)**/WARP.md"
|
|
6123
|
-
], root, GIT_TIMEOUT_MS$1, MAX_OUTPUT_BYTES$
|
|
6277
|
+
], root, GIT_TIMEOUT_MS$1, MAX_OUTPUT_BYTES$1),
|
|
6124
6278
|
this.#run(git, [
|
|
6125
6279
|
...funcname,
|
|
6126
6280
|
"diff",
|
|
@@ -6130,7 +6284,7 @@ var RepositoryActionService = class {
|
|
|
6130
6284
|
"--",
|
|
6131
6285
|
":(exclude)WARP.md",
|
|
6132
6286
|
":(exclude)**/WARP.md"
|
|
6133
|
-
], root, GIT_TIMEOUT_MS$1, MAX_OUTPUT_BYTES$
|
|
6287
|
+
], root, GIT_TIMEOUT_MS$1, MAX_OUTPUT_BYTES$1)
|
|
6134
6288
|
]);
|
|
6135
6289
|
if (branchResult.exitCode !== 0) throw new RepositoryActionError("detached-head", "A detached HEAD cannot be committed from this panel.");
|
|
6136
6290
|
if (headResult.exitCode !== 0 || statusResult.exitCode !== 0 || statusResult.lossy) throw new RepositoryActionError("repository-unavailable", "Repository state is unavailable.");
|
|
@@ -6224,14 +6378,14 @@ var RepositoryActionService = class {
|
|
|
6224
6378
|
if (result.exitCode !== 0 || result.lossy) throw new RepositoryActionError(code, message);
|
|
6225
6379
|
return result;
|
|
6226
6380
|
}
|
|
6227
|
-
#run(executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$
|
|
6381
|
+
#run(executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$1) {
|
|
6228
6382
|
return collect$1(this.#runtime.spawn({
|
|
6229
6383
|
argv: [executable, ...args],
|
|
6230
6384
|
cwd,
|
|
6231
6385
|
stdio: {
|
|
6232
6386
|
stdin: "ignore",
|
|
6233
6387
|
stdout: { maxBytes },
|
|
6234
|
-
stderr: { maxBytes: MAX_OUTPUT_BYTES$
|
|
6388
|
+
stderr: { maxBytes: MAX_OUTPUT_BYTES$1 }
|
|
6235
6389
|
},
|
|
6236
6390
|
graceMs: 1e3,
|
|
6237
6391
|
signal: AbortSignal.timeout(timeoutMs),
|
|
@@ -6342,8 +6496,10 @@ function registerRepositorySetupRoute(ctx, service, sweep, cleaned) {
|
|
|
6342
6496
|
if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
6343
6497
|
const input = await readJson$6(io);
|
|
6344
6498
|
const path = string$2(input, "path");
|
|
6345
|
-
const
|
|
6346
|
-
|
|
6499
|
+
const branch = typeof input.branch === "string" && input.branch.length > 0 ? input.branch : void 0;
|
|
6500
|
+
const baseBranch = typeof input.baseBranch === "string" && input.baseBranch.length > 0 ? input.baseBranch : void 0;
|
|
6501
|
+
const result = await service.cleanupMerged(path, baseBranch, branch, input.requirePushed === true);
|
|
6502
|
+
cleaned?.(path, branch);
|
|
6347
6503
|
return json(res, 200, result);
|
|
6348
6504
|
}
|
|
6349
6505
|
if (pathname === `/plugins/dsh-claude/repository/setup/sweep`) {
|
|
@@ -6372,7 +6528,7 @@ function registerRepositorySetupRoute(ctx, service, sweep, cleaned) {
|
|
|
6372
6528
|
//#endregion
|
|
6373
6529
|
//#region src/repository-action-routes.ts
|
|
6374
6530
|
const MAX_BODY_BYTES$6 = 16384;
|
|
6375
|
-
const MAX_SESSION_ID_CHARS$
|
|
6531
|
+
const MAX_SESSION_ID_CHARS$5 = 1024;
|
|
6376
6532
|
const MAX_ROOT_CHARS = 4096;
|
|
6377
6533
|
const ACTIONS = /* @__PURE__ */ new Set([
|
|
6378
6534
|
"commit",
|
|
@@ -6409,7 +6565,7 @@ async function readJson$5(io) {
|
|
|
6409
6565
|
}
|
|
6410
6566
|
function sessionId$1(url) {
|
|
6411
6567
|
const value = url.searchParams.get("sessionId");
|
|
6412
|
-
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$
|
|
6568
|
+
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$5) throw new RepositoryActionError("invalid-session", "The session is invalid.");
|
|
6413
6569
|
return value;
|
|
6414
6570
|
}
|
|
6415
6571
|
function requestedRoot(url) {
|
|
@@ -6447,6 +6603,9 @@ function actionRequest(input) {
|
|
|
6447
6603
|
...input.admin === void 0 ? {} : typeof input.admin === "boolean" ? { admin: input.admin } : (() => {
|
|
6448
6604
|
throw new RepositoryActionError("invalid-request", "The admin field must be a boolean.");
|
|
6449
6605
|
})(),
|
|
6606
|
+
...input.pullNumber === void 0 ? {} : Number.isSafeInteger(input.pullNumber) && input.pullNumber > 0 ? { pullNumber: input.pullNumber } : (() => {
|
|
6607
|
+
throw new RepositoryActionError("invalid-request", "The pullNumber field must be a positive integer.");
|
|
6608
|
+
})(),
|
|
6450
6609
|
...input.mergeMethod === void 0 ? {} : input.mergeMethod === "merge" || input.mergeMethod === "squash" || input.mergeMethod === "rebase" ? { mergeMethod: input.mergeMethod } : (() => {
|
|
6451
6610
|
throw new RepositoryActionError("invalid-request", "The mergeMethod field is invalid.");
|
|
6452
6611
|
})()
|
|
@@ -6528,187 +6687,6 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
|
|
|
6528
6687
|
});
|
|
6529
6688
|
}
|
|
6530
6689
|
//#endregion
|
|
6531
|
-
//#region src/editor-open.ts
|
|
6532
|
-
const MAX_OUTPUT_BYTES$1 = 8192;
|
|
6533
|
-
/** Long enough for a launcher shim to fail loudly, short enough that the
|
|
6534
|
-
* request returns while the IDE is still booting. On Windows and Linux the
|
|
6535
|
-
* IDE binary IS the launched process, so still running past this is success. */
|
|
6536
|
-
const SETTLE_MS = 1500;
|
|
6537
|
-
const EDITOR_IDS = /* @__PURE__ */ new Set(["cursor", "idea"]);
|
|
6538
|
-
/** Launch commands tried in order, first success wins. macOS keeps `open -a`
|
|
6539
|
-
* behind the CLI shim because both shims are opt-in installs there, while
|
|
6540
|
-
* `open -a` finds the bundle wherever Toolbox or the DMG dropped it. */
|
|
6541
|
-
const LAUNCHERS = {
|
|
6542
|
-
cursor: {
|
|
6543
|
-
darwin: [["cursor"], [
|
|
6544
|
-
"open",
|
|
6545
|
-
"-a",
|
|
6546
|
-
"Cursor"
|
|
6547
|
-
]],
|
|
6548
|
-
win32: [["cursor"]],
|
|
6549
|
-
linux: [["cursor"]]
|
|
6550
|
-
},
|
|
6551
|
-
idea: {
|
|
6552
|
-
darwin: [
|
|
6553
|
-
["idea"],
|
|
6554
|
-
[
|
|
6555
|
-
"open",
|
|
6556
|
-
"-a",
|
|
6557
|
-
"IntelliJ IDEA"
|
|
6558
|
-
],
|
|
6559
|
-
[
|
|
6560
|
-
"open",
|
|
6561
|
-
"-a",
|
|
6562
|
-
"IntelliJ IDEA CE"
|
|
6563
|
-
]
|
|
6564
|
-
],
|
|
6565
|
-
win32: [["idea"], ["idea64.exe"]],
|
|
6566
|
-
linux: [["idea"], ["idea.sh"]]
|
|
6567
|
-
}
|
|
6568
|
-
};
|
|
6569
|
-
/** cmd.exe re-interprets these, and a mis-parsed path opens the wrong project
|
|
6570
|
-
* rather than failing. Refuse instead of guessing. */
|
|
6571
|
-
const WINDOWS_UNSAFE = /["&|<>^%]/u;
|
|
6572
|
-
var EditorOpenError = class extends Error {
|
|
6573
|
-
code;
|
|
6574
|
-
constructor(code, message) {
|
|
6575
|
-
super(message);
|
|
6576
|
-
this.name = "EditorOpenError";
|
|
6577
|
-
this.code = code;
|
|
6578
|
-
}
|
|
6579
|
-
};
|
|
6580
|
-
/** Open a session's working directory in a desktop editor. */
|
|
6581
|
-
var EditorOpenService = class {
|
|
6582
|
-
#runtime;
|
|
6583
|
-
#platform;
|
|
6584
|
-
#settleMs;
|
|
6585
|
-
constructor(runtime, platform = process.platform, settleMs = SETTLE_MS) {
|
|
6586
|
-
this.#runtime = runtime;
|
|
6587
|
-
this.#platform = platform;
|
|
6588
|
-
this.#settleMs = settleMs;
|
|
6589
|
-
}
|
|
6590
|
-
async open(cwd, editor) {
|
|
6591
|
-
if (this.#platform === "win32" && WINDOWS_UNSAFE.test(cwd)) throw new EditorOpenError("unsupported-path", "The project path cannot be opened through the Windows shell.");
|
|
6592
|
-
const candidates = LAUNCHERS[editor][this.#platform] ?? LAUNCHERS[editor].linux ?? [];
|
|
6593
|
-
let found = false;
|
|
6594
|
-
for (const candidate of candidates) {
|
|
6595
|
-
const argv = await this.#argv(candidate, cwd);
|
|
6596
|
-
if (argv === void 0) continue;
|
|
6597
|
-
found = true;
|
|
6598
|
-
if (await this.#launch(argv, cwd)) return;
|
|
6599
|
-
}
|
|
6600
|
-
throw found ? new EditorOpenError("launch-failed", "The editor refused to open the project.") : new EditorOpenError("editor-unavailable", "The editor was not found on PATH.");
|
|
6601
|
-
}
|
|
6602
|
-
async #argv(candidate, cwd) {
|
|
6603
|
-
const [program, ...rest] = candidate;
|
|
6604
|
-
if (program === void 0) return void 0;
|
|
6605
|
-
if (this.#platform === "win32") return [
|
|
6606
|
-
"cmd.exe",
|
|
6607
|
-
"/d",
|
|
6608
|
-
"/s",
|
|
6609
|
-
"/c",
|
|
6610
|
-
program,
|
|
6611
|
-
...rest,
|
|
6612
|
-
cwd
|
|
6613
|
-
];
|
|
6614
|
-
try {
|
|
6615
|
-
return [
|
|
6616
|
-
await this.#runtime.resolveExecutable(program),
|
|
6617
|
-
...rest,
|
|
6618
|
-
cwd
|
|
6619
|
-
];
|
|
6620
|
-
} catch {
|
|
6621
|
-
return;
|
|
6622
|
-
}
|
|
6623
|
-
}
|
|
6624
|
-
/** True once the editor is launched: either the shim exited cleanly or the
|
|
6625
|
-
* process is still alive past the settle window. */
|
|
6626
|
-
async #launch(argv, cwd) {
|
|
6627
|
-
let handle;
|
|
6628
|
-
try {
|
|
6629
|
-
handle = this.#runtime.spawn({
|
|
6630
|
-
argv,
|
|
6631
|
-
cwd,
|
|
6632
|
-
stdio: {
|
|
6633
|
-
stdin: "ignore",
|
|
6634
|
-
stdout: { maxBytes: MAX_OUTPUT_BYTES$1 },
|
|
6635
|
-
stderr: { maxBytes: MAX_OUTPUT_BYTES$1 }
|
|
6636
|
-
},
|
|
6637
|
-
graceMs: 1e3,
|
|
6638
|
-
env: {}
|
|
6639
|
-
});
|
|
6640
|
-
} catch {
|
|
6641
|
-
return false;
|
|
6642
|
-
}
|
|
6643
|
-
return await Promise.race([handle.done.then((outcome) => outcome.exitCode === 0), new Promise((resolve) => {
|
|
6644
|
-
setTimeout(() => resolve(true), this.#settleMs).unref?.();
|
|
6645
|
-
})]);
|
|
6646
|
-
}
|
|
6647
|
-
};
|
|
6648
|
-
//#endregion
|
|
6649
|
-
//#region src/editor-open-routes.ts
|
|
6650
|
-
const MAX_SESSION_ID_CHARS$5 = 1024;
|
|
6651
|
-
/** Open the session's working directory in a desktop editor. Query-only: the
|
|
6652
|
-
* request carries two enum-ish values, so there is no body to parse. */
|
|
6653
|
-
function registerEditorOpenRoute(ctx, service, cwdForSession) {
|
|
6654
|
-
registerPluginRoute(ctx, {
|
|
6655
|
-
mode: "unary",
|
|
6656
|
-
kind: "exact",
|
|
6657
|
-
path: CLAUDE_EDITOR_OPEN_PATH,
|
|
6658
|
-
methods: ["POST"],
|
|
6659
|
-
budget: "fast",
|
|
6660
|
-
handler: async (io) => {
|
|
6661
|
-
const params = io.url.searchParams;
|
|
6662
|
-
const id = params.get("sessionId");
|
|
6663
|
-
const editor = params.get("editor");
|
|
6664
|
-
if (id === null || id.length === 0 || id.length > MAX_SESSION_ID_CHARS$5) return {
|
|
6665
|
-
status: 400,
|
|
6666
|
-
value: {
|
|
6667
|
-
error: "invalid-session",
|
|
6668
|
-
message: "The session is invalid."
|
|
6669
|
-
}
|
|
6670
|
-
};
|
|
6671
|
-
if (editor === null || !EDITOR_IDS.has(editor)) return {
|
|
6672
|
-
status: 400,
|
|
6673
|
-
value: {
|
|
6674
|
-
error: "invalid-editor",
|
|
6675
|
-
message: "The editor is invalid."
|
|
6676
|
-
}
|
|
6677
|
-
};
|
|
6678
|
-
const cwd = cwdForSession(id);
|
|
6679
|
-
if (cwd === void 0) return {
|
|
6680
|
-
status: 409,
|
|
6681
|
-
value: {
|
|
6682
|
-
error: "session-unavailable",
|
|
6683
|
-
message: "The Claude session is unavailable."
|
|
6684
|
-
}
|
|
6685
|
-
};
|
|
6686
|
-
try {
|
|
6687
|
-
await service.open(cwd, editor);
|
|
6688
|
-
return {
|
|
6689
|
-
status: 200,
|
|
6690
|
-
value: { opened: true }
|
|
6691
|
-
};
|
|
6692
|
-
} catch (error) {
|
|
6693
|
-
if (error instanceof EditorOpenError) return {
|
|
6694
|
-
status: 409,
|
|
6695
|
-
value: {
|
|
6696
|
-
error: error.code,
|
|
6697
|
-
message: error.message
|
|
6698
|
-
}
|
|
6699
|
-
};
|
|
6700
|
-
return {
|
|
6701
|
-
status: 500,
|
|
6702
|
-
value: {
|
|
6703
|
-
error: "editor-open-unavailable",
|
|
6704
|
-
message: "The editor could not be launched."
|
|
6705
|
-
}
|
|
6706
|
-
};
|
|
6707
|
-
}
|
|
6708
|
-
}
|
|
6709
|
-
});
|
|
6710
|
-
}
|
|
6711
|
-
//#endregion
|
|
6712
6690
|
//#region src/prompts.ts
|
|
6713
6691
|
const MAX_PROMPT_FILES = 256;
|
|
6714
6692
|
const MAX_PROMPT_BYTES = 16384;
|
|
@@ -8660,30 +8638,121 @@ const FILE_TOOLS = /* @__PURE__ */ new Set([
|
|
|
8660
8638
|
"NotebookEdit"
|
|
8661
8639
|
]);
|
|
8662
8640
|
/** The activity detail is a redacted JSON string that may be cut short, so
|
|
8663
|
-
*
|
|
8641
|
+
* these match one key each rather than parsing the document. */
|
|
8664
8642
|
const PATH_KEY = /"(?:file_path|notebook_path)"\s*:\s*"((?:[^"\\]|\\.)*)"/u;
|
|
8665
|
-
|
|
8643
|
+
const COMMAND_KEY = /"command"\s*:\s*"((?:[^"\\]|\\.)*)"/u;
|
|
8644
|
+
/** An absolute path token in shell text: not the tail of a URL, a relative
|
|
8645
|
+
* path, a `$VAR` or a `NAME=` assignment, and not reaching into quotes or
|
|
8646
|
+
* shell punctuation. */
|
|
8647
|
+
const PATH_TOKEN = String.raw`\/[\w.@+~-]+(?:\/[\w.@+~-]+)*`;
|
|
8648
|
+
/** Where a command goes to work or writes: the shell forms Claude actually
|
|
8649
|
+
* uses under full access. Reading a path (grep, cat, ls) is not touching it. */
|
|
8650
|
+
const WRITE_CONTEXTS = [
|
|
8651
|
+
new RegExp(String.raw`(?:^|[;&|(]\s*)(?:cd|pushd)\s+(${PATH_TOKEN})`, "gmu"),
|
|
8652
|
+
new RegExp(String.raw`\bgit\s+-C\s+(${PATH_TOKEN})`, "gu"),
|
|
8653
|
+
new RegExp(String.raw`\bgit\s+worktree\s+add\s+(?:-\S+\s+)*(${PATH_TOKEN})`, "gu"),
|
|
8654
|
+
new RegExp(String.raw`>{1,2}\s*(${PATH_TOKEN})`, "gu"),
|
|
8655
|
+
new RegExp(String.raw`\btee\s+(?:-\S+\s+)*(${PATH_TOKEN})`, "gu"),
|
|
8656
|
+
new RegExp(String.raw`\bmkdir\s+(?:-\S+\s+)*(${PATH_TOKEN})`, "gu"),
|
|
8657
|
+
new RegExp(String.raw`\bsed\s+-i\S*\s+(?:(?:'[^']*'|"[^"]*"|\S+)\s+)+?(${PATH_TOKEN})`, "gu"),
|
|
8658
|
+
new RegExp(String.raw`\b(?:cp|mv|install)\s+(?:\S+\s+)+?(${PATH_TOKEN})`, "gu"),
|
|
8659
|
+
new RegExp(String.raw`\bopen\(\s*['"](${PATH_TOKEN})['"]\s*,\s*['"][wa]`, "gu")
|
|
8660
|
+
];
|
|
8661
|
+
/** A pathological command (a generated file list) must not turn into a
|
|
8662
|
+
* hundred git probes. */
|
|
8663
|
+
const MAX_PATHS_PER_COMMAND = 50;
|
|
8664
|
+
const PULL_REQUEST_URL = /https:\/\/github\.com\/([\w.-]+\/[\w.-]+)\/pull\/(\d{1,9})(?![\d])/gu;
|
|
8665
|
+
const MAX_PULL_REQUESTS = 8;
|
|
8666
|
+
function unescaped(escaped) {
|
|
8667
|
+
try {
|
|
8668
|
+
return JSON.parse(`"${escaped}"`);
|
|
8669
|
+
} catch {
|
|
8670
|
+
return;
|
|
8671
|
+
}
|
|
8672
|
+
}
|
|
8673
|
+
/** Absolute paths Claude worked in or wrote to, first-seen order: the file
|
|
8674
|
+
* tools' own argument, plus the paths a Bash command changes into, sets a
|
|
8675
|
+
* worktree up at, or writes -- under full access Claude edits through
|
|
8676
|
+
* heredocs and sed. Paths outside any repository are dropped downstream. */
|
|
8666
8677
|
function touchedFilePaths(activities) {
|
|
8667
8678
|
const paths = /* @__PURE__ */ new Set();
|
|
8668
8679
|
for (const activity of activities) {
|
|
8669
|
-
if (activity.kind !== "tool-call" && activity.kind !== "subagent" || activity.toolName === void 0 ||
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8680
|
+
if (activity.kind !== "tool-call" && activity.kind !== "subagent" || activity.toolName === void 0 || activity.detail === void 0) continue;
|
|
8681
|
+
if (FILE_TOOLS.has(activity.toolName)) {
|
|
8682
|
+
const escaped = PATH_KEY.exec(activity.detail)?.[1];
|
|
8683
|
+
const path = escaped === void 0 ? void 0 : unescaped(escaped);
|
|
8684
|
+
if (path !== void 0 && isAbsolute(path)) paths.add(path);
|
|
8685
|
+
} else if (activity.toolName === "Bash") {
|
|
8686
|
+
const escaped = COMMAND_KEY.exec(activity.detail)?.[1];
|
|
8687
|
+
const command = escaped === void 0 ? void 0 : unescaped(escaped);
|
|
8688
|
+
if (command === void 0) continue;
|
|
8689
|
+
const found = [];
|
|
8690
|
+
for (const context of WRITE_CONTEXTS) for (const match of command.matchAll(context)) {
|
|
8691
|
+
const path = match[1];
|
|
8692
|
+
if (path !== void 0 && !path.startsWith("/dev/")) found.push({
|
|
8693
|
+
index: match.index + match[0].length - path.length,
|
|
8694
|
+
path
|
|
8695
|
+
});
|
|
8696
|
+
}
|
|
8697
|
+
found.sort((left, right) => left.index - right.index);
|
|
8698
|
+
for (const { path } of found.slice(0, MAX_PATHS_PER_COMMAND)) paths.add(path);
|
|
8699
|
+
}
|
|
8676
8700
|
}
|
|
8677
8701
|
return [...paths];
|
|
8678
8702
|
}
|
|
8703
|
+
const PR_CREATE = /\bgh\s+pr\s+create\b/u;
|
|
8704
|
+
/** The pull requests the session opened: the URL `gh pr create` printed,
|
|
8705
|
+
* read off that call's own result. Other than the session repository's,
|
|
8706
|
+
* once each in first-seen order. A URL merely read, quoted or mentioned
|
|
8707
|
+
* (a fixture, a summary, a `gh pr view`) is not one the session made.
|
|
8708
|
+
* A checkout that has since moved back to its base branch no longer knows
|
|
8709
|
+
* about its pull request; the log still does. */
|
|
8710
|
+
function touchedPullRequests(activities, ownRepository) {
|
|
8711
|
+
const found = /* @__PURE__ */ new Map();
|
|
8712
|
+
const own = ownRepository?.toLowerCase();
|
|
8713
|
+
const creating = /* @__PURE__ */ new Set();
|
|
8714
|
+
for (const activity of activities) {
|
|
8715
|
+
if (activity.toolUseId === void 0) continue;
|
|
8716
|
+
if (activity.kind === "tool-call" || activity.kind === "subagent" && activity.toolName !== void 0) {
|
|
8717
|
+
if (activity.toolName !== "Bash" || activity.detail === void 0) continue;
|
|
8718
|
+
const escaped = COMMAND_KEY.exec(activity.detail)?.[1];
|
|
8719
|
+
const command = escaped === void 0 ? void 0 : unescaped(escaped);
|
|
8720
|
+
if (command !== void 0 && PR_CREATE.test(command)) creating.add(activity.toolUseId);
|
|
8721
|
+
continue;
|
|
8722
|
+
}
|
|
8723
|
+
if (activity.detail === void 0 || !creating.has(activity.toolUseId)) continue;
|
|
8724
|
+
for (const match of activity.detail.matchAll(PULL_REQUEST_URL)) {
|
|
8725
|
+
const repository = match[1]?.toLowerCase();
|
|
8726
|
+
const number = Number(match[2]);
|
|
8727
|
+
if (repository === void 0 || repository === own || !Number.isSafeInteger(number) || number <= 0) continue;
|
|
8728
|
+
const key = `${repository}#${number}`;
|
|
8729
|
+
if (found.has(key)) continue;
|
|
8730
|
+
found.set(key, {
|
|
8731
|
+
repository,
|
|
8732
|
+
number
|
|
8733
|
+
});
|
|
8734
|
+
if (found.size >= MAX_PULL_REQUESTS) return [...found.values()];
|
|
8735
|
+
}
|
|
8736
|
+
}
|
|
8737
|
+
return [...found.values()];
|
|
8738
|
+
}
|
|
8739
|
+
async function isDirectory(path) {
|
|
8740
|
+
try {
|
|
8741
|
+
return (await stat(path)).isDirectory();
|
|
8742
|
+
} catch {
|
|
8743
|
+
return false;
|
|
8744
|
+
}
|
|
8745
|
+
}
|
|
8679
8746
|
/** Repository roots behind the touched paths, minus the session's own, in
|
|
8680
8747
|
* first-seen order. `rootOf` answers undefined outside any repository. */
|
|
8681
|
-
async function touchedRepositoryRoots(paths, sessionRoot, rootOf, max) {
|
|
8748
|
+
async function touchedRepositoryRoots(paths, sessionRoot, rootOf, max, directory = isDirectory) {
|
|
8682
8749
|
const roots = [];
|
|
8683
|
-
const directories = new Set(
|
|
8750
|
+
const directories = /* @__PURE__ */ new Set();
|
|
8751
|
+
for (const path of paths) directories.add(await directory(path) ? path : dirname(path));
|
|
8684
8752
|
for (const directory of directories) {
|
|
8685
8753
|
const root = await rootOf(directory);
|
|
8686
8754
|
if (root === void 0 || root === sessionRoot || roots.includes(root)) continue;
|
|
8755
|
+
if (sessionRoot.startsWith(root.endsWith(sep) ? root : root + sep)) continue;
|
|
8687
8756
|
roots.push(root);
|
|
8688
8757
|
if (roots.length >= max) break;
|
|
8689
8758
|
}
|
|
@@ -8691,13 +8760,52 @@ async function touchedRepositoryRoots(paths, sessionRoot, rootOf, max) {
|
|
|
8691
8760
|
}
|
|
8692
8761
|
/** Whether a linked checkout still has anything to show. A cleaned-up
|
|
8693
8762
|
* worktree is gone, and a checkout cleaned up in place is back on base with
|
|
8694
|
-
* nothing pending -- either way its bar comes down. A
|
|
8695
|
-
*
|
|
8763
|
+
* nothing pending -- either way its bar comes down. A checkout with an open
|
|
8764
|
+
* or merged pull request, or unpushed commits, stays. A clean branch with no
|
|
8765
|
+
* upstream is what a tool checkout looks like and is no evidence on its own. */
|
|
8696
8766
|
function linkedRepositoryShown(status) {
|
|
8697
8767
|
if (status.status !== "ready") return false;
|
|
8698
|
-
return status.dirty === true ||
|
|
8768
|
+
return status.dirty === true || (status.ahead ?? 0) > 0 || status.pullRequest !== void 0;
|
|
8699
8769
|
}
|
|
8700
8770
|
//#endregion
|
|
8771
|
+
//#region src/session-root-ledger.ts
|
|
8772
|
+
/** Which extra checkout roots a session's routes may act on.
|
|
8773
|
+
*
|
|
8774
|
+
* A session's linked repositories are re-derived on every projection sweep,
|
|
8775
|
+
* and any one sweep's git/gh probes can transiently fail -- dropping a
|
|
8776
|
+
* checkout the client is still showing a bar for. Replacing the authorised
|
|
8777
|
+
* set each sweep would then 409 that bar's controls until the next good
|
|
8778
|
+
* sweep, which reads as a control that silently does nothing. A session only
|
|
8779
|
+
* ever gains checkouts it has genuinely written into, so the ledger only
|
|
8780
|
+
* grows within a session's lifetime and never un-vouches a root a later
|
|
8781
|
+
* probe happened to miss. Bounded so a long session cannot grow it forever. */
|
|
8782
|
+
var SessionRootLedger = class {
|
|
8783
|
+
#roots = /* @__PURE__ */ new Map();
|
|
8784
|
+
#max;
|
|
8785
|
+
constructor(max) {
|
|
8786
|
+
this.#max = max;
|
|
8787
|
+
}
|
|
8788
|
+
/** Add the roots this sweep vouched for; existing ones stay. */
|
|
8789
|
+
vouch(sessionId, roots) {
|
|
8790
|
+
let set = this.#roots.get(sessionId);
|
|
8791
|
+
if (set === void 0) {
|
|
8792
|
+
set = /* @__PURE__ */ new Set();
|
|
8793
|
+
this.#roots.set(sessionId, set);
|
|
8794
|
+
}
|
|
8795
|
+
for (const root of roots) {
|
|
8796
|
+
set.delete(root);
|
|
8797
|
+
set.add(root);
|
|
8798
|
+
if (set.size > this.#max) set.delete(set.values().next().value);
|
|
8799
|
+
}
|
|
8800
|
+
}
|
|
8801
|
+
allows(sessionId, root) {
|
|
8802
|
+
return this.#roots.get(sessionId)?.has(root) === true;
|
|
8803
|
+
}
|
|
8804
|
+
forget(sessionId) {
|
|
8805
|
+
this.#roots.delete(sessionId);
|
|
8806
|
+
}
|
|
8807
|
+
};
|
|
8808
|
+
//#endregion
|
|
8701
8809
|
//#region src/update-routes.ts
|
|
8702
8810
|
const PLUGIN_PACKAGE_NAME = "@norman-else/dsh-claude";
|
|
8703
8811
|
const MAX_MANIFEST_BYTES = 262144;
|
|
@@ -8967,6 +9075,54 @@ function registerClaudeUpdateRoutes(ctx, runtime, deps = {}) {
|
|
|
8967
9075
|
});
|
|
8968
9076
|
}
|
|
8969
9077
|
//#endregion
|
|
9078
|
+
//#region src/windows-job-runner.ts
|
|
9079
|
+
const RUN_AS_NODE = "ELECTRON_RUN_AS_NODE";
|
|
9080
|
+
/**
|
|
9081
|
+
* Make the Host's Windows Job runner start as Node when the Host itself is an
|
|
9082
|
+
* Electron utility process.
|
|
9083
|
+
*
|
|
9084
|
+
* Since DSH Desktop 2.0.7 the Host runs in `utilityProcess.fork` and, on
|
|
9085
|
+
* Windows, `subprocess-local` launches every ordinary target through a helper
|
|
9086
|
+
* it spawns as `[process.execPath, runner.js]`. Inside Electron that path is
|
|
9087
|
+
* `DSH Desktop.exe`, and the fork carries no `ELECTRON_RUN_AS_NODE`, so the
|
|
9088
|
+
* helper starts as a second GUI instance, yields to the running one, and exits
|
|
9089
|
+
* 0 without ever reporting -- the Host then fails the spawn with "Windows Job
|
|
9090
|
+
* runner exited with exit code 0 before proving its managed range empty".
|
|
9091
|
+
*
|
|
9092
|
+
* The runner's environment is read from `process.env` synchronously inside
|
|
9093
|
+
* `spawn()`, so the flag is set for exactly that call and removed after. The
|
|
9094
|
+
* target's own environment is built the same way, so the spec tells the Host
|
|
9095
|
+
* to drop the flag there: a target must not inherit a variable that turns any
|
|
9096
|
+
* Electron app it launches into a bare Node process.
|
|
9097
|
+
*/
|
|
9098
|
+
function withElectronNodeRunner(runtime, options = {}) {
|
|
9099
|
+
const platform = options.platform ?? process.platform;
|
|
9100
|
+
const electron = options.electron ?? process.versions.electron !== void 0;
|
|
9101
|
+
if (platform !== "win32" || !electron) return runtime;
|
|
9102
|
+
return new Proxy(runtime, { get(target, property, receiver) {
|
|
9103
|
+
if (property !== "spawn") {
|
|
9104
|
+
const value = Reflect.get(target, property, receiver);
|
|
9105
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
9106
|
+
}
|
|
9107
|
+
return (spec) => {
|
|
9108
|
+
const withoutFlag = {
|
|
9109
|
+
...spec,
|
|
9110
|
+
env: {
|
|
9111
|
+
...spec.env,
|
|
9112
|
+
[RUN_AS_NODE]: void 0
|
|
9113
|
+
}
|
|
9114
|
+
};
|
|
9115
|
+
if (process.env[RUN_AS_NODE] !== void 0) return target.spawn(withoutFlag);
|
|
9116
|
+
process.env[RUN_AS_NODE] = "1";
|
|
9117
|
+
try {
|
|
9118
|
+
return target.spawn(withoutFlag);
|
|
9119
|
+
} finally {
|
|
9120
|
+
delete process.env[RUN_AS_NODE];
|
|
9121
|
+
}
|
|
9122
|
+
};
|
|
9123
|
+
} });
|
|
9124
|
+
}
|
|
9125
|
+
//#endregion
|
|
8970
9126
|
//#region src/plan-usage-routes.ts
|
|
8971
9127
|
const EMPTY = {
|
|
8972
9128
|
available: false,
|
|
@@ -9514,6 +9670,7 @@ async function installManagedPresetCompatibility(logger, install = ensureManaged
|
|
|
9514
9670
|
}
|
|
9515
9671
|
}
|
|
9516
9672
|
async function apply(ctx, config) {
|
|
9673
|
+
const subprocess = withElectronNodeRunner(ctx.subprocess);
|
|
9517
9674
|
await installManagedPresetCompatibility(ctx.logger);
|
|
9518
9675
|
const defaultLimits = {
|
|
9519
9676
|
idleTimeoutMs: config.idleTimeoutMs ?? 18e5,
|
|
@@ -9531,15 +9688,15 @@ async function apply(ctx, config) {
|
|
|
9531
9688
|
};
|
|
9532
9689
|
await applySettingsOverrides();
|
|
9533
9690
|
const sidecar = new ClaudeSidecarRepository();
|
|
9534
|
-
const repositoryStatus = new RepositoryStatusService(
|
|
9535
|
-
const repositorySetup = new RepositorySetupService(
|
|
9691
|
+
const repositoryStatus = new RepositoryStatusService(subprocess);
|
|
9692
|
+
const repositorySetup = new RepositorySetupService(subprocess, {
|
|
9536
9693
|
branchPrefix: () => readWorktreeBranchPrefix(),
|
|
9537
9694
|
summarizeBranch: (intent) => summarizeBranchSlug(supervisorConfig.executablePath, intent)
|
|
9538
9695
|
});
|
|
9539
9696
|
const reviewComments = new ReviewCommentStore();
|
|
9540
9697
|
const commandCatalogs = /* @__PURE__ */ new Map();
|
|
9541
9698
|
const supervisor = new ClaudeSupervisor({
|
|
9542
|
-
runtime:
|
|
9699
|
+
runtime: subprocess,
|
|
9543
9700
|
approval: ctx.approval,
|
|
9544
9701
|
userQuestions: ctx.userQuestions,
|
|
9545
9702
|
config: supervisorConfig,
|
|
@@ -9548,7 +9705,7 @@ async function apply(ctx, config) {
|
|
|
9548
9705
|
});
|
|
9549
9706
|
let resolutionError;
|
|
9550
9707
|
try {
|
|
9551
|
-
supervisorConfig.executablePath = (await resolveClaudeExecutable(
|
|
9708
|
+
supervisorConfig.executablePath = (await resolveClaudeExecutable(subprocess, config.executablePath === void 0 || config.executablePath.length === 0 ? void 0 : config.executablePath)).path;
|
|
9552
9709
|
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)));
|
|
9553
9710
|
ctx.effect(() => {
|
|
9554
9711
|
const mounted = /* @__PURE__ */ new Map();
|
|
@@ -9648,9 +9805,9 @@ async function apply(ctx, config) {
|
|
|
9648
9805
|
ctx.effect(() => () => repositoryStatus.dispose(), "dsh-claude: repository status cache");
|
|
9649
9806
|
ctx.inject(["webServer"], (webCtx) => {
|
|
9650
9807
|
registerClaudeClientDiagnosticsRoute(webCtx);
|
|
9651
|
-
registerClaudeDoctorRoutes(webCtx,
|
|
9808
|
+
registerClaudeDoctorRoutes(webCtx, subprocess, supervisor, supervisorConfig, resolutionError);
|
|
9652
9809
|
const desktopActions = webCtx.get("desktopActions");
|
|
9653
|
-
registerClaudeUpdateRoutes(webCtx,
|
|
9810
|
+
registerClaudeUpdateRoutes(webCtx, subprocess, { ...typeof desktopActions?.requestRestart === "function" ? { requestRestart: desktopActions.requestRestart.bind(desktopActions) } : {} });
|
|
9654
9811
|
registerClaudeGlobalSettingsRoute(webCtx, {
|
|
9655
9812
|
defaultLimits,
|
|
9656
9813
|
onUpdated: async () => {
|
|
@@ -9658,37 +9815,60 @@ async function apply(ctx, config) {
|
|
|
9658
9815
|
supervisor.limitsChanged();
|
|
9659
9816
|
}
|
|
9660
9817
|
});
|
|
9661
|
-
|
|
9818
|
+
/** Linked pull requests cleaned up, as `clone root + branch`: the log
|
|
9819
|
+
* still names them, so the sweep has to be told to stop listing them. */
|
|
9820
|
+
const cleanedLinked = /* @__PURE__ */ new Set();
|
|
9821
|
+
registerRepositorySetupRoute(webCtx, repositorySetup, () => sweepWorktrees?.(), (path, branch) => {
|
|
9822
|
+
repositoryStatus.invalidate(path);
|
|
9823
|
+
if (branch !== void 0) cleanedLinked.add(`${path}\0${branch}`);
|
|
9824
|
+
});
|
|
9662
9825
|
registerRepositoryStatusRoute(webCtx, repositoryStatus);
|
|
9663
9826
|
registerRepositoryFileRoute(webCtx, repositoryStatus);
|
|
9664
9827
|
registerJiraRoute(webCtx, new JiraService());
|
|
9665
|
-
const repositoryActions = new RepositoryActionService(
|
|
9666
|
-
/**
|
|
9667
|
-
*
|
|
9668
|
-
|
|
9828
|
+
const repositoryActions = new RepositoryActionService(subprocess, supervisorConfig.executablePath, (cwd) => repositoryStatus.invalidate(cwd));
|
|
9829
|
+
/** Checkouts besides its own a session's routes may act on: every root a
|
|
9830
|
+
* projection sweep has ever vouched for, so a transient probe failure in
|
|
9831
|
+
* one sweep does not un-authorise a linked bar the client still shows. */
|
|
9832
|
+
const extraRoots = new SessionRootLedger(32);
|
|
9669
9833
|
const cwdForClaudeSession = (sessionId, root) => {
|
|
9670
9834
|
const agent = webCtx.agents.get(sessionId);
|
|
9671
9835
|
if (agent === void 0 || webCtx.agentPresets.composedPreset(agent.ctx) !== "claude") return void 0;
|
|
9672
9836
|
if (root === void 0) return agent.session.header.cwd;
|
|
9673
|
-
return extraRoots.
|
|
9837
|
+
return extraRoots.allows(sessionId, root) ? root : void 0;
|
|
9674
9838
|
};
|
|
9675
9839
|
const extraRepositoriesForClaudeSession = async (sessionId, activities) => {
|
|
9676
9840
|
const cwd = cwdForClaudeSession(sessionId);
|
|
9677
9841
|
if (cwd === void 0) return [];
|
|
9678
|
-
const own = await repositoryStatus.rootOf(cwd);
|
|
9842
|
+
const [own, ownStatus] = await Promise.all([repositoryStatus.rootOf(cwd), repositoryStatus.inspect(cwd)]);
|
|
9679
9843
|
const roots = await touchedRepositoryRoots(touchedFilePaths(activities), own ?? cwd, (directory) => repositoryStatus.rootOf(directory), MAX_EXTRA_REPOSITORIES);
|
|
9680
|
-
const
|
|
9681
|
-
|
|
9682
|
-
|
|
9844
|
+
const probed = await Promise.all(roots.map((root) => repositoryStatus.inspect(root)));
|
|
9845
|
+
const checkouts = probed.filter(linkedRepositoryShown);
|
|
9846
|
+
const covered = new Set(checkouts.map((status) => `${status.remote?.toLowerCase()}#${status.pullRequest?.number}`));
|
|
9847
|
+
const pullRequests = touchedPullRequests(activities, ownStatus.remote).filter((item) => !covered.has(`${item.repository}#${item.number}`)).slice(0, Math.max(0, MAX_EXTRA_REPOSITORIES - checkouts.length));
|
|
9848
|
+
const detached = (await Promise.all(pullRequests.map(async (item) => {
|
|
9849
|
+
const clone = probed.find((status) => status.status === "ready" && status.remote?.toLowerCase() === item.repository && status.root !== void 0);
|
|
9850
|
+
const status = await repositoryStatus.inspectPullRequest(clone?.root ?? cwd, item.repository, item.number);
|
|
9851
|
+
if (clone?.root === void 0 || status.status !== "ready") return status;
|
|
9852
|
+
if (status.branch !== void 0 && cleanedLinked.has(`${clone.root}\0${status.branch}`)) return {
|
|
9853
|
+
status: "unavailable",
|
|
9854
|
+
cwd
|
|
9855
|
+
};
|
|
9856
|
+
return {
|
|
9857
|
+
...status,
|
|
9858
|
+
root: clone.root
|
|
9859
|
+
};
|
|
9860
|
+
}))).filter(linkedRepositoryShown);
|
|
9861
|
+
const linked = [...checkouts, ...detached];
|
|
9862
|
+
extraRoots.vouch(sessionId, linked.flatMap((status) => status.root === void 0 ? [] : [status.root]));
|
|
9863
|
+
return linked;
|
|
9683
9864
|
};
|
|
9684
9865
|
registerRepositoryActionRoute(webCtx, repositoryActions, cwdForClaudeSession);
|
|
9685
|
-
registerEditorOpenRoute(webCtx, new EditorOpenService(webCtx.subprocess), cwdForClaudeSession);
|
|
9686
9866
|
registerClaudePromptsRoute(webCtx);
|
|
9687
|
-
const promptAssist = new PromptAssistService(
|
|
9867
|
+
const promptAssist = new PromptAssistService(subprocess, () => supervisorConfig.executablePath);
|
|
9688
9868
|
registerClaudePromptNameRoute(webCtx, promptAssist);
|
|
9689
9869
|
registerClaudePromptRefineRoute(webCtx, promptAssist);
|
|
9690
|
-
registerPullRequestFeedbackRoute(webCtx, new PullRequestFeedbackService(
|
|
9691
|
-
registerAskRoute(webCtx, new AskService(
|
|
9870
|
+
registerPullRequestFeedbackRoute(webCtx, new PullRequestFeedbackService(subprocess), cwdForClaudeSession);
|
|
9871
|
+
registerAskRoute(webCtx, new AskService(subprocess, supervisorConfig.executablePath), cwdForClaudeSession, (sessionId) => {
|
|
9692
9872
|
const snapshot = supervisor.snapshots().find((item) => item.sessionId === sessionId);
|
|
9693
9873
|
return snapshot === void 0 ? void 0 : {
|
|
9694
9874
|
model: claudeModelValue(snapshot.model),
|
|
@@ -9710,7 +9890,7 @@ async function apply(ctx, config) {
|
|
|
9710
9890
|
reset: (sessionId) => supervisor.disposeSession(sessionId),
|
|
9711
9891
|
restoreFiles: async (sessionId, tree) => {
|
|
9712
9892
|
const cwd = webCtx.agents.get(sessionId)?.session.header.cwd;
|
|
9713
|
-
return cwd === void 0 ? false : restoreWorktreeTree(
|
|
9893
|
+
return cwd === void 0 ? false : restoreWorktreeTree(subprocess, cwd, tree);
|
|
9714
9894
|
}
|
|
9715
9895
|
});
|
|
9716
9896
|
registerPlanUsageRoute(webCtx, (fetchedAt) => probePlanUsage(supervisorConfig.executablePath, fetchedAt));
|