@norman-else/dsh-claude 0.1.39 → 0.1.41

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_STATUS_PATH, B as isClaudeAlertMode, C as CLAUDE_PROJECTION_PATH, D as CLAUDE_REPOSITORY_FEEDBACK_PATH, E as CLAUDE_REPOSITORY_ACTION_PATH, F as CLAUDE_USAGE_PATH, H as isClaudeRenderMode, I as DEFAULT_CLAUDE_PROSE_MODE, L as DEFAULT_CLAUDE_RENDER_MODE, M as CLAUDE_REWIND_PATH, N as CLAUDE_UPDATE_CHECK_PATH, O as CLAUDE_REPOSITORY_FILE_PATH, P as CLAUDE_UPDATE_PATH, S as CLAUDE_PLAN_FEEDBACK_PATH, T as CLAUDE_RENDER_MODES, V 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_REVIEW_COMMENT_PATH, k as CLAUDE_REPOSITORY_SETUP_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_PROSE_MODES, x as CLAUDE_JIRA_PATH, y as CLAUDE_EDITOR_OPEN_PATH, z as TASK_TOOL_NAMES } from "./events-B-FPMzI7.mjs";
2
+ import { a as projectClaudeCommands, i as CLAUDE_COMMANDS_SERVICE, r as dynamicPresenterDefinition, t as CLAUDE_PRESENTER_NAMES } from "./presenters-BV42EKkB.mjs";
3
+ import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-yRGfkGjd.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. */
@@ -1549,6 +1885,8 @@ var ClaudeSupervisor = class {
1549
1885
  #runtime;
1550
1886
  #approval;
1551
1887
  #userQuestions;
1888
+ /** Lets the plan panel answer a plan's approval with revisions. */
1889
+ planFeedback = new PlanFeedbackGate();
1552
1890
  #config;
1553
1891
  #queryFactory;
1554
1892
  #runDetached;
@@ -1670,6 +2008,7 @@ var ClaudeSupervisor = class {
1670
2008
  const active = {
1671
2009
  agent: request.agent,
1672
2010
  cursor,
2011
+ native: (request.renderMode ?? this.#config.renderMode ?? "plugin") === "native",
1673
2012
  output: new AsyncQueue(),
1674
2013
  promptUuid,
1675
2014
  phase: "primary",
@@ -1690,6 +2029,7 @@ var ClaudeSupervisor = class {
1690
2029
  entry.active = active;
1691
2030
  entry.state = "running";
1692
2031
  entry.lastUsedAt = Date.now();
2032
+ await this.#captureWorktree(entry, cursor.turn);
1693
2033
  try {
1694
2034
  await this.#appendActivity(active, {
1695
2035
  kind: "status",
@@ -1860,7 +2200,7 @@ var ClaudeSupervisor = class {
1860
2200
  };
1861
2201
  };
1862
2202
  const userQuestion = createUserQuestionBridge(this.#userQuestions, activeInteraction);
1863
- const canUseTool = createPermissionBridge(this.#approval, activeInteraction, userQuestion);
2203
+ const canUseTool = createPermissionBridge(this.#approval, activeInteraction, userQuestion, this.planFeedback);
1864
2204
  const options = {
1865
2205
  pathToClaudeCodeExecutable: this.#config.executablePath,
1866
2206
  cwd,
@@ -2000,7 +2340,7 @@ var ClaudeSupervisor = class {
2000
2340
  title: "Claude thinking",
2001
2341
  summary: message.text
2002
2342
  });
2003
- if (this.#nativeRendering()) active.output.push({
2343
+ if (active.native) active.output.push({
2004
2344
  type: "thinking",
2005
2345
  text: message.text
2006
2346
  });
@@ -2019,7 +2359,7 @@ var ClaudeSupervisor = class {
2019
2359
  });
2020
2360
  if (message.parentToolUseId === void 0) {
2021
2361
  active.openCalls.set(message.toolUseId, message.toolName);
2022
- if (this.#nativeRendering()) {
2362
+ if (active.native) {
2023
2363
  this.#ensureDynamicPresenter(active.agent, message.toolName);
2024
2364
  await this.#appendNativeToolCall(active, message);
2025
2365
  }
@@ -2036,7 +2376,7 @@ var ClaudeSupervisor = class {
2036
2376
  detail: message.output,
2037
2377
  isError: message.isError
2038
2378
  });
2039
- if (message.parentToolUseId === void 0 && this.#nativeRendering()) await this.#appendNativeToolResult(active, message);
2379
+ if (message.parentToolUseId === void 0 && active.native) await this.#appendNativeToolResult(active, message);
2040
2380
  return;
2041
2381
  case "subagent":
2042
2382
  await this.#appendActivity(active, {
@@ -2086,7 +2426,7 @@ var ClaudeSupervisor = class {
2086
2426
  title: message.toolName,
2087
2427
  summary: message.summary
2088
2428
  });
2089
- if (this.#nativeRendering()) await this.#appendNativeToolResult(active, {
2429
+ if (active.native) await this.#appendNativeToolResult(active, {
2090
2430
  kind: "tool-result",
2091
2431
  toolUseId: message.toolUseId,
2092
2432
  output: message.summary,
@@ -2106,9 +2446,6 @@ var ClaudeSupervisor = class {
2106
2446
  ...active.firstOutputAt === void 0 ? {} : { ttftMs: Math.max(0, active.firstOutputAt - active.startedAt) }
2107
2447
  };
2108
2448
  }
2109
- #nativeRendering() {
2110
- return (this.#config.renderMode ?? "plugin") === "native";
2111
- }
2112
2449
  /** Register one presenter-only mirror for a tool name the static preset
2113
2450
  * registry does not cover (MCP tools, newly added built-ins). Runs in the
2114
2451
  * agent scope so the mirror is visible only to this preset's sessions and
@@ -2436,6 +2773,20 @@ var ClaudeSupervisor = class {
2436
2773
  this.#sidecar.checkpoint(entry.sessionId);
2437
2774
  } catch {}
2438
2775
  }
2776
+ /** Pin the working tree this turn is about to change, so a rewind of it can
2777
+ * put the checkout back where the turn found it.
2778
+ *
2779
+ * Awaited, and deliberately: a snapshot taken after Claude's first edit
2780
+ * would restore to a state that never existed. It costs one `git add -A`
2781
+ * against a throwaway index per turn, and best effort throughout -- a
2782
+ * session with no repository simply never offers a file rewind. */
2783
+ async #captureWorktree(entry, turn) {
2784
+ try {
2785
+ const tree = await captureWorktreeTree(this.#runtime, entry.cwd);
2786
+ if (tree === void 0) return;
2787
+ await this.#sidecar.recordRewindSnapshot(entry.sessionId, turn, tree);
2788
+ } catch {}
2789
+ }
2439
2790
  /** Pin where Claude's chain ended for the DSH turn that just settled, so a
2440
2791
  * later rewind of the following turn can fork exactly here. Best effort:
2441
2792
  * a missing anchor only makes a rewind fall back to an earlier turn. */
@@ -2453,7 +2804,7 @@ var ClaudeSupervisor = class {
2453
2804
  try {
2454
2805
  this.#sidecar.appendTranscriptText(active.agent.id, {
2455
2806
  text: active.transcriptText,
2456
- ...this.#nativeRendering() ? { renderer: "native" } : {},
2807
+ ...active.native ? { renderer: "native" } : {},
2457
2808
  turn: active.cursor.turn,
2458
2809
  step: active.cursor.step,
2459
2810
  ordinal
@@ -2472,7 +2823,7 @@ var ClaudeSupervisor = class {
2472
2823
  const ordinal = active.cursor.nextOrdinal++;
2473
2824
  await this.#sidecar.appendActivity(active.agent.id, {
2474
2825
  ...activity,
2475
- ...this.#nativeRendering() ? { renderer: "native" } : {},
2826
+ ...active.native ? { renderer: "native" } : {},
2476
2827
  turn: active.cursor.turn,
2477
2828
  step: active.cursor.step,
2478
2829
  ordinal
@@ -2511,7 +2862,7 @@ var ClaudeSupervisor = class {
2511
2862
  summary,
2512
2863
  isError: true
2513
2864
  });
2514
- if (this.#nativeRendering()) await this.#appendNativeToolResult(active, {
2865
+ if (active.native) await this.#appendNativeToolResult(active, {
2515
2866
  kind: "tool-result",
2516
2867
  toolUseId,
2517
2868
  output: summary,
@@ -2670,6 +3021,85 @@ function formatReviewComments(comments) {
2670
3021
  ].join("\n");
2671
3022
  }
2672
3023
  //#endregion
3024
+ //#region src/session-title.ts
3025
+ /** Answer DSH's auxiliary session-title request with a throwaway Haiku turn.
3026
+ *
3027
+ * DSH titles a session by asking the model behind the session's own route for
3028
+ * a summary of the first human message. That route is this plugin, and the
3029
+ * session's Claude process must not answer it: the title call carries a
3030
+ * plugin-authored system prompt and would land in the user's transcript. A
3031
+ * separate one-shot turn keeps it out, for the same reasons as the branch-name
3032
+ * summary and the plan-usage probe. Deployments that never installed a second
3033
+ * model provider still get a readable title, since the only model this plugin
3034
+ * needs is the one it already runs. */
3035
+ /** Cheapest model that can summarize a sentence in the language it was written in. */
3036
+ const SESSION_TITLE_MODEL = "haiku";
3037
+ /** Backstop only. The title service wraps its own deadline (60s by default)
3038
+ * around the call and passes it as `signal`, so a shorter budget here just
3039
+ * kills a turn the caller was still happy to wait for — a cold CLI start plus
3040
+ * one Haiku reply routinely passes ten seconds. */
3041
+ const SESSION_TITLE_TIMEOUT_MS = 6e4;
3042
+ /** DSH frames the messages as JSON under its own byte cap; this only bounds a
3043
+ * caller that does not. */
3044
+ const MAX_INPUT_CHARS = 8e3;
3045
+ /** Longer than any title DSH accepts (80 bytes), short enough to bound prose. */
3046
+ const MAX_TITLE_CHARS = 200;
3047
+ /** Carry DSH's instruction as the prompt's own preamble: a Claude Code turn has
3048
+ * no separate system slot this plugin can borrow without replacing the CLI's. */
3049
+ function sessionTitlePrompt(request) {
3050
+ const input = request.input.trim().slice(0, MAX_INPUT_CHARS);
3051
+ return request.system === void 0 || request.system.length === 0 ? input : `${request.system}\n\n${input}`;
3052
+ }
3053
+ /** The one line of the reply that is the title. DSH strips control characters
3054
+ * and truncates to its own byte cap, so nothing else is cleaned here. */
3055
+ function sessionTitleLine(reply) {
3056
+ const line = reply.split("\n").map((candidate) => candidate.trim()).find((candidate) => candidate.length > 0);
3057
+ return line === void 0 ? "" : line.slice(0, MAX_TITLE_CHARS);
3058
+ }
3059
+ /**
3060
+ * Summarize the framed messages into one title line.
3061
+ *
3062
+ * Rejects rather than returning a placeholder: the title service logs the
3063
+ * failure and keeps the deterministic first-words fallback, which is a better
3064
+ * label than anything this function could invent.
3065
+ */
3066
+ async function summarizeSessionTitle(executablePath, request, factory = query) {
3067
+ const prompt = sessionTitlePrompt(request);
3068
+ if (prompt.length === 0) throw new Error("dsh-claude: the session-title request carried no text");
3069
+ const lifetime = new AbortController();
3070
+ const abort = () => {
3071
+ lifetime.abort();
3072
+ };
3073
+ request.signal?.addEventListener("abort", abort, { once: true });
3074
+ const timer = setTimeout(abort, SESSION_TITLE_TIMEOUT_MS);
3075
+ timer.unref?.();
3076
+ try {
3077
+ const query = factory({
3078
+ prompt,
3079
+ options: {
3080
+ cwd: process.cwd(),
3081
+ abortController: lifetime,
3082
+ model: SESSION_TITLE_MODEL,
3083
+ allowedTools: [],
3084
+ settingSources: [],
3085
+ maxTurns: 1,
3086
+ ...executablePath.length === 0 ? {} : { pathToClaudeCodeExecutable: executablePath }
3087
+ }
3088
+ });
3089
+ for await (const message of query) {
3090
+ if (message.type !== "result" || message.subtype !== "success") continue;
3091
+ const title = sessionTitleLine(message.result);
3092
+ if (title.length > 0) return title;
3093
+ break;
3094
+ }
3095
+ throw new Error("dsh-claude: the session-title turn produced no title");
3096
+ } finally {
3097
+ clearTimeout(timer);
3098
+ request.signal?.removeEventListener("abort", abort);
3099
+ lifetime.abort();
3100
+ }
3101
+ }
3102
+ //#endregion
2673
3103
  //#region src/adapter.ts
2674
3104
  const THINKING_MODES = [
2675
3105
  {
@@ -2822,6 +3252,12 @@ function tokenUsage(usage) {
2822
3252
  if (usage.cacheCreationTokens !== void 0) normalized.cacheWriteTokens = usage.cacheCreationTokens;
2823
3253
  return normalized;
2824
3254
  }
3255
+ /** Flatten a hand-built auxiliary request to text. Unlike a conversation turn
3256
+ * it has no images and no human-sourced message to single out: every message
3257
+ * in it was assembled by the plugin that asked the question. */
3258
+ function auxiliaryText(messages) {
3259
+ return messages.flatMap((message) => message.content.filter((block) => block.type === "text").map((block) => block.text)).join("\n");
3260
+ }
2825
3261
  function resolveAgent(agents, options) {
2826
3262
  const initiator = agents.currentInitiator();
2827
3263
  if (initiator !== void 0) return initiator;
@@ -2837,8 +3273,12 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
2837
3273
  #attachments;
2838
3274
  #presetIdFor;
2839
3275
  #drainReviewComments;
3276
+ /** The renderer setting, read from its file at the start of each turn. A
3277
+ * cached copy would go stale whenever the file is edited outside the
3278
+ * Settings dialog, and the read is dwarfed by the process the turn spawns. */
2840
3279
  #renderMode;
2841
- constructor(supervisor, agents, attachments, presetIdFor, drainReviewComments = () => [], renderMode = () => DEFAULT_CLAUDE_RENDER_MODE) {
3280
+ #summarizeTitle;
3281
+ constructor(supervisor, agents, attachments, presetIdFor, drainReviewComments = () => [], renderMode = async () => DEFAULT_CLAUDE_RENDER_MODE, summarizeTitle = (request) => summarizeSessionTitle("", request)) {
2842
3282
  super();
2843
3283
  this.#supervisor = supervisor;
2844
3284
  this.#agents = agents;
@@ -2846,6 +3286,7 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
2846
3286
  this.#presetIdFor = presetIdFor;
2847
3287
  this.#drainReviewComments = drainReviewComments;
2848
3288
  this.#renderMode = renderMode;
3289
+ this.#summarizeTitle = summarizeTitle;
2849
3290
  }
2850
3291
  providerInfo(provider) {
2851
3292
  return {
@@ -2888,7 +3329,47 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
2888
3329
  stream: (options) => this.stream(options)
2889
3330
  };
2890
3331
  }
3332
+ /** Title the session from its own provider without touching its process.
3333
+ *
3334
+ * DSH routes the title request at the session's model, which is this
3335
+ * adapter; a deployment with no second provider configured would otherwise
3336
+ * never get a title at all and keep the first five words of the first
3337
+ * message. A throwaway Haiku turn answers it, so the session's transcript,
3338
+ * context, and permission bridge stay out of it. */
3339
+ async *#titleStream(options) {
3340
+ const title = await this.#summarizeTitle({
3341
+ ...options.system === void 0 ? {} : { system: options.system },
3342
+ input: auxiliaryText(options.messages),
3343
+ ...options.signal === void 0 ? {} : { signal: options.signal }
3344
+ });
3345
+ yield {
3346
+ type: "block-start",
3347
+ index: 0,
3348
+ blockType: "text"
3349
+ };
3350
+ yield {
3351
+ type: "text-delta",
3352
+ index: 0,
3353
+ text: title
3354
+ };
3355
+ yield {
3356
+ type: "block-end",
3357
+ index: 0,
3358
+ block: {
3359
+ type: "text",
3360
+ text: title
3361
+ }
3362
+ };
3363
+ yield {
3364
+ type: "finish",
3365
+ reason: { kind: "stop" }
3366
+ };
3367
+ }
2891
3368
  async *stream(options) {
3369
+ if (options.purpose === "session-title") {
3370
+ yield* this.#titleStream(options);
3371
+ return;
3372
+ }
2892
3373
  if (options.purpose !== void 0) throw new Error(`dsh-claude: auxiliary ${options.purpose} calls are not routed into the Claude session`);
2893
3374
  const agent = resolveAgent(this.#agents, options);
2894
3375
  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 +3391,16 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
2910
3391
  };
2911
3392
  return;
2912
3393
  }
3394
+ const renderMode = await this.#renderMode();
3395
+ const native = renderMode === "native";
2913
3396
  const events = await this.#supervisor.runTurn({
2914
3397
  agent,
2915
3398
  prompt: injectReviewComments(prompt, this.#drainReviewComments(agent.id)),
2916
3399
  model: options.model,
3400
+ renderMode,
2917
3401
  ...thinkingMode === void 0 ? {} : { thinkingMode },
2918
3402
  ...options.signal === void 0 ? {} : { signal: options.signal }
2919
3403
  });
2920
- const native = this.#renderMode() === "native";
2921
3404
  let pendingUsage;
2922
3405
  let completed = false;
2923
3406
  let blockIndex = 0;
@@ -3016,8 +3499,8 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
3016
3499
  if (!completed) throw new Error("dsh-claude: Claude turn stream ended without a result");
3017
3500
  }
3018
3501
  };
3019
- function createClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments = () => [], renderMode = () => DEFAULT_CLAUDE_RENDER_MODE) {
3020
- return new ClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments, renderMode);
3502
+ function createClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments = () => [], renderMode = async () => DEFAULT_CLAUDE_RENDER_MODE, summarizeTitle = (request) => summarizeSessionTitle("", request)) {
3503
+ return new ClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments, renderMode, summarizeTitle);
3021
3504
  }
3022
3505
  //#endregion
3023
3506
  //#region src/plugin-budget.ts
@@ -3326,7 +3809,7 @@ function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolution
3326
3809
  }
3327
3810
  //#endregion
3328
3811
  //#region src/projection-routes.ts
3329
- const MAX_SESSION_ID_CHARS$6 = 1024;
3812
+ const MAX_SESSION_ID_CHARS$7 = 1024;
3330
3813
  /** Slow-moving metadata refresh and stream heartbeat cadence; deliberately off
3331
3814
  * the transcript hot path so git/gh latency never delays visible text. */
3332
3815
  const META_REFRESH_MS = 5e3;
@@ -3334,7 +3817,7 @@ const META_REFRESH_MS = 5e3;
3334
3817
  * so this cannot collide with one. */
3335
3818
  const MULTI_SEGMENT = "multi";
3336
3819
  function validSessionId(value) {
3337
- return value.length > 0 && value.length <= MAX_SESSION_ID_CHARS$6;
3820
+ return value.length > 0 && value.length <= MAX_SESSION_ID_CHARS$7;
3338
3821
  }
3339
3822
  function targetFromUrl(url) {
3340
3823
  const prefix = `${CLAUDE_PROJECTION_PATH}/`;
@@ -3676,14 +4159,14 @@ function parseGitHubRemote(value) {
3676
4159
  if (match?.[1] === void 0 || match[2] === void 0) return void 0;
3677
4160
  return `${match[1]}/${match[2]}`;
3678
4161
  }
3679
- function record$10(value) {
4162
+ function record$11(value) {
3680
4163
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3681
4164
  }
3682
4165
  function aggregateChecks(value) {
3683
4166
  if (!Array.isArray(value) || value.length === 0) return "none";
3684
4167
  let pending = false;
3685
4168
  for (const item of value) {
3686
- const check = record$10(item);
4169
+ const check = record$11(item);
3687
4170
  if (check === void 0) continue;
3688
4171
  const conclusion = typeof check.conclusion === "string" ? check.conclusion.toUpperCase() : void 0;
3689
4172
  const status = typeof check.status === "string" ? check.status.toUpperCase() : void 0;
@@ -3707,7 +4190,7 @@ function reviewState(value) {
3707
4190
  return "none";
3708
4191
  }
3709
4192
  function parsePullRequest(value) {
3710
- const input = record$10(value);
4193
+ const input = record$11(value);
3711
4194
  if (input === void 0 || !Number.isSafeInteger(input.number) || Number(input.number) <= 0 || typeof input.title !== "string" || typeof input.url !== "string") return void 0;
3712
4195
  let url;
3713
4196
  try {
@@ -3728,7 +4211,7 @@ function parsePullRequest(value) {
3728
4211
  review: reviewState(input.reviewDecision),
3729
4212
  checks: aggregateChecks(input.statusCheckRollup),
3730
4213
  ...typeof input.mergeStateStatus === "string" ? { mergeState: bounded(input.mergeStateStatus) } : {},
3731
- ...typeof record$10(input.author)?.login === "string" ? { author: bounded(String(record$10(input.author)?.login)) } : {},
4214
+ ...typeof record$11(input.author)?.login === "string" ? { author: bounded(String(record$11(input.author)?.login)) } : {},
3732
4215
  ...typeof input.createdAt === "string" && Number.isFinite(Date.parse(input.createdAt)) ? { createdAt: new Date(input.createdAt).toISOString() } : {},
3733
4216
  ...typeof input.mergedAt === "string" && Number.isFinite(Date.parse(input.mergedAt)) ? { mergedAt: new Date(input.mergedAt).toISOString() } : {},
3734
4217
  ...typeof input.baseRefName === "string" && bounded(input.baseRefName).length > 0 ? { baseBranch: bounded(input.baseRefName) } : {}
@@ -4164,6 +4647,22 @@ function parseWorktreeBranches(value) {
4164
4647
  function slug(value, fallback) {
4165
4648
  return value.toLocaleLowerCase("en-US").replace(/[^a-z0-9._-]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 48) || fallback;
4166
4649
  }
4650
+ /** A branch name as one path segment: `/` and anything else unsafe folds to
4651
+ * `-`, case is kept so a ticket key stays scannable (`PSOS-5683`, not
4652
+ * `psos-5683`). Flattened rather than nested on purpose -- see
4653
+ * {@link worktreeDirectoryName}. */
4654
+ function branchSegment(branch) {
4655
+ return branch.replace(/[^A-Za-z0-9._-]+/gu, "-").replace(/^[-.]+/u, "").replace(/-+$/u, "").slice(0, 48).replace(/-+$/u, "") || "branch";
4656
+ }
4657
+ /** Name a worktree directory after the repository and the branch it holds, so
4658
+ * the workspace list reads without opening a session.
4659
+ *
4660
+ * One flat segment, never a `feature/x` subdirectory: the orphan sweep lists
4661
+ * this root one level deep, and a prefix directory holds no lease of its own,
4662
+ * so it would be removed along with every live worktree inside it. */
4663
+ function worktreeDirectoryName(root, branch) {
4664
+ return `${slug(basename(root), "repository")}-${branchSegment(branch)}`;
4665
+ }
4167
4666
  /** Comparable form for path identity: resolved, forward slashes, case-folded
4168
4667
  * so Windows drive-letter or case spelling differences cannot hide a match. */
4169
4668
  function comparablePath(value) {
@@ -4463,6 +4962,18 @@ var RepositorySetupService = class {
4463
4962
  ], root, GIT_FETCH_TIMEOUT_MS);
4464
4963
  if (fetched.exitCode !== 0 || fetched.lossy) throw new RepositorySetupError("fetch-failed", "Git could not refresh remote references.");
4465
4964
  }
4965
+ /** The remote-tracking ref a local base branch follows, when that ref still
4966
+ * exists. Falls back to the local branch for a branch with no upstream. */
4967
+ async #upstreamRef(git, info, branch) {
4968
+ const upstream = await this.#run(git, [
4969
+ "for-each-ref",
4970
+ "--format=%(upstream:short)",
4971
+ `refs/heads/${branch}`
4972
+ ], info.root);
4973
+ if (upstream.exitCode !== 0 || upstream.lossy) return void 0;
4974
+ const name = upstream.stdout.trim();
4975
+ return name.length > 0 && info.remoteBranches.includes(name) ? `refs/remotes/${name}` : void 0;
4976
+ }
4466
4977
  async #checkout(info, branch) {
4467
4978
  if (info.current !== branch) {
4468
4979
  if (info.dirty) throw new RepositorySetupError("dirty-workspace", "Commit or stash workspace changes before switching branches.");
@@ -4493,10 +5004,11 @@ var RepositorySetupService = class {
4493
5004
  const git = await this.#git();
4494
5005
  progress("fetching");
4495
5006
  await this.#fetchRemotes(git, root);
5007
+ const startRef = reuseExistingBranch ? baseRef : await this.#upstreamRef(git, info, baseBranch) ?? baseRef;
4496
5008
  const suffix = randomUUID().slice(0, 8);
4497
5009
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/gu, "").replace(/\.\d{3}Z$/u, "Z");
4498
5010
  const branch = explicitBranchName ?? await this.#generatedBranch(info, baseBranch, intent, stamp, suffix, progress);
4499
- const path = join(this.#worktreeRoot, `${slug(basename(root), "repository")}-${stamp}-${suffix}`);
5011
+ const path = join(this.#worktreeRoot, await this.#freeDirectoryName(root, branch));
4500
5012
  progress("creating-worktree");
4501
5013
  await mkdir(this.#worktreeRoot, { recursive: true });
4502
5014
  if (reuseExistingBranch) await this.#run(git, ["worktree", "prune"], root).catch(() => void 0);
@@ -4512,7 +5024,7 @@ var RepositorySetupService = class {
4512
5024
  "-b",
4513
5025
  branch,
4514
5026
  path,
4515
- baseRef
5027
+ startRef
4516
5028
  ], root);
4517
5029
  if (created.exitCode !== 0) {
4518
5030
  const detail = created.stderr.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0).at(-1);
@@ -4559,6 +5071,21 @@ var RepositorySetupService = class {
4559
5071
  /** `<prefix>/<what the draft is about>`, falling back to
4560
5072
  * `<prefix>/<base branch>-<stamp>-<random>` whenever the summary is missing
4561
5073
  * or unusable. The prefix and the fallback shape are unchanged. */
5074
+ /** The branch-named directory, or the first `-2`, `-3`, ... spelling free of
5075
+ * anything already on disk. Names no longer carry a timestamp, so a stale
5076
+ * directory a crash left behind -- or two branches that fold to the same
5077
+ * segment -- would otherwise fail `worktree add` outright. Compared
5078
+ * case-folded, since a case-insensitive filesystem would collide anyway. */
5079
+ async #freeDirectoryName(root, branch) {
5080
+ const base = worktreeDirectoryName(root, branch);
5081
+ const taken = await readdir(this.#worktreeRoot).then((entries) => new Set(entries.map((entry) => entry.toLocaleLowerCase("en-US"))), () => /* @__PURE__ */ new Set());
5082
+ if (!taken.has(base.toLocaleLowerCase("en-US"))) return base;
5083
+ for (let index = 2; index < 100; index += 1) {
5084
+ const candidate = `${base}-${index}`;
5085
+ if (!taken.has(candidate.toLocaleLowerCase("en-US"))) return candidate;
5086
+ }
5087
+ return base;
5088
+ }
4562
5089
  async #generatedBranch(info, baseBranch, intent, stamp, suffix, progress) {
4563
5090
  const prefix = safeBranch(await this.#branchPrefix());
4564
5091
  if (intent !== void 0 && intent.trim().length > 0) {
@@ -5091,19 +5618,19 @@ var RepositoryActionService = class {
5091
5618
  };
5092
5619
  //#endregion
5093
5620
  //#region src/repository-setup-routes.ts
5094
- const MAX_BODY_BYTES$6 = 16384;
5095
- function record$9(value) {
5621
+ const MAX_BODY_BYTES$7 = 16384;
5622
+ function record$10(value) {
5096
5623
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
5097
5624
  }
5098
- async function readJson$5(io) {
5625
+ async function readJson$6(io) {
5099
5626
  let parsed;
5100
5627
  try {
5101
- parsed = await io.body(MAX_BODY_BYTES$6);
5628
+ parsed = await io.body(MAX_BODY_BYTES$7);
5102
5629
  } catch (error) {
5103
5630
  if (error instanceof SyntaxError) throw error;
5104
5631
  throw new RepositorySetupError("body-too-large", "The request body is too large.");
5105
5632
  }
5106
- const value = record$9(parsed);
5633
+ const value = record$10(parsed);
5107
5634
  if (value === void 0) throw new RepositorySetupError("invalid-request", "The request body is invalid.");
5108
5635
  return value;
5109
5636
  }
@@ -5172,7 +5699,7 @@ function registerRepositorySetupRoute(ctx, service, sweep) {
5172
5699
  try {
5173
5700
  if (pathname === `/plugins/dsh-claude/repository/setup/branches/refresh`) {
5174
5701
  if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
5175
- const input = await readJson$5(io);
5702
+ const input = await readJson$6(io);
5176
5703
  return json(res, 200, await service.refreshBranches(string$2(input, "cwd")));
5177
5704
  }
5178
5705
  if (pathname === `/plugins/dsh-claude/repository/setup/branches`) {
@@ -5183,14 +5710,14 @@ function registerRepositorySetupRoute(ctx, service, sweep) {
5183
5710
  }
5184
5711
  if (pathname === "/plugins/dsh-claude/repository/setup") {
5185
5712
  if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
5186
- const input = await readJson$5(io);
5713
+ const input = await readJson$6(io);
5187
5714
  if (typeof input.worktree !== "boolean") throw new RepositorySetupError("invalid-request", "The worktree field is required.");
5188
5715
  await streamSetup(res, service, input);
5189
5716
  return;
5190
5717
  }
5191
5718
  if (pathname === `/plugins/dsh-claude/repository/setup/cleanup`) {
5192
5719
  if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
5193
- const input = await readJson$5(io);
5720
+ const input = await readJson$6(io);
5194
5721
  return json(res, 200, await service.cleanupMerged(string$2(input, "path"), string$2(input, "baseBranch")));
5195
5722
  }
5196
5723
  if (pathname === `/plugins/dsh-claude/repository/setup/sweep`) {
@@ -5200,7 +5727,7 @@ function registerRepositorySetupRoute(ctx, service, sweep) {
5200
5727
  }
5201
5728
  if (pathname === `/plugins/dsh-claude/repository/setup/bind`) {
5202
5729
  if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
5203
- const input = await readJson$5(io);
5730
+ const input = await readJson$6(io);
5204
5731
  await service.bindLease(string$2(input, "leaseId"), string$2(input, "sessionId"));
5205
5732
  return json(res, 200, { ok: true });
5206
5733
  }
@@ -5218,8 +5745,8 @@ function registerRepositorySetupRoute(ctx, service, sweep) {
5218
5745
  }
5219
5746
  //#endregion
5220
5747
  //#region src/repository-action-routes.ts
5221
- const MAX_BODY_BYTES$5 = 16384;
5222
- const MAX_SESSION_ID_CHARS$5 = 1024;
5748
+ const MAX_BODY_BYTES$6 = 16384;
5749
+ const MAX_SESSION_ID_CHARS$6 = 1024;
5223
5750
  const ACTIONS = /* @__PURE__ */ new Set([
5224
5751
  "commit",
5225
5752
  "commit-push",
@@ -5228,24 +5755,24 @@ const ACTIONS = /* @__PURE__ */ new Set([
5228
5755
  "merge-pr",
5229
5756
  "update-branch"
5230
5757
  ]);
5231
- function record$8(value) {
5758
+ function record$9(value) {
5232
5759
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
5233
5760
  }
5234
- async function readJson$4(io) {
5761
+ async function readJson$5(io) {
5235
5762
  let parsed;
5236
5763
  try {
5237
- parsed = await io.body(MAX_BODY_BYTES$5);
5764
+ parsed = await io.body(MAX_BODY_BYTES$6);
5238
5765
  } catch (error) {
5239
5766
  if (error instanceof SyntaxError) throw error;
5240
5767
  throw new RepositoryActionError("body-too-large", "The request body is too large.");
5241
5768
  }
5242
- const value = record$8(parsed);
5769
+ const value = record$9(parsed);
5243
5770
  if (value === void 0) throw new RepositoryActionError("invalid-request", "The request body is invalid.");
5244
5771
  return value;
5245
5772
  }
5246
5773
  function sessionId$1(url) {
5247
5774
  const value = url.searchParams.get("sessionId");
5248
- if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$5) throw new RepositoryActionError("invalid-session", "The session is invalid.");
5775
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$6) throw new RepositoryActionError("invalid-session", "The session is invalid.");
5249
5776
  return value;
5250
5777
  }
5251
5778
  function string$1(input, key) {
@@ -5309,7 +5836,7 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
5309
5836
  status: 405,
5310
5837
  value: { error: "method not allowed" }
5311
5838
  };
5312
- const input = await readJson$4(io);
5839
+ const input = await readJson$5(io);
5313
5840
  return {
5314
5841
  status: 200,
5315
5842
  value: { message: await service.generateMessage(cwd, string$1(input, "fingerprint")) }
@@ -5322,7 +5849,7 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
5322
5849
  };
5323
5850
  return {
5324
5851
  status: 200,
5325
- value: await service.execute(cwd, actionRequest(await readJson$4(io)))
5852
+ value: await service.execute(cwd, actionRequest(await readJson$5(io)))
5326
5853
  };
5327
5854
  }
5328
5855
  return {
@@ -5473,7 +6000,7 @@ var EditorOpenService = class {
5473
6000
  };
5474
6001
  //#endregion
5475
6002
  //#region src/editor-open-routes.ts
5476
- const MAX_SESSION_ID_CHARS$4 = 1024;
6003
+ const MAX_SESSION_ID_CHARS$5 = 1024;
5477
6004
  /** Open the session's working directory in a desktop editor. Query-only: the
5478
6005
  * request carries two enum-ish values, so there is no body to parse. */
5479
6006
  function registerEditorOpenRoute(ctx, service, cwdForSession) {
@@ -5487,7 +6014,7 @@ function registerEditorOpenRoute(ctx, service, cwdForSession) {
5487
6014
  const params = io.url.searchParams;
5488
6015
  const id = params.get("sessionId");
5489
6016
  const editor = params.get("editor");
5490
- if (id === null || id.length === 0 || id.length > MAX_SESSION_ID_CHARS$4) return {
6017
+ if (id === null || id.length === 0 || id.length > MAX_SESSION_ID_CHARS$5) return {
5491
6018
  status: 400,
5492
6019
  value: {
5493
6020
  error: "invalid-session",
@@ -5602,7 +6129,7 @@ async function collect(handle) {
5602
6129
  lossy: stdout?.lossy === true
5603
6130
  };
5604
6131
  }
5605
- function record$7(value) {
6132
+ function record$8(value) {
5606
6133
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
5607
6134
  }
5608
6135
  /** Read `repository.pullRequest.reviewThreads.nodes` out of a GraphQL response.
@@ -5610,12 +6137,12 @@ function record$7(value) {
5610
6137
  * throwing: the caller distinguishes "no threads" from "call failed" by the
5611
6138
  * process exit code. */
5612
6139
  function parseReviewThreads(value) {
5613
- const nodes = record$7(record$7(record$7(record$7(record$7(value)?.data)?.repository)?.pullRequest)?.reviewThreads)?.nodes;
6140
+ const nodes = record$8(record$8(record$8(record$8(record$8(value)?.data)?.repository)?.pullRequest)?.reviewThreads)?.nodes;
5614
6141
  if (!Array.isArray(nodes)) return [];
5615
6142
  const threads = [];
5616
6143
  let total = 0;
5617
6144
  for (const item of nodes) {
5618
- const input = record$7(item);
6145
+ const input = record$8(item);
5619
6146
  if (input === void 0 || typeof input.id !== "string" || input.id.length === 0) continue;
5620
6147
  const path = typeof input.path === "string" ? input.path : "";
5621
6148
  if (path.length === 0) continue;
@@ -5627,14 +6154,14 @@ function parseReviewThreads(value) {
5627
6154
  side
5628
6155
  };
5629
6156
  const comments = [];
5630
- const commentNodes = record$7(input.comments)?.nodes;
6157
+ const commentNodes = record$8(input.comments)?.nodes;
5631
6158
  for (const node of Array.isArray(commentNodes) ? commentNodes : []) {
5632
6159
  if (total >= MAX_COMMENTS) break;
5633
- const comment = record$7(node);
6160
+ const comment = record$8(node);
5634
6161
  if (comment === void 0 || !Number.isSafeInteger(comment.databaseId)) continue;
5635
6162
  const body = typeof comment.body === "string" ? comment.body.trim() : "";
5636
6163
  if (body.length === 0) continue;
5637
- const author = record$7(comment.author);
6164
+ const author = record$8(comment.author);
5638
6165
  const avatarUrl = githubAvatarUrl(author?.avatarUrl);
5639
6166
  const login = typeof author?.login === "string" ? author.login : "unknown";
5640
6167
  comments.push({
@@ -5663,11 +6190,11 @@ function parseReviewThreads(value) {
5663
6190
  }
5664
6191
  /** One posted reply, shaped like the thread comments it joins. */
5665
6192
  function parseReplyComment(value, anchor) {
5666
- const input = record$7(value);
6193
+ const input = record$8(value);
5667
6194
  if (input === void 0 || !Number.isSafeInteger(input.id)) return void 0;
5668
6195
  const body = typeof input.body === "string" ? input.body.trim() : "";
5669
6196
  if (body.length === 0) return void 0;
5670
- const user = record$7(input.user);
6197
+ const user = record$8(input.user);
5671
6198
  const avatarUrl = githubAvatarUrl(user?.avatar_url);
5672
6199
  const login = typeof user?.login === "string" ? user.login : "unknown";
5673
6200
  return {
@@ -5682,11 +6209,11 @@ function parseReplyComment(value, anchor) {
5682
6209
  };
5683
6210
  }
5684
6211
  function parseMentionableUsers(value) {
5685
- const nodes = record$7(record$7(record$7(record$7(value)?.data)?.repository)?.mentionableUsers)?.nodes;
6212
+ const nodes = record$8(record$8(record$8(record$8(value)?.data)?.repository)?.mentionableUsers)?.nodes;
5686
6213
  if (!Array.isArray(nodes)) return [];
5687
6214
  const users = [];
5688
6215
  for (const item of nodes) {
5689
- const input = record$7(item);
6216
+ const input = record$8(item);
5690
6217
  if (input === void 0 || typeof input.login !== "string" || input.login.length === 0) continue;
5691
6218
  const avatarUrl = githubAvatarUrl(input.avatarUrl);
5692
6219
  users.push({
@@ -5706,7 +6233,7 @@ function parseFailingChecks(value) {
5706
6233
  if (!Array.isArray(value)) return [];
5707
6234
  const failing = [];
5708
6235
  for (const item of value) {
5709
- const input = record$7(item);
6236
+ const input = record$8(item);
5710
6237
  if (input === void 0 || input.bucket !== "fail" || typeof input.name !== "string") continue;
5711
6238
  failing.push({
5712
6239
  name: input.name,
@@ -5769,12 +6296,12 @@ var PullRequestFeedbackService = class {
5769
6296
  let posted;
5770
6297
  try {
5771
6298
  const parsed = JSON.parse(result.stdout);
5772
- const path = typeof record$7(parsed)?.path === "string" ? String(record$7(parsed)?.path) : "";
5773
- const line = Number.isSafeInteger(record$7(parsed)?.line) ? Number(record$7(parsed)?.line) : void 0;
6299
+ const path = typeof record$8(parsed)?.path === "string" ? String(record$8(parsed)?.path) : "";
6300
+ const line = Number.isSafeInteger(record$8(parsed)?.line) ? Number(record$8(parsed)?.line) : void 0;
5774
6301
  posted = parseReplyComment(parsed, {
5775
6302
  path,
5776
6303
  ...line === void 0 ? {} : { line },
5777
- side: record$7(parsed)?.side === "LEFT" ? "old" : "new"
6304
+ side: record$8(parsed)?.side === "LEFT" ? "old" : "new"
5778
6305
  });
5779
6306
  } catch {
5780
6307
  posted = void 0;
@@ -5795,7 +6322,7 @@ var PullRequestFeedbackService = class {
5795
6322
  ], cwd, GH_TIMEOUT_MS);
5796
6323
  if (result.exitCode !== 0) throw new PullRequestFeedbackError("resolve-failed", "The thread could not be updated.");
5797
6324
  try {
5798
- const thread = record$7(record$7(record$7(record$7(JSON.parse(result.stdout))?.data)?.[resolved ? "resolveReviewThread" : "unresolveReviewThread"])?.thread);
6325
+ const thread = record$8(record$8(record$8(record$8(JSON.parse(result.stdout))?.data)?.[resolved ? "resolveReviewThread" : "unresolveReviewThread"])?.thread);
5799
6326
  if (typeof thread?.isResolved !== "boolean") throw new Error("missing state");
5800
6327
  return thread.isResolved;
5801
6328
  } catch {
@@ -5918,23 +6445,23 @@ var PullRequestFeedbackService = class {
5918
6445
  };
5919
6446
  //#endregion
5920
6447
  //#region src/pr-feedback-routes.ts
5921
- const MAX_SESSION_ID_CHARS$3 = 1024;
5922
- const MAX_BODY_BYTES$4 = 16384;
6448
+ const MAX_SESSION_ID_CHARS$4 = 1024;
6449
+ const MAX_BODY_BYTES$5 = 16384;
5923
6450
  const MAX_REPLY_CHARS = 2e3;
5924
6451
  const MAX_THREAD_ID_CHARS = 512;
5925
6452
  const MAX_MENTION_QUERY_CHARS = 64;
5926
- function record$6(value) {
6453
+ function record$7(value) {
5927
6454
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
5928
6455
  }
5929
- async function readJson$3(io) {
6456
+ async function readJson$4(io) {
5930
6457
  let parsed;
5931
6458
  try {
5932
- parsed = await io.body(MAX_BODY_BYTES$4);
6459
+ parsed = await io.body(MAX_BODY_BYTES$5);
5933
6460
  } catch (error) {
5934
6461
  if (error instanceof SyntaxError) throw error;
5935
6462
  throw new PullRequestFeedbackError("body-too-large", "The request body is too large.");
5936
6463
  }
5937
- const value = record$6(parsed);
6464
+ const value = record$7(parsed);
5938
6465
  if (value === void 0) throw new PullRequestFeedbackError("invalid-request", "The request body is invalid.");
5939
6466
  return value;
5940
6467
  }
@@ -5955,7 +6482,7 @@ function threadId(input) {
5955
6482
  }
5956
6483
  function sessionId(url) {
5957
6484
  const value = url.searchParams.get("sessionId");
5958
- if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$3) throw new PullRequestFeedbackError("invalid-session", "The session is invalid.");
6485
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$4) throw new PullRequestFeedbackError("invalid-session", "The session is invalid.");
5959
6486
  return value;
5960
6487
  }
5961
6488
  function pullNumber(url) {
@@ -6014,7 +6541,7 @@ function registerPullRequestFeedbackRoute(ctx, service, cwdForSession) {
6014
6541
  status: 405,
6015
6542
  value: { error: "method not allowed" }
6016
6543
  };
6017
- const input = await readJson$3(io);
6544
+ const input = await readJson$4(io);
6018
6545
  return {
6019
6546
  status: 200,
6020
6547
  value: { comment: await service.reply(cwd, pullNumber(url), commentId(input), replyBody(input)) }
@@ -6025,7 +6552,7 @@ function registerPullRequestFeedbackRoute(ctx, service, cwdForSession) {
6025
6552
  status: 405,
6026
6553
  value: { error: "method not allowed" }
6027
6554
  };
6028
- const input = await readJson$3(io);
6555
+ const input = await readJson$4(io);
6029
6556
  if (typeof input.resolved !== "boolean") throw new PullRequestFeedbackError("invalid-request", "The resolved field is required.");
6030
6557
  return {
6031
6558
  status: 200,
@@ -6171,7 +6698,7 @@ var JiraError = class extends Error {
6171
6698
  this.code = code;
6172
6699
  }
6173
6700
  };
6174
- function record$5(value) {
6701
+ function record$6(value) {
6175
6702
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
6176
6703
  }
6177
6704
  /** Jira Cloud sites are origins; Data Center may carry a context path. */
@@ -6195,14 +6722,14 @@ function ticketKeyOf(query) {
6195
6722
  * search and does match keys; its hits become the key filter the normal
6196
6723
  * search then reads the display fields from. */
6197
6724
  function pickerKeys(value, number) {
6198
- const sections = record$5(value)?.sections;
6725
+ const sections = record$6(value)?.sections;
6199
6726
  if (!Array.isArray(sections)) return [];
6200
6727
  const keys = [];
6201
6728
  for (const section of sections) {
6202
- const issues = record$5(section)?.issues;
6729
+ const issues = record$6(section)?.issues;
6203
6730
  if (!Array.isArray(issues)) continue;
6204
6731
  for (const issue of issues) {
6205
- const raw = record$5(issue)?.key;
6732
+ const raw = record$6(issue)?.key;
6206
6733
  const key = ticketKeyOf(typeof raw === "string" ? raw : "");
6207
6734
  if (key !== void 0 && key.endsWith(`-${number}`) && !keys.includes(key)) keys.push(key);
6208
6735
  }
@@ -6221,15 +6748,15 @@ function buildJql(query) {
6221
6748
  return `text ~ "${trimmed.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}*" ORDER BY updated DESC`;
6222
6749
  }
6223
6750
  function parseTickets(value, siteUrl) {
6224
- const issues = record$5(value)?.issues;
6751
+ const issues = record$6(value)?.issues;
6225
6752
  if (!Array.isArray(issues)) return [];
6226
6753
  const tickets = [];
6227
6754
  for (const item of issues) {
6228
- const issue = record$5(item);
6229
- const fields = record$5(issue?.fields);
6755
+ const issue = record$6(item);
6756
+ const fields = record$6(issue?.fields);
6230
6757
  if (issue === void 0 || typeof issue.key !== "string" || fields === void 0) continue;
6231
- const status = record$5(fields.status)?.name;
6232
- const type = record$5(fields.issuetype)?.name;
6758
+ const status = record$6(fields.status)?.name;
6759
+ const type = record$6(fields.issuetype)?.name;
6233
6760
  tickets.push({
6234
6761
  key: issue.key,
6235
6762
  summary: typeof fields.summary === "string" ? fields.summary.slice(0, 256) : "",
@@ -6271,7 +6798,7 @@ var JiraService = class {
6271
6798
  email,
6272
6799
  apiToken
6273
6800
  };
6274
- const myself = record$5(await this.#json(connection, "/rest/api/3/myself"));
6801
+ const myself = record$6(await this.#json(connection, "/rest/api/3/myself"));
6275
6802
  const displayName = typeof myself?.displayName === "string" ? myself.displayName : void 0;
6276
6803
  const accountId = typeof myself?.accountId === "string" ? myself.accountId : void 0;
6277
6804
  const store = {
@@ -6290,7 +6817,7 @@ var JiraService = class {
6290
6817
  if (ticket === void 0) throw new JiraError("invalid-request", "The ticket key is invalid.");
6291
6818
  let accountId = store.accountId;
6292
6819
  if (accountId === void 0) {
6293
- const myself = record$5(await this.#json(store, "/rest/api/3/myself"));
6820
+ const myself = record$6(await this.#json(store, "/rest/api/3/myself"));
6294
6821
  if (typeof myself?.accountId !== "string") throw new JiraError("jira-failed", "The Jira account id is unavailable.");
6295
6822
  accountId = myself.accountId;
6296
6823
  await this.#write({
@@ -6366,7 +6893,7 @@ var JiraService = class {
6366
6893
  throw error;
6367
6894
  }
6368
6895
  if (Buffer.byteLength(text) > MAX_STORE_BYTES) return void 0;
6369
- const input = record$5(JSON.parse(text));
6896
+ const input = record$6(JSON.parse(text));
6370
6897
  if (input === void 0 || typeof input.siteUrl !== "string" || typeof input.email !== "string" || typeof input.apiToken !== "string") return void 0;
6371
6898
  return {
6372
6899
  siteUrl: input.siteUrl,
@@ -6397,21 +6924,21 @@ var JiraService = class {
6397
6924
  };
6398
6925
  //#endregion
6399
6926
  //#region src/jira-routes.ts
6400
- const MAX_BODY_BYTES$3 = 8192;
6401
- function record$4(value) {
6927
+ const MAX_BODY_BYTES$4 = 8192;
6928
+ function record$5(value) {
6402
6929
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
6403
6930
  }
6404
6931
  /** The wrapper enforces the byte cap; its plain rejection is translated back
6405
6932
  * into the JiraError shape the panel already knows how to render. */
6406
- async function readJson$2(io) {
6933
+ async function readJson$3(io) {
6407
6934
  let body;
6408
6935
  try {
6409
- body = await io.body(MAX_BODY_BYTES$3);
6936
+ body = await io.body(MAX_BODY_BYTES$4);
6410
6937
  } catch (error) {
6411
6938
  if (error instanceof SyntaxError) throw error;
6412
6939
  throw new JiraError("body-too-large", "The request body is too large.");
6413
6940
  }
6414
- const value = record$4(body);
6941
+ const value = record$5(body);
6415
6942
  if (value === void 0) throw new JiraError("invalid-request", "The request body is invalid.");
6416
6943
  return value;
6417
6944
  }
@@ -6445,7 +6972,7 @@ function registerJiraRoute(ctx, service) {
6445
6972
  status: 405,
6446
6973
  value: { error: "method not allowed" }
6447
6974
  };
6448
- const input = await readJson$2(io);
6975
+ const input = await readJson$3(io);
6449
6976
  return {
6450
6977
  status: 200,
6451
6978
  value: await service.connect({
@@ -6471,7 +6998,7 @@ function registerJiraRoute(ctx, service) {
6471
6998
  status: 405,
6472
6999
  value: { error: "method not allowed" }
6473
7000
  };
6474
- const input = await readJson$2(io);
7001
+ const input = await readJson$3(io);
6475
7002
  await service.assignToMe(string(input, "key"));
6476
7003
  return {
6477
7004
  status: 200,
@@ -6590,12 +7117,12 @@ function askArguments(preferences) {
6590
7117
  ...READ_ONLY_TOOLS
6591
7118
  ];
6592
7119
  }
6593
- function record$3(value) {
7120
+ function record$4(value) {
6594
7121
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
6595
7122
  }
6596
7123
  /** One-line description of a tool call, mirroring the main window's step titles. */
6597
7124
  function toolSummary(input) {
6598
- const fields = record$3(input);
7125
+ const fields = record$4(input);
6599
7126
  if (fields === void 0) return void 0;
6600
7127
  const candidate = [
6601
7128
  fields.command,
@@ -6609,7 +7136,7 @@ function toolSummary(input) {
6609
7136
  function eventsOfStreamLine(line) {
6610
7137
  let parsed;
6611
7138
  try {
6612
- parsed = record$3(JSON.parse(line));
7139
+ parsed = record$4(JSON.parse(line));
6613
7140
  } catch {
6614
7141
  return [];
6615
7142
  }
@@ -6619,9 +7146,9 @@ function eventsOfStreamLine(line) {
6619
7146
  text: "ready"
6620
7147
  }];
6621
7148
  if (parsed.type === "stream_event") {
6622
- const event = record$3(parsed.event);
7149
+ const event = record$4(parsed.event);
6623
7150
  if (event?.type === "content_block_start") {
6624
- const block = record$3(event.content_block);
7151
+ const block = record$4(event.content_block);
6625
7152
  if (block?.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") return [{
6626
7153
  type: "tool",
6627
7154
  id: block.id,
@@ -6630,7 +7157,7 @@ function eventsOfStreamLine(line) {
6630
7157
  }];
6631
7158
  return [];
6632
7159
  }
6633
- const delta = record$3(event?.delta);
7160
+ const delta = record$4(event?.delta);
6634
7161
  if (event?.type !== "content_block_delta" || delta === void 0) return [];
6635
7162
  if (delta.type === "text_delta" && typeof delta.text === "string") return [{
6636
7163
  type: "text",
@@ -6643,11 +7170,11 @@ function eventsOfStreamLine(line) {
6643
7170
  return [];
6644
7171
  }
6645
7172
  if (parsed.type === "assistant" || parsed.type === "user") {
6646
- const content = record$3(parsed.message)?.content;
7173
+ const content = record$4(parsed.message)?.content;
6647
7174
  if (!Array.isArray(content)) return [];
6648
7175
  const events = [];
6649
7176
  for (const item of content) {
6650
- const block = record$3(item);
7177
+ const block = record$4(item);
6651
7178
  if (block?.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
6652
7179
  const summary = toolSummary(block.input);
6653
7180
  events.push({
@@ -6739,11 +7266,11 @@ var AskService = class {
6739
7266
  };
6740
7267
  //#endregion
6741
7268
  //#region src/ask-routes.ts
6742
- const MAX_BODY_BYTES$2 = 131072;
6743
- const MAX_SESSION_ID_CHARS$2 = 1024;
7269
+ const MAX_BODY_BYTES$3 = 131072;
7270
+ const MAX_SESSION_ID_CHARS$3 = 1024;
6744
7271
  /** Two sessions may await an answer at once; a third evicts the oldest. */
6745
7272
  const MAX_CONCURRENT_ASKS = 2;
6746
- function record$2(value) {
7273
+ function record$3(value) {
6747
7274
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
6748
7275
  }
6749
7276
  function askRequest(input) {
@@ -6775,12 +7302,12 @@ function registerAskRoute(ctx, service, cwdForSession, preferencesFor) {
6775
7302
  let sessionId;
6776
7303
  try {
6777
7304
  const value = io.url.searchParams.get("sessionId");
6778
- if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$2) throw new AskError("invalid-session", "The session is invalid.");
7305
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$3) throw new AskError("invalid-session", "The session is invalid.");
6779
7306
  sessionId = value;
6780
7307
  const resolved = cwdForSession(sessionId);
6781
7308
  if (resolved === void 0) throw new AskError("session-unavailable", "The Claude session is unavailable.");
6782
7309
  cwd = resolved;
6783
- const body = record$2(await io.body(MAX_BODY_BYTES$2));
7310
+ const body = record$3(await io.body(MAX_BODY_BYTES$3));
6784
7311
  if (body === void 0) throw new AskError("invalid-request", "The request body is invalid.");
6785
7312
  request = askRequest(body);
6786
7313
  } catch (error) {
@@ -6820,26 +7347,26 @@ function registerAskRoute(ctx, service, cwdForSession, preferencesFor) {
6820
7347
  }
6821
7348
  //#endregion
6822
7349
  //#region src/review-comment-routes.ts
6823
- const MAX_BODY_BYTES$1 = 16384;
6824
- const MAX_SESSION_ID_CHARS$1 = 1024;
6825
- function record$1(value) {
7350
+ const MAX_BODY_BYTES$2 = 16384;
7351
+ const MAX_SESSION_ID_CHARS$2 = 1024;
7352
+ function record$2(value) {
6826
7353
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
6827
7354
  }
6828
- async function readJson$1(io) {
7355
+ async function readJson$2(io) {
6829
7356
  let parsed;
6830
7357
  try {
6831
- parsed = await io.body(MAX_BODY_BYTES$1);
7358
+ parsed = await io.body(MAX_BODY_BYTES$2);
6832
7359
  } catch (error) {
6833
7360
  if (error instanceof SyntaxError) throw error;
6834
7361
  throw new ReviewCommentError("body-too-large", "The request body is too large.");
6835
7362
  }
6836
- const value = record$1(parsed);
7363
+ const value = record$2(parsed);
6837
7364
  if (value === void 0) throw new ReviewCommentError("invalid-request", "The request body is invalid.");
6838
7365
  return value;
6839
7366
  }
6840
7367
  function sessionIdFromUrl(url) {
6841
7368
  const value = url.searchParams.get("sessionId");
6842
- if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$1) throw new ReviewCommentError("invalid-session", "The session is invalid.");
7369
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$2) throw new ReviewCommentError("invalid-session", "The session is invalid.");
6843
7370
  return value;
6844
7371
  }
6845
7372
  function registerReviewCommentRoute(ctx, store, ownsSession) {
@@ -6855,7 +7382,7 @@ function registerReviewCommentRoute(ctx, store, ownsSession) {
6855
7382
  if (!ownsSession(sessionId)) throw new ReviewCommentError("session-unavailable", "The Claude session is unavailable.");
6856
7383
  const pathname = io.url.pathname;
6857
7384
  if (pathname === "/plugins/dsh-claude/review-comments") {
6858
- const input = await readJson$1(io);
7385
+ const input = await readJson$2(io);
6859
7386
  return {
6860
7387
  status: 200,
6861
7388
  value: { comment: store.add(sessionId, {
@@ -6872,7 +7399,7 @@ function registerReviewCommentRoute(ctx, store, ownsSession) {
6872
7399
  value: { removed: store.drain(sessionId).length }
6873
7400
  };
6874
7401
  if (pathname === `/plugins/dsh-claude/review-comments/remove`) {
6875
- const input = await readJson$1(io);
7402
+ const input = await readJson$2(io);
6876
7403
  if (typeof input.id !== "string" || input.id.length === 0 || input.id.length > 128) throw new ReviewCommentError("invalid-request", "The comment id is invalid.");
6877
7404
  return {
6878
7405
  status: 200,
@@ -6907,6 +7434,85 @@ function registerReviewCommentRoute(ctx, store, ownsSession) {
6907
7434
  });
6908
7435
  }
6909
7436
  //#endregion
7437
+ //#region src/plan-feedback-routes.ts
7438
+ const MAX_BODY_BYTES$1 = 65536;
7439
+ const MAX_SESSION_ID_CHARS$1 = 1024;
7440
+ const MAX_TOOL_USE_ID_CHARS = 256;
7441
+ function record$1(value) {
7442
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
7443
+ }
7444
+ async function readJson$1(io) {
7445
+ let parsed;
7446
+ try {
7447
+ parsed = await io.body(MAX_BODY_BYTES$1);
7448
+ } catch (error) {
7449
+ if (error instanceof SyntaxError) throw error;
7450
+ throw new PlanFeedbackError("body-too-large", "The request body is too large.");
7451
+ }
7452
+ const value = record$1(parsed);
7453
+ if (value === void 0) throw new PlanFeedbackError("invalid-request", "The request body is invalid.");
7454
+ return value;
7455
+ }
7456
+ /** Send one plan back for changes.
7457
+ *
7458
+ * Unary rather than a stream: the panel hands over what the reviewer wrote
7459
+ * and the turn carries on in the transcript it was already watching. Answers
7460
+ * 409 when nothing is waiting, which is what the panel shows if the approval
7461
+ * dialog was answered while the reviewer was still typing. */
7462
+ function registerPlanFeedbackRoute(ctx, gate, ownsSession) {
7463
+ registerPluginRoute(ctx, {
7464
+ mode: "unary",
7465
+ budget: "fast",
7466
+ kind: "exact",
7467
+ path: CLAUDE_PLAN_FEEDBACK_PATH,
7468
+ methods: ["POST"],
7469
+ handler: async (io) => {
7470
+ try {
7471
+ const sessionId = io.url.searchParams.get("sessionId");
7472
+ if (sessionId === null || sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS$1) return {
7473
+ status: 400,
7474
+ value: { error: "invalid-session" }
7475
+ };
7476
+ if (!ownsSession(sessionId)) return {
7477
+ status: 409,
7478
+ value: { error: "session-unavailable" }
7479
+ };
7480
+ const body = await readJson$1(io);
7481
+ const toolUseId = body.toolUseId;
7482
+ if (typeof toolUseId !== "string" || toolUseId.length === 0 || toolUseId.length > MAX_TOOL_USE_ID_CHARS) return {
7483
+ status: 400,
7484
+ value: { error: "invalid-request" }
7485
+ };
7486
+ const notes = planNotesOf(body.notes);
7487
+ if (!gate.submit(toolUseId, notes)) return {
7488
+ status: 409,
7489
+ value: { error: "plan-settled" }
7490
+ };
7491
+ return {
7492
+ status: 200,
7493
+ value: { ok: true }
7494
+ };
7495
+ } catch (error) {
7496
+ if (error instanceof PlanFeedbackError) return {
7497
+ status: 400,
7498
+ value: {
7499
+ error: error.code,
7500
+ message: error.message
7501
+ }
7502
+ };
7503
+ if (error instanceof SyntaxError) return {
7504
+ status: 400,
7505
+ value: { error: "invalid-json" }
7506
+ };
7507
+ return {
7508
+ status: 500,
7509
+ value: { error: "plan-feedback-unavailable" }
7510
+ };
7511
+ }
7512
+ }
7513
+ });
7514
+ }
7515
+ //#endregion
6910
7516
  //#region src/client-diagnostics-routes.ts
6911
7517
  /** Enough for a message plus a trimmed stack; the client caps its own volume. */
6912
7518
  const MAX_DIAGNOSTIC_BYTES = 8192;
@@ -6971,8 +7577,15 @@ async function readJson(io) {
6971
7577
  return;
6972
7578
  }
6973
7579
  }
6974
- /** `POST <path>` with `{ sessionId, seq }`: hide that surface event and every
6975
- * later one, and arm Claude to resume before the turn it opened. */
7580
+ /** `POST <path>` with `{ sessionId, seq, restoreFiles? }`: hide that surface
7581
+ * event and every later one, and arm Claude to resume before the turn it
7582
+ * opened. With `restoreFiles`, the checkout is also put back to the tree that
7583
+ * turn was admitted against.
7584
+ *
7585
+ * The conversation rewind is what the user confirmed, so it lands first and
7586
+ * stands on its own; a checkout that cannot be restored — no snapshot, a
7587
+ * collected tree, no git — reports `filesRestored: false` rather than
7588
+ * failing the rewind. */
6976
7589
  function registerClaudeRewindRoute(ctx, sidecar, access) {
6977
7590
  registerPluginRoute(ctx, {
6978
7591
  mode: "unary",
@@ -6998,16 +7611,22 @@ function registerClaudeRewindRoute(ctx, sidecar, access) {
6998
7611
  status: 409,
6999
7612
  value: { error: "session-busy" }
7000
7613
  };
7001
- const planned = planRewind((await sidecar.read(sessionId)).rewind ?? EMPTY_REWIND_STATE, events, seq);
7614
+ const current = (await sidecar.read(sessionId)).rewind ?? EMPTY_REWIND_STATE;
7615
+ const planned = planRewind(current, events, seq);
7002
7616
  if (planned === void 0) return {
7003
7617
  status: 409,
7004
7618
  value: { error: "seq-unavailable" }
7005
7619
  };
7006
- await sidecar.writeRewind(sessionId, planned);
7620
+ const tree = input?.restoreFiles === true ? rewindRestoreTree(current, events, seq) : void 0;
7621
+ await sidecar.writeRewind(sessionId, planned, turnAtOrAfter(events, seq));
7007
7622
  await access.reset(sessionId);
7623
+ const filesRestored = tree === void 0 || access.restoreFiles === void 0 ? false : await access.restoreFiles(sessionId, tree).catch(() => false);
7008
7624
  return {
7009
7625
  status: 200,
7010
- value: { ranges: planned.ranges }
7626
+ value: {
7627
+ ranges: planned.ranges,
7628
+ filesRestored
7629
+ }
7011
7630
  };
7012
7631
  } catch (error) {
7013
7632
  if (error instanceof SyntaxError) return {
@@ -7511,6 +8130,31 @@ const PROSE = {
7511
8130
  else document.prose = value;
7512
8131
  }
7513
8132
  };
8133
+ /** Whether a session that needs the user interrupts them. Presentation only,
8134
+ * and read by the Client at delivery time, so like {@link PROSE} the switch
8135
+ * lands the moment it is saved. */
8136
+ const ALERTS = {
8137
+ key: "alerts",
8138
+ kind: "select",
8139
+ document: "plugin",
8140
+ effect: "immediate",
8141
+ async options() {
8142
+ return CLAUDE_ALERT_MODES.map((value) => ({
8143
+ value,
8144
+ label: value,
8145
+ source: "built-in"
8146
+ }));
8147
+ },
8148
+ read(document) {
8149
+ const value = document.alerts;
8150
+ return isClaudeAlertMode(value) ? value : "on";
8151
+ },
8152
+ apply(document, value) {
8153
+ if (!isClaudeAlertMode(value)) throw new Error("Invalid value for global setting alerts");
8154
+ if (value === "on") delete document.alerts;
8155
+ else document.alerts = value;
8156
+ }
8157
+ };
7514
8158
  function isBoundedInteger(value, min, max) {
7515
8159
  return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max;
7516
8160
  }
@@ -7536,6 +8180,7 @@ const DESCRIPTORS = [
7536
8180
  OUTPUT_STYLE,
7537
8181
  RENDERER,
7538
8182
  PROSE,
8183
+ ALERTS,
7539
8184
  WORKTREE_BRANCH_PREFIX,
7540
8185
  integerSetting("maxProcesses", 1, MAX_PROCESSES_LIMIT, (limits) => limits.maxProcesses),
7541
8186
  integerSetting("idleTimeoutMinutes", 1, MAX_IDLE_TIMEOUT_MINUTES, (limits) => Math.max(1, Math.round(limits.idleTimeoutMs / 6e4)))
@@ -7819,14 +8464,12 @@ async function apply(ctx, config) {
7819
8464
  const supervisorConfig = {
7820
8465
  executablePath: "",
7821
8466
  defaultModel: config.model ?? "default",
7822
- renderMode: DEFAULT_CLAUDE_RENDER_MODE,
7823
8467
  ...defaultLimits
7824
8468
  };
7825
8469
  const applySettingsOverrides = async () => {
7826
8470
  const overrides = await readSupervisorLimitOverrides();
7827
8471
  supervisorConfig.idleTimeoutMs = overrides.idleTimeoutMs ?? defaultLimits.idleTimeoutMs;
7828
8472
  supervisorConfig.maxProcesses = overrides.maxProcesses ?? defaultLimits.maxProcesses;
7829
- supervisorConfig.renderMode = await readRenderMode();
7830
8473
  };
7831
8474
  await applySettingsOverrides();
7832
8475
  const sidecar = new ClaudeSidecarRepository();
@@ -7848,7 +8491,7 @@ async function apply(ctx, config) {
7848
8491
  let resolutionError;
7849
8492
  try {
7850
8493
  supervisorConfig.executablePath = (await resolveClaudeExecutable(ctx.subprocess, config.executablePath === void 0 || config.executablePath.length === 0 ? void 0 : config.executablePath)).path;
7851
- 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));
8494
+ 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)));
7852
8495
  ctx.effect(() => {
7853
8496
  const mounted = /* @__PURE__ */ new Map();
7854
8497
  const pending = /* @__PURE__ */ new Set();
@@ -7979,13 +8622,18 @@ async function apply(ctx, config) {
7979
8622
  return agent !== void 0 && webCtx.agentPresets.composedPreset(agent.ctx) === "claude";
7980
8623
  };
7981
8624
  registerReviewCommentRoute(webCtx, reviewComments, ownsClaudeSession);
8625
+ registerPlanFeedbackRoute(webCtx, supervisor.planFeedback, ownsClaudeSession);
7982
8626
  registerClaudeRewindRoute(webCtx, sidecar, {
7983
8627
  eventsFor: (sessionId) => {
7984
8628
  const agent = webCtx.agents.get(sessionId);
7985
8629
  return agent === void 0 || webCtx.agentPresets.composedPreset(agent.ctx) !== "claude" ? void 0 : agent.session.events;
7986
8630
  },
7987
8631
  busy: (sessionId) => supervisor.snapshots().some((item) => item.sessionId === sessionId && (item.state === "running" || item.state === "interrupting")),
7988
- reset: (sessionId) => supervisor.disposeSession(sessionId)
8632
+ reset: (sessionId) => supervisor.disposeSession(sessionId),
8633
+ restoreFiles: async (sessionId, tree) => {
8634
+ const cwd = webCtx.agents.get(sessionId)?.session.header.cwd;
8635
+ return cwd === void 0 ? false : restoreWorktreeTree(ctx.subprocess, cwd, tree);
8636
+ }
7989
8637
  });
7990
8638
  registerPlanUsageRoute(webCtx, (fetchedAt) => probePlanUsage(supervisorConfig.executablePath, fetchedAt));
7991
8639
  registerClaudeProjectionRoute(webCtx, sidecar, ownsClaudeSession, (sessionId) => commandCatalogs.get(sessionId) ?? [], async (sessionId) => {