@norman-else/dsh-claude 0.1.41 → 0.1.43
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 +3 -1
- package/lib/bin.mjs +1 -1
- package/lib/client.d.ts +33 -1
- package/lib/client.js +1026 -107
- package/lib/client.js.map +1 -1
- package/lib/{events-B-FPMzI7.mjs → events-oovRTmX7.mjs} +5 -2
- package/lib/events-oovRTmX7.mjs.map +1 -0
- package/lib/index.d.mts +1 -0
- package/lib/index.mjs +823 -75
- package/lib/index.mjs.map +1 -1
- package/lib/{presenters-BV42EKkB.mjs → presenters-BVWj7u0a.mjs} +2 -2
- package/lib/{presenters-BV42EKkB.mjs.map → presenters-BVWj7u0a.mjs.map} +1 -1
- package/lib/{preset-installer-yRGfkGjd.mjs → preset-installer-JUktnfwS.mjs} +2 -2
- package/lib/{preset-installer-yRGfkGjd.mjs.map → preset-installer-JUktnfwS.mjs.map} +1 -1
- package/lib/preset-route.mjs +2 -2
- package/package.json +1 -1
- package/lib/events-B-FPMzI7.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_FEEDBACK_PATH, B as DEFAULT_CLAUDE_RENDER_MODE, C as CLAUDE_PROJECTION_PATH, D as CLAUDE_PROSE_MODES, E as CLAUDE_PROMPT_REFINE_PATH, F as CLAUDE_REWIND_PATH, G as isClaudeRenderMode, H as TASK_TOOL_NAMES, I as CLAUDE_UPDATE_CHECK_PATH, L as CLAUDE_UPDATE_PATH, M as CLAUDE_REPOSITORY_SETUP_PATH, N as CLAUDE_REPOSITORY_STATUS_PATH, O as CLAUDE_RENDER_MODES, P as CLAUDE_REVIEW_COMMENT_PATH, R as CLAUDE_USAGE_PATH, S as CLAUDE_PLAN_FEEDBACK_PATH, T as CLAUDE_PROMPT_NAME_PATH, U as isClaudeAlertMode, W as isClaudeProseMode, _ as CLAUDE_CODE_PROVIDER_IDS, a as latestClaudeTasks, b as CLAUDE_GLOBAL_SETTINGS_PATH, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_ALERT_MODES, g as CLAUDE_CODE_PROVIDER, h as CLAUDE_CODE_PRESET_ID, i as latestClaudeSessionBinding, j as CLAUDE_REPOSITORY_FILE_PATH, k as CLAUDE_REPOSITORY_ACTION_PATH, l as redactText, m as CLAUDE_CLIENT_DIAGNOSTICS_PATH, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_ASK_PATH, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as CLAUDE_DOCTOR_PATH, w as CLAUDE_PROMPTS_PATH, x as CLAUDE_JIRA_PATH, y as CLAUDE_EDITOR_OPEN_PATH, z as DEFAULT_CLAUDE_PROSE_MODE } from "./events-oovRTmX7.mjs";
|
|
2
|
+
import { a as projectClaudeCommands, i as CLAUDE_COMMANDS_SERVICE, r as dynamicPresenterDefinition, t as CLAUDE_PRESENTER_NAMES } from "./presenters-BVWj7u0a.mjs";
|
|
3
|
+
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-JUktnfwS.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";
|
|
@@ -1840,6 +1840,21 @@ async function withTimeout(operation, timeoutMs, label) {
|
|
|
1840
1840
|
if (timer !== void 0) clearTimeout(timer);
|
|
1841
1841
|
}
|
|
1842
1842
|
}
|
|
1843
|
+
async function withAbort(operation, signal) {
|
|
1844
|
+
if (signal === void 0) return operation;
|
|
1845
|
+
if (signal.aborted) throw abortFailure();
|
|
1846
|
+
let abortListener;
|
|
1847
|
+
try {
|
|
1848
|
+
return await Promise.race([operation, new Promise((_resolve, reject) => {
|
|
1849
|
+
abortListener = () => {
|
|
1850
|
+
reject(abortFailure());
|
|
1851
|
+
};
|
|
1852
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
1853
|
+
})]);
|
|
1854
|
+
} finally {
|
|
1855
|
+
if (abortListener !== void 0) signal.removeEventListener("abort", abortListener);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1843
1858
|
/** The uuid of one main-chain transcript entry, or undefined for anything a
|
|
1844
1859
|
* rewind must not fork at: stream partials, results, and sidechain traffic. */
|
|
1845
1860
|
function chainEntryUuid(message) {
|
|
@@ -1895,6 +1910,15 @@ var ClaudeSupervisor = class {
|
|
|
1895
1910
|
#contextWindows = /* @__PURE__ */ new Map();
|
|
1896
1911
|
#disposed = false;
|
|
1897
1912
|
#admissionGate = Promise.resolve();
|
|
1913
|
+
/** FIFO user turns live outside the gate while capacity-blocked, so
|
|
1914
|
+
* best-effort metadata can still enter the serialized path and fail. */
|
|
1915
|
+
#turnAdmissions = [];
|
|
1916
|
+
#metadataAdmissions = /* @__PURE__ */ new Set();
|
|
1917
|
+
#admissionDrainScheduled = false;
|
|
1918
|
+
/** Prevent a capacity change between a failed attempt and parking the head
|
|
1919
|
+
* from becoming a lost wake-up. */
|
|
1920
|
+
#admissionRevision = 0;
|
|
1921
|
+
#blockedAdmissionRevision;
|
|
1898
1922
|
constructor(dependencies) {
|
|
1899
1923
|
this.#runtime = dependencies.runtime;
|
|
1900
1924
|
this.#approval = dependencies.approval;
|
|
@@ -1968,28 +1992,140 @@ var ClaudeSupervisor = class {
|
|
|
1968
1992
|
runTurn(request) {
|
|
1969
1993
|
const interruption = this.#interruptions.get(request.agent.id);
|
|
1970
1994
|
if (interruption !== void 0) return interruption.then(() => this.runTurn(request));
|
|
1971
|
-
|
|
1995
|
+
return new Promise((resolve, reject) => {
|
|
1996
|
+
let complete;
|
|
1997
|
+
const completion = new Promise((done) => {
|
|
1998
|
+
complete = done;
|
|
1999
|
+
});
|
|
2000
|
+
const admission = {
|
|
2001
|
+
request,
|
|
2002
|
+
resolve,
|
|
2003
|
+
reject,
|
|
2004
|
+
delivered: false,
|
|
2005
|
+
admitting: false,
|
|
2006
|
+
waitedForCapacity: false,
|
|
2007
|
+
cancellation: new AbortController(),
|
|
2008
|
+
completion,
|
|
2009
|
+
complete: () => {
|
|
2010
|
+
complete?.();
|
|
2011
|
+
}
|
|
2012
|
+
};
|
|
2013
|
+
if (request.signal !== void 0) {
|
|
2014
|
+
const abortListener = () => {
|
|
2015
|
+
if (!admission.admitting) this.#finishTurnAdmission(admission, { error: abortFailure() });
|
|
2016
|
+
else if (admission.waitedForCapacity && !admission.delivered) {
|
|
2017
|
+
admission.delivered = true;
|
|
2018
|
+
admission.reject(abortFailure());
|
|
2019
|
+
}
|
|
2020
|
+
this.#admissionRevision += 1;
|
|
2021
|
+
this.#blockedAdmissionRevision = void 0;
|
|
2022
|
+
this.#scheduleTurnAdmissions();
|
|
2023
|
+
};
|
|
2024
|
+
admission.abortListener = abortListener;
|
|
2025
|
+
request.signal.addEventListener("abort", abortListener, { once: true });
|
|
2026
|
+
}
|
|
2027
|
+
this.#turnAdmissions.push(admission);
|
|
2028
|
+
this.#scheduleTurnAdmissions();
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
async #drainTurnAdmissions() {
|
|
2032
|
+
while (this.#turnAdmissions.length > 0) {
|
|
2033
|
+
const admission = this.#turnAdmissions[0];
|
|
2034
|
+
if (signalAborted(admission.request.signal)) {
|
|
2035
|
+
this.#finishTurnAdmission(admission, { error: abortFailure() });
|
|
2036
|
+
continue;
|
|
2037
|
+
}
|
|
2038
|
+
const attemptedRevision = this.#admissionRevision;
|
|
2039
|
+
admission.admitting = true;
|
|
2040
|
+
try {
|
|
2041
|
+
const output = await this.#runTurnAdmitted(admission.request, admission.waitedForCapacity, admission.cancellation.signal);
|
|
2042
|
+
this.#finishTurnAdmission(admission, { output });
|
|
2043
|
+
} catch (error) {
|
|
2044
|
+
if (error instanceof ClaudeProcessLimitError && !signalAborted(admission.request.signal) && !admission.cancellation.signal.aborted && !this.#disposed) {
|
|
2045
|
+
admission.admitting = false;
|
|
2046
|
+
admission.waitedForCapacity = true;
|
|
2047
|
+
if (attemptedRevision === this.#admissionRevision) {
|
|
2048
|
+
this.#blockedAdmissionRevision = attemptedRevision;
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
continue;
|
|
2052
|
+
}
|
|
2053
|
+
this.#finishTurnAdmission(admission, { error });
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
#finishTurnAdmission(admission, outcome) {
|
|
2058
|
+
if (this.#turnAdmissions[0] === admission) this.#turnAdmissions.shift();
|
|
2059
|
+
else {
|
|
2060
|
+
const index = this.#turnAdmissions.indexOf(admission);
|
|
2061
|
+
if (index >= 0) this.#turnAdmissions.splice(index, 1);
|
|
2062
|
+
}
|
|
2063
|
+
admission.admitting = false;
|
|
2064
|
+
if (admission.request.signal !== void 0 && admission.abortListener !== void 0) admission.request.signal.removeEventListener("abort", admission.abortListener);
|
|
2065
|
+
if (!admission.delivered) {
|
|
2066
|
+
admission.delivered = true;
|
|
2067
|
+
if ("error" in outcome) admission.reject(outcome.error);
|
|
2068
|
+
else admission.resolve(outcome.output);
|
|
2069
|
+
}
|
|
2070
|
+
admission.complete();
|
|
2071
|
+
}
|
|
2072
|
+
#scheduleTurnAdmissions() {
|
|
2073
|
+
if (this.#admissionDrainScheduled || this.#turnAdmissions.length === 0 || this.#blockedAdmissionRevision === this.#admissionRevision) return;
|
|
2074
|
+
this.#admissionDrainScheduled = true;
|
|
2075
|
+
const operation = this.#admissionGate.then(() => this.#drainTurnAdmissions());
|
|
1972
2076
|
this.#admissionGate = operation.then(() => void 0, () => void 0);
|
|
1973
|
-
|
|
2077
|
+
const finished = () => {
|
|
2078
|
+
this.#admissionDrainScheduled = false;
|
|
2079
|
+
this.#scheduleTurnAdmissions();
|
|
2080
|
+
};
|
|
2081
|
+
operation.then(finished, finished);
|
|
1974
2082
|
}
|
|
1975
|
-
async #runTurnAdmitted(request) {
|
|
2083
|
+
async #runTurnAdmitted(request, abortDuringAdmission, cancellationSignal) {
|
|
1976
2084
|
if (this.#disposed) throw new Error("dsh-claude: supervisor is disposed");
|
|
2085
|
+
if (cancellationSignal.aborted) throw abortFailure();
|
|
1977
2086
|
if (signalAborted(request.signal)) throw abortFailure();
|
|
1978
2087
|
const sessionId = request.agent.id;
|
|
1979
2088
|
let entry = this.#entries.get(sessionId);
|
|
2089
|
+
let createdForRequest;
|
|
2090
|
+
const throwIfUnavailable = async () => {
|
|
2091
|
+
const failure = this.#disposed ? /* @__PURE__ */ new Error("dsh-claude: supervisor is disposed") : cancellationSignal.aborted || abortDuringAdmission && signalAborted(request.signal) ? abortFailure() : void 0;
|
|
2092
|
+
if (failure === void 0) return;
|
|
2093
|
+
if (createdForRequest !== void 0) {
|
|
2094
|
+
if (this.#entries.get(sessionId) === createdForRequest) this.#entries.delete(sessionId);
|
|
2095
|
+
await this.#disposeEntry(createdForRequest);
|
|
2096
|
+
createdForRequest = void 0;
|
|
2097
|
+
}
|
|
2098
|
+
throw failure;
|
|
2099
|
+
};
|
|
1980
2100
|
if (entry?.state === "disposed" || entry?.state === "disconnected" || entry?.state === "outcome-unknown") {
|
|
1981
2101
|
this.#entries.delete(sessionId);
|
|
1982
2102
|
await this.#disposeEntry(entry);
|
|
2103
|
+
await throwIfUnavailable();
|
|
1983
2104
|
entry = void 0;
|
|
1984
2105
|
}
|
|
1985
2106
|
if (entry === void 0) {
|
|
1986
2107
|
await this.#makeRoom();
|
|
1987
|
-
|
|
2108
|
+
await throwIfUnavailable();
|
|
2109
|
+
try {
|
|
2110
|
+
entry = await this.#createEntry(request.agent, request.model ?? this.#config.defaultModel, request.thinkingMode, abortDuringAdmission ? request.signal : void 0, cancellationSignal);
|
|
2111
|
+
} catch (error) {
|
|
2112
|
+
await throwIfUnavailable();
|
|
2113
|
+
throw error;
|
|
2114
|
+
}
|
|
2115
|
+
createdForRequest = entry;
|
|
2116
|
+
await throwIfUnavailable();
|
|
1988
2117
|
this.#entries.set(sessionId, entry);
|
|
1989
2118
|
}
|
|
1990
2119
|
if (entry.ownerAgent !== request.agent) throw new Error(`dsh-claude: live agent identity changed for session ${sessionId}`);
|
|
1991
2120
|
if (entry.active !== void 0 || entry.state === "interrupting") throw new ClaudeTurnBusyError(sessionId);
|
|
1992
|
-
|
|
2121
|
+
try {
|
|
2122
|
+
const initialization = withAbort(entry.sdkInitialization, cancellationSignal);
|
|
2123
|
+
await (abortDuringAdmission ? withAbort(initialization, request.signal) : initialization);
|
|
2124
|
+
} catch (error) {
|
|
2125
|
+
await throwIfUnavailable();
|
|
2126
|
+
throw error;
|
|
2127
|
+
}
|
|
2128
|
+
await throwIfUnavailable();
|
|
1993
2129
|
if (entry.idleTimer !== void 0) {
|
|
1994
2130
|
clearTimeout(entry.idleTimer);
|
|
1995
2131
|
entry.idleTimer = void 0;
|
|
@@ -1998,13 +2134,31 @@ var ClaudeSupervisor = class {
|
|
|
1998
2134
|
if (request.thinkingMode !== entry.thinkingMode || model !== entry.model) {
|
|
1999
2135
|
this.#entries.delete(sessionId);
|
|
2000
2136
|
await this.#disposeEntry(entry);
|
|
2001
|
-
entry =
|
|
2137
|
+
if (createdForRequest === entry) createdForRequest = void 0;
|
|
2138
|
+
await throwIfUnavailable();
|
|
2139
|
+
try {
|
|
2140
|
+
entry = await this.#createEntry(request.agent, model, request.thinkingMode, abortDuringAdmission ? request.signal : void 0, cancellationSignal);
|
|
2141
|
+
} catch (error) {
|
|
2142
|
+
await throwIfUnavailable();
|
|
2143
|
+
throw error;
|
|
2144
|
+
}
|
|
2145
|
+
createdForRequest = entry;
|
|
2146
|
+
await throwIfUnavailable();
|
|
2002
2147
|
this.#entries.set(sessionId, entry);
|
|
2003
|
-
|
|
2148
|
+
try {
|
|
2149
|
+
const initialization = withAbort(entry.sdkInitialization, cancellationSignal);
|
|
2150
|
+
await (abortDuringAdmission ? withAbort(initialization, request.signal) : initialization);
|
|
2151
|
+
} catch (error) {
|
|
2152
|
+
await throwIfUnavailable();
|
|
2153
|
+
throw error;
|
|
2154
|
+
}
|
|
2004
2155
|
} else await this.#syncPermissionMode(entry);
|
|
2156
|
+
await throwIfUnavailable();
|
|
2005
2157
|
const promptUuid = randomUUID();
|
|
2006
2158
|
const cursor = currentClaudeActivityCursor(request.agent.session.events);
|
|
2007
|
-
|
|
2159
|
+
const projection = await this.#sidecar.read(sessionId);
|
|
2160
|
+
await throwIfUnavailable();
|
|
2161
|
+
cursor.nextOrdinal = projection.activities.reduce((next, activity) => activity.turn === cursor.turn && activity.step === cursor.step ? Math.max(next, activity.ordinal + 1) : next, 0);
|
|
2008
2162
|
const active = {
|
|
2009
2163
|
agent: request.agent,
|
|
2010
2164
|
cursor,
|
|
@@ -2052,9 +2206,12 @@ var ClaudeSupervisor = class {
|
|
|
2052
2206
|
title: "Claude Code turn cancelled before submission"
|
|
2053
2207
|
});
|
|
2054
2208
|
entry.active = void 0;
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2209
|
+
if (!abortDuringAdmission || createdForRequest === void 0) {
|
|
2210
|
+
entry.state = "idle";
|
|
2211
|
+
entry.lastUsedAt = Date.now();
|
|
2212
|
+
this.#armIdleTimer(entry);
|
|
2213
|
+
}
|
|
2214
|
+
await throwIfUnavailable();
|
|
2058
2215
|
return active.output;
|
|
2059
2216
|
}
|
|
2060
2217
|
if (request.signal !== void 0) {
|
|
@@ -2068,31 +2225,57 @@ var ClaudeSupervisor = class {
|
|
|
2068
2225
|
return active.output;
|
|
2069
2226
|
}
|
|
2070
2227
|
#runMetadata(agent, model, operation) {
|
|
2228
|
+
if (this.#turnAdmissions.some((admission) => admission.waitedForCapacity)) return Promise.reject(new ClaudeProcessLimitError(this.#config.maxProcesses));
|
|
2229
|
+
let complete;
|
|
2230
|
+
const admission = {
|
|
2231
|
+
sessionId: agent.id,
|
|
2232
|
+
cancellation: new AbortController(),
|
|
2233
|
+
started: false,
|
|
2234
|
+
completion: new Promise((done) => {
|
|
2235
|
+
complete = done;
|
|
2236
|
+
}),
|
|
2237
|
+
complete: () => {
|
|
2238
|
+
complete?.();
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
this.#metadataAdmissions.add(admission);
|
|
2071
2242
|
const admitted = this.#admissionGate.then(async () => {
|
|
2243
|
+
admission.started = true;
|
|
2072
2244
|
if (this.#disposed) throw new Error("dsh-claude: supervisor is disposed");
|
|
2073
|
-
|
|
2245
|
+
if (admission.cancellation.signal.aborted) throw abortFailure();
|
|
2246
|
+
if (this.#turnAdmissions.some((admission) => admission.waitedForCapacity)) throw new ClaudeProcessLimitError(this.#config.maxProcesses);
|
|
2247
|
+
const entry = await this.#metadataEntry(agent, model, admission.cancellation.signal);
|
|
2074
2248
|
try {
|
|
2075
|
-
await entry.sdkInitialization;
|
|
2076
|
-
return await this.#control(entry, operation(entry.query, entry), "Claude metadata request");
|
|
2249
|
+
await withAbort(entry.sdkInitialization, admission.cancellation.signal);
|
|
2250
|
+
return await withAbort(this.#control(entry, operation(entry.query, entry), "Claude metadata request"), admission.cancellation.signal);
|
|
2077
2251
|
} finally {
|
|
2078
2252
|
entry.lastUsedAt = Date.now();
|
|
2079
2253
|
if (entry.active === void 0 && entry.state === "idle") this.#armIdleTimer(entry);
|
|
2080
2254
|
}
|
|
2255
|
+
}).finally(() => {
|
|
2256
|
+
this.#finishMetadataAdmission(admission);
|
|
2081
2257
|
});
|
|
2082
2258
|
this.#admissionGate = admitted.then(() => void 0, () => void 0);
|
|
2083
|
-
return admitted;
|
|
2259
|
+
return withAbort(admitted, admission.cancellation.signal);
|
|
2084
2260
|
}
|
|
2085
|
-
async #metadataEntry(agent, model) {
|
|
2261
|
+
async #metadataEntry(agent, model, cancellationSignal) {
|
|
2262
|
+
if (cancellationSignal.aborted) throw abortFailure();
|
|
2086
2263
|
const sessionId = agent.id;
|
|
2087
2264
|
let entry = this.#entries.get(sessionId);
|
|
2088
2265
|
if (entry?.state === "disposed" || entry?.state === "disconnected" || entry?.state === "outcome-unknown") {
|
|
2089
2266
|
this.#entries.delete(sessionId);
|
|
2090
2267
|
await this.#disposeEntry(entry);
|
|
2268
|
+
if (cancellationSignal.aborted) throw abortFailure();
|
|
2091
2269
|
entry = void 0;
|
|
2092
2270
|
}
|
|
2093
2271
|
if (entry === void 0) {
|
|
2094
2272
|
await this.#makeRoom();
|
|
2095
|
-
|
|
2273
|
+
if (cancellationSignal.aborted) throw abortFailure();
|
|
2274
|
+
entry = await this.#createEntry(agent, model, void 0, void 0, cancellationSignal);
|
|
2275
|
+
if (cancellationSignal.aborted) {
|
|
2276
|
+
await this.#disposeEntry(entry);
|
|
2277
|
+
throw abortFailure();
|
|
2278
|
+
}
|
|
2096
2279
|
this.#entries.set(sessionId, entry);
|
|
2097
2280
|
}
|
|
2098
2281
|
if (entry.ownerAgent !== agent) throw new Error(`dsh-claude: live agent identity changed for session ${sessionId}`);
|
|
@@ -2101,14 +2284,15 @@ var ClaudeSupervisor = class {
|
|
|
2101
2284
|
clearTimeout(entry.idleTimer);
|
|
2102
2285
|
entry.idleTimer = void 0;
|
|
2103
2286
|
}
|
|
2104
|
-
await this.#syncPermissionMode(entry);
|
|
2287
|
+
await withAbort(this.#syncPermissionMode(entry), cancellationSignal);
|
|
2288
|
+
if (cancellationSignal.aborted) throw abortFailure();
|
|
2105
2289
|
return entry;
|
|
2106
2290
|
}
|
|
2107
2291
|
/** Run one SDK control request against a live entry, and discard the entry if
|
|
2108
2292
|
* it does not answer.
|
|
2109
2293
|
*
|
|
2110
|
-
* Turn admission and
|
|
2111
|
-
* unbounded control request stalls every session until the Host restarts.
|
|
2294
|
+
* Turn admission attempts and metadata reads share one process-wide gate,
|
|
2295
|
+
* so an unbounded control request stalls every session until the Host restarts.
|
|
2112
2296
|
* Bounding it is only half the cure: a timeout also proves this query has
|
|
2113
2297
|
* stopped answering, and keeping the entry means the next caller reuses the
|
|
2114
2298
|
* same dead process — timing out again, forever. Discarding it lets the next
|
|
@@ -2129,32 +2313,97 @@ var ClaudeSupervisor = class {
|
|
|
2129
2313
|
await this.#control(entry, entry.query.setPermissionMode(mode), "Claude Code permission mode switch");
|
|
2130
2314
|
entry.permissionMode = mode;
|
|
2131
2315
|
}
|
|
2316
|
+
#finishMetadataAdmission(admission) {
|
|
2317
|
+
this.#metadataAdmissions.delete(admission);
|
|
2318
|
+
admission.complete();
|
|
2319
|
+
}
|
|
2320
|
+
#cancelMetadataAdmissions(predicate) {
|
|
2321
|
+
const cancelled = [...this.#metadataAdmissions].filter(predicate);
|
|
2322
|
+
for (const admission of cancelled) {
|
|
2323
|
+
admission.cancellation.abort();
|
|
2324
|
+
if (!admission.started) this.#finishMetadataAdmission(admission);
|
|
2325
|
+
}
|
|
2326
|
+
return cancelled.map((admission) => admission.completion);
|
|
2327
|
+
}
|
|
2328
|
+
#cancelTurnAdmissions(predicate, error) {
|
|
2329
|
+
const cancelled = this.#turnAdmissions.filter(predicate);
|
|
2330
|
+
for (const admission of cancelled) {
|
|
2331
|
+
admission.cancellation.abort();
|
|
2332
|
+
if (!admission.delivered) {
|
|
2333
|
+
admission.delivered = true;
|
|
2334
|
+
admission.reject(error);
|
|
2335
|
+
}
|
|
2336
|
+
if (!admission.admitting) this.#finishTurnAdmission(admission, { error });
|
|
2337
|
+
}
|
|
2338
|
+
if (cancelled.length > 0) {
|
|
2339
|
+
this.#admissionRevision += 1;
|
|
2340
|
+
this.#blockedAdmissionRevision = void 0;
|
|
2341
|
+
this.#scheduleTurnAdmissions();
|
|
2342
|
+
}
|
|
2343
|
+
return cancelled.map((admission) => admission.completion);
|
|
2344
|
+
}
|
|
2345
|
+
limitsChanged() {
|
|
2346
|
+
if (this.#disposed) return;
|
|
2347
|
+
this.#notifyCapacityChange();
|
|
2348
|
+
this.#scheduleLimitReconciliation();
|
|
2349
|
+
}
|
|
2132
2350
|
async disposeSession(sessionId) {
|
|
2351
|
+
const pendingAdmissions = this.#cancelTurnAdmissions((admission) => admission.request.agent.id === sessionId, abortFailure());
|
|
2352
|
+
const pendingMetadata = this.#cancelMetadataAdmissions((admission) => admission.sessionId === sessionId);
|
|
2133
2353
|
const entry = this.#entries.get(sessionId);
|
|
2134
|
-
if (entry
|
|
2135
|
-
|
|
2136
|
-
|
|
2354
|
+
if (entry !== void 0) this.#entries.delete(sessionId);
|
|
2355
|
+
await Promise.allSettled([
|
|
2356
|
+
...pendingAdmissions,
|
|
2357
|
+
...pendingMetadata,
|
|
2358
|
+
...entry === void 0 ? [] : [this.#disposeEntry(entry)]
|
|
2359
|
+
]);
|
|
2137
2360
|
}
|
|
2138
2361
|
async dispose() {
|
|
2139
2362
|
if (this.#disposed) return;
|
|
2140
2363
|
this.#disposed = true;
|
|
2364
|
+
const pendingAdmissions = this.#cancelTurnAdmissions(() => true, /* @__PURE__ */ new Error("dsh-claude: supervisor is disposed"));
|
|
2365
|
+
const pendingMetadata = this.#cancelMetadataAdmissions(() => true);
|
|
2141
2366
|
const entries = [...this.#entries.values()];
|
|
2142
2367
|
this.#entries.clear();
|
|
2143
|
-
|
|
2368
|
+
this.#notifyCapacityChange();
|
|
2369
|
+
await Promise.allSettled([
|
|
2370
|
+
...pendingAdmissions,
|
|
2371
|
+
...pendingMetadata,
|
|
2372
|
+
...entries.map((entry) => this.#disposeEntry(entry))
|
|
2373
|
+
]);
|
|
2144
2374
|
}
|
|
2145
2375
|
async #makeRoom() {
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2376
|
+
while (this.#entries.size >= this.#config.maxProcesses) {
|
|
2377
|
+
const idle = [...this.#entries.values()].filter((entry) => entry.active === void 0 && entry.state === "idle").sort((left, right) => left.lastUsedAt - right.lastUsedAt)[0];
|
|
2378
|
+
if (idle === void 0) throw new ClaudeProcessLimitError(this.#config.maxProcesses);
|
|
2379
|
+
this.#entries.delete(idle.sessionId);
|
|
2380
|
+
await this.#disposeEntry(idle);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
async #trimExcessIdle() {
|
|
2384
|
+
while (this.#entries.size > this.#config.maxProcesses) {
|
|
2385
|
+
const idle = [...this.#entries.values()].filter((entry) => entry.active === void 0 && entry.state === "idle").sort((left, right) => left.lastUsedAt - right.lastUsedAt)[0];
|
|
2386
|
+
if (idle === void 0) return;
|
|
2387
|
+
this.#entries.delete(idle.sessionId);
|
|
2388
|
+
await this.#disposeEntry(idle);
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
#notifyCapacityChange() {
|
|
2392
|
+
this.#admissionRevision += 1;
|
|
2393
|
+
this.#blockedAdmissionRevision = void 0;
|
|
2394
|
+
this.#scheduleTurnAdmissions();
|
|
2395
|
+
}
|
|
2396
|
+
#scheduleLimitReconciliation() {
|
|
2397
|
+
const operation = this.#admissionGate.then(() => this.#trimExcessIdle());
|
|
2398
|
+
this.#admissionGate = operation.then(() => void 0, () => void 0);
|
|
2151
2399
|
}
|
|
2152
|
-
async #createEntry(agent, model, thinkingMode) {
|
|
2400
|
+
async #createEntry(agent, model, thinkingMode, signal, cancellationSignal) {
|
|
2153
2401
|
const sessionId = agent.id;
|
|
2154
2402
|
const cwd = agent.session.header.cwd ?? process.cwd();
|
|
2155
2403
|
const input = new AsyncQueue();
|
|
2156
2404
|
const lifetime = new AbortController();
|
|
2157
2405
|
const projection = await this.#sidecar.importLegacy(sessionId, agent.session.events);
|
|
2406
|
+
if (signalAborted(signal) || signalAborted(cancellationSignal)) throw abortFailure();
|
|
2158
2407
|
const binding = projection.binding;
|
|
2159
2408
|
const pendingRewind = projection.rewind?.pending;
|
|
2160
2409
|
const forkAt = pendingRewind !== void 0 && "resumeAt" in pendingRewind ? pendingRewind.resumeAt : void 0;
|
|
@@ -2627,20 +2876,7 @@ var ClaudeSupervisor = class {
|
|
|
2627
2876
|
...result.usage.outputTokens === void 0 ? {} : { outputTokens: result.usage.outputTokens }
|
|
2628
2877
|
};
|
|
2629
2878
|
}
|
|
2630
|
-
async #completeProgressSegment(active, result
|
|
2631
|
-
if (recordUsage && (result.usage.inputTokens !== void 0 || result.usage.outputTokens !== void 0 || result.usage.cumulativeCostUsd !== void 0)) {
|
|
2632
|
-
await this.#appendSafely(active, {
|
|
2633
|
-
kind: "usage",
|
|
2634
|
-
phase: "completed",
|
|
2635
|
-
title: "Claude usage",
|
|
2636
|
-
summary: usageSummary(result.usage),
|
|
2637
|
-
usage: this.#timedUsage(active, result.usage)
|
|
2638
|
-
});
|
|
2639
|
-
active.output.push({
|
|
2640
|
-
type: "usage",
|
|
2641
|
-
usage: this.#reportedUsage(active, result)
|
|
2642
|
-
});
|
|
2643
|
-
}
|
|
2879
|
+
async #completeProgressSegment(active, result) {
|
|
2644
2880
|
if (!active.sawTextDelta && active.text.length === 0 && result.text !== void 0) {
|
|
2645
2881
|
active.text = result.text;
|
|
2646
2882
|
active.transcriptText = result.text;
|
|
@@ -2660,6 +2896,25 @@ var ClaudeSupervisor = class {
|
|
|
2660
2896
|
this.#closeTranscriptTextSegment(active);
|
|
2661
2897
|
active.thinking = "";
|
|
2662
2898
|
}
|
|
2899
|
+
/** The turn's accounting, recorded once the turn is actually over.
|
|
2900
|
+
*
|
|
2901
|
+
* A turn that hands off to background tasks passes through the same result
|
|
2902
|
+
* handling on its way to `waiting-tasks`, and recording there drew a closing
|
|
2903
|
+
* total under a turn that was still running. */
|
|
2904
|
+
async #recordTurnUsage(active, result) {
|
|
2905
|
+
if (result.usage.inputTokens === void 0 && result.usage.outputTokens === void 0 && result.usage.cumulativeCostUsd === void 0) return;
|
|
2906
|
+
await this.#appendSafely(active, {
|
|
2907
|
+
kind: "usage",
|
|
2908
|
+
phase: "completed",
|
|
2909
|
+
title: "Claude usage",
|
|
2910
|
+
summary: usageSummary(result.usage),
|
|
2911
|
+
usage: this.#timedUsage(active, result.usage)
|
|
2912
|
+
});
|
|
2913
|
+
active.output.push({
|
|
2914
|
+
type: "usage",
|
|
2915
|
+
usage: this.#reportedUsage(active, result)
|
|
2916
|
+
});
|
|
2917
|
+
}
|
|
2663
2918
|
async #completeTurn(entry, active, result) {
|
|
2664
2919
|
if (entry.active !== active) return;
|
|
2665
2920
|
if (active.aborted) {
|
|
@@ -2677,21 +2932,10 @@ var ClaudeSupervisor = class {
|
|
|
2677
2932
|
await this.#recordChainAnchor(entry, active);
|
|
2678
2933
|
this.#checkpointProjection(entry);
|
|
2679
2934
|
this.#armIdleTimer(entry);
|
|
2935
|
+
this.#notifyCapacityChange();
|
|
2936
|
+
this.#scheduleLimitReconciliation();
|
|
2680
2937
|
return;
|
|
2681
2938
|
}
|
|
2682
|
-
if (result.usage.inputTokens !== void 0 || result.usage.outputTokens !== void 0 || result.usage.cumulativeCostUsd !== void 0) {
|
|
2683
|
-
await this.#appendSafely(active, {
|
|
2684
|
-
kind: "usage",
|
|
2685
|
-
phase: "completed",
|
|
2686
|
-
title: "Claude usage",
|
|
2687
|
-
summary: usageSummary(result.usage),
|
|
2688
|
-
usage: this.#timedUsage(active, result.usage)
|
|
2689
|
-
});
|
|
2690
|
-
active.output.push({
|
|
2691
|
-
type: "usage",
|
|
2692
|
-
usage: this.#reportedUsage(active, result)
|
|
2693
|
-
});
|
|
2694
|
-
}
|
|
2695
2939
|
const unmatchedDenials = (result.permissionDenials ?? []).filter((denial) => !active.deniedToolUseIds.has(denial.toolUseId));
|
|
2696
2940
|
if (unmatchedDenials.length > 0) await this.#appendSafely(active, {
|
|
2697
2941
|
kind: "permission",
|
|
@@ -2700,6 +2944,7 @@ var ClaudeSupervisor = class {
|
|
|
2700
2944
|
summary: unmatchedDenials.map((denial) => denial.toolName).join(", ")
|
|
2701
2945
|
});
|
|
2702
2946
|
if (!result.success) {
|
|
2947
|
+
await this.#recordTurnUsage(active, result);
|
|
2703
2948
|
if (!active.sawTextDelta && active.text.length === 0 && result.text !== void 0) {
|
|
2704
2949
|
active.text = result.text;
|
|
2705
2950
|
active.transcriptText = result.text;
|
|
@@ -2738,9 +2983,10 @@ var ClaudeSupervisor = class {
|
|
|
2738
2983
|
phase: "updated",
|
|
2739
2984
|
title: "Claude Code is waiting for background tasks"
|
|
2740
2985
|
});
|
|
2741
|
-
await this.#completeProgressSegment(active, result
|
|
2986
|
+
await this.#completeProgressSegment(active, result);
|
|
2742
2987
|
return;
|
|
2743
2988
|
}
|
|
2989
|
+
await this.#recordTurnUsage(active, result);
|
|
2744
2990
|
await this.#appendSafely(active, {
|
|
2745
2991
|
kind: "status",
|
|
2746
2992
|
phase: "completed",
|
|
@@ -2758,9 +3004,11 @@ var ClaudeSupervisor = class {
|
|
|
2758
3004
|
entry.state = "idle";
|
|
2759
3005
|
entry.lastUsedAt = Date.now();
|
|
2760
3006
|
await this.#recordChainAnchor(entry, active);
|
|
2761
|
-
await this.#learnContextWindow(entry);
|
|
2762
3007
|
this.#checkpointProjection(entry);
|
|
2763
3008
|
this.#armIdleTimer(entry);
|
|
3009
|
+
this.#notifyCapacityChange();
|
|
3010
|
+
this.#scheduleLimitReconciliation();
|
|
3011
|
+
await this.#learnContextWindow(entry);
|
|
2764
3012
|
}
|
|
2765
3013
|
/** Tell every reader where this session's delta stream ended.
|
|
2766
3014
|
*
|
|
@@ -2942,6 +3190,7 @@ var ClaudeSupervisor = class {
|
|
|
2942
3190
|
if (entry.process !== void 0) try {
|
|
2943
3191
|
await entry.process.handle.waitForExit(AbortSignal.timeout(5e3));
|
|
2944
3192
|
} catch {}
|
|
3193
|
+
this.#notifyCapacityChange();
|
|
2945
3194
|
}
|
|
2946
3195
|
};
|
|
2947
3196
|
//#endregion
|
|
@@ -3995,7 +4244,11 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
|
|
|
3995
4244
|
}
|
|
3996
4245
|
}));
|
|
3997
4246
|
for (const sessionId of sessionIds) writeSnapshot(sessionId).catch(() => void 0);
|
|
4247
|
+
let sweeping = false;
|
|
3998
4248
|
const timer = setInterval(() => {
|
|
4249
|
+
writeLine({ type: "ping" });
|
|
4250
|
+
if (sweeping) return;
|
|
4251
|
+
sweeping = true;
|
|
3999
4252
|
(async () => {
|
|
4000
4253
|
for (const sessionId of sessionIds) {
|
|
4001
4254
|
if (closed) return;
|
|
@@ -4004,8 +4257,9 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
|
|
|
4004
4257
|
if (JSON.stringify(next) === JSON.stringify(metas.get(sessionId))) continue;
|
|
4005
4258
|
writeMeta(sessionId, next);
|
|
4006
4259
|
}
|
|
4007
|
-
|
|
4008
|
-
|
|
4260
|
+
})().catch(() => void 0).finally(() => {
|
|
4261
|
+
sweeping = false;
|
|
4262
|
+
});
|
|
4009
4263
|
}, META_REFRESH_MS);
|
|
4010
4264
|
timer.unref?.();
|
|
4011
4265
|
await new Promise((resolve) => {
|
|
@@ -4069,6 +4323,7 @@ const GIT_TIMEOUT_MS$3 = 5e3;
|
|
|
4069
4323
|
const GH_TIMEOUT_MS$1 = 8e3;
|
|
4070
4324
|
const CACHE_TTL_MS = 5e3;
|
|
4071
4325
|
const MAX_TEXT_CHARS = 1024;
|
|
4326
|
+
const MAX_CONFLICT_PATHS = 100;
|
|
4072
4327
|
function bounded(value) {
|
|
4073
4328
|
return value.trim().slice(0, MAX_TEXT_CHARS);
|
|
4074
4329
|
}
|
|
@@ -4106,6 +4361,7 @@ function parseGitStatus(output) {
|
|
|
4106
4361
|
let upstream = false;
|
|
4107
4362
|
let ahead;
|
|
4108
4363
|
let behind;
|
|
4364
|
+
const conflicts = [];
|
|
4109
4365
|
for (const line of output.split(/\r?\n/u)) {
|
|
4110
4366
|
if (line.startsWith("# branch.head ")) {
|
|
4111
4367
|
const head = bounded(line.slice(14));
|
|
@@ -4125,6 +4381,10 @@ function parseGitStatus(output) {
|
|
|
4125
4381
|
}
|
|
4126
4382
|
continue;
|
|
4127
4383
|
}
|
|
4384
|
+
if (line.startsWith("u ")) {
|
|
4385
|
+
const path = line.split(" ").slice(10).join(" ");
|
|
4386
|
+
if (path.length > 0 && conflicts.length < MAX_CONFLICT_PATHS) conflicts.push(bounded(path));
|
|
4387
|
+
}
|
|
4128
4388
|
if (line.length > 0 && !line.startsWith("# ")) dirty = true;
|
|
4129
4389
|
}
|
|
4130
4390
|
return {
|
|
@@ -4133,9 +4393,36 @@ function parseGitStatus(output) {
|
|
|
4133
4393
|
dirty,
|
|
4134
4394
|
upstream,
|
|
4135
4395
|
...ahead === void 0 ? {} : { ahead },
|
|
4136
|
-
...behind === void 0 ? {} : { behind }
|
|
4396
|
+
...behind === void 0 ? {} : { behind },
|
|
4397
|
+
...conflicts.length === 0 ? {} : { conflicts }
|
|
4137
4398
|
};
|
|
4138
4399
|
}
|
|
4400
|
+
/** Ordered so the rebase directories win: a conflicted rebase also writes
|
|
4401
|
+
* MERGE_HEAD-like state, and the two are resumed by different commands. */
|
|
4402
|
+
const OPERATION_MARKERS = [
|
|
4403
|
+
["rebase-merge", "rebase"],
|
|
4404
|
+
["rebase-apply", "rebase"],
|
|
4405
|
+
["MERGE_HEAD", "merge"],
|
|
4406
|
+
["CHERRY_PICK_HEAD", "cherry-pick"],
|
|
4407
|
+
["REVERT_HEAD", "revert"]
|
|
4408
|
+
];
|
|
4409
|
+
/** Reads the in-progress operation out of the worktree's own git dir -- linked
|
|
4410
|
+
* worktrees keep their own, so this must not be handed the common dir. */
|
|
4411
|
+
async function detectRepositoryOperation(gitDir) {
|
|
4412
|
+
for (const [marker, operation] of OPERATION_MARKERS) {
|
|
4413
|
+
const path = join(gitDir, marker);
|
|
4414
|
+
try {
|
|
4415
|
+
await stat(path);
|
|
4416
|
+
} catch {
|
|
4417
|
+
continue;
|
|
4418
|
+
}
|
|
4419
|
+
const branch = bounded(operation === "rebase" ? await readFile(join(path, "head-name"), "utf8").catch(() => "") : "").replace(/^refs\/heads\//u, "");
|
|
4420
|
+
return {
|
|
4421
|
+
operation,
|
|
4422
|
+
...branch.length === 0 || branch.includes(" ") ? {} : { branch }
|
|
4423
|
+
};
|
|
4424
|
+
}
|
|
4425
|
+
}
|
|
4139
4426
|
function parseDiffNumstat(value) {
|
|
4140
4427
|
let additions = 0;
|
|
4141
4428
|
let deletions = 0;
|
|
@@ -4342,13 +4629,15 @@ var RepositoryStatusService = class {
|
|
|
4342
4629
|
cwd: safeCwd
|
|
4343
4630
|
};
|
|
4344
4631
|
const status = parseGitStatus(statusResult.stdout);
|
|
4632
|
+
const operation = await detectRepositoryOperation(gitDir);
|
|
4633
|
+
const branch = operation?.branch ?? status.branch;
|
|
4345
4634
|
const remoteResult = await run(this.#runtime, git, [
|
|
4346
4635
|
"remote",
|
|
4347
4636
|
"get-url",
|
|
4348
4637
|
"origin"
|
|
4349
4638
|
], cwd, GIT_TIMEOUT_MS$3);
|
|
4350
4639
|
const remote = remoteResult.exitCode === 0 ? parseGitHubRemote(remoteResult.stdout) : void 0;
|
|
4351
|
-
const pullRequest =
|
|
4640
|
+
const pullRequest = branch === void 0 || remote === void 0 ? void 0 : await this.#pullRequest(cwd, remote, branch);
|
|
4352
4641
|
const diffBase = pullRequest?.baseBranch === void 0 ? "HEAD" : await this.#mergeBase(cwd, git, pullRequest.baseBranch) ?? "HEAD";
|
|
4353
4642
|
const diff = status.dirty || diffBase !== "HEAD" ? await this.#diff(cwd, git, diffBase, signal) : {
|
|
4354
4643
|
additions: 0,
|
|
@@ -4362,6 +4651,8 @@ var RepositoryStatusService = class {
|
|
|
4362
4651
|
cwd: safeCwd,
|
|
4363
4652
|
root,
|
|
4364
4653
|
...status,
|
|
4654
|
+
...branch === void 0 ? {} : { branch },
|
|
4655
|
+
...operation === void 0 ? {} : { operation: operation.operation },
|
|
4365
4656
|
worktree: normalizedPath(gitDir) !== normalizedPath(commonDir),
|
|
4366
4657
|
...remote === void 0 ? {} : { remote },
|
|
4367
4658
|
...pullRequest === void 0 ? {} : { pullRequest },
|
|
@@ -5237,6 +5528,10 @@ function parseRepositoryActionStatus(output) {
|
|
|
5237
5528
|
}
|
|
5238
5529
|
return [...files.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
5239
5530
|
}
|
|
5531
|
+
function conflictPaths(result) {
|
|
5532
|
+
if (result.exitCode !== 0 || result.lossy) return [];
|
|
5533
|
+
return result.stdout.split(/\r?\n/u).filter((line) => line.length > 0).slice(0, 100);
|
|
5534
|
+
}
|
|
5240
5535
|
function fallbackCommitMessage(files) {
|
|
5241
5536
|
if (files.length === 1) return `Update ${files[0]?.path ?? "repository files"}`;
|
|
5242
5537
|
return `Update ${files.length} repository files`;
|
|
@@ -5301,6 +5596,7 @@ var RepositoryActionService = class {
|
|
|
5301
5596
|
return operation;
|
|
5302
5597
|
}
|
|
5303
5598
|
async #execute(cwd, request) {
|
|
5599
|
+
if (request.action === "resolve-continue" || request.action === "resolve-abort") return this.#resolve(cwd, request.action, request.push === true);
|
|
5304
5600
|
const before = await this.#preview(cwd);
|
|
5305
5601
|
if (before.fingerprint !== request.fingerprint) throw new RepositoryActionError("repository-changed", "Repository changes have changed. Refresh the commit panel.");
|
|
5306
5602
|
if (request.action === "push") {
|
|
@@ -5363,13 +5659,12 @@ var RepositoryActionService = class {
|
|
|
5363
5659
|
`origin/${base}`
|
|
5364
5660
|
], before.root, REMOTE_TIMEOUT_MS);
|
|
5365
5661
|
if (merged.exitCode !== 0 || merged.lossy) {
|
|
5366
|
-
const
|
|
5662
|
+
const conflicts = conflictPaths(await this.#run(git, [
|
|
5367
5663
|
"diff",
|
|
5368
5664
|
"--name-only",
|
|
5369
5665
|
"--diff-filter=U",
|
|
5370
5666
|
"--"
|
|
5371
|
-
], before.root, GIT_TIMEOUT_MS$1);
|
|
5372
|
-
const conflicts = conflicted.exitCode === 0 && !conflicted.lossy ? conflicted.stdout.split(/\r?\n/u).filter((line) => line.length > 0).slice(0, 100) : [];
|
|
5667
|
+
], before.root, GIT_TIMEOUT_MS$1));
|
|
5373
5668
|
if (conflicts.length === 0) {
|
|
5374
5669
|
await this.#run(git, [method, "--abort"], before.root, GIT_TIMEOUT_MS$1).catch(() => void 0);
|
|
5375
5670
|
throw new RepositoryActionError("merge-failed", `Git could not ${method} the base branch.`);
|
|
@@ -5468,6 +5763,87 @@ var RepositoryActionService = class {
|
|
|
5468
5763
|
pullRequestUrl: url
|
|
5469
5764
|
};
|
|
5470
5765
|
}
|
|
5766
|
+
/** Finishes or discards the merge, rebase, cherry-pick or revert git is
|
|
5767
|
+
* waiting on. The operation is read from the git dir rather than taken from
|
|
5768
|
+
* the caller: `--continue` and `--abort` are only safe against the one that
|
|
5769
|
+
* is actually in progress. */
|
|
5770
|
+
async #resolve(cwd, action, push) {
|
|
5771
|
+
const git = await this.#git();
|
|
5772
|
+
const [rootValue, gitDirValue] = (await this.#mustRun(git, [
|
|
5773
|
+
"rev-parse",
|
|
5774
|
+
"--path-format=absolute",
|
|
5775
|
+
"--show-toplevel",
|
|
5776
|
+
"--absolute-git-dir"
|
|
5777
|
+
], cwd, GIT_TIMEOUT_MS$1, "not-repository", "The session directory is not a Git repository.")).stdout.split(/\r?\n/u);
|
|
5778
|
+
const root = (rootValue ?? "").trim();
|
|
5779
|
+
const gitDir = (gitDirValue ?? "").trim();
|
|
5780
|
+
if (root.length === 0 || gitDir.length === 0) throw new RepositoryActionError("repository-unavailable", "Repository state is unavailable.");
|
|
5781
|
+
const state = await detectRepositoryOperation(gitDir);
|
|
5782
|
+
if (state === void 0) throw new RepositoryActionError("no-operation", "No merge, rebase, cherry-pick or revert is in progress.");
|
|
5783
|
+
const operation = state.operation;
|
|
5784
|
+
if (action === "resolve-abort") {
|
|
5785
|
+
await this.#mustRun(git, [operation, "--abort"], root, GIT_TIMEOUT_MS$1, "abort-failed", `Git could not abort the ${operation}.`);
|
|
5786
|
+
this.#invalidate(root);
|
|
5787
|
+
return {
|
|
5788
|
+
commit: await this.#head(git, root),
|
|
5789
|
+
pushed: false
|
|
5790
|
+
};
|
|
5791
|
+
}
|
|
5792
|
+
if (conflictPaths(await this.#run(git, [
|
|
5793
|
+
"diff",
|
|
5794
|
+
"--name-only",
|
|
5795
|
+
"--diff-filter=U",
|
|
5796
|
+
"--"
|
|
5797
|
+
], root, GIT_TIMEOUT_MS$1)).length > 0) throw new RepositoryActionError("unresolved-conflicts", "Resolve and stage every conflicted file before continuing.");
|
|
5798
|
+
const continued = await this.#run(git, [
|
|
5799
|
+
"-c",
|
|
5800
|
+
"core.editor=true",
|
|
5801
|
+
operation,
|
|
5802
|
+
"--continue"
|
|
5803
|
+
], root, REMOTE_TIMEOUT_MS);
|
|
5804
|
+
this.#invalidate(root);
|
|
5805
|
+
if (continued.exitCode !== 0 || continued.lossy) {
|
|
5806
|
+
const next = conflictPaths(await this.#run(git, [
|
|
5807
|
+
"diff",
|
|
5808
|
+
"--name-only",
|
|
5809
|
+
"--diff-filter=U",
|
|
5810
|
+
"--"
|
|
5811
|
+
], root, GIT_TIMEOUT_MS$1));
|
|
5812
|
+
if (next.length > 0) return {
|
|
5813
|
+
commit: await this.#head(git, root),
|
|
5814
|
+
pushed: false,
|
|
5815
|
+
conflicts: next
|
|
5816
|
+
};
|
|
5817
|
+
const reason = continued.stderr.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0).at(-1);
|
|
5818
|
+
throw new RepositoryActionError("continue-failed", reason === void 0 || reason.length === 0 ? `Git could not continue the ${operation}.` : reason);
|
|
5819
|
+
}
|
|
5820
|
+
const head = await this.#head(git, root);
|
|
5821
|
+
if (!push) return {
|
|
5822
|
+
commit: head,
|
|
5823
|
+
pushed: false
|
|
5824
|
+
};
|
|
5825
|
+
const branch = await this.#run(git, [
|
|
5826
|
+
"symbolic-ref",
|
|
5827
|
+
"--quiet",
|
|
5828
|
+
"--short",
|
|
5829
|
+
"HEAD"
|
|
5830
|
+
], root, GIT_TIMEOUT_MS$1);
|
|
5831
|
+
if (branch.exitCode !== 0 || branch.lossy) throw new RepositoryActionError("detached-head", "The finished operation left a detached HEAD, so nothing was pushed.", head);
|
|
5832
|
+
try {
|
|
5833
|
+
await this.#push(git, root, branch.stdout.trim(), operation === "rebase");
|
|
5834
|
+
} catch (error) {
|
|
5835
|
+
throw new RepositoryActionError("push-failed", error instanceof Error ? error.message : "Git push failed.", head);
|
|
5836
|
+
}
|
|
5837
|
+
this.#invalidate(root);
|
|
5838
|
+
return {
|
|
5839
|
+
commit: head,
|
|
5840
|
+
pushed: true
|
|
5841
|
+
};
|
|
5842
|
+
}
|
|
5843
|
+
async #head(git, root) {
|
|
5844
|
+
const head = await this.#run(git, ["rev-parse", "HEAD"], root, GIT_TIMEOUT_MS$1);
|
|
5845
|
+
return head.exitCode === 0 && !head.lossy ? head.stdout.trim() : "";
|
|
5846
|
+
}
|
|
5471
5847
|
async #preview(cwd) {
|
|
5472
5848
|
const git = await this.#git();
|
|
5473
5849
|
const root = (await this.#mustRun(git, [
|
|
@@ -5753,7 +6129,17 @@ const ACTIONS = /* @__PURE__ */ new Set([
|
|
|
5753
6129
|
"push",
|
|
5754
6130
|
"create-pr",
|
|
5755
6131
|
"merge-pr",
|
|
5756
|
-
"update-branch"
|
|
6132
|
+
"update-branch",
|
|
6133
|
+
"resolve-continue",
|
|
6134
|
+
"resolve-abort"
|
|
6135
|
+
]);
|
|
6136
|
+
/** Actions that commit nothing of their own, so the panel sends no message. */
|
|
6137
|
+
const MESSAGELESS = /* @__PURE__ */ new Set([
|
|
6138
|
+
"push",
|
|
6139
|
+
"merge-pr",
|
|
6140
|
+
"update-branch",
|
|
6141
|
+
"resolve-continue",
|
|
6142
|
+
"resolve-abort"
|
|
5757
6143
|
]);
|
|
5758
6144
|
function record$9(value) {
|
|
5759
6145
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
@@ -5792,7 +6178,7 @@ function actionRequest(input) {
|
|
|
5792
6178
|
return {
|
|
5793
6179
|
action,
|
|
5794
6180
|
fingerprint: string$1(input, "fingerprint"),
|
|
5795
|
-
message: action
|
|
6181
|
+
message: MESSAGELESS.has(action) ? optionalString(input, "message") ?? "" : string$1(input, "message"),
|
|
5796
6182
|
includeUnstaged: input.includeUnstaged,
|
|
5797
6183
|
...optionalString(input, "prTitle") === void 0 ? {} : { prTitle: optionalString(input, "prTitle") },
|
|
5798
6184
|
...optionalString(input, "prBody") === void 0 ? {} : { prBody: optionalString(input, "prBody") },
|
|
@@ -5800,6 +6186,9 @@ function actionRequest(input) {
|
|
|
5800
6186
|
...input.draft === void 0 ? {} : typeof input.draft === "boolean" ? { draft: input.draft } : (() => {
|
|
5801
6187
|
throw new RepositoryActionError("invalid-request", "The draft field must be a boolean.");
|
|
5802
6188
|
})(),
|
|
6189
|
+
...input.push === void 0 ? {} : typeof input.push === "boolean" ? { push: input.push } : (() => {
|
|
6190
|
+
throw new RepositoryActionError("invalid-request", "The push field must be a boolean.");
|
|
6191
|
+
})(),
|
|
5803
6192
|
...input.mergeMethod === void 0 ? {} : input.mergeMethod === "merge" || input.mergeMethod === "squash" || input.mergeMethod === "rebase" ? { mergeMethod: input.mergeMethod } : (() => {
|
|
5804
6193
|
throw new RepositoryActionError("invalid-request", "The mergeMethod field is invalid.");
|
|
5805
6194
|
})()
|
|
@@ -6062,6 +6451,358 @@ function registerEditorOpenRoute(ctx, service, cwdForSession) {
|
|
|
6062
6451
|
});
|
|
6063
6452
|
}
|
|
6064
6453
|
//#endregion
|
|
6454
|
+
//#region src/prompts.ts
|
|
6455
|
+
const MAX_PROMPT_FILES = 256;
|
|
6456
|
+
const MAX_PROMPT_BYTES = 16384;
|
|
6457
|
+
const MAX_DESCRIPTION_CHARS = 120;
|
|
6458
|
+
const MAX_REQUEST_BYTES$1 = 32768;
|
|
6459
|
+
/**
|
|
6460
|
+
* The same shape as the output-style name in `global-settings.ts`, plus
|
|
6461
|
+
* `\p{M}` so a macOS-decomposed name survives. Two properties matter and both
|
|
6462
|
+
* are load-bearing: no member of the class is a path separator, and the first
|
|
6463
|
+
* character can be neither a dot nor a separator — so `join(dir, name + '.md')`
|
|
6464
|
+
* cannot address anything outside `dir`. Nothing else validates the path.
|
|
6465
|
+
*/
|
|
6466
|
+
const PROMPT_NAME = /^[\p{L}\p{N}][\p{L}\p{M}\p{N} ._()\[\]-]{0,127}$/u;
|
|
6467
|
+
/** Prompt snippets live beside the rest of the user's Claude Code state. */
|
|
6468
|
+
function claudePromptsDir() {
|
|
6469
|
+
return join(homedir(), ".claude", "prompts");
|
|
6470
|
+
}
|
|
6471
|
+
/** `~/.claude/prompts/x.md` rather than the absolute path it expands to. */
|
|
6472
|
+
function displayPath(file) {
|
|
6473
|
+
const home = homedir();
|
|
6474
|
+
return file.startsWith(`${home}/`) ? `~${file.slice(home.length)}` : file;
|
|
6475
|
+
}
|
|
6476
|
+
/** The menu's second row: the first non-empty line, collapsed and bounded. */
|
|
6477
|
+
function summarize(body) {
|
|
6478
|
+
const line = body.split("\n").map((text) => text.trim()).find((text) => text.length > 0) ?? "";
|
|
6479
|
+
return redactText(line.replace(/\s+/gu, " "), MAX_DESCRIPTION_CHARS);
|
|
6480
|
+
}
|
|
6481
|
+
/**
|
|
6482
|
+
* Every `.md` file in the prompts directory, sorted by name.
|
|
6483
|
+
*
|
|
6484
|
+
* A missing directory is the ordinary state before the user saves anything,
|
|
6485
|
+
* and one unreadable file must not empty the whole menu — both answer with
|
|
6486
|
+
* what is there rather than throwing.
|
|
6487
|
+
*/
|
|
6488
|
+
async function readClaudePrompts(directory) {
|
|
6489
|
+
const prompts = [];
|
|
6490
|
+
let entries;
|
|
6491
|
+
try {
|
|
6492
|
+
entries = await opendir(directory);
|
|
6493
|
+
} catch (error) {
|
|
6494
|
+
if (error.code === "ENOENT") return prompts;
|
|
6495
|
+
throw error;
|
|
6496
|
+
}
|
|
6497
|
+
for await (const entry of entries) {
|
|
6498
|
+
if (prompts.length >= MAX_PROMPT_FILES) break;
|
|
6499
|
+
if (!entry.isFile() || extname(entry.name).toLowerCase() !== ".md") continue;
|
|
6500
|
+
const name = entry.name.slice(0, -3);
|
|
6501
|
+
if (!PROMPT_NAME.test(name)) continue;
|
|
6502
|
+
try {
|
|
6503
|
+
const file = join(directory, entry.name);
|
|
6504
|
+
const body = await readFile(file, "utf8");
|
|
6505
|
+
if (body.trim().length === 0 || Buffer.byteLength(body) > MAX_PROMPT_BYTES) continue;
|
|
6506
|
+
prompts.push({
|
|
6507
|
+
name,
|
|
6508
|
+
description: summarize(body),
|
|
6509
|
+
body,
|
|
6510
|
+
location: displayPath(file)
|
|
6511
|
+
});
|
|
6512
|
+
} catch {}
|
|
6513
|
+
}
|
|
6514
|
+
return prompts.sort((left, right) => left.name.localeCompare(right.name));
|
|
6515
|
+
}
|
|
6516
|
+
var ClaudePromptWriteError = class extends Error {
|
|
6517
|
+
code;
|
|
6518
|
+
constructor(code, message) {
|
|
6519
|
+
super(message);
|
|
6520
|
+
this.name = "ClaudePromptWriteError";
|
|
6521
|
+
this.code = code;
|
|
6522
|
+
}
|
|
6523
|
+
};
|
|
6524
|
+
/**
|
|
6525
|
+
* Save one prompt, refusing to clobber an existing file.
|
|
6526
|
+
*
|
|
6527
|
+
* `wx` is the whole collision policy: a name the user already uses is theirs
|
|
6528
|
+
* to resolve, and overwriting a prompt they keep is not something they can
|
|
6529
|
+
* undo from inside DSH.
|
|
6530
|
+
*/
|
|
6531
|
+
async function writeClaudePrompt(directory, name, body) {
|
|
6532
|
+
if (typeof name !== "string" || !PROMPT_NAME.test(name)) throw new ClaudePromptWriteError("invalid-name", "The prompt name is invalid.");
|
|
6533
|
+
if (typeof body !== "string" || body.trim().length === 0 || Buffer.byteLength(body) > MAX_PROMPT_BYTES) throw new ClaudePromptWriteError("invalid-body", "The prompt text is empty or too large.");
|
|
6534
|
+
const text = body.endsWith("\n") ? body : `${body}\n`;
|
|
6535
|
+
const file = join(directory, `${name}.md`);
|
|
6536
|
+
await mkdir(directory, {
|
|
6537
|
+
recursive: true,
|
|
6538
|
+
mode: 448
|
|
6539
|
+
});
|
|
6540
|
+
try {
|
|
6541
|
+
await writeFile(file, text, {
|
|
6542
|
+
encoding: "utf8",
|
|
6543
|
+
mode: 384,
|
|
6544
|
+
flag: "wx"
|
|
6545
|
+
});
|
|
6546
|
+
} catch (error) {
|
|
6547
|
+
if (error.code === "EEXIST") throw new ClaudePromptWriteError("name-taken", "A prompt with that name already exists.");
|
|
6548
|
+
throw error;
|
|
6549
|
+
}
|
|
6550
|
+
return {
|
|
6551
|
+
name,
|
|
6552
|
+
description: summarize(text),
|
|
6553
|
+
body: text,
|
|
6554
|
+
location: displayPath(file)
|
|
6555
|
+
};
|
|
6556
|
+
}
|
|
6557
|
+
const MAX_NAME_DRAFT_CHARS = 4e3;
|
|
6558
|
+
const MAX_NAME_OUTPUT_BYTES = 4096;
|
|
6559
|
+
const MAX_SUGGESTED_NAME_CHARS = 48;
|
|
6560
|
+
const MAX_REFINE_DRAFT_CHARS = 16e3;
|
|
6561
|
+
const MAX_REFINED_BYTES = 32768;
|
|
6562
|
+
const ASSIST_TIMEOUT_MS = 4e4;
|
|
6563
|
+
/**
|
|
6564
|
+
* Ask for a file name and nothing else, with the draft fenced as data.
|
|
6565
|
+
*
|
|
6566
|
+
* Two things here were measured rather than guessed. The instruction travels
|
|
6567
|
+
* in the user turn, not in `--system-prompt`: a saved prompt is itself an
|
|
6568
|
+
* instruction and a system prompt does not outrank it, so the model reads the
|
|
6569
|
+
* draft as its task and answers it instead of naming it. And the default
|
|
6570
|
+
* system prompt is left in place even though it is large, because the CLI
|
|
6571
|
+
* sends it as a cache read (~6.5k cached tokens); replacing it with a short
|
|
6572
|
+
* one costs a fresh 3.5k-token prefill and measured slower end to end.
|
|
6573
|
+
*
|
|
6574
|
+
* The naming spec is this specific because a vaguer one ("describe what the
|
|
6575
|
+
* template does, at most 40 characters") produced names that were both
|
|
6576
|
+
* inconsistent in style and stripped of the detail that tells two similar
|
|
6577
|
+
* templates apart.
|
|
6578
|
+
*/
|
|
6579
|
+
function promptNamePrompt(draft) {
|
|
6580
|
+
return [
|
|
6581
|
+
"Below is a reusable prompt template a user is saving to a file. Name it.",
|
|
6582
|
+
"",
|
|
6583
|
+
`"""\n${draft.trim().slice(0, MAX_NAME_DRAFT_CHARS).replaceAll("\"\"\"", "\" \" \"")}\n"""`,
|
|
6584
|
+
"",
|
|
6585
|
+
"Answer with the file name alone: no quotes, no explanation, no extension, no leading dot, no slashes.",
|
|
6586
|
+
"Write it in English however the template is written, as lower-case words joined by hyphens.",
|
|
6587
|
+
"Name the task the template performs, keeping whatever detail distinguishes it from a",
|
|
6588
|
+
"similar template. At most 6 words."
|
|
6589
|
+
].join("\n");
|
|
6590
|
+
}
|
|
6591
|
+
/**
|
|
6592
|
+
* Rewrite the draft into something an agent can act on, with the draft fenced
|
|
6593
|
+
* as data for the same reason the naming prompt fences it.
|
|
6594
|
+
*
|
|
6595
|
+
* The rule about people and places is not politeness. Asked to rewrite
|
|
6596
|
+
* "后端 assign 给我", the model reached into Claude Code's ambient system
|
|
6597
|
+
* prompt and substituted the operator's actual email address — inventing a
|
|
6598
|
+
* detail the original never carried, and putting a personal address into a
|
|
6599
|
+
* message the user had not written it into. Naming the failure explicitly is
|
|
6600
|
+
* what stopped it; stripping the ambient context with `--system-prompt` also
|
|
6601
|
+
* stopped it but cost a fresh prefill and produced looser rewrites.
|
|
6602
|
+
*/
|
|
6603
|
+
function promptRefinePrompt(draft) {
|
|
6604
|
+
return [
|
|
6605
|
+
"Below is a prompt a user is about to send to a coding agent. Rewrite it so the agent can act on it without coming back with questions.",
|
|
6606
|
+
"",
|
|
6607
|
+
`"""\n${draft.trim().slice(0, MAX_REFINE_DRAFT_CHARS).replaceAll("\"\"\"", "\" \" \"")}\n"""`,
|
|
6608
|
+
"",
|
|
6609
|
+
"Keep the intent and every concrete detail exactly as given — names, paths, branches, identifiers, numbers.",
|
|
6610
|
+
"Leave every reference to a person or place as the original wrote it (\"me\", \"我\", \"the usual branch\");",
|
|
6611
|
+
"you do not know who or what they resolve to, and guessing puts a wrong name in the user's message.",
|
|
6612
|
+
"Do not invent requirements the original does not imply, do not answer the prompt, do not add a preamble or sign-off.",
|
|
6613
|
+
"Write the rewrite in the same language the original is written in.",
|
|
6614
|
+
"Reply with the rewritten prompt alone."
|
|
6615
|
+
].join("\n");
|
|
6616
|
+
}
|
|
6617
|
+
/** One turn, no tools, no MCP: neither of these tasks needs them, and both
|
|
6618
|
+
* cost seconds of cold start. Variadic `--tools` stays last. */
|
|
6619
|
+
function promptAssistArguments() {
|
|
6620
|
+
return [
|
|
6621
|
+
"-p",
|
|
6622
|
+
"--output-format",
|
|
6623
|
+
"text",
|
|
6624
|
+
"--model",
|
|
6625
|
+
"haiku",
|
|
6626
|
+
"--strict-mcp-config",
|
|
6627
|
+
"--mcp-config",
|
|
6628
|
+
"{\"mcpServers\":{}}",
|
|
6629
|
+
"--tools",
|
|
6630
|
+
""
|
|
6631
|
+
];
|
|
6632
|
+
}
|
|
6633
|
+
/** The rewrite in a model reply, or undefined when the reply is unusable.
|
|
6634
|
+
*
|
|
6635
|
+
* A model told to answer with the text alone still sometimes wraps it in a
|
|
6636
|
+
* markdown fence, so one is peeled off; anything else is the user's to read
|
|
6637
|
+
* and edit, and is passed through untouched. */
|
|
6638
|
+
function refinedPrompt(output) {
|
|
6639
|
+
const trimmed = output.trim();
|
|
6640
|
+
const text = (/^```[^\n]*\n([\s\S]*?)\n?```$/u.exec(trimmed)?.[1] ?? trimmed).trim();
|
|
6641
|
+
return text.length === 0 || Buffer.byteLength(text) > MAX_REFINED_BYTES ? void 0 : text;
|
|
6642
|
+
}
|
|
6643
|
+
/**
|
|
6644
|
+
* The usable name in a model reply, or undefined when there is none.
|
|
6645
|
+
*
|
|
6646
|
+
* A one-line answer is what the prompt asks for and usually what comes back,
|
|
6647
|
+
* but "usually" is not a contract: the reply is scrubbed to what a file name
|
|
6648
|
+
* may hold and then held to the same {@link PROMPT_NAME} guard the write path
|
|
6649
|
+
* uses, so a chatty or malformed answer is dropped rather than offered.
|
|
6650
|
+
*/
|
|
6651
|
+
function suggestedName(output) {
|
|
6652
|
+
const scrubbed = wordBounded((output.split("\n").map((text) => text.trim()).find((text) => text.length > 0) ?? "").replace(/^["'`]+|["'`]+$/gu, "").replace(/\.md$/iu, "").replace(/[^\p{L}\p{M}\p{N} ._()\[\]-]/gu, " ").replace(/\s+/gu, " ").trim());
|
|
6653
|
+
return PROMPT_NAME.test(scrubbed) ? scrubbed : void 0;
|
|
6654
|
+
}
|
|
6655
|
+
/** Cut a long name at its last word boundary. A name the user has to repair
|
|
6656
|
+
* ("analyze-frontend-backend-create-jira-tic") is worse than a shorter one. */
|
|
6657
|
+
function wordBounded(name) {
|
|
6658
|
+
if (name.length <= MAX_SUGGESTED_NAME_CHARS) return name;
|
|
6659
|
+
const cut = name.slice(0, MAX_SUGGESTED_NAME_CHARS);
|
|
6660
|
+
const boundary = cut.search(/[\s\-_][^\s\-_]*$/u);
|
|
6661
|
+
return (boundary > 0 ? cut.slice(0, boundary) : cut).trim();
|
|
6662
|
+
}
|
|
6663
|
+
/** Runs Claude Code's cheapest model over the draft: names it, or rewrites it. */
|
|
6664
|
+
var PromptAssistService = class {
|
|
6665
|
+
#runtime;
|
|
6666
|
+
#executablePath;
|
|
6667
|
+
constructor(runtime, executablePath) {
|
|
6668
|
+
this.#runtime = runtime;
|
|
6669
|
+
this.#executablePath = executablePath;
|
|
6670
|
+
}
|
|
6671
|
+
/** One bounded, tool-less turn; undefined whenever it cannot be had. */
|
|
6672
|
+
async #ask(prompt, maxOutputBytes, signal) {
|
|
6673
|
+
const executablePath = this.#executablePath();
|
|
6674
|
+
if (executablePath.length === 0) return void 0;
|
|
6675
|
+
const timeout = AbortSignal.timeout(ASSIST_TIMEOUT_MS);
|
|
6676
|
+
try {
|
|
6677
|
+
const handle = this.#runtime.spawn({
|
|
6678
|
+
argv: [executablePath, ...promptAssistArguments()],
|
|
6679
|
+
cwd: homedir(),
|
|
6680
|
+
stdio: {
|
|
6681
|
+
stdin: { data: prompt },
|
|
6682
|
+
stdout: { maxBytes: maxOutputBytes },
|
|
6683
|
+
stderr: { maxBytes: MAX_NAME_OUTPUT_BYTES }
|
|
6684
|
+
},
|
|
6685
|
+
graceMs: 1e3,
|
|
6686
|
+
signal: signal === void 0 ? timeout : AbortSignal.any([signal, timeout]),
|
|
6687
|
+
env: { MAX_THINKING_TOKENS: "0" }
|
|
6688
|
+
});
|
|
6689
|
+
if ((await handle.done).exitCode !== 0) return void 0;
|
|
6690
|
+
return handle.collected.stdout?.readFrom(0).text ?? "";
|
|
6691
|
+
} catch {
|
|
6692
|
+
return;
|
|
6693
|
+
}
|
|
6694
|
+
}
|
|
6695
|
+
/** The suggested file name, or undefined whenever one cannot be had. The
|
|
6696
|
+
* caller already has a name derived locally, so every failure here is a
|
|
6697
|
+
* non-event: nothing is reported, and nothing is retried. */
|
|
6698
|
+
async suggest(draft, signal) {
|
|
6699
|
+
if (draft.trim().length === 0) return void 0;
|
|
6700
|
+
const output = await this.#ask(promptNamePrompt(draft), MAX_NAME_OUTPUT_BYTES, signal);
|
|
6701
|
+
return output === void 0 ? void 0 : suggestedName(output);
|
|
6702
|
+
}
|
|
6703
|
+
/** The rewritten draft, or undefined when the rewrite could not be had.
|
|
6704
|
+
* Unlike naming, this failure is worth reporting: the user asked for it and
|
|
6705
|
+
* is waiting on it. */
|
|
6706
|
+
async refine(draft, signal) {
|
|
6707
|
+
if (draft.trim().length === 0) return void 0;
|
|
6708
|
+
const output = await this.#ask(promptRefinePrompt(draft), MAX_REFINED_BYTES, signal);
|
|
6709
|
+
return output === void 0 ? void 0 : refinedPrompt(output);
|
|
6710
|
+
}
|
|
6711
|
+
};
|
|
6712
|
+
/** List the user's prompt snippets, and save the composer draft as a new one. */
|
|
6713
|
+
function registerClaudePromptsRoute(ctx, directory = claudePromptsDir()) {
|
|
6714
|
+
registerPluginRoute(ctx, {
|
|
6715
|
+
mode: "unary",
|
|
6716
|
+
kind: "exact",
|
|
6717
|
+
path: CLAUDE_PROMPTS_PATH,
|
|
6718
|
+
methods: ["GET", "POST"],
|
|
6719
|
+
budget: "fast",
|
|
6720
|
+
handler: async (io) => {
|
|
6721
|
+
if (io.method === "GET") return {
|
|
6722
|
+
status: 200,
|
|
6723
|
+
value: { prompts: await readClaudePrompts(directory) }
|
|
6724
|
+
};
|
|
6725
|
+
const payload = await io.body(MAX_REQUEST_BYTES$1);
|
|
6726
|
+
try {
|
|
6727
|
+
return {
|
|
6728
|
+
status: 200,
|
|
6729
|
+
value: {
|
|
6730
|
+
saved: true,
|
|
6731
|
+
prompt: await writeClaudePrompt(directory, payload.name, payload.body)
|
|
6732
|
+
}
|
|
6733
|
+
};
|
|
6734
|
+
} catch (error) {
|
|
6735
|
+
if (error instanceof ClaudePromptWriteError) return {
|
|
6736
|
+
status: error.code === "name-taken" ? 409 : 400,
|
|
6737
|
+
value: {
|
|
6738
|
+
error: error.code,
|
|
6739
|
+
message: error.message
|
|
6740
|
+
}
|
|
6741
|
+
};
|
|
6742
|
+
return {
|
|
6743
|
+
status: 500,
|
|
6744
|
+
value: {
|
|
6745
|
+
error: "prompt-write-failed",
|
|
6746
|
+
message: "The prompt could not be saved."
|
|
6747
|
+
}
|
|
6748
|
+
};
|
|
6749
|
+
}
|
|
6750
|
+
}
|
|
6751
|
+
});
|
|
6752
|
+
}
|
|
6753
|
+
/** Suggest a file name for a draft. Answers `{}` when no name could be had:
|
|
6754
|
+
* the caller keeps the name it derived locally, so this is never an error. */
|
|
6755
|
+
function registerClaudePromptNameRoute(ctx, service) {
|
|
6756
|
+
registerPluginRoute(ctx, {
|
|
6757
|
+
mode: "unary",
|
|
6758
|
+
kind: "exact",
|
|
6759
|
+
path: CLAUDE_PROMPT_NAME_PATH,
|
|
6760
|
+
methods: ["POST"],
|
|
6761
|
+
budget: "git",
|
|
6762
|
+
handler: async (io) => {
|
|
6763
|
+
const payload = await io.body(MAX_REQUEST_BYTES$1);
|
|
6764
|
+
const draft = typeof payload.draft === "string" ? payload.draft : "";
|
|
6765
|
+
const name = await service.suggest(draft, io.signal);
|
|
6766
|
+
return {
|
|
6767
|
+
status: 200,
|
|
6768
|
+
value: name === void 0 ? {} : { name }
|
|
6769
|
+
};
|
|
6770
|
+
}
|
|
6771
|
+
});
|
|
6772
|
+
}
|
|
6773
|
+
/** Rewrite a draft into something an agent can act on. */
|
|
6774
|
+
function registerClaudePromptRefineRoute(ctx, service) {
|
|
6775
|
+
registerPluginRoute(ctx, {
|
|
6776
|
+
mode: "unary",
|
|
6777
|
+
kind: "exact",
|
|
6778
|
+
path: CLAUDE_PROMPT_REFINE_PATH,
|
|
6779
|
+
methods: ["POST"],
|
|
6780
|
+
budget: "git",
|
|
6781
|
+
handler: async (io) => {
|
|
6782
|
+
const payload = await io.body(MAX_REQUEST_BYTES$1);
|
|
6783
|
+
const draft = typeof payload.draft === "string" ? payload.draft : "";
|
|
6784
|
+
if (draft.trim().length === 0) return {
|
|
6785
|
+
status: 400,
|
|
6786
|
+
value: {
|
|
6787
|
+
error: "empty-draft",
|
|
6788
|
+
message: "There is nothing to rewrite."
|
|
6789
|
+
}
|
|
6790
|
+
};
|
|
6791
|
+
const text = await service.refine(draft, io.signal);
|
|
6792
|
+
return text === void 0 ? {
|
|
6793
|
+
status: 503,
|
|
6794
|
+
value: {
|
|
6795
|
+
error: "refine-unavailable",
|
|
6796
|
+
message: "Claude could not rewrite the prompt."
|
|
6797
|
+
}
|
|
6798
|
+
} : {
|
|
6799
|
+
status: 200,
|
|
6800
|
+
value: { text }
|
|
6801
|
+
};
|
|
6802
|
+
}
|
|
6803
|
+
});
|
|
6804
|
+
}
|
|
6805
|
+
//#endregion
|
|
6065
6806
|
//#region src/github-url.ts
|
|
6066
6807
|
/** Only GitHub's own image hosts; the browser loads these directly, so a URL
|
|
6067
6808
|
* the API did not vouch for must never become an outbound request. */
|
|
@@ -8158,12 +8899,12 @@ const ALERTS = {
|
|
|
8158
8899
|
function isBoundedInteger(value, min, max) {
|
|
8159
8900
|
return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max;
|
|
8160
8901
|
}
|
|
8161
|
-
function integerSetting(key, min, max, defaultFor) {
|
|
8902
|
+
function integerSetting(key, min, max, defaultFor, effect = "new-session") {
|
|
8162
8903
|
return {
|
|
8163
8904
|
key,
|
|
8164
8905
|
kind: "text",
|
|
8165
8906
|
document: "plugin",
|
|
8166
|
-
effect
|
|
8907
|
+
effect,
|
|
8167
8908
|
maxLength: String(max).length,
|
|
8168
8909
|
read(document, defaults = DEFAULT_LIMITS) {
|
|
8169
8910
|
const value = document[key];
|
|
@@ -8182,7 +8923,7 @@ const DESCRIPTORS = [
|
|
|
8182
8923
|
PROSE,
|
|
8183
8924
|
ALERTS,
|
|
8184
8925
|
WORKTREE_BRANCH_PREFIX,
|
|
8185
|
-
integerSetting("maxProcesses", 1, MAX_PROCESSES_LIMIT, (limits) => limits.maxProcesses),
|
|
8926
|
+
integerSetting("maxProcesses", 1, MAX_PROCESSES_LIMIT, (limits) => limits.maxProcesses, "immediate"),
|
|
8186
8927
|
integerSetting("idleTimeoutMinutes", 1, MAX_IDLE_TIMEOUT_MINUTES, (limits) => Math.max(1, Math.round(limits.idleTimeoutMs / 6e4)))
|
|
8187
8928
|
];
|
|
8188
8929
|
const DESCRIPTOR_BY_KEY = new Map(DESCRIPTORS.map((descriptor) => [descriptor.key, descriptor]));
|
|
@@ -8595,7 +9336,10 @@ async function apply(ctx, config) {
|
|
|
8595
9336
|
registerClaudeUpdateRoutes(webCtx, webCtx.subprocess, { ...typeof desktopActions?.requestRestart === "function" ? { requestRestart: desktopActions.requestRestart.bind(desktopActions) } : {} });
|
|
8596
9337
|
registerClaudeGlobalSettingsRoute(webCtx, {
|
|
8597
9338
|
defaultLimits,
|
|
8598
|
-
onUpdated:
|
|
9339
|
+
onUpdated: async () => {
|
|
9340
|
+
await applySettingsOverrides();
|
|
9341
|
+
supervisor.limitsChanged();
|
|
9342
|
+
}
|
|
8599
9343
|
});
|
|
8600
9344
|
registerRepositorySetupRoute(webCtx, repositorySetup, () => sweepWorktrees?.());
|
|
8601
9345
|
registerRepositoryStatusRoute(webCtx, repositoryStatus);
|
|
@@ -8609,6 +9353,10 @@ async function apply(ctx, config) {
|
|
|
8609
9353
|
};
|
|
8610
9354
|
registerRepositoryActionRoute(webCtx, repositoryActions, cwdForClaudeSession);
|
|
8611
9355
|
registerEditorOpenRoute(webCtx, new EditorOpenService(webCtx.subprocess), cwdForClaudeSession);
|
|
9356
|
+
registerClaudePromptsRoute(webCtx);
|
|
9357
|
+
const promptAssist = new PromptAssistService(webCtx.subprocess, () => supervisorConfig.executablePath);
|
|
9358
|
+
registerClaudePromptNameRoute(webCtx, promptAssist);
|
|
9359
|
+
registerClaudePromptRefineRoute(webCtx, promptAssist);
|
|
8612
9360
|
registerPullRequestFeedbackRoute(webCtx, new PullRequestFeedbackService(webCtx.subprocess), cwdForClaudeSession);
|
|
8613
9361
|
registerAskRoute(webCtx, new AskService(webCtx.subprocess, supervisorConfig.executablePath), cwdForClaudeSession, (sessionId) => {
|
|
8614
9362
|
const snapshot = supervisor.snapshots().find((item) => item.sessionId === sessionId);
|