@norman-else/dsh-claude 0.1.34 → 0.1.36
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 +196 -191
- package/lib/bin.mjs +1 -1
- package/lib/client.d.ts +13 -0
- package/lib/client.js +1991 -1174
- package/lib/client.js.map +1 -1
- package/lib/{events-DeSV0S1-.mjs → events-OhBoFNKO.mjs} +13 -2
- package/lib/events-OhBoFNKO.mjs.map +1 -0
- package/lib/index.d.mts +59 -2
- package/lib/index.mjs +1261 -483
- package/lib/index.mjs.map +1 -1
- package/lib/presenters-BBoM1Ju1.mjs +300 -0
- package/lib/presenters-BBoM1Ju1.mjs.map +1 -0
- package/lib/{preset-installer-BY4846KK.mjs → preset-installer-loenwnLS.mjs} +2 -2
- package/lib/{preset-installer-BY4846KK.mjs.map → preset-installer-loenwnLS.mjs.map} +1 -1
- package/lib/preset-route.mjs +2 -172
- package/lib/preset-route.mjs.map +1 -1
- package/package.json +189 -188
- package/lib/command-bridge-DXI6nWhB.mjs +0 -71
- package/lib/command-bridge-DXI6nWhB.mjs.map +0 -1
- package/lib/events-DeSV0S1-.mjs.map +0 -1
package/lib/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
import {
|
|
3
|
-
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-
|
|
1
|
+
import { A as CLAUDE_REWIND_PATH, C as CLAUDE_RENDER_MODES, D as CLAUDE_REPOSITORY_SETUP_PATH, E as CLAUDE_REPOSITORY_FILE_PATH, F as DEFAULT_CLAUDE_RENDER_MODE, L as TASK_TOOL_NAMES, M as CLAUDE_UPDATE_PATH, N as CLAUDE_USAGE_PATH, O as CLAUDE_REPOSITORY_STATUS_PATH, P as DEFAULT_CLAUDE_PROSE_MODE, R as isClaudeProseMode, S as CLAUDE_PROSE_MODES, T as CLAUDE_REPOSITORY_FEEDBACK_PATH, _ as CLAUDE_DOCTOR_PATH, a as latestClaudeTasks, b as CLAUDE_JIRA_PATH, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_ASK_PATH, g as CLAUDE_CODE_PROVIDER_IDS, h as CLAUDE_CODE_PROVIDER, i as latestClaudeSessionBinding, j as CLAUDE_UPDATE_CHECK_PATH, k as CLAUDE_REVIEW_COMMENT_PATH, l as redactText, m as CLAUDE_CODE_PRESET_ID, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_CLIENT_DIAGNOSTICS_PATH, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as CLAUDE_EDITOR_OPEN_PATH, w as CLAUDE_REPOSITORY_ACTION_PATH, x as CLAUDE_PROJECTION_PATH, y as CLAUDE_GLOBAL_SETTINGS_PATH, z as isClaudeRenderMode } from "./events-OhBoFNKO.mjs";
|
|
2
|
+
import { a as projectClaudeCommands, i as CLAUDE_COMMANDS_SERVICE, r as dynamicPresenterDefinition, t as CLAUDE_PRESENTER_NAMES } from "./presenters-BBoM1Ju1.mjs";
|
|
3
|
+
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-loenwnLS.mjs";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
6
|
import { chmod, mkdir, opendir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
@@ -8,10 +8,11 @@ import { basename, dirname, extname, isAbsolute, join, resolve, sep } from "node
|
|
|
8
8
|
import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
11
|
+
import { CallId, LlmAdapter, ReasoningEffortId, createToolResultMessage } from "@deepseek-ai/dsh-llm";
|
|
11
12
|
import { EventEmitter } from "node:events";
|
|
12
13
|
import { DSH_ENV_PREFIX, SENSITIVE_ENV_PATTERN } from "@deepseek-ai/dsh-subprocess";
|
|
13
|
-
import { LlmAdapter, ReasoningEffortId } from "@deepseek-ai/dsh-llm";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { deadline } from "@deepseek-ai/dsh-timeout";
|
|
15
16
|
import { StringDecoder } from "node:string_decoder";
|
|
16
17
|
//#region src/rewind.ts
|
|
17
18
|
const EMPTY_REWIND_STATE = {
|
|
@@ -227,6 +228,8 @@ var ClaudeSidecarRepository = class {
|
|
|
227
228
|
/** Latest durable projection per session; disk is read once and written through. */
|
|
228
229
|
#latest = /* @__PURE__ */ new Map();
|
|
229
230
|
#listeners = /* @__PURE__ */ new Map();
|
|
231
|
+
/** Notifications published per session, so a reader can spot a hole. */
|
|
232
|
+
#seq = /* @__PURE__ */ new Map();
|
|
230
233
|
/** Streaming transcript segments not yet persisted, keyed by activity key. */
|
|
231
234
|
#live = /* @__PURE__ */ new Map();
|
|
232
235
|
/** Monotonic revision boost so merged reads advance while text stays in memory. */
|
|
@@ -275,7 +278,8 @@ var ClaudeSidecarRepository = class {
|
|
|
275
278
|
const base = {
|
|
276
279
|
turn: normalized.turn,
|
|
277
280
|
step: normalized.step,
|
|
278
|
-
ordinal: normalized.ordinal
|
|
281
|
+
ordinal: normalized.ordinal,
|
|
282
|
+
...normalized.renderer === void 0 ? {} : { renderer: normalized.renderer }
|
|
279
283
|
};
|
|
280
284
|
this.#notify(sessionId, previous?.text !== void 0 && text.startsWith(previous.text) ? {
|
|
281
285
|
kind: "text",
|
|
@@ -297,11 +301,35 @@ var ClaudeSidecarRepository = class {
|
|
|
297
301
|
}
|
|
298
302
|
return this.#flushLive(sessionId);
|
|
299
303
|
}
|
|
304
|
+
/** How many notifications this session has published. */
|
|
305
|
+
sequence(sessionId) {
|
|
306
|
+
return this.#seq.get(sessionId) ?? 0;
|
|
307
|
+
}
|
|
308
|
+
/** Restate where the stream stands without changing anything.
|
|
309
|
+
*
|
|
310
|
+
* A reader detects a lost delta from the hole the NEXT one leaves, which
|
|
311
|
+
* never comes when the lost delta was the last of a turn -- exactly the
|
|
312
|
+
* case that leaves a finished tool group pulsing forever. Called at turn
|
|
313
|
+
* settlement, this gives that reader the one line it needs to disagree. */
|
|
314
|
+
checkpoint(sessionId) {
|
|
315
|
+
this.#deliver(sessionId, {
|
|
316
|
+
kind: "checkpoint",
|
|
317
|
+
seq: this.sequence(sessionId)
|
|
318
|
+
});
|
|
319
|
+
}
|
|
300
320
|
#notify(sessionId, delta) {
|
|
321
|
+
const seq = this.sequence(sessionId) + 1;
|
|
322
|
+
this.#seq.set(sessionId, seq);
|
|
323
|
+
this.#deliver(sessionId, {
|
|
324
|
+
...delta,
|
|
325
|
+
seq
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
#deliver(sessionId, notification) {
|
|
301
329
|
const set = this.#listeners.get(sessionId);
|
|
302
330
|
if (set === void 0) return;
|
|
303
331
|
for (const listener of [...set]) try {
|
|
304
|
-
listener(
|
|
332
|
+
listener(notification);
|
|
305
333
|
} catch {}
|
|
306
334
|
}
|
|
307
335
|
#merged(sessionId, base) {
|
|
@@ -1451,6 +1479,7 @@ var ClaudeSupervisor = class {
|
|
|
1451
1479
|
#queryFactory;
|
|
1452
1480
|
#runDetached;
|
|
1453
1481
|
#sidecar;
|
|
1482
|
+
#dynamicPresenterNames = /* @__PURE__ */ new WeakMap();
|
|
1454
1483
|
#contextWindows = /* @__PURE__ */ new Map();
|
|
1455
1484
|
#disposed = false;
|
|
1456
1485
|
#admissionGate = Promise.resolve();
|
|
@@ -1581,9 +1610,11 @@ var ClaudeSupervisor = class {
|
|
|
1581
1610
|
transcriptTextOrdinal: void 0,
|
|
1582
1611
|
thinking: "",
|
|
1583
1612
|
requestUsage: void 0,
|
|
1613
|
+
startedAt: Date.now(),
|
|
1614
|
+
firstOutputAt: void 0,
|
|
1584
1615
|
aborted: false,
|
|
1585
1616
|
deniedToolUseIds: /* @__PURE__ */ new Set(),
|
|
1586
|
-
|
|
1617
|
+
openCalls: /* @__PURE__ */ new Map(),
|
|
1587
1618
|
...request.signal === void 0 ? {} : { signal: request.signal }
|
|
1588
1619
|
};
|
|
1589
1620
|
entry.active = active;
|
|
@@ -1869,6 +1900,7 @@ var ClaudeSupervisor = class {
|
|
|
1869
1900
|
active.text += message.text;
|
|
1870
1901
|
active.transcriptText += message.text;
|
|
1871
1902
|
await this.#upsertTranscriptText(active);
|
|
1903
|
+
active.firstOutputAt ??= Date.now();
|
|
1872
1904
|
active.output.push({
|
|
1873
1905
|
type: "text-delta",
|
|
1874
1906
|
text: message.text
|
|
@@ -1881,6 +1913,7 @@ var ClaudeSupervisor = class {
|
|
|
1881
1913
|
active.text += message.text;
|
|
1882
1914
|
active.transcriptText += message.text;
|
|
1883
1915
|
await this.#upsertTranscriptText(active);
|
|
1916
|
+
active.firstOutputAt ??= Date.now();
|
|
1884
1917
|
active.output.push({
|
|
1885
1918
|
type: "text-delta",
|
|
1886
1919
|
text: message.text
|
|
@@ -1900,6 +1933,10 @@ var ClaudeSupervisor = class {
|
|
|
1900
1933
|
title: "Claude thinking",
|
|
1901
1934
|
summary: message.text
|
|
1902
1935
|
});
|
|
1936
|
+
if (this.#nativeRendering()) active.output.push({
|
|
1937
|
+
type: "thinking",
|
|
1938
|
+
text: message.text
|
|
1939
|
+
});
|
|
1903
1940
|
return;
|
|
1904
1941
|
case "tool-call":
|
|
1905
1942
|
if (message.parentToolUseId === void 0) this.#closeTranscriptTextSegment(active);
|
|
@@ -1913,9 +1950,16 @@ var ClaudeSupervisor = class {
|
|
|
1913
1950
|
summary: message.parentToolUseId === void 0 ? rootCallSummary(message.toolName, message.input) : `Subagent called ${message.toolName}`,
|
|
1914
1951
|
detail: message.input
|
|
1915
1952
|
});
|
|
1916
|
-
if (message.parentToolUseId === void 0)
|
|
1953
|
+
if (message.parentToolUseId === void 0) {
|
|
1954
|
+
active.openCalls.set(message.toolUseId, message.toolName);
|
|
1955
|
+
if (this.#nativeRendering()) {
|
|
1956
|
+
this.#ensureDynamicPresenter(active.agent, message.toolName);
|
|
1957
|
+
await this.#appendNativeToolCall(active, message);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1917
1960
|
return;
|
|
1918
1961
|
case "tool-result":
|
|
1962
|
+
active.openCalls.delete(message.toolUseId);
|
|
1919
1963
|
await this.#appendActivity(active, {
|
|
1920
1964
|
kind: message.parentToolUseId === void 0 ? "tool-result" : "subagent",
|
|
1921
1965
|
phase: message.isError ? "failed" : "completed",
|
|
@@ -1925,6 +1969,7 @@ var ClaudeSupervisor = class {
|
|
|
1925
1969
|
detail: message.output,
|
|
1926
1970
|
isError: message.isError
|
|
1927
1971
|
});
|
|
1972
|
+
if (message.parentToolUseId === void 0 && this.#nativeRendering()) await this.#appendNativeToolResult(active, message);
|
|
1928
1973
|
return;
|
|
1929
1974
|
case "subagent":
|
|
1930
1975
|
await this.#appendActivity(active, {
|
|
@@ -1974,9 +2019,77 @@ var ClaudeSupervisor = class {
|
|
|
1974
2019
|
title: message.toolName,
|
|
1975
2020
|
summary: message.summary
|
|
1976
2021
|
});
|
|
2022
|
+
if (this.#nativeRendering()) await this.#appendNativeToolResult(active, {
|
|
2023
|
+
kind: "tool-result",
|
|
2024
|
+
toolUseId: message.toolUseId,
|
|
2025
|
+
output: message.summary,
|
|
2026
|
+
isError: true
|
|
2027
|
+
});
|
|
1977
2028
|
return;
|
|
1978
2029
|
}
|
|
1979
2030
|
}
|
|
2031
|
+
/** The turn's accounting with its wall clock attached. The transcript draws
|
|
2032
|
+
* this line itself under the plugin renderer: activities carry no
|
|
2033
|
+
* timestamps, so a duration it did not measure is a duration nobody has. */
|
|
2034
|
+
#timedUsage(active, usage) {
|
|
2035
|
+
const now = Date.now();
|
|
2036
|
+
return {
|
|
2037
|
+
...usage,
|
|
2038
|
+
durationMs: Math.max(0, now - active.startedAt),
|
|
2039
|
+
...active.firstOutputAt === void 0 ? {} : { ttftMs: Math.max(0, active.firstOutputAt - active.startedAt) }
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
#nativeRendering() {
|
|
2043
|
+
return (this.#config.renderMode ?? "plugin") === "native";
|
|
2044
|
+
}
|
|
2045
|
+
/** Register one presenter-only mirror for a tool name the static preset
|
|
2046
|
+
* registry does not cover (MCP tools, newly added built-ins). Runs in the
|
|
2047
|
+
* agent scope so the mirror is visible only to this preset's sessions and
|
|
2048
|
+
* unwinds with the agent; failure keeps the generic card, never the turn. */
|
|
2049
|
+
#ensureDynamicPresenter(agent, name) {
|
|
2050
|
+
if (CLAUDE_PRESENTER_NAMES.has(name)) return;
|
|
2051
|
+
let known = this.#dynamicPresenterNames.get(agent);
|
|
2052
|
+
if (known === void 0) {
|
|
2053
|
+
known = /* @__PURE__ */ new Set();
|
|
2054
|
+
this.#dynamicPresenterNames.set(agent, known);
|
|
2055
|
+
}
|
|
2056
|
+
if (known.has(name)) return;
|
|
2057
|
+
try {
|
|
2058
|
+
agent.ctx.tools.register(dynamicPresenterDefinition(name));
|
|
2059
|
+
known.add(name);
|
|
2060
|
+
} catch {}
|
|
2061
|
+
}
|
|
2062
|
+
/** Mirror one root Claude tool call into the durable native tool channel so
|
|
2063
|
+
* the host's tool presentation renders it exactly like a DSH-executed call.
|
|
2064
|
+
* Presentation duplication is best-effort and never unsettles the turn. */
|
|
2065
|
+
async #appendNativeToolCall(active, message) {
|
|
2066
|
+
try {
|
|
2067
|
+
await active.agent.session.append("tool/call", {
|
|
2068
|
+
turn: active.cursor.turn,
|
|
2069
|
+
step: active.cursor.step,
|
|
2070
|
+
callId: CallId(message.toolUseId),
|
|
2071
|
+
name: message.toolName,
|
|
2072
|
+
arguments: safeDetail(message.input) ?? "{}"
|
|
2073
|
+
});
|
|
2074
|
+
} catch {}
|
|
2075
|
+
}
|
|
2076
|
+
async #appendNativeToolResult(active, message) {
|
|
2077
|
+
const text = typeof message.output === "string" ? redactText(message.output) : safeDetail(message.output) ?? "";
|
|
2078
|
+
try {
|
|
2079
|
+
await active.agent.session.append("tool/result", {
|
|
2080
|
+
turn: active.cursor.turn,
|
|
2081
|
+
step: active.cursor.step,
|
|
2082
|
+
message: createToolResultMessage({
|
|
2083
|
+
callId: CallId(message.toolUseId),
|
|
2084
|
+
content: [{
|
|
2085
|
+
type: "text",
|
|
2086
|
+
text
|
|
2087
|
+
}],
|
|
2088
|
+
isError: message.isError
|
|
2089
|
+
})
|
|
2090
|
+
}, { surfaceOp: "append" });
|
|
2091
|
+
} catch {}
|
|
2092
|
+
}
|
|
1980
2093
|
/** Merge one task lifecycle message into the session's task board. */
|
|
1981
2094
|
async #trackTask(entry, message, taskId, originTurn) {
|
|
1982
2095
|
const previous = entry.tasks.get(taskId);
|
|
@@ -2117,7 +2230,7 @@ var ClaudeSupervisor = class {
|
|
|
2117
2230
|
phase: "completed",
|
|
2118
2231
|
title: "Claude usage",
|
|
2119
2232
|
summary: usageSummary(result.usage),
|
|
2120
|
-
usage: result.usage
|
|
2233
|
+
usage: this.#timedUsage(active, result.usage)
|
|
2121
2234
|
});
|
|
2122
2235
|
active.output.push({
|
|
2123
2236
|
type: "usage",
|
|
@@ -2148,6 +2261,7 @@ var ClaudeSupervisor = class {
|
|
|
2148
2261
|
if (active.aborted) {
|
|
2149
2262
|
await this.#upsertTranscriptText(active);
|
|
2150
2263
|
await this.#flushTranscript(active);
|
|
2264
|
+
await this.#settleOpenCalls(active, "Cancelled with the turn");
|
|
2151
2265
|
await this.#appendSafely(active, {
|
|
2152
2266
|
kind: "status",
|
|
2153
2267
|
phase: "failed",
|
|
@@ -2157,6 +2271,7 @@ var ClaudeSupervisor = class {
|
|
|
2157
2271
|
entry.state = "idle";
|
|
2158
2272
|
entry.lastUsedAt = Date.now();
|
|
2159
2273
|
await this.#recordChainAnchor(entry, active);
|
|
2274
|
+
this.#checkpointProjection(entry);
|
|
2160
2275
|
this.#armIdleTimer(entry);
|
|
2161
2276
|
return;
|
|
2162
2277
|
}
|
|
@@ -2166,7 +2281,7 @@ var ClaudeSupervisor = class {
|
|
|
2166
2281
|
phase: "completed",
|
|
2167
2282
|
title: "Claude usage",
|
|
2168
2283
|
summary: usageSummary(result.usage),
|
|
2169
|
-
usage: result.usage
|
|
2284
|
+
usage: this.#timedUsage(active, result.usage)
|
|
2170
2285
|
});
|
|
2171
2286
|
active.output.push({
|
|
2172
2287
|
type: "usage",
|
|
@@ -2240,8 +2355,20 @@ var ClaudeSupervisor = class {
|
|
|
2240
2355
|
entry.lastUsedAt = Date.now();
|
|
2241
2356
|
await this.#recordChainAnchor(entry, active);
|
|
2242
2357
|
await this.#learnContextWindow(entry);
|
|
2358
|
+
this.#checkpointProjection(entry);
|
|
2243
2359
|
this.#armIdleTimer(entry);
|
|
2244
2360
|
}
|
|
2361
|
+
/** Tell every reader where this session's delta stream ended.
|
|
2362
|
+
*
|
|
2363
|
+
* A reader that lost the turn's last delta has nothing later to reveal the
|
|
2364
|
+
* hole, and a settled turn produces nothing further -- so a finished tool
|
|
2365
|
+
* group would keep pulsing until the session was reopened by hand. Last
|
|
2366
|
+
* line of the turn, best effort: presentation must never unsettle it. */
|
|
2367
|
+
#checkpointProjection(entry) {
|
|
2368
|
+
try {
|
|
2369
|
+
this.#sidecar.checkpoint(entry.sessionId);
|
|
2370
|
+
} catch {}
|
|
2371
|
+
}
|
|
2245
2372
|
/** Pin where Claude's chain ended for the DSH turn that just settled, so a
|
|
2246
2373
|
* later rewind of the following turn can fork exactly here. Best effort:
|
|
2247
2374
|
* a missing anchor only makes a rewind fall back to an earlier turn. */
|
|
@@ -2259,6 +2386,7 @@ var ClaudeSupervisor = class {
|
|
|
2259
2386
|
try {
|
|
2260
2387
|
this.#sidecar.appendTranscriptText(active.agent.id, {
|
|
2261
2388
|
text: active.transcriptText,
|
|
2389
|
+
...this.#nativeRendering() ? { renderer: "native" } : {},
|
|
2262
2390
|
turn: active.cursor.turn,
|
|
2263
2391
|
step: active.cursor.step,
|
|
2264
2392
|
ordinal
|
|
@@ -2277,6 +2405,7 @@ var ClaudeSupervisor = class {
|
|
|
2277
2405
|
const ordinal = active.cursor.nextOrdinal++;
|
|
2278
2406
|
await this.#sidecar.appendActivity(active.agent.id, {
|
|
2279
2407
|
...activity,
|
|
2408
|
+
...this.#nativeRendering() ? { renderer: "native" } : {},
|
|
2280
2409
|
turn: active.cursor.turn,
|
|
2281
2410
|
step: active.cursor.step,
|
|
2282
2411
|
ordinal
|
|
@@ -2296,6 +2425,33 @@ var ClaudeSupervisor = class {
|
|
|
2296
2425
|
this.#interruptions.set(entry.sessionId, interruption);
|
|
2297
2426
|
return interruption;
|
|
2298
2427
|
}
|
|
2428
|
+
/** Close out the root tool calls a turn is ending without answers for.
|
|
2429
|
+
*
|
|
2430
|
+
* A tool result is the only thing that ever settles a call, and a turn that
|
|
2431
|
+
* is cancelled or disconnected produces none: the transcript would keep
|
|
2432
|
+
* drawing those calls as running for as long as the session lives, and no
|
|
2433
|
+
* later event would ever correct it. Written as the failures they are. */
|
|
2434
|
+
async #settleOpenCalls(active, summary) {
|
|
2435
|
+
const open = [...active.openCalls];
|
|
2436
|
+
active.openCalls.clear();
|
|
2437
|
+
for (const [toolUseId, toolName] of open) {
|
|
2438
|
+
await this.#appendSafely(active, {
|
|
2439
|
+
kind: "tool-result",
|
|
2440
|
+
phase: "failed",
|
|
2441
|
+
toolUseId,
|
|
2442
|
+
toolName,
|
|
2443
|
+
title: "Tool cancelled",
|
|
2444
|
+
summary,
|
|
2445
|
+
isError: true
|
|
2446
|
+
});
|
|
2447
|
+
if (this.#nativeRendering()) await this.#appendNativeToolResult(active, {
|
|
2448
|
+
kind: "tool-result",
|
|
2449
|
+
toolUseId,
|
|
2450
|
+
output: summary,
|
|
2451
|
+
isError: true
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2299
2455
|
async #interrupt(entry) {
|
|
2300
2456
|
const active = entry.active;
|
|
2301
2457
|
if (active === void 0 || entry.state === "interrupting") return;
|
|
@@ -2309,6 +2465,7 @@ var ClaudeSupervisor = class {
|
|
|
2309
2465
|
} catch (error) {
|
|
2310
2466
|
interruptError = error;
|
|
2311
2467
|
}
|
|
2468
|
+
await this.#settleOpenCalls(active, "Cancelled with the turn").catch(() => void 0);
|
|
2312
2469
|
try {
|
|
2313
2470
|
await this.#appendActivity(active, {
|
|
2314
2471
|
kind: "status",
|
|
@@ -2330,6 +2487,7 @@ var ClaudeSupervisor = class {
|
|
|
2330
2487
|
const unknown = active.sawActivity;
|
|
2331
2488
|
entry.state = unknown ? "outcome-unknown" : "disconnected";
|
|
2332
2489
|
const failure = unknown ? new ClaudeOutcomeUnknownError(stderr === void 0 || stderr.length === 0 ? void 0 : `Claude Code exited after activity; outcome unknown. ${stderr}`) : new Error(stderr === void 0 || stderr.length === 0 ? errorSummary(error) : stderr);
|
|
2490
|
+
await this.#settleOpenCalls(active, "Claude Code stopped before the tool answered").catch(() => void 0);
|
|
2333
2491
|
await this.#appendSafely(active, {
|
|
2334
2492
|
kind: "error",
|
|
2335
2493
|
phase: "failed",
|
|
@@ -2640,13 +2798,15 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2640
2798
|
#attachments;
|
|
2641
2799
|
#presetIdFor;
|
|
2642
2800
|
#drainReviewComments;
|
|
2643
|
-
|
|
2801
|
+
#renderMode;
|
|
2802
|
+
constructor(supervisor, agents, attachments, presetIdFor, drainReviewComments = () => [], renderMode = () => DEFAULT_CLAUDE_RENDER_MODE) {
|
|
2644
2803
|
super();
|
|
2645
2804
|
this.#supervisor = supervisor;
|
|
2646
2805
|
this.#agents = agents;
|
|
2647
2806
|
this.#attachments = attachments;
|
|
2648
2807
|
this.#presetIdFor = presetIdFor;
|
|
2649
2808
|
this.#drainReviewComments = drainReviewComments;
|
|
2809
|
+
this.#renderMode = renderMode;
|
|
2650
2810
|
}
|
|
2651
2811
|
providerInfo(provider) {
|
|
2652
2812
|
return {
|
|
@@ -2718,15 +2878,74 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2718
2878
|
...thinkingMode === void 0 ? {} : { thinkingMode },
|
|
2719
2879
|
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
2720
2880
|
});
|
|
2881
|
+
const native = this.#renderMode() === "native";
|
|
2721
2882
|
let pendingUsage;
|
|
2722
2883
|
let completed = false;
|
|
2884
|
+
let blockIndex = 0;
|
|
2885
|
+
let text = "";
|
|
2886
|
+
/** Settle the buffered prose as one text block. Each Claude result is one
|
|
2887
|
+
* block so tool activity stays ahead of the prose it explains and a
|
|
2888
|
+
* background-task report can follow in a block of its own. */
|
|
2889
|
+
function* flushText() {
|
|
2890
|
+
if (text.length === 0) return;
|
|
2891
|
+
const settled = text;
|
|
2892
|
+
text = "";
|
|
2893
|
+
yield {
|
|
2894
|
+
type: "block-start",
|
|
2895
|
+
index: blockIndex,
|
|
2896
|
+
blockType: "text"
|
|
2897
|
+
};
|
|
2898
|
+
yield {
|
|
2899
|
+
type: "text-delta",
|
|
2900
|
+
index: blockIndex,
|
|
2901
|
+
text: settled
|
|
2902
|
+
};
|
|
2903
|
+
yield {
|
|
2904
|
+
type: "block-end",
|
|
2905
|
+
index: blockIndex,
|
|
2906
|
+
block: {
|
|
2907
|
+
type: "text",
|
|
2908
|
+
text: settled
|
|
2909
|
+
}
|
|
2910
|
+
};
|
|
2911
|
+
blockIndex += 1;
|
|
2912
|
+
}
|
|
2723
2913
|
try {
|
|
2724
2914
|
for await (const event of events) {
|
|
2725
|
-
if (event.type === "text-delta" || event.type === "segment-complete") continue;
|
|
2726
2915
|
if (event.type === "usage") {
|
|
2727
2916
|
pendingUsage = tokenUsage(event.usage);
|
|
2728
2917
|
continue;
|
|
2729
2918
|
}
|
|
2919
|
+
if (event.type === "text-delta") {
|
|
2920
|
+
if (native) text += event.text;
|
|
2921
|
+
continue;
|
|
2922
|
+
}
|
|
2923
|
+
if (event.type === "thinking") {
|
|
2924
|
+
if (!native) continue;
|
|
2925
|
+
yield* flushText();
|
|
2926
|
+
yield {
|
|
2927
|
+
type: "block-start",
|
|
2928
|
+
index: blockIndex,
|
|
2929
|
+
blockType: "reasoning"
|
|
2930
|
+
};
|
|
2931
|
+
yield {
|
|
2932
|
+
type: "reasoning-delta",
|
|
2933
|
+
index: blockIndex,
|
|
2934
|
+
text: event.text
|
|
2935
|
+
};
|
|
2936
|
+
yield {
|
|
2937
|
+
type: "block-end",
|
|
2938
|
+
index: blockIndex,
|
|
2939
|
+
block: {
|
|
2940
|
+
type: "reasoning",
|
|
2941
|
+
text: event.text
|
|
2942
|
+
}
|
|
2943
|
+
};
|
|
2944
|
+
blockIndex += 1;
|
|
2945
|
+
continue;
|
|
2946
|
+
}
|
|
2947
|
+
yield* flushText();
|
|
2948
|
+
if (event.type === "segment-complete") continue;
|
|
2730
2949
|
completed = true;
|
|
2731
2950
|
if (pendingUsage !== void 0) yield {
|
|
2732
2951
|
type: "usage",
|
|
@@ -2740,6 +2959,7 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2740
2959
|
} catch (error) {
|
|
2741
2960
|
if (error.name === "AbortError") {
|
|
2742
2961
|
completed = true;
|
|
2962
|
+
yield* flushText();
|
|
2743
2963
|
yield {
|
|
2744
2964
|
type: "finish",
|
|
2745
2965
|
reason: {
|
|
@@ -2757,10 +2977,41 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2757
2977
|
if (!completed) throw new Error("dsh-claude: Claude turn stream ended without a result");
|
|
2758
2978
|
}
|
|
2759
2979
|
};
|
|
2760
|
-
function createClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments = () => []) {
|
|
2761
|
-
return new ClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments);
|
|
2980
|
+
function createClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments = () => [], renderMode = () => DEFAULT_CLAUDE_RENDER_MODE) {
|
|
2981
|
+
return new ClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments, renderMode);
|
|
2762
2982
|
}
|
|
2763
2983
|
//#endregion
|
|
2984
|
+
//#region src/plugin-budget.ts
|
|
2985
|
+
/**
|
|
2986
|
+
* The plugin's connection budget, in one table.
|
|
2987
|
+
*
|
|
2988
|
+
* A browser opens a small fixed number of connections to one origin — six for
|
|
2989
|
+
* HTTP/1.1 in Chromium — and shares them with the Host's own traffic. Every
|
|
2990
|
+
* response this plugin holds open costs one of them for its lifetime, and a
|
|
2991
|
+
* request that cannot get one waits in the browser's queue where no server-side
|
|
2992
|
+
* deadline can reach it. When the pool is exhausted the panels that would
|
|
2993
|
+
* *diagnose* the problem are the first thing to stop answering, which is how
|
|
2994
|
+
* this failure has always presented: four settings cards timing out at once
|
|
2995
|
+
* against a Host that is demonstrably healthy.
|
|
2996
|
+
*
|
|
2997
|
+
* So the budget is a fixed constant rather than a function of how much work is
|
|
2998
|
+
* in flight. Steady state is one connection (the multiplexed projection
|
|
2999
|
+
* carrier); the peak is `PLUGIN_GLOBAL_PERMITS`, whatever the session count.
|
|
3000
|
+
*
|
|
3001
|
+
* Both halves read this file, which is the point: a route declares a budget
|
|
3002
|
+
* class and the client derives its wait from the same entry, so a server
|
|
3003
|
+
* deadline can never be quietly longer than the client's patience.
|
|
3004
|
+
*/
|
|
3005
|
+
/** Server-side budget classes. A route declares a class, never a number. */
|
|
3006
|
+
const ROUTE_BUDGET_MS = {
|
|
3007
|
+
/** Answers from memory or a single bounded probe. */
|
|
3008
|
+
fast: 5e3,
|
|
3009
|
+
/** Chains local Git work. */
|
|
3010
|
+
git: 45e3,
|
|
3011
|
+
/** Reaches the network: remote Git, `gh`, the npm registry. */
|
|
3012
|
+
remote: 15e4
|
|
3013
|
+
};
|
|
3014
|
+
//#endregion
|
|
2764
3015
|
//#region src/http.ts
|
|
2765
3016
|
/** Accept only loopback, same-origin browser requests to plugin-private routes. */
|
|
2766
3017
|
function trustedRequest(req) {
|
|
@@ -2789,6 +3040,147 @@ function json(res, status, value) {
|
|
|
2789
3040
|
});
|
|
2790
3041
|
res.end(JSON.stringify(value));
|
|
2791
3042
|
}
|
|
3043
|
+
const ROUTE_TIMEOUT_CODE = "DSH_CLAUDE_ROUTE";
|
|
3044
|
+
const DEFAULT_BODY_BYTES = 1048576;
|
|
3045
|
+
/** Bounds live streaming responses per path, so a teardown bug cannot become a
|
|
3046
|
+
* permanent connection leak. Registration is per path, not global, because a
|
|
3047
|
+
* wedged projection stream must not evict an in-flight repository setup. */
|
|
3048
|
+
var StreamRegistry = class {
|
|
3049
|
+
#open = /* @__PURE__ */ new Map();
|
|
3050
|
+
admit(path, key, max, close) {
|
|
3051
|
+
let live = this.#open.get(path);
|
|
3052
|
+
if (live === void 0) {
|
|
3053
|
+
live = /* @__PURE__ */ new Map();
|
|
3054
|
+
this.#open.set(path, live);
|
|
3055
|
+
}
|
|
3056
|
+
live.get(key)?.();
|
|
3057
|
+
live.set(key, close);
|
|
3058
|
+
while (live.size > max) {
|
|
3059
|
+
const oldest = live.keys().next();
|
|
3060
|
+
if (oldest.done === true) break;
|
|
3061
|
+
const evict = live.get(oldest.value);
|
|
3062
|
+
live.delete(oldest.value);
|
|
3063
|
+
evict?.();
|
|
3064
|
+
}
|
|
3065
|
+
return () => {
|
|
3066
|
+
const current = this.#open.get(path);
|
|
3067
|
+
if (current?.get(key) === close) current.delete(key);
|
|
3068
|
+
};
|
|
3069
|
+
}
|
|
3070
|
+
};
|
|
3071
|
+
const streams = new StreamRegistry();
|
|
3072
|
+
function isPluginMethod(value) {
|
|
3073
|
+
return value === "GET" || value === "POST" || value === "PATCH" || value === "DELETE";
|
|
3074
|
+
}
|
|
3075
|
+
/** Distinguishable so the wrapper can answer 413 rather than folding an
|
|
3076
|
+
* oversized body into whatever generic failure the handler reports. */
|
|
3077
|
+
var PluginBodyTooLargeError = class extends Error {
|
|
3078
|
+
constructor() {
|
|
3079
|
+
super("Request body is too large");
|
|
3080
|
+
this.name = "PluginBodyTooLargeError";
|
|
3081
|
+
}
|
|
3082
|
+
};
|
|
3083
|
+
async function readBody(req, maxBytes) {
|
|
3084
|
+
const declared = Number(req.headers["content-length"] ?? 0);
|
|
3085
|
+
if (!Number.isFinite(declared) || declared < 0 || declared > maxBytes) throw new PluginBodyTooLargeError();
|
|
3086
|
+
const chunks = [];
|
|
3087
|
+
let bytes = 0;
|
|
3088
|
+
for await (const chunk of req) {
|
|
3089
|
+
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
3090
|
+
bytes += data.byteLength;
|
|
3091
|
+
if (bytes > maxBytes) throw new PluginBodyTooLargeError();
|
|
3092
|
+
chunks.push(data);
|
|
3093
|
+
}
|
|
3094
|
+
if (bytes === 0) return {};
|
|
3095
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
3096
|
+
}
|
|
3097
|
+
function rejectOn(signal) {
|
|
3098
|
+
return new Promise((_resolve, reject) => {
|
|
3099
|
+
if (signal.aborted) {
|
|
3100
|
+
reject(signal.reason);
|
|
3101
|
+
return;
|
|
3102
|
+
}
|
|
3103
|
+
signal.addEventListener("abort", () => reject(signal.reason), { once: true });
|
|
3104
|
+
});
|
|
3105
|
+
}
|
|
3106
|
+
/**
|
|
3107
|
+
* Register one plugin route.
|
|
3108
|
+
*
|
|
3109
|
+
* This is the only place in the package that calls `ctx.webServer.register`,
|
|
3110
|
+
* and the ordering inside it is the fix rather than an implementation detail:
|
|
3111
|
+
* the disconnect listeners are attached before the first `await`, so a client
|
|
3112
|
+
* that goes away while the handler is still assembling its first response
|
|
3113
|
+
* still tears the route's work down. The previous per-route code attached
|
|
3114
|
+
* `res.on('close')` after awaiting an unbounded repository probe, which is how
|
|
3115
|
+
* the projection stream reached 53 opens against 33 closes.
|
|
3116
|
+
*/
|
|
3117
|
+
function registerPluginRoute(ctx, route) {
|
|
3118
|
+
const label = `dsh-claude: ${route.path}`;
|
|
3119
|
+
ctx.effect(() => ctx.webServer.register({
|
|
3120
|
+
kind: route.kind,
|
|
3121
|
+
path: route.path,
|
|
3122
|
+
handler: async (req, res) => {
|
|
3123
|
+
const method = req.method;
|
|
3124
|
+
if (!isPluginMethod(method) || !route.methods.includes(method)) return json(res, 405, { error: "method not allowed" });
|
|
3125
|
+
if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
|
|
3126
|
+
const aborted = new AbortController();
|
|
3127
|
+
const abort = () => {
|
|
3128
|
+
aborted.abort();
|
|
3129
|
+
};
|
|
3130
|
+
req.on("close", () => {
|
|
3131
|
+
if (!req.complete) abort();
|
|
3132
|
+
});
|
|
3133
|
+
res.on("close", abort);
|
|
3134
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
3135
|
+
if (route.mode === "stream") {
|
|
3136
|
+
const release = streams.admit(route.path, route.streamKey(url), route.maxConcurrent, abort);
|
|
3137
|
+
const io = {
|
|
3138
|
+
signal: aborted.signal,
|
|
3139
|
+
method,
|
|
3140
|
+
url,
|
|
3141
|
+
body: async (maxBytes = DEFAULT_BODY_BYTES) => await readBody(req, maxBytes)
|
|
3142
|
+
};
|
|
3143
|
+
try {
|
|
3144
|
+
await route.handler(res, io);
|
|
3145
|
+
} catch (error) {
|
|
3146
|
+
if (!res.headersSent) json(res, 500, { error: "stream-failed" });
|
|
3147
|
+
ctx.logger.warn(`dsh-claude: ${route.path} stream failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3148
|
+
} finally {
|
|
3149
|
+
release();
|
|
3150
|
+
res.end();
|
|
3151
|
+
}
|
|
3152
|
+
return;
|
|
3153
|
+
}
|
|
3154
|
+
const budgetMs = ROUTE_BUDGET_MS[route.budget];
|
|
3155
|
+
const bounded = deadline(aborted.signal, budgetMs, ROUTE_TIMEOUT_CODE);
|
|
3156
|
+
const io = {
|
|
3157
|
+
signal: bounded.signal,
|
|
3158
|
+
method,
|
|
3159
|
+
url,
|
|
3160
|
+
body: async (maxBytes = DEFAULT_BODY_BYTES) => await readBody(req, maxBytes)
|
|
3161
|
+
};
|
|
3162
|
+
try {
|
|
3163
|
+
const result = await Promise.race([route.handler(io), rejectOn(bounded.signal)]);
|
|
3164
|
+
if (!res.writableEnded) json(res, result.status, result.value);
|
|
3165
|
+
} catch (error) {
|
|
3166
|
+
if (res.writableEnded || res.headersSent) return;
|
|
3167
|
+
if (bounded.signal.aborted && !aborted.signal.aborted) return json(res, 504, {
|
|
3168
|
+
error: "deadline",
|
|
3169
|
+
budget: route.budget,
|
|
3170
|
+
ms: budgetMs
|
|
3171
|
+
});
|
|
3172
|
+
if (aborted.signal.aborted) return;
|
|
3173
|
+
if (error instanceof PluginBodyTooLargeError) return json(res, 413, {
|
|
3174
|
+
error: "body-too-large",
|
|
3175
|
+
message: "The request body is too large."
|
|
3176
|
+
});
|
|
3177
|
+
json(res, 400, { error: error instanceof Error ? error.message : "request failed" });
|
|
3178
|
+
} finally {
|
|
3179
|
+
bounded[Symbol.dispose]();
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
}), label);
|
|
3183
|
+
}
|
|
2792
3184
|
//#endregion
|
|
2793
3185
|
//#region src/doctor-routes.ts
|
|
2794
3186
|
const CLAUDE_DOCTOR_PROBE_TIMEOUT_MS = 15e3;
|
|
@@ -2828,60 +3220,70 @@ function commandDiagnostics(ctx) {
|
|
|
2828
3220
|
}
|
|
2829
3221
|
}
|
|
2830
3222
|
function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolutionError) {
|
|
2831
|
-
ctx
|
|
3223
|
+
registerPluginRoute(ctx, {
|
|
3224
|
+
mode: "unary",
|
|
2832
3225
|
kind: "exact",
|
|
2833
3226
|
path: CLAUDE_DOCTOR_PATH,
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
3227
|
+
methods: ["GET"],
|
|
3228
|
+
budget: "git",
|
|
3229
|
+
handler: async (io) => {
|
|
2837
3230
|
try {
|
|
2838
|
-
if (resolutionError !== void 0) return
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
"
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
3231
|
+
if (resolutionError !== void 0) return {
|
|
3232
|
+
status: 200,
|
|
3233
|
+
value: {
|
|
3234
|
+
executable: {
|
|
3235
|
+
status: "missing",
|
|
3236
|
+
searched: config.executablePath.length > 0 ? [config.executablePath] : [
|
|
3237
|
+
"claude",
|
|
3238
|
+
"~/.local/bin/claude",
|
|
3239
|
+
"/opt/homebrew/bin/claude",
|
|
3240
|
+
"/usr/local/bin/claude"
|
|
3241
|
+
]
|
|
3242
|
+
},
|
|
3243
|
+
version: { status: "not-run" },
|
|
3244
|
+
authentication: { status: "not-run" },
|
|
3245
|
+
handshake: "not-run",
|
|
3246
|
+
message: safeMessage$2(resolutionError),
|
|
3247
|
+
limits: {
|
|
3248
|
+
idleTimeoutMs: config.idleTimeoutMs,
|
|
3249
|
+
maxProcesses: config.maxProcesses
|
|
3250
|
+
},
|
|
3251
|
+
processes: {
|
|
3252
|
+
count: 0,
|
|
3253
|
+
active: 0
|
|
3254
|
+
}
|
|
2859
3255
|
}
|
|
2860
|
-
}
|
|
3256
|
+
};
|
|
2861
3257
|
const report = await runClaudeDoctor(runtime, {
|
|
2862
3258
|
configuredPath: config.executablePath,
|
|
2863
3259
|
cwd: process.cwd(),
|
|
2864
|
-
signal: AbortSignal.timeout(CLAUDE_DOCTOR_PROBE_TIMEOUT_MS)
|
|
3260
|
+
signal: AbortSignal.any([io.signal, AbortSignal.timeout(CLAUDE_DOCTOR_PROBE_TIMEOUT_MS)])
|
|
2865
3261
|
});
|
|
2866
3262
|
const processes = supervisor.snapshots();
|
|
2867
3263
|
if (processes.some((process) => process.claudeSessionId !== void 0)) report.handshake = "ok";
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
3264
|
+
return {
|
|
3265
|
+
status: 200,
|
|
3266
|
+
value: {
|
|
3267
|
+
...report,
|
|
3268
|
+
limits: {
|
|
3269
|
+
idleTimeoutMs: config.idleTimeoutMs,
|
|
3270
|
+
maxProcesses: config.maxProcesses
|
|
3271
|
+
},
|
|
3272
|
+
processes: {
|
|
3273
|
+
count: processes.length,
|
|
3274
|
+
active: processes.filter((process) => process.state === "running" || process.state === "starting").length
|
|
3275
|
+
},
|
|
3276
|
+
commandBridge: commandDiagnostics(ctx)
|
|
3277
|
+
}
|
|
3278
|
+
};
|
|
2880
3279
|
} catch (error) {
|
|
2881
|
-
|
|
3280
|
+
return {
|
|
3281
|
+
status: 500,
|
|
3282
|
+
value: { error: safeMessage$2(error) }
|
|
3283
|
+
};
|
|
2882
3284
|
}
|
|
2883
3285
|
}
|
|
2884
|
-
})
|
|
3286
|
+
});
|
|
2885
3287
|
}
|
|
2886
3288
|
//#endregion
|
|
2887
3289
|
//#region src/projection-routes.ts
|
|
@@ -2889,21 +3291,34 @@ const MAX_SESSION_ID_CHARS$6 = 1024;
|
|
|
2889
3291
|
/** Slow-moving metadata refresh and stream heartbeat cadence; deliberately off
|
|
2890
3292
|
* the transcript hot path so git/gh latency never delays visible text. */
|
|
2891
3293
|
const META_REFRESH_MS = 5e3;
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
3294
|
+
/** The multiplexed carrier's path segment. Session ids are `session-<uuid>`,
|
|
3295
|
+
* so this cannot collide with one. */
|
|
3296
|
+
const MULTI_SEGMENT = "multi";
|
|
3297
|
+
function validSessionId(value) {
|
|
3298
|
+
return value.length > 0 && value.length <= MAX_SESSION_ID_CHARS$6;
|
|
3299
|
+
}
|
|
3300
|
+
function targetFromUrl(url) {
|
|
3301
|
+
const prefix = `${CLAUDE_PROJECTION_PATH}/`;
|
|
3302
|
+
if (!url.pathname.startsWith(prefix)) return void 0;
|
|
3303
|
+
const encoded = url.pathname.slice(prefix.length);
|
|
3304
|
+
if (encoded.length === 0 || encoded.includes("/")) return void 0;
|
|
3305
|
+
if (encoded === MULTI_SEGMENT) {
|
|
3306
|
+
const raw = url.searchParams.get("sessions");
|
|
3307
|
+
if (raw === null) return void 0;
|
|
3308
|
+
const sessionIds = [...new Set(raw.split(",").filter((id) => id.length > 0))];
|
|
3309
|
+
if (sessionIds.length === 0 || sessionIds.length > 16) return void 0;
|
|
3310
|
+
if (!sessionIds.every(validSessionId)) return void 0;
|
|
2903
3311
|
return {
|
|
2904
|
-
|
|
2905
|
-
|
|
3312
|
+
kind: "multi",
|
|
3313
|
+
sessionIds
|
|
2906
3314
|
};
|
|
3315
|
+
}
|
|
3316
|
+
try {
|
|
3317
|
+
const sessionId = decodeURIComponent(encoded);
|
|
3318
|
+
return validSessionId(sessionId) ? {
|
|
3319
|
+
kind: "snapshot",
|
|
3320
|
+
sessionId
|
|
3321
|
+
} : void 0;
|
|
2907
3322
|
} catch {
|
|
2908
3323
|
return;
|
|
2909
3324
|
}
|
|
@@ -2923,9 +3338,16 @@ function envelope(projection, meta) {
|
|
|
2923
3338
|
};
|
|
2924
3339
|
}
|
|
2925
3340
|
/** Register the browser-readable, credential-free sidecar projection endpoint.
|
|
2926
|
-
*
|
|
2927
|
-
*
|
|
2928
|
-
*
|
|
3341
|
+
*
|
|
3342
|
+
* `GET <path>/:sessionId` returns one snapshot. `GET <path>/multi?sessions=a,b`
|
|
3343
|
+
* returns ONE NDJSON stream carrying every listed session, each line stamped
|
|
3344
|
+
* with its `session`.
|
|
3345
|
+
*
|
|
3346
|
+
* There is deliberately no per-session stream URL. The browser shares a small
|
|
3347
|
+
* fixed connection budget between this plugin and the Host, and a stream per
|
|
3348
|
+
* session spent it in proportion to how many Claude sessions existed — which
|
|
3349
|
+
* is what left the settings panel unable to get a connection at all. One
|
|
3350
|
+
* carrier is one connection, whatever the session count. */
|
|
2929
3351
|
function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSession = () => [], repositoryForSession = async () => void 0, reviewCommentsForSession = () => []) {
|
|
2930
3352
|
const info = (message) => {
|
|
2931
3353
|
ctx.logger?.info?.(message);
|
|
@@ -2940,15 +3362,14 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
|
|
|
2940
3362
|
reviewComments: owned ? reviewCommentsForSession(sessionId) : []
|
|
2941
3363
|
};
|
|
2942
3364
|
};
|
|
2943
|
-
const
|
|
2944
|
-
info(`dsh-claude: projection stream opened for ${
|
|
3365
|
+
const streamMulti = async (res, io, sessionIds) => {
|
|
3366
|
+
info(`dsh-claude: projection stream opened for ${sessionIds.length} session(s)`);
|
|
2945
3367
|
let textDeltas = 0;
|
|
2946
3368
|
let textBytes = 0;
|
|
2947
3369
|
let textSince = Date.now();
|
|
2948
|
-
let meta = await assembleMeta(sessionId);
|
|
2949
3370
|
res.writeHead(200, {
|
|
2950
3371
|
"content-type": "application/x-ndjson; charset=utf-8",
|
|
2951
|
-
"cache-control": "no-store",
|
|
3372
|
+
"cache-control": "no-store, no-transform",
|
|
2952
3373
|
"x-content-type-options": "nosniff"
|
|
2953
3374
|
});
|
|
2954
3375
|
res.flushHeaders?.();
|
|
@@ -2961,102 +3382,146 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
|
|
|
2961
3382
|
closed = true;
|
|
2962
3383
|
}
|
|
2963
3384
|
};
|
|
2964
|
-
const
|
|
2965
|
-
|
|
3385
|
+
const metas = /* @__PURE__ */ new Map();
|
|
3386
|
+
const writeSnapshot = async (sessionId) => {
|
|
3387
|
+
const [projection, meta] = await Promise.all([sidecar.read(sessionId), assembleMeta(sessionId)]);
|
|
3388
|
+
metas.set(sessionId, meta);
|
|
2966
3389
|
writeLine({
|
|
2967
3390
|
type: "snapshot",
|
|
3391
|
+
session: sessionId,
|
|
3392
|
+
seq: sidecar.sequence(sessionId),
|
|
2968
3393
|
...envelope(projection, meta)
|
|
2969
3394
|
});
|
|
2970
3395
|
};
|
|
2971
|
-
|
|
2972
|
-
const unsubscribe = sidecar.subscribe(sessionId, (delta) => {
|
|
3396
|
+
const unsubscribes = sessionIds.map((sessionId) => sidecar.subscribe(sessionId, (delta) => {
|
|
2973
3397
|
switch (delta.kind) {
|
|
2974
3398
|
case "text":
|
|
2975
3399
|
textDeltas += 1;
|
|
2976
3400
|
textBytes += (delta.append ?? delta.text ?? "").length;
|
|
2977
3401
|
if (textDeltas % 25 === 0) {
|
|
2978
3402
|
const elapsed = Date.now() - textSince;
|
|
2979
|
-
info(`dsh-claude: stream
|
|
3403
|
+
info(`dsh-claude: stream 25 text deltas ${textBytes}B in ${elapsed}ms`);
|
|
2980
3404
|
textBytes = 0;
|
|
2981
3405
|
textSince = Date.now();
|
|
2982
3406
|
}
|
|
2983
3407
|
writeLine({
|
|
2984
3408
|
type: "text",
|
|
3409
|
+
session: sessionId,
|
|
2985
3410
|
turn: delta.turn,
|
|
2986
3411
|
step: delta.step,
|
|
2987
3412
|
ordinal: delta.ordinal,
|
|
2988
3413
|
...delta.append === void 0 ? {} : { append: delta.append },
|
|
2989
|
-
...delta.text === void 0 ? {} : { text: delta.text }
|
|
3414
|
+
...delta.text === void 0 ? {} : { text: delta.text },
|
|
3415
|
+
...delta.renderer === void 0 ? {} : { renderer: delta.renderer },
|
|
3416
|
+
seq: delta.seq
|
|
2990
3417
|
});
|
|
2991
3418
|
return;
|
|
2992
3419
|
case "activity":
|
|
2993
3420
|
writeLine({
|
|
2994
3421
|
type: "activity",
|
|
2995
|
-
|
|
3422
|
+
session: sessionId,
|
|
3423
|
+
activity: delta.activity,
|
|
3424
|
+
seq: delta.seq
|
|
2996
3425
|
});
|
|
2997
3426
|
return;
|
|
2998
3427
|
case "contextUsage":
|
|
2999
3428
|
writeLine({
|
|
3000
3429
|
type: "contextUsage",
|
|
3001
|
-
|
|
3430
|
+
session: sessionId,
|
|
3431
|
+
value: delta.value,
|
|
3432
|
+
seq: delta.seq
|
|
3002
3433
|
});
|
|
3003
3434
|
return;
|
|
3004
3435
|
case "tasks":
|
|
3005
3436
|
writeLine({
|
|
3006
3437
|
type: "tasks",
|
|
3007
|
-
|
|
3438
|
+
session: sessionId,
|
|
3439
|
+
value: delta.value,
|
|
3440
|
+
seq: delta.seq
|
|
3008
3441
|
});
|
|
3009
3442
|
return;
|
|
3010
|
-
case "
|
|
3443
|
+
case "checkpoint":
|
|
3444
|
+
writeLine({
|
|
3445
|
+
type: "checkpoint",
|
|
3446
|
+
session: sessionId,
|
|
3447
|
+
seq: delta.seq
|
|
3448
|
+
});
|
|
3449
|
+
return;
|
|
3450
|
+
case "sync": writeSnapshot(sessionId).catch(() => void 0);
|
|
3011
3451
|
}
|
|
3012
|
-
});
|
|
3452
|
+
}));
|
|
3453
|
+
for (const sessionId of sessionIds) writeSnapshot(sessionId).catch(() => void 0);
|
|
3013
3454
|
const timer = setInterval(() => {
|
|
3014
3455
|
(async () => {
|
|
3015
|
-
const
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3456
|
+
for (const sessionId of sessionIds) {
|
|
3457
|
+
if (closed) return;
|
|
3458
|
+
const next = await assembleMeta(sessionId);
|
|
3459
|
+
if (closed) return;
|
|
3460
|
+
if (JSON.stringify(next) === JSON.stringify(metas.get(sessionId))) continue;
|
|
3461
|
+
metas.set(sessionId, next);
|
|
3462
|
+
writeLine({
|
|
3463
|
+
type: "meta",
|
|
3464
|
+
session: sessionId,
|
|
3465
|
+
owned: next.owned,
|
|
3466
|
+
commands: next.commands,
|
|
3467
|
+
...next.repository === void 0 ? {} : { repository: next.repository },
|
|
3468
|
+
reviewComments: next.reviewComments
|
|
3469
|
+
});
|
|
3020
3470
|
}
|
|
3021
|
-
|
|
3022
|
-
writeLine({
|
|
3023
|
-
type: "meta",
|
|
3024
|
-
owned: meta.owned,
|
|
3025
|
-
commands: meta.commands,
|
|
3026
|
-
...meta.repository === void 0 ? {} : { repository: meta.repository },
|
|
3027
|
-
reviewComments: meta.reviewComments
|
|
3028
|
-
});
|
|
3471
|
+
writeLine({ type: "ping" });
|
|
3029
3472
|
})().catch(() => void 0);
|
|
3030
3473
|
}, META_REFRESH_MS);
|
|
3031
3474
|
timer.unref?.();
|
|
3032
3475
|
await new Promise((resolve) => {
|
|
3033
|
-
|
|
3476
|
+
const finish = () => {
|
|
3477
|
+
if (closed) return;
|
|
3034
3478
|
closed = true;
|
|
3035
3479
|
clearInterval(timer);
|
|
3036
|
-
unsubscribe();
|
|
3037
|
-
info(`dsh-claude: projection stream closed
|
|
3480
|
+
for (const unsubscribe of unsubscribes) unsubscribe();
|
|
3481
|
+
info(`dsh-claude: projection stream closed after ${textDeltas} text deltas`);
|
|
3038
3482
|
resolve();
|
|
3039
|
-
}
|
|
3483
|
+
};
|
|
3484
|
+
if (io.signal.aborted) finish();
|
|
3485
|
+
else io.signal.addEventListener("abort", finish, { once: true });
|
|
3040
3486
|
});
|
|
3041
3487
|
};
|
|
3042
|
-
ctx
|
|
3488
|
+
registerPluginRoute(ctx, {
|
|
3489
|
+
mode: "stream",
|
|
3043
3490
|
kind: "prefix",
|
|
3044
3491
|
path: CLAUDE_PROJECTION_PATH,
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3492
|
+
methods: ["GET"],
|
|
3493
|
+
maxConcurrent: 1,
|
|
3494
|
+
streamKey: () => MULTI_SEGMENT,
|
|
3495
|
+
handler: async (res, io) => {
|
|
3496
|
+
const target = targetFromUrl(io.url);
|
|
3497
|
+
if (target === void 0) {
|
|
3498
|
+
res.writeHead(400, {
|
|
3499
|
+
"content-type": "application/json; charset=utf-8",
|
|
3500
|
+
"cache-control": "no-store"
|
|
3501
|
+
});
|
|
3502
|
+
res.write(JSON.stringify({ error: "invalid session id" }));
|
|
3503
|
+
return;
|
|
3504
|
+
}
|
|
3505
|
+
if (target.kind === "multi") return await streamMulti(res, io, target.sessionIds);
|
|
3506
|
+
info(`dsh-claude: projection poll for ${target.sessionId.slice(0, 64)}`);
|
|
3050
3507
|
try {
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3508
|
+
const body = envelope(await sidecar.read(target.sessionId), await assembleMeta(target.sessionId));
|
|
3509
|
+
res.writeHead(200, {
|
|
3510
|
+
"content-type": "application/json; charset=utf-8",
|
|
3511
|
+
"cache-control": "no-store",
|
|
3512
|
+
"x-content-type-options": "nosniff"
|
|
3513
|
+
});
|
|
3514
|
+
res.write(JSON.stringify(body));
|
|
3054
3515
|
} catch {
|
|
3055
|
-
if (
|
|
3056
|
-
res.
|
|
3516
|
+
if (res.headersSent) return;
|
|
3517
|
+
res.writeHead(500, {
|
|
3518
|
+
"content-type": "application/json; charset=utf-8",
|
|
3519
|
+
"cache-control": "no-store"
|
|
3520
|
+
});
|
|
3521
|
+
res.write(JSON.stringify({ error: "projection unavailable" }));
|
|
3057
3522
|
}
|
|
3058
3523
|
}
|
|
3059
|
-
})
|
|
3524
|
+
});
|
|
3060
3525
|
}
|
|
3061
3526
|
//#endregion
|
|
3062
3527
|
//#region src/repository-status.ts
|
|
@@ -3235,10 +3700,13 @@ var RepositoryStatusService = class {
|
|
|
3235
3700
|
this.#runtime = runtime;
|
|
3236
3701
|
this.#cacheTtlMs = cacheTtlMs;
|
|
3237
3702
|
}
|
|
3238
|
-
|
|
3703
|
+
/** `signal` bounds the scan itself, not just the caller's patience: the
|
|
3704
|
+
* untracked-file diff below is a serial spawn loop that can outlive any
|
|
3705
|
+
* route budget, and work nobody is waiting for still occupies the process. */
|
|
3706
|
+
inspect(cwd, signal) {
|
|
3239
3707
|
const current = this.#cache.get(cwd);
|
|
3240
3708
|
if (current !== void 0 && current.expiresAt > Date.now()) return current.value;
|
|
3241
|
-
const value = this.#inspect(cwd).then((next) => this.#stabilize(cwd, next));
|
|
3709
|
+
const value = this.#inspect(cwd, signal).then((next) => this.#stabilize(cwd, next));
|
|
3242
3710
|
this.#cache.set(cwd, {
|
|
3243
3711
|
expiresAt: Date.now() + this.#cacheTtlMs,
|
|
3244
3712
|
value
|
|
@@ -3296,7 +3764,7 @@ var RepositoryStatusService = class {
|
|
|
3296
3764
|
this.#ghExecutable ??= this.#runtime.resolveExecutable("gh").catch(() => void 0);
|
|
3297
3765
|
return this.#ghExecutable;
|
|
3298
3766
|
}
|
|
3299
|
-
async #inspect(cwd) {
|
|
3767
|
+
async #inspect(cwd, signal) {
|
|
3300
3768
|
const safeCwd = bounded(cwd);
|
|
3301
3769
|
let git;
|
|
3302
3770
|
try {
|
|
@@ -3346,7 +3814,7 @@ var RepositoryStatusService = class {
|
|
|
3346
3814
|
const remote = remoteResult.exitCode === 0 ? parseGitHubRemote(remoteResult.stdout) : void 0;
|
|
3347
3815
|
const pullRequest = status.branch === void 0 || remote === void 0 ? void 0 : await this.#pullRequest(cwd, remote, status.branch);
|
|
3348
3816
|
const diffBase = pullRequest?.baseBranch === void 0 ? "HEAD" : await this.#mergeBase(cwd, git, pullRequest.baseBranch) ?? "HEAD";
|
|
3349
|
-
const diff = status.dirty || diffBase !== "HEAD" ? await this.#diff(cwd, git, diffBase) : {
|
|
3817
|
+
const diff = status.dirty || diffBase !== "HEAD" ? await this.#diff(cwd, git, diffBase, signal) : {
|
|
3350
3818
|
additions: 0,
|
|
3351
3819
|
deletions: 0,
|
|
3352
3820
|
files: 0,
|
|
@@ -3400,7 +3868,7 @@ var RepositoryStatusService = class {
|
|
|
3400
3868
|
return;
|
|
3401
3869
|
}
|
|
3402
3870
|
}
|
|
3403
|
-
async #diff(cwd, git, base) {
|
|
3871
|
+
async #diff(cwd, git, base, signal) {
|
|
3404
3872
|
try {
|
|
3405
3873
|
const numstat = await run(this.#runtime, git, [
|
|
3406
3874
|
"diff",
|
|
@@ -3423,7 +3891,7 @@ var RepositoryStatusService = class {
|
|
|
3423
3891
|
...summary,
|
|
3424
3892
|
truncated: true
|
|
3425
3893
|
};
|
|
3426
|
-
const untracked = await this.#untrackedDiff(cwd, git);
|
|
3894
|
+
const untracked = await this.#untrackedDiff(cwd, git, signal);
|
|
3427
3895
|
const combinedPatch = `${patch.stdout}${untracked.patch}`;
|
|
3428
3896
|
return {
|
|
3429
3897
|
additions: summary.additions + untracked.additions,
|
|
@@ -3436,7 +3904,7 @@ var RepositoryStatusService = class {
|
|
|
3436
3904
|
return;
|
|
3437
3905
|
}
|
|
3438
3906
|
}
|
|
3439
|
-
async #untrackedDiff(cwd, git) {
|
|
3907
|
+
async #untrackedDiff(cwd, git, signal) {
|
|
3440
3908
|
const listed = await run(this.#runtime, git, [
|
|
3441
3909
|
"ls-files",
|
|
3442
3910
|
"--others",
|
|
@@ -3454,6 +3922,12 @@ var RepositoryStatusService = class {
|
|
|
3454
3922
|
let patch = "";
|
|
3455
3923
|
let truncated = paths.length > MAX_UNTRACKED_DIFFS;
|
|
3456
3924
|
for (const path of paths.slice(0, MAX_UNTRACKED_DIFFS)) {
|
|
3925
|
+
if (signal?.aborted === true) return {
|
|
3926
|
+
additions,
|
|
3927
|
+
files: paths.length,
|
|
3928
|
+
patch,
|
|
3929
|
+
truncated: true
|
|
3930
|
+
};
|
|
3457
3931
|
const result = await run(this.#runtime, git, [
|
|
3458
3932
|
"diff",
|
|
3459
3933
|
"--no-ext-diff",
|
|
@@ -4445,16 +4919,15 @@ const MAX_BODY_BYTES$6 = 16384;
|
|
|
4445
4919
|
function record$9(value) {
|
|
4446
4920
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
4447
4921
|
}
|
|
4448
|
-
async function readJson$
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
chunks.push(buffer);
|
|
4922
|
+
async function readJson$5(io) {
|
|
4923
|
+
let parsed;
|
|
4924
|
+
try {
|
|
4925
|
+
parsed = await io.body(MAX_BODY_BYTES$6);
|
|
4926
|
+
} catch (error) {
|
|
4927
|
+
if (error instanceof SyntaxError) throw error;
|
|
4928
|
+
throw new RepositorySetupError("body-too-large", "The request body is too large.");
|
|
4456
4929
|
}
|
|
4457
|
-
const value = record$9(
|
|
4930
|
+
const value = record$9(parsed);
|
|
4458
4931
|
if (value === void 0) throw new RepositorySetupError("invalid-request", "The request body is invalid.");
|
|
4459
4932
|
return value;
|
|
4460
4933
|
}
|
|
@@ -4505,41 +4978,48 @@ async function streamSetup(res, service, input) {
|
|
|
4505
4978
|
res.end();
|
|
4506
4979
|
}
|
|
4507
4980
|
}
|
|
4508
|
-
/** Register trusted browser routes for safe pre-session Git setup.
|
|
4981
|
+
/** Register trusted browser routes for safe pre-session Git setup.
|
|
4982
|
+
*
|
|
4983
|
+
* The prefix is registered as a stream because the setup POST holds its
|
|
4984
|
+
* connection open for the whole worktree build; the short sibling paths ride
|
|
4985
|
+
* the same registration and answer with `json` before releasing it. */
|
|
4509
4986
|
function registerRepositorySetupRoute(ctx, service) {
|
|
4510
|
-
ctx
|
|
4987
|
+
registerPluginRoute(ctx, {
|
|
4988
|
+
mode: "stream",
|
|
4511
4989
|
kind: "prefix",
|
|
4512
4990
|
path: CLAUDE_REPOSITORY_SETUP_PATH,
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4991
|
+
methods: ["GET", "POST"],
|
|
4992
|
+
maxConcurrent: 2,
|
|
4993
|
+
streamKey: (url) => `${url.pathname}?${url.searchParams.get("cwd") ?? url.searchParams.get("branch") ?? ""}`,
|
|
4994
|
+
handler: async (res, io) => {
|
|
4995
|
+
const pathname = io.url.pathname;
|
|
4516
4996
|
try {
|
|
4517
4997
|
if (pathname === `/plugins/dsh-claude/repository/setup/branches/refresh`) {
|
|
4518
|
-
if (
|
|
4519
|
-
const input = await readJson$
|
|
4998
|
+
if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
4999
|
+
const input = await readJson$5(io);
|
|
4520
5000
|
return json(res, 200, await service.refreshBranches(string$2(input, "cwd")));
|
|
4521
5001
|
}
|
|
4522
5002
|
if (pathname === `/plugins/dsh-claude/repository/setup/branches`) {
|
|
4523
|
-
if (
|
|
4524
|
-
const cwd =
|
|
5003
|
+
if (io.method !== "GET") return json(res, 405, { error: "method not allowed" });
|
|
5004
|
+
const cwd = io.url.searchParams.get("cwd");
|
|
4525
5005
|
if (cwd === null) throw new RepositorySetupError("invalid-request", "The cwd query parameter is required.");
|
|
4526
5006
|
return json(res, 200, await service.listBranches(cwd));
|
|
4527
5007
|
}
|
|
4528
5008
|
if (pathname === "/plugins/dsh-claude/repository/setup") {
|
|
4529
|
-
if (
|
|
4530
|
-
const input = await readJson$
|
|
5009
|
+
if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
5010
|
+
const input = await readJson$5(io);
|
|
4531
5011
|
if (typeof input.worktree !== "boolean") throw new RepositorySetupError("invalid-request", "The worktree field is required.");
|
|
4532
5012
|
await streamSetup(res, service, input);
|
|
4533
5013
|
return;
|
|
4534
5014
|
}
|
|
4535
5015
|
if (pathname === `/plugins/dsh-claude/repository/setup/cleanup`) {
|
|
4536
|
-
if (
|
|
4537
|
-
const input = await readJson$
|
|
5016
|
+
if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
5017
|
+
const input = await readJson$5(io);
|
|
4538
5018
|
return json(res, 200, await service.cleanupMerged(string$2(input, "path"), string$2(input, "baseBranch")));
|
|
4539
5019
|
}
|
|
4540
5020
|
if (pathname === `/plugins/dsh-claude/repository/setup/bind`) {
|
|
4541
|
-
if (
|
|
4542
|
-
const input = await readJson$
|
|
5021
|
+
if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
5022
|
+
const input = await readJson$5(io);
|
|
4543
5023
|
await service.bindLease(string$2(input, "leaseId"), string$2(input, "sessionId"));
|
|
4544
5024
|
return json(res, 200, { ok: true });
|
|
4545
5025
|
}
|
|
@@ -4553,7 +5033,7 @@ function registerRepositorySetupRoute(ctx, service) {
|
|
|
4553
5033
|
return json(res, 500, { error: "repository setup unavailable" });
|
|
4554
5034
|
}
|
|
4555
5035
|
}
|
|
4556
|
-
})
|
|
5036
|
+
});
|
|
4557
5037
|
}
|
|
4558
5038
|
//#endregion
|
|
4559
5039
|
//#region src/repository-action-routes.ts
|
|
@@ -4570,16 +5050,15 @@ const ACTIONS = /* @__PURE__ */ new Set([
|
|
|
4570
5050
|
function record$8(value) {
|
|
4571
5051
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
4572
5052
|
}
|
|
4573
|
-
async function readJson$
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
chunks.push(buffer);
|
|
5053
|
+
async function readJson$4(io) {
|
|
5054
|
+
let parsed;
|
|
5055
|
+
try {
|
|
5056
|
+
parsed = await io.body(MAX_BODY_BYTES$5);
|
|
5057
|
+
} catch (error) {
|
|
5058
|
+
if (error instanceof SyntaxError) throw error;
|
|
5059
|
+
throw new RepositoryActionError("body-too-large", "The request body is too large.");
|
|
4581
5060
|
}
|
|
4582
|
-
const value = record$8(
|
|
5061
|
+
const value = record$8(parsed);
|
|
4583
5062
|
if (value === void 0) throw new RepositoryActionError("invalid-request", "The request body is invalid.");
|
|
4584
5063
|
return value;
|
|
4585
5064
|
}
|
|
@@ -4618,44 +5097,80 @@ function actionRequest(input) {
|
|
|
4618
5097
|
})()
|
|
4619
5098
|
};
|
|
4620
5099
|
}
|
|
5100
|
+
/** One prefix serves the local-Git preview and the POST arms that push, open
|
|
5101
|
+
* and merge pull requests, so the registration takes the wider of the two
|
|
5102
|
+
* budgets: a `remote` arm cut short at the `git` deadline would abandon work
|
|
5103
|
+
* that had already reached GitHub. */
|
|
4621
5104
|
function registerRepositoryActionRoute(ctx, service, cwdForSession) {
|
|
4622
|
-
ctx
|
|
5105
|
+
registerPluginRoute(ctx, {
|
|
5106
|
+
mode: "unary",
|
|
4623
5107
|
kind: "prefix",
|
|
4624
5108
|
path: CLAUDE_REPOSITORY_ACTION_PATH,
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
5109
|
+
methods: ["GET", "POST"],
|
|
5110
|
+
budget: "remote",
|
|
5111
|
+
handler: async (io) => {
|
|
5112
|
+
const url = io.url;
|
|
4628
5113
|
try {
|
|
4629
5114
|
const cwd = cwdForSession(sessionId$1(url));
|
|
4630
5115
|
if (cwd === void 0) throw new RepositoryActionError("session-unavailable", "The Claude session is unavailable.");
|
|
4631
5116
|
if (url.pathname === `/plugins/dsh-claude/repository/action/preview`) {
|
|
4632
|
-
if (
|
|
4633
|
-
|
|
5117
|
+
if (io.method !== "GET") return {
|
|
5118
|
+
status: 405,
|
|
5119
|
+
value: { error: "method not allowed" }
|
|
5120
|
+
};
|
|
5121
|
+
return {
|
|
5122
|
+
status: 200,
|
|
5123
|
+
value: await service.preview(cwd)
|
|
5124
|
+
};
|
|
4634
5125
|
}
|
|
4635
5126
|
if (url.pathname === `/plugins/dsh-claude/repository/action/message`) {
|
|
4636
|
-
if (
|
|
4637
|
-
|
|
4638
|
-
|
|
5127
|
+
if (io.method !== "POST") return {
|
|
5128
|
+
status: 405,
|
|
5129
|
+
value: { error: "method not allowed" }
|
|
5130
|
+
};
|
|
5131
|
+
const input = await readJson$4(io);
|
|
5132
|
+
return {
|
|
5133
|
+
status: 200,
|
|
5134
|
+
value: { message: await service.generateMessage(cwd, string$1(input, "fingerprint")) }
|
|
5135
|
+
};
|
|
4639
5136
|
}
|
|
4640
5137
|
if (url.pathname === "/plugins/dsh-claude/repository/action") {
|
|
4641
|
-
if (
|
|
4642
|
-
|
|
5138
|
+
if (io.method !== "POST") return {
|
|
5139
|
+
status: 405,
|
|
5140
|
+
value: { error: "method not allowed" }
|
|
5141
|
+
};
|
|
5142
|
+
return {
|
|
5143
|
+
status: 200,
|
|
5144
|
+
value: await service.execute(cwd, actionRequest(await readJson$4(io)))
|
|
5145
|
+
};
|
|
4643
5146
|
}
|
|
4644
|
-
return
|
|
5147
|
+
return {
|
|
5148
|
+
status: 404,
|
|
5149
|
+
value: { error: "not found" }
|
|
5150
|
+
};
|
|
4645
5151
|
} catch (error) {
|
|
4646
|
-
if (error instanceof RepositoryActionError) return
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
5152
|
+
if (error instanceof RepositoryActionError) return {
|
|
5153
|
+
status: 409,
|
|
5154
|
+
value: {
|
|
5155
|
+
error: error.code,
|
|
5156
|
+
message: error.message,
|
|
5157
|
+
...error.commit === void 0 ? {} : { commit: error.commit }
|
|
5158
|
+
}
|
|
5159
|
+
};
|
|
5160
|
+
if (error instanceof SyntaxError) return {
|
|
5161
|
+
status: 400,
|
|
5162
|
+
value: { error: "invalid-json" }
|
|
5163
|
+
};
|
|
5164
|
+
return {
|
|
5165
|
+
status: 500,
|
|
5166
|
+
value: {
|
|
5167
|
+
error: "repository-action-unavailable",
|
|
5168
|
+
message: "Repository action is unavailable."
|
|
5169
|
+
}
|
|
5170
|
+
};
|
|
4656
5171
|
}
|
|
4657
5172
|
}
|
|
4658
|
-
})
|
|
5173
|
+
});
|
|
4659
5174
|
}
|
|
4660
5175
|
//#endregion
|
|
4661
5176
|
//#region src/editor-open.ts
|
|
@@ -4781,43 +5296,62 @@ const MAX_SESSION_ID_CHARS$4 = 1024;
|
|
|
4781
5296
|
/** Open the session's working directory in a desktop editor. Query-only: the
|
|
4782
5297
|
* request carries two enum-ish values, so there is no body to parse. */
|
|
4783
5298
|
function registerEditorOpenRoute(ctx, service, cwdForSession) {
|
|
4784
|
-
ctx
|
|
5299
|
+
registerPluginRoute(ctx, {
|
|
5300
|
+
mode: "unary",
|
|
4785
5301
|
kind: "exact",
|
|
4786
5302
|
path: CLAUDE_EDITOR_OPEN_PATH,
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
const params =
|
|
5303
|
+
methods: ["POST"],
|
|
5304
|
+
budget: "fast",
|
|
5305
|
+
handler: async (io) => {
|
|
5306
|
+
const params = io.url.searchParams;
|
|
4791
5307
|
const id = params.get("sessionId");
|
|
4792
5308
|
const editor = params.get("editor");
|
|
4793
|
-
if (id === null || id.length === 0 || id.length > MAX_SESSION_ID_CHARS$4) return
|
|
4794
|
-
|
|
4795
|
-
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
5309
|
+
if (id === null || id.length === 0 || id.length > MAX_SESSION_ID_CHARS$4) return {
|
|
5310
|
+
status: 400,
|
|
5311
|
+
value: {
|
|
5312
|
+
error: "invalid-session",
|
|
5313
|
+
message: "The session is invalid."
|
|
5314
|
+
}
|
|
5315
|
+
};
|
|
5316
|
+
if (editor === null || !EDITOR_IDS.has(editor)) return {
|
|
5317
|
+
status: 400,
|
|
5318
|
+
value: {
|
|
5319
|
+
error: "invalid-editor",
|
|
5320
|
+
message: "The editor is invalid."
|
|
5321
|
+
}
|
|
5322
|
+
};
|
|
4801
5323
|
const cwd = cwdForSession(id);
|
|
4802
|
-
if (cwd === void 0) return
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
5324
|
+
if (cwd === void 0) return {
|
|
5325
|
+
status: 409,
|
|
5326
|
+
value: {
|
|
5327
|
+
error: "session-unavailable",
|
|
5328
|
+
message: "The Claude session is unavailable."
|
|
5329
|
+
}
|
|
5330
|
+
};
|
|
4806
5331
|
try {
|
|
4807
5332
|
await service.open(cwd, editor);
|
|
4808
|
-
return
|
|
5333
|
+
return {
|
|
5334
|
+
status: 200,
|
|
5335
|
+
value: { opened: true }
|
|
5336
|
+
};
|
|
4809
5337
|
} catch (error) {
|
|
4810
|
-
if (error instanceof EditorOpenError) return
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
5338
|
+
if (error instanceof EditorOpenError) return {
|
|
5339
|
+
status: 409,
|
|
5340
|
+
value: {
|
|
5341
|
+
error: error.code,
|
|
5342
|
+
message: error.message
|
|
5343
|
+
}
|
|
5344
|
+
};
|
|
5345
|
+
return {
|
|
5346
|
+
status: 500,
|
|
5347
|
+
value: {
|
|
5348
|
+
error: "editor-open-unavailable",
|
|
5349
|
+
message: "The editor could not be launched."
|
|
5350
|
+
}
|
|
5351
|
+
};
|
|
4818
5352
|
}
|
|
4819
5353
|
}
|
|
4820
|
-
})
|
|
5354
|
+
});
|
|
4821
5355
|
}
|
|
4822
5356
|
//#endregion
|
|
4823
5357
|
//#region src/github-url.ts
|
|
@@ -5110,7 +5644,10 @@ var PullRequestFeedbackService = class {
|
|
|
5110
5644
|
return [];
|
|
5111
5645
|
}
|
|
5112
5646
|
}
|
|
5113
|
-
|
|
5647
|
+
/** `signal` bounds the log fetches below: each is capped on its own, but they
|
|
5648
|
+
* run one after another, so the honest worst case is their sum rather than
|
|
5649
|
+
* any single ceiling — more than a route is allowed to hold a connection. */
|
|
5650
|
+
async failingChecks(cwd, pullNumber, signal) {
|
|
5114
5651
|
const gh = await this.#gh();
|
|
5115
5652
|
const result = await this.#run(gh, [
|
|
5116
5653
|
"pr",
|
|
@@ -5132,6 +5669,10 @@ var PullRequestFeedbackService = class {
|
|
|
5132
5669
|
detailed.push(check);
|
|
5133
5670
|
continue;
|
|
5134
5671
|
}
|
|
5672
|
+
if (signal?.aborted === true) {
|
|
5673
|
+
detailed.push(check);
|
|
5674
|
+
continue;
|
|
5675
|
+
}
|
|
5135
5676
|
const log = await this.#run(gh, [
|
|
5136
5677
|
"run",
|
|
5137
5678
|
"view",
|
|
@@ -5204,16 +5745,15 @@ const MAX_MENTION_QUERY_CHARS = 64;
|
|
|
5204
5745
|
function record$6(value) {
|
|
5205
5746
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
5206
5747
|
}
|
|
5207
|
-
async function readJson$
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
chunks.push(buffer);
|
|
5748
|
+
async function readJson$3(io) {
|
|
5749
|
+
let parsed;
|
|
5750
|
+
try {
|
|
5751
|
+
parsed = await io.body(MAX_BODY_BYTES$4);
|
|
5752
|
+
} catch (error) {
|
|
5753
|
+
if (error instanceof SyntaxError) throw error;
|
|
5754
|
+
throw new PullRequestFeedbackError("body-too-large", "The request body is too large.");
|
|
5215
5755
|
}
|
|
5216
|
-
const value = record$6(
|
|
5756
|
+
const value = record$6(parsed);
|
|
5217
5757
|
if (value === void 0) throw new PullRequestFeedbackError("invalid-request", "The request body is invalid.");
|
|
5218
5758
|
return value;
|
|
5219
5759
|
}
|
|
@@ -5242,123 +5782,199 @@ function pullNumber(url) {
|
|
|
5242
5782
|
if (!Number.isSafeInteger(value) || value <= 0 || value > 1e9) throw new PullRequestFeedbackError("invalid-request", "The pull request number is invalid.");
|
|
5243
5783
|
return value;
|
|
5244
5784
|
}
|
|
5785
|
+
/** Every arm shells out to `gh`, so the whole prefix carries the network budget. */
|
|
5245
5786
|
function registerPullRequestFeedbackRoute(ctx, service, cwdForSession) {
|
|
5246
|
-
ctx
|
|
5787
|
+
registerPluginRoute(ctx, {
|
|
5788
|
+
mode: "unary",
|
|
5247
5789
|
kind: "prefix",
|
|
5248
5790
|
path: CLAUDE_REPOSITORY_FEEDBACK_PATH,
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5252
|
-
const
|
|
5253
|
-
const
|
|
5791
|
+
methods: ["GET", "POST"],
|
|
5792
|
+
budget: "remote",
|
|
5793
|
+
handler: async (io) => {
|
|
5794
|
+
const url = io.url;
|
|
5795
|
+
const reads = io.method === "GET";
|
|
5796
|
+
const writes = io.method === "POST";
|
|
5254
5797
|
try {
|
|
5255
5798
|
const cwd = cwdForSession(sessionId(url));
|
|
5256
5799
|
if (cwd === void 0) throw new PullRequestFeedbackError("session-unavailable", "The Claude session is unavailable.");
|
|
5257
5800
|
if (url.pathname === `/plugins/dsh-claude/repository/feedback/comments`) {
|
|
5258
|
-
if (!reads) return
|
|
5259
|
-
|
|
5801
|
+
if (!reads) return {
|
|
5802
|
+
status: 405,
|
|
5803
|
+
value: { error: "method not allowed" }
|
|
5804
|
+
};
|
|
5805
|
+
return {
|
|
5806
|
+
status: 200,
|
|
5807
|
+
value: { threads: await service.threads(cwd, pullNumber(url)) }
|
|
5808
|
+
};
|
|
5260
5809
|
}
|
|
5261
5810
|
if (url.pathname === `/plugins/dsh-claude/repository/feedback/checks`) {
|
|
5262
|
-
if (!reads) return
|
|
5263
|
-
|
|
5811
|
+
if (!reads) return {
|
|
5812
|
+
status: 405,
|
|
5813
|
+
value: { error: "method not allowed" }
|
|
5814
|
+
};
|
|
5815
|
+
return {
|
|
5816
|
+
status: 200,
|
|
5817
|
+
value: { checks: await service.failingChecks(cwd, pullNumber(url), io.signal) }
|
|
5818
|
+
};
|
|
5264
5819
|
}
|
|
5265
5820
|
if (url.pathname === `/plugins/dsh-claude/repository/feedback/mentionables`) {
|
|
5266
|
-
if (!reads) return
|
|
5821
|
+
if (!reads) return {
|
|
5822
|
+
status: 405,
|
|
5823
|
+
value: { error: "method not allowed" }
|
|
5824
|
+
};
|
|
5267
5825
|
const query = (url.searchParams.get("q") ?? "").slice(0, MAX_MENTION_QUERY_CHARS);
|
|
5268
|
-
return
|
|
5826
|
+
return {
|
|
5827
|
+
status: 200,
|
|
5828
|
+
value: { users: await service.mentionables(cwd, query) }
|
|
5829
|
+
};
|
|
5269
5830
|
}
|
|
5270
5831
|
if (url.pathname === `/plugins/dsh-claude/repository/feedback/reply`) {
|
|
5271
|
-
if (!writes) return
|
|
5272
|
-
|
|
5273
|
-
|
|
5832
|
+
if (!writes) return {
|
|
5833
|
+
status: 405,
|
|
5834
|
+
value: { error: "method not allowed" }
|
|
5835
|
+
};
|
|
5836
|
+
const input = await readJson$3(io);
|
|
5837
|
+
return {
|
|
5838
|
+
status: 200,
|
|
5839
|
+
value: { comment: await service.reply(cwd, pullNumber(url), commentId(input), replyBody(input)) }
|
|
5840
|
+
};
|
|
5274
5841
|
}
|
|
5275
5842
|
if (url.pathname === `/plugins/dsh-claude/repository/feedback/resolve`) {
|
|
5276
|
-
if (!writes) return
|
|
5277
|
-
|
|
5843
|
+
if (!writes) return {
|
|
5844
|
+
status: 405,
|
|
5845
|
+
value: { error: "method not allowed" }
|
|
5846
|
+
};
|
|
5847
|
+
const input = await readJson$3(io);
|
|
5278
5848
|
if (typeof input.resolved !== "boolean") throw new PullRequestFeedbackError("invalid-request", "The resolved field is required.");
|
|
5279
|
-
return
|
|
5849
|
+
return {
|
|
5850
|
+
status: 200,
|
|
5851
|
+
value: { resolved: await service.setResolved(cwd, threadId(input), input.resolved) }
|
|
5852
|
+
};
|
|
5280
5853
|
}
|
|
5281
|
-
return
|
|
5854
|
+
return {
|
|
5855
|
+
status: 404,
|
|
5856
|
+
value: { error: "not found" }
|
|
5857
|
+
};
|
|
5282
5858
|
} catch (error) {
|
|
5283
|
-
if (error instanceof PullRequestFeedbackError) return
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5859
|
+
if (error instanceof PullRequestFeedbackError) return {
|
|
5860
|
+
status: 409,
|
|
5861
|
+
value: {
|
|
5862
|
+
error: error.code,
|
|
5863
|
+
message: error.message
|
|
5864
|
+
}
|
|
5865
|
+
};
|
|
5866
|
+
if (error instanceof SyntaxError) return {
|
|
5867
|
+
status: 400,
|
|
5868
|
+
value: { error: "invalid-json" }
|
|
5869
|
+
};
|
|
5870
|
+
return {
|
|
5871
|
+
status: 500,
|
|
5872
|
+
value: {
|
|
5873
|
+
error: "pr-feedback-unavailable",
|
|
5874
|
+
message: "Pull request feedback is unavailable."
|
|
5875
|
+
}
|
|
5876
|
+
};
|
|
5292
5877
|
}
|
|
5293
5878
|
}
|
|
5294
|
-
})
|
|
5879
|
+
});
|
|
5295
5880
|
}
|
|
5296
5881
|
//#endregion
|
|
5297
5882
|
//#region src/repository-status-routes.ts
|
|
5298
5883
|
const MAX_PATH_CHARS$1 = 4096;
|
|
5299
5884
|
/** Read-only repository status for an arbitrary directory (the overview panel
|
|
5300
|
-
* aggregates every Claude session's checkout through this).
|
|
5885
|
+
* aggregates every Claude session's checkout through this).
|
|
5886
|
+
*
|
|
5887
|
+
* The route signal is threaded into the service because this is the one read
|
|
5888
|
+
* the overview polls per distinct checkout every 30 seconds: freeing the
|
|
5889
|
+
* socket at the budget while the Host kept scanning would just grow a queue
|
|
5890
|
+
* of work nobody is waiting for any more. */
|
|
5301
5891
|
function registerRepositoryStatusRoute(ctx, service) {
|
|
5302
|
-
ctx
|
|
5892
|
+
registerPluginRoute(ctx, {
|
|
5893
|
+
mode: "unary",
|
|
5303
5894
|
kind: "exact",
|
|
5304
5895
|
path: CLAUDE_REPOSITORY_STATUS_PATH,
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5308
|
-
const cwd =
|
|
5309
|
-
if (cwd === null || cwd.length === 0 || cwd.length > MAX_PATH_CHARS$1 || !isAbsolute(cwd) || cwd.includes("\0")) return
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5896
|
+
methods: ["GET"],
|
|
5897
|
+
budget: "git",
|
|
5898
|
+
handler: async (io) => {
|
|
5899
|
+
const cwd = io.url.searchParams.get("cwd");
|
|
5900
|
+
if (cwd === null || cwd.length === 0 || cwd.length > MAX_PATH_CHARS$1 || !isAbsolute(cwd) || cwd.includes("\0")) return {
|
|
5901
|
+
status: 400,
|
|
5902
|
+
value: {
|
|
5903
|
+
error: "invalid-request",
|
|
5904
|
+
message: "The cwd query parameter is invalid."
|
|
5905
|
+
}
|
|
5906
|
+
};
|
|
5313
5907
|
try {
|
|
5314
|
-
return
|
|
5908
|
+
return {
|
|
5909
|
+
status: 200,
|
|
5910
|
+
value: await service.inspect(cwd, io.signal)
|
|
5911
|
+
};
|
|
5315
5912
|
} catch {
|
|
5316
|
-
return
|
|
5317
|
-
|
|
5318
|
-
|
|
5319
|
-
|
|
5913
|
+
return {
|
|
5914
|
+
status: 500,
|
|
5915
|
+
value: {
|
|
5916
|
+
error: "repository-status-unavailable",
|
|
5917
|
+
message: "Repository status is unavailable."
|
|
5918
|
+
}
|
|
5919
|
+
};
|
|
5320
5920
|
}
|
|
5321
5921
|
}
|
|
5322
|
-
})
|
|
5922
|
+
});
|
|
5323
5923
|
}
|
|
5324
5924
|
//#endregion
|
|
5325
5925
|
//#region src/repository-file-routes.ts
|
|
5326
5926
|
const MAX_PATH_CHARS = 4096;
|
|
5327
5927
|
/** Read-only slice of a working-tree file so the diff panel can expand unmodified lines around hunks. */
|
|
5328
5928
|
function registerRepositoryFileRoute(ctx, service) {
|
|
5329
|
-
ctx
|
|
5929
|
+
registerPluginRoute(ctx, {
|
|
5930
|
+
mode: "unary",
|
|
5330
5931
|
kind: "exact",
|
|
5331
5932
|
path: CLAUDE_REPOSITORY_FILE_PATH,
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
const params =
|
|
5933
|
+
methods: ["GET"],
|
|
5934
|
+
budget: "git",
|
|
5935
|
+
handler: async (io) => {
|
|
5936
|
+
const params = io.url.searchParams;
|
|
5336
5937
|
const cwd = params.get("cwd");
|
|
5337
5938
|
const path = params.get("path");
|
|
5338
5939
|
const from = Number(params.get("from"));
|
|
5339
5940
|
const to = Number(params.get("to"));
|
|
5340
|
-
if (cwd === null || cwd.length === 0 || cwd.length > MAX_PATH_CHARS || !isAbsolute(cwd) || cwd.includes("\0")) return
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5941
|
+
if (cwd === null || cwd.length === 0 || cwd.length > MAX_PATH_CHARS || !isAbsolute(cwd) || cwd.includes("\0")) return {
|
|
5942
|
+
status: 400,
|
|
5943
|
+
value: {
|
|
5944
|
+
error: "invalid-request",
|
|
5945
|
+
message: "The cwd query parameter is invalid."
|
|
5946
|
+
}
|
|
5947
|
+
};
|
|
5948
|
+
if (path === null || path.length === 0 || path.length > MAX_PATH_CHARS) return {
|
|
5949
|
+
status: 400,
|
|
5950
|
+
value: {
|
|
5951
|
+
error: "invalid-request",
|
|
5952
|
+
message: "The path query parameter is invalid."
|
|
5953
|
+
}
|
|
5954
|
+
};
|
|
5348
5955
|
try {
|
|
5349
|
-
return
|
|
5956
|
+
return {
|
|
5957
|
+
status: 200,
|
|
5958
|
+
value: await service.fileLines(cwd, path, from, to)
|
|
5959
|
+
};
|
|
5350
5960
|
} catch (error) {
|
|
5351
|
-
if (error instanceof RepositoryFileError) return
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5961
|
+
if (error instanceof RepositoryFileError) return {
|
|
5962
|
+
status: error.code === "invalid-request" ? 400 : 409,
|
|
5963
|
+
value: {
|
|
5964
|
+
error: error.code,
|
|
5965
|
+
message: error.message
|
|
5966
|
+
}
|
|
5967
|
+
};
|
|
5968
|
+
return {
|
|
5969
|
+
status: 500,
|
|
5970
|
+
value: {
|
|
5971
|
+
error: "repository-file-unavailable",
|
|
5972
|
+
message: "The file could not be read."
|
|
5973
|
+
}
|
|
5974
|
+
};
|
|
5359
5975
|
}
|
|
5360
5976
|
}
|
|
5361
|
-
})
|
|
5977
|
+
});
|
|
5362
5978
|
}
|
|
5363
5979
|
//#endregion
|
|
5364
5980
|
//#region src/jira.ts
|
|
@@ -5604,16 +6220,17 @@ const MAX_BODY_BYTES$3 = 8192;
|
|
|
5604
6220
|
function record$4(value) {
|
|
5605
6221
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
5606
6222
|
}
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5612
|
-
|
|
5613
|
-
|
|
5614
|
-
|
|
6223
|
+
/** The wrapper enforces the byte cap; its plain rejection is translated back
|
|
6224
|
+
* into the JiraError shape the panel already knows how to render. */
|
|
6225
|
+
async function readJson$2(io) {
|
|
6226
|
+
let body;
|
|
6227
|
+
try {
|
|
6228
|
+
body = await io.body(MAX_BODY_BYTES$3);
|
|
6229
|
+
} catch (error) {
|
|
6230
|
+
if (error instanceof SyntaxError) throw error;
|
|
6231
|
+
throw new JiraError("body-too-large", "The request body is too large.");
|
|
5615
6232
|
}
|
|
5616
|
-
const value = record$4(
|
|
6233
|
+
const value = record$4(body);
|
|
5617
6234
|
if (value === void 0) throw new JiraError("invalid-request", "The request body is invalid.");
|
|
5618
6235
|
return value;
|
|
5619
6236
|
}
|
|
@@ -5623,55 +6240,99 @@ function string(input, key) {
|
|
|
5623
6240
|
return value;
|
|
5624
6241
|
}
|
|
5625
6242
|
function registerJiraRoute(ctx, service) {
|
|
5626
|
-
ctx
|
|
6243
|
+
registerPluginRoute(ctx, {
|
|
6244
|
+
mode: "unary",
|
|
5627
6245
|
kind: "prefix",
|
|
5628
6246
|
path: CLAUDE_JIRA_PATH,
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
6247
|
+
methods: ["GET", "POST"],
|
|
6248
|
+
budget: "git",
|
|
6249
|
+
handler: async (io) => {
|
|
6250
|
+
const url = io.url;
|
|
5632
6251
|
try {
|
|
5633
6252
|
if (url.pathname === `/plugins/dsh-claude/jira/status`) {
|
|
5634
|
-
if (
|
|
5635
|
-
|
|
6253
|
+
if (io.method !== "GET") return {
|
|
6254
|
+
status: 405,
|
|
6255
|
+
value: { error: "method not allowed" }
|
|
6256
|
+
};
|
|
6257
|
+
return {
|
|
6258
|
+
status: 200,
|
|
6259
|
+
value: await service.status()
|
|
6260
|
+
};
|
|
5636
6261
|
}
|
|
5637
6262
|
if (url.pathname === `/plugins/dsh-claude/jira/connect`) {
|
|
5638
|
-
if (
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
6263
|
+
if (io.method !== "POST") return {
|
|
6264
|
+
status: 405,
|
|
6265
|
+
value: { error: "method not allowed" }
|
|
6266
|
+
};
|
|
6267
|
+
const input = await readJson$2(io);
|
|
6268
|
+
return {
|
|
6269
|
+
status: 200,
|
|
6270
|
+
value: await service.connect({
|
|
6271
|
+
siteUrl: string(input, "siteUrl"),
|
|
6272
|
+
email: string(input, "email"),
|
|
6273
|
+
apiToken: string(input, "apiToken")
|
|
6274
|
+
})
|
|
6275
|
+
};
|
|
5645
6276
|
}
|
|
5646
6277
|
if (url.pathname === `/plugins/dsh-claude/jira/disconnect`) {
|
|
5647
|
-
if (
|
|
6278
|
+
if (io.method !== "POST") return {
|
|
6279
|
+
status: 405,
|
|
6280
|
+
value: { error: "method not allowed" }
|
|
6281
|
+
};
|
|
5648
6282
|
await service.disconnect();
|
|
5649
|
-
return
|
|
6283
|
+
return {
|
|
6284
|
+
status: 200,
|
|
6285
|
+
value: { connected: false }
|
|
6286
|
+
};
|
|
5650
6287
|
}
|
|
5651
6288
|
if (url.pathname === `/plugins/dsh-claude/jira/assign`) {
|
|
5652
|
-
if (
|
|
5653
|
-
|
|
6289
|
+
if (io.method !== "POST") return {
|
|
6290
|
+
status: 405,
|
|
6291
|
+
value: { error: "method not allowed" }
|
|
6292
|
+
};
|
|
6293
|
+
const input = await readJson$2(io);
|
|
5654
6294
|
await service.assignToMe(string(input, "key"));
|
|
5655
|
-
return
|
|
6295
|
+
return {
|
|
6296
|
+
status: 200,
|
|
6297
|
+
value: { assigned: true }
|
|
6298
|
+
};
|
|
5656
6299
|
}
|
|
5657
6300
|
if (url.pathname === `/plugins/dsh-claude/jira/search`) {
|
|
5658
|
-
if (
|
|
5659
|
-
|
|
6301
|
+
if (io.method !== "GET") return {
|
|
6302
|
+
status: 405,
|
|
6303
|
+
value: { error: "method not allowed" }
|
|
6304
|
+
};
|
|
6305
|
+
return {
|
|
6306
|
+
status: 200,
|
|
6307
|
+
value: { tickets: await service.search(url.searchParams.get("query") ?? "") }
|
|
6308
|
+
};
|
|
5660
6309
|
}
|
|
5661
|
-
return
|
|
6310
|
+
return {
|
|
6311
|
+
status: 404,
|
|
6312
|
+
value: { error: "not found" }
|
|
6313
|
+
};
|
|
5662
6314
|
} catch (error) {
|
|
5663
|
-
if (error instanceof JiraError) return
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
6315
|
+
if (error instanceof JiraError) return {
|
|
6316
|
+
status: 409,
|
|
6317
|
+
value: {
|
|
6318
|
+
error: error.code,
|
|
6319
|
+
message: error.message
|
|
6320
|
+
}
|
|
6321
|
+
};
|
|
6322
|
+
if (error instanceof SyntaxError) return {
|
|
6323
|
+
status: 400,
|
|
6324
|
+
value: { error: "invalid-json" }
|
|
6325
|
+
};
|
|
6326
|
+
return {
|
|
6327
|
+
status: 500,
|
|
6328
|
+
value: {
|
|
6329
|
+
error: "jira-unavailable",
|
|
6330
|
+
message: "Jira is unavailable."
|
|
6331
|
+
}
|
|
6332
|
+
};
|
|
5672
6333
|
}
|
|
5673
6334
|
}
|
|
5674
|
-
})
|
|
6335
|
+
});
|
|
5675
6336
|
}
|
|
5676
6337
|
//#endregion
|
|
5677
6338
|
//#region src/ask.ts
|
|
@@ -5899,22 +6560,11 @@ var AskService = class {
|
|
|
5899
6560
|
//#region src/ask-routes.ts
|
|
5900
6561
|
const MAX_BODY_BYTES$2 = 131072;
|
|
5901
6562
|
const MAX_SESSION_ID_CHARS$2 = 1024;
|
|
6563
|
+
/** Two sessions may await an answer at once; a third evicts the oldest. */
|
|
6564
|
+
const MAX_CONCURRENT_ASKS = 2;
|
|
5902
6565
|
function record$2(value) {
|
|
5903
6566
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
5904
6567
|
}
|
|
5905
|
-
async function readJson$2(req) {
|
|
5906
|
-
const chunks = [];
|
|
5907
|
-
let size = 0;
|
|
5908
|
-
for await (const chunk of req) {
|
|
5909
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
5910
|
-
size += buffer.length;
|
|
5911
|
-
if (size > MAX_BODY_BYTES$2) throw new AskError("body-too-large", "The request body is too large.");
|
|
5912
|
-
chunks.push(buffer);
|
|
5913
|
-
}
|
|
5914
|
-
const value = record$2(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
5915
|
-
if (value === void 0) throw new AskError("invalid-request", "The request body is invalid.");
|
|
5916
|
-
return value;
|
|
5917
|
-
}
|
|
5918
6568
|
function askRequest(input) {
|
|
5919
6569
|
if (typeof input.selection !== "string" || typeof input.question !== "string" || input.context !== void 0 && typeof input.context !== "string") throw new AskError("invalid-request", "The selection and question fields are required.");
|
|
5920
6570
|
return {
|
|
@@ -5926,25 +6576,32 @@ function askRequest(input) {
|
|
|
5926
6576
|
function ndjson(res, value) {
|
|
5927
6577
|
res.write(`${JSON.stringify(value)}\n`);
|
|
5928
6578
|
}
|
|
6579
|
+
/** A stream route rather than a unary one: the answer is written as it arrives,
|
|
6580
|
+
* so the deadline stays where the work is — `ASK_TIMEOUT_MS` inside the
|
|
6581
|
+
* service, fused there with the disconnect signal — instead of a route budget
|
|
6582
|
+
* that would cut a legitimate long answer off mid-sentence. */
|
|
5929
6583
|
function registerAskRoute(ctx, service, cwdForSession, preferencesFor) {
|
|
5930
|
-
ctx
|
|
6584
|
+
registerPluginRoute(ctx, {
|
|
6585
|
+
mode: "stream",
|
|
5931
6586
|
kind: "exact",
|
|
5932
6587
|
path: CLAUDE_ASK_PATH,
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
6588
|
+
methods: ["POST"],
|
|
6589
|
+
maxConcurrent: MAX_CONCURRENT_ASKS,
|
|
6590
|
+
streamKey: (url) => url.searchParams.get("sessionId") ?? "",
|
|
6591
|
+
handler: async (res, io) => {
|
|
5937
6592
|
let cwd;
|
|
5938
6593
|
let request;
|
|
5939
6594
|
let sessionId;
|
|
5940
6595
|
try {
|
|
5941
|
-
const value = url.searchParams.get("sessionId");
|
|
6596
|
+
const value = io.url.searchParams.get("sessionId");
|
|
5942
6597
|
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$2) throw new AskError("invalid-session", "The session is invalid.");
|
|
5943
6598
|
sessionId = value;
|
|
5944
6599
|
const resolved = cwdForSession(sessionId);
|
|
5945
6600
|
if (resolved === void 0) throw new AskError("session-unavailable", "The Claude session is unavailable.");
|
|
5946
6601
|
cwd = resolved;
|
|
5947
|
-
|
|
6602
|
+
const body = record$2(await io.body(MAX_BODY_BYTES$2));
|
|
6603
|
+
if (body === void 0) throw new AskError("invalid-request", "The request body is invalid.");
|
|
6604
|
+
request = askRequest(body);
|
|
5948
6605
|
} catch (error) {
|
|
5949
6606
|
if (error instanceof AskError) return json(res, 409, {
|
|
5950
6607
|
error: error.code,
|
|
@@ -5962,17 +6619,13 @@ function registerAskRoute(ctx, service, cwdForSession, preferencesFor) {
|
|
|
5962
6619
|
"x-content-type-options": "nosniff"
|
|
5963
6620
|
});
|
|
5964
6621
|
res.flushHeaders?.();
|
|
5965
|
-
const controller = new AbortController();
|
|
5966
|
-
req.on("close", () => {
|
|
5967
|
-
controller.abort();
|
|
5968
|
-
});
|
|
5969
6622
|
try {
|
|
5970
6623
|
await service.ask(cwd, request, preferencesFor(sessionId) ?? {}, (event) => {
|
|
5971
6624
|
ndjson(res, event.type === "text" ? {
|
|
5972
6625
|
type: "delta",
|
|
5973
6626
|
text: event.text
|
|
5974
6627
|
} : event);
|
|
5975
|
-
},
|
|
6628
|
+
}, io.signal);
|
|
5976
6629
|
ndjson(res, { type: "done" });
|
|
5977
6630
|
} catch (error) {
|
|
5978
6631
|
ndjson(res, {
|
|
@@ -5980,11 +6633,9 @@ function registerAskRoute(ctx, service, cwdForSession, preferencesFor) {
|
|
|
5980
6633
|
code: error instanceof AskError ? error.code : "ask-unavailable",
|
|
5981
6634
|
message: error instanceof AskError ? error.message : "The question could not be answered."
|
|
5982
6635
|
});
|
|
5983
|
-
} finally {
|
|
5984
|
-
res.end();
|
|
5985
6636
|
}
|
|
5986
6637
|
}
|
|
5987
|
-
})
|
|
6638
|
+
});
|
|
5988
6639
|
}
|
|
5989
6640
|
//#endregion
|
|
5990
6641
|
//#region src/review-comment-routes.ts
|
|
@@ -5993,16 +6644,15 @@ const MAX_SESSION_ID_CHARS$1 = 1024;
|
|
|
5993
6644
|
function record$1(value) {
|
|
5994
6645
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
5995
6646
|
}
|
|
5996
|
-
async function readJson$1(
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
chunks.push(buffer);
|
|
6647
|
+
async function readJson$1(io) {
|
|
6648
|
+
let parsed;
|
|
6649
|
+
try {
|
|
6650
|
+
parsed = await io.body(MAX_BODY_BYTES$1);
|
|
6651
|
+
} catch (error) {
|
|
6652
|
+
if (error instanceof SyntaxError) throw error;
|
|
6653
|
+
throw new ReviewCommentError("body-too-large", "The request body is too large.");
|
|
6004
6654
|
}
|
|
6005
|
-
const value = record$1(
|
|
6655
|
+
const value = record$1(parsed);
|
|
6006
6656
|
if (value === void 0) throw new ReviewCommentError("invalid-request", "The request body is invalid.");
|
|
6007
6657
|
return value;
|
|
6008
6658
|
}
|
|
@@ -6012,45 +6662,68 @@ function sessionIdFromUrl(url) {
|
|
|
6012
6662
|
return value;
|
|
6013
6663
|
}
|
|
6014
6664
|
function registerReviewCommentRoute(ctx, store, ownsSession) {
|
|
6015
|
-
ctx
|
|
6665
|
+
registerPluginRoute(ctx, {
|
|
6666
|
+
mode: "unary",
|
|
6016
6667
|
kind: "prefix",
|
|
6017
6668
|
path: CLAUDE_REVIEW_COMMENT_PATH,
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6669
|
+
methods: ["POST"],
|
|
6670
|
+
budget: "fast",
|
|
6671
|
+
handler: async (io) => {
|
|
6021
6672
|
try {
|
|
6022
|
-
const sessionId = sessionIdFromUrl(url);
|
|
6673
|
+
const sessionId = sessionIdFromUrl(io.url);
|
|
6023
6674
|
if (!ownsSession(sessionId)) throw new ReviewCommentError("session-unavailable", "The Claude session is unavailable.");
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6675
|
+
const pathname = io.url.pathname;
|
|
6676
|
+
if (pathname === "/plugins/dsh-claude/review-comments") {
|
|
6677
|
+
const input = await readJson$1(io);
|
|
6678
|
+
return {
|
|
6679
|
+
status: 200,
|
|
6680
|
+
value: { comment: store.add(sessionId, {
|
|
6681
|
+
path: input.path,
|
|
6682
|
+
line: input.line,
|
|
6683
|
+
startLine: input.startLine,
|
|
6684
|
+
side: input.side,
|
|
6685
|
+
text: input.text
|
|
6686
|
+
}) }
|
|
6687
|
+
};
|
|
6033
6688
|
}
|
|
6034
|
-
if (
|
|
6035
|
-
|
|
6036
|
-
|
|
6689
|
+
if (pathname === `/plugins/dsh-claude/review-comments/clear`) return {
|
|
6690
|
+
status: 200,
|
|
6691
|
+
value: { removed: store.drain(sessionId).length }
|
|
6692
|
+
};
|
|
6693
|
+
if (pathname === `/plugins/dsh-claude/review-comments/remove`) {
|
|
6694
|
+
const input = await readJson$1(io);
|
|
6037
6695
|
if (typeof input.id !== "string" || input.id.length === 0 || input.id.length > 128) throw new ReviewCommentError("invalid-request", "The comment id is invalid.");
|
|
6038
|
-
return
|
|
6696
|
+
return {
|
|
6697
|
+
status: 200,
|
|
6698
|
+
value: { removed: store.remove(sessionId, input.id) }
|
|
6699
|
+
};
|
|
6039
6700
|
}
|
|
6040
|
-
return
|
|
6701
|
+
return {
|
|
6702
|
+
status: 404,
|
|
6703
|
+
value: { error: "not found" }
|
|
6704
|
+
};
|
|
6041
6705
|
} catch (error) {
|
|
6042
|
-
if (error instanceof ReviewCommentError) return
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6706
|
+
if (error instanceof ReviewCommentError) return {
|
|
6707
|
+
status: 409,
|
|
6708
|
+
value: {
|
|
6709
|
+
error: error.code,
|
|
6710
|
+
message: error.message
|
|
6711
|
+
}
|
|
6712
|
+
};
|
|
6713
|
+
if (error instanceof SyntaxError) return {
|
|
6714
|
+
status: 400,
|
|
6715
|
+
value: { error: "invalid-json" }
|
|
6716
|
+
};
|
|
6717
|
+
return {
|
|
6718
|
+
status: 500,
|
|
6719
|
+
value: {
|
|
6720
|
+
error: "review-comment-unavailable",
|
|
6721
|
+
message: "Review comments are unavailable."
|
|
6722
|
+
}
|
|
6723
|
+
};
|
|
6051
6724
|
}
|
|
6052
6725
|
}
|
|
6053
|
-
})
|
|
6726
|
+
});
|
|
6054
6727
|
}
|
|
6055
6728
|
//#endregion
|
|
6056
6729
|
//#region src/client-diagnostics-routes.ts
|
|
@@ -6058,17 +6731,6 @@ function registerReviewCommentRoute(ctx, store, ownsSession) {
|
|
|
6058
6731
|
const MAX_DIAGNOSTIC_BYTES = 8192;
|
|
6059
6732
|
const MAX_DETAIL_CHARS = 2e3;
|
|
6060
6733
|
const MAX_KIND_CHARS = 60;
|
|
6061
|
-
async function readBody(req) {
|
|
6062
|
-
const chunks = [];
|
|
6063
|
-
let size = 0;
|
|
6064
|
-
for await (const chunk of req) {
|
|
6065
|
-
const buffer = chunk;
|
|
6066
|
-
size += buffer.length;
|
|
6067
|
-
if (size > MAX_DIAGNOSTIC_BYTES) throw new Error("diagnostic too large");
|
|
6068
|
-
chunks.push(buffer);
|
|
6069
|
-
}
|
|
6070
|
-
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
6071
|
-
}
|
|
6072
6734
|
/** `POST <path>` with `{ kind, detail }`: write one renderer finding to the Host log.
|
|
6073
6735
|
*
|
|
6074
6736
|
* The renderer has no other way to speak. A Slot entry that throws is caught
|
|
@@ -6078,25 +6740,38 @@ async function readBody(req) {
|
|
|
6078
6740
|
* Every finding here is data written by this package's own client half, but
|
|
6079
6741
|
* it is still bounded and redacted like any other untrusted input. */
|
|
6080
6742
|
function registerClaudeClientDiagnosticsRoute(ctx) {
|
|
6081
|
-
ctx
|
|
6743
|
+
registerPluginRoute(ctx, {
|
|
6744
|
+
mode: "unary",
|
|
6082
6745
|
kind: "exact",
|
|
6083
6746
|
path: CLAUDE_CLIENT_DIAGNOSTICS_PATH,
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6747
|
+
methods: ["POST"],
|
|
6748
|
+
budget: "fast",
|
|
6749
|
+
handler: async (io) => {
|
|
6087
6750
|
try {
|
|
6088
|
-
const body = await
|
|
6751
|
+
const body = await io.body(MAX_DIAGNOSTIC_BYTES);
|
|
6089
6752
|
const kind = typeof body?.kind === "string" ? body.kind.slice(0, MAX_KIND_CHARS) : "unknown";
|
|
6090
6753
|
const detail = typeof body?.detail === "string" ? body.detail : "";
|
|
6091
|
-
if (detail === "") return
|
|
6754
|
+
if (detail === "") return {
|
|
6755
|
+
status: 400,
|
|
6756
|
+
value: { error: "invalid-request" }
|
|
6757
|
+
};
|
|
6092
6758
|
ctx.logger.warn(`dsh-claude client [${kind}]: ${redactText(detail, MAX_DETAIL_CHARS)}`);
|
|
6093
|
-
return
|
|
6759
|
+
return {
|
|
6760
|
+
status: 200,
|
|
6761
|
+
value: { ok: true }
|
|
6762
|
+
};
|
|
6094
6763
|
} catch (error) {
|
|
6095
|
-
if (error instanceof SyntaxError) return
|
|
6096
|
-
|
|
6764
|
+
if (error instanceof SyntaxError) return {
|
|
6765
|
+
status: 400,
|
|
6766
|
+
value: { error: "invalid-json" }
|
|
6767
|
+
};
|
|
6768
|
+
return {
|
|
6769
|
+
status: 400,
|
|
6770
|
+
value: { error: "invalid-request" }
|
|
6771
|
+
};
|
|
6097
6772
|
}
|
|
6098
6773
|
}
|
|
6099
|
-
})
|
|
6774
|
+
});
|
|
6100
6775
|
}
|
|
6101
6776
|
//#endregion
|
|
6102
6777
|
//#region src/rewind-routes.ts
|
|
@@ -6105,55 +6780,78 @@ const MAX_SESSION_ID_CHARS = 1024;
|
|
|
6105
6780
|
function record(value) {
|
|
6106
6781
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
6107
6782
|
}
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
if (
|
|
6115
|
-
|
|
6783
|
+
/** An oversized body fails the same field checks as a missing one; only
|
|
6784
|
+
* malformed JSON is worth reporting separately. */
|
|
6785
|
+
async function readJson(io) {
|
|
6786
|
+
try {
|
|
6787
|
+
return record(await io.body(MAX_BODY_BYTES));
|
|
6788
|
+
} catch (error) {
|
|
6789
|
+
if (error instanceof SyntaxError) throw error;
|
|
6790
|
+
return;
|
|
6116
6791
|
}
|
|
6117
|
-
return record(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
6118
6792
|
}
|
|
6119
6793
|
/** `POST <path>` with `{ sessionId, seq }`: hide that surface event and every
|
|
6120
6794
|
* later one, and arm Claude to resume before the turn it opened. */
|
|
6121
6795
|
function registerClaudeRewindRoute(ctx, sidecar, access) {
|
|
6122
|
-
ctx
|
|
6796
|
+
registerPluginRoute(ctx, {
|
|
6797
|
+
mode: "unary",
|
|
6123
6798
|
kind: "exact",
|
|
6124
6799
|
path: CLAUDE_REWIND_PATH,
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6800
|
+
methods: ["POST"],
|
|
6801
|
+
budget: "git",
|
|
6802
|
+
handler: async (io) => {
|
|
6128
6803
|
try {
|
|
6129
|
-
const input = await readJson(
|
|
6804
|
+
const input = await readJson(io);
|
|
6130
6805
|
const sessionId = input?.sessionId;
|
|
6131
6806
|
const seq = input?.seq;
|
|
6132
|
-
if (typeof sessionId !== "string" || sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS || typeof seq !== "number" || !Number.isSafeInteger(seq) || seq < 0) return
|
|
6807
|
+
if (typeof sessionId !== "string" || sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS || typeof seq !== "number" || !Number.isSafeInteger(seq) || seq < 0) return {
|
|
6808
|
+
status: 400,
|
|
6809
|
+
value: { error: "invalid-request" }
|
|
6810
|
+
};
|
|
6133
6811
|
const events = access.eventsFor(sessionId);
|
|
6134
|
-
if (events === void 0) return
|
|
6135
|
-
|
|
6812
|
+
if (events === void 0) return {
|
|
6813
|
+
status: 409,
|
|
6814
|
+
value: { error: "session-unavailable" }
|
|
6815
|
+
};
|
|
6816
|
+
if (access.busy(sessionId)) return {
|
|
6817
|
+
status: 409,
|
|
6818
|
+
value: { error: "session-busy" }
|
|
6819
|
+
};
|
|
6136
6820
|
const planned = planRewind((await sidecar.read(sessionId)).rewind ?? EMPTY_REWIND_STATE, events, seq);
|
|
6137
|
-
if (planned === void 0) return
|
|
6821
|
+
if (planned === void 0) return {
|
|
6822
|
+
status: 409,
|
|
6823
|
+
value: { error: "seq-unavailable" }
|
|
6824
|
+
};
|
|
6138
6825
|
await sidecar.writeRewind(sessionId, planned);
|
|
6139
6826
|
await access.reset(sessionId);
|
|
6140
|
-
return
|
|
6827
|
+
return {
|
|
6828
|
+
status: 200,
|
|
6829
|
+
value: { ranges: planned.ranges }
|
|
6830
|
+
};
|
|
6141
6831
|
} catch (error) {
|
|
6142
|
-
if (error instanceof SyntaxError) return
|
|
6143
|
-
|
|
6832
|
+
if (error instanceof SyntaxError) return {
|
|
6833
|
+
status: 400,
|
|
6834
|
+
value: { error: "invalid-json" }
|
|
6835
|
+
};
|
|
6836
|
+
return {
|
|
6837
|
+
status: 500,
|
|
6838
|
+
value: { error: "rewind-unavailable" }
|
|
6839
|
+
};
|
|
6144
6840
|
}
|
|
6145
6841
|
}
|
|
6146
|
-
})
|
|
6842
|
+
});
|
|
6147
6843
|
}
|
|
6148
6844
|
//#endregion
|
|
6149
6845
|
//#region src/update-routes.ts
|
|
6150
6846
|
const PLUGIN_PACKAGE_NAME = "@norman-else/dsh-claude";
|
|
6151
|
-
const UPDATE_TIMEOUT_MS = 3e4;
|
|
6152
|
-
const CHECK_TIMEOUT_MS = 1e4;
|
|
6153
6847
|
const MAX_MANIFEST_BYTES = 262144;
|
|
6154
6848
|
const MAX_UPDATE_OUTPUT_BYTES = 32768;
|
|
6155
6849
|
const SEMVER = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
6156
6850
|
const PROFILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
6851
|
+
/** The route owns the deadline now. A direct caller with nothing to cancel
|
|
6852
|
+
* against gets a signal that never fires, rather than a second timeout
|
|
6853
|
+
* competing with the budget the route already declared. */
|
|
6854
|
+
const NEVER_ABORTS = new AbortController().signal;
|
|
6157
6855
|
function safeMessage$1(error) {
|
|
6158
6856
|
return redactText(error instanceof Error ? error.message : String(error), 500);
|
|
6159
6857
|
}
|
|
@@ -6272,7 +6970,7 @@ async function packageContext(deps) {
|
|
|
6272
6970
|
...installation === void 0 ? {} : { installation }
|
|
6273
6971
|
};
|
|
6274
6972
|
}
|
|
6275
|
-
async function checkPluginUpdate(deps = {}) {
|
|
6973
|
+
async function checkPluginUpdate(deps = {}, signal = NEVER_ABORTS) {
|
|
6276
6974
|
try {
|
|
6277
6975
|
const { version, installation } = await packageContext(deps);
|
|
6278
6976
|
if (installation === void 0) return {
|
|
@@ -6299,7 +6997,7 @@ async function checkPluginUpdate(deps = {}) {
|
|
|
6299
6997
|
restartRequired: false,
|
|
6300
6998
|
message: "This installation source cannot be updated from the npm registry"
|
|
6301
6999
|
};
|
|
6302
|
-
const latest = await (deps.fetchLatest ?? registryLatest)(
|
|
7000
|
+
const latest = await (deps.fetchLatest ?? registryLatest)(signal);
|
|
6303
7001
|
const comparison = compareVersions(version, latest);
|
|
6304
7002
|
return {
|
|
6305
7003
|
currentVersion: version,
|
|
@@ -6325,10 +7023,10 @@ async function verifyInstalledVersion(installation, expectedVersion) {
|
|
|
6325
7023
|
const installedManifest = await readManifest(join(installation.profileDir, "node_modules", ...PLUGIN_PACKAGE_NAME.split("/"), "package.json"));
|
|
6326
7024
|
if (installedManifest.name !== "@norman-else/dsh-claude" || installedManifest.version !== expectedVersion) throw new Error("DSH plugin update completed without installing the requested version");
|
|
6327
7025
|
}
|
|
6328
|
-
async function updatePlugin(deps = {}) {
|
|
7026
|
+
async function updatePlugin(deps = {}, signal = NEVER_ABORTS) {
|
|
6329
7027
|
const { version, installation } = await packageContext(deps);
|
|
6330
7028
|
if (installation === void 0 || installation.source !== "registry") throw new Error("Plugin update is unavailable for this installation");
|
|
6331
|
-
const latest = await (deps.fetchLatest ?? registryLatest)(
|
|
7029
|
+
const latest = await (deps.fetchLatest ?? registryLatest)(signal);
|
|
6332
7030
|
if (compareVersions(version, latest) >= 0) return {
|
|
6333
7031
|
currentVersion: version,
|
|
6334
7032
|
latestVersion: latest,
|
|
@@ -6340,7 +7038,6 @@ async function updatePlugin(deps = {}) {
|
|
|
6340
7038
|
const resolveExecutable = deps.resolveExecutable;
|
|
6341
7039
|
const spawn = deps.spawn;
|
|
6342
7040
|
if (resolveExecutable === void 0 || spawn === void 0) throw new Error("DSH update runtime is unavailable");
|
|
6343
|
-
const signal = AbortSignal.timeout(UPDATE_TIMEOUT_MS);
|
|
6344
7041
|
const handle = spawn({
|
|
6345
7042
|
argv: [
|
|
6346
7043
|
await resolveExecutable("dsh", {}, signal),
|
|
@@ -6383,27 +7080,35 @@ function registerClaudeUpdateRoutes(ctx, runtime, deps = {}) {
|
|
|
6383
7080
|
resolveExecutable: runtime.resolveExecutable.bind(runtime),
|
|
6384
7081
|
spawn: runtime.spawn.bind(runtime)
|
|
6385
7082
|
};
|
|
6386
|
-
|
|
7083
|
+
const routes = [{
|
|
6387
7084
|
path: CLAUDE_UPDATE_CHECK_PATH,
|
|
6388
7085
|
method: "GET",
|
|
6389
|
-
run: () => checkPluginUpdate(shared)
|
|
7086
|
+
run: (signal) => checkPluginUpdate(shared, signal)
|
|
6390
7087
|
}, {
|
|
6391
7088
|
path: CLAUDE_UPDATE_PATH,
|
|
6392
7089
|
method: "POST",
|
|
6393
|
-
run: () => updatePlugin(shared)
|
|
6394
|
-
}]
|
|
7090
|
+
run: (signal) => updatePlugin(shared, signal)
|
|
7091
|
+
}];
|
|
7092
|
+
for (const route of routes) registerPluginRoute(ctx, {
|
|
7093
|
+
mode: "unary",
|
|
6395
7094
|
kind: "exact",
|
|
6396
7095
|
path: route.path,
|
|
6397
|
-
|
|
6398
|
-
|
|
6399
|
-
|
|
7096
|
+
methods: [route.method],
|
|
7097
|
+
budget: "remote",
|
|
7098
|
+
handler: async (io) => {
|
|
6400
7099
|
try {
|
|
6401
|
-
|
|
7100
|
+
return {
|
|
7101
|
+
status: 200,
|
|
7102
|
+
value: await route.run(io.signal)
|
|
7103
|
+
};
|
|
6402
7104
|
} catch (error) {
|
|
6403
|
-
|
|
7105
|
+
return {
|
|
7106
|
+
status: 500,
|
|
7107
|
+
value: { error: safeMessage$1(error) }
|
|
7108
|
+
};
|
|
6404
7109
|
}
|
|
6405
7110
|
}
|
|
6406
|
-
})
|
|
7111
|
+
});
|
|
6407
7112
|
}
|
|
6408
7113
|
//#endregion
|
|
6409
7114
|
//#region src/plan-usage-routes.ts
|
|
@@ -6420,22 +7125,32 @@ function safeMessage(error) {
|
|
|
6420
7125
|
* throwaway probe process so a refresh never depends on, or disturbs, a live
|
|
6421
7126
|
* Claude session. */
|
|
6422
7127
|
function registerPlanUsageRoute(ctx, probe, now = Date.now) {
|
|
6423
|
-
ctx
|
|
7128
|
+
registerPluginRoute(ctx, {
|
|
7129
|
+
mode: "unary",
|
|
6424
7130
|
kind: "exact",
|
|
6425
7131
|
path: CLAUDE_USAGE_PATH,
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
6429
|
-
if (
|
|
7132
|
+
methods: ["GET", "POST"],
|
|
7133
|
+
budget: "git",
|
|
7134
|
+
handler: async (io) => {
|
|
7135
|
+
if (io.method === "GET") return {
|
|
7136
|
+
status: 200,
|
|
7137
|
+
value: latestPlanUsage() ?? EMPTY
|
|
7138
|
+
};
|
|
6430
7139
|
try {
|
|
6431
7140
|
const report = await probe(now());
|
|
6432
7141
|
recordPlanUsage(report);
|
|
6433
|
-
|
|
7142
|
+
return {
|
|
7143
|
+
status: 200,
|
|
7144
|
+
value: report
|
|
7145
|
+
};
|
|
6434
7146
|
} catch (error) {
|
|
6435
|
-
|
|
7147
|
+
return {
|
|
7148
|
+
status: 500,
|
|
7149
|
+
value: { error: safeMessage(error) }
|
|
7150
|
+
};
|
|
6436
7151
|
}
|
|
6437
7152
|
}
|
|
6438
|
-
})
|
|
7153
|
+
});
|
|
6439
7154
|
}
|
|
6440
7155
|
//#endregion
|
|
6441
7156
|
//#region src/global-settings.ts
|
|
@@ -6564,6 +7279,57 @@ const WORKTREE_BRANCH_PREFIX = {
|
|
|
6564
7279
|
else document.worktreeBranchPrefix = value;
|
|
6565
7280
|
}
|
|
6566
7281
|
};
|
|
7282
|
+
/** Which renderer draws Claude's visible output. Plugin settings, not Claude's:
|
|
7283
|
+
* the CLI has no opinion about how DSH paints a turn. The option labels stay
|
|
7284
|
+
* machine-readable ids; the Client translates the two known values. */
|
|
7285
|
+
const RENDERER = {
|
|
7286
|
+
key: "renderer",
|
|
7287
|
+
kind: "select",
|
|
7288
|
+
document: "plugin",
|
|
7289
|
+
effect: "next-turn",
|
|
7290
|
+
async options() {
|
|
7291
|
+
return CLAUDE_RENDER_MODES.map((value) => ({
|
|
7292
|
+
value,
|
|
7293
|
+
label: value,
|
|
7294
|
+
source: "built-in"
|
|
7295
|
+
}));
|
|
7296
|
+
},
|
|
7297
|
+
read(document) {
|
|
7298
|
+
const value = document.renderer;
|
|
7299
|
+
return isClaudeRenderMode(value) ? value : DEFAULT_CLAUDE_RENDER_MODE;
|
|
7300
|
+
},
|
|
7301
|
+
apply(document, value) {
|
|
7302
|
+
if (!isClaudeRenderMode(value)) throw new Error("Invalid value for global setting renderer");
|
|
7303
|
+
if (value === "plugin") delete document.renderer;
|
|
7304
|
+
else document.renderer = value;
|
|
7305
|
+
}
|
|
7306
|
+
};
|
|
7307
|
+
/** Whether Claude's prose gets the highlight palette. Presentation only: no
|
|
7308
|
+
* record carries it and nothing on the server reads it back, so unlike
|
|
7309
|
+
* {@link RENDERER} the switch lands the moment the Client rewrites its own
|
|
7310
|
+
* stylesheet — hence 'immediate' rather than 'next-turn'. */
|
|
7311
|
+
const PROSE = {
|
|
7312
|
+
key: "prose",
|
|
7313
|
+
kind: "select",
|
|
7314
|
+
document: "plugin",
|
|
7315
|
+
effect: "immediate",
|
|
7316
|
+
async options() {
|
|
7317
|
+
return CLAUDE_PROSE_MODES.map((value) => ({
|
|
7318
|
+
value,
|
|
7319
|
+
label: value,
|
|
7320
|
+
source: "built-in"
|
|
7321
|
+
}));
|
|
7322
|
+
},
|
|
7323
|
+
read(document) {
|
|
7324
|
+
const value = document.prose;
|
|
7325
|
+
return isClaudeProseMode(value) ? value : DEFAULT_CLAUDE_PROSE_MODE;
|
|
7326
|
+
},
|
|
7327
|
+
apply(document, value) {
|
|
7328
|
+
if (!isClaudeProseMode(value)) throw new Error("Invalid value for global setting prose");
|
|
7329
|
+
if (value === "plain") delete document.prose;
|
|
7330
|
+
else document.prose = value;
|
|
7331
|
+
}
|
|
7332
|
+
};
|
|
6567
7333
|
function isBoundedInteger(value, min, max) {
|
|
6568
7334
|
return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max;
|
|
6569
7335
|
}
|
|
@@ -6587,6 +7353,8 @@ function integerSetting(key, min, max, defaultFor) {
|
|
|
6587
7353
|
}
|
|
6588
7354
|
const DESCRIPTORS = [
|
|
6589
7355
|
OUTPUT_STYLE,
|
|
7356
|
+
RENDERER,
|
|
7357
|
+
PROSE,
|
|
6590
7358
|
WORKTREE_BRANCH_PREFIX,
|
|
6591
7359
|
integerSetting("maxProcesses", 1, MAX_PROCESSES_LIMIT, (limits) => limits.maxProcesses),
|
|
6592
7360
|
integerSetting("idleTimeoutMinutes", 1, MAX_IDLE_TIMEOUT_MINUTES, (limits) => Math.max(1, Math.round(limits.idleTimeoutMs / 6e4)))
|
|
@@ -6648,6 +7416,17 @@ async function readSupervisorLimitOverrides(deps = {}) {
|
|
|
6648
7416
|
...isBoundedInteger(idleTimeoutMinutes, 1, MAX_IDLE_TIMEOUT_MINUTES) ? { idleTimeoutMs: idleTimeoutMinutes * 6e4 } : {}
|
|
6649
7417
|
};
|
|
6650
7418
|
}
|
|
7419
|
+
/** The renderer the Host should produce output for. A missing, unreadable, or
|
|
7420
|
+
* malformed plugin settings file keeps today's plugin-owned transcript. */
|
|
7421
|
+
async function readRenderMode(deps = {}) {
|
|
7422
|
+
let document;
|
|
7423
|
+
try {
|
|
7424
|
+
document = await readDocument(pathsFor(deps).pluginSettingsFile);
|
|
7425
|
+
} catch {
|
|
7426
|
+
return DEFAULT_CLAUDE_RENDER_MODE;
|
|
7427
|
+
}
|
|
7428
|
+
return isClaudeRenderMode(document.renderer) ? document.renderer : DEFAULT_CLAUDE_RENDER_MODE;
|
|
7429
|
+
}
|
|
6651
7430
|
async function readWorktreeBranchPrefix(deps = {}) {
|
|
6652
7431
|
const paths = pathsFor(deps);
|
|
6653
7432
|
return WORKTREE_BRANCH_PREFIX.read(await readDocument(paths.pluginSettingsFile));
|
|
@@ -6693,37 +7472,34 @@ function updateGlobalSettings(changes, deps = {}) {
|
|
|
6693
7472
|
pendingWrite = operation;
|
|
6694
7473
|
return operation;
|
|
6695
7474
|
}
|
|
6696
|
-
async function requestJson(
|
|
6697
|
-
const
|
|
6698
|
-
if (!Number.isFinite(declared) || declared < 0 || declared > MAX_REQUEST_BYTES) throw new Error("Request body is too large");
|
|
6699
|
-
const chunks = [];
|
|
6700
|
-
let bytes = 0;
|
|
6701
|
-
for await (const chunk of req) {
|
|
6702
|
-
const data = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
6703
|
-
bytes += data.byteLength;
|
|
6704
|
-
if (bytes > MAX_REQUEST_BYTES) throw new Error("Request body is too large");
|
|
6705
|
-
chunks.push(data);
|
|
6706
|
-
}
|
|
6707
|
-
const body = object(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
7475
|
+
async function requestJson(io) {
|
|
7476
|
+
const body = object(await io.body(MAX_REQUEST_BYTES));
|
|
6708
7477
|
if (body === void 0 || Object.keys(body).some((key) => key !== "changes")) throw new Error("Invalid global settings request");
|
|
6709
7478
|
return body.changes;
|
|
6710
7479
|
}
|
|
6711
7480
|
function registerClaudeGlobalSettingsRoute(ctx, deps = {}) {
|
|
6712
|
-
ctx
|
|
7481
|
+
registerPluginRoute(ctx, {
|
|
7482
|
+
mode: "unary",
|
|
6713
7483
|
kind: "exact",
|
|
6714
7484
|
path: CLAUDE_GLOBAL_SETTINGS_PATH,
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
7485
|
+
methods: ["GET", "PATCH"],
|
|
7486
|
+
budget: "fast",
|
|
7487
|
+
handler: async (io) => {
|
|
6718
7488
|
try {
|
|
6719
|
-
const result =
|
|
6720
|
-
if (
|
|
6721
|
-
|
|
7489
|
+
const result = io.method === "GET" ? await readGlobalSettings(deps) : await updateGlobalSettings(await requestJson(io), deps);
|
|
7490
|
+
if (io.method === "PATCH") await deps.onUpdated?.();
|
|
7491
|
+
return {
|
|
7492
|
+
status: 200,
|
|
7493
|
+
value: result
|
|
7494
|
+
};
|
|
6722
7495
|
} catch (error) {
|
|
6723
|
-
|
|
7496
|
+
return {
|
|
7497
|
+
status: 400,
|
|
7498
|
+
value: { error: error instanceof Error ? error.message : "Invalid global settings request" }
|
|
7499
|
+
};
|
|
6724
7500
|
}
|
|
6725
7501
|
}
|
|
6726
|
-
})
|
|
7502
|
+
});
|
|
6727
7503
|
}
|
|
6728
7504
|
//#endregion
|
|
6729
7505
|
//#region src/index.ts
|
|
@@ -6862,14 +7638,16 @@ async function apply(ctx, config) {
|
|
|
6862
7638
|
const supervisorConfig = {
|
|
6863
7639
|
executablePath: "",
|
|
6864
7640
|
defaultModel: config.model ?? "default",
|
|
7641
|
+
renderMode: DEFAULT_CLAUDE_RENDER_MODE,
|
|
6865
7642
|
...defaultLimits
|
|
6866
7643
|
};
|
|
6867
|
-
const
|
|
7644
|
+
const applySettingsOverrides = async () => {
|
|
6868
7645
|
const overrides = await readSupervisorLimitOverrides();
|
|
6869
7646
|
supervisorConfig.idleTimeoutMs = overrides.idleTimeoutMs ?? defaultLimits.idleTimeoutMs;
|
|
6870
7647
|
supervisorConfig.maxProcesses = overrides.maxProcesses ?? defaultLimits.maxProcesses;
|
|
7648
|
+
supervisorConfig.renderMode = await readRenderMode();
|
|
6871
7649
|
};
|
|
6872
|
-
await
|
|
7650
|
+
await applySettingsOverrides();
|
|
6873
7651
|
const sidecar = new ClaudeSidecarRepository();
|
|
6874
7652
|
const repositoryStatus = new RepositoryStatusService(ctx.subprocess);
|
|
6875
7653
|
const repositorySetup = new RepositorySetupService(ctx.subprocess, { branchPrefix: () => readWorktreeBranchPrefix() });
|
|
@@ -6886,7 +7664,7 @@ async function apply(ctx, config) {
|
|
|
6886
7664
|
let resolutionError;
|
|
6887
7665
|
try {
|
|
6888
7666
|
supervisorConfig.executablePath = (await resolveClaudeExecutable(ctx.subprocess, config.executablePath === void 0 || config.executablePath.length === 0 ? void 0 : config.executablePath)).path;
|
|
6889
|
-
ctx.llm.registerAdapter([...CLAUDE_CODE_PROVIDER_IDS], createClaudeCodeAdapter(supervisor, ctx.agents, ctx.attachments, (agent) => ctx.agentPresets.composedPreset(agent.ctx), (sessionId) => reviewComments.drain(sessionId)));
|
|
7667
|
+
ctx.llm.registerAdapter([...CLAUDE_CODE_PROVIDER_IDS], createClaudeCodeAdapter(supervisor, ctx.agents, ctx.attachments, (agent) => ctx.agentPresets.composedPreset(agent.ctx), (sessionId) => reviewComments.drain(sessionId), () => supervisorConfig.renderMode));
|
|
6890
7668
|
ctx.effect(() => {
|
|
6891
7669
|
const mounted = /* @__PURE__ */ new Map();
|
|
6892
7670
|
const pending = /* @__PURE__ */ new Set();
|
|
@@ -6975,7 +7753,7 @@ async function apply(ctx, config) {
|
|
|
6975
7753
|
registerClaudeUpdateRoutes(webCtx, webCtx.subprocess, { ...typeof desktopActions?.requestRestart === "function" ? { requestRestart: desktopActions.requestRestart.bind(desktopActions) } : {} });
|
|
6976
7754
|
registerClaudeGlobalSettingsRoute(webCtx, {
|
|
6977
7755
|
defaultLimits,
|
|
6978
|
-
onUpdated:
|
|
7756
|
+
onUpdated: applySettingsOverrides
|
|
6979
7757
|
});
|
|
6980
7758
|
registerRepositorySetupRoute(webCtx, repositorySetup);
|
|
6981
7759
|
registerRepositoryStatusRoute(webCtx, repositoryStatus);
|