@norman-else/dsh-claude 0.1.18 → 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 { _ as CLAUDE_PROJECTION_PATH, a as latestClaudeTasks, 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_UPDATE_CHECK_PATH, x as TASK_TOOL_NAMES, y as CLAUDE_UPDATE_PATH } from "./events-6xTApIQw.mjs";
2
- import { a as projectClaudeCommands, i as CLAUDE_COMMANDS_SERVICE, r as dynamicPresenterDefinition, t as CLAUDE_PRESENTER_NAMES } from "./presenters-BWXp9d3p.mjs";
3
- import { a as resolveClaudeExecutable, n as ensureManagedPreset, o as runClaudeDoctor, t as ManagedPresetConflictError } from "./preset-installer-B0NrA4Hi.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
- import { dirname, extname, join, resolve } from "node:path";
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$1(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$1(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$1(value);
36
- if (input === void 0 || !string$1(input.claudeSessionId, 512) || !string$1(input.sdkVersion, 128) || !string$1(input.cwd, 4096) || input.cliVersion !== void 0 && !string$1(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$1(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$1(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$1(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$1(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(value) {
529
+ function record$4(value) {
529
530
  return value !== null && typeof value === "object" ? value : void 0;
530
531
  }
531
- function string(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(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(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(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(item);
565
+ const block = record$4(item);
565
566
  if (block === void 0) continue;
566
567
  if (block.type === "text") {
567
- const text = string(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(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(block.id);
583
- const toolName = string(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(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(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(item);
606
+ const block = record$4(item);
606
607
  if (block?.type !== "tool_result") continue;
607
- const toolUseId = string(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(message.subtype);
621
+ const subtype = string$2(message.subtype);
621
622
  if (subtype === "init") {
622
- const sessionId = string(message.session_id);
623
- const cliVersion = string(message.claude_code_version);
624
- const cwd = string(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(message.tool_use_id);
654
- const toolName = string(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(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(message.task_id);
664
- const description = string(message.description);
665
- const subagentType = string(message.subagent_type);
666
- const taskType = string(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(message.task_id);
682
- const description = string(message.description);
683
- const summary = string(message.summary);
684
- const subagentType = string(message.subagent_type);
685
- const lastToolName = string(message.last_tool_name);
686
- const usage = taskUsageOf(record(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(message.patch);
703
- const status = string(patch?.status);
704
- const taskId = string(message.task_id);
705
- const description = string(patch?.description);
706
- const error = string(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(message.task_id);
723
- const summary = string(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(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(item);
741
- const taskId = string(entry?.task_id);
742
- const description = string(entry?.description);
743
- const taskType = string(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(message.content) ?? string(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(value.event);
784
- const parentToolUseId = string(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(event.delta);
787
+ const delta = record$4(event.delta);
787
788
  if (delta?.type === "text_delta") {
788
- const text = string(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(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(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(value.terminal_reason);
820
- const userMessageUuid = string(value.user_message_uuid);
821
- const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record(item)).filter((item) => item !== void 0).map((item) => {
822
- const toolName = string(item.tool_name);
823
- const toolUseId = string(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(record(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 {
@@ -2033,7 +2156,7 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
2033
2156
  inputModalities: ["text", "image"]
2034
2157
  }));
2035
2158
  }
2036
- async resolveModel(provider, model) {
2159
+ async resolveModel(provider, model, _signal) {
2037
2160
  const known = MODELS.find((item) => item.id === model);
2038
2161
  const contextWindow = this.#supervisor.contextWindow(model) ?? (known !== void 0 && "contextWindow" in known ? known.contextWindow : void 0);
2039
2162
  return {
@@ -2050,6 +2173,12 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
2050
2173
  })) }
2051
2174
  };
2052
2175
  }
2176
+ async prepareCall(provider, model, signal) {
2177
+ return {
2178
+ model: await this.resolveModel(provider, model, signal),
2179
+ stream: (options) => this.stream(options)
2180
+ };
2181
+ }
2053
2182
  async *stream(options) {
2054
2183
  if (options.purpose !== void 0) throw new Error(`dsh-claude: auxiliary ${options.purpose} calls are not routed into the Claude session`);
2055
2184
  const agent = resolveAgent(this.#agents, options);
@@ -2074,42 +2203,20 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
2074
2203
  }
2075
2204
  const events = await this.#supervisor.runTurn({
2076
2205
  agent,
2077
- prompt,
2206
+ prompt: injectReviewComments(prompt, this.#drainReviewComments(agent.id)),
2078
2207
  model: options.model,
2079
2208
  ...thinkingMode === void 0 ? {} : { thinkingMode },
2080
2209
  ...options.signal === void 0 ? {} : { signal: options.signal }
2081
2210
  });
2082
- let text = "";
2083
2211
  let pendingUsage;
2084
2212
  let completed = false;
2085
- let blockIndex = 0;
2086
2213
  try {
2087
- for await (const event of events) if (event.type === "text-delta") text += event.text;
2088
- else if (event.type === "usage") pendingUsage = tokenUsage(event.usage);
2089
- else {
2090
- if (text.length > 0) {
2091
- yield {
2092
- type: "block-start",
2093
- index: blockIndex,
2094
- blockType: "text"
2095
- };
2096
- yield {
2097
- type: "text-delta",
2098
- index: blockIndex,
2099
- text
2100
- };
2101
- yield {
2102
- type: "block-end",
2103
- index: blockIndex,
2104
- block: {
2105
- type: "text",
2106
- text
2107
- }
2108
- };
2109
- 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;
2110
2219
  }
2111
- text = "";
2112
- if (event.type === "segment-complete") continue;
2113
2220
  completed = true;
2114
2221
  if (pendingUsage !== void 0) yield {
2115
2222
  type: "usage",
@@ -2123,26 +2230,6 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
2123
2230
  } catch (error) {
2124
2231
  if (error.name === "AbortError") {
2125
2232
  completed = true;
2126
- if (text.length > 0) {
2127
- yield {
2128
- type: "block-start",
2129
- index: blockIndex,
2130
- blockType: "text"
2131
- };
2132
- yield {
2133
- type: "text-delta",
2134
- index: blockIndex,
2135
- text
2136
- };
2137
- yield {
2138
- type: "block-end",
2139
- index: blockIndex,
2140
- block: {
2141
- type: "text",
2142
- text
2143
- }
2144
- };
2145
- }
2146
2233
  yield {
2147
2234
  type: "finish",
2148
2235
  reason: {
@@ -2160,8 +2247,8 @@ var ClaudeCodeAdapter = class extends LlmAdapter {
2160
2247
  if (!completed) throw new Error("dsh-claude: Claude turn stream ended without a result");
2161
2248
  }
2162
2249
  };
2163
- function createClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor) {
2164
- return new ClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor);
2250
+ function createClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments = () => []) {
2251
+ return new ClaudeCodeAdapter(supervisor, agents, attachments, presetIdFor, drainReviewComments);
2165
2252
  }
2166
2253
  //#endregion
2167
2254
  //#region src/http.ts
@@ -2288,8 +2375,8 @@ function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolution
2288
2375
  }
2289
2376
  //#endregion
2290
2377
  //#region src/projection-routes.ts
2291
- const MAX_SESSION_ID_CHARS = 1024;
2292
- function sessionIdFromUrl(rawUrl) {
2378
+ const MAX_SESSION_ID_CHARS$2 = 1024;
2379
+ function sessionIdFromUrl$1(rawUrl) {
2293
2380
  try {
2294
2381
  const pathname = new URL(rawUrl ?? "/", "http://localhost").pathname;
2295
2382
  const prefix = `${CLAUDE_PROJECTION_PATH}/`;
@@ -2297,32 +2384,36 @@ function sessionIdFromUrl(rawUrl) {
2297
2384
  const encoded = pathname.slice(prefix.length);
2298
2385
  if (encoded.length === 0 || encoded.includes("/")) return void 0;
2299
2386
  const sessionId = decodeURIComponent(encoded);
2300
- 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;
2301
2388
  return sessionId;
2302
2389
  } catch {
2303
2390
  return;
2304
2391
  }
2305
2392
  }
2306
2393
  /** Register the browser-readable, credential-free sidecar projection endpoint. */
2307
- function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSession = () => []) {
2394
+ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSession = () => [], repositoryForSession = async () => void 0, reviewCommentsForSession = () => []) {
2308
2395
  ctx.effect(() => ctx.webServer.register({
2309
2396
  kind: "prefix",
2310
2397
  path: CLAUDE_PROJECTION_PATH,
2311
2398
  handler: async (req, res) => {
2312
2399
  if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
2313
2400
  if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
2314
- const sessionId = sessionIdFromUrl(req.url);
2401
+ const sessionId = sessionIdFromUrl$1(req.url);
2315
2402
  if (sessionId === void 0) return json(res, 400, { error: "invalid session id" });
2316
2403
  try {
2317
2404
  const projection = await sidecar.read(sessionId);
2405
+ const owned = ownsSession(sessionId);
2406
+ const repository = owned ? await repositoryForSession(sessionId) : void 0;
2318
2407
  return json(res, 200, {
2319
2408
  schemaVersion: projection.schemaVersion,
2320
2409
  revision: projection.revision,
2321
- owned: ownsSession(sessionId),
2410
+ owned,
2322
2411
  commands: commandsForSession(sessionId),
2323
2412
  activities: projection.activities,
2324
2413
  ...projection.contextUsage === void 0 ? {} : { contextUsage: projection.contextUsage },
2325
- ...projection.tasks === void 0 ? {} : { tasks: projection.tasks }
2414
+ ...projection.tasks === void 0 ? {} : { tasks: projection.tasks },
2415
+ ...repository === void 0 ? {} : { repository },
2416
+ reviewComments: owned ? reviewCommentsForSession(sessionId) : []
2326
2417
  });
2327
2418
  } catch {
2328
2419
  return json(res, 500, { error: "projection unavailable" });
@@ -2331,6 +2422,1394 @@ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession, commandsForSes
2331
2422
  }), "dsh-claude: sidecar projection route");
2332
2423
  }
2333
2424
  //#endregion
2425
+ //#region src/repository-status.ts
2426
+ const MAX_OUTPUT_BYTES$2 = 65536;
2427
+ const MAX_DIFF_BYTES = 262144;
2428
+ const MAX_UNTRACKED_DIFFS = 50;
2429
+ const GIT_TIMEOUT_MS$2 = 5e3;
2430
+ const GH_TIMEOUT_MS = 8e3;
2431
+ const CACHE_TTL_MS = 5e3;
2432
+ const MAX_TEXT_CHARS = 1024;
2433
+ function bounded(value) {
2434
+ return value.trim().slice(0, MAX_TEXT_CHARS);
2435
+ }
2436
+ async function collect$2(handle) {
2437
+ const outcome = await handle.done;
2438
+ const stdout = handle.collected.stdout?.readFrom(0);
2439
+ return {
2440
+ exitCode: outcome.exitCode,
2441
+ stdout: stdout?.text ?? "",
2442
+ lossy: stdout?.lossy === true
2443
+ };
2444
+ }
2445
+ async function run(runtime, executable, args, cwd, timeoutMs, maxBytes = MAX_OUTPUT_BYTES$2) {
2446
+ const signal = AbortSignal.timeout(timeoutMs);
2447
+ return collect$2(runtime.spawn({
2448
+ argv: [executable, ...args],
2449
+ cwd,
2450
+ stdio: {
2451
+ stdin: "ignore",
2452
+ stdout: { maxBytes },
2453
+ stderr: { maxBytes: MAX_OUTPUT_BYTES$2 }
2454
+ },
2455
+ graceMs: 1e3,
2456
+ signal,
2457
+ env: {}
2458
+ }));
2459
+ }
2460
+ function normalizedPath(value) {
2461
+ return value.replaceAll("\\", "/").replace(/\/+$/u, "").toLocaleLowerCase("en-US");
2462
+ }
2463
+ function parseGitStatus(output) {
2464
+ let branch;
2465
+ let detached = false;
2466
+ let dirty = false;
2467
+ let upstream = false;
2468
+ let ahead;
2469
+ let behind;
2470
+ for (const line of output.split(/\r?\n/u)) {
2471
+ if (line.startsWith("# branch.head ")) {
2472
+ const head = bounded(line.slice(14));
2473
+ if (head === "(detached)") detached = true;
2474
+ else if (head.length > 0 && head !== "(unknown)") branch = head;
2475
+ continue;
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
+ }
2489
+ if (line.length > 0 && !line.startsWith("# ")) dirty = true;
2490
+ }
2491
+ return {
2492
+ ...branch === void 0 ? {} : { branch },
2493
+ detached,
2494
+ dirty,
2495
+ upstream,
2496
+ ...ahead === void 0 ? {} : { ahead },
2497
+ ...behind === void 0 ? {} : { behind }
2498
+ };
2499
+ }
2500
+ function parseDiffNumstat(value) {
2501
+ let additions = 0;
2502
+ let deletions = 0;
2503
+ let files = 0;
2504
+ for (const line of value.split(/\r?\n/u)) {
2505
+ if (line.length === 0) continue;
2506
+ const [added, deleted] = line.split(" ");
2507
+ files += 1;
2508
+ if (added !== void 0 && /^\d+$/u.test(added)) additions += Number(added);
2509
+ if (deleted !== void 0 && /^\d+$/u.test(deleted)) deletions += Number(deleted);
2510
+ }
2511
+ return {
2512
+ additions,
2513
+ deletions,
2514
+ files
2515
+ };
2516
+ }
2517
+ function parseGitHubRemote(value) {
2518
+ const remote = bounded(value);
2519
+ const match = /^(?:https:\/\/github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)([^/\s]+)\/([^/\s]+?)(?:\.git)?$/iu.exec(remote);
2520
+ if (match?.[1] === void 0 || match[2] === void 0) return void 0;
2521
+ return `${match[1]}/${match[2]}`;
2522
+ }
2523
+ function record$3(value) {
2524
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2525
+ }
2526
+ function aggregateChecks(value) {
2527
+ if (!Array.isArray(value) || value.length === 0) return "none";
2528
+ let pending = false;
2529
+ for (const item of value) {
2530
+ const check = record$3(item);
2531
+ if (check === void 0) continue;
2532
+ const conclusion = typeof check.conclusion === "string" ? check.conclusion.toUpperCase() : void 0;
2533
+ const status = typeof check.status === "string" ? check.status.toUpperCase() : void 0;
2534
+ const state = typeof check.state === "string" ? check.state.toUpperCase() : void 0;
2535
+ if ([
2536
+ "FAILURE",
2537
+ "ERROR",
2538
+ "CANCELLED",
2539
+ "TIMED_OUT",
2540
+ "ACTION_REQUIRED"
2541
+ ].includes(conclusion ?? state ?? "")) return "failing";
2542
+ if (status !== void 0 && status !== "COMPLETED") pending = true;
2543
+ if (["PENDING", "EXPECTED"].includes(state ?? "")) pending = true;
2544
+ }
2545
+ return pending ? "pending" : "passing";
2546
+ }
2547
+ function reviewState(value) {
2548
+ if (value === "APPROVED") return "approved";
2549
+ if (value === "CHANGES_REQUESTED") return "changes-requested";
2550
+ if (value === "REVIEW_REQUIRED") return "review-required";
2551
+ return "none";
2552
+ }
2553
+ function parsePullRequest(value) {
2554
+ const input = record$3(value);
2555
+ if (input === void 0 || !Number.isSafeInteger(input.number) || Number(input.number) <= 0 || typeof input.title !== "string" || typeof input.url !== "string") return void 0;
2556
+ let url;
2557
+ try {
2558
+ url = new URL(input.url);
2559
+ } catch {
2560
+ return;
2561
+ }
2562
+ if (url.protocol !== "https:" || url.hostname !== "github.com") return void 0;
2563
+ const rawState = typeof input.state === "string" ? input.state.toUpperCase() : "";
2564
+ const state = input.mergedAt !== void 0 && input.mergedAt !== null ? "merged" : rawState === "OPEN" ? "open" : rawState === "CLOSED" ? "closed" : void 0;
2565
+ if (state === void 0) return void 0;
2566
+ return {
2567
+ number: Number(input.number),
2568
+ title: bounded(input.title),
2569
+ url: url.href,
2570
+ state,
2571
+ draft: input.isDraft === true,
2572
+ review: reviewState(input.reviewDecision),
2573
+ checks: aggregateChecks(input.statusCheckRollup),
2574
+ ...typeof input.mergeStateStatus === "string" ? { mergeState: bounded(input.mergeStateStatus) } : {},
2575
+ ...typeof record$3(input.author)?.login === "string" ? { author: bounded(String(record$3(input.author)?.login)) } : {},
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() } : {},
2578
+ ...typeof input.baseRefName === "string" && bounded(input.baseRefName).length > 0 ? { baseBranch: bounded(input.baseRefName) } : {}
2579
+ };
2580
+ }
2581
+ var RepositoryStatusService = class {
2582
+ #runtime;
2583
+ #cacheTtlMs;
2584
+ #cache = /* @__PURE__ */ new Map();
2585
+ #lastReady = /* @__PURE__ */ new Map();
2586
+ #gitExecutable;
2587
+ #ghExecutable;
2588
+ constructor(runtime, cacheTtlMs = CACHE_TTL_MS) {
2589
+ this.#runtime = runtime;
2590
+ this.#cacheTtlMs = cacheTtlMs;
2591
+ }
2592
+ inspect(cwd) {
2593
+ const current = this.#cache.get(cwd);
2594
+ if (current !== void 0 && current.expiresAt > Date.now()) return current.value;
2595
+ const value = this.#inspect(cwd).then((next) => this.#stabilize(cwd, next));
2596
+ this.#cache.set(cwd, {
2597
+ expiresAt: Date.now() + this.#cacheTtlMs,
2598
+ value
2599
+ });
2600
+ value.catch(() => this.#cache.delete(cwd));
2601
+ return value;
2602
+ }
2603
+ invalidate(cwd) {
2604
+ this.#cache.delete(cwd);
2605
+ this.#lastReady.delete(cwd);
2606
+ }
2607
+ dispose() {
2608
+ this.#cache.clear();
2609
+ this.#lastReady.clear();
2610
+ }
2611
+ #stabilize(cwd, next) {
2612
+ const previous = this.#lastReady.get(cwd);
2613
+ if (next.status !== "ready") return previous ?? next;
2614
+ const stable = previous?.status === "ready" && previous.root === next.root && previous.branch === next.branch ? {
2615
+ ...next,
2616
+ ...next.pullRequest === void 0 && previous.pullRequest !== void 0 ? { pullRequest: previous.pullRequest } : {},
2617
+ ...next.pullRequest === void 0 && previous.pullRequest !== void 0 && previous.diff !== void 0 ? { diff: previous.diff } : next.diff === void 0 && previous.diff !== void 0 ? { diff: previous.diff } : {}
2618
+ } : next;
2619
+ this.#lastReady.set(cwd, stable);
2620
+ return stable;
2621
+ }
2622
+ async #git() {
2623
+ this.#gitExecutable ??= this.#runtime.resolveExecutable("git");
2624
+ return this.#gitExecutable;
2625
+ }
2626
+ async #gh() {
2627
+ this.#ghExecutable ??= this.#runtime.resolveExecutable("gh").catch(() => void 0);
2628
+ return this.#ghExecutable;
2629
+ }
2630
+ async #inspect(cwd) {
2631
+ const safeCwd = bounded(cwd);
2632
+ let git;
2633
+ try {
2634
+ git = await this.#git();
2635
+ } catch {
2636
+ return {
2637
+ status: "unavailable",
2638
+ cwd: safeCwd
2639
+ };
2640
+ }
2641
+ try {
2642
+ const paths = await run(this.#runtime, git, [
2643
+ "rev-parse",
2644
+ "--path-format=absolute",
2645
+ "--show-toplevel",
2646
+ "--absolute-git-dir",
2647
+ "--git-common-dir"
2648
+ ], cwd, GIT_TIMEOUT_MS$2);
2649
+ if (paths.exitCode !== 0) return {
2650
+ status: "not-repository",
2651
+ cwd: safeCwd
2652
+ };
2653
+ const [rootValue, gitDirValue, commonDirValue] = paths.stdout.split(/\r?\n/u);
2654
+ const root = bounded(rootValue ?? "");
2655
+ const gitDir = bounded(gitDirValue ?? "");
2656
+ const commonDir = bounded(commonDirValue ?? "");
2657
+ if (root.length === 0 || gitDir.length === 0 || commonDir.length === 0) return {
2658
+ status: "unavailable",
2659
+ cwd: safeCwd
2660
+ };
2661
+ const statusResult = await run(this.#runtime, git, [
2662
+ "status",
2663
+ "--porcelain=v2",
2664
+ "--branch",
2665
+ "--untracked-files=normal"
2666
+ ], cwd, GIT_TIMEOUT_MS$2);
2667
+ if (statusResult.exitCode !== 0) return {
2668
+ status: "unavailable",
2669
+ cwd: safeCwd
2670
+ };
2671
+ const status = parseGitStatus(statusResult.stdout);
2672
+ const remoteResult = await run(this.#runtime, git, [
2673
+ "remote",
2674
+ "get-url",
2675
+ "origin"
2676
+ ], cwd, GIT_TIMEOUT_MS$2);
2677
+ const remote = remoteResult.exitCode === 0 ? parseGitHubRemote(remoteResult.stdout) : void 0;
2678
+ const pullRequest = status.branch === void 0 || remote === void 0 ? void 0 : await this.#pullRequest(cwd, remote, status.branch);
2679
+ const diffBase = pullRequest?.baseBranch === void 0 ? "HEAD" : await this.#mergeBase(cwd, git, pullRequest.baseBranch) ?? "HEAD";
2680
+ const diff = status.dirty || diffBase !== "HEAD" ? await this.#diff(cwd, git, diffBase) : {
2681
+ additions: 0,
2682
+ deletions: 0,
2683
+ files: 0,
2684
+ truncated: false
2685
+ };
2686
+ return {
2687
+ status: "ready",
2688
+ cwd: safeCwd,
2689
+ root,
2690
+ ...status,
2691
+ worktree: normalizedPath(gitDir) !== normalizedPath(commonDir),
2692
+ ...remote === void 0 ? {} : { remote },
2693
+ ...pullRequest === void 0 ? {} : { pullRequest },
2694
+ ...diff === void 0 ? {} : { diff }
2695
+ };
2696
+ } catch {
2697
+ return {
2698
+ status: "unavailable",
2699
+ cwd: safeCwd
2700
+ };
2701
+ }
2702
+ }
2703
+ async #mergeBase(cwd, git, baseBranch) {
2704
+ try {
2705
+ const result = await run(this.#runtime, git, [
2706
+ "merge-base",
2707
+ "HEAD",
2708
+ `refs/remotes/origin/${baseBranch}`
2709
+ ], cwd, GIT_TIMEOUT_MS$2);
2710
+ if (result.exitCode !== 0 || result.lossy) return void 0;
2711
+ const oid = bounded(result.stdout);
2712
+ return /^[0-9a-f]{40}$/iu.test(oid) ? oid : void 0;
2713
+ } catch {
2714
+ return;
2715
+ }
2716
+ }
2717
+ async #diff(cwd, git, base) {
2718
+ try {
2719
+ const numstat = await run(this.#runtime, git, [
2720
+ "diff",
2721
+ "--no-ext-diff",
2722
+ "--numstat",
2723
+ base,
2724
+ "--"
2725
+ ], cwd, GIT_TIMEOUT_MS$2);
2726
+ if (numstat.exitCode !== 0 || numstat.lossy) return void 0;
2727
+ const summary = parseDiffNumstat(numstat.stdout);
2728
+ const patch = await run(this.#runtime, git, [
2729
+ "diff",
2730
+ "--no-ext-diff",
2731
+ "--no-color",
2732
+ "--unified=3",
2733
+ base,
2734
+ "--"
2735
+ ], cwd, GIT_TIMEOUT_MS$2, MAX_DIFF_BYTES);
2736
+ if (patch.exitCode !== 0) return {
2737
+ ...summary,
2738
+ truncated: true
2739
+ };
2740
+ const untracked = await this.#untrackedDiff(cwd, git);
2741
+ const combinedPatch = `${patch.stdout}${untracked.patch}`;
2742
+ return {
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
2748
+ };
2749
+ } catch {
2750
+ return;
2751
+ }
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
+ }
2799
+ async #pullRequest(cwd, repository, branch) {
2800
+ const gh = await this.#gh();
2801
+ if (gh === void 0) return void 0;
2802
+ try {
2803
+ const result = await run(this.#runtime, gh, [
2804
+ "pr",
2805
+ "view",
2806
+ branch,
2807
+ "--repo",
2808
+ repository,
2809
+ "--json",
2810
+ "number,title,url,state,isDraft,reviewDecision,mergeStateStatus,mergedAt,statusCheckRollup,author,createdAt,baseRefName"
2811
+ ], cwd, GH_TIMEOUT_MS);
2812
+ if (result.exitCode !== 0) return void 0;
2813
+ return parsePullRequest(JSON.parse(result.stdout));
2814
+ } catch {
2815
+ return;
2816
+ }
2817
+ }
2818
+ };
2819
+ //#endregion
2820
+ //#region src/repository-setup.ts
2821
+ const MAX_OUTPUT_BYTES$1 = 131072;
2822
+ const GIT_TIMEOUT_MS$1 = 1e4;
2823
+ const GIT_FETCH_TIMEOUT_MS = 6e4;
2824
+ const MAX_PATH_CHARS = 4096;
2825
+ const MAX_BRANCH_CHARS = 512;
2826
+ const LEASE_SCHEMA_VERSION = 1;
2827
+ var RepositorySetupError = class extends Error {
2828
+ code;
2829
+ constructor(code, message) {
2830
+ super(message);
2831
+ this.name = "RepositorySetupError";
2832
+ this.code = code;
2833
+ }
2834
+ };
2835
+ async function collect$1(handle) {
2836
+ const outcome = await handle.done;
2837
+ const stdout = handle.collected.stdout?.readFrom(0);
2838
+ const stderr = handle.collected.stderr?.readFrom(0);
2839
+ return {
2840
+ exitCode: outcome.exitCode,
2841
+ stdout: stdout?.text ?? "",
2842
+ stderr: stderr?.text ?? "",
2843
+ lossy: stdout?.lossy === true || stderr?.lossy === true
2844
+ };
2845
+ }
2846
+ function safePath(value) {
2847
+ if (value.length === 0 || value.length > MAX_PATH_CHARS || !isAbsolute(value) || value.includes("\0")) throw new RepositorySetupError("invalid-path", "The workspace path is invalid.");
2848
+ return resolve(value);
2849
+ }
2850
+ function safeBranch(value) {
2851
+ if (value.length === 0 || value.length > MAX_BRANCH_CHARS || value.trim() !== value || /[\0\r\n]/u.test(value)) throw new RepositorySetupError("invalid-branch", "The branch name is invalid.");
2852
+ return value;
2853
+ }
2854
+ function parseCurrentBranch(value) {
2855
+ const branch = value.trim();
2856
+ return branch.length === 0 || branch === "HEAD" ? void 0 : branch;
2857
+ }
2858
+ function parseWorktreeBranches(value) {
2859
+ const occupied = /* @__PURE__ */ new Map();
2860
+ let path;
2861
+ for (const line of `${value}\n`.split(/\r?\n/u)) if (line.startsWith("worktree ")) path = line.slice(9);
2862
+ else if (line.startsWith("branch refs/heads/") && path !== void 0) occupied.set(line.slice(18), path);
2863
+ else if (line.length === 0) path = void 0;
2864
+ return occupied;
2865
+ }
2866
+ function slug(value, fallback) {
2867
+ return value.toLocaleLowerCase("en-US").replace(/[^a-z0-9._-]+/gu, "-").replace(/^-+|-+$/gu, "").slice(0, 48) || fallback;
2868
+ }
2869
+ function lease(value) {
2870
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
2871
+ const input = value;
2872
+ if (typeof input.id !== "string" || typeof input.root !== "string" || typeof input.path !== "string" || typeof input.branch !== "string" || typeof input.createdAt !== "string" || input.pluginGeneratedBranch !== void 0 && typeof input.pluginGeneratedBranch !== "boolean" || input.sessionId !== void 0 && typeof input.sessionId !== "string") return void 0;
2873
+ return {
2874
+ id: input.id,
2875
+ root: input.root,
2876
+ path: input.path,
2877
+ branch: input.branch,
2878
+ createdAt: input.createdAt,
2879
+ pluginGeneratedBranch: input.pluginGeneratedBranch === true,
2880
+ ...input.sessionId === void 0 ? {} : { sessionId: input.sessionId }
2881
+ };
2882
+ }
2883
+ var RepositorySetupService = class {
2884
+ #runtime;
2885
+ #leasePath;
2886
+ #worktreeRoot;
2887
+ #branchPrefix;
2888
+ #gitPath;
2889
+ #pending = Promise.resolve();
2890
+ constructor(runtime, options = {}) {
2891
+ this.#runtime = runtime;
2892
+ this.#leasePath = options.leasePath ?? dshHomePath("plugins", "dsh-claude", "worktrees.json");
2893
+ this.#worktreeRoot = options.worktreeRoot ?? dshHomePath("plugins", "dsh-claude", "worktrees");
2894
+ this.#branchPrefix = options.branchPrefix ?? (async () => "claude");
2895
+ }
2896
+ async listBranches(cwd) {
2897
+ const git = await this.#git();
2898
+ const root = await this.#repositoryRoot(git, safePath(cwd));
2899
+ const [status, refs, remoteRefs] = await Promise.all([
2900
+ this.#run(git, [
2901
+ "status",
2902
+ "--porcelain=v2",
2903
+ "--branch",
2904
+ "--untracked-files=normal"
2905
+ ], root),
2906
+ this.#run(git, [
2907
+ "for-each-ref",
2908
+ "--format=%(refname:short)",
2909
+ "refs/heads"
2910
+ ], root),
2911
+ this.#run(git, [
2912
+ "for-each-ref",
2913
+ "--format=%(refname:short) %(symref)",
2914
+ "refs/remotes"
2915
+ ], root)
2916
+ ]);
2917
+ if (status.exitCode !== 0 || status.lossy || refs.exitCode !== 0 || refs.lossy || remoteRefs.exitCode !== 0 || remoteRefs.lossy) throw new RepositorySetupError("repository-unavailable", "The repository state is unavailable.");
2918
+ const current = status.stdout.split(/\r?\n/u).find((line) => line.startsWith("# branch.head "))?.slice(14);
2919
+ const branches = refs.stdout.split(/\r?\n/u).filter(Boolean).sort((left, right) => left.localeCompare(right));
2920
+ const remoteBranches = remoteRefs.stdout.split(/\r?\n/u).map((line) => line.trim().split(/\s+/u)).filter((parts) => parts[0] !== void 0 && parts[0].length > 0 && parts.length === 1).map((parts) => parts[0]).sort((left, right) => left.localeCompare(right));
2921
+ const currentBranch = current === void 0 ? void 0 : parseCurrentBranch(current);
2922
+ return {
2923
+ root,
2924
+ ...currentBranch === void 0 ? {} : { current: currentBranch },
2925
+ dirty: status.stdout.split(/\r?\n/u).some((line) => line.length > 0 && !line.startsWith("# ")),
2926
+ branches,
2927
+ remoteBranches
2928
+ };
2929
+ }
2930
+ async setup(cwd, branchValue, useWorktree, explicitBranchName, progress = () => {}) {
2931
+ progress("inspecting");
2932
+ const branch = safeBranch(branchValue);
2933
+ const info = await this.listBranches(cwd);
2934
+ const local = info.branches.includes(branch);
2935
+ const remote = info.remoteBranches.includes(branch);
2936
+ if (!local && !remote) throw new RepositorySetupError("branch-not-found", "The selected local or remote-tracking branch does not exist.");
2937
+ const requestedBranch = explicitBranchName === void 0 ? void 0 : safeBranch(explicitBranchName);
2938
+ if (useWorktree) return this.#createWorktree(info.root, branch, local ? `refs/heads/${branch}` : `refs/remotes/${branch}`, requestedBranch, progress);
2939
+ progress("switching-branch");
2940
+ return !local && remote ? this.#checkoutRemote(info, branch) : this.#checkout(info, branch);
2941
+ }
2942
+ bindLease(leaseId, sessionId) {
2943
+ if (leaseId.length === 0 || leaseId.length > 128 || sessionId.length === 0 || sessionId.length > 1024) return Promise.reject(new RepositorySetupError("invalid-lease", "The worktree lease is invalid."));
2944
+ return this.#serialize(async () => {
2945
+ const leases = await this.#readLeases();
2946
+ const index = leases.findIndex((item) => item.id === leaseId);
2947
+ if (index < 0) throw new RepositorySetupError("lease-not-found", "The worktree lease no longer exists.");
2948
+ const existing = leases[index];
2949
+ if (existing === void 0) throw new RepositorySetupError("lease-not-found", "The worktree lease no longer exists.");
2950
+ leases[index] = {
2951
+ ...existing,
2952
+ sessionId
2953
+ };
2954
+ await this.#writeLeases(leases);
2955
+ });
2956
+ }
2957
+ cleanupSession(sessionId) {
2958
+ return this.#serialize(async () => {
2959
+ const leases = await this.#readLeases();
2960
+ const retained = [];
2961
+ let changed = false;
2962
+ const git = await this.#git();
2963
+ for (const item of leases) {
2964
+ if (item.sessionId !== sessionId) {
2965
+ retained.push(item);
2966
+ continue;
2967
+ }
2968
+ try {
2969
+ const status = await this.#run(git, [
2970
+ "status",
2971
+ "--porcelain=v1",
2972
+ "--untracked-files=normal"
2973
+ ], item.path);
2974
+ if (status.exitCode !== 0 || status.lossy || status.stdout.trim().length > 0) {
2975
+ retained.push(item);
2976
+ continue;
2977
+ }
2978
+ if ((await this.#run(git, [
2979
+ "worktree",
2980
+ "remove",
2981
+ "--",
2982
+ item.path
2983
+ ], item.root)).exitCode !== 0) {
2984
+ retained.push(item);
2985
+ continue;
2986
+ }
2987
+ if (item.pluginGeneratedBranch) await this.#run(git, [
2988
+ "branch",
2989
+ "-D",
2990
+ "--",
2991
+ item.branch
2992
+ ], item.root).catch(() => void 0);
2993
+ changed = true;
2994
+ } catch {
2995
+ retained.push(item);
2996
+ }
2997
+ }
2998
+ if (changed) await this.#writeLeases(retained);
2999
+ });
3000
+ }
3001
+ async #checkoutRemote(info, remoteBranch) {
3002
+ const separator = remoteBranch.indexOf("/");
3003
+ if (separator <= 0 || separator === remoteBranch.length - 1) throw new RepositorySetupError("invalid-branch", "The remote-tracking branch name is invalid.");
3004
+ const localBranch = safeBranch(remoteBranch.slice(separator + 1));
3005
+ if (info.branches.includes(localBranch)) {
3006
+ const git = await this.#git();
3007
+ const upstream = await this.#run(git, [
3008
+ "for-each-ref",
3009
+ "--format=%(upstream:short)",
3010
+ `refs/heads/${localBranch}`
3011
+ ], info.root);
3012
+ if (upstream.exitCode !== 0 || upstream.lossy) throw new RepositorySetupError("repository-unavailable", "The local branch tracking state is unavailable.");
3013
+ if (upstream.stdout.trim() !== remoteBranch) throw new RepositorySetupError("branch-tracking-conflict", `The local branch ${localBranch} does not track ${remoteBranch}.`);
3014
+ return this.#checkout(info, localBranch);
3015
+ }
3016
+ if (info.dirty) throw new RepositorySetupError("dirty-workspace", "Commit or stash workspace changes before switching branches.");
3017
+ const git = await this.#git();
3018
+ if ((await this.#run(git, [
3019
+ "switch",
3020
+ "--track",
3021
+ "-c",
3022
+ localBranch,
3023
+ `refs/remotes/${remoteBranch}`
3024
+ ], info.root)).exitCode !== 0) throw new RepositorySetupError("checkout-failed", "Git could not create the local tracking branch.");
3025
+ return {
3026
+ mode: "checkout",
3027
+ root: info.root,
3028
+ path: info.root,
3029
+ branch: localBranch
3030
+ };
3031
+ }
3032
+ async #checkout(info, branch) {
3033
+ if (info.dirty) throw new RepositorySetupError("dirty-workspace", "Commit or stash workspace changes before switching branches.");
3034
+ if (info.current !== branch) {
3035
+ const git = await this.#git();
3036
+ const worktrees = await this.#run(git, [
3037
+ "worktree",
3038
+ "list",
3039
+ "--porcelain"
3040
+ ], info.root);
3041
+ if (worktrees.exitCode !== 0 || worktrees.lossy) throw new RepositorySetupError("repository-unavailable", "Worktree state is unavailable.");
3042
+ const occupiedPath = parseWorktreeBranches(worktrees.stdout).get(branch);
3043
+ if (occupiedPath !== void 0 && resolve(occupiedPath) !== info.root) throw new RepositorySetupError("branch-occupied", "The selected branch is already checked out in another worktree.");
3044
+ if ((await this.#run(git, [
3045
+ "switch",
3046
+ "--",
3047
+ branch
3048
+ ], info.root)).exitCode !== 0) throw new RepositorySetupError("checkout-failed", "Git could not switch to the selected branch.");
3049
+ }
3050
+ return {
3051
+ mode: "checkout",
3052
+ root: info.root,
3053
+ path: info.root,
3054
+ branch
3055
+ };
3056
+ }
3057
+ async #createWorktree(root, baseBranch, baseRef, explicitBranchName, progress) {
3058
+ const git = await this.#git();
3059
+ progress("fetching");
3060
+ const fetched = await this.#run(git, [
3061
+ "-c",
3062
+ "credential.interactive=never",
3063
+ "fetch",
3064
+ "--all",
3065
+ "--prune"
3066
+ ], root, GIT_FETCH_TIMEOUT_MS);
3067
+ if (fetched.exitCode !== 0 || fetched.lossy) throw new RepositorySetupError("fetch-failed", "Git could not refresh remote references before creating the worktree.");
3068
+ const suffix = randomUUID().slice(0, 8);
3069
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:]/gu, "").replace(/\.\d{3}Z$/u, "Z");
3070
+ const branch = explicitBranchName ?? `${safeBranch(await this.#branchPrefix())}/${slug(baseBranch, "branch")}-${stamp}-${suffix}`;
3071
+ const path = join(this.#worktreeRoot, `${slug(basename(root), "repository")}-${stamp}-${suffix}`);
3072
+ progress("creating-worktree");
3073
+ await mkdir(this.#worktreeRoot, { recursive: true });
3074
+ if ((await this.#run(git, [
3075
+ "worktree",
3076
+ "add",
3077
+ "-b",
3078
+ branch,
3079
+ path,
3080
+ baseRef
3081
+ ], root)).exitCode !== 0) throw new RepositorySetupError("worktree-failed", "Git could not create the worktree.");
3082
+ const item = {
3083
+ id: randomUUID(),
3084
+ root,
3085
+ path,
3086
+ branch,
3087
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3088
+ pluginGeneratedBranch: explicitBranchName === void 0
3089
+ };
3090
+ progress("saving-worktree");
3091
+ try {
3092
+ await this.#serialize(async () => {
3093
+ const leases = await this.#readLeases();
3094
+ await this.#writeLeases([...leases, item]);
3095
+ });
3096
+ } catch (error) {
3097
+ await this.#run(git, [
3098
+ "worktree",
3099
+ "remove",
3100
+ "--force",
3101
+ "--",
3102
+ path
3103
+ ], root).catch(() => void 0);
3104
+ throw error;
3105
+ }
3106
+ return {
3107
+ mode: "worktree",
3108
+ root,
3109
+ path,
3110
+ branch,
3111
+ leaseId: item.id
3112
+ };
3113
+ }
3114
+ async #repositoryRoot(git, cwd) {
3115
+ const result = await this.#run(git, [
3116
+ "rev-parse",
3117
+ "--path-format=absolute",
3118
+ "--show-toplevel"
3119
+ ], cwd);
3120
+ const root = result.stdout.trim();
3121
+ if (result.exitCode !== 0 || result.lossy || root.length === 0 || !isAbsolute(root)) throw new RepositorySetupError("not-repository", "The workspace is not a Git repository.");
3122
+ return resolve(root);
3123
+ }
3124
+ #git() {
3125
+ this.#gitPath ??= this.#runtime.resolveExecutable("git");
3126
+ return this.#gitPath;
3127
+ }
3128
+ async #run(executable, args, cwd, timeoutMs = GIT_TIMEOUT_MS$1) {
3129
+ return collect$1(this.#runtime.spawn({
3130
+ argv: [executable, ...args],
3131
+ cwd,
3132
+ stdio: {
3133
+ stdin: "ignore",
3134
+ stdout: { maxBytes: MAX_OUTPUT_BYTES$1 },
3135
+ stderr: { maxBytes: MAX_OUTPUT_BYTES$1 }
3136
+ },
3137
+ graceMs: 1e3,
3138
+ signal: AbortSignal.timeout(timeoutMs),
3139
+ env: {}
3140
+ }));
3141
+ }
3142
+ async #readLeases() {
3143
+ try {
3144
+ const parsed = JSON.parse(await readFile(this.#leasePath, "utf8"));
3145
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return [];
3146
+ const input = parsed;
3147
+ if (input.schemaVersion !== LEASE_SCHEMA_VERSION || !Array.isArray(input.leases)) return [];
3148
+ return input.leases.map(lease).filter((item) => item !== void 0);
3149
+ } catch (error) {
3150
+ if (error.code === "ENOENT") return [];
3151
+ throw error;
3152
+ }
3153
+ }
3154
+ async #writeLeases(leases) {
3155
+ await mkdir(resolve(this.#leasePath, ".."), { recursive: true });
3156
+ const temporary = `${this.#leasePath}.${process.pid}.${randomUUID()}.tmp`;
3157
+ const document = {
3158
+ schemaVersion: LEASE_SCHEMA_VERSION,
3159
+ leases
3160
+ };
3161
+ try {
3162
+ await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, { mode: 384 });
3163
+ await rename(temporary, this.#leasePath);
3164
+ } finally {
3165
+ await rm(temporary, { force: true }).catch(() => void 0);
3166
+ }
3167
+ }
3168
+ #serialize(operation) {
3169
+ const result = this.#pending.then(operation, operation);
3170
+ this.#pending = result.then(() => void 0, () => void 0);
3171
+ return result;
3172
+ }
3173
+ };
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
3545
+ //#region src/repository-setup-routes.ts
3546
+ const MAX_BODY_BYTES$2 = 16384;
3547
+ function record$2(value) {
3548
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3549
+ }
3550
+ async function readJson$2(req) {
3551
+ const chunks = [];
3552
+ let size = 0;
3553
+ for await (const chunk of req) {
3554
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
3555
+ size += buffer.length;
3556
+ if (size > MAX_BODY_BYTES$2) throw new RepositorySetupError("body-too-large", "The request body is too large.");
3557
+ chunks.push(buffer);
3558
+ }
3559
+ const value = record$2(JSON.parse(Buffer.concat(chunks).toString("utf8")));
3560
+ if (value === void 0) throw new RepositorySetupError("invalid-request", "The request body is invalid.");
3561
+ return value;
3562
+ }
3563
+ function string$1(input, key) {
3564
+ const value = input[key];
3565
+ if (typeof value !== "string") throw new RepositorySetupError("invalid-request", `The ${key} field is required.`);
3566
+ return value;
3567
+ }
3568
+ function optionalString$1(input, key) {
3569
+ const value = input[key];
3570
+ if (value === void 0) return void 0;
3571
+ if (typeof value !== "string") throw new RepositorySetupError("invalid-request", `The ${key} field must be a string.`);
3572
+ return value;
3573
+ }
3574
+ function ndjsonHeaders(res) {
3575
+ res.writeHead(200, {
3576
+ "content-type": "application/x-ndjson; charset=utf-8",
3577
+ "cache-control": "no-store",
3578
+ "x-content-type-options": "nosniff"
3579
+ });
3580
+ res.flushHeaders?.();
3581
+ }
3582
+ function ndjson(res, value) {
3583
+ res.write(`${JSON.stringify(value)}\n`);
3584
+ }
3585
+ async function streamSetup(res, service, input) {
3586
+ ndjsonHeaders(res);
3587
+ let ended = false;
3588
+ try {
3589
+ ndjson(res, {
3590
+ type: "complete",
3591
+ result: await service.setup(string$1(input, "cwd"), string$1(input, "branch"), input.worktree, optionalString$1(input, "branchName"), (stage) => {
3592
+ if (!ended) ndjson(res, {
3593
+ type: "progress",
3594
+ stage
3595
+ });
3596
+ })
3597
+ });
3598
+ } catch (error) {
3599
+ const setupError = error instanceof RepositorySetupError ? error : void 0;
3600
+ ndjson(res, {
3601
+ type: "error",
3602
+ code: setupError?.code ?? "repository-setup-unavailable",
3603
+ message: setupError?.message ?? "Repository setup is unavailable."
3604
+ });
3605
+ } finally {
3606
+ ended = true;
3607
+ res.end();
3608
+ }
3609
+ }
3610
+ /** Register trusted browser routes for safe pre-session Git setup. */
3611
+ function registerRepositorySetupRoute(ctx, service) {
3612
+ ctx.effect(() => ctx.webServer.register({
3613
+ kind: "prefix",
3614
+ path: CLAUDE_REPOSITORY_SETUP_PATH,
3615
+ handler: async (req, res) => {
3616
+ if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
3617
+ const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
3618
+ try {
3619
+ if (pathname === `/plugins/dsh-claude/repository/setup/branches`) {
3620
+ if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
3621
+ const cwd = new URL(req.url ?? "/", "http://localhost").searchParams.get("cwd");
3622
+ if (cwd === null) throw new RepositorySetupError("invalid-request", "The cwd query parameter is required.");
3623
+ return json(res, 200, await service.listBranches(cwd));
3624
+ }
3625
+ if (pathname === "/plugins/dsh-claude/repository/setup") {
3626
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
3627
+ const input = await readJson$2(req);
3628
+ if (typeof input.worktree !== "boolean") throw new RepositorySetupError("invalid-request", "The worktree field is required.");
3629
+ await streamSetup(res, service, input);
3630
+ return;
3631
+ }
3632
+ if (pathname === `/plugins/dsh-claude/repository/setup/bind`) {
3633
+ if (req.method !== "POST") return json(res, 405, { error: "method not allowed" });
3634
+ const input = await readJson$2(req);
3635
+ await service.bindLease(string$1(input, "leaseId"), string$1(input, "sessionId"));
3636
+ return json(res, 200, { ok: true });
3637
+ }
3638
+ return json(res, 404, { error: "not found" });
3639
+ } catch (error) {
3640
+ if (error instanceof RepositorySetupError) return json(res, 409, {
3641
+ error: error.code,
3642
+ message: error.message
3643
+ });
3644
+ if (error instanceof SyntaxError) return json(res, 400, { error: "invalid json" });
3645
+ return json(res, 500, { error: "repository setup unavailable" });
3646
+ }
3647
+ }
3648
+ }), "dsh-claude: repository setup route");
3649
+ }
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
2334
3813
  //#region src/update-routes.ts
2335
3814
  const PLUGIN_PACKAGE_NAME = "@norman-else/dsh-claude";
2336
3815
  const UPDATE_TIMEOUT_MS = 3e4;
@@ -2604,11 +4083,14 @@ const BUILTIN_OUTPUT_STYLES = [
2604
4083
  "Learning"
2605
4084
  ];
2606
4085
  const STYLE_NAME = /^[\p{L}\p{N}][\p{L}\p{N} ._()\[\]-]{0,127}$/u;
4086
+ const DEFAULT_WORKTREE_BRANCH_PREFIX = "claude";
4087
+ const MAX_BRANCH_PREFIX_CHARS = 128;
2607
4088
  function pathsFor(deps) {
2608
4089
  const root = join(homedir(), ".claude");
2609
4090
  return {
2610
4091
  settingsFile: deps.paths?.settingsFile ?? join(root, "settings.json"),
2611
- outputStylesDir: deps.paths?.outputStylesDir ?? join(root, "output-styles")
4092
+ outputStylesDir: deps.paths?.outputStylesDir ?? join(root, "output-styles"),
4093
+ pluginSettingsFile: deps.paths?.pluginSettingsFile ?? dshHomePath("plugins", "dsh-claude", "settings.json")
2612
4094
  };
2613
4095
  }
2614
4096
  function object(value) {
@@ -2664,9 +4146,10 @@ async function userOutputStyleOptions(directory) {
2664
4146
  }
2665
4147
  return options;
2666
4148
  }
2667
- const DESCRIPTORS = [{
4149
+ const OUTPUT_STYLE = {
2668
4150
  key: "outputStyle",
2669
4151
  kind: "select",
4152
+ document: "claude",
2670
4153
  effect: "new-session",
2671
4154
  async options(paths) {
2672
4155
  const builtIn = BUILTIN_OUTPUT_STYLES.map((value) => ({
@@ -2687,11 +4170,42 @@ const DESCRIPTORS = [{
2687
4170
  if (value === "Default") delete document.outputStyle;
2688
4171
  else document.outputStyle = value;
2689
4172
  }
2690
- }];
4173
+ };
4174
+ function isValidWorktreeBranchPrefix(value) {
4175
+ return value.length > 0 && value.length <= MAX_BRANCH_PREFIX_CHARS && value.trim() === value && !value.startsWith("/") && !value.endsWith("/") && !value.includes("//") && !value.includes("..") && !value.includes("@{") && !/[\0-\x20\x7f~^:?*[\\]/u.test(value) && value.split("/").every((segment) => segment.length > 0 && !segment.startsWith(".") && !segment.endsWith(".lock"));
4176
+ }
4177
+ const WORKTREE_BRANCH_PREFIX = {
4178
+ key: "worktreeBranchPrefix",
4179
+ kind: "text",
4180
+ document: "plugin",
4181
+ effect: "next-worktree",
4182
+ maxLength: MAX_BRANCH_PREFIX_CHARS,
4183
+ read(document) {
4184
+ const value = document.worktreeBranchPrefix;
4185
+ return typeof value === "string" && isValidWorktreeBranchPrefix(value) ? value : DEFAULT_WORKTREE_BRANCH_PREFIX;
4186
+ },
4187
+ apply(document, value) {
4188
+ if (typeof value !== "string" || !isValidWorktreeBranchPrefix(value)) throw new Error("Invalid value for global setting worktreeBranchPrefix");
4189
+ if (value === DEFAULT_WORKTREE_BRANCH_PREFIX) delete document.worktreeBranchPrefix;
4190
+ else document.worktreeBranchPrefix = value;
4191
+ }
4192
+ };
4193
+ const DESCRIPTORS = [OUTPUT_STYLE, WORKTREE_BRANCH_PREFIX];
2691
4194
  const DESCRIPTOR_BY_KEY = new Map(DESCRIPTORS.map((descriptor) => [descriptor.key, descriptor]));
2692
4195
  let pendingWrite = Promise.resolve();
2693
- async function views(document, paths) {
4196
+ function documentFor(descriptor, documents) {
4197
+ return documents[descriptor.document];
4198
+ }
4199
+ async function views(documents, paths) {
2694
4200
  return { settings: await Promise.all(DESCRIPTORS.map(async (descriptor) => {
4201
+ const document = documentFor(descriptor, documents);
4202
+ if (descriptor.kind === "text") return {
4203
+ key: descriptor.key,
4204
+ kind: descriptor.kind,
4205
+ value: descriptor.read(document),
4206
+ maxLength: descriptor.maxLength,
4207
+ effect: descriptor.effect
4208
+ };
2695
4209
  const discovered = await descriptor.options(paths);
2696
4210
  const value = descriptor.read(document, discovered);
2697
4211
  const options = discovered.some((option) => option.value === value) ? discovered : [...discovered, {
@@ -2708,9 +4222,20 @@ async function views(document, paths) {
2708
4222
  };
2709
4223
  })) };
2710
4224
  }
4225
+ async function readDocuments(paths) {
4226
+ const [claude, plugin] = await Promise.all([readDocument(paths.settingsFile), readDocument(paths.pluginSettingsFile)]);
4227
+ return {
4228
+ claude,
4229
+ plugin
4230
+ };
4231
+ }
2711
4232
  async function readGlobalSettings(deps = {}) {
2712
4233
  const paths = pathsFor(deps);
2713
- return views(await readDocument(paths.settingsFile), paths);
4234
+ return views(await readDocuments(paths), paths);
4235
+ }
4236
+ async function readWorktreeBranchPrefix(deps = {}) {
4237
+ const paths = pathsFor(deps);
4238
+ return WORKTREE_BRANCH_PREFIX.read(await readDocument(paths.pluginSettingsFile));
2714
4239
  }
2715
4240
  async function atomicWrite(path, document) {
2716
4241
  await mkdir(dirname(path), {
@@ -2737,13 +4262,18 @@ function updateGlobalSettings(changes, deps = {}) {
2737
4262
  for (const key of Object.keys(changeObject)) if (!DESCRIPTOR_BY_KEY.has(key)) return Promise.reject(/* @__PURE__ */ new Error(`Unsupported global setting: ${key}`));
2738
4263
  const paths = pathsFor(deps);
2739
4264
  const operation = pendingWrite.catch(() => void 0).then(async () => {
2740
- const document = await readDocument(paths.settingsFile);
4265
+ const documents = await readDocuments(paths);
4266
+ const changedDocuments = /* @__PURE__ */ new Set();
2741
4267
  for (const [key, value] of Object.entries(changeObject)) {
2742
4268
  const descriptor = DESCRIPTOR_BY_KEY.get(key);
2743
- descriptor.apply(document, value, await descriptor.options(paths));
4269
+ const document = documentFor(descriptor, documents);
4270
+ if (descriptor.kind === "select") descriptor.apply(document, value, await descriptor.options(paths));
4271
+ else descriptor.apply(document, value);
4272
+ changedDocuments.add(descriptor.document);
2744
4273
  }
2745
- await atomicWrite(paths.settingsFile, document);
2746
- return views(document, paths);
4274
+ if (changedDocuments.has("claude")) await atomicWrite(paths.settingsFile, documents.claude);
4275
+ if (changedDocuments.has("plugin")) await atomicWrite(paths.pluginSettingsFile, documents.plugin);
4276
+ return views(documents, paths);
2747
4277
  });
2748
4278
  pendingWrite = operation;
2749
4279
  return operation;
@@ -2908,6 +4438,9 @@ async function apply(ctx, config) {
2908
4438
  maxProcesses: config.maxProcesses ?? 4
2909
4439
  };
2910
4440
  const sidecar = new ClaudeSidecarRepository();
4441
+ const repositoryStatus = new RepositoryStatusService(ctx.subprocess);
4442
+ const repositorySetup = new RepositorySetupService(ctx.subprocess, { branchPrefix: () => readWorktreeBranchPrefix() });
4443
+ const reviewComments = new ReviewCommentStore();
2911
4444
  const commandCatalogs = /* @__PURE__ */ new Map();
2912
4445
  const supervisor = new ClaudeSupervisor({
2913
4446
  runtime: ctx.subprocess,
@@ -2920,7 +4453,7 @@ async function apply(ctx, config) {
2920
4453
  let resolutionError;
2921
4454
  try {
2922
4455
  supervisorConfig.executablePath = (await resolveClaudeExecutable(ctx.subprocess, config.executablePath === void 0 || config.executablePath.length === 0 ? void 0 : config.executablePath)).path;
2923
- 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)));
2924
4457
  ctx.effect(() => {
2925
4458
  const mounted = /* @__PURE__ */ new Map();
2926
4459
  const pending = /* @__PURE__ */ new Set();
@@ -2981,18 +4514,35 @@ async function apply(ctx, config) {
2981
4514
  resolutionError = error;
2982
4515
  }
2983
4516
  ctx.on("agent/disposed", async ({ agent }) => {
4517
+ reviewComments.disposeSession(agent.id);
2984
4518
  await supervisor.disposeSession(agent.id);
4519
+ await repositorySetup.cleanupSession(agent.id);
2985
4520
  });
4521
+ ctx.effect(() => () => reviewComments.dispose(), "dsh-claude: review comments store");
2986
4522
  ctx.effect(() => () => supervisor.dispose(), "dsh-claude: process supervisor");
4523
+ ctx.effect(() => () => repositoryStatus.dispose(), "dsh-claude: repository status cache");
2987
4524
  ctx.inject(["webServer"], (webCtx) => {
2988
4525
  registerClaudeDoctorRoutes(webCtx, webCtx.subprocess, supervisor, supervisorConfig, resolutionError);
2989
4526
  const desktopActions = webCtx.get("desktopActions");
2990
4527
  registerClaudeUpdateRoutes(webCtx, webCtx.subprocess, { ...typeof desktopActions?.requestRestart === "function" ? { requestRestart: desktopActions.requestRestart.bind(desktopActions) } : {} });
2991
4528
  registerClaudeGlobalSettingsRoute(webCtx);
2992
- registerClaudeProjectionRoute(webCtx, sidecar, (sessionId) => {
4529
+ registerRepositorySetupRoute(webCtx, repositorySetup);
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) => {
2993
4536
  const agent = webCtx.agents.get(sessionId);
2994
4537
  return agent !== void 0 && webCtx.agentPresets.composedPreset(agent.ctx) === "claude";
2995
- }, (sessionId) => commandCatalogs.get(sessionId) ?? []);
4538
+ };
4539
+ registerReviewCommentRoute(webCtx, reviewComments, ownsClaudeSession);
4540
+ registerClaudeProjectionRoute(webCtx, sidecar, ownsClaudeSession, (sessionId) => commandCatalogs.get(sessionId) ?? [], async (sessionId) => {
4541
+ const agent = webCtx.agents.get(sessionId);
4542
+ if (agent === void 0 || webCtx.agentPresets.composedPreset(agent.ctx) !== "claude") return void 0;
4543
+ const cwd = agent.session.header.cwd;
4544
+ return cwd === void 0 ? void 0 : repositoryStatus.inspect(cwd);
4545
+ }, (sessionId) => reviewComments.list(sessionId));
2996
4546
  });
2997
4547
  }
2998
4548
  //#endregion