@norman-else/dsh-claude 0.1.19 → 0.1.21

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,20 +1,23 @@
1
- import { S as TASK_TOOL_NAMES, _ as CLAUDE_PROJECTION_PATH, a as latestClaudeTasks, b as CLAUDE_UPDATE_PATH, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_CODE_PRESET_ID, g as CLAUDE_GLOBAL_SETTINGS_PATH, h as CLAUDE_DOCTOR_PATH, i as latestClaudeSessionBinding, l as redactText, m as CLAUDE_CODE_PROVIDER_IDS, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_CODE_PROVIDER, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as CLAUDE_REPOSITORY_SETUP_PATH, y as CLAUDE_UPDATE_CHECK_PATH } from "./events-DPJaBReT.mjs";
2
- import { a as projectClaudeCommands, i as CLAUDE_COMMANDS_SERVICE, r as dynamicPresenterDefinition, t as CLAUDE_PRESENTER_NAMES } from "./presenters-DBRIOgJs.mjs";
3
- import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-DnoSEKGm.mjs";
1
+ import { S as CLAUDE_UPDATE_PATH, _ as CLAUDE_PROJECTION_PATH, a as latestClaudeTasks, b as CLAUDE_REVIEW_COMMENT_PATH, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_CODE_PRESET_ID, g as CLAUDE_GLOBAL_SETTINGS_PATH, h as CLAUDE_DOCTOR_PATH, i as latestClaudeSessionBinding, l as redactText, m as CLAUDE_CODE_PROVIDER_IDS, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_CODE_PROVIDER, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, v as CLAUDE_REPOSITORY_ACTION_PATH, w as TASK_TOOL_NAMES, x as CLAUDE_UPDATE_CHECK_PATH, y as CLAUDE_REPOSITORY_SETUP_PATH } from "./events-BdDs9ebF.mjs";
2
+ import { n as projectClaudeCommands, t as CLAUDE_COMMANDS_SERVICE } from "./command-bridge-C10-lz6A.mjs";
3
+ import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-DMANIjwu.mjs";
4
4
  import z from "@deepseek-ai/schemastery";
5
- import { randomUUID } from "node:crypto";
5
+ import { createHash, randomUUID } from "node:crypto";
6
6
  import { chmod, mkdir, opendir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
7
7
  import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
8
8
  import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
9
9
  import { homedir } from "node:os";
10
10
  import { query } from "@anthropic-ai/claude-agent-sdk";
11
- import { CallId, LlmAdapter, ReasoningEffortId, createToolResultMessage } from "@deepseek-ai/dsh-llm";
12
11
  import { EventEmitter } from "node:events";
13
12
  import { DSH_ENV_PREFIX, SENSITIVE_ENV_PATTERN } from "@deepseek-ai/dsh-subprocess";
13
+ import { LlmAdapter, ReasoningEffortId } from "@deepseek-ai/dsh-llm";
14
14
  import { fileURLToPath } from "node:url";
15
15
  //#region src/sidecar.ts
16
16
  const SIDECAR_SCHEMA_VERSION = 1;
17
17
  const MAX_ACTIVITIES = 1e4;
18
+ /** Trailing window that coalesces per-token transcript persistence into one
19
+ * atomic disk write; live subscribers are notified synchronously regardless. */
20
+ const TEXT_FLUSH_MS = 150;
18
21
  function emptyProjection() {
19
22
  return {
20
23
  schemaVersion: SIDECAR_SCHEMA_VERSION,
@@ -22,18 +25,18 @@ function emptyProjection() {
22
25
  activities: []
23
26
  };
24
27
  }
25
- function record$3(value) {
28
+ function record$5(value) {
26
29
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
27
30
  }
28
31
  function finiteInteger(value) {
29
32
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
30
33
  }
31
- function string$2(value, max) {
34
+ function string$3(value, max) {
32
35
  return typeof value === "string" && value.length > 0 && value.length <= max;
33
36
  }
34
37
  function binding(value) {
35
- const input = record$3(value);
36
- if (input === void 0 || !string$2(input.claudeSessionId, 512) || !string$2(input.sdkVersion, 128) || !string$2(input.cwd, 4096) || input.cliVersion !== void 0 && !string$2(input.cliVersion, 128)) return void 0;
38
+ const input = record$5(value);
39
+ if (input === void 0 || !string$3(input.claudeSessionId, 512) || !string$3(input.sdkVersion, 128) || !string$3(input.cwd, 4096) || input.cliVersion !== void 0 && !string$3(input.cliVersion, 128)) return void 0;
37
40
  return {
38
41
  claudeSessionId: input.claudeSessionId,
39
42
  sdkVersion: input.sdkVersion,
@@ -42,6 +45,7 @@ function binding(value) {
42
45
  };
43
46
  }
44
47
  const ACTIVITY_KINDS = /* @__PURE__ */ new Set([
48
+ "text",
45
49
  "status",
46
50
  "thinking",
47
51
  "tool-call",
@@ -61,22 +65,22 @@ const ACTIVITY_PHASES = /* @__PURE__ */ new Set([
61
65
  "failed"
62
66
  ]);
63
67
  function activity(value) {
64
- const input = record$3(value);
68
+ const input = record$5(value);
65
69
  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;
66
70
  return normalizeActivity(input);
67
71
  }
68
72
  function contextUsage(value) {
69
- const input = record$3(value);
73
+ const input = record$5(value);
70
74
  if (input === void 0 || !Array.isArray(input.categories)) return void 0;
71
75
  return normalizeContextUsage(input);
72
76
  }
73
77
  function tasks(value) {
74
- const input = record$3(value);
78
+ const input = record$5(value);
75
79
  if (input === void 0 || !Array.isArray(input.tasks)) return void 0;
76
80
  return normalizeTasksEvent(input.tasks);
77
81
  }
78
82
  function parseClaudeSidecar(value) {
79
- const input = record$3(value);
83
+ const input = record$5(value);
80
84
  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");
81
85
  const activities = input.activities.map(activity);
82
86
  if (activities.some((item) => item === void 0)) throw new Error("dsh-claude: invalid sidecar activity");
@@ -116,13 +120,126 @@ var ClaudeSidecarRepository = class {
116
120
  root;
117
121
  legacyRoot;
118
122
  #pending = /* @__PURE__ */ new Map();
123
+ /** Latest durable projection per session; disk is read once and written through. */
124
+ #latest = /* @__PURE__ */ new Map();
125
+ #listeners = /* @__PURE__ */ new Map();
126
+ /** Streaming transcript segments not yet persisted, keyed by activity key. */
127
+ #live = /* @__PURE__ */ new Map();
128
+ /** Monotonic revision boost so merged reads advance while text stays in memory. */
129
+ #boost = /* @__PURE__ */ new Map();
130
+ #flushTimers = /* @__PURE__ */ new Map();
119
131
  constructor(options = {}) {
120
132
  this.root = options.root ?? dshHomePath("plugins", "dsh-claude", "sessions");
121
133
  this.legacyRoot = options.legacyRoot ?? (options.root === void 0 ? dshHomePath("plugins", "dsh-claude-code", "sessions") : void 0);
122
134
  }
123
135
  async read(sessionId) {
124
136
  await this.#pending.get(sessionId)?.catch(() => void 0);
125
- return this.#readNow(sessionId);
137
+ return this.#merged(sessionId, await this.#base(sessionId));
138
+ }
139
+ /** Observe accepted changes for one session; returns the unsubscriber. */
140
+ subscribe(sessionId, listener) {
141
+ let set = this.#listeners.get(sessionId);
142
+ if (set === void 0) {
143
+ set = /* @__PURE__ */ new Set();
144
+ this.#listeners.set(sessionId, set);
145
+ }
146
+ set.add(listener);
147
+ return () => {
148
+ set.delete(listener);
149
+ if (set.size === 0) this.#listeners.delete(sessionId);
150
+ };
151
+ }
152
+ /** Record streaming assistant prose without touching the disk on the hot
153
+ * path: subscribers are notified synchronously (as an append when the
154
+ * redacted text grows in place) and persistence is coalesced. */
155
+ appendTranscriptText(sessionId, value) {
156
+ const normalized = normalizeActivity({
157
+ kind: "text",
158
+ phase: "updated",
159
+ ...value
160
+ });
161
+ const key = activityKey(normalized);
162
+ let overlay = this.#live.get(sessionId);
163
+ if (overlay === void 0) {
164
+ overlay = /* @__PURE__ */ new Map();
165
+ this.#live.set(sessionId, overlay);
166
+ }
167
+ const previous = overlay.get(key);
168
+ overlay.set(key, normalized);
169
+ this.#boost.set(sessionId, (this.#boost.get(sessionId) ?? 0) + 1);
170
+ const text = normalized.text ?? "";
171
+ const base = {
172
+ turn: normalized.turn,
173
+ step: normalized.step,
174
+ ordinal: normalized.ordinal
175
+ };
176
+ this.#notify(sessionId, previous?.text !== void 0 && text.startsWith(previous.text) ? {
177
+ kind: "text",
178
+ ...base,
179
+ append: text.slice(previous.text.length)
180
+ } : {
181
+ kind: "text",
182
+ ...base,
183
+ text
184
+ });
185
+ this.#scheduleTextFlush(sessionId);
186
+ }
187
+ /** Persist any pending streaming transcript now (segment close, turn end). */
188
+ flushTranscriptText(sessionId) {
189
+ const timer = this.#flushTimers.get(sessionId);
190
+ if (timer !== void 0) {
191
+ clearTimeout(timer);
192
+ this.#flushTimers.delete(sessionId);
193
+ }
194
+ return this.#flushLive(sessionId);
195
+ }
196
+ #notify(sessionId, delta) {
197
+ const set = this.#listeners.get(sessionId);
198
+ if (set === void 0) return;
199
+ for (const listener of [...set]) try {
200
+ listener(delta);
201
+ } catch {}
202
+ }
203
+ #merged(sessionId, base) {
204
+ const overlay = this.#live.get(sessionId);
205
+ const boost = this.#boost.get(sessionId) ?? 0;
206
+ if ((overlay === void 0 || overlay.size === 0) && boost === 0) return base;
207
+ return {
208
+ ...base,
209
+ revision: base.revision + boost,
210
+ ...overlay === void 0 || overlay.size === 0 ? {} : { activities: mergeActivities(base.activities, [...overlay.values()]) }
211
+ };
212
+ }
213
+ async #base(sessionId) {
214
+ const cached = this.#latest.get(sessionId);
215
+ if (cached !== void 0) return cached;
216
+ const loaded = await this.#readNow(sessionId);
217
+ this.#latest.set(sessionId, loaded);
218
+ return loaded;
219
+ }
220
+ #scheduleTextFlush(sessionId) {
221
+ if (this.#flushTimers.has(sessionId)) return;
222
+ const timer = setTimeout(() => {
223
+ this.#flushTimers.delete(sessionId);
224
+ this.#flushLive(sessionId);
225
+ }, TEXT_FLUSH_MS);
226
+ timer.unref?.();
227
+ this.#flushTimers.set(sessionId, timer);
228
+ }
229
+ async #flushLive(sessionId) {
230
+ const overlay = this.#live.get(sessionId);
231
+ if (overlay === void 0 || overlay.size === 0) return;
232
+ const entries = [...overlay.entries()];
233
+ try {
234
+ await this.#update(sessionId, (current) => ({
235
+ ...current,
236
+ activities: mergeActivities(current.activities, entries.map(([, value]) => value))
237
+ }));
238
+ } catch {
239
+ return;
240
+ }
241
+ for (const [key, value] of entries) if (overlay.get(key) === value) overlay.delete(key);
242
+ if (overlay.size === 0) this.#live.delete(sessionId);
126
243
  }
127
244
  writeBinding(sessionId, value) {
128
245
  const normalized = normalizeBinding(value);
@@ -136,21 +253,30 @@ var ClaudeSidecarRepository = class {
136
253
  return this.#update(sessionId, (current) => ({
137
254
  ...current,
138
255
  activities: mergeActivities(current.activities, [normalized])
139
- }));
256
+ }), false, {
257
+ kind: "activity",
258
+ activity: normalized
259
+ });
140
260
  }
141
261
  writeContextUsage(sessionId, value) {
142
262
  const normalized = normalizeContextUsage(value);
143
263
  return this.#update(sessionId, (current) => ({
144
264
  ...current,
145
265
  contextUsage: normalized
146
- }));
266
+ }), false, {
267
+ kind: "contextUsage",
268
+ value: normalized
269
+ });
147
270
  }
148
271
  writeTasks(sessionId, value) {
149
272
  const normalized = normalizeTasksEvent(value);
150
273
  return this.#update(sessionId, (current) => ({
151
274
  ...current,
152
275
  tasks: normalized
153
- }));
276
+ }), false, {
277
+ kind: "tasks",
278
+ value: normalized
279
+ });
154
280
  }
155
281
  importLegacy(sessionId, events) {
156
282
  const importedActivities = events.filter((event) => event.type === CLAUDE_ACTIVITY_EVENT).map((event) => activity(event.data)).filter((item) => item !== void 0);
@@ -163,15 +289,15 @@ var ClaudeSidecarRepository = class {
163
289
  ...current.binding !== void 0 || importedBinding === void 0 ? {} : { binding: normalizeBinding(importedBinding) },
164
290
  ...current.contextUsage !== void 0 || importedUsage === void 0 ? {} : { contextUsage: normalizeContextUsage(importedUsage) },
165
291
  ...current.tasks !== void 0 || importedTasks === void 0 ? {} : { tasks: normalizeTasksEvent(importedTasks.tasks) }
166
- }), true);
292
+ }), true, { kind: "sync" });
167
293
  }
168
294
  #path(sessionId, root = this.root) {
169
295
  if (sessionId.length === 0 || sessionId.length > 1024) throw new Error("dsh-claude: invalid session id");
170
296
  return join(root, `${Buffer.from(sessionId).toString("base64url")}.json`);
171
297
  }
172
- #update(sessionId, change, skipUnchanged = false) {
298
+ #update(sessionId, change, skipUnchanged = false, delta) {
173
299
  const operation = (this.#pending.get(sessionId) ?? Promise.resolve()).catch(() => void 0).then(async () => {
174
- const current = await this.#readNow(sessionId);
300
+ const current = await this.#base(sessionId);
175
301
  const changed = parseClaudeSidecar({
176
302
  ...change(current),
177
303
  schemaVersion: SIDECAR_SCHEMA_VERSION,
@@ -183,6 +309,8 @@ var ClaudeSidecarRepository = class {
183
309
  revision: current.revision + 1
184
310
  };
185
311
  await this.#writeNow(sessionId, next);
312
+ this.#latest.set(sessionId, next);
313
+ if (delta !== void 0) this.#notify(sessionId, delta);
186
314
  return next;
187
315
  });
188
316
  this.#pending.set(sessionId, operation);
@@ -525,10 +653,10 @@ function createUserQuestionBridge(userQuestions, activeContext) {
525
653
  }
526
654
  //#endregion
527
655
  //#region src/sdk-messages.ts
528
- function record$2(value) {
656
+ function record$4(value) {
529
657
  return value !== null && typeof value === "object" ? value : void 0;
530
658
  }
531
- function string$1(value) {
659
+ function string$2(value) {
532
660
  return typeof value === "string" ? value : void 0;
533
661
  }
534
662
  function taskUsageOf(usage) {
@@ -540,7 +668,7 @@ function taskUsageOf(usage) {
540
668
  return Object.keys(normalized).length === 0 ? void 0 : normalized;
541
669
  }
542
670
  function resultUsage(message) {
543
- const usage = record$2(message.usage);
671
+ const usage = record$4(message.usage);
544
672
  const normalized = {};
545
673
  if (usage !== void 0) {
546
674
  if (typeof usage.input_tokens === "number") normalized.inputTokens = usage.input_tokens;
@@ -552,26 +680,26 @@ function resultUsage(message) {
552
680
  return normalized;
553
681
  }
554
682
  function normalizeAssistant(message) {
555
- const content = record$2(message.message)?.content;
683
+ const content = record$4(message.message)?.content;
556
684
  if (!Array.isArray(content)) return [{
557
685
  kind: "protocol-error",
558
686
  title: "Malformed Claude assistant message",
559
687
  detail: message
560
688
  }];
561
- const parentToolUseId = string$1(message.parent_tool_use_id);
689
+ const parentToolUseId = string$2(message.parent_tool_use_id);
562
690
  const normalized = [];
563
691
  for (const item of content) {
564
- const block = record$2(item);
692
+ const block = record$4(item);
565
693
  if (block === void 0) continue;
566
694
  if (block.type === "text") {
567
- const text = string$1(block.text);
695
+ const text = string$2(block.text);
568
696
  if (text !== void 0 && text.length > 0) normalized.push({
569
697
  kind: "assistant-text",
570
698
  text,
571
699
  ...parentToolUseId === void 0 ? {} : { parentToolUseId }
572
700
  });
573
701
  } else if (block.type === "thinking") {
574
- const text = string$1(block.thinking);
702
+ const text = string$2(block.thinking);
575
703
  if (text !== void 0 && text.length > 0) normalized.push({
576
704
  kind: "thinking",
577
705
  text,
@@ -579,8 +707,8 @@ function normalizeAssistant(message) {
579
707
  ...parentToolUseId === void 0 ? {} : { parentToolUseId }
580
708
  });
581
709
  } else if (block.type === "tool_use") {
582
- const toolUseId = string$1(block.id);
583
- const toolName = string$1(block.name);
710
+ const toolUseId = string$2(block.id);
711
+ const toolName = string$2(block.name);
584
712
  if (toolUseId !== void 0 && toolName !== void 0) normalized.push({
585
713
  kind: "tool-call",
586
714
  toolUseId,
@@ -593,18 +721,20 @@ function normalizeAssistant(message) {
593
721
  return normalized;
594
722
  }
595
723
  function normalizeUser(message) {
596
- const content = record$2(message.message)?.content;
724
+ if (message.isReplay === true) return [];
725
+ const content = record$4(message.message)?.content;
726
+ if (typeof content === "string") return [];
597
727
  if (!Array.isArray(content)) return [{
598
728
  kind: "protocol-error",
599
729
  title: "Malformed Claude user message",
600
730
  detail: message
601
731
  }];
602
- const parentToolUseId = string$1(message.parent_tool_use_id);
732
+ const parentToolUseId = string$2(message.parent_tool_use_id);
603
733
  const normalized = [];
604
734
  for (const item of content) {
605
- const block = record$2(item);
735
+ const block = record$4(item);
606
736
  if (block?.type !== "tool_result") continue;
607
- const toolUseId = string$1(block.tool_use_id);
737
+ const toolUseId = string$2(block.tool_use_id);
608
738
  if (toolUseId === void 0) continue;
609
739
  normalized.push({
610
740
  kind: "tool-result",
@@ -617,11 +747,11 @@ function normalizeUser(message) {
617
747
  return normalized;
618
748
  }
619
749
  function normalizeSystem(message) {
620
- const subtype = string$1(message.subtype);
750
+ const subtype = string$2(message.subtype);
621
751
  if (subtype === "init") {
622
- const sessionId = string$1(message.session_id);
623
- const cliVersion = string$1(message.claude_code_version);
624
- const cwd = string$1(message.cwd);
752
+ const sessionId = string$2(message.session_id);
753
+ const cliVersion = string$2(message.claude_code_version);
754
+ const cwd = string$2(message.cwd);
625
755
  return sessionId !== void 0 && cliVersion !== void 0 && cwd !== void 0 ? [{
626
756
  kind: "init",
627
757
  sessionId,
@@ -650,20 +780,20 @@ function normalizeSystem(message) {
650
780
  title: `Claude session ${String(message.state)}`
651
781
  }];
652
782
  if (subtype === "permission_denied") {
653
- const toolUseId = string$1(message.tool_use_id);
654
- const toolName = string$1(message.tool_name);
783
+ const toolUseId = string$2(message.tool_use_id);
784
+ const toolName = string$2(message.tool_name);
655
785
  if (toolUseId !== void 0 && toolName !== void 0) return [{
656
786
  kind: "permission-denied",
657
787
  toolUseId,
658
788
  toolName,
659
- summary: string$1(message.message) ?? "Claude Code denied the action"
789
+ summary: string$2(message.message) ?? "Claude Code denied the action"
660
790
  }];
661
791
  }
662
792
  if (subtype === "task_started") {
663
- const taskId = string$1(message.task_id);
664
- const description = string$1(message.description);
665
- const subagentType = string$1(message.subagent_type);
666
- const taskType = string$1(message.task_type);
793
+ const taskId = string$2(message.task_id);
794
+ const description = string$2(message.description);
795
+ const subagentType = string$2(message.subagent_type);
796
+ const taskType = string$2(message.task_type);
667
797
  return [{
668
798
  kind: "subagent",
669
799
  title: description ?? taskId ?? "Claude subagent started",
@@ -678,12 +808,12 @@ function normalizeSystem(message) {
678
808
  }];
679
809
  }
680
810
  if (subtype === "task_progress") {
681
- const taskId = string$1(message.task_id);
682
- const description = string$1(message.description);
683
- const summary = string$1(message.summary);
684
- const subagentType = string$1(message.subagent_type);
685
- const lastToolName = string$1(message.last_tool_name);
686
- const usage = taskUsageOf(record$2(message.usage));
811
+ const taskId = string$2(message.task_id);
812
+ const description = string$2(message.description);
813
+ const summary = string$2(message.summary);
814
+ const subagentType = string$2(message.subagent_type);
815
+ const lastToolName = string$2(message.last_tool_name);
816
+ const usage = taskUsageOf(record$4(message.usage));
687
817
  return [{
688
818
  kind: "subagent",
689
819
  title: summary ?? description ?? "Claude subagent update",
@@ -699,11 +829,11 @@ function normalizeSystem(message) {
699
829
  }];
700
830
  }
701
831
  if (subtype === "task_updated") {
702
- const patch = record$2(message.patch);
703
- const status = string$1(patch?.status);
704
- const taskId = string$1(message.task_id);
705
- const description = string$1(patch?.description);
706
- const error = string$1(patch?.error);
832
+ const patch = record$4(message.patch);
833
+ const status = string$2(patch?.status);
834
+ const taskId = string$2(message.task_id);
835
+ const description = string$2(patch?.description);
836
+ const error = string$2(patch?.error);
707
837
  const taskStatus = status === void 0 ? void 0 : status === "killed" ? "killed" : status === "completed" ? "completed" : status === "failed" ? "failed" : "running";
708
838
  return [{
709
839
  kind: "subagent",
@@ -719,10 +849,10 @@ function normalizeSystem(message) {
719
849
  if (subtype === "task_notification") {
720
850
  const failed = message.status === "failed";
721
851
  const stopped = message.status === "stopped" || message.status === "cancelled";
722
- const taskId = string$1(message.task_id);
723
- const summary = string$1(message.summary);
852
+ const taskId = string$2(message.task_id);
853
+ const summary = string$2(message.summary);
724
854
  const taskStatus = failed ? "failed" : stopped ? "stopped" : "completed";
725
- const usage = taskUsageOf(record$2(message.usage));
855
+ const usage = taskUsageOf(record$4(message.usage));
726
856
  return [{
727
857
  kind: "subagent",
728
858
  title: summary ?? taskId ?? "Claude subagent finished",
@@ -737,10 +867,10 @@ function normalizeSystem(message) {
737
867
  if (subtype === "background_tasks_changed") return [{
738
868
  kind: "background-tasks",
739
869
  tasks: (Array.isArray(message.tasks) ? message.tasks : []).flatMap((item) => {
740
- const entry = record$2(item);
741
- const taskId = string$1(entry?.task_id);
742
- const description = string$1(entry?.description);
743
- const taskType = string$1(entry?.task_type);
870
+ const entry = record$4(item);
871
+ const taskId = string$2(entry?.task_id);
872
+ const description = string$2(entry?.description);
873
+ const taskType = string$2(entry?.task_type);
744
874
  if (taskId === void 0 || description === void 0) return [];
745
875
  return [{
746
876
  taskId,
@@ -756,7 +886,7 @@ function normalizeSystem(message) {
756
886
  }];
757
887
  if (subtype === "informational" || subtype === "notification" || subtype === "local_command_output") return [{
758
888
  kind: message.level === "warning" ? "warning" : "status",
759
- title: string$1(message.content) ?? string$1(message.text) ?? "Claude Code notice",
889
+ title: string$2(message.content) ?? string$2(message.text) ?? "Claude Code notice",
760
890
  detail: message
761
891
  }];
762
892
  if (subtype?.startsWith("hook_") === true || subtype === "plugin_install") return [{
@@ -780,12 +910,12 @@ const RESULT_ERROR_SUBTYPES = /* @__PURE__ */ new Set([
780
910
  function normalizeSdkMessage(message) {
781
911
  const value = message;
782
912
  if (value.type === "stream_event") {
783
- const event = record$2(value.event);
784
- const parentToolUseId = string$1(value.parent_tool_use_id);
913
+ const event = record$4(value.event);
914
+ const parentToolUseId = string$2(value.parent_tool_use_id);
785
915
  if (event?.type === "content_block_delta") {
786
- const delta = record$2(event.delta);
916
+ const delta = record$4(event.delta);
787
917
  if (delta?.type === "text_delta") {
788
- const text = string$1(delta.text);
918
+ const text = string$2(delta.text);
789
919
  return text === void 0 ? [] : [{
790
920
  kind: "text-delta",
791
921
  text,
@@ -793,7 +923,7 @@ function normalizeSdkMessage(message) {
793
923
  }];
794
924
  }
795
925
  if (delta?.type === "thinking_delta") {
796
- const text = string$1(delta.thinking);
926
+ const text = string$2(delta.thinking);
797
927
  return text === void 0 ? [] : [{
798
928
  kind: "thinking",
799
929
  text,
@@ -808,7 +938,7 @@ function normalizeSdkMessage(message) {
808
938
  if (value.type === "user") return normalizeUser(value);
809
939
  if (value.type === "system") return normalizeSystem(value);
810
940
  if (value.type === "result") {
811
- const sessionId = string$1(value.session_id);
941
+ const sessionId = string$2(value.session_id);
812
942
  if (sessionId === void 0 || value.subtype !== "success" && !RESULT_ERROR_SUBTYPES.has(String(value.subtype))) return [{
813
943
  kind: "protocol-error",
814
944
  title: "Malformed Claude result message",
@@ -816,11 +946,11 @@ function normalizeSdkMessage(message) {
816
946
  }];
817
947
  const success = value.subtype === "success" && value.is_error !== true;
818
948
  const errors = Array.isArray(value.errors) ? value.errors.filter((item) => typeof item === "string") : void 0;
819
- const terminalReason = string$1(value.terminal_reason);
820
- const userMessageUuid = string$1(value.user_message_uuid);
821
- const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$2(item)).filter((item) => item !== void 0).map((item) => {
822
- const toolName = string$1(item.tool_name);
823
- const toolUseId = string$1(item.tool_use_id);
949
+ const terminalReason = string$2(value.terminal_reason);
950
+ const userMessageUuid = string$2(value.user_message_uuid);
951
+ const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$4(item)).filter((item) => item !== void 0).map((item) => {
952
+ const toolName = string$2(item.tool_name);
953
+ const toolUseId = string$2(item.tool_use_id);
824
954
  return toolName === void 0 || toolUseId === void 0 ? void 0 : {
825
955
  toolName,
826
956
  toolUseId
@@ -844,11 +974,10 @@ function normalizeSdkMessage(message) {
844
974
  detail: value.error ?? value.output
845
975
  }];
846
976
  if (value.type === "rate_limit_event") {
847
- const status = string$1(record$2(value.rate_limit_info)?.status);
848
- const blocking = status !== void 0 && status !== "allowed";
977
+ const status = string$2(record$4(value.rate_limit_info)?.status);
849
978
  return [{
850
- kind: blocking ? "warning" : "status",
851
- title: blocking ? "Claude rate limit is blocking requests" : "Claude rate limit status changed",
979
+ kind: "status",
980
+ title: status !== void 0 && status !== "allowed" ? "Claude rate limit is blocking requests" : "Claude rate limit status changed",
852
981
  detail: value.rate_limit_info
853
982
  }];
854
983
  }
@@ -1034,6 +1163,7 @@ function rootCallSummary(toolName, input) {
1034
1163
  }
1035
1164
  var ClaudeSupervisor = class {
1036
1165
  #entries = /* @__PURE__ */ new Map();
1166
+ #interruptions = /* @__PURE__ */ new Map();
1037
1167
  #runtime;
1038
1168
  #approval;
1039
1169
  #userQuestions;
@@ -1041,7 +1171,6 @@ var ClaudeSupervisor = class {
1041
1171
  #queryFactory;
1042
1172
  #runDetached;
1043
1173
  #sidecar;
1044
- #dynamicPresenterNames = /* @__PURE__ */ new WeakMap();
1045
1174
  #contextWindows = /* @__PURE__ */ new Map();
1046
1175
  #disposed = false;
1047
1176
  #admissionGate = Promise.resolve();
@@ -1082,6 +1211,8 @@ var ClaudeSupervisor = class {
1082
1211
  return this.#contextWindows.get(model);
1083
1212
  }
1084
1213
  runTurn(request) {
1214
+ const interruption = this.#interruptions.get(request.agent.id);
1215
+ if (interruption !== void 0) return interruption.then(() => this.runTurn(request));
1085
1216
  const operation = this.#admissionGate.then(() => this.#runTurnAdmitted(request));
1086
1217
  this.#admissionGate = operation.then(() => void 0, () => void 0);
1087
1218
  return operation;
@@ -1134,6 +1265,8 @@ var ClaudeSupervisor = class {
1134
1265
  sawActivity: false,
1135
1266
  sawTextDelta: false,
1136
1267
  text: "",
1268
+ transcriptText: "",
1269
+ transcriptTextOrdinal: void 0,
1137
1270
  thinking: "",
1138
1271
  aborted: false,
1139
1272
  deniedToolUseIds: /* @__PURE__ */ new Set(),
@@ -1172,7 +1305,7 @@ var ClaudeSupervisor = class {
1172
1305
  }
1173
1306
  if (request.signal !== void 0) {
1174
1307
  const abortListener = () => {
1175
- this.#interrupt(entry);
1308
+ this.#startInterrupt(entry);
1176
1309
  };
1177
1310
  active.abortListener = abortListener;
1178
1311
  request.signal.addEventListener("abort", abortListener, { once: true });
@@ -1387,7 +1520,14 @@ var ClaudeSupervisor = class {
1387
1520
  if (message.userMessageUuid !== void 0 && message.userMessageUuid === entry.handshakeUuid) return;
1388
1521
  if (entry.claudeSessionId === void 0) throw new ClaudeProtocolError("Claude Code sent a result before initialization");
1389
1522
  if (message.sessionId !== entry.claudeSessionId) throw new ClaudeProtocolError(`Claude Code result session ${message.sessionId} does not match ${entry.claudeSessionId}`);
1390
- if (message.userMessageUuid !== void 0 && message.userMessageUuid !== active.promptUuid) throw new ClaudeProtocolError(`Claude Code result for user message ${message.userMessageUuid} does not match active request ${active.promptUuid}`);
1523
+ if (active.phase === "waiting-tasks") {
1524
+ await this.#completeProgressSegment(active, message);
1525
+ return;
1526
+ }
1527
+ if (message.userMessageUuid !== void 0 && message.userMessageUuid !== active.promptUuid) {
1528
+ if (active.phase === "follow-up") return;
1529
+ throw new ClaudeProtocolError(`Claude Code result for user message ${message.userMessageUuid} does not match active request ${active.promptUuid}`);
1530
+ }
1391
1531
  await this.#completeTurn(entry, active, message);
1392
1532
  return;
1393
1533
  }
@@ -1398,6 +1538,8 @@ var ClaudeSupervisor = class {
1398
1538
  if (message.parentToolUseId !== void 0) return;
1399
1539
  active.sawTextDelta = true;
1400
1540
  active.text += message.text;
1541
+ active.transcriptText += message.text;
1542
+ await this.#upsertTranscriptText(active);
1401
1543
  active.output.push({
1402
1544
  type: "text-delta",
1403
1545
  text: message.text
@@ -1408,6 +1550,8 @@ var ClaudeSupervisor = class {
1408
1550
  if (!active.sawTextDelta) {
1409
1551
  active.sawTextDelta = true;
1410
1552
  active.text += message.text;
1553
+ active.transcriptText += message.text;
1554
+ await this.#upsertTranscriptText(active);
1411
1555
  active.output.push({
1412
1556
  type: "text-delta",
1413
1557
  text: message.text
@@ -1429,6 +1573,7 @@ var ClaudeSupervisor = class {
1429
1573
  });
1430
1574
  return;
1431
1575
  case "tool-call":
1576
+ if (message.parentToolUseId === void 0) this.#closeTranscriptTextSegment(active);
1432
1577
  await this.#appendActivity(active, {
1433
1578
  kind: message.parentToolUseId === void 0 ? "tool-call" : "subagent",
1434
1579
  phase: "started",
@@ -1439,11 +1584,7 @@ var ClaudeSupervisor = class {
1439
1584
  summary: message.parentToolUseId === void 0 ? rootCallSummary(message.toolName, message.input) : `Subagent called ${message.toolName}`,
1440
1585
  detail: message.input
1441
1586
  });
1442
- if (message.parentToolUseId === void 0) {
1443
- active.callNames.set(message.toolUseId, message.toolName);
1444
- this.#ensureDynamicPresenter(active.agent, message.toolName);
1445
- if (!TASK_TOOL_NAMES.has(message.toolName)) await this.#appendNativeToolCall(active, message);
1446
- }
1587
+ if (message.parentToolUseId === void 0) active.callNames.set(message.toolUseId, message.toolName);
1447
1588
  return;
1448
1589
  case "tool-result":
1449
1590
  await this.#appendActivity(active, {
@@ -1455,7 +1596,6 @@ var ClaudeSupervisor = class {
1455
1596
  detail: message.output,
1456
1597
  isError: message.isError
1457
1598
  });
1458
- if (message.parentToolUseId === void 0 && !TASK_TOOL_NAMES.has(active.callNames.get(message.toolUseId) ?? "")) await this.#appendNativeToolResult(active, message);
1459
1599
  return;
1460
1600
  case "subagent":
1461
1601
  await this.#appendActivity(active, {
@@ -1488,12 +1628,6 @@ var ClaudeSupervisor = class {
1488
1628
  title: message.toolName,
1489
1629
  summary: message.summary
1490
1630
  });
1491
- if (!TASK_TOOL_NAMES.has(active.callNames.get(message.toolUseId) ?? message.toolName)) await this.#appendNativeToolResult(active, {
1492
- kind: "tool-result",
1493
- toolUseId: message.toolUseId,
1494
- output: message.summary,
1495
- isError: true
1496
- });
1497
1631
  return;
1498
1632
  }
1499
1633
  }
@@ -1571,6 +1705,7 @@ var ClaudeSupervisor = class {
1571
1705
  active.promptUuid = randomUUID();
1572
1706
  active.sawTextDelta = false;
1573
1707
  active.text = "";
1708
+ this.#closeTranscriptTextSegment(active);
1574
1709
  active.thinking = "";
1575
1710
  await this.#appendSafely(active, {
1576
1711
  kind: "status",
@@ -1604,9 +1739,44 @@ var ClaudeSupervisor = class {
1604
1739
  entry.taskSnapshotAt = Date.now();
1605
1740
  await this.#sidecar.writeTasks(entry.sessionId, [...entry.tasks.values()]).catch(() => void 0);
1606
1741
  }
1742
+ async #completeProgressSegment(active, result, recordUsage = true) {
1743
+ if (recordUsage && (result.usage.inputTokens !== void 0 || result.usage.outputTokens !== void 0 || result.usage.cumulativeCostUsd !== void 0)) {
1744
+ await this.#appendSafely(active, {
1745
+ kind: "usage",
1746
+ phase: "completed",
1747
+ title: "Claude usage",
1748
+ summary: usageSummary(result.usage),
1749
+ usage: result.usage
1750
+ });
1751
+ active.output.push({
1752
+ type: "usage",
1753
+ usage: result.usage
1754
+ });
1755
+ }
1756
+ if (!active.sawTextDelta && active.text.length === 0 && result.text !== void 0) {
1757
+ active.text = result.text;
1758
+ active.transcriptText = result.text;
1759
+ await this.#upsertTranscriptText(active);
1760
+ active.output.push({
1761
+ type: "text-delta",
1762
+ text: result.text
1763
+ });
1764
+ }
1765
+ await this.#upsertTranscriptText(active);
1766
+ active.output.push({
1767
+ type: "segment-complete",
1768
+ text: active.text
1769
+ });
1770
+ active.sawTextDelta = false;
1771
+ active.text = "";
1772
+ this.#closeTranscriptTextSegment(active);
1773
+ active.thinking = "";
1774
+ }
1607
1775
  async #completeTurn(entry, active, result) {
1608
1776
  if (entry.active !== active) return;
1609
1777
  if (active.aborted) {
1778
+ await this.#upsertTranscriptText(active);
1779
+ await this.#flushTranscript(active);
1610
1780
  await this.#appendSafely(active, {
1611
1781
  kind: "status",
1612
1782
  phase: "failed",
@@ -1639,6 +1809,17 @@ var ClaudeSupervisor = class {
1639
1809
  summary: unmatchedDenials.map((denial) => denial.toolName).join(", ")
1640
1810
  });
1641
1811
  if (!result.success) {
1812
+ if (!active.sawTextDelta && active.text.length === 0 && result.text !== void 0) {
1813
+ active.text = result.text;
1814
+ active.transcriptText = result.text;
1815
+ await this.#upsertTranscriptText(active);
1816
+ active.output.push({
1817
+ type: "text-delta",
1818
+ text: result.text
1819
+ });
1820
+ }
1821
+ await this.#upsertTranscriptText(active);
1822
+ await this.#flushTranscript(active);
1642
1823
  const message = result.errors?.join("\n") ?? (result.terminalReason !== void 0 ? `Claude Code failed the turn (${result.terminalReason})` : "Claude Code failed the turn");
1643
1824
  await this.#appendSafely(active, {
1644
1825
  kind: "error",
@@ -1651,11 +1832,14 @@ var ClaudeSupervisor = class {
1651
1832
  } else {
1652
1833
  if (!active.sawTextDelta && active.text.length === 0 && result.text !== void 0) {
1653
1834
  active.text = result.text;
1835
+ active.transcriptText = result.text;
1836
+ await this.#upsertTranscriptText(active);
1654
1837
  active.output.push({
1655
1838
  type: "text-delta",
1656
1839
  text: result.text
1657
1840
  });
1658
1841
  }
1842
+ await this.#upsertTranscriptText(active);
1659
1843
  if (active.phase === "primary" && this.#hasRunningTasks(entry, active)) {
1660
1844
  active.phase = "waiting-tasks";
1661
1845
  await this.#appendSafely(active, {
@@ -1663,10 +1847,7 @@ var ClaudeSupervisor = class {
1663
1847
  phase: "updated",
1664
1848
  title: "Claude Code is waiting for background tasks"
1665
1849
  });
1666
- active.output.push({
1667
- type: "segment-complete",
1668
- text: active.text
1669
- });
1850
+ await this.#completeProgressSegment(active, result, false);
1670
1851
  return;
1671
1852
  }
1672
1853
  await this.#appendSafely(active, {
@@ -1674,6 +1855,7 @@ var ClaudeSupervisor = class {
1674
1855
  phase: "completed",
1675
1856
  title: "Claude Code turn completed"
1676
1857
  });
1858
+ await this.#flushTranscript(active);
1677
1859
  active.output.push({
1678
1860
  type: "complete",
1679
1861
  text: active.text
@@ -1686,6 +1868,27 @@ var ClaudeSupervisor = class {
1686
1868
  entry.lastUsedAt = Date.now();
1687
1869
  this.#armIdleTimer(entry);
1688
1870
  }
1871
+ async #upsertTranscriptText(active) {
1872
+ if (active.transcriptText.length === 0) return;
1873
+ const ordinal = active.transcriptTextOrdinal ?? active.cursor.nextOrdinal++;
1874
+ active.transcriptTextOrdinal = ordinal;
1875
+ try {
1876
+ this.#sidecar.appendTranscriptText(active.agent.id, {
1877
+ text: active.transcriptText,
1878
+ turn: active.cursor.turn,
1879
+ step: active.cursor.step,
1880
+ ordinal
1881
+ });
1882
+ } catch {}
1883
+ }
1884
+ async #flushTranscript(active) {
1885
+ await this.#sidecar.flushTranscriptText(active.agent.id).catch(() => void 0);
1886
+ }
1887
+ #closeTranscriptTextSegment(active) {
1888
+ this.#flushTranscript(active);
1889
+ active.transcriptText = "";
1890
+ active.transcriptTextOrdinal = void 0;
1891
+ }
1689
1892
  async #appendActivity(active, activity) {
1690
1893
  const ordinal = active.cursor.nextOrdinal++;
1691
1894
  await this.#sidecar.appendActivity(active.agent.id, {
@@ -1700,53 +1903,14 @@ var ClaudeSupervisor = class {
1700
1903
  async #appendSafely(active, activity) {
1701
1904
  await this.#appendActivity(active, activity).catch(() => void 0);
1702
1905
  }
1703
- /** Register one presenter-only mirror for a tool name the static preset
1704
- * registry does not cover (MCP tools, newly added built-ins). Runs in the
1705
- * agent scope so the mirror is visible only to this preset's sessions and
1706
- * unwinds with the agent; failure keeps the generic card, never the turn. */
1707
- #ensureDynamicPresenter(agent, name) {
1708
- if (CLAUDE_PRESENTER_NAMES.has(name)) return;
1709
- let known = this.#dynamicPresenterNames.get(agent);
1710
- if (known === void 0) {
1711
- known = /* @__PURE__ */ new Set();
1712
- this.#dynamicPresenterNames.set(agent, known);
1713
- }
1714
- if (known.has(name)) return;
1715
- try {
1716
- agent.ctx.tools.register(dynamicPresenterDefinition(name));
1717
- known.add(name);
1718
- } catch {}
1719
- }
1720
- /** Mirror one root Claude tool call into the durable native tool channel so
1721
- * the host's tool presentation renders it exactly like a DSH-executed call.
1722
- * Presentation duplication is best-effort and never unsettles the turn. */
1723
- async #appendNativeToolCall(active, message) {
1724
- try {
1725
- await active.agent.session.append("tool/call", {
1726
- turn: active.cursor.turn,
1727
- step: active.cursor.step,
1728
- callId: CallId(message.toolUseId),
1729
- name: message.toolName,
1730
- arguments: safeDetail(message.input) ?? "{}"
1731
- });
1732
- } catch {}
1733
- }
1734
- async #appendNativeToolResult(active, message) {
1735
- const text = typeof message.output === "string" ? redactText(message.output) : safeDetail(message.output) ?? "";
1736
- try {
1737
- await active.agent.session.append("tool/result", {
1738
- turn: active.cursor.turn,
1739
- step: active.cursor.step,
1740
- message: createToolResultMessage({
1741
- callId: CallId(message.toolUseId),
1742
- content: [{
1743
- type: "text",
1744
- text
1745
- }],
1746
- isError: message.isError
1747
- })
1748
- }, { surfaceOp: "append" });
1749
- } catch {}
1906
+ #startInterrupt(entry) {
1907
+ const existing = this.#interruptions.get(entry.sessionId);
1908
+ if (existing !== void 0) return existing;
1909
+ const interruption = this.#interrupt(entry).finally(() => {
1910
+ this.#interruptions.delete(entry.sessionId);
1911
+ });
1912
+ this.#interruptions.set(entry.sessionId, interruption);
1913
+ return interruption;
1750
1914
  }
1751
1915
  async #interrupt(entry) {
1752
1916
  const active = entry.active;
@@ -1754,6 +1918,7 @@ var ClaudeSupervisor = class {
1754
1918
  entry.state = "interrupting";
1755
1919
  active.aborted = true;
1756
1920
  active.output.fail(abortFailure());
1921
+ await this.#upsertTranscriptText(active);
1757
1922
  let interruptError;
1758
1923
  try {
1759
1924
  if (((await withTimeout(entry.query.interrupt(), 5e3, "Claude Code interrupt"))?.still_queued ?? []).includes(active.promptUuid)) throw new Error(`Claude Code interrupt left submitted prompt ${active.promptUuid} queued`);
@@ -1775,6 +1940,8 @@ var ClaudeSupervisor = class {
1775
1940
  const active = entry.active;
1776
1941
  const stderr = entry.process?.stderrTail();
1777
1942
  if (active !== void 0) {
1943
+ await this.#upsertTranscriptText(active);
1944
+ await this.#flushTranscript(active);
1778
1945
  if (active.signal !== void 0 && active.abortListener !== void 0) active.signal.removeEventListener("abort", active.abortListener);
1779
1946
  const unknown = active.sawActivity;
1780
1947
  entry.state = unknown ? "outcome-unknown" : "disconnected";
@@ -1830,6 +1997,79 @@ var ClaudeSupervisor = class {
1830
1997
  }
1831
1998
  };
1832
1999
  //#endregion
2000
+ //#region src/review-comments.ts
2001
+ const MAX_COMMENTS_PER_SESSION = 50;
2002
+ const MAX_TEXT_CHARS$1 = 2e3;
2003
+ const MAX_PATH_CHARS$1 = 1024;
2004
+ const MAX_LINE = 1e7;
2005
+ var ReviewCommentError = class extends Error {
2006
+ code;
2007
+ constructor(code, message) {
2008
+ super(message);
2009
+ this.name = "ReviewCommentError";
2010
+ this.code = code;
2011
+ }
2012
+ };
2013
+ function validSide(value) {
2014
+ return value === "old" || value === "new";
2015
+ }
2016
+ /** Pending line comments queued per session until the next user turn drains them. */
2017
+ var ReviewCommentStore = class {
2018
+ #comments = /* @__PURE__ */ new Map();
2019
+ add(sessionId, input) {
2020
+ const path = typeof input.path === "string" ? input.path.trim() : "";
2021
+ const text = typeof input.text === "string" ? input.text.trim() : "";
2022
+ if (path.length === 0 || path.length > MAX_PATH_CHARS$1 || /[\0\r\n]/u.test(path)) throw new ReviewCommentError("invalid-request", "The comment path is invalid.");
2023
+ if (text.length === 0 || text.length > MAX_TEXT_CHARS$1 || text.includes("\0")) throw new ReviewCommentError("invalid-request", "The comment text is invalid.");
2024
+ if (typeof input.line !== "number" || !Number.isSafeInteger(input.line) || input.line < 1 || input.line > MAX_LINE) throw new ReviewCommentError("invalid-request", "The comment line is invalid.");
2025
+ if (!validSide(input.side)) throw new ReviewCommentError("invalid-request", "The comment side is invalid.");
2026
+ const existing = this.#comments.get(sessionId) ?? [];
2027
+ if (existing.length >= MAX_COMMENTS_PER_SESSION) throw new ReviewCommentError("too-many-comments", "Too many pending review comments. Remove one before adding another.");
2028
+ const comment = {
2029
+ id: randomUUID(),
2030
+ path,
2031
+ line: input.line,
2032
+ side: input.side,
2033
+ text
2034
+ };
2035
+ this.#comments.set(sessionId, [...existing, comment]);
2036
+ return comment;
2037
+ }
2038
+ remove(sessionId, id) {
2039
+ const existing = this.#comments.get(sessionId);
2040
+ if (existing === void 0) return false;
2041
+ const next = existing.filter((comment) => comment.id !== id);
2042
+ if (next.length === existing.length) return false;
2043
+ if (next.length === 0) this.#comments.delete(sessionId);
2044
+ else this.#comments.set(sessionId, next);
2045
+ return true;
2046
+ }
2047
+ list(sessionId) {
2048
+ return this.#comments.get(sessionId) ?? [];
2049
+ }
2050
+ /** Remove and return every pending comment; called when a user turn consumes them. */
2051
+ drain(sessionId) {
2052
+ const existing = this.#comments.get(sessionId) ?? [];
2053
+ this.#comments.delete(sessionId);
2054
+ return existing;
2055
+ }
2056
+ disposeSession(sessionId) {
2057
+ this.#comments.delete(sessionId);
2058
+ }
2059
+ dispose() {
2060
+ this.#comments.clear();
2061
+ }
2062
+ };
2063
+ /** Render drained comments as the prompt block preceding the user's message text. */
2064
+ function formatReviewComments(comments) {
2065
+ return [
2066
+ "<user-review-comments>",
2067
+ "The user attached these code review comments to this message. Each references a file and line from the current working tree diff:",
2068
+ ...comments.map((comment, index) => `${index + 1}. ${comment.path}:${comment.line}${comment.side === "old" ? " (old side)" : ""} — ${comment.text}`),
2069
+ "</user-review-comments>"
2070
+ ].join("\n");
2071
+ }
2072
+ //#endregion
1833
2073
  //#region src/adapter.ts
1834
2074
  const MODELS = [
1835
2075
  {
@@ -1926,6 +2166,22 @@ function validateImageRef(ref, attachments, imageIndex) {
1926
2166
  const maxDimension = "maxImageDimension" in limits && finiteNonNegative(limits.maxImageDimension) ? limits.maxImageDimension : void 0;
1927
2167
  if (!finiteNonNegative(ref.width) || !finiteNonNegative(ref.height) || ref.width * ref.height > limits.maxImagePixels || maxDimension !== void 0 && (ref.width > maxDimension || ref.height > maxDimension)) throw new Error(`dsh-claude: image ${imageIndex} exceeds the configured dimension limit`);
1928
2168
  }
2169
+ /**
2170
+ * Prepend the session's pending diff-review comments to the outgoing user
2171
+ * prompt. The comments are drained once per turn: they are formatted into one
2172
+ * `<user-review-comments>` block placed ahead of the user's own text (or as a
2173
+ * leading text block for multi-part prompts) so Claude reads them in the same
2174
+ * turn that consumed them.
2175
+ */
2176
+ function injectReviewComments(prompt, comments) {
2177
+ if (comments.length === 0) return prompt;
2178
+ const block = formatReviewComments(comments);
2179
+ if (typeof prompt === "string") return `${block}\n\n${prompt}`;
2180
+ return [{
2181
+ type: "text",
2182
+ text: block
2183
+ }, ...prompt];
2184
+ }
1929
2185
  function imageBlock(data, mediaType) {
1930
2186
  return {
1931
2187
  type: "image",
@@ -2008,12 +2264,14 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
2008
2264
  #agents;
2009
2265
  #attachments;
2010
2266
  #presetIdFor;
2011
- constructor(supervisor, agents, attachments, presetIdFor) {
2267
+ #drainReviewComments;
2268
+ constructor(supervisor, agents, attachments, presetIdFor, drainReviewComments = () => []) {
2012
2269
  super();
2013
2270
  this.#supervisor = supervisor;
2014
2271
  this.#agents = agents;
2015
2272
  this.#attachments = attachments;
2016
2273
  this.#presetIdFor = presetIdFor;
2274
+ this.#drainReviewComments = drainReviewComments;
2017
2275
  }
2018
2276
  providerInfo(provider) {
2019
2277
  return {
@@ -2080,42 +2338,20 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
2080
2338
  }
2081
2339
  const events = await this.#supervisor.runTurn({
2082
2340
  agent,
2083
- prompt,
2341
+ prompt: injectReviewComments(prompt, this.#drainReviewComments(agent.id)),
2084
2342
  model: options.model,
2085
2343
  ...thinkingMode === void 0 ? {} : { thinkingMode },
2086
2344
  ...options.signal === void 0 ? {} : { signal: options.signal }
2087
2345
  });
2088
- let text = "";
2089
2346
  let pendingUsage;
2090
2347
  let completed = false;
2091
- let blockIndex = 0;
2092
2348
  try {
2093
- for await (const event of events) if (event.type === "text-delta") text += event.text;
2094
- else if (event.type === "usage") pendingUsage = tokenUsage(event.usage);
2095
- else {
2096
- if (text.length > 0) {
2097
- yield {
2098
- type: "block-start",
2099
- index: blockIndex,
2100
- blockType: "text"
2101
- };
2102
- yield {
2103
- type: "text-delta",
2104
- index: blockIndex,
2105
- text
2106
- };
2107
- yield {
2108
- type: "block-end",
2109
- index: blockIndex,
2110
- block: {
2111
- type: "text",
2112
- text
2113
- }
2114
- };
2115
- blockIndex += 1;
2349
+ for await (const event of events) {
2350
+ if (event.type === "text-delta" || event.type === "segment-complete") continue;
2351
+ if (event.type === "usage") {
2352
+ pendingUsage = tokenUsage(event.usage);
2353
+ continue;
2116
2354
  }
2117
- text = "";
2118
- if (event.type === "segment-complete") continue;
2119
2355
  completed = true;
2120
2356
  if (pendingUsage !== void 0) yield {
2121
2357
  type: "usage",
@@ -2129,26 +2365,6 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
2129
2365
  } catch (error) {
2130
2366
  if (error.name === "AbortError") {
2131
2367
  completed = true;
2132
- if (text.length > 0) {
2133
- yield {
2134
- type: "block-start",
2135
- index: blockIndex,
2136
- blockType: "text"
2137
- };
2138
- yield {
2139
- type: "text-delta",
2140
- index: blockIndex,
2141
- text
2142
- };
2143
- yield {
2144
- type: "block-end",
2145
- index: blockIndex,
2146
- block: {
2147
- type: "text",
2148
- text
2149
- }
2150
- };
2151
- }
2152
2368
  yield {
2153
2369
  type: "finish",
2154
2370
  reason: {
@@ -2166,8 +2382,8 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
2166
2382
  if (!completed) throw new Error("dsh-claude: Claude turn stream ended without a result");
2167
2383
  }
2168
2384
  };
2169
- function createClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor) {
2170
- return new ClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor);
2385
+ function createClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments = () => []) {
2386
+ return new ClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments);
2171
2387
  }
2172
2388
  //#endregion
2173
2389
  //#region src/http.ts
@@ -2294,63 +2510,191 @@ function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolution
2294
2510
  }
2295
2511
  //#endregion
2296
2512
  //#region src/projection-routes.ts
2297
- const MAX_SESSION_ID_CHARS = 1024;
2298
- function sessionIdFromUrl(rawUrl) {
2513
+ const MAX_SESSION_ID_CHARS$2 = 1024;
2514
+ /** Slow-moving metadata refresh and stream heartbeat cadence; deliberately off
2515
+ * the transcript hot path so git/gh latency never delays visible text. */
2516
+ const META_REFRESH_MS = 5e3;
2517
+ function targetFromUrl(rawUrl) {
2299
2518
  try {
2300
2519
  const pathname = new URL(rawUrl ?? "/", "http://localhost").pathname;
2301
2520
  const prefix = `${CLAUDE_PROJECTION_PATH}/`;
2302
2521
  if (!pathname.startsWith(prefix)) return void 0;
2303
- const encoded = pathname.slice(prefix.length);
2522
+ let encoded = pathname.slice(prefix.length);
2523
+ const stream = encoded.endsWith("/stream");
2524
+ if (stream) encoded = encoded.slice(0, -7);
2304
2525
  if (encoded.length === 0 || encoded.includes("/")) return void 0;
2305
2526
  const sessionId = decodeURIComponent(encoded);
2306
- if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS) return void 0;
2307
- return sessionId;
2527
+ if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS$2) return void 0;
2528
+ return {
2529
+ sessionId,
2530
+ stream
2531
+ };
2308
2532
  } catch {
2309
2533
  return;
2310
2534
  }
2311
2535
  }
2312
- /** Register the browser-readable, credential-free sidecar projection endpoint. */
2313
- function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSession = () => [], repositoryForSession = async () => void 0) {
2536
+ function envelope(projection, meta) {
2537
+ return {
2538
+ schemaVersion: projection.schemaVersion,
2539
+ revision: projection.revision,
2540
+ owned: meta.owned,
2541
+ commands: meta.commands,
2542
+ activities: projection.activities,
2543
+ ...projection.contextUsage === void 0 ? {} : { contextUsage: projection.contextUsage },
2544
+ ...projection.tasks === void 0 ? {} : { tasks: projection.tasks },
2545
+ ...meta.repository === void 0 ? {} : { repository: meta.repository },
2546
+ reviewComments: meta.reviewComments
2547
+ };
2548
+ }
2549
+ /** Register the browser-readable, credential-free sidecar projection endpoint.
2550
+ * `GET <path>/:sessionId` returns one snapshot; `GET <path>/:sessionId/stream`
2551
+ * returns an NDJSON stream: a full snapshot line followed by incremental
2552
+ * transcript/activity deltas and periodic metadata/heartbeat lines. */
2553
+ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSession = () => [], repositoryForSession = async () => void 0, reviewCommentsForSession = () => []) {
2554
+ const info = (message) => {
2555
+ ctx.logger?.info?.(message);
2556
+ };
2557
+ const assembleMeta = async (sessionId) => {
2558
+ const owned = ownsSession(sessionId);
2559
+ const repository = owned ? await repositoryForSession(sessionId) : void 0;
2560
+ return {
2561
+ owned,
2562
+ commands: commandsForSession(sessionId),
2563
+ ...repository === void 0 ? {} : { repository },
2564
+ reviewComments: owned ? reviewCommentsForSession(sessionId) : []
2565
+ };
2566
+ };
2567
+ const streamProjection = async (res, sessionId) => {
2568
+ info(`dsh-claude: projection stream opened for ${sessionId.slice(0, 64)}`);
2569
+ let textDeltas = 0;
2570
+ let textBytes = 0;
2571
+ let textSince = Date.now();
2572
+ let meta = await assembleMeta(sessionId);
2573
+ res.writeHead(200, {
2574
+ "content-type": "application/x-ndjson; charset=utf-8",
2575
+ "cache-control": "no-store",
2576
+ "x-content-type-options": "nosniff"
2577
+ });
2578
+ res.flushHeaders?.();
2579
+ let closed = false;
2580
+ const writeLine = (value) => {
2581
+ if (closed) return;
2582
+ try {
2583
+ res.write(`${JSON.stringify(value)}\n`);
2584
+ } catch {
2585
+ closed = true;
2586
+ }
2587
+ };
2588
+ const writeSnapshot = async () => {
2589
+ const projection = await sidecar.read(sessionId);
2590
+ writeLine({
2591
+ type: "snapshot",
2592
+ ...envelope(projection, meta)
2593
+ });
2594
+ };
2595
+ await writeSnapshot();
2596
+ const unsubscribe = sidecar.subscribe(sessionId, (delta) => {
2597
+ switch (delta.kind) {
2598
+ case "text":
2599
+ textDeltas += 1;
2600
+ textBytes += (delta.append ?? delta.text ?? "").length;
2601
+ if (textDeltas % 25 === 0) {
2602
+ const elapsed = Date.now() - textSince;
2603
+ info(`dsh-claude: stream ${sessionId.slice(0, 24)} 25 text deltas ${textBytes}B in ${elapsed}ms`);
2604
+ textBytes = 0;
2605
+ textSince = Date.now();
2606
+ }
2607
+ writeLine({
2608
+ type: "text",
2609
+ turn: delta.turn,
2610
+ step: delta.step,
2611
+ ordinal: delta.ordinal,
2612
+ ...delta.append === void 0 ? {} : { append: delta.append },
2613
+ ...delta.text === void 0 ? {} : { text: delta.text }
2614
+ });
2615
+ return;
2616
+ case "activity":
2617
+ writeLine({
2618
+ type: "activity",
2619
+ activity: delta.activity
2620
+ });
2621
+ return;
2622
+ case "contextUsage":
2623
+ writeLine({
2624
+ type: "contextUsage",
2625
+ value: delta.value
2626
+ });
2627
+ return;
2628
+ case "tasks":
2629
+ writeLine({
2630
+ type: "tasks",
2631
+ value: delta.value
2632
+ });
2633
+ return;
2634
+ case "sync": writeSnapshot().catch(() => void 0);
2635
+ }
2636
+ });
2637
+ const timer = setInterval(() => {
2638
+ (async () => {
2639
+ const next = await assembleMeta(sessionId);
2640
+ if (closed) return;
2641
+ if (JSON.stringify(next) === JSON.stringify(meta)) {
2642
+ writeLine({ type: "ping" });
2643
+ return;
2644
+ }
2645
+ meta = next;
2646
+ writeLine({
2647
+ type: "meta",
2648
+ owned: meta.owned,
2649
+ commands: meta.commands,
2650
+ ...meta.repository === void 0 ? {} : { repository: meta.repository },
2651
+ reviewComments: meta.reviewComments
2652
+ });
2653
+ })().catch(() => void 0);
2654
+ }, META_REFRESH_MS);
2655
+ timer.unref?.();
2656
+ await new Promise((resolve) => {
2657
+ res.on("close", () => {
2658
+ closed = true;
2659
+ clearInterval(timer);
2660
+ unsubscribe();
2661
+ info(`dsh-claude: projection stream closed for ${sessionId.slice(0, 64)} after ${textDeltas} text deltas`);
2662
+ resolve();
2663
+ });
2664
+ });
2665
+ };
2314
2666
  ctx.effect(() => ctx.webServer.register({
2315
2667
  kind: "prefix",
2316
2668
  path: CLAUDE_PROJECTION_PATH,
2317
2669
  handler: async (req, res) => {
2318
2670
  if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
2319
2671
  if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
2320
- const sessionId = sessionIdFromUrl(req.url);
2321
- if (sessionId === void 0) return json(res, 400, { error: "invalid session id" });
2672
+ const target = targetFromUrl(req.url);
2673
+ if (target === void 0) return json(res, 400, { error: "invalid session id" });
2322
2674
  try {
2323
- const projection = await sidecar.read(sessionId);
2324
- const owned = ownsSession(sessionId);
2325
- const repository = owned ? await repositoryForSession(sessionId) : void 0;
2326
- return json(res, 200, {
2327
- schemaVersion: projection.schemaVersion,
2328
- revision: projection.revision,
2329
- owned,
2330
- commands: commandsForSession(sessionId),
2331
- activities: projection.activities,
2332
- ...projection.contextUsage === void 0 ? {} : { contextUsage: projection.contextUsage },
2333
- ...projection.tasks === void 0 ? {} : { tasks: projection.tasks },
2334
- ...repository === void 0 ? {} : { repository }
2335
- });
2675
+ if (target.stream) return await streamProjection(res, target.sessionId);
2676
+ info(`dsh-claude: projection poll for ${target.sessionId.slice(0, 64)}`);
2677
+ return json(res, 200, envelope(await sidecar.read(target.sessionId), await assembleMeta(target.sessionId)));
2336
2678
  } catch {
2337
- return json(res, 500, { error: "projection unavailable" });
2679
+ if (!res.headersSent) return json(res, 500, { error: "projection unavailable" });
2680
+ res.end();
2338
2681
  }
2339
2682
  }
2340
2683
  }), "dsh-claude: sidecar projection route");
2341
2684
  }
2342
2685
  //#endregion
2343
2686
  //#region src/repository-status.ts
2344
- const MAX_OUTPUT_BYTES$1 = 65536;
2687
+ const MAX_OUTPUT_BYTES$2 = 65536;
2345
2688
  const MAX_DIFF_BYTES = 262144;
2346
- const GIT_TIMEOUT_MS$1 = 5e3;
2689
+ const MAX_UNTRACKED_DIFFS = 50;
2690
+ const GIT_TIMEOUT_MS$2 = 5e3;
2347
2691
  const GH_TIMEOUT_MS = 8e3;
2348
2692
  const CACHE_TTL_MS = 5e3;
2349
2693
  const MAX_TEXT_CHARS = 1024;
2350
2694
  function bounded(value) {
2351
2695
  return value.trim().slice(0, MAX_TEXT_CHARS);
2352
2696
  }
2353
- async function collect$1(handle) {
2697
+ async function collect$2(handle) {
2354
2698
  const outcome = await handle.done;
2355
2699
  const stdout = handle.collected.stdout?.readFrom(0);
2356
2700
  return {
@@ -2359,15 +2703,15 @@ async function collect$1(handle) {
2359
2703
  lossy: stdout?.lossy === true
2360
2704
  };
2361
2705
  }
2362
- async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$1) {
2706
+ async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$2) {
2363
2707
  const signal = AbortSignal.timeout(timeoutMs);
2364
- return collect$1(runtime.spawn({
2708
+ return collect$2(runtime.spawn({
2365
2709
  argv: [executable, ...args],
2366
2710
  cwd,
2367
2711
  stdio: {
2368
2712
  stdin: "ignore",
2369
2713
  stdout: { maxBytes },
2370
- stderr: { maxBytes: MAX_OUTPUT_BYTES$1 }
2714
+ stderr: { maxBytes: MAX_OUTPUT_BYTES$2 }
2371
2715
  },
2372
2716
  graceMs: 1e3,
2373
2717
  signal,
@@ -2381,6 +2725,9 @@ function parseGitStatus(output) {
2381
2725
  let branch;
2382
2726
  let detached = false;
2383
2727
  let dirty = false;
2728
+ let upstream = false;
2729
+ let ahead;
2730
+ let behind;
2384
2731
  for (const line of output.split(/\r?\n/u)) {
2385
2732
  if (line.startsWith("# branch.head ")) {
2386
2733
  const head = bounded(line.slice(14));
@@ -2388,12 +2735,27 @@ function parseGitStatus(output) {
2388
2735
  else if (head.length > 0 && head !== "(unknown)") branch = head;
2389
2736
  continue;
2390
2737
  }
2738
+ if (line.startsWith("# branch.upstream ")) {
2739
+ upstream = true;
2740
+ continue;
2741
+ }
2742
+ if (line.startsWith("# branch.ab ")) {
2743
+ const counts = /^# branch\.ab \+(\d+) -(\d+)$/u.exec(line);
2744
+ if (counts !== null) {
2745
+ ahead = Number(counts[1]);
2746
+ behind = Number(counts[2]);
2747
+ }
2748
+ continue;
2749
+ }
2391
2750
  if (line.length > 0 && !line.startsWith("# ")) dirty = true;
2392
2751
  }
2393
2752
  return {
2394
2753
  ...branch === void 0 ? {} : { branch },
2395
2754
  detached,
2396
- dirty
2755
+ dirty,
2756
+ upstream,
2757
+ ...ahead === void 0 ? {} : { ahead },
2758
+ ...behind === void 0 ? {} : { behind }
2397
2759
  };
2398
2760
  }
2399
2761
  function parseDiffNumstat(value) {
@@ -2419,14 +2781,14 @@ function parseGitHubRemote(value) {
2419
2781
  if (match?.[1] === void 0 || match[2] === void 0) return void 0;
2420
2782
  return `${match[1]}/${match[2]}`;
2421
2783
  }
2422
- function record$1(value) {
2784
+ function record$3(value) {
2423
2785
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2424
2786
  }
2425
2787
  function aggregateChecks(value) {
2426
2788
  if (!Array.isArray(value) || value.length === 0) return "none";
2427
2789
  let pending = false;
2428
2790
  for (const item of value) {
2429
- const check = record$1(item);
2791
+ const check = record$3(item);
2430
2792
  if (check === void 0) continue;
2431
2793
  const conclusion = typeof check.conclusion === "string" ? check.conclusion.toUpperCase() : void 0;
2432
2794
  const status = typeof check.status === "string" ? check.status.toUpperCase() : void 0;
@@ -2450,7 +2812,7 @@ function reviewState(value) {
2450
2812
  return "none";
2451
2813
  }
2452
2814
  function parsePullRequest(value) {
2453
- const input = record$1(value);
2815
+ const input = record$3(value);
2454
2816
  if (input === void 0 || !Number.isSafeInteger(input.number) || Number(input.number) <= 0 || typeof input.title !== "string" || typeof input.url !== "string") return void 0;
2455
2817
  let url;
2456
2818
  try {
@@ -2471,8 +2833,9 @@ function parsePullRequest(value) {
2471
2833
  review: reviewState(input.reviewDecision),
2472
2834
  checks: aggregateChecks(input.statusCheckRollup),
2473
2835
  ...typeof input.mergeStateStatus === "string" ? { mergeState: bounded(input.mergeStateStatus) } : {},
2474
- ...typeof record$1(input.author)?.login === "string" ? { author: bounded(String(record$1(input.author)?.login)) } : {},
2836
+ ...typeof record$3(input.author)?.login === "string" ? { author: bounded(String(record$3(input.author)?.login)) } : {},
2475
2837
  ...typeof input.createdAt === "string" && Number.isFinite(Date.parse(input.createdAt)) ? { createdAt: new Date(input.createdAt).toISOString() } : {},
2838
+ ...typeof input.mergedAt === "string" && Number.isFinite(Date.parse(input.mergedAt)) ? { mergedAt: new Date(input.mergedAt).toISOString() } : {},
2476
2839
  ...typeof input.baseRefName === "string" && bounded(input.baseRefName).length > 0 ? { baseBranch: bounded(input.baseRefName) } : {}
2477
2840
  };
2478
2841
  }
@@ -2498,6 +2861,10 @@ var RepositoryStatusService = class {
2498
2861
  value.catch(() => this.#cache.delete(cwd));
2499
2862
  return value;
2500
2863
  }
2864
+ invalidate(cwd) {
2865
+ this.#cache.delete(cwd);
2866
+ this.#lastReady.delete(cwd);
2867
+ }
2501
2868
  dispose() {
2502
2869
  this.#cache.clear();
2503
2870
  this.#lastReady.clear();
@@ -2539,7 +2906,7 @@ var RepositoryStatusService = class {
2539
2906
  "--show-toplevel",
2540
2907
  "--absolute-git-dir",
2541
2908
  "--git-common-dir"
2542
- ], cwd, GIT_TIMEOUT_MS$1);
2909
+ ], cwd, GIT_TIMEOUT_MS$2);
2543
2910
  if (paths.exitCode !== 0) return {
2544
2911
  status: "not-repository",
2545
2912
  cwd: safeCwd
@@ -2556,8 +2923,8 @@ var RepositoryStatusService = class {
2556
2923
  "status",
2557
2924
  "--porcelain=v2",
2558
2925
  "--branch",
2559
- "--untracked-files=no"
2560
- ], cwd, GIT_TIMEOUT_MS$1);
2926
+ "--untracked-files=normal"
2927
+ ], cwd, GIT_TIMEOUT_MS$2);
2561
2928
  if (statusResult.exitCode !== 0) return {
2562
2929
  status: "unavailable",
2563
2930
  cwd: safeCwd
@@ -2567,7 +2934,7 @@ var RepositoryStatusService = class {
2567
2934
  "remote",
2568
2935
  "get-url",
2569
2936
  "origin"
2570
- ], cwd, GIT_TIMEOUT_MS$1);
2937
+ ], cwd, GIT_TIMEOUT_MS$2);
2571
2938
  const remote = remoteResult.exitCode === 0 ? parseGitHubRemote(remoteResult.stdout) : void 0;
2572
2939
  const pullRequest = status.branch === void 0 || remote === void 0 ? void 0 : await this.#pullRequest(cwd, remote, status.branch);
2573
2940
  const diffBase = pullRequest?.baseBranch === void 0 ? "HEAD" : await this.#mergeBase(cwd, git, pullRequest.baseBranch) ?? "HEAD";
@@ -2600,7 +2967,7 @@ var RepositoryStatusService = class {
2600
2967
  "merge-base",
2601
2968
  "HEAD",
2602
2969
  `refs/remotes/origin/${baseBranch}`
2603
- ], cwd, GIT_TIMEOUT_MS$1);
2970
+ ], cwd, GIT_TIMEOUT_MS$2);
2604
2971
  if (result.exitCode !== 0 || result.lossy) return void 0;
2605
2972
  const oid = bounded(result.stdout);
2606
2973
  return /^[0-9a-f]{40}$/iu.test(oid) ? oid : void 0;
@@ -2616,7 +2983,7 @@ var RepositoryStatusService = class {
2616
2983
  "--numstat",
2617
2984
  base,
2618
2985
  "--"
2619
- ], cwd, GIT_TIMEOUT_MS$1);
2986
+ ], cwd, GIT_TIMEOUT_MS$2);
2620
2987
  if (numstat.exitCode !== 0 || numstat.lossy) return void 0;
2621
2988
  const summary = parseDiffNumstat(numstat.stdout);
2622
2989
  const patch = await run(this.#runtime, git, [
@@ -2626,20 +2993,70 @@ var RepositoryStatusService = class {
2626
2993
  "--unified=3",
2627
2994
  base,
2628
2995
  "--"
2629
- ], cwd, GIT_TIMEOUT_MS$1, MAX_DIFF_BYTES);
2996
+ ], cwd, GIT_TIMEOUT_MS$2, MAX_DIFF_BYTES);
2630
2997
  if (patch.exitCode !== 0) return {
2631
2998
  ...summary,
2632
2999
  truncated: true
2633
3000
  };
3001
+ const untracked = await this.#untrackedDiff(cwd, git);
3002
+ const combinedPatch = `${patch.stdout}${untracked.patch}`;
2634
3003
  return {
2635
- ...summary,
2636
- ...patch.lossy ? {} : { patch: patch.stdout },
2637
- truncated: patch.lossy
3004
+ additions: summary.additions + untracked.additions,
3005
+ deletions: summary.deletions,
3006
+ files: summary.files + untracked.files,
3007
+ ...patch.lossy ? {} : { patch: combinedPatch.slice(0, MAX_DIFF_BYTES) },
3008
+ truncated: patch.lossy || untracked.truncated || combinedPatch.length > MAX_DIFF_BYTES
2638
3009
  };
2639
3010
  } catch {
2640
3011
  return;
2641
3012
  }
2642
3013
  }
3014
+ async #untrackedDiff(cwd, git) {
3015
+ const listed = await run(this.#runtime, git, [
3016
+ "ls-files",
3017
+ "--others",
3018
+ "--exclude-standard",
3019
+ "-z"
3020
+ ], cwd, GIT_TIMEOUT_MS$2);
3021
+ if (listed.exitCode !== 0 || listed.lossy) return {
3022
+ additions: 0,
3023
+ files: 0,
3024
+ patch: "",
3025
+ truncated: listed.lossy
3026
+ };
3027
+ const paths = listed.stdout.split("\0").filter((path) => path.length > 0);
3028
+ let additions = 0;
3029
+ let patch = "";
3030
+ let truncated = paths.length > MAX_UNTRACKED_DIFFS;
3031
+ for (const path of paths.slice(0, MAX_UNTRACKED_DIFFS)) {
3032
+ const result = await run(this.#runtime, git, [
3033
+ "diff",
3034
+ "--no-ext-diff",
3035
+ "--no-color",
3036
+ "--unified=3",
3037
+ "--numstat",
3038
+ "--patch",
3039
+ "--no-index",
3040
+ "--",
3041
+ "/dev/null",
3042
+ path
3043
+ ], cwd, GIT_TIMEOUT_MS$2, MAX_DIFF_BYTES);
3044
+ if (result.exitCode !== 0 && result.exitCode !== 1) {
3045
+ truncated = true;
3046
+ continue;
3047
+ }
3048
+ const start = result.stdout.indexOf("diff --git ");
3049
+ additions += parseDiffNumstat(start >= 0 ? result.stdout.slice(0, start) : result.stdout).additions;
3050
+ if (result.lossy) truncated = true;
3051
+ else if (start >= 0) patch += result.stdout.slice(start);
3052
+ }
3053
+ return {
3054
+ additions,
3055
+ files: paths.length,
3056
+ patch,
3057
+ truncated
3058
+ };
3059
+ }
2643
3060
  async #pullRequest(cwd, repository, branch) {
2644
3061
  const gh = await this.#gh();
2645
3062
  if (gh === void 0) return void 0;
@@ -2662,8 +3079,8 @@ var RepositoryStatusService = class {
2662
3079
  };
2663
3080
  //#endregion
2664
3081
  //#region src/repository-setup.ts
2665
- const MAX_OUTPUT_BYTES = 131072;
2666
- const GIT_TIMEOUT_MS = 1e4;
3082
+ const MAX_OUTPUT_BYTES$1 = 131072;
3083
+ const GIT_TIMEOUT_MS$1 = 1e4;
2667
3084
  const GIT_FETCH_TIMEOUT_MS = 6e4;
2668
3085
  const MAX_PATH_CHARS = 4096;
2669
3086
  const MAX_BRANCH_CHARS = 512;
@@ -2676,7 +3093,7 @@ var RepositorySetupError = class extends Error {
2676
3093
  this.code = code;
2677
3094
  }
2678
3095
  };
2679
- async function collect(handle) {
3096
+ async function collect$1(handle) {
2680
3097
  const outcome = await handle.done;
2681
3098
  const stdout = handle.collected.stdout?.readFrom(0);
2682
3099
  const stderr = handle.collected.stderr?.readFrom(0);
@@ -2969,14 +3386,14 @@ var RepositorySetupService = class {
2969
3386
  this.#gitPath ??= this.#runtime.resolveExecutable("git");
2970
3387
  return this.#gitPath;
2971
3388
  }
2972
- async #run(executable, args, cwd, timeoutMs = GIT_TIMEOUT_MS) {
2973
- return collect(this.#runtime.spawn({
3389
+ async #run(executable, args, cwd, timeoutMs = GIT_TIMEOUT_MS$1) {
3390
+ return collect$1(this.#runtime.spawn({
2974
3391
  argv: [executable, ...args],
2975
3392
  cwd,
2976
3393
  stdio: {
2977
3394
  stdin: "ignore",
2978
- stdout: { maxBytes: MAX_OUTPUT_BYTES },
2979
- stderr: { maxBytes: MAX_OUTPUT_BYTES }
3395
+ stdout: { maxBytes: MAX_OUTPUT_BYTES$1 },
3396
+ stderr: { maxBytes: MAX_OUTPUT_BYTES$1 }
2980
3397
  },
2981
3398
  graceMs: 1e3,
2982
3399
  signal: AbortSignal.timeout(timeoutMs),
@@ -3016,30 +3433,400 @@ var RepositorySetupService = class {
3016
3433
  }
3017
3434
  };
3018
3435
  //#endregion
3436
+ //#region src/repository-actions.ts
3437
+ const MAX_OUTPUT_BYTES = 262144;
3438
+ const MAX_PATCH_CHARS = 65536;
3439
+ const MAX_MESSAGE_CHARS = 512;
3440
+ const MAX_PR_TEXT_CHARS = 8192;
3441
+ const MAX_UNPUSHED_COMMITS = 20;
3442
+ const GIT_TIMEOUT_MS = 15e3;
3443
+ const REMOTE_TIMEOUT_MS = 6e4;
3444
+ const GENERATE_TIMEOUT_MS = 6e4;
3445
+ var RepositoryActionError = class extends Error {
3446
+ code;
3447
+ commit;
3448
+ constructor(code, message, commit) {
3449
+ super(message);
3450
+ this.name = "RepositoryActionError";
3451
+ this.code = code;
3452
+ if (commit !== void 0) this.commit = commit;
3453
+ }
3454
+ };
3455
+ async function collect(handle) {
3456
+ const outcome = await handle.done;
3457
+ const stdout = handle.collected.stdout?.readFrom(0);
3458
+ const stderr = handle.collected.stderr?.readFrom(0);
3459
+ return {
3460
+ exitCode: outcome.exitCode,
3461
+ stdout: stdout?.text ?? "",
3462
+ stderr: stderr?.text ?? "",
3463
+ lossy: stdout?.lossy === true || stderr?.lossy === true
3464
+ };
3465
+ }
3466
+ function safeText(value, maximum, label) {
3467
+ const text = value.trim();
3468
+ if (text.length === 0 || text.length > maximum || /[\0\r]/u.test(text)) throw new RepositoryActionError("invalid-request", `${label} is invalid.`);
3469
+ return text;
3470
+ }
3471
+ function isProtectedWarpPath(path) {
3472
+ return basename(path.replaceAll("\\", "/")).toLocaleLowerCase("en-US") === "warp.md";
3473
+ }
3474
+ function parseRepositoryActionStatus(output) {
3475
+ const files = /* @__PURE__ */ new Map();
3476
+ const records = output.includes("\0") ? output.split("\0") : output.split(/\r?\n/u);
3477
+ for (let position = 0; position < records.length; position += 1) {
3478
+ const line = records[position] ?? "";
3479
+ if (line.length < 4) continue;
3480
+ const index = line[0] ?? " ";
3481
+ const worktree = line[1] ?? " ";
3482
+ let path = line.slice(3);
3483
+ const rename = path.lastIndexOf(" -> ");
3484
+ if (rename >= 0) path = path.slice(rename + 4);
3485
+ if (output.includes("\0") && (index === "R" || index === "C" || worktree === "R" || worktree === "C")) position += 1;
3486
+ if (path.length === 0 || path.includes("\0") || isProtectedWarpPath(path)) continue;
3487
+ files.set(path, {
3488
+ path,
3489
+ staged: index !== " " && index !== "?",
3490
+ unstaged: worktree !== " " && worktree !== "?",
3491
+ untracked: index === "?" && worktree === "?"
3492
+ });
3493
+ }
3494
+ return [...files.values()].sort((left, right) => left.path.localeCompare(right.path));
3495
+ }
3496
+ function fallbackCommitMessage(files) {
3497
+ if (files.length === 1) return `Update ${files[0]?.path ?? "repository files"}`;
3498
+ return `Update ${files.length} repository files`;
3499
+ }
3500
+ function normalizedGeneratedMessage(value, fallback) {
3501
+ const first = value.split(/\r?\n/u).map((line) => line.trim()).find(Boolean);
3502
+ if (first === void 0) return fallback;
3503
+ const message = first.replace(/^['"`]+|['"`]+$/gu, "").replace(/[\0\r\n]/gu, " ").trim();
3504
+ return message.length === 0 || message.length > 72 ? fallback : message;
3505
+ }
3506
+ function validPrUrl(value) {
3507
+ try {
3508
+ const url = new URL(value.trim());
3509
+ return url.protocol === "https:" && url.hostname === "github.com" ? url.href : void 0;
3510
+ } catch {
3511
+ return;
3512
+ }
3513
+ }
3514
+ function validPullRequestBody(value) {
3515
+ const body = value.trim();
3516
+ const match = /^Summary:\s+([^\r\n]+)\r?\n\r?\nChanges:\s*\r?\n([\s\S]+)$/u.exec(body);
3517
+ const summary = match?.[1]?.trim();
3518
+ const changes = match?.[2]?.trim();
3519
+ if (summary === void 0 || summary.length === 0 || changes === void 0 || changes.length === 0) return false;
3520
+ return !/^#{1,6}\s|^[A-Za-z][A-Za-z ]+:\s*$/mu.test(changes);
3521
+ }
3522
+ var RepositoryActionService = class {
3523
+ #runtime;
3524
+ #claudeExecutable;
3525
+ #invalidate;
3526
+ #gitExecutable;
3527
+ #ghExecutable;
3528
+ #pending = Promise.resolve();
3529
+ constructor(runtime, claudeExecutable, invalidate = () => {}) {
3530
+ this.#runtime = runtime;
3531
+ this.#claudeExecutable = claudeExecutable;
3532
+ this.#invalidate = invalidate;
3533
+ }
3534
+ preview(cwd) {
3535
+ return this.#preview(cwd);
3536
+ }
3537
+ async generateMessage(cwd, fingerprint) {
3538
+ const preview = await this.#preview(cwd);
3539
+ if (preview.fingerprint !== fingerprint) throw new RepositoryActionError("repository-changed", "Repository changes have changed. Refresh the commit panel.");
3540
+ const fallback = fallbackCommitMessage(preview.files);
3541
+ const prompt = [
3542
+ "Write one concise English git commit subject (imperative mood, maximum 72 characters).",
3543
+ "Return only the subject without quotes, markdown, body, or explanation.",
3544
+ `Files: ${preview.files.map((file) => file.path).join(", ")}`,
3545
+ `Diff:\n${preview.patch.slice(0, 24576)}`
3546
+ ].join("\n");
3547
+ try {
3548
+ const result = await this.#run(this.#claudeExecutable, [
3549
+ "-p",
3550
+ "--tools",
3551
+ "",
3552
+ "--output-format",
3553
+ "text",
3554
+ prompt
3555
+ ], preview.root, GENERATE_TIMEOUT_MS);
3556
+ return result.exitCode === 0 && !result.lossy ? normalizedGeneratedMessage(result.stdout, fallback) : fallback;
3557
+ } catch {
3558
+ return fallback;
3559
+ }
3560
+ }
3561
+ execute(cwd, request) {
3562
+ const operation = this.#pending.then(() => this.#execute(cwd, request));
3563
+ this.#pending = operation.then(() => void 0, () => void 0);
3564
+ return operation;
3565
+ }
3566
+ async #execute(cwd, request) {
3567
+ const before = await this.#preview(cwd);
3568
+ if (before.fingerprint !== request.fingerprint) throw new RepositoryActionError("repository-changed", "Repository changes have changed. Refresh the commit panel.");
3569
+ if (request.action === "push") {
3570
+ const git = await this.#git();
3571
+ try {
3572
+ await this.#push(git, before.root, before.branch);
3573
+ } catch (error) {
3574
+ throw new RepositoryActionError("push-failed", error instanceof Error ? error.message : "Git push failed.");
3575
+ }
3576
+ this.#invalidate(before.root);
3577
+ return {
3578
+ commit: before.head,
3579
+ pushed: true
3580
+ };
3581
+ }
3582
+ const message = safeText(request.message, MAX_MESSAGE_CHARS, "Commit message");
3583
+ if (before.files.length === 0 && request.action !== "create-pr") throw new RepositoryActionError("nothing-to-commit", "There are no changes to commit.");
3584
+ const git = await this.#git();
3585
+ let oid = before.head;
3586
+ if (before.files.length > 0) {
3587
+ await this.#rejectStagedWarp(git, before.root);
3588
+ if (request.includeUnstaged) {
3589
+ const paths = before.files.filter((file) => file.unstaged || file.untracked).map((file) => file.path);
3590
+ if (paths.length > 0) await this.#mustRun(git, [
3591
+ "add",
3592
+ "--",
3593
+ ...paths
3594
+ ], before.root, GIT_TIMEOUT_MS, "stage-failed", "Changes could not be staged.");
3595
+ }
3596
+ await this.#rejectStagedWarp(git, before.root);
3597
+ const staged = await this.#run(git, [
3598
+ "diff",
3599
+ "--cached",
3600
+ "--quiet",
3601
+ "--exit-code",
3602
+ "--"
3603
+ ], before.root, GIT_TIMEOUT_MS);
3604
+ if (staged.exitCode === 0) throw new RepositoryActionError("nothing-to-commit", "There are no staged changes to commit.");
3605
+ if (staged.exitCode !== 1) throw new RepositoryActionError("repository-unavailable", "The staged changes could not be verified.");
3606
+ await this.#mustRun(git, [
3607
+ "commit",
3608
+ "-m",
3609
+ message,
3610
+ "--"
3611
+ ], before.root, GIT_TIMEOUT_MS, "commit-failed", "Git commit failed.");
3612
+ oid = (await this.#mustRun(git, ["rev-parse", "HEAD"], before.root, GIT_TIMEOUT_MS, "commit-failed", "The new commit could not be verified.")).stdout.trim();
3613
+ this.#invalidate(before.root);
3614
+ if (request.action === "commit") return {
3615
+ commit: oid,
3616
+ pushed: false
3617
+ };
3618
+ }
3619
+ try {
3620
+ await this.#push(git, before.root, before.branch);
3621
+ } catch (error) {
3622
+ throw new RepositoryActionError("push-failed", error instanceof Error ? error.message : "Git push failed.", oid);
3623
+ }
3624
+ this.#invalidate(before.root);
3625
+ if (request.action === "commit-push") return {
3626
+ commit: oid,
3627
+ pushed: true
3628
+ };
3629
+ const title = safeText(request.prTitle ?? message, 256, "Pull request title");
3630
+ const body = safeText(request.prBody ?? "", MAX_PR_TEXT_CHARS, "Pull request description");
3631
+ if (!validPullRequestBody(body)) throw new RepositoryActionError("invalid-pr-description", "Pull request description must contain only Summary and Changes sections.", oid);
3632
+ let gh;
3633
+ try {
3634
+ gh = await this.#gh();
3635
+ } catch (error) {
3636
+ throw new RepositoryActionError("gh-unavailable", error instanceof Error ? error.message : "GitHub CLI is unavailable.", oid);
3637
+ }
3638
+ const args = [
3639
+ "pr",
3640
+ "create",
3641
+ "--title",
3642
+ title,
3643
+ "--body",
3644
+ body
3645
+ ];
3646
+ if (request.draft !== false) args.push("--draft");
3647
+ if (request.baseBranch !== void 0) args.push("--base", safeText(request.baseBranch, 512, "Base branch"));
3648
+ const created = await this.#run(gh, args, before.root, REMOTE_TIMEOUT_MS);
3649
+ const url = created.exitCode === 0 && !created.lossy ? validPrUrl(created.stdout) : void 0;
3650
+ if (url === void 0) throw new RepositoryActionError("pr-failed", "The pull request could not be created.", oid);
3651
+ return {
3652
+ commit: oid,
3653
+ pushed: true,
3654
+ pullRequestUrl: url
3655
+ };
3656
+ }
3657
+ async #preview(cwd) {
3658
+ const git = await this.#git();
3659
+ const root = (await this.#mustRun(git, [
3660
+ "rev-parse",
3661
+ "--path-format=absolute",
3662
+ "--show-toplevel"
3663
+ ], cwd, GIT_TIMEOUT_MS, "not-repository", "The session directory is not a Git repository.")).stdout.trim();
3664
+ const [branchResult, headResult, statusResult, stagedPatch, unstagedPatch] = await Promise.all([
3665
+ this.#run(git, [
3666
+ "symbolic-ref",
3667
+ "--quiet",
3668
+ "--short",
3669
+ "HEAD"
3670
+ ], root, GIT_TIMEOUT_MS),
3671
+ this.#run(git, ["rev-parse", "HEAD"], root, GIT_TIMEOUT_MS),
3672
+ this.#run(git, [
3673
+ "status",
3674
+ "--porcelain=v1",
3675
+ "-z",
3676
+ "--untracked-files=all"
3677
+ ], root, GIT_TIMEOUT_MS),
3678
+ this.#run(git, [
3679
+ "diff",
3680
+ "--cached",
3681
+ "--no-ext-diff",
3682
+ "--no-color",
3683
+ "--unified=3",
3684
+ "--",
3685
+ ":(exclude)WARP.md",
3686
+ ":(exclude)**/WARP.md"
3687
+ ], root, GIT_TIMEOUT_MS, MAX_OUTPUT_BYTES),
3688
+ this.#run(git, [
3689
+ "diff",
3690
+ "--no-ext-diff",
3691
+ "--no-color",
3692
+ "--unified=3",
3693
+ "--",
3694
+ ":(exclude)WARP.md",
3695
+ ":(exclude)**/WARP.md"
3696
+ ], root, GIT_TIMEOUT_MS, MAX_OUTPUT_BYTES)
3697
+ ]);
3698
+ if (branchResult.exitCode !== 0) throw new RepositoryActionError("detached-head", "A detached HEAD cannot be committed from this panel.");
3699
+ if (headResult.exitCode !== 0 || statusResult.exitCode !== 0 || statusResult.lossy) throw new RepositoryActionError("repository-unavailable", "Repository state is unavailable.");
3700
+ const files = parseRepositoryActionStatus(statusResult.stdout);
3701
+ const patch = `${stagedPatch.stdout}${stagedPatch.stdout.length > 0 && unstagedPatch.stdout.length > 0 ? "\n" : ""}${unstagedPatch.stdout}`.slice(0, MAX_PATCH_CHARS);
3702
+ const branch = branchResult.stdout.trim();
3703
+ const head = headResult.stdout.trim();
3704
+ const fingerprint = createHash("sha256").update([
3705
+ head,
3706
+ branch,
3707
+ statusResult.stdout,
3708
+ stagedPatch.stdout,
3709
+ unstagedPatch.stdout
3710
+ ].join("\0")).digest("hex");
3711
+ const upstreamResult = await this.#run(git, [
3712
+ "rev-parse",
3713
+ "--abbrev-ref",
3714
+ "--symbolic-full-name",
3715
+ "@{upstream}"
3716
+ ], root, GIT_TIMEOUT_MS);
3717
+ const upstream = upstreamResult.exitCode === 0 && !upstreamResult.lossy ? upstreamResult.stdout.trim() : void 0;
3718
+ const logResult = await this.#run(git, [
3719
+ "log",
3720
+ "--format=%H%x09%s",
3721
+ `-n`,
3722
+ String(21),
3723
+ upstream === void 0 ? "HEAD" : "@{upstream}..HEAD",
3724
+ "--"
3725
+ ], root, GIT_TIMEOUT_MS);
3726
+ const commitLines = logResult.exitCode === 0 && !logResult.lossy ? logResult.stdout.split(/\r?\n/u).filter((line) => line.includes(" ")) : [];
3727
+ const unpushedCommits = commitLines.slice(0, MAX_UNPUSHED_COMMITS).flatMap((line) => {
3728
+ const tab = line.indexOf(" ");
3729
+ const hash = line.slice(0, tab);
3730
+ return /^[0-9a-f]{40}$/iu.test(hash) ? [{
3731
+ hash,
3732
+ subject: line.slice(tab + 1).slice(0, 140)
3733
+ }] : [];
3734
+ });
3735
+ return {
3736
+ root,
3737
+ branch,
3738
+ head,
3739
+ fingerprint,
3740
+ files,
3741
+ patch,
3742
+ truncated: stagedPatch.lossy || unstagedPatch.lossy || stagedPatch.stdout.length + unstagedPatch.stdout.length > MAX_PATCH_CHARS,
3743
+ hasStaged: files.some((file) => file.staged),
3744
+ hasUnstaged: files.some((file) => file.unstaged),
3745
+ hasUntracked: files.some((file) => file.untracked),
3746
+ ...upstream === void 0 ? {} : { upstream },
3747
+ unpushedCommits,
3748
+ unpushedTruncated: commitLines.length > MAX_UNPUSHED_COMMITS
3749
+ };
3750
+ }
3751
+ async #rejectStagedWarp(git, cwd) {
3752
+ const staged = await this.#run(git, [
3753
+ "diff",
3754
+ "--cached",
3755
+ "--name-only",
3756
+ "--"
3757
+ ], cwd, GIT_TIMEOUT_MS);
3758
+ if (staged.exitCode !== 0 || staged.lossy) throw new RepositoryActionError("repository-unavailable", "Staged files could not be verified.");
3759
+ if (staged.stdout.split(/\r?\n/u).some((path) => path.length > 0 && isProtectedWarpPath(path))) throw new RepositoryActionError("protected-warp-file", "WARP.md files cannot be committed. Unstage them before continuing.");
3760
+ }
3761
+ async #push(git, cwd, branch) {
3762
+ const args = (await this.#run(git, [
3763
+ "rev-parse",
3764
+ "--abbrev-ref",
3765
+ "--symbolic-full-name",
3766
+ "@{upstream}"
3767
+ ], cwd, GIT_TIMEOUT_MS)).exitCode === 0 ? ["push"] : [
3768
+ "push",
3769
+ "--set-upstream",
3770
+ "origin",
3771
+ branch
3772
+ ];
3773
+ await this.#mustRun(git, args, cwd, REMOTE_TIMEOUT_MS, "push-failed", "Git push failed.");
3774
+ }
3775
+ #git() {
3776
+ this.#gitExecutable ??= this.#runtime.resolveExecutable("git");
3777
+ return this.#gitExecutable;
3778
+ }
3779
+ #gh() {
3780
+ this.#ghExecutable ??= this.#runtime.resolveExecutable("gh").catch(() => {
3781
+ throw new RepositoryActionError("gh-unavailable", "GitHub CLI is unavailable.");
3782
+ });
3783
+ return this.#ghExecutable;
3784
+ }
3785
+ async #mustRun(executable, args, cwd, timeoutMs, code, message) {
3786
+ const result = await this.#run(executable, args, cwd, timeoutMs);
3787
+ if (result.exitCode !== 0 || result.lossy) throw new RepositoryActionError(code, message);
3788
+ return result;
3789
+ }
3790
+ #run(executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES) {
3791
+ return collect(this.#runtime.spawn({
3792
+ argv: [executable, ...args],
3793
+ cwd,
3794
+ stdio: {
3795
+ stdin: "ignore",
3796
+ stdout: { maxBytes },
3797
+ stderr: { maxBytes: MAX_OUTPUT_BYTES }
3798
+ },
3799
+ graceMs: 1e3,
3800
+ signal: AbortSignal.timeout(timeoutMs),
3801
+ env: {}
3802
+ }));
3803
+ }
3804
+ };
3805
+ //#endregion
3019
3806
  //#region src/repository-setup-routes.ts
3020
- const MAX_BODY_BYTES = 16384;
3021
- function record(value) {
3807
+ const MAX_BODY_BYTES$2 = 16384;
3808
+ function record$2(value) {
3022
3809
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3023
3810
  }
3024
- async function readJson(req) {
3811
+ async function readJson$2(req) {
3025
3812
  const chunks = [];
3026
3813
  let size = 0;
3027
3814
  for await (const chunk of req) {
3028
3815
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
3029
3816
  size += buffer.length;
3030
- if (size > MAX_BODY_BYTES) throw new RepositorySetupError("body-too-large", "The request body is too large.");
3817
+ if (size > MAX_BODY_BYTES$2) throw new RepositorySetupError("body-too-large", "The request body is too large.");
3031
3818
  chunks.push(buffer);
3032
3819
  }
3033
- const value = record(JSON.parse(Buffer.concat(chunks).toString("utf8")));
3820
+ const value = record$2(JSON.parse(Buffer.concat(chunks).toString("utf8")));
3034
3821
  if (value === void 0) throw new RepositorySetupError("invalid-request", "The request body is invalid.");
3035
3822
  return value;
3036
3823
  }
3037
- function string(input, key) {
3824
+ function string$1(input, key) {
3038
3825
  const value = input[key];
3039
3826
  if (typeof value !== "string") throw new RepositorySetupError("invalid-request", `The ${key} field is required.`);
3040
3827
  return value;
3041
3828
  }
3042
- function optionalString(input, key) {
3829
+ function optionalString$1(input, key) {
3043
3830
  const value = input[key];
3044
3831
  if (value === void 0) return void 0;
3045
3832
  if (typeof value !== "string") throw new RepositorySetupError("invalid-request", `The ${key} field must be a string.`);
@@ -3062,7 +3849,7 @@ async function streamSetup(res, service, input) {
3062
3849
  try {
3063
3850
  ndjson(res, {
3064
3851
  type: "complete",
3065
- result: await service.setup(string(input, "cwd"), string(input, "branch"), input.worktree, optionalString(input, "branchName"), (stage) => {
3852
+ result: await service.setup(string$1(input, "cwd"), string$1(input, "branch"), input.worktree, optionalString$1(input, "branchName"), (stage) => {
3066
3853
  if (!ended) ndjson(res, {
3067
3854
  type: "progress",
3068
3855
  stage
@@ -3098,15 +3885,15 @@ function registerRepositorySetupRoute(ctx, service) {
3098
3885
  }
3099
3886
  if (pathname === "/plugins/dsh-claude/repository/setup") {
3100
3887
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
3101
- const input = await readJson(req);
3888
+ const input = await readJson$2(req);
3102
3889
  if (typeof input.worktree !== "boolean") throw new RepositorySetupError("invalid-request", "The worktree field is required.");
3103
3890
  await streamSetup(res, service, input);
3104
3891
  return;
3105
3892
  }
3106
3893
  if (pathname === `/plugins/dsh-claude/repository/setup/bind`) {
3107
3894
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
3108
- const input = await readJson(req);
3109
- await service.bindLease(string(input, "leaseId"), string(input, "sessionId"));
3895
+ const input = await readJson$2(req);
3896
+ await service.bindLease(string$1(input, "leaseId"), string$1(input, "sessionId"));
3110
3897
  return json(res, 200, { ok: true });
3111
3898
  }
3112
3899
  return json(res, 404, { error: "not found" });
@@ -3122,6 +3909,168 @@ function registerRepositorySetupRoute(ctx, service) {
3122
3909
  }), "dsh-claude: repository setup route");
3123
3910
  }
3124
3911
  //#endregion
3912
+ //#region src/repository-action-routes.ts
3913
+ const MAX_BODY_BYTES$1 = 16384;
3914
+ const MAX_SESSION_ID_CHARS$1 = 1024;
3915
+ const ACTIONS = /* @__PURE__ */ new Set([
3916
+ "commit",
3917
+ "commit-push",
3918
+ "push",
3919
+ "create-pr"
3920
+ ]);
3921
+ function record$1(value) {
3922
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3923
+ }
3924
+ async function readJson$1(req) {
3925
+ const chunks = [];
3926
+ let size = 0;
3927
+ for await (const chunk of req) {
3928
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
3929
+ size += buffer.length;
3930
+ if (size > MAX_BODY_BYTES$1) throw new RepositoryActionError("body-too-large", "The request body is too large.");
3931
+ chunks.push(buffer);
3932
+ }
3933
+ const value = record$1(JSON.parse(Buffer.concat(chunks).toString("utf8")));
3934
+ if (value === void 0) throw new RepositoryActionError("invalid-request", "The request body is invalid.");
3935
+ return value;
3936
+ }
3937
+ function sessionId(url) {
3938
+ const value = url.searchParams.get("sessionId");
3939
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$1) throw new RepositoryActionError("invalid-session", "The session is invalid.");
3940
+ return value;
3941
+ }
3942
+ function string(input, key) {
3943
+ const value = input[key];
3944
+ if (typeof value !== "string") throw new RepositoryActionError("invalid-request", `The ${key} field is required.`);
3945
+ return value;
3946
+ }
3947
+ function optionalString(input, key) {
3948
+ const value = input[key];
3949
+ if (value === void 0) return void 0;
3950
+ if (typeof value !== "string") throw new RepositoryActionError("invalid-request", `The ${key} field must be a string.`);
3951
+ return value;
3952
+ }
3953
+ function actionRequest(input) {
3954
+ const action = input.action;
3955
+ if (typeof action !== "string" || !ACTIONS.has(action) || typeof input.includeUnstaged !== "boolean") throw new RepositoryActionError("invalid-request", "The repository action is invalid.");
3956
+ return {
3957
+ action,
3958
+ fingerprint: string(input, "fingerprint"),
3959
+ message: action === "push" ? optionalString(input, "message") ?? "" : string(input, "message"),
3960
+ includeUnstaged: input.includeUnstaged,
3961
+ ...optionalString(input, "prTitle") === void 0 ? {} : { prTitle: optionalString(input, "prTitle") },
3962
+ ...optionalString(input, "prBody") === void 0 ? {} : { prBody: optionalString(input, "prBody") },
3963
+ ...optionalString(input, "baseBranch") === void 0 ? {} : { baseBranch: optionalString(input, "baseBranch") },
3964
+ ...input.draft === void 0 ? {} : typeof input.draft === "boolean" ? { draft: input.draft } : (() => {
3965
+ throw new RepositoryActionError("invalid-request", "The draft field must be a boolean.");
3966
+ })()
3967
+ };
3968
+ }
3969
+ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
3970
+ ctx.effect(() => ctx.webServer.register({
3971
+ kind: "prefix",
3972
+ path: CLAUDE_REPOSITORY_ACTION_PATH,
3973
+ handler: async (req, res) => {
3974
+ if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
3975
+ const url = new URL(req.url ?? "/", "http://localhost");
3976
+ try {
3977
+ const cwd = cwdForSession(sessionId(url));
3978
+ if (cwd === void 0) throw new RepositoryActionError("session-unavailable", "The Claude session is unavailable.");
3979
+ if (url.pathname === `/plugins/dsh-claude/repository/action/preview`) {
3980
+ if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
3981
+ return json(res, 200, await service.preview(cwd));
3982
+ }
3983
+ if (url.pathname === `/plugins/dsh-claude/repository/action/message`) {
3984
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
3985
+ const input = await readJson$1(req);
3986
+ return json(res, 200, { message: await service.generateMessage(cwd, string(input, "fingerprint")) });
3987
+ }
3988
+ if (url.pathname === "/plugins/dsh-claude/repository/action") {
3989
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
3990
+ return json(res, 200, await service.execute(cwd, actionRequest(await readJson$1(req))));
3991
+ }
3992
+ return json(res, 404, { error: "not found" });
3993
+ } catch (error) {
3994
+ if (error instanceof RepositoryActionError) return json(res, 409, {
3995
+ error: error.code,
3996
+ message: error.message,
3997
+ ...error.commit === void 0 ? {} : { commit: error.commit }
3998
+ });
3999
+ if (error instanceof SyntaxError) return json(res, 400, { error: "invalid-json" });
4000
+ return json(res, 500, {
4001
+ error: "repository-action-unavailable",
4002
+ message: "Repository action is unavailable."
4003
+ });
4004
+ }
4005
+ }
4006
+ }), "dsh-claude: repository action route");
4007
+ }
4008
+ //#endregion
4009
+ //#region src/review-comment-routes.ts
4010
+ const MAX_BODY_BYTES = 16384;
4011
+ const MAX_SESSION_ID_CHARS = 1024;
4012
+ function record(value) {
4013
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4014
+ }
4015
+ async function readJson(req) {
4016
+ const chunks = [];
4017
+ let size = 0;
4018
+ for await (const chunk of req) {
4019
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4020
+ size += buffer.length;
4021
+ if (size > MAX_BODY_BYTES) throw new ReviewCommentError("body-too-large", "The request body is too large.");
4022
+ chunks.push(buffer);
4023
+ }
4024
+ const value = record(JSON.parse(Buffer.concat(chunks).toString("utf8")));
4025
+ if (value === void 0) throw new ReviewCommentError("invalid-request", "The request body is invalid.");
4026
+ return value;
4027
+ }
4028
+ function sessionIdFromUrl(url) {
4029
+ const value = url.searchParams.get("sessionId");
4030
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS) throw new ReviewCommentError("invalid-session", "The session is invalid.");
4031
+ return value;
4032
+ }
4033
+ function registerReviewCommentRoute(ctx, store, ownsSession) {
4034
+ ctx.effect(() => ctx.webServer.register({
4035
+ kind: "prefix",
4036
+ path: CLAUDE_REVIEW_COMMENT_PATH,
4037
+ handler: async (req, res) => {
4038
+ if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
4039
+ const url = new URL(req.url ?? "/", "http://localhost");
4040
+ try {
4041
+ const sessionId = sessionIdFromUrl(url);
4042
+ if (!ownsSession(sessionId)) throw new ReviewCommentError("session-unavailable", "The Claude session is unavailable.");
4043
+ if (url.pathname === "/plugins/dsh-claude/review-comments" && req.method === "POST") {
4044
+ const input = await readJson(req);
4045
+ return json(res, 200, { comment: store.add(sessionId, {
4046
+ path: input.path,
4047
+ line: input.line,
4048
+ side: input.side,
4049
+ text: input.text
4050
+ }) });
4051
+ }
4052
+ if (url.pathname === `/plugins/dsh-claude/review-comments/clear` && req.method === "POST") return json(res, 200, { removed: store.drain(sessionId).length });
4053
+ if (url.pathname === `/plugins/dsh-claude/review-comments/remove` && req.method === "POST") {
4054
+ const input = await readJson(req);
4055
+ if (typeof input.id !== "string" || input.id.length === 0 || input.id.length > 128) throw new ReviewCommentError("invalid-request", "The comment id is invalid.");
4056
+ return json(res, 200, { removed: store.remove(sessionId, input.id) });
4057
+ }
4058
+ return json(res, 404, { error: "not found" });
4059
+ } catch (error) {
4060
+ if (error instanceof ReviewCommentError) return json(res, 409, {
4061
+ error: error.code,
4062
+ message: error.message
4063
+ });
4064
+ if (error instanceof SyntaxError) return json(res, 400, { error: "invalid-json" });
4065
+ return json(res, 500, {
4066
+ error: "review-comment-unavailable",
4067
+ message: "Review comments are unavailable."
4068
+ });
4069
+ }
4070
+ }
4071
+ }), "dsh-claude: review comment route");
4072
+ }
4073
+ //#endregion
3125
4074
  //#region src/update-routes.ts
3126
4075
  const PLUGIN_PACKAGE_NAME = "@norman-else/dsh-claude";
3127
4076
  const UPDATE_TIMEOUT_MS = 3e4;
@@ -3752,6 +4701,7 @@ async function apply(ctx, config) {
3752
4701
  const sidecar = new ClaudeSidecarRepository();
3753
4702
  const repositoryStatus = new RepositoryStatusService(ctx.subprocess);
3754
4703
  const repositorySetup = new RepositorySetupService(ctx.subprocess, { branchPrefix: () => readWorktreeBranchPrefix() });
4704
+ const reviewComments = new ReviewCommentStore();
3755
4705
  const commandCatalogs = /* @__PURE__ */ new Map();
3756
4706
  const supervisor = new ClaudeSupervisor({
3757
4707
  runtime: ctx.subprocess,
@@ -3764,7 +4714,7 @@ async function apply(ctx, config) {
3764
4714
  let resolutionError;
3765
4715
  try {
3766
4716
  supervisorConfig.executablePath = (await resolveClaudeExecutable(ctx.subprocess, config.executablePath === void 0 || config.executablePath.length === 0 ? void 0 : config.executablePath)).path;
3767
- ctx.llm.registerAdapter([...CLAUDE_CODE_PROVIDER_IDS], createClaudeCodeAdapter(supervisor, ctx.agents, ctx.attachments, (agent) => ctx.agentPresets.composedPreset(agent.ctx)));
4717
+ ctx.llm.registerAdapter([...CLAUDE_CODE_PROVIDER_IDS], createClaudeCodeAdapter(supervisor, ctx.agents, ctx.attachments, (agent) => ctx.agentPresets.composedPreset(agent.ctx), (sessionId) => reviewComments.drain(sessionId)));
3768
4718
  ctx.effect(() => {
3769
4719
  const mounted = /* @__PURE__ */ new Map();
3770
4720
  const pending = /* @__PURE__ */ new Set();
@@ -3825,9 +4775,11 @@ async function apply(ctx, config) {
3825
4775
  resolutionError = error;
3826
4776
  }
3827
4777
  ctx.on("agent/disposed", async ({ agent }) => {
4778
+ reviewComments.disposeSession(agent.id);
3828
4779
  await supervisor.disposeSession(agent.id);
3829
4780
  await repositorySetup.cleanupSession(agent.id);
3830
4781
  });
4782
+ ctx.effect(() => () => reviewComments.dispose(), "dsh-claude: review comments store");
3831
4783
  ctx.effect(() => () => supervisor.dispose(), "dsh-claude: process supervisor");
3832
4784
  ctx.effect(() => () => repositoryStatus.dispose(), "dsh-claude: repository status cache");
3833
4785
  ctx.inject(["webServer"], (webCtx) => {
@@ -3836,15 +4788,22 @@ async function apply(ctx, config) {
3836
4788
  registerClaudeUpdateRoutes(webCtx, webCtx.subprocess, { ...typeof desktopActions?.requestRestart === "function" ? { requestRestart: desktopActions.requestRestart.bind(desktopActions) } : {} });
3837
4789
  registerClaudeGlobalSettingsRoute(webCtx);
3838
4790
  registerRepositorySetupRoute(webCtx, repositorySetup);
3839
- registerClaudeProjectionRoute(webCtx, sidecar, (sessionId) => {
4791
+ registerRepositoryActionRoute(webCtx, new RepositoryActionService(webCtx.subprocess, supervisorConfig.executablePath, (cwd) => repositoryStatus.invalidate(cwd)), (sessionId) => {
4792
+ const agent = webCtx.agents.get(sessionId);
4793
+ if (agent === void 0 || webCtx.agentPresets.composedPreset(agent.ctx) !== "claude") return void 0;
4794
+ return agent.session.header.cwd;
4795
+ });
4796
+ const ownsClaudeSession = (sessionId) => {
3840
4797
  const agent = webCtx.agents.get(sessionId);
3841
4798
  return agent !== void 0 && webCtx.agentPresets.composedPreset(agent.ctx) === "claude";
3842
- }, (sessionId) => commandCatalogs.get(sessionId) ?? [], async (sessionId) => {
4799
+ };
4800
+ registerReviewCommentRoute(webCtx, reviewComments, ownsClaudeSession);
4801
+ registerClaudeProjectionRoute(webCtx, sidecar, ownsClaudeSession, (sessionId) => commandCatalogs.get(sessionId) ?? [], async (sessionId) => {
3843
4802
  const agent = webCtx.agents.get(sessionId);
3844
4803
  if (agent === void 0 || webCtx.agentPresets.composedPreset(agent.ctx) !== "claude") return void 0;
3845
4804
  const cwd = agent.session.header.cwd;
3846
4805
  return cwd === void 0 ? void 0 : repositoryStatus.inspect(cwd);
3847
- });
4806
+ }, (sessionId) => reviewComments.list(sessionId));
3848
4807
  });
3849
4808
  }
3850
4809
  //#endregion