@norman-else/dsh-claude 0.1.40 → 0.1.42
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 +7 -3
- package/lib/bin.mjs +1 -1
- package/lib/client.d.ts +47 -1
- package/lib/client.js +2192 -523
- package/lib/client.js.map +1 -1
- package/lib/{events-OhBoFNKO.mjs → events-oovRTmX7.mjs} +10 -2
- package/lib/events-oovRTmX7.mjs.map +1 -0
- package/lib/index.d.mts +56 -3
- package/lib/index.mjs +1424 -202
- package/lib/index.mjs.map +1 -1
- package/lib/{presenters-BBoM1Ju1.mjs → presenters-BVWj7u0a.mjs} +21 -2
- package/lib/{presenters-BBoM1Ju1.mjs.map → presenters-BVWj7u0a.mjs.map} +1 -1
- package/lib/{preset-installer-loenwnLS.mjs → preset-installer-JUktnfwS.mjs} +2 -2
- package/lib/{preset-installer-loenwnLS.mjs.map → preset-installer-JUktnfwS.mjs.map} +1 -1
- package/lib/preset-route.mjs +2 -2
- package/package.json +1 -1
- package/lib/events-OhBoFNKO.mjs.map +0 -1
package/lib/index.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
import { a as projectClaudeCommands, i as CLAUDE_COMMANDS_SERVICE, r as dynamicPresenterDefinition, t as CLAUDE_PRESENTER_NAMES } from "./presenters-
|
|
3
|
-
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-
|
|
1
|
+
import { A as CLAUDE_REPOSITORY_FEEDBACK_PATH, B as DEFAULT_CLAUDE_RENDER_MODE, C as CLAUDE_PROJECTION_PATH, D as CLAUDE_PROSE_MODES, E as CLAUDE_PROMPT_REFINE_PATH, F as CLAUDE_REWIND_PATH, G as isClaudeRenderMode, H as TASK_TOOL_NAMES, I as CLAUDE_UPDATE_CHECK_PATH, L as CLAUDE_UPDATE_PATH, M as CLAUDE_REPOSITORY_SETUP_PATH, N as CLAUDE_REPOSITORY_STATUS_PATH, O as CLAUDE_RENDER_MODES, P as CLAUDE_REVIEW_COMMENT_PATH, R as CLAUDE_USAGE_PATH, S as CLAUDE_PLAN_FEEDBACK_PATH, T as CLAUDE_PROMPT_NAME_PATH, U as isClaudeAlertMode, W as isClaudeProseMode, _ as CLAUDE_CODE_PROVIDER_IDS, a as latestClaudeTasks, b as CLAUDE_GLOBAL_SETTINGS_PATH, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_ALERT_MODES, g as CLAUDE_CODE_PROVIDER, h as CLAUDE_CODE_PRESET_ID, i as latestClaudeSessionBinding, j as CLAUDE_REPOSITORY_FILE_PATH, k as CLAUDE_REPOSITORY_ACTION_PATH, l as redactText, m as CLAUDE_CLIENT_DIAGNOSTICS_PATH, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_ASK_PATH, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as CLAUDE_DOCTOR_PATH, w as CLAUDE_PROMPTS_PATH, x as CLAUDE_JIRA_PATH, y as CLAUDE_EDITOR_OPEN_PATH, z as DEFAULT_CLAUDE_PROSE_MODE } from "./events-oovRTmX7.mjs";
|
|
2
|
+
import { a as projectClaudeCommands, i as CLAUDE_COMMANDS_SERVICE, r as dynamicPresenterDefinition, t as CLAUDE_PRESENTER_NAMES } from "./presenters-BVWj7u0a.mjs";
|
|
3
|
+
import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-JUktnfwS.mjs";
|
|
4
4
|
import z from "@deepseek-ai/schemastery";
|
|
5
5
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
6
|
import { chmod, mkdir, opendir, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
7
7
|
import { basename, dirname, extname, isAbsolute, join, resolve, sep } from "node:path";
|
|
8
8
|
import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
9
|
-
import { homedir } from "node:os";
|
|
9
|
+
import { homedir, tmpdir } from "node:os";
|
|
10
10
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
11
11
|
import { LlmAdapter, ReasoningEffortId, ToolCallId, createToolResultMessage } from "@deepseek-ai/dsh-llm";
|
|
12
12
|
import { EventEmitter } from "node:events";
|
|
@@ -17,7 +17,8 @@ import { StringDecoder } from "node:string_decoder";
|
|
|
17
17
|
//#region src/rewind.ts
|
|
18
18
|
const EMPTY_REWIND_STATE = {
|
|
19
19
|
ranges: [],
|
|
20
|
-
anchors: []
|
|
20
|
+
anchors: [],
|
|
21
|
+
snapshots: []
|
|
21
22
|
};
|
|
22
23
|
/** Absorb one span into an ascending, non-overlapping range list. Adjacent
|
|
23
24
|
* spans merge so a rewind of a rewind reads as one hidden block. */
|
|
@@ -50,15 +51,32 @@ function recordRewindAnchor(state, anchor) {
|
|
|
50
51
|
anchors
|
|
51
52
|
};
|
|
52
53
|
}
|
|
54
|
+
/** Record the working tree one turn was admitted against, replacing a re-run
|
|
55
|
+
* turn's. */
|
|
56
|
+
function recordRewindSnapshot(state, snapshot) {
|
|
57
|
+
const snapshots = [...state.snapshots.filter((item) => item.turn !== snapshot.turn), snapshot].sort((left, right) => left.turn - right.turn).slice(-100);
|
|
58
|
+
return {
|
|
59
|
+
...state,
|
|
60
|
+
snapshots
|
|
61
|
+
};
|
|
62
|
+
}
|
|
53
63
|
/** The turn a surface seq belongs to: the first turn opened at or after it.
|
|
54
64
|
* A message accepted but never run belongs to no logged turn, so nothing
|
|
55
65
|
* Claude holds is discarded and every anchor stays valid. */
|
|
56
66
|
function turnAtOrAfter(events, seq) {
|
|
57
67
|
for (const event of events) if (event.seq >= seq && event.type === "turn/start") return event.data.turn;
|
|
58
68
|
}
|
|
69
|
+
/** The working tree a rewind at `seq` restores: the snapshot of the first turn
|
|
70
|
+
* it discards. Undefined when nothing is discarded, or when that turn ran
|
|
71
|
+
* before this session captured trees. */
|
|
72
|
+
function rewindRestoreTree(state, events, seq) {
|
|
73
|
+
const turn = turnAtOrAfter(events, seq);
|
|
74
|
+
return turn === void 0 ? void 0 : state.snapshots.find((item) => item.turn === turn)?.tree;
|
|
75
|
+
}
|
|
59
76
|
/** Plan one rewind at `seq`, or undefined when the seq is not in the log.
|
|
60
|
-
* Anchors of the discarded turns go with them: after this
|
|
61
|
-
* longer holds those entries, so a later rewind must never
|
|
77
|
+
* Anchors and snapshots of the discarded turns go with them: after this
|
|
78
|
+
* rewind Claude no longer holds those entries, so a later rewind must never
|
|
79
|
+
* fork at one — nor restore a tree for a turn that no longer exists. */
|
|
62
80
|
function planRewind(state, events, seq) {
|
|
63
81
|
const last = events.at(-1)?.seq;
|
|
64
82
|
if (last === void 0 || seq > last) return void 0;
|
|
@@ -71,6 +89,7 @@ function planRewind(state, events, seq) {
|
|
|
71
89
|
end: last
|
|
72
90
|
}),
|
|
73
91
|
anchors,
|
|
92
|
+
snapshots: state.snapshots.filter((item) => item.turn < turn),
|
|
74
93
|
pending: kept === void 0 ? { fresh: true } : { resumeAt: kept.uuid }
|
|
75
94
|
};
|
|
76
95
|
}
|
|
@@ -88,7 +107,7 @@ function emptyProjection() {
|
|
|
88
107
|
activities: []
|
|
89
108
|
};
|
|
90
109
|
}
|
|
91
|
-
function record$
|
|
110
|
+
function record$14(value) {
|
|
92
111
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
93
112
|
}
|
|
94
113
|
function finiteInteger(value) {
|
|
@@ -98,7 +117,7 @@ function string$4(value, max) {
|
|
|
98
117
|
return typeof value === "string" && value.length > 0 && value.length <= max;
|
|
99
118
|
}
|
|
100
119
|
function binding(value) {
|
|
101
|
-
const input = record$
|
|
120
|
+
const input = record$14(value);
|
|
102
121
|
if (input === void 0 || !string$4(input.claudeSessionId, 512) || !string$4(input.sdkVersion, 128) || !string$4(input.cwd, 4096) || input.cliVersion !== void 0 && !string$4(input.cliVersion, 128)) return void 0;
|
|
103
122
|
return {
|
|
104
123
|
claudeSessionId: input.claudeSessionId,
|
|
@@ -129,21 +148,21 @@ const ACTIVITY_PHASES = /* @__PURE__ */ new Set([
|
|
|
129
148
|
"failed"
|
|
130
149
|
]);
|
|
131
150
|
function activity(value) {
|
|
132
|
-
const input = record$
|
|
151
|
+
const input = record$14(value);
|
|
133
152
|
if (input === void 0 || !finiteInteger(input.turn) || !finiteInteger(input.step) || !finiteInteger(input.ordinal) || typeof input.kind !== "string" || !ACTIVITY_KINDS.has(input.kind) || input.phase !== void 0 && (typeof input.phase !== "string" || !ACTIVITY_PHASES.has(input.phase))) return void 0;
|
|
134
153
|
return normalizeActivity(input);
|
|
135
154
|
}
|
|
136
155
|
function contextUsage(value) {
|
|
137
|
-
const input = record$
|
|
156
|
+
const input = record$14(value);
|
|
138
157
|
if (input === void 0 || !Array.isArray(input.categories)) return void 0;
|
|
139
158
|
return normalizeContextUsage(input);
|
|
140
159
|
}
|
|
141
160
|
function rewind(value) {
|
|
142
|
-
const input = record$
|
|
161
|
+
const input = record$14(value);
|
|
143
162
|
if (input === void 0 || !Array.isArray(input.ranges) || input.ranges.length > 200 || !Array.isArray(input.anchors) || input.anchors.length > 2e3) return void 0;
|
|
144
163
|
const ranges = [];
|
|
145
164
|
for (const item of input.ranges) {
|
|
146
|
-
const range = record$
|
|
165
|
+
const range = record$14(item);
|
|
147
166
|
if (range === void 0 || !finiteInteger(range.start) || !finiteInteger(range.end) || range.end < range.start) return void 0;
|
|
148
167
|
ranges.push({
|
|
149
168
|
start: range.start,
|
|
@@ -152,38 +171,53 @@ function rewind(value) {
|
|
|
152
171
|
}
|
|
153
172
|
const anchors = [];
|
|
154
173
|
for (const item of input.anchors) {
|
|
155
|
-
const anchor = record$
|
|
174
|
+
const anchor = record$14(item);
|
|
156
175
|
if (anchor === void 0 || !finiteInteger(anchor.turn) || !string$4(anchor.uuid, 128)) return void 0;
|
|
157
176
|
anchors.push({
|
|
158
177
|
turn: anchor.turn,
|
|
159
178
|
uuid: anchor.uuid
|
|
160
179
|
});
|
|
161
180
|
}
|
|
162
|
-
const
|
|
181
|
+
const snapshots = [];
|
|
182
|
+
if (input.snapshots !== void 0) {
|
|
183
|
+
if (!Array.isArray(input.snapshots) || input.snapshots.length > 100) return void 0;
|
|
184
|
+
for (const item of input.snapshots) {
|
|
185
|
+
const snapshot = record$14(item);
|
|
186
|
+
if (snapshot === void 0 || !finiteInteger(snapshot.turn) || !string$4(snapshot.tree, 64)) return void 0;
|
|
187
|
+
snapshots.push({
|
|
188
|
+
turn: snapshot.turn,
|
|
189
|
+
tree: snapshot.tree
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const pending = record$14(input.pending);
|
|
163
194
|
if (input.pending !== void 0 && pending === void 0) return void 0;
|
|
164
195
|
if (pending === void 0) return {
|
|
165
196
|
ranges,
|
|
166
|
-
anchors
|
|
197
|
+
anchors,
|
|
198
|
+
snapshots
|
|
167
199
|
};
|
|
168
200
|
if (pending.fresh === true) return {
|
|
169
201
|
ranges,
|
|
170
202
|
anchors,
|
|
203
|
+
snapshots,
|
|
171
204
|
pending: { fresh: true }
|
|
172
205
|
};
|
|
173
206
|
if (!string$4(pending.resumeAt, 128)) return void 0;
|
|
174
207
|
return {
|
|
175
208
|
ranges,
|
|
176
209
|
anchors,
|
|
210
|
+
snapshots,
|
|
177
211
|
pending: { resumeAt: pending.resumeAt }
|
|
178
212
|
};
|
|
179
213
|
}
|
|
180
214
|
function tasks(value) {
|
|
181
|
-
const input = record$
|
|
215
|
+
const input = record$14(value);
|
|
182
216
|
if (input === void 0 || !Array.isArray(input.tasks)) return void 0;
|
|
183
217
|
return normalizeTasksEvent(input.tasks);
|
|
184
218
|
}
|
|
185
219
|
function parseClaudeSidecar(value) {
|
|
186
|
-
const input = record$
|
|
220
|
+
const input = record$14(value);
|
|
187
221
|
if (input === void 0 || input.schemaVersion !== SIDECAR_SCHEMA_VERSION || !finiteInteger(input.revision) || !Array.isArray(input.activities) || input.activities.length > MAX_ACTIVITIES) throw new Error("dsh-claude: invalid sidecar document");
|
|
188
222
|
const activities = input.activities.map(activity);
|
|
189
223
|
if (activities.some((item) => item === void 0)) throw new Error("dsh-claude: invalid sidecar activity");
|
|
@@ -411,11 +445,19 @@ var ClaudeSidecarRepository = class {
|
|
|
411
445
|
});
|
|
412
446
|
}
|
|
413
447
|
/** Land one planned rewind: hidden ranges, surviving anchors, and the fork
|
|
414
|
-
* target the next Claude spawn consumes.
|
|
415
|
-
|
|
448
|
+
* target the next Claude spawn consumes.
|
|
449
|
+
*
|
|
450
|
+
* `droppedFromTurn` also drops the discarded turns' activity. The hidden
|
|
451
|
+
* ranges are surface seqs and activity records carry none, so a reader that
|
|
452
|
+
* works in turns — the plan panel, the task board — has nothing to filter
|
|
453
|
+
* on and would go on showing a plan the session no longer contains. This
|
|
454
|
+
* projection is rebuildable from the session log, so trimming it is not
|
|
455
|
+
* losing anything the log still holds. */
|
|
456
|
+
writeRewind(sessionId, value, droppedFromTurn) {
|
|
416
457
|
return this.#update(sessionId, (current) => ({
|
|
417
458
|
...current,
|
|
418
|
-
rewind: value
|
|
459
|
+
rewind: value,
|
|
460
|
+
...droppedFromTurn === void 0 ? {} : { activities: current.activities.filter((activity) => activity.turn < droppedFromTurn) }
|
|
419
461
|
}), false, { kind: "sync" });
|
|
420
462
|
}
|
|
421
463
|
/** Remember where Claude's chain ended for one completed DSH turn. */
|
|
@@ -428,6 +470,16 @@ var ClaudeSidecarRepository = class {
|
|
|
428
470
|
})
|
|
429
471
|
}));
|
|
430
472
|
}
|
|
473
|
+
/** Remember the working tree one DSH turn was admitted against. */
|
|
474
|
+
recordRewindSnapshot(sessionId, turn, tree) {
|
|
475
|
+
return this.#update(sessionId, (current) => ({
|
|
476
|
+
...current,
|
|
477
|
+
rewind: recordRewindSnapshot(current.rewind ?? EMPTY_REWIND_STATE, {
|
|
478
|
+
turn,
|
|
479
|
+
tree
|
|
480
|
+
})
|
|
481
|
+
}));
|
|
482
|
+
}
|
|
431
483
|
/** Disarm the fork target once a Claude process has resumed at it, so a
|
|
432
484
|
* later respawn continues the rewound session instead of re-truncating it. */
|
|
433
485
|
clearRewindPending(sessionId) {
|
|
@@ -435,7 +487,8 @@ var ClaudeSidecarRepository = class {
|
|
|
435
487
|
...current,
|
|
436
488
|
rewind: {
|
|
437
489
|
ranges: current.rewind.ranges,
|
|
438
|
-
anchors: current.rewind.anchors
|
|
490
|
+
anchors: current.rewind.anchors,
|
|
491
|
+
snapshots: current.rewind.snapshots
|
|
439
492
|
}
|
|
440
493
|
});
|
|
441
494
|
}
|
|
@@ -600,6 +653,93 @@ var AsyncQueue = class {
|
|
|
600
653
|
}
|
|
601
654
|
};
|
|
602
655
|
//#endregion
|
|
656
|
+
//#region src/plan-feedback.ts
|
|
657
|
+
const MAX_NOTES = 30;
|
|
658
|
+
const MAX_NOTE_CHARS = 2e3;
|
|
659
|
+
const MAX_QUOTE_CHARS = 1e3;
|
|
660
|
+
var PlanFeedbackError = class extends Error {
|
|
661
|
+
code;
|
|
662
|
+
constructor(code, message) {
|
|
663
|
+
super(message);
|
|
664
|
+
this.name = "PlanFeedbackError";
|
|
665
|
+
this.code = code;
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
/** Validate and bound one submission's notes. */
|
|
669
|
+
function planNotesOf(value) {
|
|
670
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > MAX_NOTES) throw new PlanFeedbackError("invalid-request", "The review notes are invalid.");
|
|
671
|
+
return value.map((item) => {
|
|
672
|
+
const note = item !== null && typeof item === "object" ? item : void 0;
|
|
673
|
+
const text = typeof note?.text === "string" ? note.text.trim() : "";
|
|
674
|
+
if (text.length === 0 || text.length > MAX_NOTE_CHARS) throw new PlanFeedbackError("invalid-request", "A review note is empty or too long.");
|
|
675
|
+
const quote = typeof note?.quote === "string" ? note.quote.trim().slice(0, MAX_QUOTE_CHARS) : "";
|
|
676
|
+
return quote.length === 0 ? { text } : {
|
|
677
|
+
quote,
|
|
678
|
+
text
|
|
679
|
+
};
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
/** What Claude is told when the reviewer asks for changes.
|
|
683
|
+
*
|
|
684
|
+
* Addressed to Claude rather than logged at it: a rejection it cannot act on
|
|
685
|
+
* is the thing this whole path exists to replace. The quotes are fenced as
|
|
686
|
+
* block quotes so a passage containing its own Markdown cannot be mistaken
|
|
687
|
+
* for the reviewer's instruction. */
|
|
688
|
+
function planFeedbackMessage(notes) {
|
|
689
|
+
return [
|
|
690
|
+
"The user reviewed the plan in DeepSeek Harness and asked for changes rather than approving or rejecting it.",
|
|
691
|
+
"",
|
|
692
|
+
notes.map((note) => note.quote === void 0 ? note.text : `On this part of the plan:\n${note.quote.split("\n").map((line) => `> ${line}`).join("\n")}\n\n${note.text}`).join("\n\n---\n\n"),
|
|
693
|
+
"",
|
|
694
|
+
"Revise the plan accordingly and propose it again."
|
|
695
|
+
].join("\n");
|
|
696
|
+
}
|
|
697
|
+
/** The seam between the panel's "send for changes" and the permission bridge
|
|
698
|
+
* waiting on that plan's approval.
|
|
699
|
+
*
|
|
700
|
+
* The approval promise lives inside the bridge and cannot be resolved from
|
|
701
|
+
* outside, so the bridge races it against this gate instead: whichever
|
|
702
|
+
* answers first decides, and the loser is aborted. One waiter per tool use —
|
|
703
|
+
* a plan is approved once. */
|
|
704
|
+
var PlanFeedbackGate = class {
|
|
705
|
+
#waiting = /* @__PURE__ */ new Map();
|
|
706
|
+
/** Notes for the plan under `toolUseId`, or undefined when the wait is
|
|
707
|
+
* abandoned because the approval surface answered first. */
|
|
708
|
+
wait(toolUseId, signal) {
|
|
709
|
+
return new Promise((resolve) => {
|
|
710
|
+
const settle = (notes) => {
|
|
711
|
+
if (this.#waiting.get(toolUseId) === deliver) this.#waiting.delete(toolUseId);
|
|
712
|
+
signal.removeEventListener("abort", onAbort);
|
|
713
|
+
resolve(notes);
|
|
714
|
+
};
|
|
715
|
+
const deliver = (notes) => {
|
|
716
|
+
settle(notes);
|
|
717
|
+
};
|
|
718
|
+
const onAbort = () => {
|
|
719
|
+
settle(void 0);
|
|
720
|
+
};
|
|
721
|
+
if (signal.aborted) {
|
|
722
|
+
resolve(void 0);
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
726
|
+
this.#waiting.set(toolUseId, deliver);
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
/** Hand notes to a waiting plan approval. False when nothing is waiting —
|
|
730
|
+
* the plan was already decided, or never existed. */
|
|
731
|
+
submit(toolUseId, notes) {
|
|
732
|
+
const deliver = this.#waiting.get(toolUseId);
|
|
733
|
+
if (deliver === void 0) return false;
|
|
734
|
+
deliver(notes);
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
737
|
+
/** Whether a plan is currently open for review. */
|
|
738
|
+
pending(toolUseId) {
|
|
739
|
+
return this.#waiting.has(toolUseId);
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
//#endregion
|
|
603
743
|
//#region src/permission.ts
|
|
604
744
|
function denialMessage(outcome) {
|
|
605
745
|
switch (outcome) {
|
|
@@ -609,10 +749,56 @@ function denialMessage(outcome) {
|
|
|
609
749
|
case "allowed-once": return "";
|
|
610
750
|
}
|
|
611
751
|
}
|
|
752
|
+
/** Claude leaving plan mode is not an ordinary tool call: its argument is the
|
|
753
|
+
* plan, written for the user, and the approval that follows is the user
|
|
754
|
+
* agreeing to it. Everything else reads better as a prompt plus its input. */
|
|
755
|
+
const PLAN_TOOL = "ExitPlanMode";
|
|
756
|
+
const MAX_REASON_CHARS = 1200;
|
|
757
|
+
/** What the approval dialog says instead of the plan.
|
|
758
|
+
*
|
|
759
|
+
* A plan is written to be read at length, and the approval dialog is a
|
|
760
|
+
* cramped modal that renders its reason as plain text: pasting the plan there
|
|
761
|
+
* turned a document into an unreadable wall. The decision stays with the Host
|
|
762
|
+
* — this is still an ordinary tool approval — and the plan itself is drawn by
|
|
763
|
+
* the plugin's own panel, where Markdown and the reader's chosen prose
|
|
764
|
+
* palette both apply. The dialog only has to say what is being decided and
|
|
765
|
+
* where to read it. */
|
|
766
|
+
const PLAN_APPROVAL_PROMPT = "Claude proposed a plan. Read it in the Plan panel, then approve or reject here.";
|
|
767
|
+
/** The plan an `ExitPlanMode` call carries, if it carries one.
|
|
768
|
+
*
|
|
769
|
+
* Travels to the client on the permission activity's `text` field: `summary`
|
|
770
|
+
* is capped at 1k and `detail` at 4k, both of which truncate a real plan,
|
|
771
|
+
* while `text` holds 64k. Only `kind: 'text'` activities are drawn as prose
|
|
772
|
+
* by the transcript, so a plan riding a `kind: 'permission'` record reaches
|
|
773
|
+
* the panel without being painted twice. */
|
|
774
|
+
function planText(toolName, input) {
|
|
775
|
+
if (toolName !== PLAN_TOOL) return void 0;
|
|
776
|
+
return typeof input.plan === "string" && input.plan.length > 0 ? input.plan : void 0;
|
|
777
|
+
}
|
|
778
|
+
/** The session's approval-policy override, or undefined when it never switched.
|
|
779
|
+
*
|
|
780
|
+
* The same fold the approval service does — the last `approval/policy` event
|
|
781
|
+
* wins, because replaying the log IS the state. Read here rather than
|
|
782
|
+
* imported so this package keeps its runtime surface to the Host services it
|
|
783
|
+
* is actually handed. */
|
|
784
|
+
function approvalPolicyOf(events) {
|
|
785
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
786
|
+
const event = events[index];
|
|
787
|
+
if (event?.type !== "approval/policy") continue;
|
|
788
|
+
const policy = event.data.policy;
|
|
789
|
+
return typeof policy === "string" ? policy : void 0;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
/** The policy that answers every request with `rejected` before reaching an
|
|
793
|
+
* answerer, so no approval surface is ever shown. DSH writes it alongside
|
|
794
|
+
* `sandbox/mode: danger-full-access` — Full access means "stop asking". */
|
|
795
|
+
const SILENT_POLICY = "never";
|
|
796
|
+
const ASKING_POLICY = "ask";
|
|
612
797
|
function permissionReason(toolName, input, options) {
|
|
798
|
+
if (planText(toolName, input) !== void 0) return PLAN_APPROVAL_PROMPT;
|
|
613
799
|
const prompt = options.title ?? options.description ?? options.decisionReason ?? `Claude Code wants to use ${toolName}.`;
|
|
614
800
|
const detail = safeDetail(input);
|
|
615
|
-
return boundText(detail === void 0 ? prompt : `${prompt}\nInput: ${detail}`,
|
|
801
|
+
return boundText(detail === void 0 ? prompt : `${prompt}\nInput: ${detail}`, MAX_REASON_CHARS);
|
|
616
802
|
}
|
|
617
803
|
function mapApprovalOutcome(outcome, input, toolUseID) {
|
|
618
804
|
if (outcome === "allowed-once") return {
|
|
@@ -628,7 +814,7 @@ function mapApprovalOutcome(outcome, input, toolUseID) {
|
|
|
628
814
|
decisionClassification: "user_reject"
|
|
629
815
|
};
|
|
630
816
|
}
|
|
631
|
-
function createPermissionBridge(approval, activeContext, userQuestion) {
|
|
817
|
+
function createPermissionBridge(approval, activeContext, userQuestion, planFeedback) {
|
|
632
818
|
return async (toolName, input, options) => {
|
|
633
819
|
if (toolName === "AskUserQuestion") return userQuestion === void 0 ? {
|
|
634
820
|
behavior: "deny",
|
|
@@ -645,6 +831,9 @@ function createPermissionBridge(approval, activeContext, userQuestion) {
|
|
|
645
831
|
};
|
|
646
832
|
active.markActivity?.();
|
|
647
833
|
const reason = permissionReason(toolName, input, options);
|
|
834
|
+
const plan = planText(toolName, input);
|
|
835
|
+
const session = active.agent.session;
|
|
836
|
+
let silenced = false;
|
|
648
837
|
try {
|
|
649
838
|
await active.appendActivity({
|
|
650
839
|
kind: "permission",
|
|
@@ -653,16 +842,50 @@ function createPermissionBridge(approval, activeContext, userQuestion) {
|
|
|
653
842
|
toolName,
|
|
654
843
|
title: options.displayName ?? toolName,
|
|
655
844
|
summary: options.title ?? options.description ?? reason,
|
|
656
|
-
detail: input
|
|
845
|
+
detail: input,
|
|
846
|
+
...plan === void 0 ? {} : { text: plan }
|
|
657
847
|
});
|
|
658
|
-
const
|
|
659
|
-
|
|
848
|
+
const userDecides = plan !== void 0;
|
|
849
|
+
silenced = userDecides && approvalPolicyOf(session.events) === SILENT_POLICY;
|
|
850
|
+
if (silenced) session.append("approval/policy", { policy: ASKING_POLICY });
|
|
851
|
+
const alreadyFullAccess = !userDecides && await active.hasFullAccess?.() === true;
|
|
852
|
+
const revision = new AbortController();
|
|
853
|
+
const notes = plan === void 0 || planFeedback === void 0 ? void 0 : planFeedback.wait(options.toolUseID, AbortSignal.any([options.signal, revision.signal]));
|
|
854
|
+
const decided = new AbortController();
|
|
855
|
+
const asked = alreadyFullAccess ? Promise.resolve("allowed-once") : approval.request({
|
|
660
856
|
agent: active.agent,
|
|
661
857
|
toolName,
|
|
662
858
|
reason,
|
|
663
|
-
signal: options.signal
|
|
859
|
+
signal: notes === void 0 ? options.signal : AbortSignal.any([options.signal, decided.signal])
|
|
860
|
+
});
|
|
861
|
+
const answer = notes === void 0 ? { outcome: await asked } : await Promise.race([asked.then((outcome) => {
|
|
862
|
+
revision.abort();
|
|
863
|
+
return { outcome };
|
|
864
|
+
}), notes.then((value) => value === void 0 ? void 0 : { revisions: value })]).then(async (first) => {
|
|
865
|
+
if (first !== void 0 && "revisions" in first) decided.abort();
|
|
866
|
+
return first ?? { outcome: await asked };
|
|
664
867
|
});
|
|
665
|
-
|
|
868
|
+
if ("revisions" in answer) {
|
|
869
|
+
const message = planFeedbackMessage(answer.revisions);
|
|
870
|
+
active.recordDenial?.(options.toolUseID);
|
|
871
|
+
await active.appendActivity({
|
|
872
|
+
kind: "permission",
|
|
873
|
+
phase: "denied",
|
|
874
|
+
toolUseId: options.toolUseID,
|
|
875
|
+
toolName,
|
|
876
|
+
title: options.displayName ?? toolName,
|
|
877
|
+
summary: "Sent back for changes in DeepSeek Harness",
|
|
878
|
+
text: message
|
|
879
|
+
});
|
|
880
|
+
return {
|
|
881
|
+
behavior: "deny",
|
|
882
|
+
message,
|
|
883
|
+
toolUseID: options.toolUseID,
|
|
884
|
+
decisionClassification: "user_reject"
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
const outcome = answer.outcome;
|
|
888
|
+
const fullAccess = alreadyFullAccess || !userDecides && await active.hasFullAccess?.() === true;
|
|
666
889
|
const effectiveOutcome = fullAccess ? "allowed-once" : outcome;
|
|
667
890
|
const result = mapApprovalOutcome(effectiveOutcome, input, options.toolUseID);
|
|
668
891
|
if (result.behavior === "deny") active.recordDenial?.(options.toolUseID);
|
|
@@ -695,6 +918,10 @@ function createPermissionBridge(approval, activeContext, userQuestion) {
|
|
|
695
918
|
toolUseID: options.toolUseID,
|
|
696
919
|
decisionClassification: "user_reject"
|
|
697
920
|
};
|
|
921
|
+
} finally {
|
|
922
|
+
try {
|
|
923
|
+
if (silenced) session.append("approval/policy", { policy: SILENT_POLICY });
|
|
924
|
+
} catch {}
|
|
698
925
|
}
|
|
699
926
|
};
|
|
700
927
|
}
|
|
@@ -814,7 +1041,7 @@ function createUserQuestionBridge(userQuestions, activeContext) {
|
|
|
814
1041
|
}
|
|
815
1042
|
//#endregion
|
|
816
1043
|
//#region src/sdk-messages.ts
|
|
817
|
-
function record$
|
|
1044
|
+
function record$13(value) {
|
|
818
1045
|
return value !== null && typeof value === "object" ? value : void 0;
|
|
819
1046
|
}
|
|
820
1047
|
function string$3(value) {
|
|
@@ -846,12 +1073,12 @@ function hasUsageCounts(usage) {
|
|
|
846
1073
|
return usage.inputTokens !== void 0 || usage.outputTokens !== void 0 || usage.cacheReadTokens !== void 0 || usage.cacheCreationTokens !== void 0;
|
|
847
1074
|
}
|
|
848
1075
|
function resultUsage(message) {
|
|
849
|
-
const normalized = usageOf(record$
|
|
1076
|
+
const normalized = usageOf(record$13(message.usage));
|
|
850
1077
|
if (typeof message.total_cost_usd === "number") normalized.cumulativeCostUsd = message.total_cost_usd;
|
|
851
1078
|
return normalized;
|
|
852
1079
|
}
|
|
853
1080
|
function normalizeAssistant(message) {
|
|
854
|
-
const envelope = record$
|
|
1081
|
+
const envelope = record$13(message.message);
|
|
855
1082
|
const content = envelope?.content;
|
|
856
1083
|
if (!Array.isArray(content)) return [{
|
|
857
1084
|
kind: "protocol-error",
|
|
@@ -861,7 +1088,7 @@ function normalizeAssistant(message) {
|
|
|
861
1088
|
const parentToolUseId = string$3(message.parent_tool_use_id);
|
|
862
1089
|
const normalized = [];
|
|
863
1090
|
for (const item of content) {
|
|
864
|
-
const block = record$
|
|
1091
|
+
const block = record$13(item);
|
|
865
1092
|
if (block === void 0) continue;
|
|
866
1093
|
if (block.type === "text") {
|
|
867
1094
|
const text = string$3(block.text);
|
|
@@ -890,7 +1117,7 @@ function normalizeAssistant(message) {
|
|
|
890
1117
|
});
|
|
891
1118
|
}
|
|
892
1119
|
}
|
|
893
|
-
const usage = usageOf(record$
|
|
1120
|
+
const usage = usageOf(record$13(envelope?.usage));
|
|
894
1121
|
if (hasUsageCounts(usage)) normalized.push({
|
|
895
1122
|
kind: "request-usage",
|
|
896
1123
|
usage,
|
|
@@ -900,7 +1127,7 @@ function normalizeAssistant(message) {
|
|
|
900
1127
|
}
|
|
901
1128
|
function normalizeUser(message) {
|
|
902
1129
|
if (message.isReplay === true) return [];
|
|
903
|
-
const content = record$
|
|
1130
|
+
const content = record$13(message.message)?.content;
|
|
904
1131
|
if (typeof content === "string") return [];
|
|
905
1132
|
if (!Array.isArray(content)) return [{
|
|
906
1133
|
kind: "protocol-error",
|
|
@@ -910,7 +1137,7 @@ function normalizeUser(message) {
|
|
|
910
1137
|
const parentToolUseId = string$3(message.parent_tool_use_id);
|
|
911
1138
|
const normalized = [];
|
|
912
1139
|
for (const item of content) {
|
|
913
|
-
const block = record$
|
|
1140
|
+
const block = record$13(item);
|
|
914
1141
|
if (block?.type !== "tool_result") continue;
|
|
915
1142
|
const toolUseId = string$3(block.tool_use_id);
|
|
916
1143
|
if (toolUseId === void 0) continue;
|
|
@@ -991,7 +1218,7 @@ function normalizeSystem(message) {
|
|
|
991
1218
|
const summary = string$3(message.summary);
|
|
992
1219
|
const subagentType = string$3(message.subagent_type);
|
|
993
1220
|
const lastToolName = string$3(message.last_tool_name);
|
|
994
|
-
const usage = taskUsageOf(record$
|
|
1221
|
+
const usage = taskUsageOf(record$13(message.usage));
|
|
995
1222
|
return [{
|
|
996
1223
|
kind: "subagent",
|
|
997
1224
|
title: summary ?? description ?? "Claude subagent update",
|
|
@@ -1007,7 +1234,7 @@ function normalizeSystem(message) {
|
|
|
1007
1234
|
}];
|
|
1008
1235
|
}
|
|
1009
1236
|
if (subtype === "task_updated") {
|
|
1010
|
-
const patch = record$
|
|
1237
|
+
const patch = record$13(message.patch);
|
|
1011
1238
|
const status = string$3(patch?.status);
|
|
1012
1239
|
const taskId = string$3(message.task_id);
|
|
1013
1240
|
const description = string$3(patch?.description);
|
|
@@ -1030,7 +1257,7 @@ function normalizeSystem(message) {
|
|
|
1030
1257
|
const taskId = string$3(message.task_id);
|
|
1031
1258
|
const summary = string$3(message.summary);
|
|
1032
1259
|
const taskStatus = failed ? "failed" : stopped ? "stopped" : "completed";
|
|
1033
|
-
const usage = taskUsageOf(record$
|
|
1260
|
+
const usage = taskUsageOf(record$13(message.usage));
|
|
1034
1261
|
return [{
|
|
1035
1262
|
kind: "subagent",
|
|
1036
1263
|
title: summary ?? taskId ?? "Claude subagent finished",
|
|
@@ -1045,7 +1272,7 @@ function normalizeSystem(message) {
|
|
|
1045
1272
|
if (subtype === "background_tasks_changed") return [{
|
|
1046
1273
|
kind: "background-tasks",
|
|
1047
1274
|
tasks: (Array.isArray(message.tasks) ? message.tasks : []).flatMap((item) => {
|
|
1048
|
-
const entry = record$
|
|
1275
|
+
const entry = record$13(item);
|
|
1049
1276
|
const taskId = string$3(entry?.task_id);
|
|
1050
1277
|
const description = string$3(entry?.description);
|
|
1051
1278
|
const taskType = string$3(entry?.task_type);
|
|
@@ -1063,7 +1290,7 @@ function normalizeSystem(message) {
|
|
|
1063
1290
|
detail: message
|
|
1064
1291
|
}];
|
|
1065
1292
|
if (subtype === "compact_boundary") {
|
|
1066
|
-
const metadata = record$
|
|
1293
|
+
const metadata = record$13(message.compact_metadata);
|
|
1067
1294
|
const trigger = metadata?.trigger === "auto" || metadata?.trigger === "manual" ? metadata.trigger : void 0;
|
|
1068
1295
|
const preTokens = finiteNumber(metadata?.pre_tokens);
|
|
1069
1296
|
const postTokens = finiteNumber(metadata?.post_tokens);
|
|
@@ -1102,10 +1329,10 @@ const RESULT_ERROR_SUBTYPES = /* @__PURE__ */ new Set([
|
|
|
1102
1329
|
function normalizeSdkMessage(message) {
|
|
1103
1330
|
const value = message;
|
|
1104
1331
|
if (value.type === "stream_event") {
|
|
1105
|
-
const event = record$
|
|
1332
|
+
const event = record$13(value.event);
|
|
1106
1333
|
const parentToolUseId = string$3(value.parent_tool_use_id);
|
|
1107
1334
|
if (event?.type === "content_block_delta") {
|
|
1108
|
-
const delta = record$
|
|
1335
|
+
const delta = record$13(event.delta);
|
|
1109
1336
|
if (delta?.type === "text_delta") {
|
|
1110
1337
|
const text = string$3(delta.text);
|
|
1111
1338
|
return text === void 0 ? [] : [{
|
|
@@ -1140,7 +1367,7 @@ function normalizeSdkMessage(message) {
|
|
|
1140
1367
|
const errors = Array.isArray(value.errors) ? value.errors.filter((item) => typeof item === "string") : void 0;
|
|
1141
1368
|
const terminalReason = string$3(value.terminal_reason);
|
|
1142
1369
|
const userMessageUuid = string$3(value.user_message_uuid);
|
|
1143
|
-
const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$
|
|
1370
|
+
const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$13(item)).filter((item) => item !== void 0).map((item) => {
|
|
1144
1371
|
const toolName = string$3(item.tool_name);
|
|
1145
1372
|
const toolUseId = string$3(item.tool_use_id);
|
|
1146
1373
|
return toolName === void 0 || toolUseId === void 0 ? void 0 : {
|
|
@@ -1166,7 +1393,7 @@ function normalizeSdkMessage(message) {
|
|
|
1166
1393
|
detail: value.error ?? value.output
|
|
1167
1394
|
}];
|
|
1168
1395
|
if (value.type === "rate_limit_event") {
|
|
1169
|
-
const status = string$3(record$
|
|
1396
|
+
const status = string$3(record$13(value.rate_limit_info)?.status);
|
|
1170
1397
|
return [{
|
|
1171
1398
|
kind: "status",
|
|
1172
1399
|
title: status !== void 0 && status !== "allowed" ? "Claude rate limit is blocking requests" : "Claude rate limit status changed",
|
|
@@ -1279,7 +1506,7 @@ const FIXED_WINDOWS = [
|
|
|
1279
1506
|
"seven_day_opus",
|
|
1280
1507
|
"seven_day_sonnet"
|
|
1281
1508
|
];
|
|
1282
|
-
function record$
|
|
1509
|
+
function record$12(value) {
|
|
1283
1510
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
1284
1511
|
}
|
|
1285
1512
|
/** Utilization is documented as 0-100; clamp so a server glitch cannot render
|
|
@@ -1291,7 +1518,7 @@ function resetsAt(value) {
|
|
|
1291
1518
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1292
1519
|
}
|
|
1293
1520
|
function window(id, source, label) {
|
|
1294
|
-
const entry = record$
|
|
1521
|
+
const entry = record$12(source);
|
|
1295
1522
|
if (entry === void 0) return void 0;
|
|
1296
1523
|
const used = utilization(entry.utilization);
|
|
1297
1524
|
const reset = resetsAt(entry.resets_at);
|
|
@@ -1305,9 +1532,9 @@ function window(id, source, label) {
|
|
|
1305
1532
|
}
|
|
1306
1533
|
/** Project the SDK's `/usage` response onto the windows the settings card shows. */
|
|
1307
1534
|
function normalizePlanUsage(value, fetchedAt) {
|
|
1308
|
-
const response = record$
|
|
1535
|
+
const response = record$12(value);
|
|
1309
1536
|
const subscription = typeof response?.subscription_type === "string" ? response.subscription_type : void 0;
|
|
1310
|
-
const limits = record$
|
|
1537
|
+
const limits = record$12(response?.rate_limits);
|
|
1311
1538
|
if (response?.rate_limits_available !== true || limits === void 0) return {
|
|
1312
1539
|
available: false,
|
|
1313
1540
|
...subscription === void 0 ? {} : { subscription },
|
|
@@ -1315,7 +1542,7 @@ function normalizePlanUsage(value, fetchedAt) {
|
|
|
1315
1542
|
fetchedAt
|
|
1316
1543
|
};
|
|
1317
1544
|
const windows = [...FIXED_WINDOWS.map((id) => window(id, limits[id])), ...(Array.isArray(limits.model_scoped) ? limits.model_scoped : []).map((entry, index) => {
|
|
1318
|
-
const name = record$
|
|
1545
|
+
const name = record$12(entry)?.display_name;
|
|
1319
1546
|
return window(`model:${typeof name === "string" ? name : index}`, entry, typeof name === "string" ? name : void 0);
|
|
1320
1547
|
})].filter((entry) => entry !== void 0);
|
|
1321
1548
|
return {
|
|
@@ -1442,6 +1669,115 @@ function createManagedClaudeSpawner(runtime, executablePath, observe) {
|
|
|
1442
1669
|
};
|
|
1443
1670
|
}
|
|
1444
1671
|
//#endregion
|
|
1672
|
+
//#region src/worktree-snapshot.ts
|
|
1673
|
+
/** Working-tree snapshots taken and restored with git's own plumbing.
|
|
1674
|
+
*
|
|
1675
|
+
* A rewind that only truncates Claude's transcript leaves the files Claude
|
|
1676
|
+
* wrote on disk, so the next turn resumes against a checkout the model no
|
|
1677
|
+
* longer remembers writing. Git already stores trees: a throwaway index turns
|
|
1678
|
+
* the working tree into one tree object without touching HEAD, the real
|
|
1679
|
+
* index, or the checkout, and a restore reads that tree back.
|
|
1680
|
+
*
|
|
1681
|
+
* Ignored files stay outside the snapshot in both directions. `git add -A`
|
|
1682
|
+
* honours `.gitignore`, so build output and `node_modules` are neither
|
|
1683
|
+
* captured nor removed.
|
|
1684
|
+
*/
|
|
1685
|
+
const MAX_OUTPUT_BYTES$5 = 65536;
|
|
1686
|
+
/** `add -A` walks the whole checkout; a large repository needs more than the
|
|
1687
|
+
* status probes' five seconds, and this sits in front of every turn. */
|
|
1688
|
+
const GIT_TIMEOUT_MS$4 = 3e4;
|
|
1689
|
+
const OBJECT_NAME = /^[0-9a-f]{40}$|^[0-9a-f]{64}$/u;
|
|
1690
|
+
async function collect$4(handle) {
|
|
1691
|
+
return {
|
|
1692
|
+
exitCode: (await handle.done).exitCode,
|
|
1693
|
+
stdout: handle.collected.stdout?.readFrom(0).text ?? ""
|
|
1694
|
+
};
|
|
1695
|
+
}
|
|
1696
|
+
async function run$1(runtime, git, args, cwd, env = {}) {
|
|
1697
|
+
return collect$4(runtime.spawn({
|
|
1698
|
+
argv: [git, ...args],
|
|
1699
|
+
cwd,
|
|
1700
|
+
stdio: {
|
|
1701
|
+
stdin: "ignore",
|
|
1702
|
+
stdout: { maxBytes: MAX_OUTPUT_BYTES$5 },
|
|
1703
|
+
stderr: { maxBytes: MAX_OUTPUT_BYTES$5 }
|
|
1704
|
+
},
|
|
1705
|
+
graceMs: 1e3,
|
|
1706
|
+
signal: AbortSignal.timeout(GIT_TIMEOUT_MS$4),
|
|
1707
|
+
env
|
|
1708
|
+
}));
|
|
1709
|
+
}
|
|
1710
|
+
/** Capture the working tree as a git tree object.
|
|
1711
|
+
*
|
|
1712
|
+
* Undefined whenever git cannot answer -- no git, not a repository, a locked
|
|
1713
|
+
* or broken checkout. Snapshots are advisory: a turn without one simply
|
|
1714
|
+
* cannot offer a file rewind, and must never fail for it.
|
|
1715
|
+
*
|
|
1716
|
+
* ponytail: the tree is unreachable from any ref, so `git gc --prune` will
|
|
1717
|
+
* eventually collect it. Two weeks is git's default grace period and a rewind
|
|
1718
|
+
* happens minutes after the turn it undoes; anchor it to a real ref only if
|
|
1719
|
+
* snapshots ever need to outlive a gc.
|
|
1720
|
+
*/
|
|
1721
|
+
async function captureWorktreeTree(runtime, cwd) {
|
|
1722
|
+
let git;
|
|
1723
|
+
try {
|
|
1724
|
+
git = await runtime.resolveExecutable("git");
|
|
1725
|
+
} catch {
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
const indexFile = join(tmpdir(), `dsh-claude-index-${process.pid}-${randomUUID()}`);
|
|
1729
|
+
const env = { GIT_INDEX_FILE: indexFile };
|
|
1730
|
+
try {
|
|
1731
|
+
await run$1(runtime, git, ["read-tree", "HEAD"], cwd, env);
|
|
1732
|
+
if ((await run$1(runtime, git, ["add", "-A"], cwd, env)).exitCode !== 0) return void 0;
|
|
1733
|
+
const written = await run$1(runtime, git, ["write-tree"], cwd, env);
|
|
1734
|
+
const tree = written.stdout.trim();
|
|
1735
|
+
return written.exitCode === 0 && OBJECT_NAME.test(tree) ? tree : void 0;
|
|
1736
|
+
} catch {
|
|
1737
|
+
return;
|
|
1738
|
+
} finally {
|
|
1739
|
+
await rm(indexFile, { force: true }).catch(() => void 0);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
/** Put a captured tree back over the working tree, reporting whether it landed.
|
|
1743
|
+
*
|
|
1744
|
+
* Files the tree holds are rewritten, and files created after the snapshot are
|
|
1745
|
+
* removed. Nothing outside the snapshot's own scope is touched: ignored files
|
|
1746
|
+
* survive, because `clean` runs without `-x`.
|
|
1747
|
+
*/
|
|
1748
|
+
async function restoreWorktreeTree(runtime, cwd, tree) {
|
|
1749
|
+
if (!OBJECT_NAME.test(tree)) return false;
|
|
1750
|
+
let git;
|
|
1751
|
+
try {
|
|
1752
|
+
git = await runtime.resolveExecutable("git");
|
|
1753
|
+
} catch {
|
|
1754
|
+
return false;
|
|
1755
|
+
}
|
|
1756
|
+
try {
|
|
1757
|
+
const kind = await run$1(runtime, git, [
|
|
1758
|
+
"cat-file",
|
|
1759
|
+
"-t",
|
|
1760
|
+
tree
|
|
1761
|
+
], cwd);
|
|
1762
|
+
if (kind.exitCode !== 0 || kind.stdout.trim() !== "tree") return false;
|
|
1763
|
+
if ((await run$1(runtime, git, [
|
|
1764
|
+
"read-tree",
|
|
1765
|
+
"--reset",
|
|
1766
|
+
"-u",
|
|
1767
|
+
tree
|
|
1768
|
+
], cwd)).exitCode !== 0) return false;
|
|
1769
|
+
await run$1(runtime, git, ["clean", "-fd"], cwd);
|
|
1770
|
+
await run$1(runtime, git, [
|
|
1771
|
+
"reset",
|
|
1772
|
+
"--mixed",
|
|
1773
|
+
"HEAD"
|
|
1774
|
+
], cwd);
|
|
1775
|
+
return true;
|
|
1776
|
+
} catch {
|
|
1777
|
+
return false;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
//#endregion
|
|
1445
1781
|
//#region src/supervisor.ts
|
|
1446
1782
|
const CLAUDE_INITIALIZATION_TIMEOUT_MS = 3e4;
|
|
1447
1783
|
/** Control requests must settle; a wedged one must not clog the metadata chain. */
|
|
@@ -1504,6 +1840,21 @@ async function withTimeout(operation, timeoutMs, label) {
|
|
|
1504
1840
|
if (timer !== void 0) clearTimeout(timer);
|
|
1505
1841
|
}
|
|
1506
1842
|
}
|
|
1843
|
+
async function withAbort(operation, signal) {
|
|
1844
|
+
if (signal === void 0) return operation;
|
|
1845
|
+
if (signal.aborted) throw abortFailure();
|
|
1846
|
+
let abortListener;
|
|
1847
|
+
try {
|
|
1848
|
+
return await Promise.race([operation, new Promise((_resolve, reject) => {
|
|
1849
|
+
abortListener = () => {
|
|
1850
|
+
reject(abortFailure());
|
|
1851
|
+
};
|
|
1852
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
1853
|
+
})]);
|
|
1854
|
+
} finally {
|
|
1855
|
+
if (abortListener !== void 0) signal.removeEventListener("abort", abortListener);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1507
1858
|
/** The uuid of one main-chain transcript entry, or undefined for anything a
|
|
1508
1859
|
* rewind must not fork at: stream partials, results, and sidechain traffic. */
|
|
1509
1860
|
function chainEntryUuid(message) {
|
|
@@ -1549,6 +1900,8 @@ var ClaudeSupervisor = class {
|
|
|
1549
1900
|
#runtime;
|
|
1550
1901
|
#approval;
|
|
1551
1902
|
#userQuestions;
|
|
1903
|
+
/** Lets the plan panel answer a plan's approval with revisions. */
|
|
1904
|
+
planFeedback = new PlanFeedbackGate();
|
|
1552
1905
|
#config;
|
|
1553
1906
|
#queryFactory;
|
|
1554
1907
|
#runDetached;
|
|
@@ -1557,6 +1910,15 @@ var ClaudeSupervisor = class {
|
|
|
1557
1910
|
#contextWindows = /* @__PURE__ */ new Map();
|
|
1558
1911
|
#disposed = false;
|
|
1559
1912
|
#admissionGate = Promise.resolve();
|
|
1913
|
+
/** FIFO user turns live outside the gate while capacity-blocked, so
|
|
1914
|
+
* best-effort metadata can still enter the serialized path and fail. */
|
|
1915
|
+
#turnAdmissions = [];
|
|
1916
|
+
#metadataAdmissions = /* @__PURE__ */ new Set();
|
|
1917
|
+
#admissionDrainScheduled = false;
|
|
1918
|
+
/** Prevent a capacity change between a failed attempt and parking the head
|
|
1919
|
+
* from becoming a lost wake-up. */
|
|
1920
|
+
#admissionRevision = 0;
|
|
1921
|
+
#blockedAdmissionRevision;
|
|
1560
1922
|
constructor(dependencies) {
|
|
1561
1923
|
this.#runtime = dependencies.runtime;
|
|
1562
1924
|
this.#approval = dependencies.approval;
|
|
@@ -1630,28 +1992,140 @@ var ClaudeSupervisor = class {
|
|
|
1630
1992
|
runTurn(request) {
|
|
1631
1993
|
const interruption = this.#interruptions.get(request.agent.id);
|
|
1632
1994
|
if (interruption !== void 0) return interruption.then(() => this.runTurn(request));
|
|
1633
|
-
|
|
1995
|
+
return new Promise((resolve, reject) => {
|
|
1996
|
+
let complete;
|
|
1997
|
+
const completion = new Promise((done) => {
|
|
1998
|
+
complete = done;
|
|
1999
|
+
});
|
|
2000
|
+
const admission = {
|
|
2001
|
+
request,
|
|
2002
|
+
resolve,
|
|
2003
|
+
reject,
|
|
2004
|
+
delivered: false,
|
|
2005
|
+
admitting: false,
|
|
2006
|
+
waitedForCapacity: false,
|
|
2007
|
+
cancellation: new AbortController(),
|
|
2008
|
+
completion,
|
|
2009
|
+
complete: () => {
|
|
2010
|
+
complete?.();
|
|
2011
|
+
}
|
|
2012
|
+
};
|
|
2013
|
+
if (request.signal !== void 0) {
|
|
2014
|
+
const abortListener = () => {
|
|
2015
|
+
if (!admission.admitting) this.#finishTurnAdmission(admission, { error: abortFailure() });
|
|
2016
|
+
else if (admission.waitedForCapacity && !admission.delivered) {
|
|
2017
|
+
admission.delivered = true;
|
|
2018
|
+
admission.reject(abortFailure());
|
|
2019
|
+
}
|
|
2020
|
+
this.#admissionRevision += 1;
|
|
2021
|
+
this.#blockedAdmissionRevision = void 0;
|
|
2022
|
+
this.#scheduleTurnAdmissions();
|
|
2023
|
+
};
|
|
2024
|
+
admission.abortListener = abortListener;
|
|
2025
|
+
request.signal.addEventListener("abort", abortListener, { once: true });
|
|
2026
|
+
}
|
|
2027
|
+
this.#turnAdmissions.push(admission);
|
|
2028
|
+
this.#scheduleTurnAdmissions();
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
async #drainTurnAdmissions() {
|
|
2032
|
+
while (this.#turnAdmissions.length > 0) {
|
|
2033
|
+
const admission = this.#turnAdmissions[0];
|
|
2034
|
+
if (signalAborted(admission.request.signal)) {
|
|
2035
|
+
this.#finishTurnAdmission(admission, { error: abortFailure() });
|
|
2036
|
+
continue;
|
|
2037
|
+
}
|
|
2038
|
+
const attemptedRevision = this.#admissionRevision;
|
|
2039
|
+
admission.admitting = true;
|
|
2040
|
+
try {
|
|
2041
|
+
const output = await this.#runTurnAdmitted(admission.request, admission.waitedForCapacity, admission.cancellation.signal);
|
|
2042
|
+
this.#finishTurnAdmission(admission, { output });
|
|
2043
|
+
} catch (error) {
|
|
2044
|
+
if (error instanceof ClaudeProcessLimitError && !signalAborted(admission.request.signal) && !admission.cancellation.signal.aborted && !this.#disposed) {
|
|
2045
|
+
admission.admitting = false;
|
|
2046
|
+
admission.waitedForCapacity = true;
|
|
2047
|
+
if (attemptedRevision === this.#admissionRevision) {
|
|
2048
|
+
this.#blockedAdmissionRevision = attemptedRevision;
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
continue;
|
|
2052
|
+
}
|
|
2053
|
+
this.#finishTurnAdmission(admission, { error });
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
#finishTurnAdmission(admission, outcome) {
|
|
2058
|
+
if (this.#turnAdmissions[0] === admission) this.#turnAdmissions.shift();
|
|
2059
|
+
else {
|
|
2060
|
+
const index = this.#turnAdmissions.indexOf(admission);
|
|
2061
|
+
if (index >= 0) this.#turnAdmissions.splice(index, 1);
|
|
2062
|
+
}
|
|
2063
|
+
admission.admitting = false;
|
|
2064
|
+
if (admission.request.signal !== void 0 && admission.abortListener !== void 0) admission.request.signal.removeEventListener("abort", admission.abortListener);
|
|
2065
|
+
if (!admission.delivered) {
|
|
2066
|
+
admission.delivered = true;
|
|
2067
|
+
if ("error" in outcome) admission.reject(outcome.error);
|
|
2068
|
+
else admission.resolve(outcome.output);
|
|
2069
|
+
}
|
|
2070
|
+
admission.complete();
|
|
2071
|
+
}
|
|
2072
|
+
#scheduleTurnAdmissions() {
|
|
2073
|
+
if (this.#admissionDrainScheduled || this.#turnAdmissions.length === 0 || this.#blockedAdmissionRevision === this.#admissionRevision) return;
|
|
2074
|
+
this.#admissionDrainScheduled = true;
|
|
2075
|
+
const operation = this.#admissionGate.then(() => this.#drainTurnAdmissions());
|
|
1634
2076
|
this.#admissionGate = operation.then(() => void 0, () => void 0);
|
|
1635
|
-
|
|
2077
|
+
const finished = () => {
|
|
2078
|
+
this.#admissionDrainScheduled = false;
|
|
2079
|
+
this.#scheduleTurnAdmissions();
|
|
2080
|
+
};
|
|
2081
|
+
operation.then(finished, finished);
|
|
1636
2082
|
}
|
|
1637
|
-
async #runTurnAdmitted(request) {
|
|
2083
|
+
async #runTurnAdmitted(request, abortDuringAdmission, cancellationSignal) {
|
|
1638
2084
|
if (this.#disposed) throw new Error("dsh-claude: supervisor is disposed");
|
|
2085
|
+
if (cancellationSignal.aborted) throw abortFailure();
|
|
1639
2086
|
if (signalAborted(request.signal)) throw abortFailure();
|
|
1640
2087
|
const sessionId = request.agent.id;
|
|
1641
2088
|
let entry = this.#entries.get(sessionId);
|
|
2089
|
+
let createdForRequest;
|
|
2090
|
+
const throwIfUnavailable = async () => {
|
|
2091
|
+
const failure = this.#disposed ? /* @__PURE__ */ new Error("dsh-claude: supervisor is disposed") : cancellationSignal.aborted || abortDuringAdmission && signalAborted(request.signal) ? abortFailure() : void 0;
|
|
2092
|
+
if (failure === void 0) return;
|
|
2093
|
+
if (createdForRequest !== void 0) {
|
|
2094
|
+
if (this.#entries.get(sessionId) === createdForRequest) this.#entries.delete(sessionId);
|
|
2095
|
+
await this.#disposeEntry(createdForRequest);
|
|
2096
|
+
createdForRequest = void 0;
|
|
2097
|
+
}
|
|
2098
|
+
throw failure;
|
|
2099
|
+
};
|
|
1642
2100
|
if (entry?.state === "disposed" || entry?.state === "disconnected" || entry?.state === "outcome-unknown") {
|
|
1643
2101
|
this.#entries.delete(sessionId);
|
|
1644
2102
|
await this.#disposeEntry(entry);
|
|
2103
|
+
await throwIfUnavailable();
|
|
1645
2104
|
entry = void 0;
|
|
1646
2105
|
}
|
|
1647
2106
|
if (entry === void 0) {
|
|
1648
2107
|
await this.#makeRoom();
|
|
1649
|
-
|
|
2108
|
+
await throwIfUnavailable();
|
|
2109
|
+
try {
|
|
2110
|
+
entry = await this.#createEntry(request.agent, request.model ?? this.#config.defaultModel, request.thinkingMode, abortDuringAdmission ? request.signal : void 0, cancellationSignal);
|
|
2111
|
+
} catch (error) {
|
|
2112
|
+
await throwIfUnavailable();
|
|
2113
|
+
throw error;
|
|
2114
|
+
}
|
|
2115
|
+
createdForRequest = entry;
|
|
2116
|
+
await throwIfUnavailable();
|
|
1650
2117
|
this.#entries.set(sessionId, entry);
|
|
1651
2118
|
}
|
|
1652
2119
|
if (entry.ownerAgent !== request.agent) throw new Error(`dsh-claude: live agent identity changed for session ${sessionId}`);
|
|
1653
2120
|
if (entry.active !== void 0 || entry.state === "interrupting") throw new ClaudeTurnBusyError(sessionId);
|
|
1654
|
-
|
|
2121
|
+
try {
|
|
2122
|
+
const initialization = withAbort(entry.sdkInitialization, cancellationSignal);
|
|
2123
|
+
await (abortDuringAdmission ? withAbort(initialization, request.signal) : initialization);
|
|
2124
|
+
} catch (error) {
|
|
2125
|
+
await throwIfUnavailable();
|
|
2126
|
+
throw error;
|
|
2127
|
+
}
|
|
2128
|
+
await throwIfUnavailable();
|
|
1655
2129
|
if (entry.idleTimer !== void 0) {
|
|
1656
2130
|
clearTimeout(entry.idleTimer);
|
|
1657
2131
|
entry.idleTimer = void 0;
|
|
@@ -1660,16 +2134,35 @@ var ClaudeSupervisor = class {
|
|
|
1660
2134
|
if (request.thinkingMode !== entry.thinkingMode || model !== entry.model) {
|
|
1661
2135
|
this.#entries.delete(sessionId);
|
|
1662
2136
|
await this.#disposeEntry(entry);
|
|
1663
|
-
entry =
|
|
2137
|
+
if (createdForRequest === entry) createdForRequest = void 0;
|
|
2138
|
+
await throwIfUnavailable();
|
|
2139
|
+
try {
|
|
2140
|
+
entry = await this.#createEntry(request.agent, model, request.thinkingMode, abortDuringAdmission ? request.signal : void 0, cancellationSignal);
|
|
2141
|
+
} catch (error) {
|
|
2142
|
+
await throwIfUnavailable();
|
|
2143
|
+
throw error;
|
|
2144
|
+
}
|
|
2145
|
+
createdForRequest = entry;
|
|
2146
|
+
await throwIfUnavailable();
|
|
1664
2147
|
this.#entries.set(sessionId, entry);
|
|
1665
|
-
|
|
2148
|
+
try {
|
|
2149
|
+
const initialization = withAbort(entry.sdkInitialization, cancellationSignal);
|
|
2150
|
+
await (abortDuringAdmission ? withAbort(initialization, request.signal) : initialization);
|
|
2151
|
+
} catch (error) {
|
|
2152
|
+
await throwIfUnavailable();
|
|
2153
|
+
throw error;
|
|
2154
|
+
}
|
|
1666
2155
|
} else await this.#syncPermissionMode(entry);
|
|
2156
|
+
await throwIfUnavailable();
|
|
1667
2157
|
const promptUuid = randomUUID();
|
|
1668
2158
|
const cursor = currentClaudeActivityCursor(request.agent.session.events);
|
|
1669
|
-
|
|
2159
|
+
const projection = await this.#sidecar.read(sessionId);
|
|
2160
|
+
await throwIfUnavailable();
|
|
2161
|
+
cursor.nextOrdinal = projection.activities.reduce((next, activity) => activity.turn === cursor.turn && activity.step === cursor.step ? Math.max(next, activity.ordinal + 1) : next, 0);
|
|
1670
2162
|
const active = {
|
|
1671
2163
|
agent: request.agent,
|
|
1672
2164
|
cursor,
|
|
2165
|
+
native: (request.renderMode ?? this.#config.renderMode ?? "plugin") === "native",
|
|
1673
2166
|
output: new AsyncQueue(),
|
|
1674
2167
|
promptUuid,
|
|
1675
2168
|
phase: "primary",
|
|
@@ -1690,6 +2183,7 @@ var ClaudeSupervisor = class {
|
|
|
1690
2183
|
entry.active = active;
|
|
1691
2184
|
entry.state = "running";
|
|
1692
2185
|
entry.lastUsedAt = Date.now();
|
|
2186
|
+
await this.#captureWorktree(entry, cursor.turn);
|
|
1693
2187
|
try {
|
|
1694
2188
|
await this.#appendActivity(active, {
|
|
1695
2189
|
kind: "status",
|
|
@@ -1712,9 +2206,12 @@ var ClaudeSupervisor = class {
|
|
|
1712
2206
|
title: "Claude Code turn cancelled before submission"
|
|
1713
2207
|
});
|
|
1714
2208
|
entry.active = void 0;
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
2209
|
+
if (!abortDuringAdmission || createdForRequest === void 0) {
|
|
2210
|
+
entry.state = "idle";
|
|
2211
|
+
entry.lastUsedAt = Date.now();
|
|
2212
|
+
this.#armIdleTimer(entry);
|
|
2213
|
+
}
|
|
2214
|
+
await throwIfUnavailable();
|
|
1718
2215
|
return active.output;
|
|
1719
2216
|
}
|
|
1720
2217
|
if (request.signal !== void 0) {
|
|
@@ -1728,31 +2225,57 @@ var ClaudeSupervisor = class {
|
|
|
1728
2225
|
return active.output;
|
|
1729
2226
|
}
|
|
1730
2227
|
#runMetadata(agent, model, operation) {
|
|
2228
|
+
if (this.#turnAdmissions.some((admission) => admission.waitedForCapacity)) return Promise.reject(new ClaudeProcessLimitError(this.#config.maxProcesses));
|
|
2229
|
+
let complete;
|
|
2230
|
+
const admission = {
|
|
2231
|
+
sessionId: agent.id,
|
|
2232
|
+
cancellation: new AbortController(),
|
|
2233
|
+
started: false,
|
|
2234
|
+
completion: new Promise((done) => {
|
|
2235
|
+
complete = done;
|
|
2236
|
+
}),
|
|
2237
|
+
complete: () => {
|
|
2238
|
+
complete?.();
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
this.#metadataAdmissions.add(admission);
|
|
1731
2242
|
const admitted = this.#admissionGate.then(async () => {
|
|
2243
|
+
admission.started = true;
|
|
1732
2244
|
if (this.#disposed) throw new Error("dsh-claude: supervisor is disposed");
|
|
1733
|
-
|
|
2245
|
+
if (admission.cancellation.signal.aborted) throw abortFailure();
|
|
2246
|
+
if (this.#turnAdmissions.some((admission) => admission.waitedForCapacity)) throw new ClaudeProcessLimitError(this.#config.maxProcesses);
|
|
2247
|
+
const entry = await this.#metadataEntry(agent, model, admission.cancellation.signal);
|
|
1734
2248
|
try {
|
|
1735
|
-
await entry.sdkInitialization;
|
|
1736
|
-
return await this.#control(entry, operation(entry.query, entry), "Claude metadata request");
|
|
2249
|
+
await withAbort(entry.sdkInitialization, admission.cancellation.signal);
|
|
2250
|
+
return await withAbort(this.#control(entry, operation(entry.query, entry), "Claude metadata request"), admission.cancellation.signal);
|
|
1737
2251
|
} finally {
|
|
1738
2252
|
entry.lastUsedAt = Date.now();
|
|
1739
2253
|
if (entry.active === void 0 && entry.state === "idle") this.#armIdleTimer(entry);
|
|
1740
2254
|
}
|
|
2255
|
+
}).finally(() => {
|
|
2256
|
+
this.#finishMetadataAdmission(admission);
|
|
1741
2257
|
});
|
|
1742
2258
|
this.#admissionGate = admitted.then(() => void 0, () => void 0);
|
|
1743
|
-
return admitted;
|
|
2259
|
+
return withAbort(admitted, admission.cancellation.signal);
|
|
1744
2260
|
}
|
|
1745
|
-
async #metadataEntry(agent, model) {
|
|
2261
|
+
async #metadataEntry(agent, model, cancellationSignal) {
|
|
2262
|
+
if (cancellationSignal.aborted) throw abortFailure();
|
|
1746
2263
|
const sessionId = agent.id;
|
|
1747
2264
|
let entry = this.#entries.get(sessionId);
|
|
1748
2265
|
if (entry?.state === "disposed" || entry?.state === "disconnected" || entry?.state === "outcome-unknown") {
|
|
1749
2266
|
this.#entries.delete(sessionId);
|
|
1750
2267
|
await this.#disposeEntry(entry);
|
|
2268
|
+
if (cancellationSignal.aborted) throw abortFailure();
|
|
1751
2269
|
entry = void 0;
|
|
1752
2270
|
}
|
|
1753
2271
|
if (entry === void 0) {
|
|
1754
2272
|
await this.#makeRoom();
|
|
1755
|
-
|
|
2273
|
+
if (cancellationSignal.aborted) throw abortFailure();
|
|
2274
|
+
entry = await this.#createEntry(agent, model, void 0, void 0, cancellationSignal);
|
|
2275
|
+
if (cancellationSignal.aborted) {
|
|
2276
|
+
await this.#disposeEntry(entry);
|
|
2277
|
+
throw abortFailure();
|
|
2278
|
+
}
|
|
1756
2279
|
this.#entries.set(sessionId, entry);
|
|
1757
2280
|
}
|
|
1758
2281
|
if (entry.ownerAgent !== agent) throw new Error(`dsh-claude: live agent identity changed for session ${sessionId}`);
|
|
@@ -1761,14 +2284,15 @@ var ClaudeSupervisor = class {
|
|
|
1761
2284
|
clearTimeout(entry.idleTimer);
|
|
1762
2285
|
entry.idleTimer = void 0;
|
|
1763
2286
|
}
|
|
1764
|
-
await this.#syncPermissionMode(entry);
|
|
2287
|
+
await withAbort(this.#syncPermissionMode(entry), cancellationSignal);
|
|
2288
|
+
if (cancellationSignal.aborted) throw abortFailure();
|
|
1765
2289
|
return entry;
|
|
1766
2290
|
}
|
|
1767
2291
|
/** Run one SDK control request against a live entry, and discard the entry if
|
|
1768
2292
|
* it does not answer.
|
|
1769
2293
|
*
|
|
1770
|
-
* Turn admission and
|
|
1771
|
-
* unbounded control request stalls every session until the Host restarts.
|
|
2294
|
+
* Turn admission attempts and metadata reads share one process-wide gate,
|
|
2295
|
+
* so an unbounded control request stalls every session until the Host restarts.
|
|
1772
2296
|
* Bounding it is only half the cure: a timeout also proves this query has
|
|
1773
2297
|
* stopped answering, and keeping the entry means the next caller reuses the
|
|
1774
2298
|
* same dead process — timing out again, forever. Discarding it lets the next
|
|
@@ -1789,32 +2313,97 @@ var ClaudeSupervisor = class {
|
|
|
1789
2313
|
await this.#control(entry, entry.query.setPermissionMode(mode), "Claude Code permission mode switch");
|
|
1790
2314
|
entry.permissionMode = mode;
|
|
1791
2315
|
}
|
|
2316
|
+
#finishMetadataAdmission(admission) {
|
|
2317
|
+
this.#metadataAdmissions.delete(admission);
|
|
2318
|
+
admission.complete();
|
|
2319
|
+
}
|
|
2320
|
+
#cancelMetadataAdmissions(predicate) {
|
|
2321
|
+
const cancelled = [...this.#metadataAdmissions].filter(predicate);
|
|
2322
|
+
for (const admission of cancelled) {
|
|
2323
|
+
admission.cancellation.abort();
|
|
2324
|
+
if (!admission.started) this.#finishMetadataAdmission(admission);
|
|
2325
|
+
}
|
|
2326
|
+
return cancelled.map((admission) => admission.completion);
|
|
2327
|
+
}
|
|
2328
|
+
#cancelTurnAdmissions(predicate, error) {
|
|
2329
|
+
const cancelled = this.#turnAdmissions.filter(predicate);
|
|
2330
|
+
for (const admission of cancelled) {
|
|
2331
|
+
admission.cancellation.abort();
|
|
2332
|
+
if (!admission.delivered) {
|
|
2333
|
+
admission.delivered = true;
|
|
2334
|
+
admission.reject(error);
|
|
2335
|
+
}
|
|
2336
|
+
if (!admission.admitting) this.#finishTurnAdmission(admission, { error });
|
|
2337
|
+
}
|
|
2338
|
+
if (cancelled.length > 0) {
|
|
2339
|
+
this.#admissionRevision += 1;
|
|
2340
|
+
this.#blockedAdmissionRevision = void 0;
|
|
2341
|
+
this.#scheduleTurnAdmissions();
|
|
2342
|
+
}
|
|
2343
|
+
return cancelled.map((admission) => admission.completion);
|
|
2344
|
+
}
|
|
2345
|
+
limitsChanged() {
|
|
2346
|
+
if (this.#disposed) return;
|
|
2347
|
+
this.#notifyCapacityChange();
|
|
2348
|
+
this.#scheduleLimitReconciliation();
|
|
2349
|
+
}
|
|
1792
2350
|
async disposeSession(sessionId) {
|
|
2351
|
+
const pendingAdmissions = this.#cancelTurnAdmissions((admission) => admission.request.agent.id === sessionId, abortFailure());
|
|
2352
|
+
const pendingMetadata = this.#cancelMetadataAdmissions((admission) => admission.sessionId === sessionId);
|
|
1793
2353
|
const entry = this.#entries.get(sessionId);
|
|
1794
|
-
if (entry
|
|
1795
|
-
|
|
1796
|
-
|
|
2354
|
+
if (entry !== void 0) this.#entries.delete(sessionId);
|
|
2355
|
+
await Promise.allSettled([
|
|
2356
|
+
...pendingAdmissions,
|
|
2357
|
+
...pendingMetadata,
|
|
2358
|
+
...entry === void 0 ? [] : [this.#disposeEntry(entry)]
|
|
2359
|
+
]);
|
|
1797
2360
|
}
|
|
1798
2361
|
async dispose() {
|
|
1799
2362
|
if (this.#disposed) return;
|
|
1800
2363
|
this.#disposed = true;
|
|
2364
|
+
const pendingAdmissions = this.#cancelTurnAdmissions(() => true, /* @__PURE__ */ new Error("dsh-claude: supervisor is disposed"));
|
|
2365
|
+
const pendingMetadata = this.#cancelMetadataAdmissions(() => true);
|
|
1801
2366
|
const entries = [...this.#entries.values()];
|
|
1802
2367
|
this.#entries.clear();
|
|
1803
|
-
|
|
2368
|
+
this.#notifyCapacityChange();
|
|
2369
|
+
await Promise.allSettled([
|
|
2370
|
+
...pendingAdmissions,
|
|
2371
|
+
...pendingMetadata,
|
|
2372
|
+
...entries.map((entry) => this.#disposeEntry(entry))
|
|
2373
|
+
]);
|
|
1804
2374
|
}
|
|
1805
2375
|
async #makeRoom() {
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
2376
|
+
while (this.#entries.size >= this.#config.maxProcesses) {
|
|
2377
|
+
const idle = [...this.#entries.values()].filter((entry) => entry.active === void 0 && entry.state === "idle").sort((left, right) => left.lastUsedAt - right.lastUsedAt)[0];
|
|
2378
|
+
if (idle === void 0) throw new ClaudeProcessLimitError(this.#config.maxProcesses);
|
|
2379
|
+
this.#entries.delete(idle.sessionId);
|
|
2380
|
+
await this.#disposeEntry(idle);
|
|
2381
|
+
}
|
|
1811
2382
|
}
|
|
1812
|
-
async #
|
|
2383
|
+
async #trimExcessIdle() {
|
|
2384
|
+
while (this.#entries.size > this.#config.maxProcesses) {
|
|
2385
|
+
const idle = [...this.#entries.values()].filter((entry) => entry.active === void 0 && entry.state === "idle").sort((left, right) => left.lastUsedAt - right.lastUsedAt)[0];
|
|
2386
|
+
if (idle === void 0) return;
|
|
2387
|
+
this.#entries.delete(idle.sessionId);
|
|
2388
|
+
await this.#disposeEntry(idle);
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
#notifyCapacityChange() {
|
|
2392
|
+
this.#admissionRevision += 1;
|
|
2393
|
+
this.#blockedAdmissionRevision = void 0;
|
|
2394
|
+
this.#scheduleTurnAdmissions();
|
|
2395
|
+
}
|
|
2396
|
+
#scheduleLimitReconciliation() {
|
|
2397
|
+
const operation = this.#admissionGate.then(() => this.#trimExcessIdle());
|
|
2398
|
+
this.#admissionGate = operation.then(() => void 0, () => void 0);
|
|
2399
|
+
}
|
|
2400
|
+
async #createEntry(agent, model, thinkingMode, signal, cancellationSignal) {
|
|
1813
2401
|
const sessionId = agent.id;
|
|
1814
2402
|
const cwd = agent.session.header.cwd ?? process.cwd();
|
|
1815
2403
|
const input = new AsyncQueue();
|
|
1816
2404
|
const lifetime = new AbortController();
|
|
1817
2405
|
const projection = await this.#sidecar.importLegacy(sessionId, agent.session.events);
|
|
2406
|
+
if (signalAborted(signal) || signalAborted(cancellationSignal)) throw abortFailure();
|
|
1818
2407
|
const binding = projection.binding;
|
|
1819
2408
|
const pendingRewind = projection.rewind?.pending;
|
|
1820
2409
|
const forkAt = pendingRewind !== void 0 && "resumeAt" in pendingRewind ? pendingRewind.resumeAt : void 0;
|
|
@@ -1860,7 +2449,7 @@ var ClaudeSupervisor = class {
|
|
|
1860
2449
|
};
|
|
1861
2450
|
};
|
|
1862
2451
|
const userQuestion = createUserQuestionBridge(this.#userQuestions, activeInteraction);
|
|
1863
|
-
const canUseTool = createPermissionBridge(this.#approval, activeInteraction, userQuestion);
|
|
2452
|
+
const canUseTool = createPermissionBridge(this.#approval, activeInteraction, userQuestion, this.planFeedback);
|
|
1864
2453
|
const options = {
|
|
1865
2454
|
pathToClaudeCodeExecutable: this.#config.executablePath,
|
|
1866
2455
|
cwd,
|
|
@@ -2000,7 +2589,7 @@ var ClaudeSupervisor = class {
|
|
|
2000
2589
|
title: "Claude thinking",
|
|
2001
2590
|
summary: message.text
|
|
2002
2591
|
});
|
|
2003
|
-
if (
|
|
2592
|
+
if (active.native) active.output.push({
|
|
2004
2593
|
type: "thinking",
|
|
2005
2594
|
text: message.text
|
|
2006
2595
|
});
|
|
@@ -2019,7 +2608,7 @@ var ClaudeSupervisor = class {
|
|
|
2019
2608
|
});
|
|
2020
2609
|
if (message.parentToolUseId === void 0) {
|
|
2021
2610
|
active.openCalls.set(message.toolUseId, message.toolName);
|
|
2022
|
-
if (
|
|
2611
|
+
if (active.native) {
|
|
2023
2612
|
this.#ensureDynamicPresenter(active.agent, message.toolName);
|
|
2024
2613
|
await this.#appendNativeToolCall(active, message);
|
|
2025
2614
|
}
|
|
@@ -2036,7 +2625,7 @@ var ClaudeSupervisor = class {
|
|
|
2036
2625
|
detail: message.output,
|
|
2037
2626
|
isError: message.isError
|
|
2038
2627
|
});
|
|
2039
|
-
if (message.parentToolUseId === void 0 &&
|
|
2628
|
+
if (message.parentToolUseId === void 0 && active.native) await this.#appendNativeToolResult(active, message);
|
|
2040
2629
|
return;
|
|
2041
2630
|
case "subagent":
|
|
2042
2631
|
await this.#appendActivity(active, {
|
|
@@ -2086,7 +2675,7 @@ var ClaudeSupervisor = class {
|
|
|
2086
2675
|
title: message.toolName,
|
|
2087
2676
|
summary: message.summary
|
|
2088
2677
|
});
|
|
2089
|
-
if (
|
|
2678
|
+
if (active.native) await this.#appendNativeToolResult(active, {
|
|
2090
2679
|
kind: "tool-result",
|
|
2091
2680
|
toolUseId: message.toolUseId,
|
|
2092
2681
|
output: message.summary,
|
|
@@ -2106,9 +2695,6 @@ var ClaudeSupervisor = class {
|
|
|
2106
2695
|
...active.firstOutputAt === void 0 ? {} : { ttftMs: Math.max(0, active.firstOutputAt - active.startedAt) }
|
|
2107
2696
|
};
|
|
2108
2697
|
}
|
|
2109
|
-
#nativeRendering() {
|
|
2110
|
-
return (this.#config.renderMode ?? "plugin") === "native";
|
|
2111
|
-
}
|
|
2112
2698
|
/** Register one presenter-only mirror for a tool name the static preset
|
|
2113
2699
|
* registry does not cover (MCP tools, newly added built-ins). Runs in the
|
|
2114
2700
|
* agent scope so the mirror is visible only to this preset's sessions and
|
|
@@ -2340,6 +2926,8 @@ var ClaudeSupervisor = class {
|
|
|
2340
2926
|
await this.#recordChainAnchor(entry, active);
|
|
2341
2927
|
this.#checkpointProjection(entry);
|
|
2342
2928
|
this.#armIdleTimer(entry);
|
|
2929
|
+
this.#notifyCapacityChange();
|
|
2930
|
+
this.#scheduleLimitReconciliation();
|
|
2343
2931
|
return;
|
|
2344
2932
|
}
|
|
2345
2933
|
if (result.usage.inputTokens !== void 0 || result.usage.outputTokens !== void 0 || result.usage.cumulativeCostUsd !== void 0) {
|
|
@@ -2421,9 +3009,11 @@ var ClaudeSupervisor = class {
|
|
|
2421
3009
|
entry.state = "idle";
|
|
2422
3010
|
entry.lastUsedAt = Date.now();
|
|
2423
3011
|
await this.#recordChainAnchor(entry, active);
|
|
2424
|
-
await this.#learnContextWindow(entry);
|
|
2425
3012
|
this.#checkpointProjection(entry);
|
|
2426
3013
|
this.#armIdleTimer(entry);
|
|
3014
|
+
this.#notifyCapacityChange();
|
|
3015
|
+
this.#scheduleLimitReconciliation();
|
|
3016
|
+
await this.#learnContextWindow(entry);
|
|
2427
3017
|
}
|
|
2428
3018
|
/** Tell every reader where this session's delta stream ended.
|
|
2429
3019
|
*
|
|
@@ -2436,6 +3026,20 @@ var ClaudeSupervisor = class {
|
|
|
2436
3026
|
this.#sidecar.checkpoint(entry.sessionId);
|
|
2437
3027
|
} catch {}
|
|
2438
3028
|
}
|
|
3029
|
+
/** Pin the working tree this turn is about to change, so a rewind of it can
|
|
3030
|
+
* put the checkout back where the turn found it.
|
|
3031
|
+
*
|
|
3032
|
+
* Awaited, and deliberately: a snapshot taken after Claude's first edit
|
|
3033
|
+
* would restore to a state that never existed. It costs one `git add -A`
|
|
3034
|
+
* against a throwaway index per turn, and best effort throughout -- a
|
|
3035
|
+
* session with no repository simply never offers a file rewind. */
|
|
3036
|
+
async #captureWorktree(entry, turn) {
|
|
3037
|
+
try {
|
|
3038
|
+
const tree = await captureWorktreeTree(this.#runtime, entry.cwd);
|
|
3039
|
+
if (tree === void 0) return;
|
|
3040
|
+
await this.#sidecar.recordRewindSnapshot(entry.sessionId, turn, tree);
|
|
3041
|
+
} catch {}
|
|
3042
|
+
}
|
|
2439
3043
|
/** Pin where Claude's chain ended for the DSH turn that just settled, so a
|
|
2440
3044
|
* later rewind of the following turn can fork exactly here. Best effort:
|
|
2441
3045
|
* a missing anchor only makes a rewind fall back to an earlier turn. */
|
|
@@ -2453,7 +3057,7 @@ var ClaudeSupervisor = class {
|
|
|
2453
3057
|
try {
|
|
2454
3058
|
this.#sidecar.appendTranscriptText(active.agent.id, {
|
|
2455
3059
|
text: active.transcriptText,
|
|
2456
|
-
...
|
|
3060
|
+
...active.native ? { renderer: "native" } : {},
|
|
2457
3061
|
turn: active.cursor.turn,
|
|
2458
3062
|
step: active.cursor.step,
|
|
2459
3063
|
ordinal
|
|
@@ -2472,7 +3076,7 @@ var ClaudeSupervisor = class {
|
|
|
2472
3076
|
const ordinal = active.cursor.nextOrdinal++;
|
|
2473
3077
|
await this.#sidecar.appendActivity(active.agent.id, {
|
|
2474
3078
|
...activity,
|
|
2475
|
-
...
|
|
3079
|
+
...active.native ? { renderer: "native" } : {},
|
|
2476
3080
|
turn: active.cursor.turn,
|
|
2477
3081
|
step: active.cursor.step,
|
|
2478
3082
|
ordinal
|
|
@@ -2511,7 +3115,7 @@ var ClaudeSupervisor = class {
|
|
|
2511
3115
|
summary,
|
|
2512
3116
|
isError: true
|
|
2513
3117
|
});
|
|
2514
|
-
if (
|
|
3118
|
+
if (active.native) await this.#appendNativeToolResult(active, {
|
|
2515
3119
|
kind: "tool-result",
|
|
2516
3120
|
toolUseId,
|
|
2517
3121
|
output: summary,
|
|
@@ -2591,6 +3195,7 @@ var ClaudeSupervisor = class {
|
|
|
2591
3195
|
if (entry.process !== void 0) try {
|
|
2592
3196
|
await entry.process.handle.waitForExit(AbortSignal.timeout(5e3));
|
|
2593
3197
|
} catch {}
|
|
3198
|
+
this.#notifyCapacityChange();
|
|
2594
3199
|
}
|
|
2595
3200
|
};
|
|
2596
3201
|
//#endregion
|
|
@@ -2670,6 +3275,85 @@ function formatReviewComments(comments) {
|
|
|
2670
3275
|
].join("\n");
|
|
2671
3276
|
}
|
|
2672
3277
|
//#endregion
|
|
3278
|
+
//#region src/session-title.ts
|
|
3279
|
+
/** Answer DSH's auxiliary session-title request with a throwaway Haiku turn.
|
|
3280
|
+
*
|
|
3281
|
+
* DSH titles a session by asking the model behind the session's own route for
|
|
3282
|
+
* a summary of the first human message. That route is this plugin, and the
|
|
3283
|
+
* session's Claude process must not answer it: the title call carries a
|
|
3284
|
+
* plugin-authored system prompt and would land in the user's transcript. A
|
|
3285
|
+
* separate one-shot turn keeps it out, for the same reasons as the branch-name
|
|
3286
|
+
* summary and the plan-usage probe. Deployments that never installed a second
|
|
3287
|
+
* model provider still get a readable title, since the only model this plugin
|
|
3288
|
+
* needs is the one it already runs. */
|
|
3289
|
+
/** Cheapest model that can summarize a sentence in the language it was written in. */
|
|
3290
|
+
const SESSION_TITLE_MODEL = "haiku";
|
|
3291
|
+
/** Backstop only. The title service wraps its own deadline (60s by default)
|
|
3292
|
+
* around the call and passes it as `signal`, so a shorter budget here just
|
|
3293
|
+
* kills a turn the caller was still happy to wait for — a cold CLI start plus
|
|
3294
|
+
* one Haiku reply routinely passes ten seconds. */
|
|
3295
|
+
const SESSION_TITLE_TIMEOUT_MS = 6e4;
|
|
3296
|
+
/** DSH frames the messages as JSON under its own byte cap; this only bounds a
|
|
3297
|
+
* caller that does not. */
|
|
3298
|
+
const MAX_INPUT_CHARS = 8e3;
|
|
3299
|
+
/** Longer than any title DSH accepts (80 bytes), short enough to bound prose. */
|
|
3300
|
+
const MAX_TITLE_CHARS = 200;
|
|
3301
|
+
/** Carry DSH's instruction as the prompt's own preamble: a Claude Code turn has
|
|
3302
|
+
* no separate system slot this plugin can borrow without replacing the CLI's. */
|
|
3303
|
+
function sessionTitlePrompt(request) {
|
|
3304
|
+
const input = request.input.trim().slice(0, MAX_INPUT_CHARS);
|
|
3305
|
+
return request.system === void 0 || request.system.length === 0 ? input : `${request.system}\n\n${input}`;
|
|
3306
|
+
}
|
|
3307
|
+
/** The one line of the reply that is the title. DSH strips control characters
|
|
3308
|
+
* and truncates to its own byte cap, so nothing else is cleaned here. */
|
|
3309
|
+
function sessionTitleLine(reply) {
|
|
3310
|
+
const line = reply.split("\n").map((candidate) => candidate.trim()).find((candidate) => candidate.length > 0);
|
|
3311
|
+
return line === void 0 ? "" : line.slice(0, MAX_TITLE_CHARS);
|
|
3312
|
+
}
|
|
3313
|
+
/**
|
|
3314
|
+
* Summarize the framed messages into one title line.
|
|
3315
|
+
*
|
|
3316
|
+
* Rejects rather than returning a placeholder: the title service logs the
|
|
3317
|
+
* failure and keeps the deterministic first-words fallback, which is a better
|
|
3318
|
+
* label than anything this function could invent.
|
|
3319
|
+
*/
|
|
3320
|
+
async function summarizeSessionTitle(executablePath, request, factory = query) {
|
|
3321
|
+
const prompt = sessionTitlePrompt(request);
|
|
3322
|
+
if (prompt.length === 0) throw new Error("dsh-claude: the session-title request carried no text");
|
|
3323
|
+
const lifetime = new AbortController();
|
|
3324
|
+
const abort = () => {
|
|
3325
|
+
lifetime.abort();
|
|
3326
|
+
};
|
|
3327
|
+
request.signal?.addEventListener("abort", abort, { once: true });
|
|
3328
|
+
const timer = setTimeout(abort, SESSION_TITLE_TIMEOUT_MS);
|
|
3329
|
+
timer.unref?.();
|
|
3330
|
+
try {
|
|
3331
|
+
const query = factory({
|
|
3332
|
+
prompt,
|
|
3333
|
+
options: {
|
|
3334
|
+
cwd: process.cwd(),
|
|
3335
|
+
abortController: lifetime,
|
|
3336
|
+
model: SESSION_TITLE_MODEL,
|
|
3337
|
+
allowedTools: [],
|
|
3338
|
+
settingSources: [],
|
|
3339
|
+
maxTurns: 1,
|
|
3340
|
+
...executablePath.length === 0 ? {} : { pathToClaudeCodeExecutable: executablePath }
|
|
3341
|
+
}
|
|
3342
|
+
});
|
|
3343
|
+
for await (const message of query) {
|
|
3344
|
+
if (message.type !== "result" || message.subtype !== "success") continue;
|
|
3345
|
+
const title = sessionTitleLine(message.result);
|
|
3346
|
+
if (title.length > 0) return title;
|
|
3347
|
+
break;
|
|
3348
|
+
}
|
|
3349
|
+
throw new Error("dsh-claude: the session-title turn produced no title");
|
|
3350
|
+
} finally {
|
|
3351
|
+
clearTimeout(timer);
|
|
3352
|
+
request.signal?.removeEventListener("abort", abort);
|
|
3353
|
+
lifetime.abort();
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
//#endregion
|
|
2673
3357
|
//#region src/adapter.ts
|
|
2674
3358
|
const THINKING_MODES = [
|
|
2675
3359
|
{
|
|
@@ -2822,6 +3506,12 @@ function tokenUsage(usage) {
|
|
|
2822
3506
|
if (usage.cacheCreationTokens !== void 0) normalized.cacheWriteTokens = usage.cacheCreationTokens;
|
|
2823
3507
|
return normalized;
|
|
2824
3508
|
}
|
|
3509
|
+
/** Flatten a hand-built auxiliary request to text. Unlike a conversation turn
|
|
3510
|
+
* it has no images and no human-sourced message to single out: every message
|
|
3511
|
+
* in it was assembled by the plugin that asked the question. */
|
|
3512
|
+
function auxiliaryText(messages) {
|
|
3513
|
+
return messages.flatMap((message) => message.content.filter((block) => block.type === "text").map((block) => block.text)).join("\n");
|
|
3514
|
+
}
|
|
2825
3515
|
function resolveAgent(agents, options) {
|
|
2826
3516
|
const initiator = agents.currentInitiator();
|
|
2827
3517
|
if (initiator !== void 0) return initiator;
|
|
@@ -2837,8 +3527,12 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2837
3527
|
#attachments;
|
|
2838
3528
|
#presetIdFor;
|
|
2839
3529
|
#drainReviewComments;
|
|
3530
|
+
/** The renderer setting, read from its file at the start of each turn. A
|
|
3531
|
+
* cached copy would go stale whenever the file is edited outside the
|
|
3532
|
+
* Settings dialog, and the read is dwarfed by the process the turn spawns. */
|
|
2840
3533
|
#renderMode;
|
|
2841
|
-
|
|
3534
|
+
#summarizeTitle;
|
|
3535
|
+
constructor(supervisor, agents, attachments, presetIdFor, drainReviewComments = () => [], renderMode = async () => DEFAULT_CLAUDE_RENDER_MODE, summarizeTitle = (request) => summarizeSessionTitle("", request)) {
|
|
2842
3536
|
super();
|
|
2843
3537
|
this.#supervisor = supervisor;
|
|
2844
3538
|
this.#agents = agents;
|
|
@@ -2846,6 +3540,7 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2846
3540
|
this.#presetIdFor = presetIdFor;
|
|
2847
3541
|
this.#drainReviewComments = drainReviewComments;
|
|
2848
3542
|
this.#renderMode = renderMode;
|
|
3543
|
+
this.#summarizeTitle = summarizeTitle;
|
|
2849
3544
|
}
|
|
2850
3545
|
providerInfo(provider) {
|
|
2851
3546
|
return {
|
|
@@ -2888,7 +3583,47 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2888
3583
|
stream: (options) => this.stream(options)
|
|
2889
3584
|
};
|
|
2890
3585
|
}
|
|
3586
|
+
/** Title the session from its own provider without touching its process.
|
|
3587
|
+
*
|
|
3588
|
+
* DSH routes the title request at the session's model, which is this
|
|
3589
|
+
* adapter; a deployment with no second provider configured would otherwise
|
|
3590
|
+
* never get a title at all and keep the first five words of the first
|
|
3591
|
+
* message. A throwaway Haiku turn answers it, so the session's transcript,
|
|
3592
|
+
* context, and permission bridge stay out of it. */
|
|
3593
|
+
async *#titleStream(options) {
|
|
3594
|
+
const title = await this.#summarizeTitle({
|
|
3595
|
+
...options.system === void 0 ? {} : { system: options.system },
|
|
3596
|
+
input: auxiliaryText(options.messages),
|
|
3597
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
3598
|
+
});
|
|
3599
|
+
yield {
|
|
3600
|
+
type: "block-start",
|
|
3601
|
+
index: 0,
|
|
3602
|
+
blockType: "text"
|
|
3603
|
+
};
|
|
3604
|
+
yield {
|
|
3605
|
+
type: "text-delta",
|
|
3606
|
+
index: 0,
|
|
3607
|
+
text: title
|
|
3608
|
+
};
|
|
3609
|
+
yield {
|
|
3610
|
+
type: "block-end",
|
|
3611
|
+
index: 0,
|
|
3612
|
+
block: {
|
|
3613
|
+
type: "text",
|
|
3614
|
+
text: title
|
|
3615
|
+
}
|
|
3616
|
+
};
|
|
3617
|
+
yield {
|
|
3618
|
+
type: "finish",
|
|
3619
|
+
reason: { kind: "stop" }
|
|
3620
|
+
};
|
|
3621
|
+
}
|
|
2891
3622
|
async *stream(options) {
|
|
3623
|
+
if (options.purpose === "session-title") {
|
|
3624
|
+
yield* this.#titleStream(options);
|
|
3625
|
+
return;
|
|
3626
|
+
}
|
|
2892
3627
|
if (options.purpose !== void 0) throw new Error(`dsh-claude: auxiliary ${options.purpose} calls are not routed into the Claude session`);
|
|
2893
3628
|
const agent = resolveAgent(this.#agents, options);
|
|
2894
3629
|
if (this.#presetIdFor(agent) !== "claude") throw new Error(`dsh-claude: provider ${CLAUDE_CODE_PROVIDER} is available only to the ${CLAUDE_CODE_PRESET_ID} preset`);
|
|
@@ -2910,14 +3645,16 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
2910
3645
|
};
|
|
2911
3646
|
return;
|
|
2912
3647
|
}
|
|
3648
|
+
const renderMode = await this.#renderMode();
|
|
3649
|
+
const native = renderMode === "native";
|
|
2913
3650
|
const events = await this.#supervisor.runTurn({
|
|
2914
3651
|
agent,
|
|
2915
3652
|
prompt: injectReviewComments(prompt, this.#drainReviewComments(agent.id)),
|
|
2916
3653
|
model: options.model,
|
|
3654
|
+
renderMode,
|
|
2917
3655
|
...thinkingMode === void 0 ? {} : { thinkingMode },
|
|
2918
3656
|
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
2919
3657
|
});
|
|
2920
|
-
const native = this.#renderMode() === "native";
|
|
2921
3658
|
let pendingUsage;
|
|
2922
3659
|
let completed = false;
|
|
2923
3660
|
let blockIndex = 0;
|
|
@@ -3016,8 +3753,8 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
|
|
|
3016
3753
|
if (!completed) throw new Error("dsh-claude: Claude turn stream ended without a result");
|
|
3017
3754
|
}
|
|
3018
3755
|
};
|
|
3019
|
-
function createClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments = () => [], renderMode = () => DEFAULT_CLAUDE_RENDER_MODE) {
|
|
3020
|
-
return new ClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments, renderMode);
|
|
3756
|
+
function createClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments = () => [], renderMode = async () => DEFAULT_CLAUDE_RENDER_MODE, summarizeTitle = (request) => summarizeSessionTitle("", request)) {
|
|
3757
|
+
return new ClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments, renderMode, summarizeTitle);
|
|
3021
3758
|
}
|
|
3022
3759
|
//#endregion
|
|
3023
3760
|
//#region src/plugin-budget.ts
|
|
@@ -3326,7 +4063,7 @@ function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolution
|
|
|
3326
4063
|
}
|
|
3327
4064
|
//#endregion
|
|
3328
4065
|
//#region src/projection-routes.ts
|
|
3329
|
-
const MAX_SESSION_ID_CHARS$
|
|
4066
|
+
const MAX_SESSION_ID_CHARS$7 = 1024;
|
|
3330
4067
|
/** Slow-moving metadata refresh and stream heartbeat cadence; deliberately off
|
|
3331
4068
|
* the transcript hot path so git/gh latency never delays visible text. */
|
|
3332
4069
|
const META_REFRESH_MS = 5e3;
|
|
@@ -3334,7 +4071,7 @@ const META_REFRESH_MS = 5e3;
|
|
|
3334
4071
|
* so this cannot collide with one. */
|
|
3335
4072
|
const MULTI_SEGMENT = "multi";
|
|
3336
4073
|
function validSessionId(value) {
|
|
3337
|
-
return value.length > 0 && value.length <= MAX_SESSION_ID_CHARS$
|
|
4074
|
+
return value.length > 0 && value.length <= MAX_SESSION_ID_CHARS$7;
|
|
3338
4075
|
}
|
|
3339
4076
|
function targetFromUrl(url) {
|
|
3340
4077
|
const prefix = `${CLAUDE_PROJECTION_PATH}/`;
|
|
@@ -3512,7 +4249,11 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
|
|
|
3512
4249
|
}
|
|
3513
4250
|
}));
|
|
3514
4251
|
for (const sessionId of sessionIds) writeSnapshot(sessionId).catch(() => void 0);
|
|
4252
|
+
let sweeping = false;
|
|
3515
4253
|
const timer = setInterval(() => {
|
|
4254
|
+
writeLine({ type: "ping" });
|
|
4255
|
+
if (sweeping) return;
|
|
4256
|
+
sweeping = true;
|
|
3516
4257
|
(async () => {
|
|
3517
4258
|
for (const sessionId of sessionIds) {
|
|
3518
4259
|
if (closed) return;
|
|
@@ -3521,8 +4262,9 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
|
|
|
3521
4262
|
if (JSON.stringify(next) === JSON.stringify(metas.get(sessionId))) continue;
|
|
3522
4263
|
writeMeta(sessionId, next);
|
|
3523
4264
|
}
|
|
3524
|
-
|
|
3525
|
-
|
|
4265
|
+
})().catch(() => void 0).finally(() => {
|
|
4266
|
+
sweeping = false;
|
|
4267
|
+
});
|
|
3526
4268
|
}, META_REFRESH_MS);
|
|
3527
4269
|
timer.unref?.();
|
|
3528
4270
|
await new Promise((resolve) => {
|
|
@@ -3676,14 +4418,14 @@ function parseGitHubRemote(value) {
|
|
|
3676
4418
|
if (match?.[1] === void 0 || match[2] === void 0) return void 0;
|
|
3677
4419
|
return `${match[1]}/${match[2]}`;
|
|
3678
4420
|
}
|
|
3679
|
-
function record$
|
|
4421
|
+
function record$11(value) {
|
|
3680
4422
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
3681
4423
|
}
|
|
3682
4424
|
function aggregateChecks(value) {
|
|
3683
4425
|
if (!Array.isArray(value) || value.length === 0) return "none";
|
|
3684
4426
|
let pending = false;
|
|
3685
4427
|
for (const item of value) {
|
|
3686
|
-
const check = record$
|
|
4428
|
+
const check = record$11(item);
|
|
3687
4429
|
if (check === void 0) continue;
|
|
3688
4430
|
const conclusion = typeof check.conclusion === "string" ? check.conclusion.toUpperCase() : void 0;
|
|
3689
4431
|
const status = typeof check.status === "string" ? check.status.toUpperCase() : void 0;
|
|
@@ -3707,7 +4449,7 @@ function reviewState(value) {
|
|
|
3707
4449
|
return "none";
|
|
3708
4450
|
}
|
|
3709
4451
|
function parsePullRequest(value) {
|
|
3710
|
-
const input = record$
|
|
4452
|
+
const input = record$11(value);
|
|
3711
4453
|
if (input === void 0 || !Number.isSafeInteger(input.number) || Number(input.number) <= 0 || typeof input.title !== "string" || typeof input.url !== "string") return void 0;
|
|
3712
4454
|
let url;
|
|
3713
4455
|
try {
|
|
@@ -3728,7 +4470,7 @@ function parsePullRequest(value) {
|
|
|
3728
4470
|
review: reviewState(input.reviewDecision),
|
|
3729
4471
|
checks: aggregateChecks(input.statusCheckRollup),
|
|
3730
4472
|
...typeof input.mergeStateStatus === "string" ? { mergeState: bounded(input.mergeStateStatus) } : {},
|
|
3731
|
-
...typeof record$
|
|
4473
|
+
...typeof record$11(input.author)?.login === "string" ? { author: bounded(String(record$11(input.author)?.login)) } : {},
|
|
3732
4474
|
...typeof input.createdAt === "string" && Number.isFinite(Date.parse(input.createdAt)) ? { createdAt: new Date(input.createdAt).toISOString() } : {},
|
|
3733
4475
|
...typeof input.mergedAt === "string" && Number.isFinite(Date.parse(input.mergedAt)) ? { mergedAt: new Date(input.mergedAt).toISOString() } : {},
|
|
3734
4476
|
...typeof input.baseRefName === "string" && bounded(input.baseRefName).length > 0 ? { baseBranch: bounded(input.baseRefName) } : {}
|
|
@@ -5135,19 +5877,19 @@ var RepositoryActionService = class {
|
|
|
5135
5877
|
};
|
|
5136
5878
|
//#endregion
|
|
5137
5879
|
//#region src/repository-setup-routes.ts
|
|
5138
|
-
const MAX_BODY_BYTES$
|
|
5139
|
-
function record$
|
|
5880
|
+
const MAX_BODY_BYTES$7 = 16384;
|
|
5881
|
+
function record$10(value) {
|
|
5140
5882
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
5141
5883
|
}
|
|
5142
|
-
async function readJson$
|
|
5884
|
+
async function readJson$6(io) {
|
|
5143
5885
|
let parsed;
|
|
5144
5886
|
try {
|
|
5145
|
-
parsed = await io.body(MAX_BODY_BYTES$
|
|
5887
|
+
parsed = await io.body(MAX_BODY_BYTES$7);
|
|
5146
5888
|
} catch (error) {
|
|
5147
5889
|
if (error instanceof SyntaxError) throw error;
|
|
5148
5890
|
throw new RepositorySetupError("body-too-large", "The request body is too large.");
|
|
5149
5891
|
}
|
|
5150
|
-
const value = record$
|
|
5892
|
+
const value = record$10(parsed);
|
|
5151
5893
|
if (value === void 0) throw new RepositorySetupError("invalid-request", "The request body is invalid.");
|
|
5152
5894
|
return value;
|
|
5153
5895
|
}
|
|
@@ -5216,7 +5958,7 @@ function registerRepositorySetupRoute(ctx, service, sweep) {
|
|
|
5216
5958
|
try {
|
|
5217
5959
|
if (pathname === `/plugins/dsh-claude/repository/setup/branches/refresh`) {
|
|
5218
5960
|
if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
5219
|
-
const input = await readJson$
|
|
5961
|
+
const input = await readJson$6(io);
|
|
5220
5962
|
return json(res, 200, await service.refreshBranches(string$2(input, "cwd")));
|
|
5221
5963
|
}
|
|
5222
5964
|
if (pathname === `/plugins/dsh-claude/repository/setup/branches`) {
|
|
@@ -5227,14 +5969,14 @@ function registerRepositorySetupRoute(ctx, service, sweep) {
|
|
|
5227
5969
|
}
|
|
5228
5970
|
if (pathname === "/plugins/dsh-claude/repository/setup") {
|
|
5229
5971
|
if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
5230
|
-
const input = await readJson$
|
|
5972
|
+
const input = await readJson$6(io);
|
|
5231
5973
|
if (typeof input.worktree !== "boolean") throw new RepositorySetupError("invalid-request", "The worktree field is required.");
|
|
5232
5974
|
await streamSetup(res, service, input);
|
|
5233
5975
|
return;
|
|
5234
5976
|
}
|
|
5235
5977
|
if (pathname === `/plugins/dsh-claude/repository/setup/cleanup`) {
|
|
5236
5978
|
if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
5237
|
-
const input = await readJson$
|
|
5979
|
+
const input = await readJson$6(io);
|
|
5238
5980
|
return json(res, 200, await service.cleanupMerged(string$2(input, "path"), string$2(input, "baseBranch")));
|
|
5239
5981
|
}
|
|
5240
5982
|
if (pathname === `/plugins/dsh-claude/repository/setup/sweep`) {
|
|
@@ -5244,7 +5986,7 @@ function registerRepositorySetupRoute(ctx, service, sweep) {
|
|
|
5244
5986
|
}
|
|
5245
5987
|
if (pathname === `/plugins/dsh-claude/repository/setup/bind`) {
|
|
5246
5988
|
if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
|
|
5247
|
-
const input = await readJson$
|
|
5989
|
+
const input = await readJson$6(io);
|
|
5248
5990
|
await service.bindLease(string$2(input, "leaseId"), string$2(input, "sessionId"));
|
|
5249
5991
|
return json(res, 200, { ok: true });
|
|
5250
5992
|
}
|
|
@@ -5262,8 +6004,8 @@ function registerRepositorySetupRoute(ctx, service, sweep) {
|
|
|
5262
6004
|
}
|
|
5263
6005
|
//#endregion
|
|
5264
6006
|
//#region src/repository-action-routes.ts
|
|
5265
|
-
const MAX_BODY_BYTES$
|
|
5266
|
-
const MAX_SESSION_ID_CHARS$
|
|
6007
|
+
const MAX_BODY_BYTES$6 = 16384;
|
|
6008
|
+
const MAX_SESSION_ID_CHARS$6 = 1024;
|
|
5267
6009
|
const ACTIONS = /* @__PURE__ */ new Set([
|
|
5268
6010
|
"commit",
|
|
5269
6011
|
"commit-push",
|
|
@@ -5272,24 +6014,24 @@ const ACTIONS = /* @__PURE__ */ new Set([
|
|
|
5272
6014
|
"merge-pr",
|
|
5273
6015
|
"update-branch"
|
|
5274
6016
|
]);
|
|
5275
|
-
function record$
|
|
6017
|
+
function record$9(value) {
|
|
5276
6018
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
5277
6019
|
}
|
|
5278
|
-
async function readJson$
|
|
6020
|
+
async function readJson$5(io) {
|
|
5279
6021
|
let parsed;
|
|
5280
6022
|
try {
|
|
5281
|
-
parsed = await io.body(MAX_BODY_BYTES$
|
|
6023
|
+
parsed = await io.body(MAX_BODY_BYTES$6);
|
|
5282
6024
|
} catch (error) {
|
|
5283
6025
|
if (error instanceof SyntaxError) throw error;
|
|
5284
6026
|
throw new RepositoryActionError("body-too-large", "The request body is too large.");
|
|
5285
6027
|
}
|
|
5286
|
-
const value = record$
|
|
6028
|
+
const value = record$9(parsed);
|
|
5287
6029
|
if (value === void 0) throw new RepositoryActionError("invalid-request", "The request body is invalid.");
|
|
5288
6030
|
return value;
|
|
5289
6031
|
}
|
|
5290
6032
|
function sessionId$1(url) {
|
|
5291
6033
|
const value = url.searchParams.get("sessionId");
|
|
5292
|
-
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$
|
|
6034
|
+
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$6) throw new RepositoryActionError("invalid-session", "The session is invalid.");
|
|
5293
6035
|
return value;
|
|
5294
6036
|
}
|
|
5295
6037
|
function string$1(input, key) {
|
|
@@ -5353,7 +6095,7 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
|
|
|
5353
6095
|
status: 405,
|
|
5354
6096
|
value: { error: "method not allowed" }
|
|
5355
6097
|
};
|
|
5356
|
-
const input = await readJson$
|
|
6098
|
+
const input = await readJson$5(io);
|
|
5357
6099
|
return {
|
|
5358
6100
|
status: 200,
|
|
5359
6101
|
value: { message: await service.generateMessage(cwd, string$1(input, "fingerprint")) }
|
|
@@ -5366,7 +6108,7 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
|
|
|
5366
6108
|
};
|
|
5367
6109
|
return {
|
|
5368
6110
|
status: 200,
|
|
5369
|
-
value: await service.execute(cwd, actionRequest(await readJson$
|
|
6111
|
+
value: await service.execute(cwd, actionRequest(await readJson$5(io)))
|
|
5370
6112
|
};
|
|
5371
6113
|
}
|
|
5372
6114
|
return {
|
|
@@ -5517,7 +6259,7 @@ var EditorOpenService = class {
|
|
|
5517
6259
|
};
|
|
5518
6260
|
//#endregion
|
|
5519
6261
|
//#region src/editor-open-routes.ts
|
|
5520
|
-
const MAX_SESSION_ID_CHARS$
|
|
6262
|
+
const MAX_SESSION_ID_CHARS$5 = 1024;
|
|
5521
6263
|
/** Open the session's working directory in a desktop editor. Query-only: the
|
|
5522
6264
|
* request carries two enum-ish values, so there is no body to parse. */
|
|
5523
6265
|
function registerEditorOpenRoute(ctx, service, cwdForSession) {
|
|
@@ -5531,7 +6273,7 @@ function registerEditorOpenRoute(ctx, service, cwdForSession) {
|
|
|
5531
6273
|
const params = io.url.searchParams;
|
|
5532
6274
|
const id = params.get("sessionId");
|
|
5533
6275
|
const editor = params.get("editor");
|
|
5534
|
-
if (id === null || id.length === 0 || id.length > MAX_SESSION_ID_CHARS$
|
|
6276
|
+
if (id === null || id.length === 0 || id.length > MAX_SESSION_ID_CHARS$5) return {
|
|
5535
6277
|
status: 400,
|
|
5536
6278
|
value: {
|
|
5537
6279
|
error: "invalid-session",
|
|
@@ -5579,6 +6321,358 @@ function registerEditorOpenRoute(ctx, service, cwdForSession) {
|
|
|
5579
6321
|
});
|
|
5580
6322
|
}
|
|
5581
6323
|
//#endregion
|
|
6324
|
+
//#region src/prompts.ts
|
|
6325
|
+
const MAX_PROMPT_FILES = 256;
|
|
6326
|
+
const MAX_PROMPT_BYTES = 16384;
|
|
6327
|
+
const MAX_DESCRIPTION_CHARS = 120;
|
|
6328
|
+
const MAX_REQUEST_BYTES$1 = 32768;
|
|
6329
|
+
/**
|
|
6330
|
+
* The same shape as the output-style name in `global-settings.ts`, plus
|
|
6331
|
+
* `\p{M}` so a macOS-decomposed name survives. Two properties matter and both
|
|
6332
|
+
* are load-bearing: no member of the class is a path separator, and the first
|
|
6333
|
+
* character can be neither a dot nor a separator — so `join(dir, name + '.md')`
|
|
6334
|
+
* cannot address anything outside `dir`. Nothing else validates the path.
|
|
6335
|
+
*/
|
|
6336
|
+
const PROMPT_NAME = /^[\p{L}\p{N}][\p{L}\p{M}\p{N} ._()\[\]-]{0,127}$/u;
|
|
6337
|
+
/** Prompt snippets live beside the rest of the user's Claude Code state. */
|
|
6338
|
+
function claudePromptsDir() {
|
|
6339
|
+
return join(homedir(), ".claude", "prompts");
|
|
6340
|
+
}
|
|
6341
|
+
/** `~/.claude/prompts/x.md` rather than the absolute path it expands to. */
|
|
6342
|
+
function displayPath(file) {
|
|
6343
|
+
const home = homedir();
|
|
6344
|
+
return file.startsWith(`${home}/`) ? `~${file.slice(home.length)}` : file;
|
|
6345
|
+
}
|
|
6346
|
+
/** The menu's second row: the first non-empty line, collapsed and bounded. */
|
|
6347
|
+
function summarize(body) {
|
|
6348
|
+
const line = body.split("\n").map((text) => text.trim()).find((text) => text.length > 0) ?? "";
|
|
6349
|
+
return redactText(line.replace(/\s+/gu, " "), MAX_DESCRIPTION_CHARS);
|
|
6350
|
+
}
|
|
6351
|
+
/**
|
|
6352
|
+
* Every `.md` file in the prompts directory, sorted by name.
|
|
6353
|
+
*
|
|
6354
|
+
* A missing directory is the ordinary state before the user saves anything,
|
|
6355
|
+
* and one unreadable file must not empty the whole menu — both answer with
|
|
6356
|
+
* what is there rather than throwing.
|
|
6357
|
+
*/
|
|
6358
|
+
async function readClaudePrompts(directory) {
|
|
6359
|
+
const prompts = [];
|
|
6360
|
+
let entries;
|
|
6361
|
+
try {
|
|
6362
|
+
entries = await opendir(directory);
|
|
6363
|
+
} catch (error) {
|
|
6364
|
+
if (error.code === "ENOENT") return prompts;
|
|
6365
|
+
throw error;
|
|
6366
|
+
}
|
|
6367
|
+
for await (const entry of entries) {
|
|
6368
|
+
if (prompts.length >= MAX_PROMPT_FILES) break;
|
|
6369
|
+
if (!entry.isFile() || extname(entry.name).toLowerCase() !== ".md") continue;
|
|
6370
|
+
const name = entry.name.slice(0, -3);
|
|
6371
|
+
if (!PROMPT_NAME.test(name)) continue;
|
|
6372
|
+
try {
|
|
6373
|
+
const file = join(directory, entry.name);
|
|
6374
|
+
const body = await readFile(file, "utf8");
|
|
6375
|
+
if (body.trim().length === 0 || Buffer.byteLength(body) > MAX_PROMPT_BYTES) continue;
|
|
6376
|
+
prompts.push({
|
|
6377
|
+
name,
|
|
6378
|
+
description: summarize(body),
|
|
6379
|
+
body,
|
|
6380
|
+
location: displayPath(file)
|
|
6381
|
+
});
|
|
6382
|
+
} catch {}
|
|
6383
|
+
}
|
|
6384
|
+
return prompts.sort((left, right) => left.name.localeCompare(right.name));
|
|
6385
|
+
}
|
|
6386
|
+
var ClaudePromptWriteError = class extends Error {
|
|
6387
|
+
code;
|
|
6388
|
+
constructor(code, message) {
|
|
6389
|
+
super(message);
|
|
6390
|
+
this.name = "ClaudePromptWriteError";
|
|
6391
|
+
this.code = code;
|
|
6392
|
+
}
|
|
6393
|
+
};
|
|
6394
|
+
/**
|
|
6395
|
+
* Save one prompt, refusing to clobber an existing file.
|
|
6396
|
+
*
|
|
6397
|
+
* `wx` is the whole collision policy: a name the user already uses is theirs
|
|
6398
|
+
* to resolve, and overwriting a prompt they keep is not something they can
|
|
6399
|
+
* undo from inside DSH.
|
|
6400
|
+
*/
|
|
6401
|
+
async function writeClaudePrompt(directory, name, body) {
|
|
6402
|
+
if (typeof name !== "string" || !PROMPT_NAME.test(name)) throw new ClaudePromptWriteError("invalid-name", "The prompt name is invalid.");
|
|
6403
|
+
if (typeof body !== "string" || body.trim().length === 0 || Buffer.byteLength(body) > MAX_PROMPT_BYTES) throw new ClaudePromptWriteError("invalid-body", "The prompt text is empty or too large.");
|
|
6404
|
+
const text = body.endsWith("\n") ? body : `${body}\n`;
|
|
6405
|
+
const file = join(directory, `${name}.md`);
|
|
6406
|
+
await mkdir(directory, {
|
|
6407
|
+
recursive: true,
|
|
6408
|
+
mode: 448
|
|
6409
|
+
});
|
|
6410
|
+
try {
|
|
6411
|
+
await writeFile(file, text, {
|
|
6412
|
+
encoding: "utf8",
|
|
6413
|
+
mode: 384,
|
|
6414
|
+
flag: "wx"
|
|
6415
|
+
});
|
|
6416
|
+
} catch (error) {
|
|
6417
|
+
if (error.code === "EEXIST") throw new ClaudePromptWriteError("name-taken", "A prompt with that name already exists.");
|
|
6418
|
+
throw error;
|
|
6419
|
+
}
|
|
6420
|
+
return {
|
|
6421
|
+
name,
|
|
6422
|
+
description: summarize(text),
|
|
6423
|
+
body: text,
|
|
6424
|
+
location: displayPath(file)
|
|
6425
|
+
};
|
|
6426
|
+
}
|
|
6427
|
+
const MAX_NAME_DRAFT_CHARS = 4e3;
|
|
6428
|
+
const MAX_NAME_OUTPUT_BYTES = 4096;
|
|
6429
|
+
const MAX_SUGGESTED_NAME_CHARS = 48;
|
|
6430
|
+
const MAX_REFINE_DRAFT_CHARS = 16e3;
|
|
6431
|
+
const MAX_REFINED_BYTES = 32768;
|
|
6432
|
+
const ASSIST_TIMEOUT_MS = 4e4;
|
|
6433
|
+
/**
|
|
6434
|
+
* Ask for a file name and nothing else, with the draft fenced as data.
|
|
6435
|
+
*
|
|
6436
|
+
* Two things here were measured rather than guessed. The instruction travels
|
|
6437
|
+
* in the user turn, not in `--system-prompt`: a saved prompt is itself an
|
|
6438
|
+
* instruction and a system prompt does not outrank it, so the model reads the
|
|
6439
|
+
* draft as its task and answers it instead of naming it. And the default
|
|
6440
|
+
* system prompt is left in place even though it is large, because the CLI
|
|
6441
|
+
* sends it as a cache read (~6.5k cached tokens); replacing it with a short
|
|
6442
|
+
* one costs a fresh 3.5k-token prefill and measured slower end to end.
|
|
6443
|
+
*
|
|
6444
|
+
* The naming spec is this specific because a vaguer one ("describe what the
|
|
6445
|
+
* template does, at most 40 characters") produced names that were both
|
|
6446
|
+
* inconsistent in style and stripped of the detail that tells two similar
|
|
6447
|
+
* templates apart.
|
|
6448
|
+
*/
|
|
6449
|
+
function promptNamePrompt(draft) {
|
|
6450
|
+
return [
|
|
6451
|
+
"Below is a reusable prompt template a user is saving to a file. Name it.",
|
|
6452
|
+
"",
|
|
6453
|
+
`"""\n${draft.trim().slice(0, MAX_NAME_DRAFT_CHARS).replaceAll("\"\"\"", "\" \" \"")}\n"""`,
|
|
6454
|
+
"",
|
|
6455
|
+
"Answer with the file name alone: no quotes, no explanation, no extension, no leading dot, no slashes.",
|
|
6456
|
+
"Write it in English however the template is written, as lower-case words joined by hyphens.",
|
|
6457
|
+
"Name the task the template performs, keeping whatever detail distinguishes it from a",
|
|
6458
|
+
"similar template. At most 6 words."
|
|
6459
|
+
].join("\n");
|
|
6460
|
+
}
|
|
6461
|
+
/**
|
|
6462
|
+
* Rewrite the draft into something an agent can act on, with the draft fenced
|
|
6463
|
+
* as data for the same reason the naming prompt fences it.
|
|
6464
|
+
*
|
|
6465
|
+
* The rule about people and places is not politeness. Asked to rewrite
|
|
6466
|
+
* "后端 assign 给我", the model reached into Claude Code's ambient system
|
|
6467
|
+
* prompt and substituted the operator's actual email address — inventing a
|
|
6468
|
+
* detail the original never carried, and putting a personal address into a
|
|
6469
|
+
* message the user had not written it into. Naming the failure explicitly is
|
|
6470
|
+
* what stopped it; stripping the ambient context with `--system-prompt` also
|
|
6471
|
+
* stopped it but cost a fresh prefill and produced looser rewrites.
|
|
6472
|
+
*/
|
|
6473
|
+
function promptRefinePrompt(draft) {
|
|
6474
|
+
return [
|
|
6475
|
+
"Below is a prompt a user is about to send to a coding agent. Rewrite it so the agent can act on it without coming back with questions.",
|
|
6476
|
+
"",
|
|
6477
|
+
`"""\n${draft.trim().slice(0, MAX_REFINE_DRAFT_CHARS).replaceAll("\"\"\"", "\" \" \"")}\n"""`,
|
|
6478
|
+
"",
|
|
6479
|
+
"Keep the intent and every concrete detail exactly as given — names, paths, branches, identifiers, numbers.",
|
|
6480
|
+
"Leave every reference to a person or place as the original wrote it (\"me\", \"我\", \"the usual branch\");",
|
|
6481
|
+
"you do not know who or what they resolve to, and guessing puts a wrong name in the user's message.",
|
|
6482
|
+
"Do not invent requirements the original does not imply, do not answer the prompt, do not add a preamble or sign-off.",
|
|
6483
|
+
"Write the rewrite in the same language the original is written in.",
|
|
6484
|
+
"Reply with the rewritten prompt alone."
|
|
6485
|
+
].join("\n");
|
|
6486
|
+
}
|
|
6487
|
+
/** One turn, no tools, no MCP: neither of these tasks needs them, and both
|
|
6488
|
+
* cost seconds of cold start. Variadic `--tools` stays last. */
|
|
6489
|
+
function promptAssistArguments() {
|
|
6490
|
+
return [
|
|
6491
|
+
"-p",
|
|
6492
|
+
"--output-format",
|
|
6493
|
+
"text",
|
|
6494
|
+
"--model",
|
|
6495
|
+
"haiku",
|
|
6496
|
+
"--strict-mcp-config",
|
|
6497
|
+
"--mcp-config",
|
|
6498
|
+
"{\"mcpServers\":{}}",
|
|
6499
|
+
"--tools",
|
|
6500
|
+
""
|
|
6501
|
+
];
|
|
6502
|
+
}
|
|
6503
|
+
/** The rewrite in a model reply, or undefined when the reply is unusable.
|
|
6504
|
+
*
|
|
6505
|
+
* A model told to answer with the text alone still sometimes wraps it in a
|
|
6506
|
+
* markdown fence, so one is peeled off; anything else is the user's to read
|
|
6507
|
+
* and edit, and is passed through untouched. */
|
|
6508
|
+
function refinedPrompt(output) {
|
|
6509
|
+
const trimmed = output.trim();
|
|
6510
|
+
const text = (/^```[^\n]*\n([\s\S]*?)\n?```$/u.exec(trimmed)?.[1] ?? trimmed).trim();
|
|
6511
|
+
return text.length === 0 || Buffer.byteLength(text) > MAX_REFINED_BYTES ? void 0 : text;
|
|
6512
|
+
}
|
|
6513
|
+
/**
|
|
6514
|
+
* The usable name in a model reply, or undefined when there is none.
|
|
6515
|
+
*
|
|
6516
|
+
* A one-line answer is what the prompt asks for and usually what comes back,
|
|
6517
|
+
* but "usually" is not a contract: the reply is scrubbed to what a file name
|
|
6518
|
+
* may hold and then held to the same {@link PROMPT_NAME} guard the write path
|
|
6519
|
+
* uses, so a chatty or malformed answer is dropped rather than offered.
|
|
6520
|
+
*/
|
|
6521
|
+
function suggestedName(output) {
|
|
6522
|
+
const scrubbed = wordBounded((output.split("\n").map((text) => text.trim()).find((text) => text.length > 0) ?? "").replace(/^["'`]+|["'`]+$/gu, "").replace(/\.md$/iu, "").replace(/[^\p{L}\p{M}\p{N} ._()\[\]-]/gu, " ").replace(/\s+/gu, " ").trim());
|
|
6523
|
+
return PROMPT_NAME.test(scrubbed) ? scrubbed : void 0;
|
|
6524
|
+
}
|
|
6525
|
+
/** Cut a long name at its last word boundary. A name the user has to repair
|
|
6526
|
+
* ("analyze-frontend-backend-create-jira-tic") is worse than a shorter one. */
|
|
6527
|
+
function wordBounded(name) {
|
|
6528
|
+
if (name.length <= MAX_SUGGESTED_NAME_CHARS) return name;
|
|
6529
|
+
const cut = name.slice(0, MAX_SUGGESTED_NAME_CHARS);
|
|
6530
|
+
const boundary = cut.search(/[\s\-_][^\s\-_]*$/u);
|
|
6531
|
+
return (boundary > 0 ? cut.slice(0, boundary) : cut).trim();
|
|
6532
|
+
}
|
|
6533
|
+
/** Runs Claude Code's cheapest model over the draft: names it, or rewrites it. */
|
|
6534
|
+
var PromptAssistService = class {
|
|
6535
|
+
#runtime;
|
|
6536
|
+
#executablePath;
|
|
6537
|
+
constructor(runtime, executablePath) {
|
|
6538
|
+
this.#runtime = runtime;
|
|
6539
|
+
this.#executablePath = executablePath;
|
|
6540
|
+
}
|
|
6541
|
+
/** One bounded, tool-less turn; undefined whenever it cannot be had. */
|
|
6542
|
+
async #ask(prompt, maxOutputBytes, signal) {
|
|
6543
|
+
const executablePath = this.#executablePath();
|
|
6544
|
+
if (executablePath.length === 0) return void 0;
|
|
6545
|
+
const timeout = AbortSignal.timeout(ASSIST_TIMEOUT_MS);
|
|
6546
|
+
try {
|
|
6547
|
+
const handle = this.#runtime.spawn({
|
|
6548
|
+
argv: [executablePath, ...promptAssistArguments()],
|
|
6549
|
+
cwd: homedir(),
|
|
6550
|
+
stdio: {
|
|
6551
|
+
stdin: { data: prompt },
|
|
6552
|
+
stdout: { maxBytes: maxOutputBytes },
|
|
6553
|
+
stderr: { maxBytes: MAX_NAME_OUTPUT_BYTES }
|
|
6554
|
+
},
|
|
6555
|
+
graceMs: 1e3,
|
|
6556
|
+
signal: signal === void 0 ? timeout : AbortSignal.any([signal, timeout]),
|
|
6557
|
+
env: { MAX_THINKING_TOKENS: "0" }
|
|
6558
|
+
});
|
|
6559
|
+
if ((await handle.done).exitCode !== 0) return void 0;
|
|
6560
|
+
return handle.collected.stdout?.readFrom(0).text ?? "";
|
|
6561
|
+
} catch {
|
|
6562
|
+
return;
|
|
6563
|
+
}
|
|
6564
|
+
}
|
|
6565
|
+
/** The suggested file name, or undefined whenever one cannot be had. The
|
|
6566
|
+
* caller already has a name derived locally, so every failure here is a
|
|
6567
|
+
* non-event: nothing is reported, and nothing is retried. */
|
|
6568
|
+
async suggest(draft, signal) {
|
|
6569
|
+
if (draft.trim().length === 0) return void 0;
|
|
6570
|
+
const output = await this.#ask(promptNamePrompt(draft), MAX_NAME_OUTPUT_BYTES, signal);
|
|
6571
|
+
return output === void 0 ? void 0 : suggestedName(output);
|
|
6572
|
+
}
|
|
6573
|
+
/** The rewritten draft, or undefined when the rewrite could not be had.
|
|
6574
|
+
* Unlike naming, this failure is worth reporting: the user asked for it and
|
|
6575
|
+
* is waiting on it. */
|
|
6576
|
+
async refine(draft, signal) {
|
|
6577
|
+
if (draft.trim().length === 0) return void 0;
|
|
6578
|
+
const output = await this.#ask(promptRefinePrompt(draft), MAX_REFINED_BYTES, signal);
|
|
6579
|
+
return output === void 0 ? void 0 : refinedPrompt(output);
|
|
6580
|
+
}
|
|
6581
|
+
};
|
|
6582
|
+
/** List the user's prompt snippets, and save the composer draft as a new one. */
|
|
6583
|
+
function registerClaudePromptsRoute(ctx, directory = claudePromptsDir()) {
|
|
6584
|
+
registerPluginRoute(ctx, {
|
|
6585
|
+
mode: "unary",
|
|
6586
|
+
kind: "exact",
|
|
6587
|
+
path: CLAUDE_PROMPTS_PATH,
|
|
6588
|
+
methods: ["GET", "POST"],
|
|
6589
|
+
budget: "fast",
|
|
6590
|
+
handler: async (io) => {
|
|
6591
|
+
if (io.method === "GET") return {
|
|
6592
|
+
status: 200,
|
|
6593
|
+
value: { prompts: await readClaudePrompts(directory) }
|
|
6594
|
+
};
|
|
6595
|
+
const payload = await io.body(MAX_REQUEST_BYTES$1);
|
|
6596
|
+
try {
|
|
6597
|
+
return {
|
|
6598
|
+
status: 200,
|
|
6599
|
+
value: {
|
|
6600
|
+
saved: true,
|
|
6601
|
+
prompt: await writeClaudePrompt(directory, payload.name, payload.body)
|
|
6602
|
+
}
|
|
6603
|
+
};
|
|
6604
|
+
} catch (error) {
|
|
6605
|
+
if (error instanceof ClaudePromptWriteError) return {
|
|
6606
|
+
status: error.code === "name-taken" ? 409 : 400,
|
|
6607
|
+
value: {
|
|
6608
|
+
error: error.code,
|
|
6609
|
+
message: error.message
|
|
6610
|
+
}
|
|
6611
|
+
};
|
|
6612
|
+
return {
|
|
6613
|
+
status: 500,
|
|
6614
|
+
value: {
|
|
6615
|
+
error: "prompt-write-failed",
|
|
6616
|
+
message: "The prompt could not be saved."
|
|
6617
|
+
}
|
|
6618
|
+
};
|
|
6619
|
+
}
|
|
6620
|
+
}
|
|
6621
|
+
});
|
|
6622
|
+
}
|
|
6623
|
+
/** Suggest a file name for a draft. Answers `{}` when no name could be had:
|
|
6624
|
+
* the caller keeps the name it derived locally, so this is never an error. */
|
|
6625
|
+
function registerClaudePromptNameRoute(ctx, service) {
|
|
6626
|
+
registerPluginRoute(ctx, {
|
|
6627
|
+
mode: "unary",
|
|
6628
|
+
kind: "exact",
|
|
6629
|
+
path: CLAUDE_PROMPT_NAME_PATH,
|
|
6630
|
+
methods: ["POST"],
|
|
6631
|
+
budget: "git",
|
|
6632
|
+
handler: async (io) => {
|
|
6633
|
+
const payload = await io.body(MAX_REQUEST_BYTES$1);
|
|
6634
|
+
const draft = typeof payload.draft === "string" ? payload.draft : "";
|
|
6635
|
+
const name = await service.suggest(draft, io.signal);
|
|
6636
|
+
return {
|
|
6637
|
+
status: 200,
|
|
6638
|
+
value: name === void 0 ? {} : { name }
|
|
6639
|
+
};
|
|
6640
|
+
}
|
|
6641
|
+
});
|
|
6642
|
+
}
|
|
6643
|
+
/** Rewrite a draft into something an agent can act on. */
|
|
6644
|
+
function registerClaudePromptRefineRoute(ctx, service) {
|
|
6645
|
+
registerPluginRoute(ctx, {
|
|
6646
|
+
mode: "unary",
|
|
6647
|
+
kind: "exact",
|
|
6648
|
+
path: CLAUDE_PROMPT_REFINE_PATH,
|
|
6649
|
+
methods: ["POST"],
|
|
6650
|
+
budget: "git",
|
|
6651
|
+
handler: async (io) => {
|
|
6652
|
+
const payload = await io.body(MAX_REQUEST_BYTES$1);
|
|
6653
|
+
const draft = typeof payload.draft === "string" ? payload.draft : "";
|
|
6654
|
+
if (draft.trim().length === 0) return {
|
|
6655
|
+
status: 400,
|
|
6656
|
+
value: {
|
|
6657
|
+
error: "empty-draft",
|
|
6658
|
+
message: "There is nothing to rewrite."
|
|
6659
|
+
}
|
|
6660
|
+
};
|
|
6661
|
+
const text = await service.refine(draft, io.signal);
|
|
6662
|
+
return text === void 0 ? {
|
|
6663
|
+
status: 503,
|
|
6664
|
+
value: {
|
|
6665
|
+
error: "refine-unavailable",
|
|
6666
|
+
message: "Claude could not rewrite the prompt."
|
|
6667
|
+
}
|
|
6668
|
+
} : {
|
|
6669
|
+
status: 200,
|
|
6670
|
+
value: { text }
|
|
6671
|
+
};
|
|
6672
|
+
}
|
|
6673
|
+
});
|
|
6674
|
+
}
|
|
6675
|
+
//#endregion
|
|
5582
6676
|
//#region src/github-url.ts
|
|
5583
6677
|
/** Only GitHub's own image hosts; the browser loads these directly, so a URL
|
|
5584
6678
|
* the API did not vouch for must never become an outbound request. */
|
|
@@ -5646,7 +6740,7 @@ async function collect(handle) {
|
|
|
5646
6740
|
lossy: stdout?.lossy === true
|
|
5647
6741
|
};
|
|
5648
6742
|
}
|
|
5649
|
-
function record$
|
|
6743
|
+
function record$8(value) {
|
|
5650
6744
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
5651
6745
|
}
|
|
5652
6746
|
/** Read `repository.pullRequest.reviewThreads.nodes` out of a GraphQL response.
|
|
@@ -5654,12 +6748,12 @@ function record$7(value) {
|
|
|
5654
6748
|
* throwing: the caller distinguishes "no threads" from "call failed" by the
|
|
5655
6749
|
* process exit code. */
|
|
5656
6750
|
function parseReviewThreads(value) {
|
|
5657
|
-
const nodes = record$
|
|
6751
|
+
const nodes = record$8(record$8(record$8(record$8(record$8(value)?.data)?.repository)?.pullRequest)?.reviewThreads)?.nodes;
|
|
5658
6752
|
if (!Array.isArray(nodes)) return [];
|
|
5659
6753
|
const threads = [];
|
|
5660
6754
|
let total = 0;
|
|
5661
6755
|
for (const item of nodes) {
|
|
5662
|
-
const input = record$
|
|
6756
|
+
const input = record$8(item);
|
|
5663
6757
|
if (input === void 0 || typeof input.id !== "string" || input.id.length === 0) continue;
|
|
5664
6758
|
const path = typeof input.path === "string" ? input.path : "";
|
|
5665
6759
|
if (path.length === 0) continue;
|
|
@@ -5671,14 +6765,14 @@ function parseReviewThreads(value) {
|
|
|
5671
6765
|
side
|
|
5672
6766
|
};
|
|
5673
6767
|
const comments = [];
|
|
5674
|
-
const commentNodes = record$
|
|
6768
|
+
const commentNodes = record$8(input.comments)?.nodes;
|
|
5675
6769
|
for (const node of Array.isArray(commentNodes) ? commentNodes : []) {
|
|
5676
6770
|
if (total >= MAX_COMMENTS) break;
|
|
5677
|
-
const comment = record$
|
|
6771
|
+
const comment = record$8(node);
|
|
5678
6772
|
if (comment === void 0 || !Number.isSafeInteger(comment.databaseId)) continue;
|
|
5679
6773
|
const body = typeof comment.body === "string" ? comment.body.trim() : "";
|
|
5680
6774
|
if (body.length === 0) continue;
|
|
5681
|
-
const author = record$
|
|
6775
|
+
const author = record$8(comment.author);
|
|
5682
6776
|
const avatarUrl = githubAvatarUrl(author?.avatarUrl);
|
|
5683
6777
|
const login = typeof author?.login === "string" ? author.login : "unknown";
|
|
5684
6778
|
comments.push({
|
|
@@ -5707,11 +6801,11 @@ function parseReviewThreads(value) {
|
|
|
5707
6801
|
}
|
|
5708
6802
|
/** One posted reply, shaped like the thread comments it joins. */
|
|
5709
6803
|
function parseReplyComment(value, anchor) {
|
|
5710
|
-
const input = record$
|
|
6804
|
+
const input = record$8(value);
|
|
5711
6805
|
if (input === void 0 || !Number.isSafeInteger(input.id)) return void 0;
|
|
5712
6806
|
const body = typeof input.body === "string" ? input.body.trim() : "";
|
|
5713
6807
|
if (body.length === 0) return void 0;
|
|
5714
|
-
const user = record$
|
|
6808
|
+
const user = record$8(input.user);
|
|
5715
6809
|
const avatarUrl = githubAvatarUrl(user?.avatar_url);
|
|
5716
6810
|
const login = typeof user?.login === "string" ? user.login : "unknown";
|
|
5717
6811
|
return {
|
|
@@ -5726,11 +6820,11 @@ function parseReplyComment(value, anchor) {
|
|
|
5726
6820
|
};
|
|
5727
6821
|
}
|
|
5728
6822
|
function parseMentionableUsers(value) {
|
|
5729
|
-
const nodes = record$
|
|
6823
|
+
const nodes = record$8(record$8(record$8(record$8(value)?.data)?.repository)?.mentionableUsers)?.nodes;
|
|
5730
6824
|
if (!Array.isArray(nodes)) return [];
|
|
5731
6825
|
const users = [];
|
|
5732
6826
|
for (const item of nodes) {
|
|
5733
|
-
const input = record$
|
|
6827
|
+
const input = record$8(item);
|
|
5734
6828
|
if (input === void 0 || typeof input.login !== "string" || input.login.length === 0) continue;
|
|
5735
6829
|
const avatarUrl = githubAvatarUrl(input.avatarUrl);
|
|
5736
6830
|
users.push({
|
|
@@ -5750,7 +6844,7 @@ function parseFailingChecks(value) {
|
|
|
5750
6844
|
if (!Array.isArray(value)) return [];
|
|
5751
6845
|
const failing = [];
|
|
5752
6846
|
for (const item of value) {
|
|
5753
|
-
const input = record$
|
|
6847
|
+
const input = record$8(item);
|
|
5754
6848
|
if (input === void 0 || input.bucket !== "fail" || typeof input.name !== "string") continue;
|
|
5755
6849
|
failing.push({
|
|
5756
6850
|
name: input.name,
|
|
@@ -5813,12 +6907,12 @@ var PullRequestFeedbackService = class {
|
|
|
5813
6907
|
let posted;
|
|
5814
6908
|
try {
|
|
5815
6909
|
const parsed = JSON.parse(result.stdout);
|
|
5816
|
-
const path = typeof record$
|
|
5817
|
-
const line = Number.isSafeInteger(record$
|
|
6910
|
+
const path = typeof record$8(parsed)?.path === "string" ? String(record$8(parsed)?.path) : "";
|
|
6911
|
+
const line = Number.isSafeInteger(record$8(parsed)?.line) ? Number(record$8(parsed)?.line) : void 0;
|
|
5818
6912
|
posted = parseReplyComment(parsed, {
|
|
5819
6913
|
path,
|
|
5820
6914
|
...line === void 0 ? {} : { line },
|
|
5821
|
-
side: record$
|
|
6915
|
+
side: record$8(parsed)?.side === "LEFT" ? "old" : "new"
|
|
5822
6916
|
});
|
|
5823
6917
|
} catch {
|
|
5824
6918
|
posted = void 0;
|
|
@@ -5839,7 +6933,7 @@ var PullRequestFeedbackService = class {
|
|
|
5839
6933
|
], cwd, GH_TIMEOUT_MS);
|
|
5840
6934
|
if (result.exitCode !== 0) throw new PullRequestFeedbackError("resolve-failed", "The thread could not be updated.");
|
|
5841
6935
|
try {
|
|
5842
|
-
const thread = record$
|
|
6936
|
+
const thread = record$8(record$8(record$8(record$8(JSON.parse(result.stdout))?.data)?.[resolved ? "resolveReviewThread" : "unresolveReviewThread"])?.thread);
|
|
5843
6937
|
if (typeof thread?.isResolved !== "boolean") throw new Error("missing state");
|
|
5844
6938
|
return thread.isResolved;
|
|
5845
6939
|
} catch {
|
|
@@ -5962,23 +7056,23 @@ var PullRequestFeedbackService = class {
|
|
|
5962
7056
|
};
|
|
5963
7057
|
//#endregion
|
|
5964
7058
|
//#region src/pr-feedback-routes.ts
|
|
5965
|
-
const MAX_SESSION_ID_CHARS$
|
|
5966
|
-
const MAX_BODY_BYTES$
|
|
7059
|
+
const MAX_SESSION_ID_CHARS$4 = 1024;
|
|
7060
|
+
const MAX_BODY_BYTES$5 = 16384;
|
|
5967
7061
|
const MAX_REPLY_CHARS = 2e3;
|
|
5968
7062
|
const MAX_THREAD_ID_CHARS = 512;
|
|
5969
7063
|
const MAX_MENTION_QUERY_CHARS = 64;
|
|
5970
|
-
function record$
|
|
7064
|
+
function record$7(value) {
|
|
5971
7065
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
5972
7066
|
}
|
|
5973
|
-
async function readJson$
|
|
7067
|
+
async function readJson$4(io) {
|
|
5974
7068
|
let parsed;
|
|
5975
7069
|
try {
|
|
5976
|
-
parsed = await io.body(MAX_BODY_BYTES$
|
|
7070
|
+
parsed = await io.body(MAX_BODY_BYTES$5);
|
|
5977
7071
|
} catch (error) {
|
|
5978
7072
|
if (error instanceof SyntaxError) throw error;
|
|
5979
7073
|
throw new PullRequestFeedbackError("body-too-large", "The request body is too large.");
|
|
5980
7074
|
}
|
|
5981
|
-
const value = record$
|
|
7075
|
+
const value = record$7(parsed);
|
|
5982
7076
|
if (value === void 0) throw new PullRequestFeedbackError("invalid-request", "The request body is invalid.");
|
|
5983
7077
|
return value;
|
|
5984
7078
|
}
|
|
@@ -5999,7 +7093,7 @@ function threadId(input) {
|
|
|
5999
7093
|
}
|
|
6000
7094
|
function sessionId(url) {
|
|
6001
7095
|
const value = url.searchParams.get("sessionId");
|
|
6002
|
-
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$
|
|
7096
|
+
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$4) throw new PullRequestFeedbackError("invalid-session", "The session is invalid.");
|
|
6003
7097
|
return value;
|
|
6004
7098
|
}
|
|
6005
7099
|
function pullNumber(url) {
|
|
@@ -6058,7 +7152,7 @@ function registerPullRequestFeedbackRoute(ctx, service, cwdForSession) {
|
|
|
6058
7152
|
status: 405,
|
|
6059
7153
|
value: { error: "method not allowed" }
|
|
6060
7154
|
};
|
|
6061
|
-
const input = await readJson$
|
|
7155
|
+
const input = await readJson$4(io);
|
|
6062
7156
|
return {
|
|
6063
7157
|
status: 200,
|
|
6064
7158
|
value: { comment: await service.reply(cwd, pullNumber(url), commentId(input), replyBody(input)) }
|
|
@@ -6069,7 +7163,7 @@ function registerPullRequestFeedbackRoute(ctx, service, cwdForSession) {
|
|
|
6069
7163
|
status: 405,
|
|
6070
7164
|
value: { error: "method not allowed" }
|
|
6071
7165
|
};
|
|
6072
|
-
const input = await readJson$
|
|
7166
|
+
const input = await readJson$4(io);
|
|
6073
7167
|
if (typeof input.resolved !== "boolean") throw new PullRequestFeedbackError("invalid-request", "The resolved field is required.");
|
|
6074
7168
|
return {
|
|
6075
7169
|
status: 200,
|
|
@@ -6215,7 +7309,7 @@ var JiraError = class extends Error {
|
|
|
6215
7309
|
this.code = code;
|
|
6216
7310
|
}
|
|
6217
7311
|
};
|
|
6218
|
-
function record$
|
|
7312
|
+
function record$6(value) {
|
|
6219
7313
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
6220
7314
|
}
|
|
6221
7315
|
/** Jira Cloud sites are origins; Data Center may carry a context path. */
|
|
@@ -6239,14 +7333,14 @@ function ticketKeyOf(query) {
|
|
|
6239
7333
|
* search and does match keys; its hits become the key filter the normal
|
|
6240
7334
|
* search then reads the display fields from. */
|
|
6241
7335
|
function pickerKeys(value, number) {
|
|
6242
|
-
const sections = record$
|
|
7336
|
+
const sections = record$6(value)?.sections;
|
|
6243
7337
|
if (!Array.isArray(sections)) return [];
|
|
6244
7338
|
const keys = [];
|
|
6245
7339
|
for (const section of sections) {
|
|
6246
|
-
const issues = record$
|
|
7340
|
+
const issues = record$6(section)?.issues;
|
|
6247
7341
|
if (!Array.isArray(issues)) continue;
|
|
6248
7342
|
for (const issue of issues) {
|
|
6249
|
-
const raw = record$
|
|
7343
|
+
const raw = record$6(issue)?.key;
|
|
6250
7344
|
const key = ticketKeyOf(typeof raw === "string" ? raw : "");
|
|
6251
7345
|
if (key !== void 0 && key.endsWith(`-${number}`) && !keys.includes(key)) keys.push(key);
|
|
6252
7346
|
}
|
|
@@ -6265,15 +7359,15 @@ function buildJql(query) {
|
|
|
6265
7359
|
return `text ~ "${trimmed.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}*" ORDER BY updated DESC`;
|
|
6266
7360
|
}
|
|
6267
7361
|
function parseTickets(value, siteUrl) {
|
|
6268
|
-
const issues = record$
|
|
7362
|
+
const issues = record$6(value)?.issues;
|
|
6269
7363
|
if (!Array.isArray(issues)) return [];
|
|
6270
7364
|
const tickets = [];
|
|
6271
7365
|
for (const item of issues) {
|
|
6272
|
-
const issue = record$
|
|
6273
|
-
const fields = record$
|
|
7366
|
+
const issue = record$6(item);
|
|
7367
|
+
const fields = record$6(issue?.fields);
|
|
6274
7368
|
if (issue === void 0 || typeof issue.key !== "string" || fields === void 0) continue;
|
|
6275
|
-
const status = record$
|
|
6276
|
-
const type = record$
|
|
7369
|
+
const status = record$6(fields.status)?.name;
|
|
7370
|
+
const type = record$6(fields.issuetype)?.name;
|
|
6277
7371
|
tickets.push({
|
|
6278
7372
|
key: issue.key,
|
|
6279
7373
|
summary: typeof fields.summary === "string" ? fields.summary.slice(0, 256) : "",
|
|
@@ -6315,7 +7409,7 @@ var JiraService = class {
|
|
|
6315
7409
|
email,
|
|
6316
7410
|
apiToken
|
|
6317
7411
|
};
|
|
6318
|
-
const myself = record$
|
|
7412
|
+
const myself = record$6(await this.#json(connection, "/rest/api/3/myself"));
|
|
6319
7413
|
const displayName = typeof myself?.displayName === "string" ? myself.displayName : void 0;
|
|
6320
7414
|
const accountId = typeof myself?.accountId === "string" ? myself.accountId : void 0;
|
|
6321
7415
|
const store = {
|
|
@@ -6334,7 +7428,7 @@ var JiraService = class {
|
|
|
6334
7428
|
if (ticket === void 0) throw new JiraError("invalid-request", "The ticket key is invalid.");
|
|
6335
7429
|
let accountId = store.accountId;
|
|
6336
7430
|
if (accountId === void 0) {
|
|
6337
|
-
const myself = record$
|
|
7431
|
+
const myself = record$6(await this.#json(store, "/rest/api/3/myself"));
|
|
6338
7432
|
if (typeof myself?.accountId !== "string") throw new JiraError("jira-failed", "The Jira account id is unavailable.");
|
|
6339
7433
|
accountId = myself.accountId;
|
|
6340
7434
|
await this.#write({
|
|
@@ -6410,7 +7504,7 @@ var JiraService = class {
|
|
|
6410
7504
|
throw error;
|
|
6411
7505
|
}
|
|
6412
7506
|
if (Buffer.byteLength(text) > MAX_STORE_BYTES) return void 0;
|
|
6413
|
-
const input = record$
|
|
7507
|
+
const input = record$6(JSON.parse(text));
|
|
6414
7508
|
if (input === void 0 || typeof input.siteUrl !== "string" || typeof input.email !== "string" || typeof input.apiToken !== "string") return void 0;
|
|
6415
7509
|
return {
|
|
6416
7510
|
siteUrl: input.siteUrl,
|
|
@@ -6441,21 +7535,21 @@ var JiraService = class {
|
|
|
6441
7535
|
};
|
|
6442
7536
|
//#endregion
|
|
6443
7537
|
//#region src/jira-routes.ts
|
|
6444
|
-
const MAX_BODY_BYTES$
|
|
6445
|
-
function record$
|
|
7538
|
+
const MAX_BODY_BYTES$4 = 8192;
|
|
7539
|
+
function record$5(value) {
|
|
6446
7540
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
6447
7541
|
}
|
|
6448
7542
|
/** The wrapper enforces the byte cap; its plain rejection is translated back
|
|
6449
7543
|
* into the JiraError shape the panel already knows how to render. */
|
|
6450
|
-
async function readJson$
|
|
7544
|
+
async function readJson$3(io) {
|
|
6451
7545
|
let body;
|
|
6452
7546
|
try {
|
|
6453
|
-
body = await io.body(MAX_BODY_BYTES$
|
|
7547
|
+
body = await io.body(MAX_BODY_BYTES$4);
|
|
6454
7548
|
} catch (error) {
|
|
6455
7549
|
if (error instanceof SyntaxError) throw error;
|
|
6456
7550
|
throw new JiraError("body-too-large", "The request body is too large.");
|
|
6457
7551
|
}
|
|
6458
|
-
const value = record$
|
|
7552
|
+
const value = record$5(body);
|
|
6459
7553
|
if (value === void 0) throw new JiraError("invalid-request", "The request body is invalid.");
|
|
6460
7554
|
return value;
|
|
6461
7555
|
}
|
|
@@ -6489,7 +7583,7 @@ function registerJiraRoute(ctx, service) {
|
|
|
6489
7583
|
status: 405,
|
|
6490
7584
|
value: { error: "method not allowed" }
|
|
6491
7585
|
};
|
|
6492
|
-
const input = await readJson$
|
|
7586
|
+
const input = await readJson$3(io);
|
|
6493
7587
|
return {
|
|
6494
7588
|
status: 200,
|
|
6495
7589
|
value: await service.connect({
|
|
@@ -6515,7 +7609,7 @@ function registerJiraRoute(ctx, service) {
|
|
|
6515
7609
|
status: 405,
|
|
6516
7610
|
value: { error: "method not allowed" }
|
|
6517
7611
|
};
|
|
6518
|
-
const input = await readJson$
|
|
7612
|
+
const input = await readJson$3(io);
|
|
6519
7613
|
await service.assignToMe(string(input, "key"));
|
|
6520
7614
|
return {
|
|
6521
7615
|
status: 200,
|
|
@@ -6634,12 +7728,12 @@ function askArguments(preferences) {
|
|
|
6634
7728
|
...READ_ONLY_TOOLS
|
|
6635
7729
|
];
|
|
6636
7730
|
}
|
|
6637
|
-
function record$
|
|
7731
|
+
function record$4(value) {
|
|
6638
7732
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
6639
7733
|
}
|
|
6640
7734
|
/** One-line description of a tool call, mirroring the main window's step titles. */
|
|
6641
7735
|
function toolSummary(input) {
|
|
6642
|
-
const fields = record$
|
|
7736
|
+
const fields = record$4(input);
|
|
6643
7737
|
if (fields === void 0) return void 0;
|
|
6644
7738
|
const candidate = [
|
|
6645
7739
|
fields.command,
|
|
@@ -6653,7 +7747,7 @@ function toolSummary(input) {
|
|
|
6653
7747
|
function eventsOfStreamLine(line) {
|
|
6654
7748
|
let parsed;
|
|
6655
7749
|
try {
|
|
6656
|
-
parsed = record$
|
|
7750
|
+
parsed = record$4(JSON.parse(line));
|
|
6657
7751
|
} catch {
|
|
6658
7752
|
return [];
|
|
6659
7753
|
}
|
|
@@ -6663,9 +7757,9 @@ function eventsOfStreamLine(line) {
|
|
|
6663
7757
|
text: "ready"
|
|
6664
7758
|
}];
|
|
6665
7759
|
if (parsed.type === "stream_event") {
|
|
6666
|
-
const event = record$
|
|
7760
|
+
const event = record$4(parsed.event);
|
|
6667
7761
|
if (event?.type === "content_block_start") {
|
|
6668
|
-
const block = record$
|
|
7762
|
+
const block = record$4(event.content_block);
|
|
6669
7763
|
if (block?.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") return [{
|
|
6670
7764
|
type: "tool",
|
|
6671
7765
|
id: block.id,
|
|
@@ -6674,7 +7768,7 @@ function eventsOfStreamLine(line) {
|
|
|
6674
7768
|
}];
|
|
6675
7769
|
return [];
|
|
6676
7770
|
}
|
|
6677
|
-
const delta = record$
|
|
7771
|
+
const delta = record$4(event?.delta);
|
|
6678
7772
|
if (event?.type !== "content_block_delta" || delta === void 0) return [];
|
|
6679
7773
|
if (delta.type === "text_delta" && typeof delta.text === "string") return [{
|
|
6680
7774
|
type: "text",
|
|
@@ -6687,11 +7781,11 @@ function eventsOfStreamLine(line) {
|
|
|
6687
7781
|
return [];
|
|
6688
7782
|
}
|
|
6689
7783
|
if (parsed.type === "assistant" || parsed.type === "user") {
|
|
6690
|
-
const content = record$
|
|
7784
|
+
const content = record$4(parsed.message)?.content;
|
|
6691
7785
|
if (!Array.isArray(content)) return [];
|
|
6692
7786
|
const events = [];
|
|
6693
7787
|
for (const item of content) {
|
|
6694
|
-
const block = record$
|
|
7788
|
+
const block = record$4(item);
|
|
6695
7789
|
if (block?.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
|
|
6696
7790
|
const summary = toolSummary(block.input);
|
|
6697
7791
|
events.push({
|
|
@@ -6783,11 +7877,11 @@ var AskService = class {
|
|
|
6783
7877
|
};
|
|
6784
7878
|
//#endregion
|
|
6785
7879
|
//#region src/ask-routes.ts
|
|
6786
|
-
const MAX_BODY_BYTES$
|
|
6787
|
-
const MAX_SESSION_ID_CHARS$
|
|
7880
|
+
const MAX_BODY_BYTES$3 = 131072;
|
|
7881
|
+
const MAX_SESSION_ID_CHARS$3 = 1024;
|
|
6788
7882
|
/** Two sessions may await an answer at once; a third evicts the oldest. */
|
|
6789
7883
|
const MAX_CONCURRENT_ASKS = 2;
|
|
6790
|
-
function record$
|
|
7884
|
+
function record$3(value) {
|
|
6791
7885
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
6792
7886
|
}
|
|
6793
7887
|
function askRequest(input) {
|
|
@@ -6819,12 +7913,12 @@ function registerAskRoute(ctx, service, cwdForSession, preferencesFor) {
|
|
|
6819
7913
|
let sessionId;
|
|
6820
7914
|
try {
|
|
6821
7915
|
const value = io.url.searchParams.get("sessionId");
|
|
6822
|
-
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$
|
|
7916
|
+
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$3) throw new AskError("invalid-session", "The session is invalid.");
|
|
6823
7917
|
sessionId = value;
|
|
6824
7918
|
const resolved = cwdForSession(sessionId);
|
|
6825
7919
|
if (resolved === void 0) throw new AskError("session-unavailable", "The Claude session is unavailable.");
|
|
6826
7920
|
cwd = resolved;
|
|
6827
|
-
const body = record$
|
|
7921
|
+
const body = record$3(await io.body(MAX_BODY_BYTES$3));
|
|
6828
7922
|
if (body === void 0) throw new AskError("invalid-request", "The request body is invalid.");
|
|
6829
7923
|
request = askRequest(body);
|
|
6830
7924
|
} catch (error) {
|
|
@@ -6864,26 +7958,26 @@ function registerAskRoute(ctx, service, cwdForSession, preferencesFor) {
|
|
|
6864
7958
|
}
|
|
6865
7959
|
//#endregion
|
|
6866
7960
|
//#region src/review-comment-routes.ts
|
|
6867
|
-
const MAX_BODY_BYTES$
|
|
6868
|
-
const MAX_SESSION_ID_CHARS$
|
|
6869
|
-
function record$
|
|
7961
|
+
const MAX_BODY_BYTES$2 = 16384;
|
|
7962
|
+
const MAX_SESSION_ID_CHARS$2 = 1024;
|
|
7963
|
+
function record$2(value) {
|
|
6870
7964
|
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
6871
7965
|
}
|
|
6872
|
-
async function readJson$
|
|
7966
|
+
async function readJson$2(io) {
|
|
6873
7967
|
let parsed;
|
|
6874
7968
|
try {
|
|
6875
|
-
parsed = await io.body(MAX_BODY_BYTES$
|
|
7969
|
+
parsed = await io.body(MAX_BODY_BYTES$2);
|
|
6876
7970
|
} catch (error) {
|
|
6877
7971
|
if (error instanceof SyntaxError) throw error;
|
|
6878
7972
|
throw new ReviewCommentError("body-too-large", "The request body is too large.");
|
|
6879
7973
|
}
|
|
6880
|
-
const value = record$
|
|
7974
|
+
const value = record$2(parsed);
|
|
6881
7975
|
if (value === void 0) throw new ReviewCommentError("invalid-request", "The request body is invalid.");
|
|
6882
7976
|
return value;
|
|
6883
7977
|
}
|
|
6884
7978
|
function sessionIdFromUrl(url) {
|
|
6885
7979
|
const value = url.searchParams.get("sessionId");
|
|
6886
|
-
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$
|
|
7980
|
+
if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$2) throw new ReviewCommentError("invalid-session", "The session is invalid.");
|
|
6887
7981
|
return value;
|
|
6888
7982
|
}
|
|
6889
7983
|
function registerReviewCommentRoute(ctx, store, ownsSession) {
|
|
@@ -6899,7 +7993,7 @@ function registerReviewCommentRoute(ctx, store, ownsSession) {
|
|
|
6899
7993
|
if (!ownsSession(sessionId)) throw new ReviewCommentError("session-unavailable", "The Claude session is unavailable.");
|
|
6900
7994
|
const pathname = io.url.pathname;
|
|
6901
7995
|
if (pathname === "/plugins/dsh-claude/review-comments") {
|
|
6902
|
-
const input = await readJson$
|
|
7996
|
+
const input = await readJson$2(io);
|
|
6903
7997
|
return {
|
|
6904
7998
|
status: 200,
|
|
6905
7999
|
value: { comment: store.add(sessionId, {
|
|
@@ -6916,7 +8010,7 @@ function registerReviewCommentRoute(ctx, store, ownsSession) {
|
|
|
6916
8010
|
value: { removed: store.drain(sessionId).length }
|
|
6917
8011
|
};
|
|
6918
8012
|
if (pathname === `/plugins/dsh-claude/review-comments/remove`) {
|
|
6919
|
-
const input = await readJson$
|
|
8013
|
+
const input = await readJson$2(io);
|
|
6920
8014
|
if (typeof input.id !== "string" || input.id.length === 0 || input.id.length > 128) throw new ReviewCommentError("invalid-request", "The comment id is invalid.");
|
|
6921
8015
|
return {
|
|
6922
8016
|
status: 200,
|
|
@@ -6951,6 +8045,85 @@ function registerReviewCommentRoute(ctx, store, ownsSession) {
|
|
|
6951
8045
|
});
|
|
6952
8046
|
}
|
|
6953
8047
|
//#endregion
|
|
8048
|
+
//#region src/plan-feedback-routes.ts
|
|
8049
|
+
const MAX_BODY_BYTES$1 = 65536;
|
|
8050
|
+
const MAX_SESSION_ID_CHARS$1 = 1024;
|
|
8051
|
+
const MAX_TOOL_USE_ID_CHARS = 256;
|
|
8052
|
+
function record$1(value) {
|
|
8053
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
8054
|
+
}
|
|
8055
|
+
async function readJson$1(io) {
|
|
8056
|
+
let parsed;
|
|
8057
|
+
try {
|
|
8058
|
+
parsed = await io.body(MAX_BODY_BYTES$1);
|
|
8059
|
+
} catch (error) {
|
|
8060
|
+
if (error instanceof SyntaxError) throw error;
|
|
8061
|
+
throw new PlanFeedbackError("body-too-large", "The request body is too large.");
|
|
8062
|
+
}
|
|
8063
|
+
const value = record$1(parsed);
|
|
8064
|
+
if (value === void 0) throw new PlanFeedbackError("invalid-request", "The request body is invalid.");
|
|
8065
|
+
return value;
|
|
8066
|
+
}
|
|
8067
|
+
/** Send one plan back for changes.
|
|
8068
|
+
*
|
|
8069
|
+
* Unary rather than a stream: the panel hands over what the reviewer wrote
|
|
8070
|
+
* and the turn carries on in the transcript it was already watching. Answers
|
|
8071
|
+
* 409 when nothing is waiting, which is what the panel shows if the approval
|
|
8072
|
+
* dialog was answered while the reviewer was still typing. */
|
|
8073
|
+
function registerPlanFeedbackRoute(ctx, gate, ownsSession) {
|
|
8074
|
+
registerPluginRoute(ctx, {
|
|
8075
|
+
mode: "unary",
|
|
8076
|
+
budget: "fast",
|
|
8077
|
+
kind: "exact",
|
|
8078
|
+
path: CLAUDE_PLAN_FEEDBACK_PATH,
|
|
8079
|
+
methods: ["POST"],
|
|
8080
|
+
handler: async (io) => {
|
|
8081
|
+
try {
|
|
8082
|
+
const sessionId = io.url.searchParams.get("sessionId");
|
|
8083
|
+
if (sessionId === null || sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS$1) return {
|
|
8084
|
+
status: 400,
|
|
8085
|
+
value: { error: "invalid-session" }
|
|
8086
|
+
};
|
|
8087
|
+
if (!ownsSession(sessionId)) return {
|
|
8088
|
+
status: 409,
|
|
8089
|
+
value: { error: "session-unavailable" }
|
|
8090
|
+
};
|
|
8091
|
+
const body = await readJson$1(io);
|
|
8092
|
+
const toolUseId = body.toolUseId;
|
|
8093
|
+
if (typeof toolUseId !== "string" || toolUseId.length === 0 || toolUseId.length > MAX_TOOL_USE_ID_CHARS) return {
|
|
8094
|
+
status: 400,
|
|
8095
|
+
value: { error: "invalid-request" }
|
|
8096
|
+
};
|
|
8097
|
+
const notes = planNotesOf(body.notes);
|
|
8098
|
+
if (!gate.submit(toolUseId, notes)) return {
|
|
8099
|
+
status: 409,
|
|
8100
|
+
value: { error: "plan-settled" }
|
|
8101
|
+
};
|
|
8102
|
+
return {
|
|
8103
|
+
status: 200,
|
|
8104
|
+
value: { ok: true }
|
|
8105
|
+
};
|
|
8106
|
+
} catch (error) {
|
|
8107
|
+
if (error instanceof PlanFeedbackError) return {
|
|
8108
|
+
status: 400,
|
|
8109
|
+
value: {
|
|
8110
|
+
error: error.code,
|
|
8111
|
+
message: error.message
|
|
8112
|
+
}
|
|
8113
|
+
};
|
|
8114
|
+
if (error instanceof SyntaxError) return {
|
|
8115
|
+
status: 400,
|
|
8116
|
+
value: { error: "invalid-json" }
|
|
8117
|
+
};
|
|
8118
|
+
return {
|
|
8119
|
+
status: 500,
|
|
8120
|
+
value: { error: "plan-feedback-unavailable" }
|
|
8121
|
+
};
|
|
8122
|
+
}
|
|
8123
|
+
}
|
|
8124
|
+
});
|
|
8125
|
+
}
|
|
8126
|
+
//#endregion
|
|
6954
8127
|
//#region src/client-diagnostics-routes.ts
|
|
6955
8128
|
/** Enough for a message plus a trimmed stack; the client caps its own volume. */
|
|
6956
8129
|
const MAX_DIAGNOSTIC_BYTES = 8192;
|
|
@@ -7015,8 +8188,15 @@ async function readJson(io) {
|
|
|
7015
8188
|
return;
|
|
7016
8189
|
}
|
|
7017
8190
|
}
|
|
7018
|
-
/** `POST <path>` with `{ sessionId, seq }`: hide that surface
|
|
7019
|
-
* later one, and arm Claude to resume before the turn it
|
|
8191
|
+
/** `POST <path>` with `{ sessionId, seq, restoreFiles? }`: hide that surface
|
|
8192
|
+
* event and every later one, and arm Claude to resume before the turn it
|
|
8193
|
+
* opened. With `restoreFiles`, the checkout is also put back to the tree that
|
|
8194
|
+
* turn was admitted against.
|
|
8195
|
+
*
|
|
8196
|
+
* The conversation rewind is what the user confirmed, so it lands first and
|
|
8197
|
+
* stands on its own; a checkout that cannot be restored — no snapshot, a
|
|
8198
|
+
* collected tree, no git — reports `filesRestored: false` rather than
|
|
8199
|
+
* failing the rewind. */
|
|
7020
8200
|
function registerClaudeRewindRoute(ctx, sidecar, access) {
|
|
7021
8201
|
registerPluginRoute(ctx, {
|
|
7022
8202
|
mode: "unary",
|
|
@@ -7042,16 +8222,22 @@ function registerClaudeRewindRoute(ctx, sidecar, access) {
|
|
|
7042
8222
|
status: 409,
|
|
7043
8223
|
value: { error: "session-busy" }
|
|
7044
8224
|
};
|
|
7045
|
-
const
|
|
8225
|
+
const current = (await sidecar.read(sessionId)).rewind ?? EMPTY_REWIND_STATE;
|
|
8226
|
+
const planned = planRewind(current, events, seq);
|
|
7046
8227
|
if (planned === void 0) return {
|
|
7047
8228
|
status: 409,
|
|
7048
8229
|
value: { error: "seq-unavailable" }
|
|
7049
8230
|
};
|
|
7050
|
-
|
|
8231
|
+
const tree = input?.restoreFiles === true ? rewindRestoreTree(current, events, seq) : void 0;
|
|
8232
|
+
await sidecar.writeRewind(sessionId, planned, turnAtOrAfter(events, seq));
|
|
7051
8233
|
await access.reset(sessionId);
|
|
8234
|
+
const filesRestored = tree === void 0 || access.restoreFiles === void 0 ? false : await access.restoreFiles(sessionId, tree).catch(() => false);
|
|
7052
8235
|
return {
|
|
7053
8236
|
status: 200,
|
|
7054
|
-
value: {
|
|
8237
|
+
value: {
|
|
8238
|
+
ranges: planned.ranges,
|
|
8239
|
+
filesRestored
|
|
8240
|
+
}
|
|
7055
8241
|
};
|
|
7056
8242
|
} catch (error) {
|
|
7057
8243
|
if (error instanceof SyntaxError) return {
|
|
@@ -7555,15 +8741,40 @@ const PROSE = {
|
|
|
7555
8741
|
else document.prose = value;
|
|
7556
8742
|
}
|
|
7557
8743
|
};
|
|
8744
|
+
/** Whether a session that needs the user interrupts them. Presentation only,
|
|
8745
|
+
* and read by the Client at delivery time, so like {@link PROSE} the switch
|
|
8746
|
+
* lands the moment it is saved. */
|
|
8747
|
+
const ALERTS = {
|
|
8748
|
+
key: "alerts",
|
|
8749
|
+
kind: "select",
|
|
8750
|
+
document: "plugin",
|
|
8751
|
+
effect: "immediate",
|
|
8752
|
+
async options() {
|
|
8753
|
+
return CLAUDE_ALERT_MODES.map((value) => ({
|
|
8754
|
+
value,
|
|
8755
|
+
label: value,
|
|
8756
|
+
source: "built-in"
|
|
8757
|
+
}));
|
|
8758
|
+
},
|
|
8759
|
+
read(document) {
|
|
8760
|
+
const value = document.alerts;
|
|
8761
|
+
return isClaudeAlertMode(value) ? value : "on";
|
|
8762
|
+
},
|
|
8763
|
+
apply(document, value) {
|
|
8764
|
+
if (!isClaudeAlertMode(value)) throw new Error("Invalid value for global setting alerts");
|
|
8765
|
+
if (value === "on") delete document.alerts;
|
|
8766
|
+
else document.alerts = value;
|
|
8767
|
+
}
|
|
8768
|
+
};
|
|
7558
8769
|
function isBoundedInteger(value, min, max) {
|
|
7559
8770
|
return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max;
|
|
7560
8771
|
}
|
|
7561
|
-
function integerSetting(key, min, max, defaultFor) {
|
|
8772
|
+
function integerSetting(key, min, max, defaultFor, effect = "new-session") {
|
|
7562
8773
|
return {
|
|
7563
8774
|
key,
|
|
7564
8775
|
kind: "text",
|
|
7565
8776
|
document: "plugin",
|
|
7566
|
-
effect
|
|
8777
|
+
effect,
|
|
7567
8778
|
maxLength: String(max).length,
|
|
7568
8779
|
read(document, defaults = DEFAULT_LIMITS) {
|
|
7569
8780
|
const value = document[key];
|
|
@@ -7580,8 +8791,9 @@ const DESCRIPTORS = [
|
|
|
7580
8791
|
OUTPUT_STYLE,
|
|
7581
8792
|
RENDERER,
|
|
7582
8793
|
PROSE,
|
|
8794
|
+
ALERTS,
|
|
7583
8795
|
WORKTREE_BRANCH_PREFIX,
|
|
7584
|
-
integerSetting("maxProcesses", 1, MAX_PROCESSES_LIMIT, (limits) => limits.maxProcesses),
|
|
8796
|
+
integerSetting("maxProcesses", 1, MAX_PROCESSES_LIMIT, (limits) => limits.maxProcesses, "immediate"),
|
|
7585
8797
|
integerSetting("idleTimeoutMinutes", 1, MAX_IDLE_TIMEOUT_MINUTES, (limits) => Math.max(1, Math.round(limits.idleTimeoutMs / 6e4)))
|
|
7586
8798
|
];
|
|
7587
8799
|
const DESCRIPTOR_BY_KEY = new Map(DESCRIPTORS.map((descriptor) => [descriptor.key, descriptor]));
|
|
@@ -7863,14 +9075,12 @@ async function apply(ctx, config) {
|
|
|
7863
9075
|
const supervisorConfig = {
|
|
7864
9076
|
executablePath: "",
|
|
7865
9077
|
defaultModel: config.model ?? "default",
|
|
7866
|
-
renderMode: DEFAULT_CLAUDE_RENDER_MODE,
|
|
7867
9078
|
...defaultLimits
|
|
7868
9079
|
};
|
|
7869
9080
|
const applySettingsOverrides = async () => {
|
|
7870
9081
|
const overrides = await readSupervisorLimitOverrides();
|
|
7871
9082
|
supervisorConfig.idleTimeoutMs = overrides.idleTimeoutMs ?? defaultLimits.idleTimeoutMs;
|
|
7872
9083
|
supervisorConfig.maxProcesses = overrides.maxProcesses ?? defaultLimits.maxProcesses;
|
|
7873
|
-
supervisorConfig.renderMode = await readRenderMode();
|
|
7874
9084
|
};
|
|
7875
9085
|
await applySettingsOverrides();
|
|
7876
9086
|
const sidecar = new ClaudeSidecarRepository();
|
|
@@ -7892,7 +9102,7 @@ async function apply(ctx, config) {
|
|
|
7892
9102
|
let resolutionError;
|
|
7893
9103
|
try {
|
|
7894
9104
|
supervisorConfig.executablePath = (await resolveClaudeExecutable(ctx.subprocess, config.executablePath === void 0 || config.executablePath.length === 0 ? void 0 : config.executablePath)).path;
|
|
7895
|
-
ctx.llm.registerAdapter([...CLAUDE_CODE_PROVIDER_IDS], createClaudeCodeAdapter(supervisor, ctx.agents, ctx.attachments, (agent) => ctx.agentPresets.composedPreset(agent.ctx), (sessionId) => reviewComments.drain(sessionId), () => supervisorConfig.
|
|
9105
|
+
ctx.llm.registerAdapter([...CLAUDE_CODE_PROVIDER_IDS], createClaudeCodeAdapter(supervisor, ctx.agents, ctx.attachments, (agent) => ctx.agentPresets.composedPreset(agent.ctx), (sessionId) => reviewComments.drain(sessionId), () => readRenderMode(), (request) => summarizeSessionTitle(supervisorConfig.executablePath, request)));
|
|
7896
9106
|
ctx.effect(() => {
|
|
7897
9107
|
const mounted = /* @__PURE__ */ new Map();
|
|
7898
9108
|
const pending = /* @__PURE__ */ new Set();
|
|
@@ -7996,7 +9206,10 @@ async function apply(ctx, config) {
|
|
|
7996
9206
|
registerClaudeUpdateRoutes(webCtx, webCtx.subprocess, { ...typeof desktopActions?.requestRestart === "function" ? { requestRestart: desktopActions.requestRestart.bind(desktopActions) } : {} });
|
|
7997
9207
|
registerClaudeGlobalSettingsRoute(webCtx, {
|
|
7998
9208
|
defaultLimits,
|
|
7999
|
-
onUpdated:
|
|
9209
|
+
onUpdated: async () => {
|
|
9210
|
+
await applySettingsOverrides();
|
|
9211
|
+
supervisor.limitsChanged();
|
|
9212
|
+
}
|
|
8000
9213
|
});
|
|
8001
9214
|
registerRepositorySetupRoute(webCtx, repositorySetup, () => sweepWorktrees?.());
|
|
8002
9215
|
registerRepositoryStatusRoute(webCtx, repositoryStatus);
|
|
@@ -8010,6 +9223,10 @@ async function apply(ctx, config) {
|
|
|
8010
9223
|
};
|
|
8011
9224
|
registerRepositoryActionRoute(webCtx, repositoryActions, cwdForClaudeSession);
|
|
8012
9225
|
registerEditorOpenRoute(webCtx, new EditorOpenService(webCtx.subprocess), cwdForClaudeSession);
|
|
9226
|
+
registerClaudePromptsRoute(webCtx);
|
|
9227
|
+
const promptAssist = new PromptAssistService(webCtx.subprocess, () => supervisorConfig.executablePath);
|
|
9228
|
+
registerClaudePromptNameRoute(webCtx, promptAssist);
|
|
9229
|
+
registerClaudePromptRefineRoute(webCtx, promptAssist);
|
|
8013
9230
|
registerPullRequestFeedbackRoute(webCtx, new PullRequestFeedbackService(webCtx.subprocess), cwdForClaudeSession);
|
|
8014
9231
|
registerAskRoute(webCtx, new AskService(webCtx.subprocess, supervisorConfig.executablePath), cwdForClaudeSession, (sessionId) => {
|
|
8015
9232
|
const snapshot = supervisor.snapshots().find((item) => item.sessionId === sessionId);
|
|
@@ -8023,13 +9240,18 @@ async function apply(ctx, config) {
|
|
|
8023
9240
|
return agent !== void 0 && webCtx.agentPresets.composedPreset(agent.ctx) === "claude";
|
|
8024
9241
|
};
|
|
8025
9242
|
registerReviewCommentRoute(webCtx, reviewComments, ownsClaudeSession);
|
|
9243
|
+
registerPlanFeedbackRoute(webCtx, supervisor.planFeedback, ownsClaudeSession);
|
|
8026
9244
|
registerClaudeRewindRoute(webCtx, sidecar, {
|
|
8027
9245
|
eventsFor: (sessionId) => {
|
|
8028
9246
|
const agent = webCtx.agents.get(sessionId);
|
|
8029
9247
|
return agent === void 0 || webCtx.agentPresets.composedPreset(agent.ctx) !== "claude" ? void 0 : agent.session.events;
|
|
8030
9248
|
},
|
|
8031
9249
|
busy: (sessionId) => supervisor.snapshots().some((item) => item.sessionId === sessionId && (item.state === "running" || item.state === "interrupting")),
|
|
8032
|
-
reset: (sessionId) => supervisor.disposeSession(sessionId)
|
|
9250
|
+
reset: (sessionId) => supervisor.disposeSession(sessionId),
|
|
9251
|
+
restoreFiles: async (sessionId, tree) => {
|
|
9252
|
+
const cwd = webCtx.agents.get(sessionId)?.session.header.cwd;
|
|
9253
|
+
return cwd === void 0 ? false : restoreWorktreeTree(ctx.subprocess, cwd, tree);
|
|
9254
|
+
}
|
|
8033
9255
|
});
|
|
8034
9256
|
registerPlanUsageRoute(webCtx, (fetchedAt) => probePlanUsage(supervisorConfig.executablePath, fetchedAt));
|
|
8035
9257
|
registerClaudeProjectionRoute(webCtx, sidecar, ownsClaudeSession, (sessionId) => commandCatalogs.get(sessionId) ?? [], async (sessionId) => {
|