@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/lib/index.mjs CHANGED
@@ -1,12 +1,12 @@
1
- import { A as CLAUDE_REWIND_PATH, C as CLAUDE_RENDER_MODES, D as CLAUDE_REPOSITORY_SETUP_PATH, E as CLAUDE_REPOSITORY_FILE_PATH, F as DEFAULT_CLAUDE_RENDER_MODE, L as TASK_TOOL_NAMES, M as CLAUDE_UPDATE_PATH, N as CLAUDE_USAGE_PATH, O as CLAUDE_REPOSITORY_STATUS_PATH, P as DEFAULT_CLAUDE_PROSE_MODE, R as isClaudeProseMode, S as CLAUDE_PROSE_MODES, T as CLAUDE_REPOSITORY_FEEDBACK_PATH, _ as CLAUDE_DOCTOR_PATH, a as latestClaudeTasks, b as CLAUDE_JIRA_PATH, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_ASK_PATH, g as CLAUDE_CODE_PROVIDER_IDS, h as CLAUDE_CODE_PROVIDER, i as latestClaudeSessionBinding, j as CLAUDE_UPDATE_CHECK_PATH, k as CLAUDE_REVIEW_COMMENT_PATH, l as redactText, m as CLAUDE_CODE_PRESET_ID, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_CLIENT_DIAGNOSTICS_PATH, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as CLAUDE_EDITOR_OPEN_PATH, w as CLAUDE_REPOSITORY_ACTION_PATH, x as CLAUDE_PROJECTION_PATH, y as CLAUDE_GLOBAL_SETTINGS_PATH, z as isClaudeRenderMode } from "./events-OhBoFNKO.mjs";
2
- import { a as projectClaudeCommands, i as CLAUDE_COMMANDS_SERVICE, r as dynamicPresenterDefinition, t as CLAUDE_PRESENTER_NAMES } from "./presenters-BBoM1Ju1.mjs";
3
- import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-loenwnLS.mjs";
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 rewind Claude no
61
- * longer holds those entries, so a later rewind must never fork at one. */
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$13(value) {
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$13(value);
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$13(value);
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$13(value);
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$13(value);
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$13(item);
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$13(item);
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 pending = record$13(input.pending);
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$13(value);
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$13(value);
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
- writeRewind(sessionId, value) {
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}`, 1200);
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 alreadyFullAccess = await active.hasFullAccess?.() === true;
659
- const outcome = alreadyFullAccess ? "allowed-once" : await approval.request({
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
- const fullAccess = alreadyFullAccess || await active.hasFullAccess?.() === true;
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$12(value) {
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$12(message.usage));
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$12(message.message);
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$12(item);
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$12(envelope?.usage));
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$12(message.message)?.content;
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$12(item);
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$12(message.usage));
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$12(message.patch);
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$12(message.usage));
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$12(item);
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$12(message.compact_metadata);
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$12(value.event);
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$12(event.delta);
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$12(item)).filter((item) => item !== void 0).map((item) => {
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$12(value.rate_limit_info)?.status);
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$11(value) {
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$11(source);
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$11(value);
1535
+ const response = record$12(value);
1309
1536
  const subscription = typeof response?.subscription_type === "string" ? response.subscription_type : void 0;
1310
- const limits = record$11(response?.rate_limits);
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$11(entry)?.display_name;
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
- const operation = this.#admissionGate.then(() => this.#runTurnAdmitted(request));
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
- return operation;
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
- entry = await this.#createEntry(request.agent, request.model ?? this.#config.defaultModel, request.thinkingMode);
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
- await entry.sdkInitialization;
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 = await this.#createEntry(request.agent, model, request.thinkingMode);
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
- await entry.sdkInitialization;
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
- cursor.nextOrdinal = (await this.#sidecar.read(sessionId)).activities.reduce((next, activity) => activity.turn === cursor.turn && activity.step === cursor.step ? Math.max(next, activity.ordinal + 1) : next, 0);
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
- entry.state = "idle";
1716
- entry.lastUsedAt = Date.now();
1717
- this.#armIdleTimer(entry);
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
- const entry = await this.#metadataEntry(agent, model);
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
- entry = await this.#createEntry(agent, model);
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 every metadata read share one process-wide gate, so an
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 === void 0) return;
1795
- this.#entries.delete(sessionId);
1796
- await this.#disposeEntry(entry);
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
- await Promise.allSettled(entries.map((entry) => this.#disposeEntry(entry)));
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
- if (this.#entries.size < this.#config.maxProcesses) return;
1807
- const idle = [...this.#entries.values()].filter((entry) => entry.active === void 0 && entry.state === "idle").sort((left, right) => left.lastUsedAt - right.lastUsedAt)[0];
1808
- if (idle === void 0) throw new ClaudeProcessLimitError(this.#config.maxProcesses);
1809
- this.#entries.delete(idle.sessionId);
1810
- await this.#disposeEntry(idle);
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 #createEntry(agent, model, thinkingMode) {
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 (this.#nativeRendering()) active.output.push({
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 (this.#nativeRendering()) {
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 && this.#nativeRendering()) await this.#appendNativeToolResult(active, message);
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 (this.#nativeRendering()) await this.#appendNativeToolResult(active, {
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
- ...this.#nativeRendering() ? { renderer: "native" } : {},
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
- ...this.#nativeRendering() ? { renderer: "native" } : {},
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 (this.#nativeRendering()) await this.#appendNativeToolResult(active, {
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
- constructor(supervisor, agents, attachments, presetIdFor, drainReviewComments = () => [], renderMode = () => DEFAULT_CLAUDE_RENDER_MODE) {
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$6 = 1024;
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$6;
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
- writeLine({ type: "ping" });
3525
- })().catch(() => void 0);
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$10(value) {
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$10(item);
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$10(value);
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$10(input.author)?.login === "string" ? { author: bounded(String(record$10(input.author)?.login)) } : {},
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$6 = 16384;
5139
- function record$9(value) {
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$5(io) {
5884
+ async function readJson$6(io) {
5143
5885
  let parsed;
5144
5886
  try {
5145
- parsed = await io.body(MAX_BODY_BYTES$6);
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$9(parsed);
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$5(io);
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$5(io);
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$5(io);
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$5(io);
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$5 = 16384;
5266
- const MAX_SESSION_ID_CHARS$5 = 1024;
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$8(value) {
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$4(io) {
6020
+ async function readJson$5(io) {
5279
6021
  let parsed;
5280
6022
  try {
5281
- parsed = await io.body(MAX_BODY_BYTES$5);
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$8(parsed);
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$5) throw new RepositoryActionError("invalid-session", "The session is invalid.");
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$4(io);
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$4(io)))
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$4 = 1024;
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$4) return {
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$7(value) {
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$7(record$7(record$7(record$7(record$7(value)?.data)?.repository)?.pullRequest)?.reviewThreads)?.nodes;
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$7(item);
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$7(input.comments)?.nodes;
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$7(node);
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$7(comment.author);
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$7(value);
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$7(input.user);
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$7(record$7(record$7(record$7(value)?.data)?.repository)?.mentionableUsers)?.nodes;
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$7(item);
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$7(item);
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$7(parsed)?.path === "string" ? String(record$7(parsed)?.path) : "";
5817
- const line = Number.isSafeInteger(record$7(parsed)?.line) ? Number(record$7(parsed)?.line) : void 0;
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$7(parsed)?.side === "LEFT" ? "old" : "new"
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$7(record$7(record$7(record$7(JSON.parse(result.stdout))?.data)?.[resolved ? "resolveReviewThread" : "unresolveReviewThread"])?.thread);
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$3 = 1024;
5966
- const MAX_BODY_BYTES$4 = 16384;
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$6(value) {
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$3(io) {
7067
+ async function readJson$4(io) {
5974
7068
  let parsed;
5975
7069
  try {
5976
- parsed = await io.body(MAX_BODY_BYTES$4);
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$6(parsed);
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$3) throw new PullRequestFeedbackError("invalid-session", "The session is invalid.");
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$3(io);
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$3(io);
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$5(value) {
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$5(value)?.sections;
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$5(section)?.issues;
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$5(issue)?.key;
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$5(value)?.issues;
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$5(item);
6273
- const fields = record$5(issue?.fields);
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$5(fields.status)?.name;
6276
- const type = record$5(fields.issuetype)?.name;
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$5(await this.#json(connection, "/rest/api/3/myself"));
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$5(await this.#json(store, "/rest/api/3/myself"));
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$5(JSON.parse(text));
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$3 = 8192;
6445
- function record$4(value) {
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$2(io) {
7544
+ async function readJson$3(io) {
6451
7545
  let body;
6452
7546
  try {
6453
- body = await io.body(MAX_BODY_BYTES$3);
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$4(body);
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$2(io);
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$2(io);
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$3(value) {
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$3(input);
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$3(JSON.parse(line));
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$3(parsed.event);
7760
+ const event = record$4(parsed.event);
6667
7761
  if (event?.type === "content_block_start") {
6668
- const block = record$3(event.content_block);
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$3(event?.delta);
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$3(parsed.message)?.content;
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$3(item);
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$2 = 131072;
6787
- const MAX_SESSION_ID_CHARS$2 = 1024;
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$2(value) {
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$2) throw new AskError("invalid-session", "The session is invalid.");
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$2(await io.body(MAX_BODY_BYTES$2));
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$1 = 16384;
6868
- const MAX_SESSION_ID_CHARS$1 = 1024;
6869
- function record$1(value) {
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$1(io) {
7966
+ async function readJson$2(io) {
6873
7967
  let parsed;
6874
7968
  try {
6875
- parsed = await io.body(MAX_BODY_BYTES$1);
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$1(parsed);
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$1) throw new ReviewCommentError("invalid-session", "The session is invalid.");
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$1(io);
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$1(io);
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 event and every
7019
- * later one, and arm Claude to resume before the turn it opened. */
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 planned = planRewind((await sidecar.read(sessionId)).rewind ?? EMPTY_REWIND_STATE, events, seq);
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
- await sidecar.writeRewind(sessionId, planned);
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: { ranges: planned.ranges }
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: "new-session",
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.renderMode));
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: applySettingsOverrides
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) => {