@norman-else/dsh-claude 0.1.40 → 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])
664
860
  });
665
- const fullAccess = alreadyFullAccess || await active.hasFullAccess?.() === true;
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 };
867
+ });
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) } : {}
@@ -5135,19 +5618,19 @@ var RepositoryActionService = class {
5135
5618
  };
5136
5619
  //#endregion
5137
5620
  //#region src/repository-setup-routes.ts
5138
- const MAX_BODY_BYTES$6 = 16384;
5139
- function record$9(value) {
5621
+ const MAX_BODY_BYTES$7 = 16384;
5622
+ function record$10(value) {
5140
5623
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
5141
5624
  }
5142
- async function readJson$5(io) {
5625
+ async function readJson$6(io) {
5143
5626
  let parsed;
5144
5627
  try {
5145
- parsed = await io.body(MAX_BODY_BYTES$6);
5628
+ parsed = await io.body(MAX_BODY_BYTES$7);
5146
5629
  } catch (error) {
5147
5630
  if (error instanceof SyntaxError) throw error;
5148
5631
  throw new RepositorySetupError("body-too-large", "The request body is too large.");
5149
5632
  }
5150
- const value = record$9(parsed);
5633
+ const value = record$10(parsed);
5151
5634
  if (value === void 0) throw new RepositorySetupError("invalid-request", "The request body is invalid.");
5152
5635
  return value;
5153
5636
  }
@@ -5216,7 +5699,7 @@ function registerRepositorySetupRoute(ctx, service, sweep) {
5216
5699
  try {
5217
5700
  if (pathname === `/plugins/dsh-claude/repository/setup/branches/refresh`) {
5218
5701
  if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
5219
- const input = await readJson$5(io);
5702
+ const input = await readJson$6(io);
5220
5703
  return json(res, 200, await service.refreshBranches(string$2(input, "cwd")));
5221
5704
  }
5222
5705
  if (pathname === `/plugins/dsh-claude/repository/setup/branches`) {
@@ -5227,14 +5710,14 @@ function registerRepositorySetupRoute(ctx, service, sweep) {
5227
5710
  }
5228
5711
  if (pathname === "/plugins/dsh-claude/repository/setup") {
5229
5712
  if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
5230
- const input = await readJson$5(io);
5713
+ const input = await readJson$6(io);
5231
5714
  if (typeof input.worktree !== "boolean") throw new RepositorySetupError("invalid-request", "The worktree field is required.");
5232
5715
  await streamSetup(res, service, input);
5233
5716
  return;
5234
5717
  }
5235
5718
  if (pathname === `/plugins/dsh-claude/repository/setup/cleanup`) {
5236
5719
  if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
5237
- const input = await readJson$5(io);
5720
+ const input = await readJson$6(io);
5238
5721
  return json(res, 200, await service.cleanupMerged(string$2(input, "path"), string$2(input, "baseBranch")));
5239
5722
  }
5240
5723
  if (pathname === `/plugins/dsh-claude/repository/setup/sweep`) {
@@ -5244,7 +5727,7 @@ function registerRepositorySetupRoute(ctx, service, sweep) {
5244
5727
  }
5245
5728
  if (pathname === `/plugins/dsh-claude/repository/setup/bind`) {
5246
5729
  if (io.method !== "POST") return json(res, 405, { error: "method not allowed" });
5247
- const input = await readJson$5(io);
5730
+ const input = await readJson$6(io);
5248
5731
  await service.bindLease(string$2(input, "leaseId"), string$2(input, "sessionId"));
5249
5732
  return json(res, 200, { ok: true });
5250
5733
  }
@@ -5262,8 +5745,8 @@ function registerRepositorySetupRoute(ctx, service, sweep) {
5262
5745
  }
5263
5746
  //#endregion
5264
5747
  //#region src/repository-action-routes.ts
5265
- const MAX_BODY_BYTES$5 = 16384;
5266
- const MAX_SESSION_ID_CHARS$5 = 1024;
5748
+ const MAX_BODY_BYTES$6 = 16384;
5749
+ const MAX_SESSION_ID_CHARS$6 = 1024;
5267
5750
  const ACTIONS = /* @__PURE__ */ new Set([
5268
5751
  "commit",
5269
5752
  "commit-push",
@@ -5272,24 +5755,24 @@ const ACTIONS = /* @__PURE__ */ new Set([
5272
5755
  "merge-pr",
5273
5756
  "update-branch"
5274
5757
  ]);
5275
- function record$8(value) {
5758
+ function record$9(value) {
5276
5759
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
5277
5760
  }
5278
- async function readJson$4(io) {
5761
+ async function readJson$5(io) {
5279
5762
  let parsed;
5280
5763
  try {
5281
- parsed = await io.body(MAX_BODY_BYTES$5);
5764
+ parsed = await io.body(MAX_BODY_BYTES$6);
5282
5765
  } catch (error) {
5283
5766
  if (error instanceof SyntaxError) throw error;
5284
5767
  throw new RepositoryActionError("body-too-large", "The request body is too large.");
5285
5768
  }
5286
- const value = record$8(parsed);
5769
+ const value = record$9(parsed);
5287
5770
  if (value === void 0) throw new RepositoryActionError("invalid-request", "The request body is invalid.");
5288
5771
  return value;
5289
5772
  }
5290
5773
  function sessionId$1(url) {
5291
5774
  const value = url.searchParams.get("sessionId");
5292
- if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$5) throw new RepositoryActionError("invalid-session", "The session is invalid.");
5775
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$6) throw new RepositoryActionError("invalid-session", "The session is invalid.");
5293
5776
  return value;
5294
5777
  }
5295
5778
  function string$1(input, key) {
@@ -5353,7 +5836,7 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
5353
5836
  status: 405,
5354
5837
  value: { error: "method not allowed" }
5355
5838
  };
5356
- const input = await readJson$4(io);
5839
+ const input = await readJson$5(io);
5357
5840
  return {
5358
5841
  status: 200,
5359
5842
  value: { message: await service.generateMessage(cwd, string$1(input, "fingerprint")) }
@@ -5366,7 +5849,7 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
5366
5849
  };
5367
5850
  return {
5368
5851
  status: 200,
5369
- value: await service.execute(cwd, actionRequest(await readJson$4(io)))
5852
+ value: await service.execute(cwd, actionRequest(await readJson$5(io)))
5370
5853
  };
5371
5854
  }
5372
5855
  return {
@@ -5517,7 +6000,7 @@ var EditorOpenService = class {
5517
6000
  };
5518
6001
  //#endregion
5519
6002
  //#region src/editor-open-routes.ts
5520
- const MAX_SESSION_ID_CHARS$4 = 1024;
6003
+ const MAX_SESSION_ID_CHARS$5 = 1024;
5521
6004
  /** Open the session's working directory in a desktop editor. Query-only: the
5522
6005
  * request carries two enum-ish values, so there is no body to parse. */
5523
6006
  function registerEditorOpenRoute(ctx, service, cwdForSession) {
@@ -5531,7 +6014,7 @@ function registerEditorOpenRoute(ctx, service, cwdForSession) {
5531
6014
  const params = io.url.searchParams;
5532
6015
  const id = params.get("sessionId");
5533
6016
  const editor = params.get("editor");
5534
- 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 {
5535
6018
  status: 400,
5536
6019
  value: {
5537
6020
  error: "invalid-session",
@@ -5646,7 +6129,7 @@ async function collect(handle) {
5646
6129
  lossy: stdout?.lossy === true
5647
6130
  };
5648
6131
  }
5649
- function record$7(value) {
6132
+ function record$8(value) {
5650
6133
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
5651
6134
  }
5652
6135
  /** Read `repository.pullRequest.reviewThreads.nodes` out of a GraphQL response.
@@ -5654,12 +6137,12 @@ function record$7(value) {
5654
6137
  * throwing: the caller distinguishes "no threads" from "call failed" by the
5655
6138
  * process exit code. */
5656
6139
  function parseReviewThreads(value) {
5657
- 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;
5658
6141
  if (!Array.isArray(nodes)) return [];
5659
6142
  const threads = [];
5660
6143
  let total = 0;
5661
6144
  for (const item of nodes) {
5662
- const input = record$7(item);
6145
+ const input = record$8(item);
5663
6146
  if (input === void 0 || typeof input.id !== "string" || input.id.length === 0) continue;
5664
6147
  const path = typeof input.path === "string" ? input.path : "";
5665
6148
  if (path.length === 0) continue;
@@ -5671,14 +6154,14 @@ function parseReviewThreads(value) {
5671
6154
  side
5672
6155
  };
5673
6156
  const comments = [];
5674
- const commentNodes = record$7(input.comments)?.nodes;
6157
+ const commentNodes = record$8(input.comments)?.nodes;
5675
6158
  for (const node of Array.isArray(commentNodes) ? commentNodes : []) {
5676
6159
  if (total >= MAX_COMMENTS) break;
5677
- const comment = record$7(node);
6160
+ const comment = record$8(node);
5678
6161
  if (comment === void 0 || !Number.isSafeInteger(comment.databaseId)) continue;
5679
6162
  const body = typeof comment.body === "string" ? comment.body.trim() : "";
5680
6163
  if (body.length === 0) continue;
5681
- const author = record$7(comment.author);
6164
+ const author = record$8(comment.author);
5682
6165
  const avatarUrl = githubAvatarUrl(author?.avatarUrl);
5683
6166
  const login = typeof author?.login === "string" ? author.login : "unknown";
5684
6167
  comments.push({
@@ -5707,11 +6190,11 @@ function parseReviewThreads(value) {
5707
6190
  }
5708
6191
  /** One posted reply, shaped like the thread comments it joins. */
5709
6192
  function parseReplyComment(value, anchor) {
5710
- const input = record$7(value);
6193
+ const input = record$8(value);
5711
6194
  if (input === void 0 || !Number.isSafeInteger(input.id)) return void 0;
5712
6195
  const body = typeof input.body === "string" ? input.body.trim() : "";
5713
6196
  if (body.length === 0) return void 0;
5714
- const user = record$7(input.user);
6197
+ const user = record$8(input.user);
5715
6198
  const avatarUrl = githubAvatarUrl(user?.avatar_url);
5716
6199
  const login = typeof user?.login === "string" ? user.login : "unknown";
5717
6200
  return {
@@ -5726,11 +6209,11 @@ function parseReplyComment(value, anchor) {
5726
6209
  };
5727
6210
  }
5728
6211
  function parseMentionableUsers(value) {
5729
- 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;
5730
6213
  if (!Array.isArray(nodes)) return [];
5731
6214
  const users = [];
5732
6215
  for (const item of nodes) {
5733
- const input = record$7(item);
6216
+ const input = record$8(item);
5734
6217
  if (input === void 0 || typeof input.login !== "string" || input.login.length === 0) continue;
5735
6218
  const avatarUrl = githubAvatarUrl(input.avatarUrl);
5736
6219
  users.push({
@@ -5750,7 +6233,7 @@ function parseFailingChecks(value) {
5750
6233
  if (!Array.isArray(value)) return [];
5751
6234
  const failing = [];
5752
6235
  for (const item of value) {
5753
- const input = record$7(item);
6236
+ const input = record$8(item);
5754
6237
  if (input === void 0 || input.bucket !== "fail" || typeof input.name !== "string") continue;
5755
6238
  failing.push({
5756
6239
  name: input.name,
@@ -5813,12 +6296,12 @@ var PullRequestFeedbackService = class {
5813
6296
  let posted;
5814
6297
  try {
5815
6298
  const parsed = JSON.parse(result.stdout);
5816
- const path = typeof record$7(parsed)?.path === "string" ? String(record$7(parsed)?.path) : "";
5817
- const line = Number.isSafeInteger(record$7(parsed)?.line) ? Number(record$7(parsed)?.line) : void 0;
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;
5818
6301
  posted = parseReplyComment(parsed, {
5819
6302
  path,
5820
6303
  ...line === void 0 ? {} : { line },
5821
- side: record$7(parsed)?.side === "LEFT" ? "old" : "new"
6304
+ side: record$8(parsed)?.side === "LEFT" ? "old" : "new"
5822
6305
  });
5823
6306
  } catch {
5824
6307
  posted = void 0;
@@ -5839,7 +6322,7 @@ var PullRequestFeedbackService = class {
5839
6322
  ], cwd, GH_TIMEOUT_MS);
5840
6323
  if (result.exitCode !== 0) throw new PullRequestFeedbackError("resolve-failed", "The thread could not be updated.");
5841
6324
  try {
5842
- 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);
5843
6326
  if (typeof thread?.isResolved !== "boolean") throw new Error("missing state");
5844
6327
  return thread.isResolved;
5845
6328
  } catch {
@@ -5962,23 +6445,23 @@ var PullRequestFeedbackService = class {
5962
6445
  };
5963
6446
  //#endregion
5964
6447
  //#region src/pr-feedback-routes.ts
5965
- const MAX_SESSION_ID_CHARS$3 = 1024;
5966
- const MAX_BODY_BYTES$4 = 16384;
6448
+ const MAX_SESSION_ID_CHARS$4 = 1024;
6449
+ const MAX_BODY_BYTES$5 = 16384;
5967
6450
  const MAX_REPLY_CHARS = 2e3;
5968
6451
  const MAX_THREAD_ID_CHARS = 512;
5969
6452
  const MAX_MENTION_QUERY_CHARS = 64;
5970
- function record$6(value) {
6453
+ function record$7(value) {
5971
6454
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
5972
6455
  }
5973
- async function readJson$3(io) {
6456
+ async function readJson$4(io) {
5974
6457
  let parsed;
5975
6458
  try {
5976
- parsed = await io.body(MAX_BODY_BYTES$4);
6459
+ parsed = await io.body(MAX_BODY_BYTES$5);
5977
6460
  } catch (error) {
5978
6461
  if (error instanceof SyntaxError) throw error;
5979
6462
  throw new PullRequestFeedbackError("body-too-large", "The request body is too large.");
5980
6463
  }
5981
- const value = record$6(parsed);
6464
+ const value = record$7(parsed);
5982
6465
  if (value === void 0) throw new PullRequestFeedbackError("invalid-request", "The request body is invalid.");
5983
6466
  return value;
5984
6467
  }
@@ -5999,7 +6482,7 @@ function threadId(input) {
5999
6482
  }
6000
6483
  function sessionId(url) {
6001
6484
  const value = url.searchParams.get("sessionId");
6002
- if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$3) throw new PullRequestFeedbackError("invalid-session", "The session is invalid.");
6485
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$4) throw new PullRequestFeedbackError("invalid-session", "The session is invalid.");
6003
6486
  return value;
6004
6487
  }
6005
6488
  function pullNumber(url) {
@@ -6058,7 +6541,7 @@ function registerPullRequestFeedbackRoute(ctx, service, cwdForSession) {
6058
6541
  status: 405,
6059
6542
  value: { error: "method not allowed" }
6060
6543
  };
6061
- const input = await readJson$3(io);
6544
+ const input = await readJson$4(io);
6062
6545
  return {
6063
6546
  status: 200,
6064
6547
  value: { comment: await service.reply(cwd, pullNumber(url), commentId(input), replyBody(input)) }
@@ -6069,7 +6552,7 @@ function registerPullRequestFeedbackRoute(ctx, service, cwdForSession) {
6069
6552
  status: 405,
6070
6553
  value: { error: "method not allowed" }
6071
6554
  };
6072
- const input = await readJson$3(io);
6555
+ const input = await readJson$4(io);
6073
6556
  if (typeof input.resolved !== "boolean") throw new PullRequestFeedbackError("invalid-request", "The resolved field is required.");
6074
6557
  return {
6075
6558
  status: 200,
@@ -6215,7 +6698,7 @@ var JiraError = class extends Error {
6215
6698
  this.code = code;
6216
6699
  }
6217
6700
  };
6218
- function record$5(value) {
6701
+ function record$6(value) {
6219
6702
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
6220
6703
  }
6221
6704
  /** Jira Cloud sites are origins; Data Center may carry a context path. */
@@ -6239,14 +6722,14 @@ function ticketKeyOf(query) {
6239
6722
  * search and does match keys; its hits become the key filter the normal
6240
6723
  * search then reads the display fields from. */
6241
6724
  function pickerKeys(value, number) {
6242
- const sections = record$5(value)?.sections;
6725
+ const sections = record$6(value)?.sections;
6243
6726
  if (!Array.isArray(sections)) return [];
6244
6727
  const keys = [];
6245
6728
  for (const section of sections) {
6246
- const issues = record$5(section)?.issues;
6729
+ const issues = record$6(section)?.issues;
6247
6730
  if (!Array.isArray(issues)) continue;
6248
6731
  for (const issue of issues) {
6249
- const raw = record$5(issue)?.key;
6732
+ const raw = record$6(issue)?.key;
6250
6733
  const key = ticketKeyOf(typeof raw === "string" ? raw : "");
6251
6734
  if (key !== void 0 && key.endsWith(`-${number}`) && !keys.includes(key)) keys.push(key);
6252
6735
  }
@@ -6265,15 +6748,15 @@ function buildJql(query) {
6265
6748
  return `text ~ "${trimmed.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}*" ORDER BY updated DESC`;
6266
6749
  }
6267
6750
  function parseTickets(value, siteUrl) {
6268
- const issues = record$5(value)?.issues;
6751
+ const issues = record$6(value)?.issues;
6269
6752
  if (!Array.isArray(issues)) return [];
6270
6753
  const tickets = [];
6271
6754
  for (const item of issues) {
6272
- const issue = record$5(item);
6273
- const fields = record$5(issue?.fields);
6755
+ const issue = record$6(item);
6756
+ const fields = record$6(issue?.fields);
6274
6757
  if (issue === void 0 || typeof issue.key !== "string" || fields === void 0) continue;
6275
- const status = record$5(fields.status)?.name;
6276
- const type = record$5(fields.issuetype)?.name;
6758
+ const status = record$6(fields.status)?.name;
6759
+ const type = record$6(fields.issuetype)?.name;
6277
6760
  tickets.push({
6278
6761
  key: issue.key,
6279
6762
  summary: typeof fields.summary === "string" ? fields.summary.slice(0, 256) : "",
@@ -6315,7 +6798,7 @@ var JiraService = class {
6315
6798
  email,
6316
6799
  apiToken
6317
6800
  };
6318
- 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"));
6319
6802
  const displayName = typeof myself?.displayName === "string" ? myself.displayName : void 0;
6320
6803
  const accountId = typeof myself?.accountId === "string" ? myself.accountId : void 0;
6321
6804
  const store = {
@@ -6334,7 +6817,7 @@ var JiraService = class {
6334
6817
  if (ticket === void 0) throw new JiraError("invalid-request", "The ticket key is invalid.");
6335
6818
  let accountId = store.accountId;
6336
6819
  if (accountId === void 0) {
6337
- 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"));
6338
6821
  if (typeof myself?.accountId !== "string") throw new JiraError("jira-failed", "The Jira account id is unavailable.");
6339
6822
  accountId = myself.accountId;
6340
6823
  await this.#write({
@@ -6410,7 +6893,7 @@ var JiraService = class {
6410
6893
  throw error;
6411
6894
  }
6412
6895
  if (Buffer.byteLength(text) > MAX_STORE_BYTES) return void 0;
6413
- const input = record$5(JSON.parse(text));
6896
+ const input = record$6(JSON.parse(text));
6414
6897
  if (input === void 0 || typeof input.siteUrl !== "string" || typeof input.email !== "string" || typeof input.apiToken !== "string") return void 0;
6415
6898
  return {
6416
6899
  siteUrl: input.siteUrl,
@@ -6441,21 +6924,21 @@ var JiraService = class {
6441
6924
  };
6442
6925
  //#endregion
6443
6926
  //#region src/jira-routes.ts
6444
- const MAX_BODY_BYTES$3 = 8192;
6445
- function record$4(value) {
6927
+ const MAX_BODY_BYTES$4 = 8192;
6928
+ function record$5(value) {
6446
6929
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
6447
6930
  }
6448
6931
  /** The wrapper enforces the byte cap; its plain rejection is translated back
6449
6932
  * into the JiraError shape the panel already knows how to render. */
6450
- async function readJson$2(io) {
6933
+ async function readJson$3(io) {
6451
6934
  let body;
6452
6935
  try {
6453
- body = await io.body(MAX_BODY_BYTES$3);
6936
+ body = await io.body(MAX_BODY_BYTES$4);
6454
6937
  } catch (error) {
6455
6938
  if (error instanceof SyntaxError) throw error;
6456
6939
  throw new JiraError("body-too-large", "The request body is too large.");
6457
6940
  }
6458
- const value = record$4(body);
6941
+ const value = record$5(body);
6459
6942
  if (value === void 0) throw new JiraError("invalid-request", "The request body is invalid.");
6460
6943
  return value;
6461
6944
  }
@@ -6489,7 +6972,7 @@ function registerJiraRoute(ctx, service) {
6489
6972
  status: 405,
6490
6973
  value: { error: "method not allowed" }
6491
6974
  };
6492
- const input = await readJson$2(io);
6975
+ const input = await readJson$3(io);
6493
6976
  return {
6494
6977
  status: 200,
6495
6978
  value: await service.connect({
@@ -6515,7 +6998,7 @@ function registerJiraRoute(ctx, service) {
6515
6998
  status: 405,
6516
6999
  value: { error: "method not allowed" }
6517
7000
  };
6518
- const input = await readJson$2(io);
7001
+ const input = await readJson$3(io);
6519
7002
  await service.assignToMe(string(input, "key"));
6520
7003
  return {
6521
7004
  status: 200,
@@ -6634,12 +7117,12 @@ function askArguments(preferences) {
6634
7117
  ...READ_ONLY_TOOLS
6635
7118
  ];
6636
7119
  }
6637
- function record$3(value) {
7120
+ function record$4(value) {
6638
7121
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
6639
7122
  }
6640
7123
  /** One-line description of a tool call, mirroring the main window's step titles. */
6641
7124
  function toolSummary(input) {
6642
- const fields = record$3(input);
7125
+ const fields = record$4(input);
6643
7126
  if (fields === void 0) return void 0;
6644
7127
  const candidate = [
6645
7128
  fields.command,
@@ -6653,7 +7136,7 @@ function toolSummary(input) {
6653
7136
  function eventsOfStreamLine(line) {
6654
7137
  let parsed;
6655
7138
  try {
6656
- parsed = record$3(JSON.parse(line));
7139
+ parsed = record$4(JSON.parse(line));
6657
7140
  } catch {
6658
7141
  return [];
6659
7142
  }
@@ -6663,9 +7146,9 @@ function eventsOfStreamLine(line) {
6663
7146
  text: "ready"
6664
7147
  }];
6665
7148
  if (parsed.type === "stream_event") {
6666
- const event = record$3(parsed.event);
7149
+ const event = record$4(parsed.event);
6667
7150
  if (event?.type === "content_block_start") {
6668
- const block = record$3(event.content_block);
7151
+ const block = record$4(event.content_block);
6669
7152
  if (block?.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") return [{
6670
7153
  type: "tool",
6671
7154
  id: block.id,
@@ -6674,7 +7157,7 @@ function eventsOfStreamLine(line) {
6674
7157
  }];
6675
7158
  return [];
6676
7159
  }
6677
- const delta = record$3(event?.delta);
7160
+ const delta = record$4(event?.delta);
6678
7161
  if (event?.type !== "content_block_delta" || delta === void 0) return [];
6679
7162
  if (delta.type === "text_delta" && typeof delta.text === "string") return [{
6680
7163
  type: "text",
@@ -6687,11 +7170,11 @@ function eventsOfStreamLine(line) {
6687
7170
  return [];
6688
7171
  }
6689
7172
  if (parsed.type === "assistant" || parsed.type === "user") {
6690
- const content = record$3(parsed.message)?.content;
7173
+ const content = record$4(parsed.message)?.content;
6691
7174
  if (!Array.isArray(content)) return [];
6692
7175
  const events = [];
6693
7176
  for (const item of content) {
6694
- const block = record$3(item);
7177
+ const block = record$4(item);
6695
7178
  if (block?.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
6696
7179
  const summary = toolSummary(block.input);
6697
7180
  events.push({
@@ -6783,11 +7266,11 @@ var AskService = class {
6783
7266
  };
6784
7267
  //#endregion
6785
7268
  //#region src/ask-routes.ts
6786
- const MAX_BODY_BYTES$2 = 131072;
6787
- const MAX_SESSION_ID_CHARS$2 = 1024;
7269
+ const MAX_BODY_BYTES$3 = 131072;
7270
+ const MAX_SESSION_ID_CHARS$3 = 1024;
6788
7271
  /** Two sessions may await an answer at once; a third evicts the oldest. */
6789
7272
  const MAX_CONCURRENT_ASKS = 2;
6790
- function record$2(value) {
7273
+ function record$3(value) {
6791
7274
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
6792
7275
  }
6793
7276
  function askRequest(input) {
@@ -6819,12 +7302,12 @@ function registerAskRoute(ctx, service, cwdForSession, preferencesFor) {
6819
7302
  let sessionId;
6820
7303
  try {
6821
7304
  const value = io.url.searchParams.get("sessionId");
6822
- if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$2) throw new AskError("invalid-session", "The session is invalid.");
7305
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$3) throw new AskError("invalid-session", "The session is invalid.");
6823
7306
  sessionId = value;
6824
7307
  const resolved = cwdForSession(sessionId);
6825
7308
  if (resolved === void 0) throw new AskError("session-unavailable", "The Claude session is unavailable.");
6826
7309
  cwd = resolved;
6827
- const body = record$2(await io.body(MAX_BODY_BYTES$2));
7310
+ const body = record$3(await io.body(MAX_BODY_BYTES$3));
6828
7311
  if (body === void 0) throw new AskError("invalid-request", "The request body is invalid.");
6829
7312
  request = askRequest(body);
6830
7313
  } catch (error) {
@@ -6864,26 +7347,26 @@ function registerAskRoute(ctx, service, cwdForSession, preferencesFor) {
6864
7347
  }
6865
7348
  //#endregion
6866
7349
  //#region src/review-comment-routes.ts
6867
- const MAX_BODY_BYTES$1 = 16384;
6868
- const MAX_SESSION_ID_CHARS$1 = 1024;
6869
- function record$1(value) {
7350
+ const MAX_BODY_BYTES$2 = 16384;
7351
+ const MAX_SESSION_ID_CHARS$2 = 1024;
7352
+ function record$2(value) {
6870
7353
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
6871
7354
  }
6872
- async function readJson$1(io) {
7355
+ async function readJson$2(io) {
6873
7356
  let parsed;
6874
7357
  try {
6875
- parsed = await io.body(MAX_BODY_BYTES$1);
7358
+ parsed = await io.body(MAX_BODY_BYTES$2);
6876
7359
  } catch (error) {
6877
7360
  if (error instanceof SyntaxError) throw error;
6878
7361
  throw new ReviewCommentError("body-too-large", "The request body is too large.");
6879
7362
  }
6880
- const value = record$1(parsed);
7363
+ const value = record$2(parsed);
6881
7364
  if (value === void 0) throw new ReviewCommentError("invalid-request", "The request body is invalid.");
6882
7365
  return value;
6883
7366
  }
6884
7367
  function sessionIdFromUrl(url) {
6885
7368
  const value = url.searchParams.get("sessionId");
6886
- if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$1) throw new ReviewCommentError("invalid-session", "The session is invalid.");
7369
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$2) throw new ReviewCommentError("invalid-session", "The session is invalid.");
6887
7370
  return value;
6888
7371
  }
6889
7372
  function registerReviewCommentRoute(ctx, store, ownsSession) {
@@ -6899,7 +7382,7 @@ function registerReviewCommentRoute(ctx, store, ownsSession) {
6899
7382
  if (!ownsSession(sessionId)) throw new ReviewCommentError("session-unavailable", "The Claude session is unavailable.");
6900
7383
  const pathname = io.url.pathname;
6901
7384
  if (pathname === "/plugins/dsh-claude/review-comments") {
6902
- const input = await readJson$1(io);
7385
+ const input = await readJson$2(io);
6903
7386
  return {
6904
7387
  status: 200,
6905
7388
  value: { comment: store.add(sessionId, {
@@ -6916,7 +7399,7 @@ function registerReviewCommentRoute(ctx, store, ownsSession) {
6916
7399
  value: { removed: store.drain(sessionId).length }
6917
7400
  };
6918
7401
  if (pathname === `/plugins/dsh-claude/review-comments/remove`) {
6919
- const input = await readJson$1(io);
7402
+ const input = await readJson$2(io);
6920
7403
  if (typeof input.id !== "string" || input.id.length === 0 || input.id.length > 128) throw new ReviewCommentError("invalid-request", "The comment id is invalid.");
6921
7404
  return {
6922
7405
  status: 200,
@@ -6951,6 +7434,85 @@ function registerReviewCommentRoute(ctx, store, ownsSession) {
6951
7434
  });
6952
7435
  }
6953
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
6954
7516
  //#region src/client-diagnostics-routes.ts
6955
7517
  /** Enough for a message plus a trimmed stack; the client caps its own volume. */
6956
7518
  const MAX_DIAGNOSTIC_BYTES = 8192;
@@ -7015,8 +7577,15 @@ async function readJson(io) {
7015
7577
  return;
7016
7578
  }
7017
7579
  }
7018
- /** `POST <path>` with `{ sessionId, seq }`: hide that surface event and every
7019
- * later one, and arm Claude to resume before the turn it opened. */
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. */
7020
7589
  function registerClaudeRewindRoute(ctx, sidecar, access) {
7021
7590
  registerPluginRoute(ctx, {
7022
7591
  mode: "unary",
@@ -7042,16 +7611,22 @@ function registerClaudeRewindRoute(ctx, sidecar, access) {
7042
7611
  status: 409,
7043
7612
  value: { error: "session-busy" }
7044
7613
  };
7045
- 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);
7046
7616
  if (planned === void 0) return {
7047
7617
  status: 409,
7048
7618
  value: { error: "seq-unavailable" }
7049
7619
  };
7050
- 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));
7051
7622
  await access.reset(sessionId);
7623
+ const filesRestored = tree === void 0 || access.restoreFiles === void 0 ? false : await access.restoreFiles(sessionId, tree).catch(() => false);
7052
7624
  return {
7053
7625
  status: 200,
7054
- value: { ranges: planned.ranges }
7626
+ value: {
7627
+ ranges: planned.ranges,
7628
+ filesRestored
7629
+ }
7055
7630
  };
7056
7631
  } catch (error) {
7057
7632
  if (error instanceof SyntaxError) return {
@@ -7555,6 +8130,31 @@ const PROSE = {
7555
8130
  else document.prose = value;
7556
8131
  }
7557
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
+ };
7558
8158
  function isBoundedInteger(value, min, max) {
7559
8159
  return typeof value === "number" && Number.isInteger(value) && value >= min && value <= max;
7560
8160
  }
@@ -7580,6 +8180,7 @@ const DESCRIPTORS = [
7580
8180
  OUTPUT_STYLE,
7581
8181
  RENDERER,
7582
8182
  PROSE,
8183
+ ALERTS,
7583
8184
  WORKTREE_BRANCH_PREFIX,
7584
8185
  integerSetting("maxProcesses", 1, MAX_PROCESSES_LIMIT, (limits) => limits.maxProcesses),
7585
8186
  integerSetting("idleTimeoutMinutes", 1, MAX_IDLE_TIMEOUT_MINUTES, (limits) => Math.max(1, Math.round(limits.idleTimeoutMs / 6e4)))
@@ -7863,14 +8464,12 @@ async function apply(ctx, config) {
7863
8464
  const supervisorConfig = {
7864
8465
  executablePath: "",
7865
8466
  defaultModel: config.model ?? "default",
7866
- renderMode: DEFAULT_CLAUDE_RENDER_MODE,
7867
8467
  ...defaultLimits
7868
8468
  };
7869
8469
  const applySettingsOverrides = async () => {
7870
8470
  const overrides = await readSupervisorLimitOverrides();
7871
8471
  supervisorConfig.idleTimeoutMs = overrides.idleTimeoutMs ?? defaultLimits.idleTimeoutMs;
7872
8472
  supervisorConfig.maxProcesses = overrides.maxProcesses ?? defaultLimits.maxProcesses;
7873
- supervisorConfig.renderMode = await readRenderMode();
7874
8473
  };
7875
8474
  await applySettingsOverrides();
7876
8475
  const sidecar = new ClaudeSidecarRepository();
@@ -7892,7 +8491,7 @@ async function apply(ctx, config) {
7892
8491
  let resolutionError;
7893
8492
  try {
7894
8493
  supervisorConfig.executablePath = (await resolveClaudeExecutable(ctx.subprocess, config.executablePath === void 0 || config.executablePath.length === 0 ? void 0 : config.executablePath)).path;
7895
- ctx.llm.registerAdapter([...CLAUDE_CODE_PROVIDER_IDS], createClaudeCodeAdapter(supervisor, ctx.agents, ctx.attachments, (agent) => ctx.agentPresets.composedPreset(agent.ctx), (sessionId) => reviewComments.drain(sessionId), () => supervisorConfig.renderMode));
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)));
7896
8495
  ctx.effect(() => {
7897
8496
  const mounted = /* @__PURE__ */ new Map();
7898
8497
  const pending = /* @__PURE__ */ new Set();
@@ -8023,13 +8622,18 @@ async function apply(ctx, config) {
8023
8622
  return agent !== void 0 && webCtx.agentPresets.composedPreset(agent.ctx) === "claude";
8024
8623
  };
8025
8624
  registerReviewCommentRoute(webCtx, reviewComments, ownsClaudeSession);
8625
+ registerPlanFeedbackRoute(webCtx, supervisor.planFeedback, ownsClaudeSession);
8026
8626
  registerClaudeRewindRoute(webCtx, sidecar, {
8027
8627
  eventsFor: (sessionId) => {
8028
8628
  const agent = webCtx.agents.get(sessionId);
8029
8629
  return agent === void 0 || webCtx.agentPresets.composedPreset(agent.ctx) !== "claude" ? void 0 : agent.session.events;
8030
8630
  },
8031
8631
  busy: (sessionId) => supervisor.snapshots().some((item) => item.sessionId === sessionId && (item.state === "running" || item.state === "interrupting")),
8032
- 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
+ }
8033
8637
  });
8034
8638
  registerPlanUsageRoute(webCtx, (fetchedAt) => probePlanUsage(supervisorConfig.executablePath, fetchedAt));
8035
8639
  registerClaudeProjectionRoute(webCtx, sidecar, ownsClaudeSession, (sessionId) => commandCatalogs.get(sessionId) ?? [], async (sessionId) => {