@norman-else/dsh-claude 0.1.19 → 0.1.20

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,16 +1,16 @@
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;
@@ -22,18 +22,18 @@ function emptyProjection() {
22
22
  activities: []
23
23
  };
24
24
  }
25
- function record$3(value) {
25
+ function record$5(value) {
26
26
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
27
27
  }
28
28
  function finiteInteger(value) {
29
29
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
30
30
  }
31
- function string$2(value, max) {
31
+ function string$3(value, max) {
32
32
  return typeof value === "string" && value.length > 0 && value.length <= max;
33
33
  }
34
34
  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;
35
+ const input = record$5(value);
36
+ 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
37
  return {
38
38
  claudeSessionId: input.claudeSessionId,
39
39
  sdkVersion: input.sdkVersion,
@@ -42,6 +42,7 @@ function binding(value) {
42
42
  };
43
43
  }
44
44
  const ACTIVITY_KINDS = /* @__PURE__ */ new Set([
45
+ "text",
45
46
  "status",
46
47
  "thinking",
47
48
  "tool-call",
@@ -61,22 +62,22 @@ const ACTIVITY_PHASES = /* @__PURE__ */ new Set([
61
62
  "failed"
62
63
  ]);
63
64
  function activity(value) {
64
- const input = record$3(value);
65
+ const input = record$5(value);
65
66
  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
67
  return normalizeActivity(input);
67
68
  }
68
69
  function contextUsage(value) {
69
- const input = record$3(value);
70
+ const input = record$5(value);
70
71
  if (input === void 0 || !Array.isArray(input.categories)) return void 0;
71
72
  return normalizeContextUsage(input);
72
73
  }
73
74
  function tasks(value) {
74
- const input = record$3(value);
75
+ const input = record$5(value);
75
76
  if (input === void 0 || !Array.isArray(input.tasks)) return void 0;
76
77
  return normalizeTasksEvent(input.tasks);
77
78
  }
78
79
  function parseClaudeSidecar(value) {
79
- const input = record$3(value);
80
+ const input = record$5(value);
80
81
  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
82
  const activities = input.activities.map(activity);
82
83
  if (activities.some((item) => item === void 0)) throw new Error("dsh-claude: invalid sidecar activity");
@@ -525,10 +526,10 @@ function createUserQuestionBridge(userQuestions, activeContext) {
525
526
  }
526
527
  //#endregion
527
528
  //#region src/sdk-messages.ts
528
- function record$2(value) {
529
+ function record$4(value) {
529
530
  return value !== null && typeof value === "object" ? value : void 0;
530
531
  }
531
- function string$1(value) {
532
+ function string$2(value) {
532
533
  return typeof value === "string" ? value : void 0;
533
534
  }
534
535
  function taskUsageOf(usage) {
@@ -540,7 +541,7 @@ function taskUsageOf(usage) {
540
541
  return Object.keys(normalized).length === 0 ? void 0 : normalized;
541
542
  }
542
543
  function resultUsage(message) {
543
- const usage = record$2(message.usage);
544
+ const usage = record$4(message.usage);
544
545
  const normalized = {};
545
546
  if (usage !== void 0) {
546
547
  if (typeof usage.input_tokens === "number") normalized.inputTokens = usage.input_tokens;
@@ -552,26 +553,26 @@ function resultUsage(message) {
552
553
  return normalized;
553
554
  }
554
555
  function normalizeAssistant(message) {
555
- const content = record$2(message.message)?.content;
556
+ const content = record$4(message.message)?.content;
556
557
  if (!Array.isArray(content)) return [{
557
558
  kind: "protocol-error",
558
559
  title: "Malformed Claude assistant message",
559
560
  detail: message
560
561
  }];
561
- const parentToolUseId = string$1(message.parent_tool_use_id);
562
+ const parentToolUseId = string$2(message.parent_tool_use_id);
562
563
  const normalized = [];
563
564
  for (const item of content) {
564
- const block = record$2(item);
565
+ const block = record$4(item);
565
566
  if (block === void 0) continue;
566
567
  if (block.type === "text") {
567
- const text = string$1(block.text);
568
+ const text = string$2(block.text);
568
569
  if (text !== void 0 && text.length > 0) normalized.push({
569
570
  kind: "assistant-text",
570
571
  text,
571
572
  ...parentToolUseId === void 0 ? {} : { parentToolUseId }
572
573
  });
573
574
  } else if (block.type === "thinking") {
574
- const text = string$1(block.thinking);
575
+ const text = string$2(block.thinking);
575
576
  if (text !== void 0 && text.length > 0) normalized.push({
576
577
  kind: "thinking",
577
578
  text,
@@ -579,8 +580,8 @@ function normalizeAssistant(message) {
579
580
  ...parentToolUseId === void 0 ? {} : { parentToolUseId }
580
581
  });
581
582
  } else if (block.type === "tool_use") {
582
- const toolUseId = string$1(block.id);
583
- const toolName = string$1(block.name);
583
+ const toolUseId = string$2(block.id);
584
+ const toolName = string$2(block.name);
584
585
  if (toolUseId !== void 0 && toolName !== void 0) normalized.push({
585
586
  kind: "tool-call",
586
587
  toolUseId,
@@ -593,18 +594,18 @@ function normalizeAssistant(message) {
593
594
  return normalized;
594
595
  }
595
596
  function normalizeUser(message) {
596
- const content = record$2(message.message)?.content;
597
+ const content = record$4(message.message)?.content;
597
598
  if (!Array.isArray(content)) return [{
598
599
  kind: "protocol-error",
599
600
  title: "Malformed Claude user message",
600
601
  detail: message
601
602
  }];
602
- const parentToolUseId = string$1(message.parent_tool_use_id);
603
+ const parentToolUseId = string$2(message.parent_tool_use_id);
603
604
  const normalized = [];
604
605
  for (const item of content) {
605
- const block = record$2(item);
606
+ const block = record$4(item);
606
607
  if (block?.type !== "tool_result") continue;
607
- const toolUseId = string$1(block.tool_use_id);
608
+ const toolUseId = string$2(block.tool_use_id);
608
609
  if (toolUseId === void 0) continue;
609
610
  normalized.push({
610
611
  kind: "tool-result",
@@ -617,11 +618,11 @@ function normalizeUser(message) {
617
618
  return normalized;
618
619
  }
619
620
  function normalizeSystem(message) {
620
- const subtype = string$1(message.subtype);
621
+ const subtype = string$2(message.subtype);
621
622
  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);
623
+ const sessionId = string$2(message.session_id);
624
+ const cliVersion = string$2(message.claude_code_version);
625
+ const cwd = string$2(message.cwd);
625
626
  return sessionId !== void 0 && cliVersion !== void 0 && cwd !== void 0 ? [{
626
627
  kind: "init",
627
628
  sessionId,
@@ -650,20 +651,20 @@ function normalizeSystem(message) {
650
651
  title: `Claude session ${String(message.state)}`
651
652
  }];
652
653
  if (subtype === "permission_denied") {
653
- const toolUseId = string$1(message.tool_use_id);
654
- const toolName = string$1(message.tool_name);
654
+ const toolUseId = string$2(message.tool_use_id);
655
+ const toolName = string$2(message.tool_name);
655
656
  if (toolUseId !== void 0 && toolName !== void 0) return [{
656
657
  kind: "permission-denied",
657
658
  toolUseId,
658
659
  toolName,
659
- summary: string$1(message.message) ?? "Claude Code denied the action"
660
+ summary: string$2(message.message) ?? "Claude Code denied the action"
660
661
  }];
661
662
  }
662
663
  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);
664
+ const taskId = string$2(message.task_id);
665
+ const description = string$2(message.description);
666
+ const subagentType = string$2(message.subagent_type);
667
+ const taskType = string$2(message.task_type);
667
668
  return [{
668
669
  kind: "subagent",
669
670
  title: description ?? taskId ?? "Claude subagent started",
@@ -678,12 +679,12 @@ function normalizeSystem(message) {
678
679
  }];
679
680
  }
680
681
  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));
682
+ const taskId = string$2(message.task_id);
683
+ const description = string$2(message.description);
684
+ const summary = string$2(message.summary);
685
+ const subagentType = string$2(message.subagent_type);
686
+ const lastToolName = string$2(message.last_tool_name);
687
+ const usage = taskUsageOf(record$4(message.usage));
687
688
  return [{
688
689
  kind: "subagent",
689
690
  title: summary ?? description ?? "Claude subagent update",
@@ -699,11 +700,11 @@ function normalizeSystem(message) {
699
700
  }];
700
701
  }
701
702
  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);
703
+ const patch = record$4(message.patch);
704
+ const status = string$2(patch?.status);
705
+ const taskId = string$2(message.task_id);
706
+ const description = string$2(patch?.description);
707
+ const error = string$2(patch?.error);
707
708
  const taskStatus = status === void 0 ? void 0 : status === "killed" ? "killed" : status === "completed" ? "completed" : status === "failed" ? "failed" : "running";
708
709
  return [{
709
710
  kind: "subagent",
@@ -719,10 +720,10 @@ function normalizeSystem(message) {
719
720
  if (subtype === "task_notification") {
720
721
  const failed = message.status === "failed";
721
722
  const stopped = message.status === "stopped" || message.status === "cancelled";
722
- const taskId = string$1(message.task_id);
723
- const summary = string$1(message.summary);
723
+ const taskId = string$2(message.task_id);
724
+ const summary = string$2(message.summary);
724
725
  const taskStatus = failed ? "failed" : stopped ? "stopped" : "completed";
725
- const usage = taskUsageOf(record$2(message.usage));
726
+ const usage = taskUsageOf(record$4(message.usage));
726
727
  return [{
727
728
  kind: "subagent",
728
729
  title: summary ?? taskId ?? "Claude subagent finished",
@@ -737,10 +738,10 @@ function normalizeSystem(message) {
737
738
  if (subtype === "background_tasks_changed") return [{
738
739
  kind: "background-tasks",
739
740
  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);
741
+ const entry = record$4(item);
742
+ const taskId = string$2(entry?.task_id);
743
+ const description = string$2(entry?.description);
744
+ const taskType = string$2(entry?.task_type);
744
745
  if (taskId === void 0 || description === void 0) return [];
745
746
  return [{
746
747
  taskId,
@@ -756,7 +757,7 @@ function normalizeSystem(message) {
756
757
  }];
757
758
  if (subtype === "informational" || subtype === "notification" || subtype === "local_command_output") return [{
758
759
  kind: message.level === "warning" ? "warning" : "status",
759
- title: string$1(message.content) ?? string$1(message.text) ?? "Claude Code notice",
760
+ title: string$2(message.content) ?? string$2(message.text) ?? "Claude Code notice",
760
761
  detail: message
761
762
  }];
762
763
  if (subtype?.startsWith("hook_") === true || subtype === "plugin_install") return [{
@@ -780,12 +781,12 @@ const RESULT_ERROR_SUBTYPES = /* @__PURE__ */ new Set([
780
781
  function normalizeSdkMessage(message) {
781
782
  const value = message;
782
783
  if (value.type === "stream_event") {
783
- const event = record$2(value.event);
784
- const parentToolUseId = string$1(value.parent_tool_use_id);
784
+ const event = record$4(value.event);
785
+ const parentToolUseId = string$2(value.parent_tool_use_id);
785
786
  if (event?.type === "content_block_delta") {
786
- const delta = record$2(event.delta);
787
+ const delta = record$4(event.delta);
787
788
  if (delta?.type === "text_delta") {
788
- const text = string$1(delta.text);
789
+ const text = string$2(delta.text);
789
790
  return text === void 0 ? [] : [{
790
791
  kind: "text-delta",
791
792
  text,
@@ -793,7 +794,7 @@ function normalizeSdkMessage(message) {
793
794
  }];
794
795
  }
795
796
  if (delta?.type === "thinking_delta") {
796
- const text = string$1(delta.thinking);
797
+ const text = string$2(delta.thinking);
797
798
  return text === void 0 ? [] : [{
798
799
  kind: "thinking",
799
800
  text,
@@ -808,7 +809,7 @@ function normalizeSdkMessage(message) {
808
809
  if (value.type === "user") return normalizeUser(value);
809
810
  if (value.type === "system") return normalizeSystem(value);
810
811
  if (value.type === "result") {
811
- const sessionId = string$1(value.session_id);
812
+ const sessionId = string$2(value.session_id);
812
813
  if (sessionId === void 0 || value.subtype !== "success" && !RESULT_ERROR_SUBTYPES.has(String(value.subtype))) return [{
813
814
  kind: "protocol-error",
814
815
  title: "Malformed Claude result message",
@@ -816,11 +817,11 @@ function normalizeSdkMessage(message) {
816
817
  }];
817
818
  const success = value.subtype === "success" && value.is_error !== true;
818
819
  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);
820
+ const terminalReason = string$2(value.terminal_reason);
821
+ const userMessageUuid = string$2(value.user_message_uuid);
822
+ const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record$4(item)).filter((item) => item !== void 0).map((item) => {
823
+ const toolName = string$2(item.tool_name);
824
+ const toolUseId = string$2(item.tool_use_id);
824
825
  return toolName === void 0 || toolUseId === void 0 ? void 0 : {
825
826
  toolName,
826
827
  toolUseId
@@ -844,11 +845,10 @@ function normalizeSdkMessage(message) {
844
845
  detail: value.error ?? value.output
845
846
  }];
846
847
  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";
848
+ const status = string$2(record$4(value.rate_limit_info)?.status);
849
849
  return [{
850
- kind: blocking ? "warning" : "status",
851
- title: blocking ? "Claude rate limit is blocking requests" : "Claude rate limit status changed",
850
+ kind: "status",
851
+ title: status !== void 0 && status !== "allowed" ? "Claude rate limit is blocking requests" : "Claude rate limit status changed",
852
852
  detail: value.rate_limit_info
853
853
  }];
854
854
  }
@@ -1034,6 +1034,7 @@ function rootCallSummary(toolName, input) {
1034
1034
  }
1035
1035
  var ClaudeSupervisor = class {
1036
1036
  #entries = /* @__PURE__ */ new Map();
1037
+ #interruptions = /* @__PURE__ */ new Map();
1037
1038
  #runtime;
1038
1039
  #approval;
1039
1040
  #userQuestions;
@@ -1041,7 +1042,6 @@ var ClaudeSupervisor = class {
1041
1042
  #queryFactory;
1042
1043
  #runDetached;
1043
1044
  #sidecar;
1044
- #dynamicPresenterNames = /* @__PURE__ */ new WeakMap();
1045
1045
  #contextWindows = /* @__PURE__ */ new Map();
1046
1046
  #disposed = false;
1047
1047
  #admissionGate = Promise.resolve();
@@ -1082,6 +1082,8 @@ var ClaudeSupervisor = class {
1082
1082
  return this.#contextWindows.get(model);
1083
1083
  }
1084
1084
  runTurn(request) {
1085
+ const interruption = this.#interruptions.get(request.agent.id);
1086
+ if (interruption !== void 0) return interruption.then(() => this.runTurn(request));
1085
1087
  const operation = this.#admissionGate.then(() => this.#runTurnAdmitted(request));
1086
1088
  this.#admissionGate = operation.then(() => void 0, () => void 0);
1087
1089
  return operation;
@@ -1134,6 +1136,8 @@ var ClaudeSupervisor = class {
1134
1136
  sawActivity: false,
1135
1137
  sawTextDelta: false,
1136
1138
  text: "",
1139
+ transcriptText: "",
1140
+ transcriptTextOrdinal: void 0,
1137
1141
  thinking: "",
1138
1142
  aborted: false,
1139
1143
  deniedToolUseIds: /* @__PURE__ */ new Set(),
@@ -1172,7 +1176,7 @@ var ClaudeSupervisor = class {
1172
1176
  }
1173
1177
  if (request.signal !== void 0) {
1174
1178
  const abortListener = () => {
1175
- this.#interrupt(entry);
1179
+ this.#startInterrupt(entry);
1176
1180
  };
1177
1181
  active.abortListener = abortListener;
1178
1182
  request.signal.addEventListener("abort", abortListener, { once: true });
@@ -1387,7 +1391,14 @@ var ClaudeSupervisor = class {
1387
1391
  if (message.userMessageUuid !== void 0 && message.userMessageUuid === entry.handshakeUuid) return;
1388
1392
  if (entry.claudeSessionId === void 0) throw new ClaudeProtocolError("Claude Code sent a result before initialization");
1389
1393
  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}`);
1394
+ if (active.phase === "waiting-tasks") {
1395
+ await this.#completeProgressSegment(active, message);
1396
+ return;
1397
+ }
1398
+ if (message.userMessageUuid !== void 0 && message.userMessageUuid !== active.promptUuid) {
1399
+ if (active.phase === "follow-up") return;
1400
+ throw new ClaudeProtocolError(`Claude Code result for user message ${message.userMessageUuid} does not match active request ${active.promptUuid}`);
1401
+ }
1391
1402
  await this.#completeTurn(entry, active, message);
1392
1403
  return;
1393
1404
  }
@@ -1398,6 +1409,8 @@ var ClaudeSupervisor = class {
1398
1409
  if (message.parentToolUseId !== void 0) return;
1399
1410
  active.sawTextDelta = true;
1400
1411
  active.text += message.text;
1412
+ active.transcriptText += message.text;
1413
+ await this.#upsertTranscriptText(active);
1401
1414
  active.output.push({
1402
1415
  type: "text-delta",
1403
1416
  text: message.text
@@ -1408,6 +1421,8 @@ var ClaudeSupervisor = class {
1408
1421
  if (!active.sawTextDelta) {
1409
1422
  active.sawTextDelta = true;
1410
1423
  active.text += message.text;
1424
+ active.transcriptText += message.text;
1425
+ await this.#upsertTranscriptText(active);
1411
1426
  active.output.push({
1412
1427
  type: "text-delta",
1413
1428
  text: message.text
@@ -1429,6 +1444,7 @@ var ClaudeSupervisor = class {
1429
1444
  });
1430
1445
  return;
1431
1446
  case "tool-call":
1447
+ if (message.parentToolUseId === void 0) this.#closeTranscriptTextSegment(active);
1432
1448
  await this.#appendActivity(active, {
1433
1449
  kind: message.parentToolUseId === void 0 ? "tool-call" : "subagent",
1434
1450
  phase: "started",
@@ -1439,11 +1455,7 @@ var ClaudeSupervisor = class {
1439
1455
  summary: message.parentToolUseId === void 0 ? rootCallSummary(message.toolName, message.input) : `Subagent called ${message.toolName}`,
1440
1456
  detail: message.input
1441
1457
  });
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
- }
1458
+ if (message.parentToolUseId === void 0) active.callNames.set(message.toolUseId, message.toolName);
1447
1459
  return;
1448
1460
  case "tool-result":
1449
1461
  await this.#appendActivity(active, {
@@ -1455,7 +1467,6 @@ var ClaudeSupervisor = class {
1455
1467
  detail: message.output,
1456
1468
  isError: message.isError
1457
1469
  });
1458
- if (message.parentToolUseId === void 0 && !TASK_TOOL_NAMES.has(active.callNames.get(message.toolUseId) ?? "")) await this.#appendNativeToolResult(active, message);
1459
1470
  return;
1460
1471
  case "subagent":
1461
1472
  await this.#appendActivity(active, {
@@ -1488,12 +1499,6 @@ var ClaudeSupervisor = class {
1488
1499
  title: message.toolName,
1489
1500
  summary: message.summary
1490
1501
  });
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
1502
  return;
1498
1503
  }
1499
1504
  }
@@ -1571,6 +1576,7 @@ var ClaudeSupervisor = class {
1571
1576
  active.promptUuid = randomUUID();
1572
1577
  active.sawTextDelta = false;
1573
1578
  active.text = "";
1579
+ this.#closeTranscriptTextSegment(active);
1574
1580
  active.thinking = "";
1575
1581
  await this.#appendSafely(active, {
1576
1582
  kind: "status",
@@ -1604,9 +1610,43 @@ var ClaudeSupervisor = class {
1604
1610
  entry.taskSnapshotAt = Date.now();
1605
1611
  await this.#sidecar.writeTasks(entry.sessionId, [...entry.tasks.values()]).catch(() => void 0);
1606
1612
  }
1613
+ async #completeProgressSegment(active, result, recordUsage = true) {
1614
+ if (recordUsage && (result.usage.inputTokens !== void 0 || result.usage.outputTokens !== void 0 || result.usage.cumulativeCostUsd !== void 0)) {
1615
+ await this.#appendSafely(active, {
1616
+ kind: "usage",
1617
+ phase: "completed",
1618
+ title: "Claude usage",
1619
+ summary: usageSummary(result.usage),
1620
+ usage: result.usage
1621
+ });
1622
+ active.output.push({
1623
+ type: "usage",
1624
+ usage: result.usage
1625
+ });
1626
+ }
1627
+ if (!active.sawTextDelta && active.text.length === 0 && result.text !== void 0) {
1628
+ active.text = result.text;
1629
+ active.transcriptText = result.text;
1630
+ await this.#upsertTranscriptText(active);
1631
+ active.output.push({
1632
+ type: "text-delta",
1633
+ text: result.text
1634
+ });
1635
+ }
1636
+ await this.#upsertTranscriptText(active);
1637
+ active.output.push({
1638
+ type: "segment-complete",
1639
+ text: active.text
1640
+ });
1641
+ active.sawTextDelta = false;
1642
+ active.text = "";
1643
+ this.#closeTranscriptTextSegment(active);
1644
+ active.thinking = "";
1645
+ }
1607
1646
  async #completeTurn(entry, active, result) {
1608
1647
  if (entry.active !== active) return;
1609
1648
  if (active.aborted) {
1649
+ await this.#upsertTranscriptText(active);
1610
1650
  await this.#appendSafely(active, {
1611
1651
  kind: "status",
1612
1652
  phase: "failed",
@@ -1639,6 +1679,16 @@ var ClaudeSupervisor = class {
1639
1679
  summary: unmatchedDenials.map((denial) => denial.toolName).join(", ")
1640
1680
  });
1641
1681
  if (!result.success) {
1682
+ if (!active.sawTextDelta && active.text.length === 0 && result.text !== void 0) {
1683
+ active.text = result.text;
1684
+ active.transcriptText = result.text;
1685
+ await this.#upsertTranscriptText(active);
1686
+ active.output.push({
1687
+ type: "text-delta",
1688
+ text: result.text
1689
+ });
1690
+ }
1691
+ await this.#upsertTranscriptText(active);
1642
1692
  const message = result.errors?.join("\n") ?? (result.terminalReason !== void 0 ? `Claude Code failed the turn (${result.terminalReason})` : "Claude Code failed the turn");
1643
1693
  await this.#appendSafely(active, {
1644
1694
  kind: "error",
@@ -1651,11 +1701,14 @@ var ClaudeSupervisor = class {
1651
1701
  } else {
1652
1702
  if (!active.sawTextDelta && active.text.length === 0 && result.text !== void 0) {
1653
1703
  active.text = result.text;
1704
+ active.transcriptText = result.text;
1705
+ await this.#upsertTranscriptText(active);
1654
1706
  active.output.push({
1655
1707
  type: "text-delta",
1656
1708
  text: result.text
1657
1709
  });
1658
1710
  }
1711
+ await this.#upsertTranscriptText(active);
1659
1712
  if (active.phase === "primary" && this.#hasRunningTasks(entry, active)) {
1660
1713
  active.phase = "waiting-tasks";
1661
1714
  await this.#appendSafely(active, {
@@ -1663,10 +1716,7 @@ var ClaudeSupervisor = class {
1663
1716
  phase: "updated",
1664
1717
  title: "Claude Code is waiting for background tasks"
1665
1718
  });
1666
- active.output.push({
1667
- type: "segment-complete",
1668
- text: active.text
1669
- });
1719
+ await this.#completeProgressSegment(active, result, false);
1670
1720
  return;
1671
1721
  }
1672
1722
  await this.#appendSafely(active, {
@@ -1686,6 +1736,25 @@ var ClaudeSupervisor = class {
1686
1736
  entry.lastUsedAt = Date.now();
1687
1737
  this.#armIdleTimer(entry);
1688
1738
  }
1739
+ async #upsertTranscriptText(active) {
1740
+ if (active.transcriptText.length === 0) return;
1741
+ const ordinal = active.transcriptTextOrdinal ?? active.cursor.nextOrdinal++;
1742
+ active.transcriptTextOrdinal = ordinal;
1743
+ try {
1744
+ await this.#sidecar.appendActivity(active.agent.id, {
1745
+ kind: "text",
1746
+ phase: "updated",
1747
+ text: active.transcriptText,
1748
+ turn: active.cursor.turn,
1749
+ step: active.cursor.step,
1750
+ ordinal
1751
+ });
1752
+ } catch {}
1753
+ }
1754
+ #closeTranscriptTextSegment(active) {
1755
+ active.transcriptText = "";
1756
+ active.transcriptTextOrdinal = void 0;
1757
+ }
1689
1758
  async #appendActivity(active, activity) {
1690
1759
  const ordinal = active.cursor.nextOrdinal++;
1691
1760
  await this.#sidecar.appendActivity(active.agent.id, {
@@ -1700,53 +1769,14 @@ var ClaudeSupervisor = class {
1700
1769
  async #appendSafely(active, activity) {
1701
1770
  await this.#appendActivity(active, activity).catch(() => void 0);
1702
1771
  }
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 {}
1772
+ #startInterrupt(entry) {
1773
+ const existing = this.#interruptions.get(entry.sessionId);
1774
+ if (existing !== void 0) return existing;
1775
+ const interruption = this.#interrupt(entry).finally(() => {
1776
+ this.#interruptions.delete(entry.sessionId);
1777
+ });
1778
+ this.#interruptions.set(entry.sessionId, interruption);
1779
+ return interruption;
1750
1780
  }
1751
1781
  async #interrupt(entry) {
1752
1782
  const active = entry.active;
@@ -1754,6 +1784,7 @@ var ClaudeSupervisor = class {
1754
1784
  entry.state = "interrupting";
1755
1785
  active.aborted = true;
1756
1786
  active.output.fail(abortFailure());
1787
+ await this.#upsertTranscriptText(active);
1757
1788
  let interruptError;
1758
1789
  try {
1759
1790
  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 +1806,7 @@ var ClaudeSupervisor = class {
1775
1806
  const active = entry.active;
1776
1807
  const stderr = entry.process?.stderrTail();
1777
1808
  if (active !== void 0) {
1809
+ await this.#upsertTranscriptText(active);
1778
1810
  if (active.signal !== void 0 && active.abortListener !== void 0) active.signal.removeEventListener("abort", active.abortListener);
1779
1811
  const unknown = active.sawActivity;
1780
1812
  entry.state = unknown ? "outcome-unknown" : "disconnected";
@@ -1830,6 +1862,79 @@ var ClaudeSupervisor = class {
1830
1862
  }
1831
1863
  };
1832
1864
  //#endregion
1865
+ //#region src/review-comments.ts
1866
+ const MAX_COMMENTS_PER_SESSION = 50;
1867
+ const MAX_TEXT_CHARS$1 = 2e3;
1868
+ const MAX_PATH_CHARS$1 = 1024;
1869
+ const MAX_LINE = 1e7;
1870
+ var ReviewCommentError = class extends Error {
1871
+ code;
1872
+ constructor(code, message) {
1873
+ super(message);
1874
+ this.name = "ReviewCommentError";
1875
+ this.code = code;
1876
+ }
1877
+ };
1878
+ function validSide(value) {
1879
+ return value === "old" || value === "new";
1880
+ }
1881
+ /** Pending line comments queued per session until the next user turn drains them. */
1882
+ var ReviewCommentStore = class {
1883
+ #comments = /* @__PURE__ */ new Map();
1884
+ add(sessionId, input) {
1885
+ const path = typeof input.path === "string" ? input.path.trim() : "";
1886
+ const text = typeof input.text === "string" ? input.text.trim() : "";
1887
+ 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.");
1888
+ if (text.length === 0 || text.length > MAX_TEXT_CHARS$1 || text.includes("\0")) throw new ReviewCommentError("invalid-request", "The comment text is invalid.");
1889
+ 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.");
1890
+ if (!validSide(input.side)) throw new ReviewCommentError("invalid-request", "The comment side is invalid.");
1891
+ const existing = this.#comments.get(sessionId) ?? [];
1892
+ if (existing.length >= MAX_COMMENTS_PER_SESSION) throw new ReviewCommentError("too-many-comments", "Too many pending review comments. Remove one before adding another.");
1893
+ const comment = {
1894
+ id: randomUUID(),
1895
+ path,
1896
+ line: input.line,
1897
+ side: input.side,
1898
+ text
1899
+ };
1900
+ this.#comments.set(sessionId, [...existing, comment]);
1901
+ return comment;
1902
+ }
1903
+ remove(sessionId, id) {
1904
+ const existing = this.#comments.get(sessionId);
1905
+ if (existing === void 0) return false;
1906
+ const next = existing.filter((comment) => comment.id !== id);
1907
+ if (next.length === existing.length) return false;
1908
+ if (next.length === 0) this.#comments.delete(sessionId);
1909
+ else this.#comments.set(sessionId, next);
1910
+ return true;
1911
+ }
1912
+ list(sessionId) {
1913
+ return this.#comments.get(sessionId) ?? [];
1914
+ }
1915
+ /** Remove and return every pending comment; called when a user turn consumes them. */
1916
+ drain(sessionId) {
1917
+ const existing = this.#comments.get(sessionId) ?? [];
1918
+ this.#comments.delete(sessionId);
1919
+ return existing;
1920
+ }
1921
+ disposeSession(sessionId) {
1922
+ this.#comments.delete(sessionId);
1923
+ }
1924
+ dispose() {
1925
+ this.#comments.clear();
1926
+ }
1927
+ };
1928
+ /** Render drained comments as the prompt block preceding the user's message text. */
1929
+ function formatReviewComments(comments) {
1930
+ return [
1931
+ "<user-review-comments>",
1932
+ "The user attached these code review comments to this message. Each references a file and line from the current working tree diff:",
1933
+ ...comments.map((comment, index) => `${index + 1}. ${comment.path}:${comment.line}${comment.side === "old" ? " (old side)" : ""} — ${comment.text}`),
1934
+ "</user-review-comments>"
1935
+ ].join("\n");
1936
+ }
1937
+ //#endregion
1833
1938
  //#region src/adapter.ts
1834
1939
  const MODELS = [
1835
1940
  {
@@ -1926,6 +2031,22 @@ function validateImageRef(ref, attachments, imageIndex) {
1926
2031
  const maxDimension = "maxImageDimension" in limits && finiteNonNegative(limits.maxImageDimension) ? limits.maxImageDimension : void 0;
1927
2032
  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
2033
  }
2034
+ /**
2035
+ * Prepend the session's pending diff-review comments to the outgoing user
2036
+ * prompt. The comments are drained once per turn: they are formatted into one
2037
+ * `<user-review-comments>` block placed ahead of the user's own text (or as a
2038
+ * leading text block for multi-part prompts) so Claude reads them in the same
2039
+ * turn that consumed them.
2040
+ */
2041
+ function injectReviewComments(prompt, comments) {
2042
+ if (comments.length === 0) return prompt;
2043
+ const block = formatReviewComments(comments);
2044
+ if (typeof prompt === "string") return `${block}\n\n${prompt}`;
2045
+ return [{
2046
+ type: "text",
2047
+ text: block
2048
+ }, ...prompt];
2049
+ }
1929
2050
  function imageBlock(data, mediaType) {
1930
2051
  return {
1931
2052
  type: "image",
@@ -2008,12 +2129,14 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
2008
2129
  #agents;
2009
2130
  #attachments;
2010
2131
  #presetIdFor;
2011
- constructor(supervisor, agents, attachments, presetIdFor) {
2132
+ #drainReviewComments;
2133
+ constructor(supervisor, agents, attachments, presetIdFor, drainReviewComments = () => []) {
2012
2134
  super();
2013
2135
  this.#supervisor = supervisor;
2014
2136
  this.#agents = agents;
2015
2137
  this.#attachments = attachments;
2016
2138
  this.#presetIdFor = presetIdFor;
2139
+ this.#drainReviewComments = drainReviewComments;
2017
2140
  }
2018
2141
  providerInfo(provider) {
2019
2142
  return {
@@ -2080,42 +2203,20 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
2080
2203
  }
2081
2204
  const events = await this.#supervisor.runTurn({
2082
2205
  agent,
2083
- prompt,
2206
+ prompt: injectReviewComments(prompt, this.#drainReviewComments(agent.id)),
2084
2207
  model: options.model,
2085
2208
  ...thinkingMode === void 0 ? {} : { thinkingMode },
2086
2209
  ...options.signal === void 0 ? {} : { signal: options.signal }
2087
2210
  });
2088
- let text = "";
2089
2211
  let pendingUsage;
2090
2212
  let completed = false;
2091
- let blockIndex = 0;
2092
2213
  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;
2214
+ for await (const event of events) {
2215
+ if (event.type === "text-delta" || event.type === "segment-complete") continue;
2216
+ if (event.type === "usage") {
2217
+ pendingUsage = tokenUsage(event.usage);
2218
+ continue;
2116
2219
  }
2117
- text = "";
2118
- if (event.type === "segment-complete") continue;
2119
2220
  completed = true;
2120
2221
  if (pendingUsage !== void 0) yield {
2121
2222
  type: "usage",
@@ -2129,26 +2230,6 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
2129
2230
  } catch (error) {
2130
2231
  if (error.name === "AbortError") {
2131
2232
  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
2233
  yield {
2153
2234
  type: "finish",
2154
2235
  reason: {
@@ -2166,8 +2247,8 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
2166
2247
  if (!completed) throw new Error("dsh-claude: Claude turn stream ended without a result");
2167
2248
  }
2168
2249
  };
2169
- function createClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor) {
2170
- return new ClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor);
2250
+ function createClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments = () => []) {
2251
+ return new ClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments);
2171
2252
  }
2172
2253
  //#endregion
2173
2254
  //#region src/http.ts
@@ -2294,8 +2375,8 @@ function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolution
2294
2375
  }
2295
2376
  //#endregion
2296
2377
  //#region src/projection-routes.ts
2297
- const MAX_SESSION_ID_CHARS = 1024;
2298
- function sessionIdFromUrl(rawUrl) {
2378
+ const MAX_SESSION_ID_CHARS$2 = 1024;
2379
+ function sessionIdFromUrl$1(rawUrl) {
2299
2380
  try {
2300
2381
  const pathname = new URL(rawUrl ?? "/", "http://localhost").pathname;
2301
2382
  const prefix = `${CLAUDE_PROJECTION_PATH}/`;
@@ -2303,21 +2384,21 @@ function sessionIdFromUrl(rawUrl) {
2303
2384
  const encoded = pathname.slice(prefix.length);
2304
2385
  if (encoded.length === 0 || encoded.includes("/")) return void 0;
2305
2386
  const sessionId = decodeURIComponent(encoded);
2306
- if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS) return void 0;
2387
+ if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS$2) return void 0;
2307
2388
  return sessionId;
2308
2389
  } catch {
2309
2390
  return;
2310
2391
  }
2311
2392
  }
2312
2393
  /** Register the browser-readable, credential-free sidecar projection endpoint. */
2313
- function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSession = () => [], repositoryForSession = async () => void 0) {
2394
+ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSession = () => [], repositoryForSession = async () => void 0, reviewCommentsForSession = () => []) {
2314
2395
  ctx.effect(() => ctx.webServer.register({
2315
2396
  kind: "prefix",
2316
2397
  path: CLAUDE_PROJECTION_PATH,
2317
2398
  handler: async (req, res) => {
2318
2399
  if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
2319
2400
  if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
2320
- const sessionId = sessionIdFromUrl(req.url);
2401
+ const sessionId = sessionIdFromUrl$1(req.url);
2321
2402
  if (sessionId === void 0) return json(res, 400, { error: "invalid session id" });
2322
2403
  try {
2323
2404
  const projection = await sidecar.read(sessionId);
@@ -2331,7 +2412,8 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
2331
2412
  activities: projection.activities,
2332
2413
  ...projection.contextUsage === void 0 ? {} : { contextUsage: projection.contextUsage },
2333
2414
  ...projection.tasks === void 0 ? {} : { tasks: projection.tasks },
2334
- ...repository === void 0 ? {} : { repository }
2415
+ ...repository === void 0 ? {} : { repository },
2416
+ reviewComments: owned ? reviewCommentsForSession(sessionId) : []
2335
2417
  });
2336
2418
  } catch {
2337
2419
  return json(res, 500, { error: "projection unavailable" });
@@ -2341,16 +2423,17 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
2341
2423
  }
2342
2424
  //#endregion
2343
2425
  //#region src/repository-status.ts
2344
- const MAX_OUTPUT_BYTES$1 = 65536;
2426
+ const MAX_OUTPUT_BYTES$2 = 65536;
2345
2427
  const MAX_DIFF_BYTES = 262144;
2346
- const GIT_TIMEOUT_MS$1 = 5e3;
2428
+ const MAX_UNTRACKED_DIFFS = 50;
2429
+ const GIT_TIMEOUT_MS$2 = 5e3;
2347
2430
  const GH_TIMEOUT_MS = 8e3;
2348
2431
  const CACHE_TTL_MS = 5e3;
2349
2432
  const MAX_TEXT_CHARS = 1024;
2350
2433
  function bounded(value) {
2351
2434
  return value.trim().slice(0, MAX_TEXT_CHARS);
2352
2435
  }
2353
- async function collect$1(handle) {
2436
+ async function collect$2(handle) {
2354
2437
  const outcome = await handle.done;
2355
2438
  const stdout = handle.collected.stdout?.readFrom(0);
2356
2439
  return {
@@ -2359,15 +2442,15 @@ async function collect$1(handle) {
2359
2442
  lossy: stdout?.lossy === true
2360
2443
  };
2361
2444
  }
2362
- async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$1) {
2445
+ async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$2) {
2363
2446
  const signal = AbortSignal.timeout(timeoutMs);
2364
- return collect$1(runtime.spawn({
2447
+ return collect$2(runtime.spawn({
2365
2448
  argv: [executable, ...args],
2366
2449
  cwd,
2367
2450
  stdio: {
2368
2451
  stdin: "ignore",
2369
2452
  stdout: { maxBytes },
2370
- stderr: { maxBytes: MAX_OUTPUT_BYTES$1 }
2453
+ stderr: { maxBytes: MAX_OUTPUT_BYTES$2 }
2371
2454
  },
2372
2455
  graceMs: 1e3,
2373
2456
  signal,
@@ -2381,6 +2464,9 @@ function parseGitStatus(output) {
2381
2464
  let branch;
2382
2465
  let detached = false;
2383
2466
  let dirty = false;
2467
+ let upstream = false;
2468
+ let ahead;
2469
+ let behind;
2384
2470
  for (const line of output.split(/\r?\n/u)) {
2385
2471
  if (line.startsWith("# branch.head ")) {
2386
2472
  const head = bounded(line.slice(14));
@@ -2388,12 +2474,27 @@ function parseGitStatus(output) {
2388
2474
  else if (head.length > 0 && head !== "(unknown)") branch = head;
2389
2475
  continue;
2390
2476
  }
2477
+ if (line.startsWith("# branch.upstream ")) {
2478
+ upstream = true;
2479
+ continue;
2480
+ }
2481
+ if (line.startsWith("# branch.ab ")) {
2482
+ const counts = /^# branch\.ab \+(\d+) -(\d+)$/u.exec(line);
2483
+ if (counts !== null) {
2484
+ ahead = Number(counts[1]);
2485
+ behind = Number(counts[2]);
2486
+ }
2487
+ continue;
2488
+ }
2391
2489
  if (line.length > 0 && !line.startsWith("# ")) dirty = true;
2392
2490
  }
2393
2491
  return {
2394
2492
  ...branch === void 0 ? {} : { branch },
2395
2493
  detached,
2396
- dirty
2494
+ dirty,
2495
+ upstream,
2496
+ ...ahead === void 0 ? {} : { ahead },
2497
+ ...behind === void 0 ? {} : { behind }
2397
2498
  };
2398
2499
  }
2399
2500
  function parseDiffNumstat(value) {
@@ -2419,14 +2520,14 @@ function parseGitHubRemote(value) {
2419
2520
  if (match?.[1] === void 0 || match[2] === void 0) return void 0;
2420
2521
  return `${match[1]}/${match[2]}`;
2421
2522
  }
2422
- function record$1(value) {
2523
+ function record$3(value) {
2423
2524
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2424
2525
  }
2425
2526
  function aggregateChecks(value) {
2426
2527
  if (!Array.isArray(value) || value.length === 0) return "none";
2427
2528
  let pending = false;
2428
2529
  for (const item of value) {
2429
- const check = record$1(item);
2530
+ const check = record$3(item);
2430
2531
  if (check === void 0) continue;
2431
2532
  const conclusion = typeof check.conclusion === "string" ? check.conclusion.toUpperCase() : void 0;
2432
2533
  const status = typeof check.status === "string" ? check.status.toUpperCase() : void 0;
@@ -2450,7 +2551,7 @@ function reviewState(value) {
2450
2551
  return "none";
2451
2552
  }
2452
2553
  function parsePullRequest(value) {
2453
- const input = record$1(value);
2554
+ const input = record$3(value);
2454
2555
  if (input === void 0 || !Number.isSafeInteger(input.number) || Number(input.number) <= 0 || typeof input.title !== "string" || typeof input.url !== "string") return void 0;
2455
2556
  let url;
2456
2557
  try {
@@ -2471,8 +2572,9 @@ function parsePullRequest(value) {
2471
2572
  review: reviewState(input.reviewDecision),
2472
2573
  checks: aggregateChecks(input.statusCheckRollup),
2473
2574
  ...typeof input.mergeStateStatus === "string" ? { mergeState: bounded(input.mergeStateStatus) } : {},
2474
- ...typeof record$1(input.author)?.login === "string" ? { author: bounded(String(record$1(input.author)?.login)) } : {},
2575
+ ...typeof record$3(input.author)?.login === "string" ? { author: bounded(String(record$3(input.author)?.login)) } : {},
2475
2576
  ...typeof input.createdAt === "string" && Number.isFinite(Date.parse(input.createdAt)) ? { createdAt: new Date(input.createdAt).toISOString() } : {},
2577
+ ...typeof input.mergedAt === "string" && Number.isFinite(Date.parse(input.mergedAt)) ? { mergedAt: new Date(input.mergedAt).toISOString() } : {},
2476
2578
  ...typeof input.baseRefName === "string" && bounded(input.baseRefName).length > 0 ? { baseBranch: bounded(input.baseRefName) } : {}
2477
2579
  };
2478
2580
  }
@@ -2498,6 +2600,10 @@ var RepositoryStatusService = class {
2498
2600
  value.catch(() => this.#cache.delete(cwd));
2499
2601
  return value;
2500
2602
  }
2603
+ invalidate(cwd) {
2604
+ this.#cache.delete(cwd);
2605
+ this.#lastReady.delete(cwd);
2606
+ }
2501
2607
  dispose() {
2502
2608
  this.#cache.clear();
2503
2609
  this.#lastReady.clear();
@@ -2539,7 +2645,7 @@ var RepositoryStatusService = class {
2539
2645
  "--show-toplevel",
2540
2646
  "--absolute-git-dir",
2541
2647
  "--git-common-dir"
2542
- ], cwd, GIT_TIMEOUT_MS$1);
2648
+ ], cwd, GIT_TIMEOUT_MS$2);
2543
2649
  if (paths.exitCode !== 0) return {
2544
2650
  status: "not-repository",
2545
2651
  cwd: safeCwd
@@ -2556,8 +2662,8 @@ var RepositoryStatusService = class {
2556
2662
  "status",
2557
2663
  "--porcelain=v2",
2558
2664
  "--branch",
2559
- "--untracked-files=no"
2560
- ], cwd, GIT_TIMEOUT_MS$1);
2665
+ "--untracked-files=normal"
2666
+ ], cwd, GIT_TIMEOUT_MS$2);
2561
2667
  if (statusResult.exitCode !== 0) return {
2562
2668
  status: "unavailable",
2563
2669
  cwd: safeCwd
@@ -2567,7 +2673,7 @@ var RepositoryStatusService = class {
2567
2673
  "remote",
2568
2674
  "get-url",
2569
2675
  "origin"
2570
- ], cwd, GIT_TIMEOUT_MS$1);
2676
+ ], cwd, GIT_TIMEOUT_MS$2);
2571
2677
  const remote = remoteResult.exitCode === 0 ? parseGitHubRemote(remoteResult.stdout) : void 0;
2572
2678
  const pullRequest = status.branch === void 0 || remote === void 0 ? void 0 : await this.#pullRequest(cwd, remote, status.branch);
2573
2679
  const diffBase = pullRequest?.baseBranch === void 0 ? "HEAD" : await this.#mergeBase(cwd, git, pullRequest.baseBranch) ?? "HEAD";
@@ -2600,7 +2706,7 @@ var RepositoryStatusService = class {
2600
2706
  "merge-base",
2601
2707
  "HEAD",
2602
2708
  `refs/remotes/origin/${baseBranch}`
2603
- ], cwd, GIT_TIMEOUT_MS$1);
2709
+ ], cwd, GIT_TIMEOUT_MS$2);
2604
2710
  if (result.exitCode !== 0 || result.lossy) return void 0;
2605
2711
  const oid = bounded(result.stdout);
2606
2712
  return /^[0-9a-f]{40}$/iu.test(oid) ? oid : void 0;
@@ -2616,7 +2722,7 @@ var RepositoryStatusService = class {
2616
2722
  "--numstat",
2617
2723
  base,
2618
2724
  "--"
2619
- ], cwd, GIT_TIMEOUT_MS$1);
2725
+ ], cwd, GIT_TIMEOUT_MS$2);
2620
2726
  if (numstat.exitCode !== 0 || numstat.lossy) return void 0;
2621
2727
  const summary = parseDiffNumstat(numstat.stdout);
2622
2728
  const patch = await run(this.#runtime, git, [
@@ -2626,20 +2732,70 @@ var RepositoryStatusService = class {
2626
2732
  "--unified=3",
2627
2733
  base,
2628
2734
  "--"
2629
- ], cwd, GIT_TIMEOUT_MS$1, MAX_DIFF_BYTES);
2735
+ ], cwd, GIT_TIMEOUT_MS$2, MAX_DIFF_BYTES);
2630
2736
  if (patch.exitCode !== 0) return {
2631
2737
  ...summary,
2632
2738
  truncated: true
2633
2739
  };
2740
+ const untracked = await this.#untrackedDiff(cwd, git);
2741
+ const combinedPatch = `${patch.stdout}${untracked.patch}`;
2634
2742
  return {
2635
- ...summary,
2636
- ...patch.lossy ? {} : { patch: patch.stdout },
2637
- truncated: patch.lossy
2743
+ additions: summary.additions + untracked.additions,
2744
+ deletions: summary.deletions,
2745
+ files: summary.files + untracked.files,
2746
+ ...patch.lossy ? {} : { patch: combinedPatch.slice(0, MAX_DIFF_BYTES) },
2747
+ truncated: patch.lossy || untracked.truncated || combinedPatch.length > MAX_DIFF_BYTES
2638
2748
  };
2639
2749
  } catch {
2640
2750
  return;
2641
2751
  }
2642
2752
  }
2753
+ async #untrackedDiff(cwd, git) {
2754
+ const listed = await run(this.#runtime, git, [
2755
+ "ls-files",
2756
+ "--others",
2757
+ "--exclude-standard",
2758
+ "-z"
2759
+ ], cwd, GIT_TIMEOUT_MS$2);
2760
+ if (listed.exitCode !== 0 || listed.lossy) return {
2761
+ additions: 0,
2762
+ files: 0,
2763
+ patch: "",
2764
+ truncated: listed.lossy
2765
+ };
2766
+ const paths = listed.stdout.split("\0").filter((path) => path.length > 0);
2767
+ let additions = 0;
2768
+ let patch = "";
2769
+ let truncated = paths.length > MAX_UNTRACKED_DIFFS;
2770
+ for (const path of paths.slice(0, MAX_UNTRACKED_DIFFS)) {
2771
+ const result = await run(this.#runtime, git, [
2772
+ "diff",
2773
+ "--no-ext-diff",
2774
+ "--no-color",
2775
+ "--unified=3",
2776
+ "--numstat",
2777
+ "--patch",
2778
+ "--no-index",
2779
+ "--",
2780
+ "/dev/null",
2781
+ path
2782
+ ], cwd, GIT_TIMEOUT_MS$2, MAX_DIFF_BYTES);
2783
+ if (result.exitCode !== 0 && result.exitCode !== 1) {
2784
+ truncated = true;
2785
+ continue;
2786
+ }
2787
+ const start = result.stdout.indexOf("diff --git ");
2788
+ additions += parseDiffNumstat(start >= 0 ? result.stdout.slice(0, start) : result.stdout).additions;
2789
+ if (result.lossy) truncated = true;
2790
+ else if (start >= 0) patch += result.stdout.slice(start);
2791
+ }
2792
+ return {
2793
+ additions,
2794
+ files: paths.length,
2795
+ patch,
2796
+ truncated
2797
+ };
2798
+ }
2643
2799
  async #pullRequest(cwd, repository, branch) {
2644
2800
  const gh = await this.#gh();
2645
2801
  if (gh === void 0) return void 0;
@@ -2662,8 +2818,8 @@ var RepositoryStatusService = class {
2662
2818
  };
2663
2819
  //#endregion
2664
2820
  //#region src/repository-setup.ts
2665
- const MAX_OUTPUT_BYTES = 131072;
2666
- const GIT_TIMEOUT_MS = 1e4;
2821
+ const MAX_OUTPUT_BYTES$1 = 131072;
2822
+ const GIT_TIMEOUT_MS$1 = 1e4;
2667
2823
  const GIT_FETCH_TIMEOUT_MS = 6e4;
2668
2824
  const MAX_PATH_CHARS = 4096;
2669
2825
  const MAX_BRANCH_CHARS = 512;
@@ -2676,7 +2832,7 @@ var RepositorySetupError = class extends Error {
2676
2832
  this.code = code;
2677
2833
  }
2678
2834
  };
2679
- async function collect(handle) {
2835
+ async function collect$1(handle) {
2680
2836
  const outcome = await handle.done;
2681
2837
  const stdout = handle.collected.stdout?.readFrom(0);
2682
2838
  const stderr = handle.collected.stderr?.readFrom(0);
@@ -2969,14 +3125,14 @@ var RepositorySetupService = class {
2969
3125
  this.#gitPath ??= this.#runtime.resolveExecutable("git");
2970
3126
  return this.#gitPath;
2971
3127
  }
2972
- async #run(executable, args, cwd, timeoutMs = GIT_TIMEOUT_MS) {
2973
- return collect(this.#runtime.spawn({
3128
+ async #run(executable, args, cwd, timeoutMs = GIT_TIMEOUT_MS$1) {
3129
+ return collect$1(this.#runtime.spawn({
2974
3130
  argv: [executable, ...args],
2975
3131
  cwd,
2976
3132
  stdio: {
2977
3133
  stdin: "ignore",
2978
- stdout: { maxBytes: MAX_OUTPUT_BYTES },
2979
- stderr: { maxBytes: MAX_OUTPUT_BYTES }
3134
+ stdout: { maxBytes: MAX_OUTPUT_BYTES$1 },
3135
+ stderr: { maxBytes: MAX_OUTPUT_BYTES$1 }
2980
3136
  },
2981
3137
  graceMs: 1e3,
2982
3138
  signal: AbortSignal.timeout(timeoutMs),
@@ -3016,30 +3172,400 @@ var RepositorySetupService = class {
3016
3172
  }
3017
3173
  };
3018
3174
  //#endregion
3175
+ //#region src/repository-actions.ts
3176
+ const MAX_OUTPUT_BYTES = 262144;
3177
+ const MAX_PATCH_CHARS = 65536;
3178
+ const MAX_MESSAGE_CHARS = 512;
3179
+ const MAX_PR_TEXT_CHARS = 8192;
3180
+ const MAX_UNPUSHED_COMMITS = 20;
3181
+ const GIT_TIMEOUT_MS = 15e3;
3182
+ const REMOTE_TIMEOUT_MS = 6e4;
3183
+ const GENERATE_TIMEOUT_MS = 6e4;
3184
+ var RepositoryActionError = class extends Error {
3185
+ code;
3186
+ commit;
3187
+ constructor(code, message, commit) {
3188
+ super(message);
3189
+ this.name = "RepositoryActionError";
3190
+ this.code = code;
3191
+ if (commit !== void 0) this.commit = commit;
3192
+ }
3193
+ };
3194
+ async function collect(handle) {
3195
+ const outcome = await handle.done;
3196
+ const stdout = handle.collected.stdout?.readFrom(0);
3197
+ const stderr = handle.collected.stderr?.readFrom(0);
3198
+ return {
3199
+ exitCode: outcome.exitCode,
3200
+ stdout: stdout?.text ?? "",
3201
+ stderr: stderr?.text ?? "",
3202
+ lossy: stdout?.lossy === true || stderr?.lossy === true
3203
+ };
3204
+ }
3205
+ function safeText(value, maximum, label) {
3206
+ const text = value.trim();
3207
+ if (text.length === 0 || text.length > maximum || /[\0\r]/u.test(text)) throw new RepositoryActionError("invalid-request", `${label} is invalid.`);
3208
+ return text;
3209
+ }
3210
+ function isProtectedWarpPath(path) {
3211
+ return basename(path.replaceAll("\\", "/")).toLocaleLowerCase("en-US") === "warp.md";
3212
+ }
3213
+ function parseRepositoryActionStatus(output) {
3214
+ const files = /* @__PURE__ */ new Map();
3215
+ const records = output.includes("\0") ? output.split("\0") : output.split(/\r?\n/u);
3216
+ for (let position = 0; position < records.length; position += 1) {
3217
+ const line = records[position] ?? "";
3218
+ if (line.length < 4) continue;
3219
+ const index = line[0] ?? " ";
3220
+ const worktree = line[1] ?? " ";
3221
+ let path = line.slice(3);
3222
+ const rename = path.lastIndexOf(" -> ");
3223
+ if (rename >= 0) path = path.slice(rename + 4);
3224
+ if (output.includes("\0") && (index === "R" || index === "C" || worktree === "R" || worktree === "C")) position += 1;
3225
+ if (path.length === 0 || path.includes("\0") || isProtectedWarpPath(path)) continue;
3226
+ files.set(path, {
3227
+ path,
3228
+ staged: index !== " " && index !== "?",
3229
+ unstaged: worktree !== " " && worktree !== "?",
3230
+ untracked: index === "?" && worktree === "?"
3231
+ });
3232
+ }
3233
+ return [...files.values()].sort((left, right) => left.path.localeCompare(right.path));
3234
+ }
3235
+ function fallbackCommitMessage(files) {
3236
+ if (files.length === 1) return `Update ${files[0]?.path ?? "repository files"}`;
3237
+ return `Update ${files.length} repository files`;
3238
+ }
3239
+ function normalizedGeneratedMessage(value, fallback) {
3240
+ const first = value.split(/\r?\n/u).map((line) => line.trim()).find(Boolean);
3241
+ if (first === void 0) return fallback;
3242
+ const message = first.replace(/^['"`]+|['"`]+$/gu, "").replace(/[\0\r\n]/gu, " ").trim();
3243
+ return message.length === 0 || message.length > 72 ? fallback : message;
3244
+ }
3245
+ function validPrUrl(value) {
3246
+ try {
3247
+ const url = new URL(value.trim());
3248
+ return url.protocol === "https:" && url.hostname === "github.com" ? url.href : void 0;
3249
+ } catch {
3250
+ return;
3251
+ }
3252
+ }
3253
+ function validPullRequestBody(value) {
3254
+ const body = value.trim();
3255
+ const match = /^Summary:\s+([^\r\n]+)\r?\n\r?\nChanges:\s*\r?\n([\s\S]+)$/u.exec(body);
3256
+ const summary = match?.[1]?.trim();
3257
+ const changes = match?.[2]?.trim();
3258
+ if (summary === void 0 || summary.length === 0 || changes === void 0 || changes.length === 0) return false;
3259
+ return !/^#{1,6}\s|^[A-Za-z][A-Za-z ]+:\s*$/mu.test(changes);
3260
+ }
3261
+ var RepositoryActionService = class {
3262
+ #runtime;
3263
+ #claudeExecutable;
3264
+ #invalidate;
3265
+ #gitExecutable;
3266
+ #ghExecutable;
3267
+ #pending = Promise.resolve();
3268
+ constructor(runtime, claudeExecutable, invalidate = () => {}) {
3269
+ this.#runtime = runtime;
3270
+ this.#claudeExecutable = claudeExecutable;
3271
+ this.#invalidate = invalidate;
3272
+ }
3273
+ preview(cwd) {
3274
+ return this.#preview(cwd);
3275
+ }
3276
+ async generateMessage(cwd, fingerprint) {
3277
+ const preview = await this.#preview(cwd);
3278
+ if (preview.fingerprint !== fingerprint) throw new RepositoryActionError("repository-changed", "Repository changes have changed. Refresh the commit panel.");
3279
+ const fallback = fallbackCommitMessage(preview.files);
3280
+ const prompt = [
3281
+ "Write one concise English git commit subject (imperative mood, maximum 72 characters).",
3282
+ "Return only the subject without quotes, markdown, body, or explanation.",
3283
+ `Files: ${preview.files.map((file) => file.path).join(", ")}`,
3284
+ `Diff:\n${preview.patch.slice(0, 24576)}`
3285
+ ].join("\n");
3286
+ try {
3287
+ const result = await this.#run(this.#claudeExecutable, [
3288
+ "-p",
3289
+ "--tools",
3290
+ "",
3291
+ "--output-format",
3292
+ "text",
3293
+ prompt
3294
+ ], preview.root, GENERATE_TIMEOUT_MS);
3295
+ return result.exitCode === 0 && !result.lossy ? normalizedGeneratedMessage(result.stdout, fallback) : fallback;
3296
+ } catch {
3297
+ return fallback;
3298
+ }
3299
+ }
3300
+ execute(cwd, request) {
3301
+ const operation = this.#pending.then(() => this.#execute(cwd, request));
3302
+ this.#pending = operation.then(() => void 0, () => void 0);
3303
+ return operation;
3304
+ }
3305
+ async #execute(cwd, request) {
3306
+ const before = await this.#preview(cwd);
3307
+ if (before.fingerprint !== request.fingerprint) throw new RepositoryActionError("repository-changed", "Repository changes have changed. Refresh the commit panel.");
3308
+ if (request.action === "push") {
3309
+ const git = await this.#git();
3310
+ try {
3311
+ await this.#push(git, before.root, before.branch);
3312
+ } catch (error) {
3313
+ throw new RepositoryActionError("push-failed", error instanceof Error ? error.message : "Git push failed.");
3314
+ }
3315
+ this.#invalidate(before.root);
3316
+ return {
3317
+ commit: before.head,
3318
+ pushed: true
3319
+ };
3320
+ }
3321
+ const message = safeText(request.message, MAX_MESSAGE_CHARS, "Commit message");
3322
+ if (before.files.length === 0 && request.action !== "create-pr") throw new RepositoryActionError("nothing-to-commit", "There are no changes to commit.");
3323
+ const git = await this.#git();
3324
+ let oid = before.head;
3325
+ if (before.files.length > 0) {
3326
+ await this.#rejectStagedWarp(git, before.root);
3327
+ if (request.includeUnstaged) {
3328
+ const paths = before.files.filter((file) => file.unstaged || file.untracked).map((file) => file.path);
3329
+ if (paths.length > 0) await this.#mustRun(git, [
3330
+ "add",
3331
+ "--",
3332
+ ...paths
3333
+ ], before.root, GIT_TIMEOUT_MS, "stage-failed", "Changes could not be staged.");
3334
+ }
3335
+ await this.#rejectStagedWarp(git, before.root);
3336
+ const staged = await this.#run(git, [
3337
+ "diff",
3338
+ "--cached",
3339
+ "--quiet",
3340
+ "--exit-code",
3341
+ "--"
3342
+ ], before.root, GIT_TIMEOUT_MS);
3343
+ if (staged.exitCode === 0) throw new RepositoryActionError("nothing-to-commit", "There are no staged changes to commit.");
3344
+ if (staged.exitCode !== 1) throw new RepositoryActionError("repository-unavailable", "The staged changes could not be verified.");
3345
+ await this.#mustRun(git, [
3346
+ "commit",
3347
+ "-m",
3348
+ message,
3349
+ "--"
3350
+ ], before.root, GIT_TIMEOUT_MS, "commit-failed", "Git commit failed.");
3351
+ oid = (await this.#mustRun(git, ["rev-parse", "HEAD"], before.root, GIT_TIMEOUT_MS, "commit-failed", "The new commit could not be verified.")).stdout.trim();
3352
+ this.#invalidate(before.root);
3353
+ if (request.action === "commit") return {
3354
+ commit: oid,
3355
+ pushed: false
3356
+ };
3357
+ }
3358
+ try {
3359
+ await this.#push(git, before.root, before.branch);
3360
+ } catch (error) {
3361
+ throw new RepositoryActionError("push-failed", error instanceof Error ? error.message : "Git push failed.", oid);
3362
+ }
3363
+ this.#invalidate(before.root);
3364
+ if (request.action === "commit-push") return {
3365
+ commit: oid,
3366
+ pushed: true
3367
+ };
3368
+ const title = safeText(request.prTitle ?? message, 256, "Pull request title");
3369
+ const body = safeText(request.prBody ?? "", MAX_PR_TEXT_CHARS, "Pull request description");
3370
+ if (!validPullRequestBody(body)) throw new RepositoryActionError("invalid-pr-description", "Pull request description must contain only Summary and Changes sections.", oid);
3371
+ let gh;
3372
+ try {
3373
+ gh = await this.#gh();
3374
+ } catch (error) {
3375
+ throw new RepositoryActionError("gh-unavailable", error instanceof Error ? error.message : "GitHub CLI is unavailable.", oid);
3376
+ }
3377
+ const args = [
3378
+ "pr",
3379
+ "create",
3380
+ "--title",
3381
+ title,
3382
+ "--body",
3383
+ body
3384
+ ];
3385
+ if (request.draft !== false) args.push("--draft");
3386
+ if (request.baseBranch !== void 0) args.push("--base", safeText(request.baseBranch, 512, "Base branch"));
3387
+ const created = await this.#run(gh, args, before.root, REMOTE_TIMEOUT_MS);
3388
+ const url = created.exitCode === 0 && !created.lossy ? validPrUrl(created.stdout) : void 0;
3389
+ if (url === void 0) throw new RepositoryActionError("pr-failed", "The pull request could not be created.", oid);
3390
+ return {
3391
+ commit: oid,
3392
+ pushed: true,
3393
+ pullRequestUrl: url
3394
+ };
3395
+ }
3396
+ async #preview(cwd) {
3397
+ const git = await this.#git();
3398
+ const root = (await this.#mustRun(git, [
3399
+ "rev-parse",
3400
+ "--path-format=absolute",
3401
+ "--show-toplevel"
3402
+ ], cwd, GIT_TIMEOUT_MS, "not-repository", "The session directory is not a Git repository.")).stdout.trim();
3403
+ const [branchResult, headResult, statusResult, stagedPatch, unstagedPatch] = await Promise.all([
3404
+ this.#run(git, [
3405
+ "symbolic-ref",
3406
+ "--quiet",
3407
+ "--short",
3408
+ "HEAD"
3409
+ ], root, GIT_TIMEOUT_MS),
3410
+ this.#run(git, ["rev-parse", "HEAD"], root, GIT_TIMEOUT_MS),
3411
+ this.#run(git, [
3412
+ "status",
3413
+ "--porcelain=v1",
3414
+ "-z",
3415
+ "--untracked-files=all"
3416
+ ], root, GIT_TIMEOUT_MS),
3417
+ this.#run(git, [
3418
+ "diff",
3419
+ "--cached",
3420
+ "--no-ext-diff",
3421
+ "--no-color",
3422
+ "--unified=3",
3423
+ "--",
3424
+ ":(exclude)WARP.md",
3425
+ ":(exclude)**/WARP.md"
3426
+ ], root, GIT_TIMEOUT_MS, MAX_OUTPUT_BYTES),
3427
+ this.#run(git, [
3428
+ "diff",
3429
+ "--no-ext-diff",
3430
+ "--no-color",
3431
+ "--unified=3",
3432
+ "--",
3433
+ ":(exclude)WARP.md",
3434
+ ":(exclude)**/WARP.md"
3435
+ ], root, GIT_TIMEOUT_MS, MAX_OUTPUT_BYTES)
3436
+ ]);
3437
+ if (branchResult.exitCode !== 0) throw new RepositoryActionError("detached-head", "A detached HEAD cannot be committed from this panel.");
3438
+ if (headResult.exitCode !== 0 || statusResult.exitCode !== 0 || statusResult.lossy) throw new RepositoryActionError("repository-unavailable", "Repository state is unavailable.");
3439
+ const files = parseRepositoryActionStatus(statusResult.stdout);
3440
+ const patch = `${stagedPatch.stdout}${stagedPatch.stdout.length > 0 && unstagedPatch.stdout.length > 0 ? "\n" : ""}${unstagedPatch.stdout}`.slice(0, MAX_PATCH_CHARS);
3441
+ const branch = branchResult.stdout.trim();
3442
+ const head = headResult.stdout.trim();
3443
+ const fingerprint = createHash("sha256").update([
3444
+ head,
3445
+ branch,
3446
+ statusResult.stdout,
3447
+ stagedPatch.stdout,
3448
+ unstagedPatch.stdout
3449
+ ].join("\0")).digest("hex");
3450
+ const upstreamResult = await this.#run(git, [
3451
+ "rev-parse",
3452
+ "--abbrev-ref",
3453
+ "--symbolic-full-name",
3454
+ "@{upstream}"
3455
+ ], root, GIT_TIMEOUT_MS);
3456
+ const upstream = upstreamResult.exitCode === 0 && !upstreamResult.lossy ? upstreamResult.stdout.trim() : void 0;
3457
+ const logResult = await this.#run(git, [
3458
+ "log",
3459
+ "--format=%H%x09%s",
3460
+ `-n`,
3461
+ String(21),
3462
+ upstream === void 0 ? "HEAD" : "@{upstream}..HEAD",
3463
+ "--"
3464
+ ], root, GIT_TIMEOUT_MS);
3465
+ const commitLines = logResult.exitCode === 0 && !logResult.lossy ? logResult.stdout.split(/\r?\n/u).filter((line) => line.includes(" ")) : [];
3466
+ const unpushedCommits = commitLines.slice(0, MAX_UNPUSHED_COMMITS).flatMap((line) => {
3467
+ const tab = line.indexOf(" ");
3468
+ const hash = line.slice(0, tab);
3469
+ return /^[0-9a-f]{40}$/iu.test(hash) ? [{
3470
+ hash,
3471
+ subject: line.slice(tab + 1).slice(0, 140)
3472
+ }] : [];
3473
+ });
3474
+ return {
3475
+ root,
3476
+ branch,
3477
+ head,
3478
+ fingerprint,
3479
+ files,
3480
+ patch,
3481
+ truncated: stagedPatch.lossy || unstagedPatch.lossy || stagedPatch.stdout.length + unstagedPatch.stdout.length > MAX_PATCH_CHARS,
3482
+ hasStaged: files.some((file) => file.staged),
3483
+ hasUnstaged: files.some((file) => file.unstaged),
3484
+ hasUntracked: files.some((file) => file.untracked),
3485
+ ...upstream === void 0 ? {} : { upstream },
3486
+ unpushedCommits,
3487
+ unpushedTruncated: commitLines.length > MAX_UNPUSHED_COMMITS
3488
+ };
3489
+ }
3490
+ async #rejectStagedWarp(git, cwd) {
3491
+ const staged = await this.#run(git, [
3492
+ "diff",
3493
+ "--cached",
3494
+ "--name-only",
3495
+ "--"
3496
+ ], cwd, GIT_TIMEOUT_MS);
3497
+ if (staged.exitCode !== 0 || staged.lossy) throw new RepositoryActionError("repository-unavailable", "Staged files could not be verified.");
3498
+ 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.");
3499
+ }
3500
+ async #push(git, cwd, branch) {
3501
+ const args = (await this.#run(git, [
3502
+ "rev-parse",
3503
+ "--abbrev-ref",
3504
+ "--symbolic-full-name",
3505
+ "@{upstream}"
3506
+ ], cwd, GIT_TIMEOUT_MS)).exitCode === 0 ? ["push"] : [
3507
+ "push",
3508
+ "--set-upstream",
3509
+ "origin",
3510
+ branch
3511
+ ];
3512
+ await this.#mustRun(git, args, cwd, REMOTE_TIMEOUT_MS, "push-failed", "Git push failed.");
3513
+ }
3514
+ #git() {
3515
+ this.#gitExecutable ??= this.#runtime.resolveExecutable("git");
3516
+ return this.#gitExecutable;
3517
+ }
3518
+ #gh() {
3519
+ this.#ghExecutable ??= this.#runtime.resolveExecutable("gh").catch(() => {
3520
+ throw new RepositoryActionError("gh-unavailable", "GitHub CLI is unavailable.");
3521
+ });
3522
+ return this.#ghExecutable;
3523
+ }
3524
+ async #mustRun(executable, args, cwd, timeoutMs, code, message) {
3525
+ const result = await this.#run(executable, args, cwd, timeoutMs);
3526
+ if (result.exitCode !== 0 || result.lossy) throw new RepositoryActionError(code, message);
3527
+ return result;
3528
+ }
3529
+ #run(executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES) {
3530
+ return collect(this.#runtime.spawn({
3531
+ argv: [executable, ...args],
3532
+ cwd,
3533
+ stdio: {
3534
+ stdin: "ignore",
3535
+ stdout: { maxBytes },
3536
+ stderr: { maxBytes: MAX_OUTPUT_BYTES }
3537
+ },
3538
+ graceMs: 1e3,
3539
+ signal: AbortSignal.timeout(timeoutMs),
3540
+ env: {}
3541
+ }));
3542
+ }
3543
+ };
3544
+ //#endregion
3019
3545
  //#region src/repository-setup-routes.ts
3020
- const MAX_BODY_BYTES = 16384;
3021
- function record(value) {
3546
+ const MAX_BODY_BYTES$2 = 16384;
3547
+ function record$2(value) {
3022
3548
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3023
3549
  }
3024
- async function readJson(req) {
3550
+ async function readJson$2(req) {
3025
3551
  const chunks = [];
3026
3552
  let size = 0;
3027
3553
  for await (const chunk of req) {
3028
3554
  const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
3029
3555
  size += buffer.length;
3030
- if (size > MAX_BODY_BYTES) throw new RepositorySetupError("body-too-large", "The request body is too large.");
3556
+ if (size > MAX_BODY_BYTES$2) throw new RepositorySetupError("body-too-large", "The request body is too large.");
3031
3557
  chunks.push(buffer);
3032
3558
  }
3033
- const value = record(JSON.parse(Buffer.concat(chunks).toString("utf8")));
3559
+ const value = record$2(JSON.parse(Buffer.concat(chunks).toString("utf8")));
3034
3560
  if (value === void 0) throw new RepositorySetupError("invalid-request", "The request body is invalid.");
3035
3561
  return value;
3036
3562
  }
3037
- function string(input, key) {
3563
+ function string$1(input, key) {
3038
3564
  const value = input[key];
3039
3565
  if (typeof value !== "string") throw new RepositorySetupError("invalid-request", `The ${key} field is required.`);
3040
3566
  return value;
3041
3567
  }
3042
- function optionalString(input, key) {
3568
+ function optionalString$1(input, key) {
3043
3569
  const value = input[key];
3044
3570
  if (value === void 0) return void 0;
3045
3571
  if (typeof value !== "string") throw new RepositorySetupError("invalid-request", `The ${key} field must be a string.`);
@@ -3062,7 +3588,7 @@ async function streamSetup(res, service, input) {
3062
3588
  try {
3063
3589
  ndjson(res, {
3064
3590
  type: "complete",
3065
- result: await service.setup(string(input, "cwd"), string(input, "branch"), input.worktree, optionalString(input, "branchName"), (stage) => {
3591
+ result: await service.setup(string$1(input, "cwd"), string$1(input, "branch"), input.worktree, optionalString$1(input, "branchName"), (stage) => {
3066
3592
  if (!ended) ndjson(res, {
3067
3593
  type: "progress",
3068
3594
  stage
@@ -3098,15 +3624,15 @@ function registerRepositorySetupRoute(ctx, service) {
3098
3624
  }
3099
3625
  if (pathname === "/plugins/dsh-claude/repository/setup") {
3100
3626
  if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
3101
- const input = await readJson(req);
3627
+ const input = await readJson$2(req);
3102
3628
  if (typeof input.worktree !== "boolean") throw new RepositorySetupError("invalid-request", "The worktree field is required.");
3103
3629
  await streamSetup(res, service, input);
3104
3630
  return;
3105
3631
  }
3106
3632
  if (pathname === `/plugins/dsh-claude/repository/setup/bind`) {
3107
3633
  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"));
3634
+ const input = await readJson$2(req);
3635
+ await service.bindLease(string$1(input, "leaseId"), string$1(input, "sessionId"));
3110
3636
  return json(res, 200, { ok: true });
3111
3637
  }
3112
3638
  return json(res, 404, { error: "not found" });
@@ -3122,6 +3648,168 @@ function registerRepositorySetupRoute(ctx, service) {
3122
3648
  }), "dsh-claude: repository setup route");
3123
3649
  }
3124
3650
  //#endregion
3651
+ //#region src/repository-action-routes.ts
3652
+ const MAX_BODY_BYTES$1 = 16384;
3653
+ const MAX_SESSION_ID_CHARS$1 = 1024;
3654
+ const ACTIONS = /* @__PURE__ */ new Set([
3655
+ "commit",
3656
+ "commit-push",
3657
+ "push",
3658
+ "create-pr"
3659
+ ]);
3660
+ function record$1(value) {
3661
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3662
+ }
3663
+ async function readJson$1(req) {
3664
+ const chunks = [];
3665
+ let size = 0;
3666
+ for await (const chunk of req) {
3667
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
3668
+ size += buffer.length;
3669
+ if (size > MAX_BODY_BYTES$1) throw new RepositoryActionError("body-too-large", "The request body is too large.");
3670
+ chunks.push(buffer);
3671
+ }
3672
+ const value = record$1(JSON.parse(Buffer.concat(chunks).toString("utf8")));
3673
+ if (value === void 0) throw new RepositoryActionError("invalid-request", "The request body is invalid.");
3674
+ return value;
3675
+ }
3676
+ function sessionId(url) {
3677
+ const value = url.searchParams.get("sessionId");
3678
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS$1) throw new RepositoryActionError("invalid-session", "The session is invalid.");
3679
+ return value;
3680
+ }
3681
+ function string(input, key) {
3682
+ const value = input[key];
3683
+ if (typeof value !== "string") throw new RepositoryActionError("invalid-request", `The ${key} field is required.`);
3684
+ return value;
3685
+ }
3686
+ function optionalString(input, key) {
3687
+ const value = input[key];
3688
+ if (value === void 0) return void 0;
3689
+ if (typeof value !== "string") throw new RepositoryActionError("invalid-request", `The ${key} field must be a string.`);
3690
+ return value;
3691
+ }
3692
+ function actionRequest(input) {
3693
+ const action = input.action;
3694
+ if (typeof action !== "string" || !ACTIONS.has(action) || typeof input.includeUnstaged !== "boolean") throw new RepositoryActionError("invalid-request", "The repository action is invalid.");
3695
+ return {
3696
+ action,
3697
+ fingerprint: string(input, "fingerprint"),
3698
+ message: action === "push" ? optionalString(input, "message") ?? "" : string(input, "message"),
3699
+ includeUnstaged: input.includeUnstaged,
3700
+ ...optionalString(input, "prTitle") === void 0 ? {} : { prTitle: optionalString(input, "prTitle") },
3701
+ ...optionalString(input, "prBody") === void 0 ? {} : { prBody: optionalString(input, "prBody") },
3702
+ ...optionalString(input, "baseBranch") === void 0 ? {} : { baseBranch: optionalString(input, "baseBranch") },
3703
+ ...input.draft === void 0 ? {} : typeof input.draft === "boolean" ? { draft: input.draft } : (() => {
3704
+ throw new RepositoryActionError("invalid-request", "The draft field must be a boolean.");
3705
+ })()
3706
+ };
3707
+ }
3708
+ function registerRepositoryActionRoute(ctx, service, cwdForSession) {
3709
+ ctx.effect(() => ctx.webServer.register({
3710
+ kind: "prefix",
3711
+ path: CLAUDE_REPOSITORY_ACTION_PATH,
3712
+ handler: async (req, res) => {
3713
+ if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
3714
+ const url = new URL(req.url ?? "/", "http://localhost");
3715
+ try {
3716
+ const cwd = cwdForSession(sessionId(url));
3717
+ if (cwd === void 0) throw new RepositoryActionError("session-unavailable", "The Claude session is unavailable.");
3718
+ if (url.pathname === `/plugins/dsh-claude/repository/action/preview`) {
3719
+ if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
3720
+ return json(res, 200, await service.preview(cwd));
3721
+ }
3722
+ if (url.pathname === `/plugins/dsh-claude/repository/action/message`) {
3723
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
3724
+ const input = await readJson$1(req);
3725
+ return json(res, 200, { message: await service.generateMessage(cwd, string(input, "fingerprint")) });
3726
+ }
3727
+ if (url.pathname === "/plugins/dsh-claude/repository/action") {
3728
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
3729
+ return json(res, 200, await service.execute(cwd, actionRequest(await readJson$1(req))));
3730
+ }
3731
+ return json(res, 404, { error: "not found" });
3732
+ } catch (error) {
3733
+ if (error instanceof RepositoryActionError) return json(res, 409, {
3734
+ error: error.code,
3735
+ message: error.message,
3736
+ ...error.commit === void 0 ? {} : { commit: error.commit }
3737
+ });
3738
+ if (error instanceof SyntaxError) return json(res, 400, { error: "invalid-json" });
3739
+ return json(res, 500, {
3740
+ error: "repository-action-unavailable",
3741
+ message: "Repository action is unavailable."
3742
+ });
3743
+ }
3744
+ }
3745
+ }), "dsh-claude: repository action route");
3746
+ }
3747
+ //#endregion
3748
+ //#region src/review-comment-routes.ts
3749
+ const MAX_BODY_BYTES = 16384;
3750
+ const MAX_SESSION_ID_CHARS = 1024;
3751
+ function record(value) {
3752
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3753
+ }
3754
+ async function readJson(req) {
3755
+ const chunks = [];
3756
+ let size = 0;
3757
+ for await (const chunk of req) {
3758
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
3759
+ size += buffer.length;
3760
+ if (size > MAX_BODY_BYTES) throw new ReviewCommentError("body-too-large", "The request body is too large.");
3761
+ chunks.push(buffer);
3762
+ }
3763
+ const value = record(JSON.parse(Buffer.concat(chunks).toString("utf8")));
3764
+ if (value === void 0) throw new ReviewCommentError("invalid-request", "The request body is invalid.");
3765
+ return value;
3766
+ }
3767
+ function sessionIdFromUrl(url) {
3768
+ const value = url.searchParams.get("sessionId");
3769
+ if (value === null || value.length === 0 || value.length > MAX_SESSION_ID_CHARS) throw new ReviewCommentError("invalid-session", "The session is invalid.");
3770
+ return value;
3771
+ }
3772
+ function registerReviewCommentRoute(ctx, store, ownsSession) {
3773
+ ctx.effect(() => ctx.webServer.register({
3774
+ kind: "prefix",
3775
+ path: CLAUDE_REVIEW_COMMENT_PATH,
3776
+ handler: async (req, res) => {
3777
+ if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
3778
+ const url = new URL(req.url ?? "/", "http://localhost");
3779
+ try {
3780
+ const sessionId = sessionIdFromUrl(url);
3781
+ if (!ownsSession(sessionId)) throw new ReviewCommentError("session-unavailable", "The Claude session is unavailable.");
3782
+ if (url.pathname === "/plugins/dsh-claude/review-comments" && req.method === "POST") {
3783
+ const input = await readJson(req);
3784
+ return json(res, 200, { comment: store.add(sessionId, {
3785
+ path: input.path,
3786
+ line: input.line,
3787
+ side: input.side,
3788
+ text: input.text
3789
+ }) });
3790
+ }
3791
+ if (url.pathname === `/plugins/dsh-claude/review-comments/clear` && req.method === "POST") return json(res, 200, { removed: store.drain(sessionId).length });
3792
+ if (url.pathname === `/plugins/dsh-claude/review-comments/remove` && req.method === "POST") {
3793
+ const input = await readJson(req);
3794
+ if (typeof input.id !== "string" || input.id.length === 0 || input.id.length > 128) throw new ReviewCommentError("invalid-request", "The comment id is invalid.");
3795
+ return json(res, 200, { removed: store.remove(sessionId, input.id) });
3796
+ }
3797
+ return json(res, 404, { error: "not found" });
3798
+ } catch (error) {
3799
+ if (error instanceof ReviewCommentError) return json(res, 409, {
3800
+ error: error.code,
3801
+ message: error.message
3802
+ });
3803
+ if (error instanceof SyntaxError) return json(res, 400, { error: "invalid-json" });
3804
+ return json(res, 500, {
3805
+ error: "review-comment-unavailable",
3806
+ message: "Review comments are unavailable."
3807
+ });
3808
+ }
3809
+ }
3810
+ }), "dsh-claude: review comment route");
3811
+ }
3812
+ //#endregion
3125
3813
  //#region src/update-routes.ts
3126
3814
  const PLUGIN_PACKAGE_NAME = "@norman-else/dsh-claude";
3127
3815
  const UPDATE_TIMEOUT_MS = 3e4;
@@ -3752,6 +4440,7 @@ async function apply(ctx, config) {
3752
4440
  const sidecar = new ClaudeSidecarRepository();
3753
4441
  const repositoryStatus = new RepositoryStatusService(ctx.subprocess);
3754
4442
  const repositorySetup = new RepositorySetupService(ctx.subprocess, { branchPrefix: () => readWorktreeBranchPrefix() });
4443
+ const reviewComments = new ReviewCommentStore();
3755
4444
  const commandCatalogs = /* @__PURE__ */ new Map();
3756
4445
  const supervisor = new ClaudeSupervisor({
3757
4446
  runtime: ctx.subprocess,
@@ -3764,7 +4453,7 @@ async function apply(ctx, config) {
3764
4453
  let resolutionError;
3765
4454
  try {
3766
4455
  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)));
4456
+ ctx.llm.registerAdapter([...CLAUDE_CODE_PROVIDER_IDS], createClaudeCodeAdapter(supervisor, ctx.agents, ctx.attachments, (agent) => ctx.agentPresets.composedPreset(agent.ctx), (sessionId) => reviewComments.drain(sessionId)));
3768
4457
  ctx.effect(() => {
3769
4458
  const mounted = /* @__PURE__ */ new Map();
3770
4459
  const pending = /* @__PURE__ */ new Set();
@@ -3825,9 +4514,11 @@ async function apply(ctx, config) {
3825
4514
  resolutionError = error;
3826
4515
  }
3827
4516
  ctx.on("agent/disposed", async ({ agent }) => {
4517
+ reviewComments.disposeSession(agent.id);
3828
4518
  await supervisor.disposeSession(agent.id);
3829
4519
  await repositorySetup.cleanupSession(agent.id);
3830
4520
  });
4521
+ ctx.effect(() => () => reviewComments.dispose(), "dsh-claude: review comments store");
3831
4522
  ctx.effect(() => () => supervisor.dispose(), "dsh-claude: process supervisor");
3832
4523
  ctx.effect(() => () => repositoryStatus.dispose(), "dsh-claude: repository status cache");
3833
4524
  ctx.inject(["webServer"], (webCtx) => {
@@ -3836,15 +4527,22 @@ async function apply(ctx, config) {
3836
4527
  registerClaudeUpdateRoutes(webCtx, webCtx.subprocess, { ...typeof desktopActions?.requestRestart === "function" ? { requestRestart: desktopActions.requestRestart.bind(desktopActions) } : {} });
3837
4528
  registerClaudeGlobalSettingsRoute(webCtx);
3838
4529
  registerRepositorySetupRoute(webCtx, repositorySetup);
3839
- registerClaudeProjectionRoute(webCtx, sidecar, (sessionId) => {
4530
+ registerRepositoryActionRoute(webCtx, new RepositoryActionService(webCtx.subprocess, supervisorConfig.executablePath, (cwd) => repositoryStatus.invalidate(cwd)), (sessionId) => {
4531
+ const agent = webCtx.agents.get(sessionId);
4532
+ if (agent === void 0 || webCtx.agentPresets.composedPreset(agent.ctx) !== "claude") return void 0;
4533
+ return agent.session.header.cwd;
4534
+ });
4535
+ const ownsClaudeSession = (sessionId) => {
3840
4536
  const agent = webCtx.agents.get(sessionId);
3841
4537
  return agent !== void 0 && webCtx.agentPresets.composedPreset(agent.ctx) === "claude";
3842
- }, (sessionId) => commandCatalogs.get(sessionId) ?? [], async (sessionId) => {
4538
+ };
4539
+ registerReviewCommentRoute(webCtx, reviewComments, ownsClaudeSession);
4540
+ registerClaudeProjectionRoute(webCtx, sidecar, ownsClaudeSession, (sessionId) => commandCatalogs.get(sessionId) ?? [], async (sessionId) => {
3843
4541
  const agent = webCtx.agents.get(sessionId);
3844
4542
  if (agent === void 0 || webCtx.agentPresets.composedPreset(agent.ctx) !== "claude") return void 0;
3845
4543
  const cwd = agent.session.header.cwd;
3846
4544
  return cwd === void 0 ? void 0 : repositoryStatus.inspect(cwd);
3847
- });
4545
+ }, (sessionId) => reviewComments.list(sessionId));
3848
4546
  });
3849
4547
  }
3850
4548
  //#endregion