@norman-else/dsh-claude 0.1.29 → 0.1.32

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,6 +1,6 @@
1
- import { A as TASK_TOOL_NAMES, C as CLAUDE_REPOSITORY_SETUP_PATH, D as CLAUDE_UPDATE_PATH, E as CLAUDE_UPDATE_CHECK_PATH, O as CLAUDE_USAGE_PATH, S as CLAUDE_REPOSITORY_FILE_PATH, T as CLAUDE_REVIEW_COMMENT_PATH, _ as CLAUDE_GLOBAL_SETTINGS_PATH, a as latestClaudeTasks, b as CLAUDE_REPOSITORY_ACTION_PATH, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_ASK_PATH, g as CLAUDE_DOCTOR_PATH, h as CLAUDE_CODE_PROVIDER_IDS, i as latestClaudeSessionBinding, l as redactText, m as CLAUDE_CODE_PROVIDER, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_CODE_PRESET_ID, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as CLAUDE_JIRA_PATH, w as CLAUDE_REPOSITORY_STATUS_PATH, x as CLAUDE_REPOSITORY_FEEDBACK_PATH, y as CLAUDE_PROJECTION_PATH } from "./events-lDt9nTUw.mjs";
2
- import { n as projectClaudeCommands, t as CLAUDE_COMMANDS_SERVICE } from "./command-bridge-BYj0VF4J.mjs";
3
- import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-CPOH9lAr.mjs";
1
+ import { A as CLAUDE_USAGE_PATH, C as CLAUDE_REPOSITORY_FILE_PATH, D as CLAUDE_REWIND_PATH, E as CLAUDE_REVIEW_COMMENT_PATH, M as TASK_TOOL_NAMES, O as CLAUDE_UPDATE_CHECK_PATH, S as CLAUDE_REPOSITORY_FEEDBACK_PATH, T as CLAUDE_REPOSITORY_STATUS_PATH, _ as CLAUDE_EDITOR_OPEN_PATH, a as latestClaudeTasks, b as CLAUDE_PROJECTION_PATH, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_ASK_PATH, g as CLAUDE_DOCTOR_PATH, h as CLAUDE_CODE_PROVIDER_IDS, i as latestClaudeSessionBinding, k as CLAUDE_UPDATE_PATH, l as redactText, m as CLAUDE_CODE_PROVIDER, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_CODE_PRESET_ID, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as CLAUDE_GLOBAL_SETTINGS_PATH, w as CLAUDE_REPOSITORY_SETUP_PATH, x as CLAUDE_REPOSITORY_ACTION_PATH, y as CLAUDE_JIRA_PATH } from "./events-CSDcXsWE.mjs";
2
+ import { n as projectClaudeCommands, t as CLAUDE_COMMANDS_SERVICE } from "./command-bridge-tqsu15hR.mjs";
3
+ import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-CVdbU87E.mjs";
4
4
  import z from "@deepseek-ai/schemastery";
5
5
  import { createHash, randomUUID } from "node:crypto";
6
6
  import { chmod, mkdir, opendir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
@@ -13,6 +13,67 @@ import { DSH_ENV_PREFIX, SENSITIVE_ENV_PATTERN } from "@deepseek-ai/dsh-subproce
13
13
  import { LlmAdapter, ReasoningEffortId } from "@deepseek-ai/dsh-llm";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { StringDecoder } from "node:string_decoder";
16
+ //#region src/rewind.ts
17
+ const EMPTY_REWIND_STATE = {
18
+ ranges: [],
19
+ anchors: []
20
+ };
21
+ /** Absorb one span into an ascending, non-overlapping range list. Adjacent
22
+ * spans merge so a rewind of a rewind reads as one hidden block. */
23
+ function mergeRewindRanges(ranges, addition) {
24
+ const merged = [];
25
+ let { start, end } = addition;
26
+ for (const range of [...ranges].sort((left, right) => left.start - right.start)) if (range.end + 1 < start) merged.push(range);
27
+ else if (range.start > end + 1) {
28
+ merged.push({
29
+ start,
30
+ end
31
+ });
32
+ start = range.start;
33
+ end = range.end;
34
+ } else {
35
+ start = Math.min(start, range.start);
36
+ end = Math.max(end, range.end);
37
+ }
38
+ merged.push({
39
+ start,
40
+ end
41
+ });
42
+ return merged.slice(-200);
43
+ }
44
+ /** Record one completed turn's last chain entry, replacing a re-run turn. */
45
+ function recordRewindAnchor(state, anchor) {
46
+ const anchors = [...state.anchors.filter((item) => item.turn !== anchor.turn), anchor].sort((left, right) => left.turn - right.turn).slice(-2e3);
47
+ return {
48
+ ...state,
49
+ anchors
50
+ };
51
+ }
52
+ /** The turn a surface seq belongs to: the first turn opened at or after it.
53
+ * A message accepted but never run belongs to no logged turn, so nothing
54
+ * Claude holds is discarded and every anchor stays valid. */
55
+ function turnAtOrAfter(events, seq) {
56
+ for (const event of events) if (event.seq >= seq && event.type === "turn/start") return event.data.turn;
57
+ }
58
+ /** Plan one rewind at `seq`, or undefined when the seq is not in the log.
59
+ * Anchors of the discarded turns go with them: after this rewind Claude no
60
+ * longer holds those entries, so a later rewind must never fork at one. */
61
+ function planRewind(state, events, seq) {
62
+ const last = events.at(-1)?.seq;
63
+ if (last === void 0 || seq > last) return void 0;
64
+ const turn = turnAtOrAfter(events, seq) ?? Number.MAX_SAFE_INTEGER;
65
+ const anchors = state.anchors.filter((anchor) => anchor.turn < turn);
66
+ const kept = anchors.at(-1);
67
+ return {
68
+ ranges: mergeRewindRanges(state.ranges, {
69
+ start: seq,
70
+ end: last
71
+ }),
72
+ anchors,
73
+ pending: kept === void 0 ? { fresh: true } : { resumeAt: kept.uuid }
74
+ };
75
+ }
76
+ //#endregion
16
77
  //#region src/sidecar.ts
17
78
  const SIDECAR_SCHEMA_VERSION = 1;
18
79
  const MAX_ACTIVITIES = 1e4;
@@ -26,7 +87,7 @@ function emptyProjection() {
26
87
  activities: []
27
88
  };
28
89
  }
29
- function record$11(value) {
90
+ function record$12(value) {
30
91
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
31
92
  }
32
93
  function finiteInteger(value) {
@@ -36,7 +97,7 @@ function string$4(value, max) {
36
97
  return typeof value === "string" && value.length > 0 && value.length <= max;
37
98
  }
38
99
  function binding(value) {
39
- const input = record$11(value);
100
+ const input = record$12(value);
40
101
  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;
41
102
  return {
42
103
  claudeSessionId: input.claudeSessionId,
@@ -48,6 +109,7 @@ function binding(value) {
48
109
  const ACTIVITY_KINDS = /* @__PURE__ */ new Set([
49
110
  "text",
50
111
  "status",
112
+ "compaction",
51
113
  "thinking",
52
114
  "tool-call",
53
115
  "tool-result",
@@ -66,36 +128,77 @@ const ACTIVITY_PHASES = /* @__PURE__ */ new Set([
66
128
  "failed"
67
129
  ]);
68
130
  function activity(value) {
69
- const input = record$11(value);
131
+ const input = record$12(value);
70
132
  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;
71
133
  return normalizeActivity(input);
72
134
  }
73
135
  function contextUsage(value) {
74
- const input = record$11(value);
136
+ const input = record$12(value);
75
137
  if (input === void 0 || !Array.isArray(input.categories)) return void 0;
76
138
  return normalizeContextUsage(input);
77
139
  }
140
+ function rewind(value) {
141
+ const input = record$12(value);
142
+ if (input === void 0 || !Array.isArray(input.ranges) || input.ranges.length > 200 || !Array.isArray(input.anchors) || input.anchors.length > 2e3) return void 0;
143
+ const ranges = [];
144
+ for (const item of input.ranges) {
145
+ const range = record$12(item);
146
+ if (range === void 0 || !finiteInteger(range.start) || !finiteInteger(range.end) || range.end < range.start) return void 0;
147
+ ranges.push({
148
+ start: range.start,
149
+ end: range.end
150
+ });
151
+ }
152
+ const anchors = [];
153
+ for (const item of input.anchors) {
154
+ const anchor = record$12(item);
155
+ if (anchor === void 0 || !finiteInteger(anchor.turn) || !string$4(anchor.uuid, 128)) return void 0;
156
+ anchors.push({
157
+ turn: anchor.turn,
158
+ uuid: anchor.uuid
159
+ });
160
+ }
161
+ const pending = record$12(input.pending);
162
+ if (input.pending !== void 0 && pending === void 0) return void 0;
163
+ if (pending === void 0) return {
164
+ ranges,
165
+ anchors
166
+ };
167
+ if (pending.fresh === true) return {
168
+ ranges,
169
+ anchors,
170
+ pending: { fresh: true }
171
+ };
172
+ if (!string$4(pending.resumeAt, 128)) return void 0;
173
+ return {
174
+ ranges,
175
+ anchors,
176
+ pending: { resumeAt: pending.resumeAt }
177
+ };
178
+ }
78
179
  function tasks(value) {
79
- const input = record$11(value);
180
+ const input = record$12(value);
80
181
  if (input === void 0 || !Array.isArray(input.tasks)) return void 0;
81
182
  return normalizeTasksEvent(input.tasks);
82
183
  }
83
184
  function parseClaudeSidecar(value) {
84
- const input = record$11(value);
185
+ const input = record$12(value);
85
186
  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");
86
187
  const activities = input.activities.map(activity);
87
188
  if (activities.some((item) => item === void 0)) throw new Error("dsh-claude: invalid sidecar activity");
88
189
  const parsedBinding = input.binding === void 0 ? void 0 : binding(input.binding);
89
190
  const parsedUsage = input.contextUsage === void 0 ? void 0 : contextUsage(input.contextUsage);
90
191
  const parsedTasks = input.tasks === void 0 ? void 0 : tasks(input.tasks);
91
- if (input.binding !== void 0 && parsedBinding === void 0 || input.contextUsage !== void 0 && parsedUsage === void 0 || input.tasks !== void 0 && parsedTasks === void 0) throw new Error("dsh-claude: invalid sidecar projection");
192
+ const parsedRewind = input.rewind === void 0 ? void 0 : rewind(input.rewind);
193
+ if (input.binding !== void 0 && parsedBinding === void 0 || input.contextUsage !== void 0 && parsedUsage === void 0 || input.tasks !== void 0 && parsedTasks === void 0 || input.rewind !== void 0 && parsedRewind === void 0) throw new Error("dsh-claude: invalid sidecar projection");
92
194
  return {
93
195
  schemaVersion: SIDECAR_SCHEMA_VERSION,
94
196
  revision: input.revision,
95
197
  activities,
96
198
  ...parsedBinding === void 0 ? {} : { binding: parsedBinding },
97
199
  ...parsedUsage === void 0 ? {} : { contextUsage: parsedUsage },
98
- ...parsedTasks === void 0 ? {} : { tasks: parsedTasks }
200
+ ...parsedTasks === void 0 ? {} : { tasks: parsedTasks },
201
+ ...parsedRewind === void 0 ? {} : { rewind: parsedRewind }
99
202
  };
100
203
  }
101
204
  function compareActivity(left, right) {
@@ -279,6 +382,35 @@ var ClaudeSidecarRepository = class {
279
382
  value: normalized
280
383
  });
281
384
  }
385
+ /** Land one planned rewind: hidden ranges, surviving anchors, and the fork
386
+ * target the next Claude spawn consumes. */
387
+ writeRewind(sessionId, value) {
388
+ return this.#update(sessionId, (current) => ({
389
+ ...current,
390
+ rewind: value
391
+ }), false, { kind: "sync" });
392
+ }
393
+ /** Remember where Claude's chain ended for one completed DSH turn. */
394
+ recordRewindAnchor(sessionId, turn, uuid) {
395
+ return this.#update(sessionId, (current) => ({
396
+ ...current,
397
+ rewind: recordRewindAnchor(current.rewind ?? EMPTY_REWIND_STATE, {
398
+ turn,
399
+ uuid
400
+ })
401
+ }));
402
+ }
403
+ /** Disarm the fork target once a Claude process has resumed at it, so a
404
+ * later respawn continues the rewound session instead of re-truncating it. */
405
+ clearRewindPending(sessionId) {
406
+ return this.#update(sessionId, (current) => current.rewind?.pending === void 0 ? current : {
407
+ ...current,
408
+ rewind: {
409
+ ranges: current.rewind.ranges,
410
+ anchors: current.rewind.anchors
411
+ }
412
+ });
413
+ }
282
414
  importLegacy(sessionId, events) {
283
415
  const importedActivities = events.filter((event) => event.type === CLAUDE_ACTIVITY_EVENT).map((event) => activity(event.data)).filter((item) => item !== void 0);
284
416
  const importedBinding = latestClaudeSessionBinding(events);
@@ -654,12 +786,15 @@ function createUserQuestionBridge(userQuestions, activeContext) {
654
786
  }
655
787
  //#endregion
656
788
  //#region src/sdk-messages.ts
657
- function record$10(value) {
789
+ function record$11(value) {
658
790
  return value !== null && typeof value === "object" ? value : void 0;
659
791
  }
660
792
  function string$3(value) {
661
793
  return typeof value === "string" ? value : void 0;
662
794
  }
795
+ function finiteNumber(value) {
796
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
797
+ }
663
798
  function taskUsageOf(usage) {
664
799
  if (usage === void 0) return void 0;
665
800
  const normalized = {};
@@ -668,20 +803,28 @@ function taskUsageOf(usage) {
668
803
  if (typeof usage.duration_ms === "number") normalized.durationMs = usage.duration_ms;
669
804
  return Object.keys(normalized).length === 0 ? void 0 : normalized;
670
805
  }
671
- function resultUsage(message) {
672
- const usage = record$10(message.usage);
806
+ /** Read one Anthropic `usage` envelope. Shared by the per-call sample on an
807
+ * assistant message and the turn total on a result message. */
808
+ function usageOf(usage) {
673
809
  const normalized = {};
674
- if (usage !== void 0) {
675
- if (typeof usage.input_tokens === "number") normalized.inputTokens = usage.input_tokens;
676
- if (typeof usage.output_tokens === "number") normalized.outputTokens = usage.output_tokens;
677
- if (typeof usage.cache_read_input_tokens === "number") normalized.cacheReadTokens = usage.cache_read_input_tokens;
678
- if (typeof usage.cache_creation_input_tokens === "number") normalized.cacheCreationTokens = usage.cache_creation_input_tokens;
679
- }
810
+ if (usage === void 0) return normalized;
811
+ if (typeof usage.input_tokens === "number") normalized.inputTokens = usage.input_tokens;
812
+ if (typeof usage.output_tokens === "number") normalized.outputTokens = usage.output_tokens;
813
+ if (typeof usage.cache_read_input_tokens === "number") normalized.cacheReadTokens = usage.cache_read_input_tokens;
814
+ if (typeof usage.cache_creation_input_tokens === "number") normalized.cacheCreationTokens = usage.cache_creation_input_tokens;
815
+ return normalized;
816
+ }
817
+ function hasUsageCounts(usage) {
818
+ return usage.inputTokens !== void 0 || usage.outputTokens !== void 0 || usage.cacheReadTokens !== void 0 || usage.cacheCreationTokens !== void 0;
819
+ }
820
+ function resultUsage(message) {
821
+ const normalized = usageOf(record$11(message.usage));
680
822
  if (typeof message.total_cost_usd === "number") normalized.cumulativeCostUsd = message.total_cost_usd;
681
823
  return normalized;
682
824
  }
683
825
  function normalizeAssistant(message) {
684
- const content = record$10(message.message)?.content;
826
+ const envelope = record$11(message.message);
827
+ const content = envelope?.content;
685
828
  if (!Array.isArray(content)) return [{
686
829
  kind: "protocol-error",
687
830
  title: "Malformed Claude assistant message",
@@ -690,7 +833,7 @@ function normalizeAssistant(message) {
690
833
  const parentToolUseId = string$3(message.parent_tool_use_id);
691
834
  const normalized = [];
692
835
  for (const item of content) {
693
- const block = record$10(item);
836
+ const block = record$11(item);
694
837
  if (block === void 0) continue;
695
838
  if (block.type === "text") {
696
839
  const text = string$3(block.text);
@@ -719,11 +862,17 @@ function normalizeAssistant(message) {
719
862
  });
720
863
  }
721
864
  }
865
+ const usage = usageOf(record$11(envelope?.usage));
866
+ if (hasUsageCounts(usage)) normalized.push({
867
+ kind: "request-usage",
868
+ usage,
869
+ ...parentToolUseId === void 0 ? {} : { parentToolUseId }
870
+ });
722
871
  return normalized;
723
872
  }
724
873
  function normalizeUser(message) {
725
874
  if (message.isReplay === true) return [];
726
- const content = record$10(message.message)?.content;
875
+ const content = record$11(message.message)?.content;
727
876
  if (typeof content === "string") return [];
728
877
  if (!Array.isArray(content)) return [{
729
878
  kind: "protocol-error",
@@ -733,7 +882,7 @@ function normalizeUser(message) {
733
882
  const parentToolUseId = string$3(message.parent_tool_use_id);
734
883
  const normalized = [];
735
884
  for (const item of content) {
736
- const block = record$10(item);
885
+ const block = record$11(item);
737
886
  if (block?.type !== "tool_result") continue;
738
887
  const toolUseId = string$3(block.tool_use_id);
739
888
  if (toolUseId === void 0) continue;
@@ -814,7 +963,7 @@ function normalizeSystem(message) {
814
963
  const summary = string$3(message.summary);
815
964
  const subagentType = string$3(message.subagent_type);
816
965
  const lastToolName = string$3(message.last_tool_name);
817
- const usage = taskUsageOf(record$10(message.usage));
966
+ const usage = taskUsageOf(record$11(message.usage));
818
967
  return [{
819
968
  kind: "subagent",
820
969
  title: summary ?? description ?? "Claude subagent update",
@@ -830,7 +979,7 @@ function normalizeSystem(message) {
830
979
  }];
831
980
  }
832
981
  if (subtype === "task_updated") {
833
- const patch = record$10(message.patch);
982
+ const patch = record$11(message.patch);
834
983
  const status = string$3(patch?.status);
835
984
  const taskId = string$3(message.task_id);
836
985
  const description = string$3(patch?.description);
@@ -853,7 +1002,7 @@ function normalizeSystem(message) {
853
1002
  const taskId = string$3(message.task_id);
854
1003
  const summary = string$3(message.summary);
855
1004
  const taskStatus = failed ? "failed" : stopped ? "stopped" : "completed";
856
- const usage = taskUsageOf(record$10(message.usage));
1005
+ const usage = taskUsageOf(record$11(message.usage));
857
1006
  return [{
858
1007
  kind: "subagent",
859
1008
  title: summary ?? taskId ?? "Claude subagent finished",
@@ -868,7 +1017,7 @@ function normalizeSystem(message) {
868
1017
  if (subtype === "background_tasks_changed") return [{
869
1018
  kind: "background-tasks",
870
1019
  tasks: (Array.isArray(message.tasks) ? message.tasks : []).flatMap((item) => {
871
- const entry = record$10(item);
1020
+ const entry = record$11(item);
872
1021
  const taskId = string$3(entry?.task_id);
873
1022
  const description = string$3(entry?.description);
874
1023
  const taskType = string$3(entry?.task_type);
@@ -885,6 +1034,20 @@ function normalizeSystem(message) {
885
1034
  title: "Claude API retry",
886
1035
  detail: message
887
1036
  }];
1037
+ if (subtype === "compact_boundary") {
1038
+ const metadata = record$11(message.compact_metadata);
1039
+ const trigger = metadata?.trigger === "auto" || metadata?.trigger === "manual" ? metadata.trigger : void 0;
1040
+ const preTokens = finiteNumber(metadata?.pre_tokens);
1041
+ const postTokens = finiteNumber(metadata?.post_tokens);
1042
+ const durationMs = finiteNumber(metadata?.duration_ms);
1043
+ return [{
1044
+ kind: "compaction",
1045
+ ...trigger === void 0 ? {} : { trigger },
1046
+ ...preTokens === void 0 ? {} : { preTokens },
1047
+ ...postTokens === void 0 ? {} : { postTokens },
1048
+ ...durationMs === void 0 ? {} : { durationMs }
1049
+ }];
1050
+ }
888
1051
  if (subtype === "informational" || subtype === "notification" || subtype === "local_command_output") return [{
889
1052
  kind: message.level === "warning" ? "warning" : "status",
890
1053
  title: string$3(message.content) ?? string$3(message.text) ?? "Claude Code notice",
@@ -911,10 +1074,10 @@ const RESULT_ERROR_SUBTYPES = /* @__PURE__ */ new Set([
911
1074
  function normalizeSdkMessage(message) {
912
1075
  const value = message;
913
1076
  if (value.type === "stream_event") {
914
- const event = record$10(value.event);
1077
+ const event = record$11(value.event);
915
1078
  const parentToolUseId = string$3(value.parent_tool_use_id);
916
1079
  if (event?.type === "content_block_delta") {
917
- const delta = record$10(event.delta);
1080
+ const delta = record$11(event.delta);
918
1081
  if (delta?.type === "text_delta") {
919
1082
  const text = string$3(delta.text);
920
1083
  return text === void 0 ? [] : [{
@@ -949,7 +1112,7 @@ function normalizeSdkMessage(message) {
949
1112
  const errors = Array.isArray(value.errors) ? value.errors.filter((item) => typeof item === "string") : void 0;
950
1113
  const terminalReason = string$3(value.terminal_reason);
951
1114
  const userMessageUuid = string$3(value.user_message_uuid);
952
- const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$10(item)).filter((item) => item !== void 0).map((item) => {
1115
+ const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$11(item)).filter((item) => item !== void 0).map((item) => {
953
1116
  const toolName = string$3(item.tool_name);
954
1117
  const toolUseId = string$3(item.tool_use_id);
955
1118
  return toolName === void 0 || toolUseId === void 0 ? void 0 : {
@@ -975,7 +1138,7 @@ function normalizeSdkMessage(message) {
975
1138
  detail: value.error ?? value.output
976
1139
  }];
977
1140
  if (value.type === "rate_limit_event") {
978
- const status = string$3(record$10(value.rate_limit_info)?.status);
1141
+ const status = string$3(record$11(value.rate_limit_info)?.status);
979
1142
  return [{
980
1143
  kind: "status",
981
1144
  title: status !== void 0 && status !== "allowed" ? "Claude rate limit is blocking requests" : "Claude rate limit status changed",
@@ -1014,7 +1177,7 @@ const FIXED_WINDOWS = [
1014
1177
  "seven_day_opus",
1015
1178
  "seven_day_sonnet"
1016
1179
  ];
1017
- function record$9(value) {
1180
+ function record$10(value) {
1018
1181
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1019
1182
  }
1020
1183
  /** Utilization is documented as 0-100; clamp so a server glitch cannot render
@@ -1026,7 +1189,7 @@ function resetsAt(value) {
1026
1189
  return typeof value === "string" && value.length > 0 ? value : void 0;
1027
1190
  }
1028
1191
  function window(id, source, label) {
1029
- const entry = record$9(source);
1192
+ const entry = record$10(source);
1030
1193
  if (entry === void 0) return void 0;
1031
1194
  const used = utilization(entry.utilization);
1032
1195
  const reset = resetsAt(entry.resets_at);
@@ -1040,9 +1203,9 @@ function window(id, source, label) {
1040
1203
  }
1041
1204
  /** Project the SDK's `/usage` response onto the windows the settings card shows. */
1042
1205
  function normalizePlanUsage(value, fetchedAt) {
1043
- const response = record$9(value);
1206
+ const response = record$10(value);
1044
1207
  const subscription = typeof response?.subscription_type === "string" ? response.subscription_type : void 0;
1045
- const limits = record$9(response?.rate_limits);
1208
+ const limits = record$10(response?.rate_limits);
1046
1209
  if (response?.rate_limits_available !== true || limits === void 0) return {
1047
1210
  available: false,
1048
1211
  ...subscription === void 0 ? {} : { subscription },
@@ -1050,7 +1213,7 @@ function normalizePlanUsage(value, fetchedAt) {
1050
1213
  fetchedAt
1051
1214
  };
1052
1215
  const windows = [...FIXED_WINDOWS.map((id) => window(id, limits[id])), ...(Array.isArray(limits.model_scoped) ? limits.model_scoped : []).map((entry, index) => {
1053
- const name = record$9(entry)?.display_name;
1216
+ const name = record$10(entry)?.display_name;
1054
1217
  return window(`model:${typeof name === "string" ? name : index}`, entry, typeof name === "string" ? name : void 0);
1055
1218
  })].filter((entry) => entry !== void 0);
1056
1219
  return {
@@ -1086,7 +1249,9 @@ async function probePlanUsage(executablePath, fetchedAt, factory = query) {
1086
1249
  (async () => {
1087
1250
  for await (const _ of query$1);
1088
1251
  })().catch(() => void 0);
1089
- return normalizePlanUsage(await readPlanUsageFrom(query$1), fetchedAt);
1252
+ return normalizePlanUsage(await Promise.race([readPlanUsageFrom(query$1), new Promise((_resolve, reject) => {
1253
+ setTimeout(() => reject(/* @__PURE__ */ new Error("dsh-claude: the plan usage request did not answer in time")), PLAN_USAGE_TIMEOUT_MS).unref?.();
1254
+ })]), fetchedAt);
1090
1255
  } finally {
1091
1256
  clearTimeout(timer);
1092
1257
  lifetime.abort();
@@ -1237,6 +1402,14 @@ async function withTimeout(operation, timeoutMs, label) {
1237
1402
  if (timer !== void 0) clearTimeout(timer);
1238
1403
  }
1239
1404
  }
1405
+ /** The uuid of one main-chain transcript entry, or undefined for anything a
1406
+ * rewind must not fork at: stream partials, results, and sidechain traffic. */
1407
+ function chainEntryUuid(message) {
1408
+ if (message.type !== "assistant" && message.type !== "user") return void 0;
1409
+ const envelope = message;
1410
+ if (typeof envelope.parent_tool_use_id === "string") return void 0;
1411
+ return typeof envelope.uuid === "string" && envelope.uuid.length > 0 ? envelope.uuid : void 0;
1412
+ }
1240
1413
  function sdkUserMessage(prompt, uuid) {
1241
1414
  return {
1242
1415
  type: "user",
@@ -1307,13 +1480,39 @@ var ClaudeSupervisor = class {
1307
1480
  }
1308
1481
  async contextUsage(agent, model = this.#config.defaultModel) {
1309
1482
  const usage = await this.#runMetadata(agent, model, (query) => query.getContextUsage());
1310
- const contextWindow = usage.rawMaxTokens > 0 ? usage.rawMaxTokens : usage.maxTokens;
1311
- if (contextWindow > 0) {
1312
- this.#contextWindows.set(model, contextWindow);
1313
- this.#contextWindows.set(usage.model, contextWindow);
1314
- }
1483
+ this.#recordContextWindow(model, usage);
1315
1484
  return usage;
1316
1485
  }
1486
+ /** Cache a window under both the selector id the caller asked for and the
1487
+ * concrete model the CLI reports, so either name resolves it later. */
1488
+ #recordContextWindow(model, usage) {
1489
+ const contextWindow = usage.rawMaxTokens > 0 ? usage.rawMaxTokens : usage.maxTokens;
1490
+ if (contextWindow <= 0) return;
1491
+ this.#contextWindows.set(model, contextWindow);
1492
+ this.#contextWindows.set(usage.model, contextWindow);
1493
+ }
1494
+ /** Learn a model's context window the first time a turn finishes on it.
1495
+ *
1496
+ * DSH hides its context meter entirely unless the route publishes a
1497
+ * capacity, and these numbers move with Claude releases — so none are
1498
+ * hardcoded; the CLI is asked over the session's own live process.
1499
+ *
1500
+ * Turn completion is the earliest honest moment to ask. `entry.model` is
1501
+ * already the model that just ran, so no model switch is provoked — which
1502
+ * rules out asking from `resolveModel`, since DSH resolves every model in
1503
+ * the catalog to build its picker and `#metadataEntry` would switch the live
1504
+ * session once per entry. And nothing is lost by waiting: the meter needs a
1505
+ * usage sample too, and no turn has reported one before the first turn ends.
1506
+ *
1507
+ * Best-effort — a failure leaves the window unknown (the meter stays hidden,
1508
+ * exactly as before) and the next completed turn tries again. */
1509
+ async #learnContextWindow(entry) {
1510
+ if (this.#contextWindows.has(entry.model)) return;
1511
+ try {
1512
+ const usage = await withTimeout(entry.query.getContextUsage(), CLAUDE_METADATA_TIMEOUT_MS, "Claude context window probe");
1513
+ this.#recordContextWindow(entry.model, usage);
1514
+ } catch {}
1515
+ }
1317
1516
  contextWindow(model) {
1318
1517
  return this.#contextWindows.get(model);
1319
1518
  }
@@ -1362,7 +1561,7 @@ var ClaudeSupervisor = class {
1362
1561
  } else {
1363
1562
  await this.#syncPermissionMode(entry);
1364
1563
  if (model !== entry.model) {
1365
- await entry.query.setModel(model);
1564
+ await this.#control(entry, entry.query.setModel(model), "Claude Code model switch");
1366
1565
  entry.model = model;
1367
1566
  }
1368
1567
  }
@@ -1381,6 +1580,7 @@ var ClaudeSupervisor = class {
1381
1580
  transcriptText: "",
1382
1581
  transcriptTextOrdinal: void 0,
1383
1582
  thinking: "",
1583
+ requestUsage: void 0,
1384
1584
  aborted: false,
1385
1585
  deniedToolUseIds: /* @__PURE__ */ new Set(),
1386
1586
  callNames: /* @__PURE__ */ new Map(),
@@ -1432,7 +1632,7 @@ var ClaudeSupervisor = class {
1432
1632
  const entry = await this.#metadataEntry(agent, model);
1433
1633
  try {
1434
1634
  await entry.sdkInitialization;
1435
- return await withTimeout(operation(entry.query, entry), CLAUDE_METADATA_TIMEOUT_MS, "Claude metadata request");
1635
+ return await this.#control(entry, operation(entry.query, entry), "Claude metadata request");
1436
1636
  } finally {
1437
1637
  entry.lastUsedAt = Date.now();
1438
1638
  if (entry.active === void 0 && entry.state === "idle") this.#armIdleTimer(entry);
@@ -1462,15 +1662,34 @@ var ClaudeSupervisor = class {
1462
1662
  }
1463
1663
  await this.#syncPermissionMode(entry);
1464
1664
  if (model !== entry.model) {
1465
- await entry.query.setModel(model);
1665
+ await this.#control(entry, entry.query.setModel(model), "Claude Code model switch");
1466
1666
  entry.model = model;
1467
1667
  }
1468
1668
  return entry;
1469
1669
  }
1670
+ /** Run one SDK control request against a live entry, and discard the entry if
1671
+ * it does not answer.
1672
+ *
1673
+ * Turn admission and every metadata read share one process-wide gate, so an
1674
+ * unbounded control request stalls every session until the Host restarts.
1675
+ * Bounding it is only half the cure: a timeout also proves this query has
1676
+ * stopped answering, and keeping the entry means the next caller reuses the
1677
+ * same dead process — timing out again, forever. Discarding it lets the next
1678
+ * attempt spawn a fresh one. Every control request goes through here so a
1679
+ * new call site cannot quietly reintroduce either half. */
1680
+ async #control(entry, operation, label, timeoutMs = CLAUDE_METADATA_TIMEOUT_MS) {
1681
+ try {
1682
+ return await withTimeout(operation, timeoutMs, label);
1683
+ } catch (error) {
1684
+ if (this.#entries.get(entry.sessionId) === entry) this.#entries.delete(entry.sessionId);
1685
+ await this.#disposeEntry(entry);
1686
+ throw error;
1687
+ }
1688
+ }
1470
1689
  async #syncPermissionMode(entry) {
1471
1690
  const mode = claudePermissionMode(entry.ownerAgent.session.events);
1472
1691
  if (mode === entry.permissionMode) return;
1473
- await entry.query.setPermissionMode(mode);
1692
+ await this.#control(entry, entry.query.setPermissionMode(mode), "Claude Code permission mode switch");
1474
1693
  entry.permissionMode = mode;
1475
1694
  }
1476
1695
  async disposeSession(sessionId) {
@@ -1498,7 +1717,11 @@ var ClaudeSupervisor = class {
1498
1717
  const cwd = agent.session.header.cwd ?? process.cwd();
1499
1718
  const input = new AsyncQueue();
1500
1719
  const lifetime = new AbortController();
1501
- const binding = (await this.#sidecar.importLegacy(sessionId, agent.session.events)).binding;
1720
+ const projection = await this.#sidecar.importLegacy(sessionId, agent.session.events);
1721
+ const binding = projection.binding;
1722
+ const pendingRewind = projection.rewind?.pending;
1723
+ const forkAt = pendingRewind !== void 0 && "resumeAt" in pendingRewind ? pendingRewind.resumeAt : void 0;
1724
+ const startFresh = pendingRewind !== void 0 && "fresh" in pendingRewind;
1502
1725
  const permissionMode = claudePermissionMode(agent.session.events);
1503
1726
  const entry = {
1504
1727
  sessionId,
@@ -1511,8 +1734,10 @@ var ClaudeSupervisor = class {
1511
1734
  lastUsedAt: Date.now(),
1512
1735
  input,
1513
1736
  lifetime,
1514
- claudeSessionId: binding?.claudeSessionId,
1515
- expectedResume: binding?.claudeSessionId,
1737
+ claudeSessionId: startFresh ? void 0 : binding?.claudeSessionId,
1738
+ expectedResume: startFresh || forkAt !== void 0 ? void 0 : binding?.claudeSessionId,
1739
+ lastChainUuid: void 0,
1740
+ consumedRewind: pendingRewind !== void 0,
1516
1741
  initialized: false,
1517
1742
  idleTimer: void 0,
1518
1743
  tasks: /* @__PURE__ */ new Map(),
@@ -1563,7 +1788,10 @@ var ClaudeSupervisor = class {
1563
1788
  spawnClaudeCodeProcess: createManagedClaudeSpawner(this.#runtime, this.#config.executablePath, (process) => {
1564
1789
  entry.process = process;
1565
1790
  }),
1566
- ...binding === void 0 ? {} : { resume: binding.claudeSessionId },
1791
+ ...binding === void 0 || startFresh ? {} : {
1792
+ resume: binding.claudeSessionId,
1793
+ ...forkAt === void 0 ? {} : { resumeSessionAt: forkAt }
1794
+ },
1567
1795
  model,
1568
1796
  ...thinkingMode === void 0 ? {} : thinkingMode === "off" ? { thinking: { type: "disabled" } } : thinkingMode === "ultracode" ? { settings: { ultracode: true } } : { effort: thinkingMode }
1569
1797
  };
@@ -1580,7 +1808,11 @@ var ClaudeSupervisor = class {
1580
1808
  }
1581
1809
  async #pump(entry) {
1582
1810
  try {
1583
- for await (const sdkMessage of entry.query) for (const message of normalizeSdkMessage(sdkMessage)) await this.#handleMessage(entry, message);
1811
+ for await (const sdkMessage of entry.query) {
1812
+ const chainUuid = chainEntryUuid(sdkMessage);
1813
+ if (chainUuid !== void 0) entry.lastChainUuid = chainUuid;
1814
+ for (const message of normalizeSdkMessage(sdkMessage)) await this.#handleMessage(entry, message);
1815
+ }
1584
1816
  if (entry.state !== "disposed") await this.#handleDisconnect(entry, /* @__PURE__ */ new Error("Claude Code stream ended"));
1585
1817
  } catch (error) {
1586
1818
  if (entry.state !== "disposed") await this.#handleDisconnect(entry, error);
@@ -1603,6 +1835,10 @@ var ClaudeSupervisor = class {
1603
1835
  cliVersion: message.cliVersion,
1604
1836
  cwd: message.cwd
1605
1837
  });
1838
+ if (entry.consumedRewind) {
1839
+ entry.consumedRewind = false;
1840
+ await this.#sidecar.clearRewindPending(entry.sessionId);
1841
+ }
1606
1842
  return;
1607
1843
  }
1608
1844
  const taskId = message.kind === "subagent" ? message.taskId : void 0;
@@ -1701,6 +1937,23 @@ var ClaudeSupervisor = class {
1701
1937
  isError: message.phase === "failed"
1702
1938
  });
1703
1939
  return;
1940
+ case "request-usage":
1941
+ if (message.parentToolUseId === void 0) active.requestUsage = message.usage;
1942
+ return;
1943
+ case "compaction":
1944
+ this.#closeTranscriptTextSegment(active);
1945
+ await this.#appendActivity(active, {
1946
+ kind: "compaction",
1947
+ phase: "completed",
1948
+ title: "Claude compacted the conversation",
1949
+ detail: {
1950
+ ...message.trigger === void 0 ? {} : { trigger: message.trigger },
1951
+ ...message.preTokens === void 0 ? {} : { preTokens: message.preTokens },
1952
+ ...message.postTokens === void 0 ? {} : { postTokens: message.postTokens },
1953
+ ...message.durationMs === void 0 ? {} : { durationMs: message.durationMs }
1954
+ }
1955
+ });
1956
+ return;
1704
1957
  case "status":
1705
1958
  case "warning":
1706
1959
  case "unknown":
@@ -1832,6 +2085,21 @@ var ClaudeSupervisor = class {
1832
2085
  entry.taskSnapshotAt = Date.now();
1833
2086
  await this.#sidecar.writeTasks(entry.sessionId, [...entry.tasks.values()]).catch(() => void 0);
1834
2087
  }
2088
+ /** What DSH is told about token usage.
2089
+ *
2090
+ * `TokenUsage` is documented as "token accounting for ONE model call", and
2091
+ * DSH's token meter divides `uncachedInput + cacheRead + cacheWrite` by the
2092
+ * context window to draw context pressure. One Claude turn makes many calls
2093
+ * and the CLI's result usage sums all of them, so reporting that sum pinned
2094
+ * the meter at 100%: a 35-call turn reads the same prompt from cache 35
2095
+ * times, which sums past the window without the conversation ever growing.
2096
+ * The newest single call answers "how big is this conversation now".
2097
+ *
2098
+ * The sidecar activity keeps the turn total instead — that is the audit and
2099
+ * cost record, and nothing divides it by a window. */
2100
+ #reportedUsage(active, result) {
2101
+ return active.requestUsage ?? result.usage;
2102
+ }
1835
2103
  async #completeProgressSegment(active, result, recordUsage = true) {
1836
2104
  if (recordUsage && (result.usage.inputTokens !== void 0 || result.usage.outputTokens !== void 0 || result.usage.cumulativeCostUsd !== void 0)) {
1837
2105
  await this.#appendSafely(active, {
@@ -1843,7 +2111,7 @@ var ClaudeSupervisor = class {
1843
2111
  });
1844
2112
  active.output.push({
1845
2113
  type: "usage",
1846
- usage: result.usage
2114
+ usage: this.#reportedUsage(active, result)
1847
2115
  });
1848
2116
  }
1849
2117
  if (!active.sawTextDelta && active.text.length === 0 && result.text !== void 0) {
@@ -1878,6 +2146,7 @@ var ClaudeSupervisor = class {
1878
2146
  entry.active = void 0;
1879
2147
  entry.state = "idle";
1880
2148
  entry.lastUsedAt = Date.now();
2149
+ await this.#recordChainAnchor(entry, active);
1881
2150
  this.#armIdleTimer(entry);
1882
2151
  return;
1883
2152
  }
@@ -1891,7 +2160,7 @@ var ClaudeSupervisor = class {
1891
2160
  });
1892
2161
  active.output.push({
1893
2162
  type: "usage",
1894
- usage: result.usage
2163
+ usage: this.#reportedUsage(active, result)
1895
2164
  });
1896
2165
  }
1897
2166
  const unmatchedDenials = (result.permissionDenials ?? []).filter((denial) => !active.deniedToolUseIds.has(denial.toolUseId));
@@ -1959,8 +2228,20 @@ var ClaudeSupervisor = class {
1959
2228
  entry.active = void 0;
1960
2229
  entry.state = "idle";
1961
2230
  entry.lastUsedAt = Date.now();
2231
+ await this.#recordChainAnchor(entry, active);
2232
+ await this.#learnContextWindow(entry);
1962
2233
  this.#armIdleTimer(entry);
1963
2234
  }
2235
+ /** Pin where Claude's chain ended for the DSH turn that just settled, so a
2236
+ * later rewind of the following turn can fork exactly here. Best effort:
2237
+ * a missing anchor only makes a rewind fall back to an earlier turn. */
2238
+ async #recordChainAnchor(entry, active) {
2239
+ const uuid = entry.lastChainUuid;
2240
+ if (uuid === void 0) return;
2241
+ try {
2242
+ await this.#sidecar.recordRewindAnchor(entry.sessionId, active.cursor.turn, uuid);
2243
+ } catch {}
2244
+ }
1964
2245
  async #upsertTranscriptText(active) {
1965
2246
  if (active.transcriptText.length === 0) return;
1966
2247
  const ordinal = active.transcriptTextOrdinal ?? active.cursor.nextOrdinal++;
@@ -2159,28 +2440,28 @@ const MODELS = [
2159
2440
  {
2160
2441
  id: "default",
2161
2442
  name: "Default (recommended)",
2162
- description: "Use Claude Code’s recommended default model."
2443
+ description: ""
2163
2444
  },
2164
2445
  {
2165
2446
  id: "opus[1m]",
2166
2447
  name: "Opus (1M context)",
2167
- description: "Use Opus with a 1M-token context window.",
2448
+ description: "",
2168
2449
  contextWindow: 1e6
2169
2450
  },
2170
2451
  {
2171
2452
  id: "fable",
2172
2453
  name: "Fable",
2173
- description: "Use Fable, Claude Code’s most capable coding model."
2454
+ description: ""
2174
2455
  },
2175
2456
  {
2176
2457
  id: "sonnet",
2177
2458
  name: "Sonnet",
2178
- description: "Use Sonnet for efficient routine coding work."
2459
+ description: ""
2179
2460
  },
2180
2461
  {
2181
2462
  id: "haiku",
2182
2463
  name: "Haiku",
2183
- description: "Use Haiku for fast, lightweight tasks."
2464
+ description: ""
2184
2465
  }
2185
2466
  ];
2186
2467
  const THINKING_MODES = [
@@ -2594,7 +2875,7 @@ function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolution
2594
2875
  }
2595
2876
  //#endregion
2596
2877
  //#region src/projection-routes.ts
2597
- const MAX_SESSION_ID_CHARS$4 = 1024;
2878
+ const MAX_SESSION_ID_CHARS$6 = 1024;
2598
2879
  /** Slow-moving metadata refresh and stream heartbeat cadence; deliberately off
2599
2880
  * the transcript hot path so git/gh latency never delays visible text. */
2600
2881
  const META_REFRESH_MS = 5e3;
@@ -2608,7 +2889,7 @@ function targetFromUrl(rawUrl) {
2608
2889
  if (stream) encoded = encoded.slice(0, -7);
2609
2890
  if (encoded.length === 0 || encoded.includes("/")) return void 0;
2610
2891
  const sessionId = decodeURIComponent(encoded);
2611
- if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS$4) return void 0;
2892
+ if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS$6) return void 0;
2612
2893
  return {
2613
2894
  sessionId,
2614
2895
  stream
@@ -2627,7 +2908,8 @@ function envelope(projection, meta) {
2627
2908
  ...projection.contextUsage === void 0 ? {} : { contextUsage: projection.contextUsage },
2628
2909
  ...projection.tasks === void 0 ? {} : { tasks: projection.tasks },
2629
2910
  ...meta.repository === void 0 ? {} : { repository: meta.repository },
2630
- reviewComments: meta.reviewComments
2911
+ reviewComments: meta.reviewComments,
2912
+ ...projection.rewind === void 0 ? {} : { rewind: { ranges: projection.rewind.ranges } }
2631
2913
  };
2632
2914
  }
2633
2915
  /** Register the browser-readable, credential-free sidecar projection endpoint.
@@ -2768,7 +3050,7 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
2768
3050
  }
2769
3051
  //#endregion
2770
3052
  //#region src/repository-status.ts
2771
- const MAX_OUTPUT_BYTES$3 = 65536;
3053
+ const MAX_OUTPUT_BYTES$4 = 65536;
2772
3054
  const MAX_DIFF_BYTES = 262144;
2773
3055
  const MAX_FILE_BYTES = 8388608;
2774
3056
  const MAX_UNTRACKED_DIFFS = 50;
@@ -2788,7 +3070,7 @@ async function collect$3(handle) {
2788
3070
  lossy: stdout?.lossy === true
2789
3071
  };
2790
3072
  }
2791
- async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$3) {
3073
+ async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$4) {
2792
3074
  const signal = AbortSignal.timeout(timeoutMs);
2793
3075
  return collect$3(runtime.spawn({
2794
3076
  argv: [executable, ...args],
@@ -2796,7 +3078,7 @@ async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUT
2796
3078
  stdio: {
2797
3079
  stdin: "ignore",
2798
3080
  stdout: { maxBytes },
2799
- stderr: { maxBytes: MAX_OUTPUT_BYTES$3 }
3081
+ stderr: { maxBytes: MAX_OUTPUT_BYTES$4 }
2800
3082
  },
2801
3083
  graceMs: 1e3,
2802
3084
  signal,
@@ -2866,14 +3148,14 @@ function parseGitHubRemote(value) {
2866
3148
  if (match?.[1] === void 0 || match[2] === void 0) return void 0;
2867
3149
  return `${match[1]}/${match[2]}`;
2868
3150
  }
2869
- function record$8(value) {
3151
+ function record$9(value) {
2870
3152
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2871
3153
  }
2872
3154
  function aggregateChecks(value) {
2873
3155
  if (!Array.isArray(value) || value.length === 0) return "none";
2874
3156
  let pending = false;
2875
3157
  for (const item of value) {
2876
- const check = record$8(item);
3158
+ const check = record$9(item);
2877
3159
  if (check === void 0) continue;
2878
3160
  const conclusion = typeof check.conclusion === "string" ? check.conclusion.toUpperCase() : void 0;
2879
3161
  const status = typeof check.status === "string" ? check.status.toUpperCase() : void 0;
@@ -2897,7 +3179,7 @@ function reviewState(value) {
2897
3179
  return "none";
2898
3180
  }
2899
3181
  function parsePullRequest(value) {
2900
- const input = record$8(value);
3182
+ const input = record$9(value);
2901
3183
  if (input === void 0 || !Number.isSafeInteger(input.number) || Number(input.number) <= 0 || typeof input.title !== "string" || typeof input.url !== "string") return void 0;
2902
3184
  let url;
2903
3185
  try {
@@ -2918,7 +3200,7 @@ function parsePullRequest(value) {
2918
3200
  review: reviewState(input.reviewDecision),
2919
3201
  checks: aggregateChecks(input.statusCheckRollup),
2920
3202
  ...typeof input.mergeStateStatus === "string" ? { mergeState: bounded(input.mergeStateStatus) } : {},
2921
- ...typeof record$8(input.author)?.login === "string" ? { author: bounded(String(record$8(input.author)?.login)) } : {},
3203
+ ...typeof record$9(input.author)?.login === "string" ? { author: bounded(String(record$9(input.author)?.login)) } : {},
2922
3204
  ...typeof input.createdAt === "string" && Number.isFinite(Date.parse(input.createdAt)) ? { createdAt: new Date(input.createdAt).toISOString() } : {},
2923
3205
  ...typeof input.mergedAt === "string" && Number.isFinite(Date.parse(input.mergedAt)) ? { mergedAt: new Date(input.mergedAt).toISOString() } : {},
2924
3206
  ...typeof input.baseRefName === "string" && bounded(input.baseRefName).length > 0 ? { baseBranch: bounded(input.baseRefName) } : {}
@@ -3212,7 +3494,7 @@ var RepositoryStatusService = class {
3212
3494
  };
3213
3495
  //#endregion
3214
3496
  //#region src/repository-setup.ts
3215
- const MAX_OUTPUT_BYTES$2 = 131072;
3497
+ const MAX_OUTPUT_BYTES$3 = 131072;
3216
3498
  const GIT_TIMEOUT_MS$2 = 1e4;
3217
3499
  const GIT_FETCH_TIMEOUT_MS = 6e4;
3218
3500
  const MAX_PATH_CHARS$2 = 4096;
@@ -3299,8 +3581,18 @@ var RepositorySetupService = class {
3299
3581
  this.#cleanupGraceMs = options.cleanupGraceMs ?? CLEANUP_GRACE_MS;
3300
3582
  }
3301
3583
  async listBranches(cwd) {
3584
+ const git = await this.#git();
3585
+ return this.#listBranches(git, await this.#repositoryRoot(git, safePath(cwd)));
3586
+ }
3587
+ /** Refresh remote-tracking refs before listing: a branch pushed after this
3588
+ * checkout last fetched has no local ref, so the picker cannot offer it. */
3589
+ async refreshBranches(cwd) {
3302
3590
  const git = await this.#git();
3303
3591
  const root = await this.#repositoryRoot(git, safePath(cwd));
3592
+ await this.#fetchRemotes(git, root);
3593
+ return this.#listBranches(git, root);
3594
+ }
3595
+ async #listBranches(git, root) {
3304
3596
  const [status, refs, remoteRefs] = await Promise.all([
3305
3597
  this.#run(git, [
3306
3598
  "status",
@@ -3514,6 +3806,16 @@ var RepositorySetupService = class {
3514
3806
  branch: localBranch
3515
3807
  };
3516
3808
  }
3809
+ async #fetchRemotes(git, root) {
3810
+ const fetched = await this.#run(git, [
3811
+ "-c",
3812
+ "credential.interactive=never",
3813
+ "fetch",
3814
+ "--all",
3815
+ "--prune"
3816
+ ], root, GIT_FETCH_TIMEOUT_MS);
3817
+ if (fetched.exitCode !== 0 || fetched.lossy) throw new RepositorySetupError("fetch-failed", "Git could not refresh remote references.");
3818
+ }
3517
3819
  async #checkout(info, branch) {
3518
3820
  if (info.current !== branch) {
3519
3821
  if (info.dirty) throw new RepositorySetupError("dirty-workspace", "Commit or stash workspace changes before switching branches.");
@@ -3542,14 +3844,7 @@ var RepositorySetupService = class {
3542
3844
  async #createWorktree(root, baseBranch, baseRef, explicitBranchName, reuseExistingBranch, progress) {
3543
3845
  const git = await this.#git();
3544
3846
  progress("fetching");
3545
- const fetched = await this.#run(git, [
3546
- "-c",
3547
- "credential.interactive=never",
3548
- "fetch",
3549
- "--all",
3550
- "--prune"
3551
- ], root, GIT_FETCH_TIMEOUT_MS);
3552
- if (fetched.exitCode !== 0 || fetched.lossy) throw new RepositorySetupError("fetch-failed", "Git could not refresh remote references before creating the worktree.");
3847
+ await this.#fetchRemotes(git, root);
3553
3848
  const suffix = randomUUID().slice(0, 8);
3554
3849
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/gu, "").replace(/\.\d{3}Z$/u, "Z");
3555
3850
  const branch = explicitBranchName ?? `${safeBranch(await this.#branchPrefix())}/${slug(baseBranch, "branch")}-${stamp}-${suffix}`;
@@ -3636,8 +3931,8 @@ var RepositorySetupService = class {
3636
3931
  cwd,
3637
3932
  stdio: {
3638
3933
  stdin: "ignore",
3639
- stdout: { maxBytes: MAX_OUTPUT_BYTES$2 },
3640
- stderr: { maxBytes: MAX_OUTPUT_BYTES$2 }
3934
+ stdout: { maxBytes: MAX_OUTPUT_BYTES$3 },
3935
+ stderr: { maxBytes: MAX_OUTPUT_BYTES$3 }
3641
3936
  },
3642
3937
  graceMs: 1e3,
3643
3938
  signal: AbortSignal.timeout(timeoutMs),
@@ -3678,7 +3973,7 @@ var RepositorySetupService = class {
3678
3973
  };
3679
3974
  //#endregion
3680
3975
  //#region src/repository-actions.ts
3681
- const MAX_OUTPUT_BYTES$1 = 262144;
3976
+ const MAX_OUTPUT_BYTES$2 = 262144;
3682
3977
  const MAX_PATCH_CHARS = 65536;
3683
3978
  const MAX_MESSAGE_CHARS = 512;
3684
3979
  const MAX_PR_TEXT_CHARS = 8192;
@@ -4005,7 +4300,7 @@ var RepositoryActionService = class {
4005
4300
  "--",
4006
4301
  ":(exclude)WARP.md",
4007
4302
  ":(exclude)**/WARP.md"
4008
- ], root, GIT_TIMEOUT_MS$1, MAX_OUTPUT_BYTES$1),
4303
+ ], root, GIT_TIMEOUT_MS$1, MAX_OUTPUT_BYTES$2),
4009
4304
  this.#run(git, [
4010
4305
  "diff",
4011
4306
  "--no-ext-diff",
@@ -4014,7 +4309,7 @@ var RepositoryActionService = class {
4014
4309
  "--",
4015
4310
  ":(exclude)WARP.md",
4016
4311
  ":(exclude)**/WARP.md"
4017
- ], root, GIT_TIMEOUT_MS$1, MAX_OUTPUT_BYTES$1)
4312
+ ], root, GIT_TIMEOUT_MS$1, MAX_OUTPUT_BYTES$2)
4018
4313
  ]);
4019
4314
  if (branchResult.exitCode !== 0) throw new RepositoryActionError("detached-head", "A detached HEAD cannot be committed from this panel.");
4020
4315
  if (headResult.exitCode !== 0 || statusResult.exitCode !== 0 || statusResult.lossy) throw new RepositoryActionError("repository-unavailable", "Repository state is unavailable.");
@@ -4108,14 +4403,14 @@ var RepositoryActionService = class {
4108
4403
  if (result.exitCode !== 0 || result.lossy) throw new RepositoryActionError(code, message);
4109
4404
  return result;
4110
4405
  }
4111
- #run(executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$1) {
4406
+ #run(executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$2) {
4112
4407
  return collect$1(this.#runtime.spawn({
4113
4408
  argv: [executable, ...args],
4114
4409
  cwd,
4115
4410
  stdio: {
4116
4411
  stdin: "ignore",
4117
4412
  stdout: { maxBytes },
4118
- stderr: { maxBytes: MAX_OUTPUT_BYTES$1 }
4413
+ stderr: { maxBytes: MAX_OUTPUT_BYTES$2 }
4119
4414
  },
4120
4415
  graceMs: 1e3,
4121
4416
  signal: AbortSignal.timeout(timeoutMs),
@@ -4125,20 +4420,20 @@ var RepositoryActionService = class {
4125
4420
  };
4126
4421
  //#endregion
4127
4422
  //#region src/repository-setup-routes.ts
4128
- const MAX_BODY_BYTES$4 = 16384;
4129
- function record$7(value) {
4423
+ const MAX_BODY_BYTES$5 = 16384;
4424
+ function record$8(value) {
4130
4425
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4131
4426
  }
4132
- async function readJson$4(req) {
4427
+ async function readJson$5(req) {
4133
4428
  const chunks = [];
4134
4429
  let size = 0;
4135
4430
  for await (const chunk of req) {
4136
4431
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4137
4432
  size += buffer.length;
4138
- if (size > MAX_BODY_BYTES$4) throw new RepositorySetupError("body-too-large", "The request body is too large.");
4433
+ if (size > MAX_BODY_BYTES$5) throw new RepositorySetupError("body-too-large", "The request body is too large.");
4139
4434
  chunks.push(buffer);
4140
4435
  }
4141
- const value = record$7(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4436
+ const value = record$8(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4142
4437
  if (value === void 0) throw new RepositorySetupError("invalid-request", "The request body is invalid.");
4143
4438
  return value;
4144
4439
  }
@@ -4198,6 +4493,11 @@ function registerRepositorySetupRoute(ctx, service) {
4198
4493
  if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
4199
4494
  const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
4200
4495
  try {
4496
+ if (pathname === `/plugins/dsh-claude/repository/setup/branches/refresh`) {
4497
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4498
+ const input = await readJson$5(req);
4499
+ return json(res, 200, await service.refreshBranches(string$2(input, "cwd")));
4500
+ }
4201
4501
  if (pathname === `/plugins/dsh-claude/repository/setup/branches`) {
4202
4502
  if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
4203
4503
  const cwd = new URL(req.url ?? "/", "http://localhost").searchParams.get("cwd");
@@ -4206,19 +4506,19 @@ function registerRepositorySetupRoute(ctx, service) {
4206
4506
  }
4207
4507
  if (pathname === "/plugins/dsh-claude/repository/setup") {
4208
4508
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4209
- const input = await readJson$4(req);
4509
+ const input = await readJson$5(req);
4210
4510
  if (typeof input.worktree !== "boolean") throw new RepositorySetupError("invalid-request", "The worktree field is required.");
4211
4511
  await streamSetup(res, service, input);
4212
4512
  return;
4213
4513
  }
4214
4514
  if (pathname === `/plugins/dsh-claude/repository/setup/cleanup`) {
4215
4515
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4216
- const input = await readJson$4(req);
4516
+ const input = await readJson$5(req);
4217
4517
  return json(res, 200, await service.cleanupMerged(string$2(input, "path"), string$2(input, "baseBranch")));
4218
4518
  }
4219
4519
  if (pathname === `/plugins/dsh-claude/repository/setup/bind`) {
4220
4520
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4221
- const input = await readJson$4(req);
4521
+ const input = await readJson$5(req);
4222
4522
  await service.bindLease(string$2(input, "leaseId"), string$2(input, "sessionId"));
4223
4523
  return json(res, 200, { ok: true });
4224
4524
  }
@@ -4236,8 +4536,8 @@ function registerRepositorySetupRoute(ctx, service) {
4236
4536
  }
4237
4537
  //#endregion
4238
4538
  //#region src/repository-action-routes.ts
4239
- const MAX_BODY_BYTES$3 = 16384;
4240
- const MAX_SESSION_ID_CHARS$3 = 1024;
4539
+ const MAX_BODY_BYTES$4 = 16384;
4540
+ const MAX_SESSION_ID_CHARS$5 = 1024;
4241
4541
  const ACTIONS = /* @__PURE__ */ new Set([
4242
4542
  "commit",
4243
4543
  "commit-push",
@@ -4246,25 +4546,25 @@ const ACTIONS = /* @__PURE__ */ new Set([
4246
4546
  "merge-pr",
4247
4547
  "update-branch"
4248
4548
  ]);
4249
- function record$6(value) {
4549
+ function record$7(value) {
4250
4550
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4251
4551
  }
4252
- async function readJson$3(req) {
4552
+ async function readJson$4(req) {
4253
4553
  const chunks = [];
4254
4554
  let size = 0;
4255
4555
  for await (const chunk of req) {
4256
4556
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4257
4557
  size += buffer.length;
4258
- if (size > MAX_BODY_BYTES$3) throw new RepositoryActionError("body-too-large", "The request body is too large.");
4558
+ if (size > MAX_BODY_BYTES$4) throw new RepositoryActionError("body-too-large", "The request body is too large.");
4259
4559
  chunks.push(buffer);
4260
4560
  }
4261
- const value = record$6(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4561
+ const value = record$7(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4262
4562
  if (value === void 0) throw new RepositoryActionError("invalid-request", "The request body is invalid.");
4263
4563
  return value;
4264
4564
  }
4265
4565
  function sessionId$1(url) {
4266
4566
  const value = url.searchParams.get("sessionId");
4267
- if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$3) throw new RepositoryActionError("invalid-session", "The session is invalid.");
4567
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$5) throw new RepositoryActionError("invalid-session", "The session is invalid.");
4268
4568
  return value;
4269
4569
  }
4270
4570
  function string$1(input, key) {
@@ -4313,12 +4613,12 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
4313
4613
  }
4314
4614
  if (url.pathname === `/plugins/dsh-claude/repository/action/message`) {
4315
4615
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4316
- const input = await readJson$3(req);
4616
+ const input = await readJson$4(req);
4317
4617
  return json(res, 200, { message: await service.generateMessage(cwd, string$1(input, "fingerprint")) });
4318
4618
  }
4319
4619
  if (url.pathname === "/plugins/dsh-claude/repository/action") {
4320
4620
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4321
- return json(res, 200, await service.execute(cwd, actionRequest(await readJson$3(req))));
4621
+ return json(res, 200, await service.execute(cwd, actionRequest(await readJson$4(req))));
4322
4622
  }
4323
4623
  return json(res, 404, { error: "not found" });
4324
4624
  } catch (error) {
@@ -4337,6 +4637,182 @@ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
4337
4637
  }), "dsh-claude: repository action route");
4338
4638
  }
4339
4639
  //#endregion
4640
+ //#region src/editor-open.ts
4641
+ const MAX_OUTPUT_BYTES$1 = 8192;
4642
+ /** Long enough for a launcher shim to fail loudly, short enough that the
4643
+ * request returns while the IDE is still booting. On Windows and Linux the
4644
+ * IDE binary IS the launched process, so still running past this is success. */
4645
+ const SETTLE_MS = 1500;
4646
+ const EDITOR_IDS = /* @__PURE__ */ new Set(["cursor", "idea"]);
4647
+ /** Launch commands tried in order, first success wins. macOS keeps `open -a`
4648
+ * behind the CLI shim because both shims are opt-in installs there, while
4649
+ * `open -a` finds the bundle wherever Toolbox or the DMG dropped it. */
4650
+ const LAUNCHERS = {
4651
+ cursor: {
4652
+ darwin: [["cursor"], [
4653
+ "open",
4654
+ "-a",
4655
+ "Cursor"
4656
+ ]],
4657
+ win32: [["cursor"]],
4658
+ linux: [["cursor"]]
4659
+ },
4660
+ idea: {
4661
+ darwin: [
4662
+ ["idea"],
4663
+ [
4664
+ "open",
4665
+ "-a",
4666
+ "IntelliJ IDEA"
4667
+ ],
4668
+ [
4669
+ "open",
4670
+ "-a",
4671
+ "IntelliJ IDEA CE"
4672
+ ]
4673
+ ],
4674
+ win32: [["idea"], ["idea64.exe"]],
4675
+ linux: [["idea"], ["idea.sh"]]
4676
+ }
4677
+ };
4678
+ /** cmd.exe re-interprets these, and a mis-parsed path opens the wrong project
4679
+ * rather than failing. Refuse instead of guessing. */
4680
+ const WINDOWS_UNSAFE = /["&|<>^%]/u;
4681
+ var EditorOpenError = class extends Error {
4682
+ code;
4683
+ constructor(code, message) {
4684
+ super(message);
4685
+ this.name = "EditorOpenError";
4686
+ this.code = code;
4687
+ }
4688
+ };
4689
+ /** Open a session's working directory in a desktop editor. */
4690
+ var EditorOpenService = class {
4691
+ #runtime;
4692
+ #platform;
4693
+ #settleMs;
4694
+ constructor(runtime, platform = process.platform, settleMs = SETTLE_MS) {
4695
+ this.#runtime = runtime;
4696
+ this.#platform = platform;
4697
+ this.#settleMs = settleMs;
4698
+ }
4699
+ async open(cwd, editor) {
4700
+ if (this.#platform === "win32" && WINDOWS_UNSAFE.test(cwd)) throw new EditorOpenError("unsupported-path", "The project path cannot be opened through the Windows shell.");
4701
+ const candidates = LAUNCHERS[editor][this.#platform] ?? LAUNCHERS[editor].linux ?? [];
4702
+ let found = false;
4703
+ for (const candidate of candidates) {
4704
+ const argv = await this.#argv(candidate, cwd);
4705
+ if (argv === void 0) continue;
4706
+ found = true;
4707
+ if (await this.#launch(argv, cwd)) return;
4708
+ }
4709
+ throw found ? new EditorOpenError("launch-failed", "The editor refused to open the project.") : new EditorOpenError("editor-unavailable", "The editor was not found on PATH.");
4710
+ }
4711
+ async #argv(candidate, cwd) {
4712
+ const [program, ...rest] = candidate;
4713
+ if (program === void 0) return void 0;
4714
+ if (this.#platform === "win32") return [
4715
+ "cmd.exe",
4716
+ "/d",
4717
+ "/s",
4718
+ "/c",
4719
+ program,
4720
+ ...rest,
4721
+ cwd
4722
+ ];
4723
+ try {
4724
+ return [
4725
+ await this.#runtime.resolveExecutable(program),
4726
+ ...rest,
4727
+ cwd
4728
+ ];
4729
+ } catch {
4730
+ return;
4731
+ }
4732
+ }
4733
+ /** True once the editor is launched: either the shim exited cleanly or the
4734
+ * process is still alive past the settle window. */
4735
+ async #launch(argv, cwd) {
4736
+ let handle;
4737
+ try {
4738
+ handle = this.#runtime.spawn({
4739
+ argv,
4740
+ cwd,
4741
+ stdio: {
4742
+ stdin: "ignore",
4743
+ stdout: { maxBytes: MAX_OUTPUT_BYTES$1 },
4744
+ stderr: { maxBytes: MAX_OUTPUT_BYTES$1 }
4745
+ },
4746
+ graceMs: 1e3,
4747
+ env: {}
4748
+ });
4749
+ } catch {
4750
+ return false;
4751
+ }
4752
+ return await Promise.race([handle.done.then((outcome) => outcome.exitCode === 0), new Promise((resolve) => {
4753
+ setTimeout(() => resolve(true), this.#settleMs).unref?.();
4754
+ })]);
4755
+ }
4756
+ };
4757
+ //#endregion
4758
+ //#region src/editor-open-routes.ts
4759
+ const MAX_SESSION_ID_CHARS$4 = 1024;
4760
+ /** Open the session's working directory in a desktop editor. Query-only: the
4761
+ * request carries two enum-ish values, so there is no body to parse. */
4762
+ function registerEditorOpenRoute(ctx, service, cwdForSession) {
4763
+ ctx.effect(() => ctx.webServer.register({
4764
+ kind: "exact",
4765
+ path: CLAUDE_EDITOR_OPEN_PATH,
4766
+ handler: async (req, res) => {
4767
+ if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
4768
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4769
+ const params = new URL(req.url ?? "/", "http://localhost").searchParams;
4770
+ const id = params.get("sessionId");
4771
+ const editor = params.get("editor");
4772
+ if (id === null || id.length === 0 || id.length > MAX_SESSION_ID_CHARS$4) return json(res, 400, {
4773
+ error: "invalid-session",
4774
+ message: "The session is invalid."
4775
+ });
4776
+ if (editor === null || !EDITOR_IDS.has(editor)) return json(res, 400, {
4777
+ error: "invalid-editor",
4778
+ message: "The editor is invalid."
4779
+ });
4780
+ const cwd = cwdForSession(id);
4781
+ if (cwd === void 0) return json(res, 409, {
4782
+ error: "session-unavailable",
4783
+ message: "The Claude session is unavailable."
4784
+ });
4785
+ try {
4786
+ await service.open(cwd, editor);
4787
+ return json(res, 200, { opened: true });
4788
+ } catch (error) {
4789
+ if (error instanceof EditorOpenError) return json(res, 409, {
4790
+ error: error.code,
4791
+ message: error.message
4792
+ });
4793
+ return json(res, 500, {
4794
+ error: "editor-open-unavailable",
4795
+ message: "The editor could not be launched."
4796
+ });
4797
+ }
4798
+ }
4799
+ }), "dsh-claude: editor open route");
4800
+ }
4801
+ //#endregion
4802
+ //#region src/github-url.ts
4803
+ /** Only GitHub's own image hosts; the browser loads these directly, so a URL
4804
+ * the API did not vouch for must never become an outbound request. */
4805
+ function githubAvatarUrl(value) {
4806
+ if (typeof value !== "string" || value.length === 0 || value.length > 1024) return void 0;
4807
+ try {
4808
+ const url = new URL(value);
4809
+ const allowed = url.hostname === "github.com" || url.hostname === "githubusercontent.com" || url.hostname.endsWith(".githubusercontent.com");
4810
+ return url.protocol === "https:" && allowed ? url.href : void 0;
4811
+ } catch {
4812
+ return;
4813
+ }
4814
+ }
4815
+ //#endregion
4340
4816
  //#region src/pr-feedback.ts
4341
4817
  const MAX_OUTPUT_BYTES = 524288;
4342
4818
  const GIT_TIMEOUT_MS = 1e4;
@@ -4362,26 +4838,28 @@ async function collect(handle) {
4362
4838
  lossy: stdout?.lossy === true
4363
4839
  };
4364
4840
  }
4365
- function record$5(value) {
4841
+ function record$6(value) {
4366
4842
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4367
4843
  }
4368
4844
  function parseReviewComments(value) {
4369
4845
  if (!Array.isArray(value)) return [];
4370
4846
  const comments = [];
4371
4847
  for (const item of value) {
4372
- const input = record$5(item);
4848
+ const input = record$6(item);
4373
4849
  if (input === void 0 || !Number.isSafeInteger(input.id)) continue;
4374
4850
  const body = typeof input.body === "string" ? input.body.trim() : "";
4375
4851
  const path = typeof input.path === "string" ? input.path : "";
4376
4852
  const url = typeof input.html_url === "string" ? input.html_url : "";
4377
4853
  if (body.length === 0 || path.length === 0) continue;
4854
+ const avatarUrl = githubAvatarUrl(record$6(input.user)?.avatar_url);
4378
4855
  const line = Number.isSafeInteger(input.line) ? Number(input.line) : Number.isSafeInteger(input.original_line) ? Number(input.original_line) : void 0;
4379
4856
  comments.push({
4380
4857
  id: Number(input.id),
4381
4858
  path,
4382
4859
  ...line === void 0 ? {} : { line },
4383
4860
  side: input.side === "LEFT" ? "old" : "new",
4384
- author: typeof record$5(input.user)?.login === "string" ? String(record$5(input.user)?.login) : "unknown",
4861
+ author: typeof record$6(input.user)?.login === "string" ? String(record$6(input.user)?.login) : "unknown",
4862
+ ...avatarUrl === void 0 ? {} : { avatarUrl },
4385
4863
  body: body.slice(0, MAX_COMMENT_CHARS),
4386
4864
  url
4387
4865
  });
@@ -4398,7 +4876,7 @@ function parseFailingChecks(value) {
4398
4876
  if (!Array.isArray(value)) return [];
4399
4877
  const failing = [];
4400
4878
  for (const item of value) {
4401
- const input = record$5(item);
4879
+ const input = record$6(item);
4402
4880
  if (input === void 0 || input.bucket !== "fail" || typeof input.name !== "string") continue;
4403
4881
  failing.push({
4404
4882
  name: input.name,
@@ -4510,10 +4988,10 @@ var PullRequestFeedbackService = class {
4510
4988
  };
4511
4989
  //#endregion
4512
4990
  //#region src/pr-feedback-routes.ts
4513
- const MAX_SESSION_ID_CHARS$2 = 1024;
4991
+ const MAX_SESSION_ID_CHARS$3 = 1024;
4514
4992
  function sessionId(url) {
4515
4993
  const value = url.searchParams.get("sessionId");
4516
- if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$2) throw new PullRequestFeedbackError("invalid-session", "The session is invalid.");
4994
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$3) throw new PullRequestFeedbackError("invalid-session", "The session is invalid.");
4517
4995
  return value;
4518
4996
  }
4519
4997
  function pullNumber(url) {
@@ -4629,7 +5107,7 @@ var JiraError = class extends Error {
4629
5107
  this.code = code;
4630
5108
  }
4631
5109
  };
4632
- function record$4(value) {
5110
+ function record$5(value) {
4633
5111
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4634
5112
  }
4635
5113
  /** Jira Cloud sites are origins; Data Center may carry a context path. */
@@ -4647,6 +5125,30 @@ function ticketKeyOf(query) {
4647
5125
  const match = /^([A-Za-z][A-Za-z0-9_]*)-(\d+)$/u.exec(query.trim());
4648
5126
  return match === null ? void 0 : `${match[1].toUpperCase()}-${match[2]}`;
4649
5127
  }
5128
+ /** Jira's text index carries summary, description and comments -- never the
5129
+ * issue key -- so a bare ticket number ("5697") can never reach PSOS-5697
5130
+ * through `text ~`. The issue picker is the endpoint behind Jira's own quick
5131
+ * search and does match keys; its hits become the key filter the normal
5132
+ * search then reads the display fields from. */
5133
+ function pickerKeys(value, number) {
5134
+ const sections = record$5(value)?.sections;
5135
+ if (!Array.isArray(sections)) return [];
5136
+ const keys = [];
5137
+ for (const section of sections) {
5138
+ const issues = record$5(section)?.issues;
5139
+ if (!Array.isArray(issues)) continue;
5140
+ for (const issue of issues) {
5141
+ const raw = record$5(issue)?.key;
5142
+ const key = ticketKeyOf(typeof raw === "string" ? raw : "");
5143
+ if (key !== void 0 && key.endsWith(`-${number}`) && !keys.includes(key)) keys.push(key);
5144
+ }
5145
+ }
5146
+ return keys;
5147
+ }
5148
+ /** Keys arrive validated by `ticketKeyOf`, so they cannot break out of the quotes. */
5149
+ function keyJql(keys) {
5150
+ return `key in (${keys.map((key) => `"${key}"`).join(", ")}) ORDER BY updated DESC`;
5151
+ }
4650
5152
  function buildJql(query) {
4651
5153
  const trimmed = query.trim().slice(0, MAX_QUERY_CHARS);
4652
5154
  if (trimmed.length === 0) return "statusCategory != Done ORDER BY updated DESC";
@@ -4655,15 +5157,15 @@ function buildJql(query) {
4655
5157
  return `text ~ "${trimmed.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}*" ORDER BY updated DESC`;
4656
5158
  }
4657
5159
  function parseTickets(value, siteUrl) {
4658
- const issues = record$4(value)?.issues;
5160
+ const issues = record$5(value)?.issues;
4659
5161
  if (!Array.isArray(issues)) return [];
4660
5162
  const tickets = [];
4661
5163
  for (const item of issues) {
4662
- const issue = record$4(item);
4663
- const fields = record$4(issue?.fields);
5164
+ const issue = record$5(item);
5165
+ const fields = record$5(issue?.fields);
4664
5166
  if (issue === void 0 || typeof issue.key !== "string" || fields === void 0) continue;
4665
- const status = record$4(fields.status)?.name;
4666
- const type = record$4(fields.issuetype)?.name;
5167
+ const status = record$5(fields.status)?.name;
5168
+ const type = record$5(fields.issuetype)?.name;
4667
5169
  tickets.push({
4668
5170
  key: issue.key,
4669
5171
  summary: typeof fields.summary === "string" ? fields.summary.slice(0, 256) : "",
@@ -4705,7 +5207,7 @@ var JiraService = class {
4705
5207
  email,
4706
5208
  apiToken
4707
5209
  };
4708
- const myself = record$4(await this.#json(connection, "/rest/api/3/myself"));
5210
+ const myself = record$5(await this.#json(connection, "/rest/api/3/myself"));
4709
5211
  const displayName = typeof myself?.displayName === "string" ? myself.displayName : void 0;
4710
5212
  const accountId = typeof myself?.accountId === "string" ? myself.accountId : void 0;
4711
5213
  const store = {
@@ -4724,7 +5226,7 @@ var JiraService = class {
4724
5226
  if (ticket === void 0) throw new JiraError("invalid-request", "The ticket key is invalid.");
4725
5227
  let accountId = store.accountId;
4726
5228
  if (accountId === void 0) {
4727
- const myself = record$4(await this.#json(store, "/rest/api/3/myself"));
5229
+ const myself = record$5(await this.#json(store, "/rest/api/3/myself"));
4728
5230
  if (typeof myself?.accountId !== "string") throw new JiraError("jira-failed", "The Jira account id is unavailable.");
4729
5231
  accountId = myself.accountId;
4730
5232
  await this.#write({
@@ -4744,7 +5246,7 @@ var JiraService = class {
4744
5246
  const store = await this.#read();
4745
5247
  if (store === void 0) throw new JiraError("not-connected", "Connect Jira in Settings first.");
4746
5248
  const params = new URLSearchParams({
4747
- jql: buildJql(query),
5249
+ jql: await this.#searchJql(store, query.trim().slice(0, MAX_QUERY_CHARS)),
4748
5250
  maxResults: String(MAX_RESULTS),
4749
5251
  fields: "summary,status,issuetype"
4750
5252
  });
@@ -4757,6 +5259,14 @@ var JiraService = class {
4757
5259
  }
4758
5260
  return parseTickets(body, store.siteUrl);
4759
5261
  }
5262
+ /** A bare ticket number resolves through the picker; everything else is JQL.
5263
+ * A site that cannot serve the picker falls back to the text search rather
5264
+ * than failing the whole lookup. */
5265
+ async #searchJql(store, query) {
5266
+ if (!/^\d+$/u.test(query)) return buildJql(query);
5267
+ const keys = await this.#json(store, `/rest/api/3/issue/picker?query=${encodeURIComponent(query)}`).then((body) => pickerKeys(body, query), () => []);
5268
+ return keys.length === 0 ? buildJql(query) : keyJql(keys);
5269
+ }
4760
5270
  async #json(connection, path, init = {}) {
4761
5271
  let response;
4762
5272
  try {
@@ -4792,7 +5302,7 @@ var JiraService = class {
4792
5302
  throw error;
4793
5303
  }
4794
5304
  if (Buffer.byteLength(text) > MAX_STORE_BYTES) return void 0;
4795
- const input = record$4(JSON.parse(text));
5305
+ const input = record$5(JSON.parse(text));
4796
5306
  if (input === void 0 || typeof input.siteUrl !== "string" || typeof input.email !== "string" || typeof input.apiToken !== "string") return void 0;
4797
5307
  return {
4798
5308
  siteUrl: input.siteUrl,
@@ -4823,20 +5333,20 @@ var JiraService = class {
4823
5333
  };
4824
5334
  //#endregion
4825
5335
  //#region src/jira-routes.ts
4826
- const MAX_BODY_BYTES$2 = 8192;
4827
- function record$3(value) {
5336
+ const MAX_BODY_BYTES$3 = 8192;
5337
+ function record$4(value) {
4828
5338
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4829
5339
  }
4830
- async function readJson$2(req) {
5340
+ async function readJson$3(req) {
4831
5341
  const chunks = [];
4832
5342
  let size = 0;
4833
5343
  for await (const chunk of req) {
4834
5344
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4835
5345
  size += buffer.length;
4836
- if (size > MAX_BODY_BYTES$2) throw new JiraError("body-too-large", "The request body is too large.");
5346
+ if (size > MAX_BODY_BYTES$3) throw new JiraError("body-too-large", "The request body is too large.");
4837
5347
  chunks.push(buffer);
4838
5348
  }
4839
- const value = record$3(JSON.parse(Buffer.concat(chunks).toString("utf8")));
5349
+ const value = record$4(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4840
5350
  if (value === void 0) throw new JiraError("invalid-request", "The request body is invalid.");
4841
5351
  return value;
4842
5352
  }
@@ -4859,7 +5369,7 @@ function registerJiraRoute(ctx, service) {
4859
5369
  }
4860
5370
  if (url.pathname === `/plugins/dsh-claude/jira/connect`) {
4861
5371
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4862
- const input = await readJson$2(req);
5372
+ const input = await readJson$3(req);
4863
5373
  return json(res, 200, await service.connect({
4864
5374
  siteUrl: string(input, "siteUrl"),
4865
5375
  email: string(input, "email"),
@@ -4873,7 +5383,7 @@ function registerJiraRoute(ctx, service) {
4873
5383
  }
4874
5384
  if (url.pathname === `/plugins/dsh-claude/jira/assign`) {
4875
5385
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
4876
- const input = await readJson$2(req);
5386
+ const input = await readJson$3(req);
4877
5387
  await service.assignToMe(string(input, "key"));
4878
5388
  return json(res, 200, { assigned: true });
4879
5389
  }
@@ -4971,12 +5481,12 @@ function askArguments(preferences) {
4971
5481
  ...READ_ONLY_TOOLS
4972
5482
  ];
4973
5483
  }
4974
- function record$2(value) {
5484
+ function record$3(value) {
4975
5485
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4976
5486
  }
4977
5487
  /** One-line description of a tool call, mirroring the main window's step titles. */
4978
5488
  function toolSummary(input) {
4979
- const fields = record$2(input);
5489
+ const fields = record$3(input);
4980
5490
  if (fields === void 0) return void 0;
4981
5491
  const candidate = [
4982
5492
  fields.command,
@@ -4990,7 +5500,7 @@ function toolSummary(input) {
4990
5500
  function eventsOfStreamLine(line) {
4991
5501
  let parsed;
4992
5502
  try {
4993
- parsed = record$2(JSON.parse(line));
5503
+ parsed = record$3(JSON.parse(line));
4994
5504
  } catch {
4995
5505
  return [];
4996
5506
  }
@@ -5000,9 +5510,9 @@ function eventsOfStreamLine(line) {
5000
5510
  text: "ready"
5001
5511
  }];
5002
5512
  if (parsed.type === "stream_event") {
5003
- const event = record$2(parsed.event);
5513
+ const event = record$3(parsed.event);
5004
5514
  if (event?.type === "content_block_start") {
5005
- const block = record$2(event.content_block);
5515
+ const block = record$3(event.content_block);
5006
5516
  if (block?.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") return [{
5007
5517
  type: "tool",
5008
5518
  id: block.id,
@@ -5011,7 +5521,7 @@ function eventsOfStreamLine(line) {
5011
5521
  }];
5012
5522
  return [];
5013
5523
  }
5014
- const delta = record$2(event?.delta);
5524
+ const delta = record$3(event?.delta);
5015
5525
  if (event?.type !== "content_block_delta" || delta === void 0) return [];
5016
5526
  if (delta.type === "text_delta" && typeof delta.text === "string") return [{
5017
5527
  type: "text",
@@ -5024,11 +5534,11 @@ function eventsOfStreamLine(line) {
5024
5534
  return [];
5025
5535
  }
5026
5536
  if (parsed.type === "assistant" || parsed.type === "user") {
5027
- const content = record$2(parsed.message)?.content;
5537
+ const content = record$3(parsed.message)?.content;
5028
5538
  if (!Array.isArray(content)) return [];
5029
5539
  const events = [];
5030
5540
  for (const item of content) {
5031
- const block = record$2(item);
5541
+ const block = record$3(item);
5032
5542
  if (block?.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
5033
5543
  const summary = toolSummary(block.input);
5034
5544
  events.push({
@@ -5120,21 +5630,21 @@ var AskService = class {
5120
5630
  };
5121
5631
  //#endregion
5122
5632
  //#region src/ask-routes.ts
5123
- const MAX_BODY_BYTES$1 = 131072;
5124
- const MAX_SESSION_ID_CHARS$1 = 1024;
5125
- function record$1(value) {
5633
+ const MAX_BODY_BYTES$2 = 131072;
5634
+ const MAX_SESSION_ID_CHARS$2 = 1024;
5635
+ function record$2(value) {
5126
5636
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
5127
5637
  }
5128
- async function readJson$1(req) {
5638
+ async function readJson$2(req) {
5129
5639
  const chunks = [];
5130
5640
  let size = 0;
5131
5641
  for await (const chunk of req) {
5132
5642
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
5133
5643
  size += buffer.length;
5134
- if (size > MAX_BODY_BYTES$1) throw new AskError("body-too-large", "The request body is too large.");
5644
+ if (size > MAX_BODY_BYTES$2) throw new AskError("body-too-large", "The request body is too large.");
5135
5645
  chunks.push(buffer);
5136
5646
  }
5137
- const value = record$1(JSON.parse(Buffer.concat(chunks).toString("utf8")));
5647
+ const value = record$2(JSON.parse(Buffer.concat(chunks).toString("utf8")));
5138
5648
  if (value === void 0) throw new AskError("invalid-request", "The request body is invalid.");
5139
5649
  return value;
5140
5650
  }
@@ -5162,12 +5672,12 @@ function registerAskRoute(ctx, service, cwdForSession, preferencesFor) {
5162
5672
  let sessionId;
5163
5673
  try {
5164
5674
  const value = url.searchParams.get("sessionId");
5165
- if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$1) throw new AskError("invalid-session", "The session is invalid.");
5675
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$2) throw new AskError("invalid-session", "The session is invalid.");
5166
5676
  sessionId = value;
5167
5677
  const resolved = cwdForSession(sessionId);
5168
5678
  if (resolved === void 0) throw new AskError("session-unavailable", "The Claude session is unavailable.");
5169
5679
  cwd = resolved;
5170
- request = askRequest(await readJson$1(req));
5680
+ request = askRequest(await readJson$2(req));
5171
5681
  } catch (error) {
5172
5682
  if (error instanceof AskError) return json(res, 409, {
5173
5683
  error: error.code,
@@ -5211,27 +5721,27 @@ function registerAskRoute(ctx, service, cwdForSession, preferencesFor) {
5211
5721
  }
5212
5722
  //#endregion
5213
5723
  //#region src/review-comment-routes.ts
5214
- const MAX_BODY_BYTES = 16384;
5215
- const MAX_SESSION_ID_CHARS = 1024;
5216
- function record(value) {
5724
+ const MAX_BODY_BYTES$1 = 16384;
5725
+ const MAX_SESSION_ID_CHARS$1 = 1024;
5726
+ function record$1(value) {
5217
5727
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
5218
5728
  }
5219
- async function readJson(req) {
5729
+ async function readJson$1(req) {
5220
5730
  const chunks = [];
5221
5731
  let size = 0;
5222
5732
  for await (const chunk of req) {
5223
5733
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
5224
5734
  size += buffer.length;
5225
- if (size > MAX_BODY_BYTES) throw new ReviewCommentError("body-too-large", "The request body is too large.");
5735
+ if (size > MAX_BODY_BYTES$1) throw new ReviewCommentError("body-too-large", "The request body is too large.");
5226
5736
  chunks.push(buffer);
5227
5737
  }
5228
- const value = record(JSON.parse(Buffer.concat(chunks).toString("utf8")));
5738
+ const value = record$1(JSON.parse(Buffer.concat(chunks).toString("utf8")));
5229
5739
  if (value === void 0) throw new ReviewCommentError("invalid-request", "The request body is invalid.");
5230
5740
  return value;
5231
5741
  }
5232
5742
  function sessionIdFromUrl(url) {
5233
5743
  const value = url.searchParams.get("sessionId");
5234
- if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS) throw new ReviewCommentError("invalid-session", "The session is invalid.");
5744
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$1) throw new ReviewCommentError("invalid-session", "The session is invalid.");
5235
5745
  return value;
5236
5746
  }
5237
5747
  function registerReviewCommentRoute(ctx, store, ownsSession) {
@@ -5245,7 +5755,7 @@ function registerReviewCommentRoute(ctx, store, ownsSession) {
5245
5755
  const sessionId = sessionIdFromUrl(url);
5246
5756
  if (!ownsSession(sessionId)) throw new ReviewCommentError("session-unavailable", "The Claude session is unavailable.");
5247
5757
  if (url.pathname === "/plugins/dsh-claude/review-comments" && req.method === "POST") {
5248
- const input = await readJson(req);
5758
+ const input = await readJson$1(req);
5249
5759
  return json(res, 200, { comment: store.add(sessionId, {
5250
5760
  path: input.path,
5251
5761
  line: input.line,
@@ -5256,7 +5766,7 @@ function registerReviewCommentRoute(ctx, store, ownsSession) {
5256
5766
  }
5257
5767
  if (url.pathname === `/plugins/dsh-claude/review-comments/clear` && req.method === "POST") return json(res, 200, { removed: store.drain(sessionId).length });
5258
5768
  if (url.pathname === `/plugins/dsh-claude/review-comments/remove` && req.method === "POST") {
5259
- const input = await readJson(req);
5769
+ const input = await readJson$1(req);
5260
5770
  if (typeof input.id !== "string" || input.id.length === 0 || input.id.length > 128) throw new ReviewCommentError("invalid-request", "The comment id is invalid.");
5261
5771
  return json(res, 200, { removed: store.remove(sessionId, input.id) });
5262
5772
  }
@@ -5276,6 +5786,53 @@ function registerReviewCommentRoute(ctx, store, ownsSession) {
5276
5786
  }), "dsh-claude: review comment route");
5277
5787
  }
5278
5788
  //#endregion
5789
+ //#region src/rewind-routes.ts
5790
+ const MAX_BODY_BYTES = 4096;
5791
+ const MAX_SESSION_ID_CHARS = 1024;
5792
+ function record(value) {
5793
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
5794
+ }
5795
+ async function readJson(req) {
5796
+ const chunks = [];
5797
+ let size = 0;
5798
+ for await (const chunk of req) {
5799
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
5800
+ size += buffer.length;
5801
+ if (size > MAX_BODY_BYTES) return void 0;
5802
+ chunks.push(buffer);
5803
+ }
5804
+ return record(JSON.parse(Buffer.concat(chunks).toString("utf8")));
5805
+ }
5806
+ /** `POST <path>` with `{ sessionId, seq }`: hide that surface event and every
5807
+ * later one, and arm Claude to resume before the turn it opened. */
5808
+ function registerClaudeRewindRoute(ctx, sidecar, access) {
5809
+ ctx.effect(() => ctx.webServer.register({
5810
+ kind: "exact",
5811
+ path: CLAUDE_REWIND_PATH,
5812
+ handler: async (req, res) => {
5813
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
5814
+ if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
5815
+ try {
5816
+ const input = await readJson(req);
5817
+ const sessionId = input?.sessionId;
5818
+ const seq = input?.seq;
5819
+ if (typeof sessionId !== "string" || sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS || typeof seq !== "number" || !Number.isSafeInteger(seq) || seq < 0) return json(res, 400, { error: "invalid-request" });
5820
+ const events = access.eventsFor(sessionId);
5821
+ if (events === void 0) return json(res, 409, { error: "session-unavailable" });
5822
+ if (access.busy(sessionId)) return json(res, 409, { error: "session-busy" });
5823
+ const planned = planRewind((await sidecar.read(sessionId)).rewind ?? EMPTY_REWIND_STATE, events, seq);
5824
+ if (planned === void 0) return json(res, 409, { error: "seq-unavailable" });
5825
+ await sidecar.writeRewind(sessionId, planned);
5826
+ await access.reset(sessionId);
5827
+ return json(res, 200, { ranges: planned.ranges });
5828
+ } catch (error) {
5829
+ if (error instanceof SyntaxError) return json(res, 400, { error: "invalid-json" });
5830
+ return json(res, 500, { error: "rewind-unavailable" });
5831
+ }
5832
+ }
5833
+ }), "dsh-claude: rewind route");
5834
+ }
5835
+ //#endregion
5279
5836
  //#region src/update-routes.ts
5280
5837
  const PLUGIN_PACKAGE_NAME = "@norman-else/dsh-claude";
5281
5838
  const UPDATE_TIMEOUT_MS = 3e4;
@@ -6117,6 +6674,7 @@ async function apply(ctx, config) {
6117
6674
  return agent.session.header.cwd;
6118
6675
  };
6119
6676
  registerRepositoryActionRoute(webCtx, repositoryActions, cwdForClaudeSession);
6677
+ registerEditorOpenRoute(webCtx, new EditorOpenService(webCtx.subprocess), cwdForClaudeSession);
6120
6678
  registerPullRequestFeedbackRoute(webCtx, new PullRequestFeedbackService(webCtx.subprocess), cwdForClaudeSession);
6121
6679
  registerAskRoute(webCtx, new AskService(webCtx.subprocess, supervisorConfig.executablePath), cwdForClaudeSession, (sessionId) => {
6122
6680
  const snapshot = supervisor.snapshots().find((item) => item.sessionId === sessionId);
@@ -6130,6 +6688,14 @@ async function apply(ctx, config) {
6130
6688
  return agent !== void 0 && webCtx.agentPresets.composedPreset(agent.ctx) === "claude";
6131
6689
  };
6132
6690
  registerReviewCommentRoute(webCtx, reviewComments, ownsClaudeSession);
6691
+ registerClaudeRewindRoute(webCtx, sidecar, {
6692
+ eventsFor: (sessionId) => {
6693
+ const agent = webCtx.agents.get(sessionId);
6694
+ return agent === void 0 || webCtx.agentPresets.composedPreset(agent.ctx) !== "claude" ? void 0 : agent.session.events;
6695
+ },
6696
+ busy: (sessionId) => supervisor.snapshots().some((item) => item.sessionId === sessionId && (item.state === "running" || item.state === "interrupting")),
6697
+ reset: (sessionId) => supervisor.disposeSession(sessionId)
6698
+ });
6133
6699
  registerPlanUsageRoute(webCtx, (fetchedAt) => probePlanUsage(supervisorConfig.executablePath, fetchedAt));
6134
6700
  registerClaudeProjectionRoute(webCtx, sidecar, ownsClaudeSession, (sessionId) => commandCatalogs.get(sessionId) ?? [], async (sessionId) => {
6135
6701
  const agent = webCtx.agents.get(sessionId);