@norman-else/dsh-claude 0.1.39 → 0.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -27,6 +27,7 @@ window.__ModuleLoader__.load({
27
27
  const CLAUDE_ASK_PATH = "/plugins/dsh-claude/ask";
28
28
  const CLAUDE_EDITOR_OPEN_PATH = "/plugins/dsh-claude/editor/open";
29
29
  const CLAUDE_REWIND_PATH = "/plugins/dsh-claude/rewind";
30
+ const CLAUDE_PLAN_FEEDBACK_PATH = "/plugins/dsh-claude/plan/feedback";
30
31
  function isClaudeRenderMode(value) {
31
32
  return value === "plugin" || value === "native";
32
33
  }
@@ -34,6 +35,9 @@ window.__ModuleLoader__.load({
34
35
  function isClaudeProseMode(value) {
35
36
  return value === "plain" || value === "enhanced";
36
37
  }
38
+ function isClaudeAlertMode(value) {
39
+ return value === "off" || value === "on";
40
+ }
37
41
  //#endregion
38
42
  //#region src/client/task-projection.ts
39
43
  /** Tasks UI is reserved for detached work and genuine Claude subagents. */
@@ -268,6 +272,10 @@ window.__ModuleLoader__.load({
268
272
  failedAction = description === void 0 ? `run ${command}` : description;
269
273
  break;
270
274
  }
275
+ case "ExitPlanMode":
276
+ completed = "Proposed a plan";
277
+ failedAction = "propose a plan";
278
+ break;
271
279
  default:
272
280
  completed = description ?? `${toolName}${target === void 0 ? "" : ` ${target}`}`;
273
281
  failedAction = completed.charAt(0).toLowerCase() + completed.slice(1);
@@ -1096,6 +1104,163 @@ window.__ModuleLoader__.load({
1096
1104
  fontSize: 12,
1097
1105
  lineHeight: "18px"
1098
1106
  };
1107
+ /** Title and state chip: one phrase at the header's left end. */
1108
+ const planHeaderStart = {
1109
+ display: "flex",
1110
+ alignItems: "center",
1111
+ gap: 8,
1112
+ minWidth: 0
1113
+ };
1114
+ /** "2 / 5" next to the title when the session has proposed more than one. */
1115
+ const planCount = {
1116
+ flex: "none",
1117
+ color: "var(--dsw-alias-label-tertiary)",
1118
+ fontSize: 12,
1119
+ lineHeight: "17px",
1120
+ fontVariantNumeric: "tabular-nums"
1121
+ };
1122
+ /** The review composer: notes filed so far, the pending quote, and the box.
1123
+ * Docked below the plan body so the plan itself keeps the scrolling room. */
1124
+ const planComposer = {
1125
+ flex: "none",
1126
+ display: "flex",
1127
+ flexDirection: "column",
1128
+ gap: 6,
1129
+ maxHeight: "45%",
1130
+ overflowY: "auto",
1131
+ padding: "10px 14px 12px",
1132
+ borderTop: "1px solid var(--dsw-alias-border-l2, color-mix(in srgb, currentColor 16%, transparent))",
1133
+ background: "var(--dsw-alias-bg-layer-1)"
1134
+ };
1135
+ const planNoteList = {
1136
+ display: "flex",
1137
+ flexDirection: "column",
1138
+ gap: 6,
1139
+ margin: 0,
1140
+ padding: 0,
1141
+ listStyle: "none"
1142
+ };
1143
+ const planNote = {
1144
+ position: "relative",
1145
+ padding: "6px 26px 6px 8px",
1146
+ borderRadius: 7,
1147
+ background: "var(--dsw-alias-bg-base)"
1148
+ };
1149
+ /** The quoted passage, marked as someone else's words by the rule down its
1150
+ * left edge rather than by quotation marks it may already contain. */
1151
+ const planNoteQuote = {
1152
+ display: "-webkit-box",
1153
+ WebkitLineClamp: 3,
1154
+ WebkitBoxOrient: "vertical",
1155
+ overflow: "hidden",
1156
+ margin: 0,
1157
+ paddingLeft: 8,
1158
+ borderLeft: "2px solid var(--dsw-alias-border-l2, color-mix(in srgb, currentColor 24%, transparent))",
1159
+ color: "var(--dsw-alias-label-tertiary)",
1160
+ fontSize: 11,
1161
+ lineHeight: "16px",
1162
+ whiteSpace: "pre-wrap"
1163
+ };
1164
+ const planNoteText = {
1165
+ margin: "4px 0 0",
1166
+ color: "var(--dsw-alias-label-primary)",
1167
+ fontSize: 12,
1168
+ lineHeight: "17px",
1169
+ whiteSpace: "pre-wrap"
1170
+ };
1171
+ const planNoteRemove = {
1172
+ position: "absolute",
1173
+ top: 4,
1174
+ right: 4,
1175
+ width: 18,
1176
+ height: 18,
1177
+ display: "grid",
1178
+ placeItems: "center",
1179
+ padding: 0,
1180
+ border: 0,
1181
+ borderRadius: 5,
1182
+ background: "transparent",
1183
+ color: "var(--dsw-alias-label-tertiary)",
1184
+ fontSize: 14,
1185
+ lineHeight: 1,
1186
+ cursor: "pointer"
1187
+ };
1188
+ /** The live selection, shown attached to the box it will be filed with. */
1189
+ const planQuoteChip = {
1190
+ position: "relative",
1191
+ padding: "6px 26px 6px 8px",
1192
+ borderRadius: 7,
1193
+ background: "var(--dsw-alias-interactive-bg-hover)"
1194
+ };
1195
+ const planComposerInput = {
1196
+ boxSizing: "border-box",
1197
+ width: "100%",
1198
+ minHeight: 56,
1199
+ padding: "7px 9px",
1200
+ border: "1px solid var(--dsw-alias-border-l2, color-mix(in srgb, currentColor 16%, transparent))",
1201
+ borderRadius: 8,
1202
+ background: "var(--dsw-alias-bg-base)",
1203
+ color: "var(--dsw-alias-label-primary)",
1204
+ fontFamily: "inherit",
1205
+ fontSize: 12,
1206
+ lineHeight: "18px",
1207
+ resize: "vertical"
1208
+ };
1209
+ const planComposerActions = {
1210
+ display: "flex",
1211
+ alignItems: "center",
1212
+ justifyContent: "space-between",
1213
+ gap: 8
1214
+ };
1215
+ const planComposerHint = {
1216
+ minWidth: 0,
1217
+ color: "var(--dsw-alias-label-tertiary)",
1218
+ fontSize: 11,
1219
+ lineHeight: "16px"
1220
+ };
1221
+ const planComposerError = {
1222
+ margin: 0,
1223
+ color: "var(--dsw-alias-state-error-primary)",
1224
+ fontSize: 11,
1225
+ lineHeight: "16px"
1226
+ };
1227
+ /** Maximize and close: the control group at the header's right end. */
1228
+ const planHeaderEnd = {
1229
+ flex: "none",
1230
+ display: "flex",
1231
+ alignItems: "center",
1232
+ gap: 2
1233
+ };
1234
+ const planBadge = {
1235
+ flex: "none",
1236
+ padding: "2px 8px",
1237
+ borderRadius: 999,
1238
+ background: "color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent)",
1239
+ color: "var(--dsw-alias-state-success-primary)",
1240
+ fontSize: 11,
1241
+ lineHeight: "16px",
1242
+ fontWeight: 600,
1243
+ whiteSpace: "nowrap"
1244
+ };
1245
+ const planBadgePending = {
1246
+ background: "var(--dsw-alias-interactive-bg-hover)",
1247
+ color: "var(--dsw-alias-label-secondary)"
1248
+ };
1249
+ const planBadgeRejected = {
1250
+ background: "color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent)",
1251
+ color: "var(--dsw-alias-state-error-primary)"
1252
+ };
1253
+ /** Says where the decision is made, since this panel deliberately does not
1254
+ * make it: the Host's approval dialog owns the buttons. */
1255
+ const planHint = {
1256
+ margin: "0 0 12px",
1257
+ padding: "8px 10px",
1258
+ borderRadius: 8,
1259
+ background: "var(--dsw-alias-interactive-bg-hover)",
1260
+ color: "var(--dsw-alias-label-secondary)",
1261
+ fontSize: 12,
1262
+ lineHeight: "18px"
1263
+ };
1099
1264
  const tasksFinishedSection = {
1100
1265
  marginTop: 12,
1101
1266
  paddingTop: 8,
@@ -2687,17 +2852,8 @@ window.__ModuleLoader__.load({
2687
2852
  color: "var(--dsw-alias-label-secondary)",
2688
2853
  transition: "transform 120ms ease"
2689
2854
  };
2690
- const diffSummaryAction = {
2691
- marginLeft: "auto",
2692
- padding: "2px 8px",
2693
- border: "1px solid var(--dsw-alias-border-l2, color-mix(in srgb, currentColor 16%, transparent))",
2694
- borderRadius: 6,
2695
- background: "transparent",
2696
- color: "var(--dsw-alias-label-secondary)",
2697
- font: "inherit",
2698
- fontSize: 12,
2699
- cursor: "pointer"
2700
- };
2855
+ /** Sits at the far end of the summary row; the rest of the button is the shared icon-button class. */
2856
+ const diffSummaryAction = { marginLeft: "auto" };
2701
2857
  const diffCommentNav = {
2702
2858
  display: "inline-flex",
2703
2859
  alignItems: "center",
@@ -3041,6 +3197,9 @@ window.__ModuleLoader__.load({
3041
3197
  };
3042
3198
  const diffFile = { borderBottom: "1px solid var(--dsw-alias-border-l2, color-mix(in srgb, currentColor 16%, transparent))" };
3043
3199
  const diffFileHeader = {
3200
+ position: "sticky",
3201
+ top: 0,
3202
+ zIndex: 1,
3044
3203
  width: "100%",
3045
3204
  minHeight: 38,
3046
3205
  display: "flex",
@@ -4393,9 +4552,8 @@ window.__ModuleLoader__.load({
4393
4552
  feed: applyLine,
4394
4553
  subscribe(listener) {
4395
4554
  if (disposed) return () => {};
4396
- const wasIdle = listeners.size === 0;
4397
4555
  listeners.add(listener);
4398
- if (wasIdle) onDemand(true);
4556
+ onDemand(true);
4399
4557
  return () => {
4400
4558
  listeners.delete(listener);
4401
4559
  if (listeners.size !== 0) return;
@@ -4411,6 +4569,13 @@ window.__ModuleLoader__.load({
4411
4569
  }
4412
4570
  };
4413
4571
  }
4572
+ /** Whether two lane sets carry the same sessions. Order is the LRU's business,
4573
+ * not the carrier's: the server reads `sessions=` as a set. */
4574
+ function sameLanes(before, after) {
4575
+ if (before.length !== after.length) return false;
4576
+ const held = new Set(before);
4577
+ return after.every((sessionId) => held.has(sessionId));
4578
+ }
4414
4579
  /**
4415
4580
  * Every session's projection over ONE connection.
4416
4581
  *
@@ -4483,10 +4648,12 @@ window.__ModuleLoader__.load({
4483
4648
  * a session list would otherwise reopen it once per row. */
4484
4649
  #demand(sessionId, active) {
4485
4650
  if (this.#disposed) return;
4651
+ const before = this.#lanes();
4486
4652
  if (active) {
4487
4653
  this.#wanted.delete(sessionId);
4488
4654
  this.#wanted.add(sessionId);
4489
4655
  } else if (!this.#wanted.delete(sessionId)) return;
4656
+ if (sameLanes(before, this.#lanes())) return;
4490
4657
  if (this.#settle !== void 0) clearTimeout(this.#settle);
4491
4658
  const timer = setTimeout(() => {
4492
4659
  this.#settle = void 0;
@@ -4630,10 +4797,10 @@ window.__ModuleLoader__.load({
4630
4797
  ".dsh-claude-act-running{animation:dsh-claude-act-pulse 1.2s ease-in-out infinite}",
4631
4798
  "@keyframes dsh-claude-act-pulse{0%,100%{opacity:1}50%{opacity:.3}}"
4632
4799
  ].join("");
4633
- let cssInjected$3 = false;
4634
- function ensureCss$3() {
4635
- if (cssInjected$3 || typeof document === "undefined") return;
4636
- cssInjected$3 = true;
4800
+ let cssInjected$4 = false;
4801
+ function ensureCss$4() {
4802
+ if (cssInjected$4 || typeof document === "undefined") return;
4803
+ cssInjected$4 = true;
4637
4804
  const element = document.createElement("style");
4638
4805
  element.dataset.dshClaudeActivity = "";
4639
4806
  element.textContent = ACTIVITY_CSS;
@@ -5061,7 +5228,7 @@ window.__ModuleLoader__.load({
5061
5228
  });
5062
5229
  }
5063
5230
  function ClaudeActivityNode({ node, useClaudeProjection, t }) {
5064
- ensureCss$3();
5231
+ ensureCss$4();
5065
5232
  const marker = node.data;
5066
5233
  const activities = useClaudeProjection((value) => selectStepActivities(value, marker.turn, marker.step));
5067
5234
  const tasks = useClaudeProjection((value) => value.tasks?.tasks ?? EMPTY_TASKS$1);
@@ -5527,87 +5694,588 @@ window.__ModuleLoader__.load({
5527
5694
  });
5528
5695
  }
5529
5696
  //#endregion
5530
- //#region src/client/jira-api.ts
5531
- var JiraClientError = class extends Error {
5532
- code;
5533
- constructor(message, code) {
5534
- super(message);
5535
- this.name = "JiraClientError";
5536
- if (code !== void 0) this.code = code;
5697
+ //#region src/github-url.ts
5698
+ /** Only GitHub's own image hosts; the browser loads these directly, so a URL
5699
+ * the API did not vouch for must never become an outbound request. */
5700
+ function githubAvatarUrl(value) {
5701
+ if (typeof value !== "string" || value.length === 0 || value.length > 1024) return void 0;
5702
+ try {
5703
+ const url = new URL(value);
5704
+ const allowed = url.hostname === "github.com" || url.hostname === "githubusercontent.com" || url.hostname.endsWith(".githubusercontent.com");
5705
+ return url.protocol === "https:" && allowed ? url.href : void 0;
5706
+ } catch {
5707
+ return;
5537
5708
  }
5538
- };
5709
+ }
5710
+ //#endregion
5711
+ //#region src/client/pr-feedback-api.ts
5539
5712
  function record$5(value) {
5540
5713
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
5541
5714
  }
5542
- /**
5543
- * Every Jira failure the panels catch is a `JiraClientError`, whatever the
5544
- * transport threw: `ClaudeHeroRepositoryControls` branches on `code` to tell
5545
- * 'not-connected' apart from a real outage, and the settings card renders the
5546
- * message verbatim.
5547
- *
5548
- * The routes answer `{ error, message }`, so `message` is already the sentence
5549
- * to show. A body carrying only a code — a 405, a bad JSON body — used to read
5550
- * 'Jira is unavailable.' rather than leaking the code as prose, and it still
5551
- * does. Transport failures (a starved pool, an elapsed budget, an older Host
5552
- * without the route) carry their own wording and keep it.
5553
- */
5554
- function jiraFailure(cause) {
5555
- if (cause instanceof JiraClientError) return cause;
5556
- if (!(cause instanceof PluginRequestError)) return new JiraClientError(cause instanceof Error ? cause.message : String(cause));
5557
- return new JiraClientError(cause.message === cause.code ? "Jira is unavailable." : cause.message, cause.code);
5715
+ function feedbackQuery(sessionId, pullNumber, extra) {
5716
+ return {
5717
+ sessionId,
5718
+ number: String(pullNumber),
5719
+ ...extra
5720
+ };
5558
5721
  }
5559
- function payload(value) {
5722
+ function answer(value) {
5560
5723
  const body = record$5(value);
5561
- if (body === void 0) throw new JiraClientError("Invalid Jira response.");
5562
- return body;
5563
- }
5564
- function status(value) {
5565
- const body = payload(value);
5566
- if (typeof body.connected !== "boolean") throw new JiraClientError("Invalid Jira status response.");
5724
+ if (body === void 0) throw new Error("Invalid pull request feedback response.");
5567
5725
  return body;
5568
5726
  }
5569
- async function loadJiraStatus(signal) {
5570
- try {
5571
- return status(await pluginRead(`${CLAUDE_JIRA_PATH}/status`, "remote", signal));
5572
- } catch (cause) {
5573
- throw jiraFailure(cause);
5574
- }
5727
+ /** Every arm of this route shells out to `gh`, so reads and writes alike take
5728
+ * the remote budget. */
5729
+ async function loadJson(path, sessionId, pullNumber, signal, extra) {
5730
+ return answer(await pluginRead(`${CLAUDE_REPOSITORY_FEEDBACK_PATH}${path}`, "remote", signal, { query: feedbackQuery(sessionId, pullNumber, extra) }));
5575
5731
  }
5576
- async function connectJira(input) {
5577
- try {
5578
- return status(await pluginWrite(`${CLAUDE_JIRA_PATH}/connect`, "remote", void 0, { json: input }));
5579
- } catch (cause) {
5580
- throw jiraFailure(cause);
5581
- }
5732
+ async function postJson(path, sessionId, pullNumber, input) {
5733
+ return answer(await pluginWrite(`${CLAUDE_REPOSITORY_FEEDBACK_PATH}${path}`, "remote", void 0, {
5734
+ query: feedbackQuery(sessionId, pullNumber),
5735
+ json: input
5736
+ }));
5582
5737
  }
5583
- async function disconnectJira() {
5584
- try {
5585
- await pluginWrite(`${CLAUDE_JIRA_PATH}/disconnect`, "remote");
5586
- } catch (cause) {
5587
- throw jiraFailure(cause);
5588
- }
5738
+ function reviewComment(value) {
5739
+ const input = record$5(value);
5740
+ if (input === void 0 || typeof input.id !== "number" || typeof input.path !== "string" || typeof input.author !== "string" || typeof input.body !== "string" || typeof input.url !== "string" || input.avatarUrl !== void 0 && githubAvatarUrl(input.avatarUrl) === void 0 || input.side !== "new" && input.side !== "old" || input.line !== void 0 && typeof input.line !== "number" || input.createdAt !== void 0 && typeof input.createdAt !== "string" || input.bot !== void 0 && typeof input.bot !== "boolean") return void 0;
5741
+ return input;
5589
5742
  }
5590
- async function searchJiraTickets(query, signal) {
5591
- try {
5592
- const body = payload(await pluginRead(`${CLAUDE_JIRA_PATH}/search`, "remote", signal, { query: { query } }));
5593
- if (!Array.isArray(body.tickets)) throw new JiraClientError("Invalid Jira search response.");
5594
- const tickets = [];
5595
- for (const item of body.tickets) {
5596
- const ticket = record$5(item);
5597
- if (ticket === void 0 || typeof ticket.key !== "string" || typeof ticket.summary !== "string" || typeof ticket.url !== "string") continue;
5598
- tickets.push(ticket);
5599
- }
5600
- return tickets;
5601
- } catch (cause) {
5602
- throw jiraFailure(cause);
5743
+ async function loadPullRequestThreads(sessionId, pullNumber, signal) {
5744
+ const body = await loadJson("/comments", sessionId, pullNumber, signal);
5745
+ if (!Array.isArray(body.threads)) throw new Error("Invalid pull request comments response.");
5746
+ const threads = [];
5747
+ for (const item of body.threads) {
5748
+ const input = record$5(item);
5749
+ if (input === void 0 || typeof input.id !== "string" || typeof input.path !== "string" || input.side !== "new" && input.side !== "old" || input.line !== void 0 && typeof input.line !== "number" || !Array.isArray(input.comments)) continue;
5750
+ const comments = input.comments.map(reviewComment).filter((value) => value !== void 0);
5751
+ if (comments.length === 0) continue;
5752
+ threads.push({
5753
+ id: input.id,
5754
+ path: input.path,
5755
+ ...typeof input.line === "number" ? { line: input.line } : {},
5756
+ side: input.side,
5757
+ resolved: input.resolved === true,
5758
+ outdated: input.outdated === true,
5759
+ comments
5760
+ });
5603
5761
  }
5762
+ return threads;
5604
5763
  }
5605
- async function assignJiraTicket(key) {
5606
- try {
5607
- await pluginWrite(`${CLAUDE_JIRA_PATH}/assign`, "remote", void 0, { json: { key } });
5608
- } catch (cause) {
5609
- throw jiraFailure(cause);
5610
- }
5764
+ /** Post one reply into the thread that `commentId` belongs to. */
5765
+ async function replyToReviewThread(sessionId, pullNumber, commentId, body) {
5766
+ const comment = reviewComment((await postJson("/reply", sessionId, pullNumber, {
5767
+ commentId,
5768
+ body
5769
+ })).comment);
5770
+ if (comment === void 0) throw new Error("Invalid pull request reply response.");
5771
+ return comment;
5772
+ }
5773
+ /** Resolve or reopen a thread; returns the state GitHub reports afterwards. */
5774
+ async function setReviewThreadResolved(sessionId, pullNumber, threadId, resolved) {
5775
+ const answer = await postJson("/resolve", sessionId, pullNumber, {
5776
+ threadId,
5777
+ resolved
5778
+ });
5779
+ if (typeof answer.resolved !== "boolean") throw new Error("Invalid pull request resolve response.");
5780
+ return answer.resolved;
5781
+ }
5782
+ /** Logins GitHub would notify, for the reply composer's `@` completion. */
5783
+ async function loadMentionableUsers(sessionId, pullNumber, query, signal) {
5784
+ const body = await loadJson("/mentionables", sessionId, pullNumber, signal, { q: query });
5785
+ if (!Array.isArray(body.users)) return [];
5786
+ const users = [];
5787
+ for (const item of body.users) {
5788
+ const input = record$5(item);
5789
+ if (input === void 0 || typeof input.login !== "string" || input.login.length === 0) continue;
5790
+ const avatarUrl = githubAvatarUrl(input.avatarUrl);
5791
+ users.push({
5792
+ login: input.login,
5793
+ ...avatarUrl === void 0 ? {} : { avatarUrl }
5794
+ });
5795
+ }
5796
+ return users;
5797
+ }
5798
+ async function loadFailingChecks(sessionId, pullNumber, signal) {
5799
+ const body = await loadJson("/checks", sessionId, pullNumber, signal);
5800
+ if (!Array.isArray(body.checks)) throw new Error("Invalid pull request checks response.");
5801
+ const checks = [];
5802
+ for (const item of body.checks) {
5803
+ const input = record$5(item);
5804
+ if (input === void 0 || typeof input.name !== "string" || input.link !== void 0 && typeof input.link !== "string" || input.description !== void 0 && typeof input.description !== "string" || input.log !== void 0 && typeof input.log !== "string") continue;
5805
+ checks.push(input);
5806
+ }
5807
+ return checks;
5808
+ }
5809
+ /** Draft handed to Claude when the user forwards GitHub review comments. A
5810
+ * resolved thread is a settled conversation: forwarding it would ask Claude to
5811
+ * redo work the reviewers already signed off. */
5812
+ function composeCommentsPrompt(threads) {
5813
+ const open = threads.filter((thread) => !thread.resolved);
5814
+ if (open.length === 0) return "";
5815
+ return `Please address the following GitHub pull request review comments. Make the requested changes, or explain briefly when a comment should not be applied.\n\n${open.map((thread) => {
5816
+ const [first, ...rest] = thread.comments;
5817
+ if (first === void 0) return "";
5818
+ return [`- ${`${thread.path}${thread.line === void 0 ? "" : `:${thread.line}`}`} (@${first.author}): ${first.body.replaceAll("\n", "\n ")}`, ...rest.map((reply) => ` (@${reply.author}): ${reply.body.replaceAll("\n", "\n ")}`)].join("\n");
5819
+ }).filter((block) => block.length > 0).join("\n")}`;
5820
+ }
5821
+ /** Draft handed to Claude when the user forwards failing CI checks. */
5822
+ function composeChecksPrompt(checks) {
5823
+ return `The following CI checks are failing on the current pull request. Investigate the failure logs, fix the underlying problems, and re-run the relevant commands locally when possible.\n\n${checks.map((check) => {
5824
+ return `${`## ${check.name}${check.link === void 0 ? "" : ` (${check.link})`}`}${check.description === void 0 ? "" : `\n${check.description}`}${check.log === void 0 ? "" : `\n\n\`\`\`\n${check.log}\n\`\`\``}`;
5825
+ }).join("\n\n")}`;
5826
+ }
5827
+ /** Draft handed to Claude after an update-branch merge left conflicts behind. */
5828
+ function composeConflictsPrompt(baseBranch, conflicts, method = "merge") {
5829
+ const list = conflicts.map((file) => `- ${file}`).join("\n");
5830
+ if (method === "rebase") return `Rebasing the current branch onto origin/${baseBranch} stopped on conflicts in the files below. Resolve each conflict preserving the intent of both sides, stage the files, run \`git rebase --continue\` until the rebase finishes, then push with \`git push --force-with-lease\`.\n\n${list}`;
5831
+ return `Merging origin/${baseBranch} into the current branch left merge conflicts in the files below. Resolve each conflict preserving the intent of both sides, then commit the merge.\n\n${list}`;
5832
+ }
5833
+ //#endregion
5834
+ //#region src/client/auto-fix.ts
5835
+ const AUTO_FIX_INTERVAL_MS = 3e4;
5836
+ const AUTO_FIX_FOOTER = "This request was generated automatically by the pull request watcher. After making the changes, commit and push to the pull request branch so the checks re-run.";
5837
+ const EMPTY_MEMORY = { handledCommentIds: /* @__PURE__ */ new Set() };
5838
+ const sessions = /* @__PURE__ */ new Map();
5839
+ function session(sessionId) {
5840
+ let entry = sessions.get(sessionId);
5841
+ if (entry === void 0) {
5842
+ entry = {
5843
+ enabled: false,
5844
+ memory: EMPTY_MEMORY
5845
+ };
5846
+ sessions.set(sessionId, entry);
5847
+ }
5848
+ return entry;
5849
+ }
5850
+ function autoFixEnabled(sessionId) {
5851
+ return session(sessionId).enabled;
5852
+ }
5853
+ function setAutoFixEnabled(sessionId, enabled) {
5854
+ session(sessionId).enabled = enabled;
5855
+ }
5856
+ function autoFixMemory(sessionId) {
5857
+ return session(sessionId).memory;
5858
+ }
5859
+ function rememberAutoFix(sessionId, memory) {
5860
+ session(sessionId).memory = memory;
5861
+ }
5862
+ /** One failing CI run yields one fix attempt: run links change when CI re-runs. */
5863
+ function checksSignature(checks) {
5864
+ if (checks.length === 0) return void 0;
5865
+ return checks.map((check) => `${check.name}|${check.link ?? ""}`).sort().join("\n");
5866
+ }
5867
+ function planAutoFix(memory, threads, checks) {
5868
+ const unhandled = threads.filter((thread) => !thread.resolved).filter((thread) => thread.comments.some((comment) => !memory.handledCommentIds.has(comment.id)));
5869
+ const fresh = unhandled.flatMap((thread) => thread.comments);
5870
+ const signature = checksSignature(checks);
5871
+ const checksChanged = signature !== void 0 && signature !== memory.handledChecksSignature;
5872
+ const sections = [];
5873
+ if (unhandled.length > 0) sections.push(composeCommentsPrompt(unhandled));
5874
+ if (checksChanged) sections.push(composeChecksPrompt(checks));
5875
+ if (sections.length === 0) return { memory };
5876
+ const nextSignature = checksChanged ? signature : memory.handledChecksSignature;
5877
+ return {
5878
+ prompt: `${sections.join("\n\n")}\n\n${AUTO_FIX_FOOTER}`,
5879
+ memory: {
5880
+ handledCommentIds: /* @__PURE__ */ new Set([...memory.handledCommentIds, ...fresh.map((comment) => comment.id)]),
5881
+ ...nextSignature === void 0 ? {} : { handledChecksSignature: nextSignature }
5882
+ }
5883
+ };
5884
+ }
5885
+ //#endregion
5886
+ //#region src/client/session-preset.ts
5887
+ /**
5888
+ * Resolve one row's preset id, newest seat first.
5889
+ * @param row - a session-list row, or undefined when the id is not listed.
5890
+ * @returns the preset id, or undefined when neither source carries one.
5891
+ */
5892
+ function sessionRowPreset(row) {
5893
+ return row?.agentPreset ?? row?.projectionValues?.agentPreset ?? void 0;
5894
+ }
5895
+ //#endregion
5896
+ //#region src/client/ClaudePullRequestsPanel.tsx
5897
+ const NO_WORKSPACE_STATE = {};
5898
+ const NO_WORKSPACES = {
5899
+ subscribe: () => () => {},
5900
+ getSnapshot: () => NO_WORKSPACE_STATE
5901
+ };
5902
+ const OVERVIEW_REFRESH_MS = 3e4;
5903
+ /** What a running session is blocked on: the latest permission or question
5904
+ * activity that is still in its started phase. */
5905
+ function overviewAttention(activities) {
5906
+ for (let index = activities.length - 1; index >= 0; index -= 1) {
5907
+ const activity = activities[index];
5908
+ if (activity === void 0 || activity.kind !== "permission" && activity.kind !== "question") continue;
5909
+ return activity.phase === "started" ? activity.kind : void 0;
5910
+ }
5911
+ }
5912
+ /** Claude sessions worth listing: rows still in the host list (byId keeps
5913
+ * deleted and breadcrumb rows), non-blank, non-subagent, with a checkout;
5914
+ * running first. */
5915
+ function claudeSessionRows(state, archivedSessionIds = []) {
5916
+ const rows = state.ids === void 0 ? Object.values(state.byId) : state.ids.map((id) => state.byId[id]);
5917
+ const archived = new Set(archivedSessionIds);
5918
+ return rows.filter((row) => row !== void 0 && sessionRowPreset(row) === "claude" && row.blank !== true && row.origin !== "subagent" && !archived.has(row.id) && typeof row.cwd === "string").sort((left, right) => Number(right.running === true) - Number(left.running === true) || (left.displayTitle ?? left.id).localeCompare(right.displayTitle ?? right.id));
5919
+ }
5920
+ function Badge({ label, tone = "neutral" }) {
5921
+ const toneStyle = tone === "success" ? repositoryItemSuccess : tone === "warning" ? repositoryItemWarning : tone === "error" ? repositoryItemError : tone === "merged" ? { color: "#a78bfa" } : {};
5922
+ return /* @__PURE__ */ jsxs("span", {
5923
+ style: {
5924
+ ...repositoryItem,
5925
+ ...toneStyle
5926
+ },
5927
+ children: [/* @__PURE__ */ jsx("span", {
5928
+ style: repositoryItemDot,
5929
+ "aria-hidden": "true"
5930
+ }), /* @__PURE__ */ jsx("span", {
5931
+ style: repositoryItemLabel,
5932
+ children: label
5933
+ })]
5934
+ });
5935
+ }
5936
+ function repositoryName$1(remote) {
5937
+ return remote?.split("/").at(-1);
5938
+ }
5939
+ function OverviewAttention({ source, running, t }) {
5940
+ const snapshot = useSyncExternalStore(source.subscribe, source.getSnapshot, source.getSnapshot);
5941
+ const attention = running ? overviewAttention(snapshot.activities) : void 0;
5942
+ const usage = snapshot.contextUsage;
5943
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
5944
+ attention === "permission" ? /* @__PURE__ */ jsx(Badge, {
5945
+ label: t("overviewNeedsPermission"),
5946
+ tone: "warning"
5947
+ }) : null,
5948
+ attention === "question" ? /* @__PURE__ */ jsx(Badge, {
5949
+ label: t("overviewNeedsAnswer"),
5950
+ tone: "warning"
5951
+ }) : null,
5952
+ usage === void 0 ? null : /* @__PURE__ */ jsx("span", { children: t("overviewContextUsage", { percentage: usage.percentage }) })
5953
+ ] });
5954
+ }
5955
+ function ClaudePullRequestsPanel({ t, closeDetails, openSession, loadStatus, sessions, workspaces, projectionFor }) {
5956
+ const sessionStore = useMemo(() => ({
5957
+ subscribe: (listener) => sessions.subscribe(listener),
5958
+ getSnapshot: () => sessions.getSnapshot()
5959
+ }), [sessions]);
5960
+ const snapshot = useSyncExternalStore(sessionStore.subscribe, sessionStore.getSnapshot, sessionStore.getSnapshot);
5961
+ const workspaceStore = useMemo(() => {
5962
+ const source = workspaces ?? NO_WORKSPACES;
5963
+ return {
5964
+ subscribe: (listener) => source.subscribe(listener),
5965
+ getSnapshot: () => source.getSnapshot()
5966
+ };
5967
+ }, [workspaces]);
5968
+ const workspaceState = useSyncExternalStore(workspaceStore.subscribe, workspaceStore.getSnapshot, workspaceStore.getSnapshot);
5969
+ const rows = useMemo(() => claudeSessionRows(snapshot, workspaceState.archivedSessionIds ?? []), [snapshot, workspaceState]);
5970
+ const cwdKey = useMemo(() => [...new Set(rows.map((row) => row.cwd ?? ""))].sort().join("\0"), [rows]);
5971
+ const [statuses, setStatuses] = useState({});
5972
+ useEffect(() => {
5973
+ const cwds = cwdKey.length === 0 ? [] : cwdKey.split("\0");
5974
+ if (cwds.length === 0) return;
5975
+ const controller = new AbortController();
5976
+ const refresh = () => {
5977
+ for (const cwd of cwds) loadStatus(cwd, controller.signal).then((status) => {
5978
+ if (!controller.signal.aborted) setStatuses((previous) => ({
5979
+ ...previous,
5980
+ [cwd]: status
5981
+ }));
5982
+ }, () => void 0);
5983
+ };
5984
+ refresh();
5985
+ const timer = setInterval(refresh, OVERVIEW_REFRESH_MS);
5986
+ return () => {
5987
+ controller.abort();
5988
+ clearInterval(timer);
5989
+ };
5990
+ }, [cwdKey, loadStatus]);
5991
+ return /* @__PURE__ */ jsxs("div", {
5992
+ className: detailsCardClass,
5993
+ style: tasksPanel,
5994
+ children: [
5995
+ /* @__PURE__ */ jsxs("style", {
5996
+ "data-dsh-claude-overview-styles": true,
5997
+ children: [detailsCardCss, panelIconButtonCss]
5998
+ }),
5999
+ /* @__PURE__ */ jsxs("header", {
6000
+ style: tasksHeader,
6001
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
6002
+ style: tasksHeading,
6003
+ children: t("overviewTitle")
6004
+ }), /* @__PURE__ */ jsx("span", {
6005
+ style: tasksTurnMeta,
6006
+ children: t("overviewBody")
6007
+ })] }), /* @__PURE__ */ jsx("button", {
6008
+ type: "button",
6009
+ className: panelIconButtonClass,
6010
+ "aria-label": t("diffClose"),
6011
+ onClick: closeDetails,
6012
+ children: /* @__PURE__ */ jsx(IconCloseOutline16, {})
6013
+ })]
6014
+ }),
6015
+ /* @__PURE__ */ jsx("div", {
6016
+ style: overviewBody,
6017
+ children: rows.length === 0 ? /* @__PURE__ */ jsx("p", {
6018
+ style: overviewEmpty,
6019
+ children: t("overviewEmpty")
6020
+ }) : rows.map((row) => {
6021
+ const repository = row.cwd === void 0 ? void 0 : statuses[row.cwd];
6022
+ const pullRequest = repository?.pullRequest;
6023
+ const branch = repository?.status === "ready" ? repository.detached === true ? t("repositoryDetached") : repository.branch ?? t("repositoryUnknownBranch") : repository === void 0 ? t("overviewLoading") : t("repositoryUnavailable");
6024
+ return /* @__PURE__ */ jsxs("button", {
6025
+ type: "button",
6026
+ style: overviewRow,
6027
+ onClick: () => {
6028
+ openSession(row.id);
6029
+ },
6030
+ children: [/* @__PURE__ */ jsxs("span", {
6031
+ style: overviewRowTop,
6032
+ children: [
6033
+ row.running === true ? /* @__PURE__ */ jsx("span", {
6034
+ style: overviewRunningDot,
6035
+ "aria-label": t("overviewRunning")
6036
+ }) : null,
6037
+ /* @__PURE__ */ jsx("span", {
6038
+ style: overviewTitle,
6039
+ children: row.displayTitle ?? row.id
6040
+ }),
6041
+ pullRequest === void 0 ? /* @__PURE__ */ jsx(Badge, { label: t("overviewNoPr") }) : /* @__PURE__ */ jsx(Badge, {
6042
+ label: `#${pullRequest.number} · ${t(`repositoryState_${pullRequest.state}`)}`,
6043
+ tone: pullRequest.state === "merged" ? "merged" : pullRequest.state === "open" ? "success" : "neutral"
6044
+ })
6045
+ ]
6046
+ }), /* @__PURE__ */ jsxs("span", {
6047
+ style: overviewMeta,
6048
+ children: [
6049
+ repositoryName$1(repository?.remote) === void 0 ? null : /* @__PURE__ */ jsx("span", { children: repositoryName$1(repository?.remote) }),
6050
+ /* @__PURE__ */ jsx("span", {
6051
+ style: overviewBranch,
6052
+ children: branch
6053
+ }),
6054
+ pullRequest?.state === "open" && pullRequest.checks !== "none" ? /* @__PURE__ */ jsx(Badge, {
6055
+ label: t(`repositoryChecks_${pullRequest.checks}`),
6056
+ tone: pullRequest.checks === "passing" ? "success" : pullRequest.checks === "failing" ? "error" : "warning"
6057
+ }) : null,
6058
+ pullRequest?.state === "open" && pullRequest.review !== "none" ? /* @__PURE__ */ jsx(Badge, {
6059
+ label: t(`repositoryReview_${pullRequest.review}`),
6060
+ tone: pullRequest.review === "approved" ? "success" : pullRequest.review === "changes-requested" ? "error" : "neutral"
6061
+ }) : null,
6062
+ autoFixEnabled(row.id) ? /* @__PURE__ */ jsx(Badge, {
6063
+ label: t("overviewAutoFix"),
6064
+ tone: "success"
6065
+ }) : null,
6066
+ projectionFor === void 0 ? null : /* @__PURE__ */ jsx(OverviewAttention, {
6067
+ source: projectionFor(row.id),
6068
+ running: row.running === true,
6069
+ t
6070
+ })
6071
+ ]
6072
+ })]
6073
+ }, row.id);
6074
+ })
6075
+ })
6076
+ ]
6077
+ });
6078
+ }
6079
+ //#endregion
6080
+ //#region src/client/session-alerts.ts
6081
+ /** Desktop notifications for the sessions the user is not looking at.
6082
+ *
6083
+ * The session board already knows which session is blocked on an approval or
6084
+ * a question and which has gone quiet — but only while it is open, which
6085
+ * makes the user the poller. Running several worktree sessions at once is the
6086
+ * workflow this plugin is built for, and it is the one thing that gets worse
6087
+ * the more of them there are.
6088
+ *
6089
+ * Everything here is derived from the two feeds the board already reads, so a
6090
+ * standing watcher costs one more subscriber on the shared projection
6091
+ * carrier rather than a stream per session.
6092
+ */
6093
+ /** ponytail: module state, like the auto-fix toggle next door. The Settings
6094
+ * panel and the boot read both write it, and the watcher reads it per alert,
6095
+ * so switching alerts off lands without a reload. */
6096
+ let alertsEnabled = true;
6097
+ function claudeAlertsEnabled() {
6098
+ return alertsEnabled;
6099
+ }
6100
+ function setClaudeAlertsEnabled(enabled) {
6101
+ alertsEnabled = enabled;
6102
+ }
6103
+ /** The alert one observation earns, or undefined when nothing happened that is
6104
+ * worth interrupting the user for.
6105
+ *
6106
+ * A session observed for the first time earns nothing: the watcher starts
6107
+ * with every session unknown, and announcing the state each one merely
6108
+ * happens to be in would greet a restart with a burst. */
6109
+ function sessionAlert(previous, next) {
6110
+ if (previous === void 0) return void 0;
6111
+ if (next.attention !== void 0 && next.attention !== previous.attention) return next.attention;
6112
+ if (previous.running && !next.running) return "idle";
6113
+ }
6114
+ /** Deliver one alert as a desktop notification.
6115
+ *
6116
+ * Best effort throughout: a Host without the Notification API, or a user who
6117
+ * has refused permission, simply gets no alerts. The tag collapses repeat
6118
+ * alerts for one session into a single banner rather than a stack. */
6119
+ function postSessionAlert(alert) {
6120
+ if (typeof Notification === "undefined" || Notification.permission === "denied") return;
6121
+ const show = () => {
6122
+ try {
6123
+ const notification = new Notification(alert.title, {
6124
+ body: alert.body,
6125
+ tag: `dsh-claude:${alert.sessionId}`
6126
+ });
6127
+ notification.onclick = () => {
6128
+ globalThis.focus?.();
6129
+ alert.open();
6130
+ };
6131
+ } catch {}
6132
+ };
6133
+ if (Notification.permission === "granted") show();
6134
+ else Notification.requestPermission().then((result) => {
6135
+ if (result === "granted") show();
6136
+ }, () => void 0);
6137
+ }
6138
+ const BODY_KEY = {
6139
+ permission: "alertNeedsPermission",
6140
+ question: "alertNeedsAnswer",
6141
+ idle: "alertTurnFinished"
6142
+ };
6143
+ /** Watch every Claude session and announce the ones that need the user.
6144
+ * Returns the unsubscriber. */
6145
+ function startClaudeSessionAlerts(deps) {
6146
+ const post = deps.post ?? postSessionAlert;
6147
+ const known = /* @__PURE__ */ new Map();
6148
+ const projections = /* @__PURE__ */ new Map();
6149
+ let disposed = false;
6150
+ const announce = (row, kind) => {
6151
+ if (!(deps.enabled ?? claudeAlertsEnabled)()) return;
6152
+ post({
6153
+ sessionId: row.id,
6154
+ title: row.displayTitle ?? deps.t("alertFallbackTitle"),
6155
+ body: deps.t(BODY_KEY[kind]),
6156
+ open: () => {
6157
+ deps.open(row.id);
6158
+ }
6159
+ });
6160
+ };
6161
+ const evaluate = () => {
6162
+ if (disposed) return;
6163
+ const snapshot = deps.sessions.getSnapshot();
6164
+ const rows = claudeSessionRows(snapshot);
6165
+ const live = new Set(rows.map((row) => row.id));
6166
+ for (const [sessionId, unsubscribe] of [...projections]) {
6167
+ if (live.has(sessionId)) continue;
6168
+ unsubscribe();
6169
+ projections.delete(sessionId);
6170
+ known.delete(sessionId);
6171
+ }
6172
+ for (const row of rows) {
6173
+ const source = deps.projectionFor(row.id);
6174
+ if (!projections.has(row.id)) projections.set(row.id, source.subscribe(evaluate));
6175
+ const running = row.running === true;
6176
+ const next = {
6177
+ running,
6178
+ attention: running ? overviewAttention(source.getSnapshot().activities) : void 0
6179
+ };
6180
+ const previous = known.get(row.id);
6181
+ known.set(row.id, next);
6182
+ if (row.id === snapshot.current) continue;
6183
+ const kind = sessionAlert(previous, next);
6184
+ if (kind !== void 0) announce(row, kind);
6185
+ }
6186
+ };
6187
+ const unsubscribe = deps.sessions.subscribe(evaluate);
6188
+ evaluate();
6189
+ return () => {
6190
+ disposed = true;
6191
+ unsubscribe();
6192
+ for (const dispose of projections.values()) dispose();
6193
+ projections.clear();
6194
+ known.clear();
6195
+ };
6196
+ }
6197
+ //#endregion
6198
+ //#region src/client/jira-api.ts
6199
+ var JiraClientError = class extends Error {
6200
+ code;
6201
+ constructor(message, code) {
6202
+ super(message);
6203
+ this.name = "JiraClientError";
6204
+ if (code !== void 0) this.code = code;
6205
+ }
6206
+ };
6207
+ function record$4(value) {
6208
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
6209
+ }
6210
+ /**
6211
+ * Every Jira failure the panels catch is a `JiraClientError`, whatever the
6212
+ * transport threw: `ClaudeHeroRepositoryControls` branches on `code` to tell
6213
+ * 'not-connected' apart from a real outage, and the settings card renders the
6214
+ * message verbatim.
6215
+ *
6216
+ * The routes answer `{ error, message }`, so `message` is already the sentence
6217
+ * to show. A body carrying only a code — a 405, a bad JSON body — used to read
6218
+ * 'Jira is unavailable.' rather than leaking the code as prose, and it still
6219
+ * does. Transport failures (a starved pool, an elapsed budget, an older Host
6220
+ * without the route) carry their own wording and keep it.
6221
+ */
6222
+ function jiraFailure(cause) {
6223
+ if (cause instanceof JiraClientError) return cause;
6224
+ if (!(cause instanceof PluginRequestError)) return new JiraClientError(cause instanceof Error ? cause.message : String(cause));
6225
+ return new JiraClientError(cause.message === cause.code ? "Jira is unavailable." : cause.message, cause.code);
6226
+ }
6227
+ function payload(value) {
6228
+ const body = record$4(value);
6229
+ if (body === void 0) throw new JiraClientError("Invalid Jira response.");
6230
+ return body;
6231
+ }
6232
+ function status(value) {
6233
+ const body = payload(value);
6234
+ if (typeof body.connected !== "boolean") throw new JiraClientError("Invalid Jira status response.");
6235
+ return body;
6236
+ }
6237
+ async function loadJiraStatus(signal) {
6238
+ try {
6239
+ return status(await pluginRead(`${CLAUDE_JIRA_PATH}/status`, "remote", signal));
6240
+ } catch (cause) {
6241
+ throw jiraFailure(cause);
6242
+ }
6243
+ }
6244
+ async function connectJira(input) {
6245
+ try {
6246
+ return status(await pluginWrite(`${CLAUDE_JIRA_PATH}/connect`, "remote", void 0, { json: input }));
6247
+ } catch (cause) {
6248
+ throw jiraFailure(cause);
6249
+ }
6250
+ }
6251
+ async function disconnectJira() {
6252
+ try {
6253
+ await pluginWrite(`${CLAUDE_JIRA_PATH}/disconnect`, "remote");
6254
+ } catch (cause) {
6255
+ throw jiraFailure(cause);
6256
+ }
6257
+ }
6258
+ async function searchJiraTickets(query, signal) {
6259
+ try {
6260
+ const body = payload(await pluginRead(`${CLAUDE_JIRA_PATH}/search`, "remote", signal, { query: { query } }));
6261
+ if (!Array.isArray(body.tickets)) throw new JiraClientError("Invalid Jira search response.");
6262
+ const tickets = [];
6263
+ for (const item of body.tickets) {
6264
+ const ticket = record$4(item);
6265
+ if (ticket === void 0 || typeof ticket.key !== "string" || typeof ticket.summary !== "string" || typeof ticket.url !== "string") continue;
6266
+ tickets.push(ticket);
6267
+ }
6268
+ return tickets;
6269
+ } catch (cause) {
6270
+ throw jiraFailure(cause);
6271
+ }
6272
+ }
6273
+ async function assignJiraTicket(key) {
6274
+ try {
6275
+ await pluginWrite(`${CLAUDE_JIRA_PATH}/assign`, "remote", void 0, { json: { key } });
6276
+ } catch (cause) {
6277
+ throw jiraFailure(cause);
6278
+ }
5611
6279
  }
5612
6280
  /** Draft seeded into the composer when a session starts from a ticket. */
5613
6281
  function ticketPrompt(ticket) {
@@ -5841,6 +6509,12 @@ window.__ModuleLoader__.load({
5841
6509
  const value = settings.find((setting) => setting.key === "prose")?.value;
5842
6510
  return isClaudeProseMode(value) ? value : DEFAULT_CLAUDE_PROSE_MODE;
5843
6511
  }
6512
+ /** The alert mode a settings payload carries, read the same way and for the
6513
+ * same reason as {@link proseModeOf}. */
6514
+ function alertModeOf(settings) {
6515
+ const value = settings.find((setting) => setting.key === "alerts")?.value;
6516
+ return isClaudeAlertMode(value) ? value : "on";
6517
+ }
5844
6518
  /** Settings whose row only makes sense under a particular value of another.
5845
6519
  * Filtering here rather than server-side keeps the descriptor list flat: the
5846
6520
  * server has no view of what the Client can paint. Fails OPEN — a payload
@@ -5866,6 +6540,10 @@ window.__ModuleLoader__.load({
5866
6540
  label: "prose",
5867
6541
  hint: "proseEffect"
5868
6542
  },
6543
+ alerts: {
6544
+ label: "alerts",
6545
+ hint: "alertsEffect"
6546
+ },
5869
6547
  worktreeBranchPrefix: {
5870
6548
  label: "worktreeBranchPrefix",
5871
6549
  hint: "worktreeBranchPrefixEffect"
@@ -5886,7 +6564,9 @@ window.__ModuleLoader__.load({
5886
6564
  "renderer:plugin": "rendererPlugin",
5887
6565
  "renderer:native": "rendererNative",
5888
6566
  "prose:plain": "prosePlain",
5889
- "prose:enhanced": "proseEnhanced"
6567
+ "prose:enhanced": "proseEnhanced",
6568
+ "alerts:off": "alertsOff",
6569
+ "alerts:on": "alertsOn"
5890
6570
  };
5891
6571
  function settingOptionLabel(settingKey, option, t) {
5892
6572
  const key = SETTING_OPTION_COPY[`${settingKey}:${option.value}`];
@@ -6126,6 +6806,7 @@ window.__ModuleLoader__.load({
6126
6806
  if (!isGlobalSettingsView(payload)) throw new Error("Invalid global settings response");
6127
6807
  setGlobalSettings(payload);
6128
6808
  applyClaudeMarkdownTheme(proseModeOf(payload.settings));
6809
+ setClaudeAlertsEnabled(alertModeOf(payload.settings) === "on");
6129
6810
  } catch (cause) {
6130
6811
  setGlobalSettingsError(cardFailure(cause));
6131
6812
  } finally {
@@ -6449,30 +7130,555 @@ window.__ModuleLoader__.load({
6449
7130
  children: updateBusy === "check" ? t("checkingUpdates") : t("checkUpdates")
6450
7131
  }), /* @__PURE__ */ jsx("button", {
6451
7132
  type: "button",
6452
- style: primaryButton,
6453
- onClick: () => {
6454
- requestUpdate("update");
7133
+ style: primaryButton,
7134
+ onClick: () => {
7135
+ requestUpdate("update");
7136
+ },
7137
+ disabled: updateBusy !== void 0 || updateStatus?.canUpdate !== true,
7138
+ children: updateBusy === "update" ? t("updatingPlugin") : t("updatePlugin")
7139
+ })]
7140
+ })
7141
+ ]
7142
+ }),
7143
+ /* @__PURE__ */ jsxs("section", {
7144
+ style: settingsCard,
7145
+ children: [/* @__PURE__ */ jsx("h3", {
7146
+ style: settingsSectionHeading,
7147
+ children: t("security")
7148
+ }), /* @__PURE__ */ jsx("p", {
7149
+ style: settingsBody,
7150
+ children: t("securityBody")
7151
+ })]
7152
+ })
7153
+ ]
7154
+ });
7155
+ }
7156
+ //#endregion
7157
+ //#region src/client/plan-feedback-api.ts
7158
+ /** Send one plan back for changes. */
7159
+ async function sendPlanForChanges(sessionId, toolUseId, notes) {
7160
+ try {
7161
+ await pluginWrite(CLAUDE_PLAN_FEEDBACK_PATH, "fast", void 0, {
7162
+ query: { sessionId },
7163
+ json: {
7164
+ toolUseId,
7165
+ notes
7166
+ }
7167
+ });
7168
+ } catch (error) {
7169
+ const settled = error instanceof PluginRequestError && error.reason === "http" && error.status === 409;
7170
+ throw new Error(settled ? "planSettled" : "planFeedbackFailed");
7171
+ }
7172
+ }
7173
+ //#endregion
7174
+ //#region src/client/ClaudePlanPanel.tsx
7175
+ const PLAN_TOOL = "ExitPlanMode";
7176
+ /** Every plan this session handed over, oldest first, each with where its
7177
+ * approval stands.
7178
+ *
7179
+ * The permission bridge writes one `started` record carrying the plan and,
7180
+ * once the user decides, a second record under the same `toolUseId` whose
7181
+ * phase says which way it went. Both arrive on the ordinary activity stream,
7182
+ * so the panel needs no channel of its own — it reads the transcript the
7183
+ * session already has. */
7184
+ function planReviews(activities) {
7185
+ const plans = /* @__PURE__ */ new Map();
7186
+ const states = /* @__PURE__ */ new Map();
7187
+ for (const activity of activities) {
7188
+ const { toolUseId } = activity;
7189
+ if (activity.kind !== "permission" || toolUseId === void 0) continue;
7190
+ if (activity.toolName === PLAN_TOOL && activity.phase === "started" && activity.text !== void 0 && activity.text.length > 0) {
7191
+ plans.set(toolUseId, activity.text);
7192
+ if (!states.has(toolUseId)) states.set(toolUseId, "pending");
7193
+ }
7194
+ if (activity.phase === "completed") states.set(toolUseId, "approved");
7195
+ else if (activity.phase === "denied" || activity.phase === "failed") states.set(toolUseId, "rejected");
7196
+ }
7197
+ return [...plans].map(([toolUseId, plan]) => ({
7198
+ toolUseId,
7199
+ plan,
7200
+ state: states.get(toolUseId) ?? "pending"
7201
+ }));
7202
+ }
7203
+ /** The newest plan, for readers that only care what is on the table now. */
7204
+ function latestPlanReview(activities) {
7205
+ return planReviews(activities).at(-1);
7206
+ }
7207
+ /** A plan's own first heading, so a list of several can name them.
7208
+ *
7209
+ * Falls back to the opening line: a plan without a heading is unusual but
7210
+ * still has to be pickable. Fenced blocks are not scanned — a `#` comment in
7211
+ * the first code block of a heading-less plan is a worse label than the first
7212
+ * line, but not a wrong one, and the cost of getting it exactly right is a
7213
+ * fence-state machine for a fallback. */
7214
+ function planTitle(plan) {
7215
+ for (const raw of plan.split("\n")) {
7216
+ const line = raw.trim();
7217
+ if (line.length === 0) continue;
7218
+ const heading = /^#{1,6}\s+(.+?)\s*#*$/u.exec(line);
7219
+ if (heading?.[1] !== void 0) return heading[1].slice(0, MAX_TITLE_CHARS);
7220
+ return line.slice(0, MAX_TITLE_CHARS);
7221
+ }
7222
+ return "";
7223
+ }
7224
+ const MAX_TITLE_CHARS = 80;
7225
+ const MAX_QUOTE_CHARS = 1e3;
7226
+ /** The passage under the current selection, when it lies inside the plan body.
7227
+ *
7228
+ * Reads the live selection rather than mirroring the DOM: the body is the
7229
+ * Host's Markdown output, which this package renders but does not own, and
7230
+ * the only stable thing about it is that it is inside this element. */
7231
+ function quotedSelection(selection, body) {
7232
+ if (selection === null || body === null || selection.isCollapsed || selection.rangeCount === 0) return void 0;
7233
+ const range = selection.getRangeAt(0);
7234
+ if (!body.contains(range.startContainer) || !body.contains(range.endContainer)) return void 0;
7235
+ const text = selection.toString().trim();
7236
+ return text.length === 0 ? void 0 : text.slice(0, MAX_QUOTE_CHARS);
7237
+ }
7238
+ /** The newest review as one primitive, for readers that only need to know
7239
+ * whether it changed. A snapshot hook keeps its value only while the
7240
+ * selection compares equal, and a fresh object per snapshot would defeat
7241
+ * that. Empty string when the session has proposed nothing. */
7242
+ function planReviewKey(activities) {
7243
+ const review = latestPlanReview(activities);
7244
+ return review === void 0 ? "" : `${review.state}:${review.toolUseId}`;
7245
+ }
7246
+ /** Split what {@link planReviewKey} joined. */
7247
+ function parsePlanReviewKey(key) {
7248
+ const cut = key.indexOf(":");
7249
+ if (cut < 0) return void 0;
7250
+ const state = key.slice(0, cut);
7251
+ if (state !== "pending" && state !== "approved" && state !== "rejected") return void 0;
7252
+ return {
7253
+ state,
7254
+ toolUseId: key.slice(cut + 1)
7255
+ };
7256
+ }
7257
+ /** Restore-from-maximized: four corners pulling inward. Mirrors the diff
7258
+ * panel's own, which the primitives set has no counterpart for. */
7259
+ function RestorePanelIcon$1() {
7260
+ return /* @__PURE__ */ jsx("svg", {
7261
+ width: "16",
7262
+ height: "16",
7263
+ viewBox: "0 0 16 16",
7264
+ fill: "currentColor",
7265
+ "aria-hidden": "true",
7266
+ children: /* @__PURE__ */ jsx("path", { d: "M1.5 5h3V2h1.4v4.4H1.5V5Zm9.9-3h1.4v3h3v1.4h-4.4V2ZM1.5 9.6h4.4V14H4.5v-3h-3V9.6Zm9.9 0h4.4V11h-3v3h-1.4V9.6Z" })
7267
+ });
7268
+ }
7269
+ /** Card and rows reproduce the primitives' menu surface, like the session
7270
+ * menu next door: r7, inverted hairline, shadow-lv3, 2px inset. */
7271
+ const PICKER_CSS = [
7272
+ ".dsh-claude-plan-picker-root{position:relative;display:inline-flex;min-width:0}",
7273
+ ".dsh-claude-plan-picker{display:inline-flex;align-items:center;gap:4px;min-width:0;padding:2px 6px;",
7274
+ "margin:-2px -6px;border:0;border-radius:7px;background:transparent;color:inherit;font:inherit;cursor:pointer;",
7275
+ "transition:background .12s ease}",
7276
+ ".dsh-claude-plan-picker:hover,.dsh-claude-plan-picker[aria-expanded=\"true\"]{background:var(--dsw-alias-interactive-bg-hover)}",
7277
+ ".dsh-claude-plan-picker:focus-visible{outline:none;background:var(--dsw-alias-interactive-bg-hover)}",
7278
+ ".dsh-claude-plan-picker>svg{flex:none;transition:transform .12s ease}",
7279
+ ".dsh-claude-plan-picker[aria-expanded=\"true\"]>svg{transform:rotate(180deg)}",
7280
+ ".dsh-claude-plan-picker-card{box-sizing:border-box;position:absolute;top:calc(100% + 6px);left:-6px;z-index:100;",
7281
+ "display:flex;flex-direction:column;gap:1px;padding:2px;min-width:240px;max-width:min(420px,70vw);",
7282
+ "max-height:min(320px,50vh);overflow-y:auto;border:1px solid var(--dsw-alias-border-inverted);",
7283
+ "border-radius:7px;background:var(--dsw-specific-menu);box-shadow:var(--dsw-shadow-lv3)}",
7284
+ ".dsh-claude-plan-picker-item{display:flex;align-items:center;gap:8px;width:100%;min-height:26px;",
7285
+ "padding:5px 8px;border:0;border-radius:5px;background:transparent;color:var(--dsw-alias-label-primary);",
7286
+ "font:inherit;font-size:12px;line-height:17px;text-align:left;cursor:pointer}",
7287
+ ".dsh-claude-plan-picker-item:hover,.dsh-claude-plan-picker-item:focus-visible{outline:none;",
7288
+ "background:var(--dsw-alias-interactive-bg-hover)}",
7289
+ ".dsh-claude-plan-picker-item[aria-current=\"true\"]{background:var(--dsw-alias-interactive-bg-hover)}",
7290
+ ".dsh-claude-plan-picker-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
7291
+ ".dsh-claude-plan-picker-ordinal{flex:none;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}"
7292
+ ].join("");
7293
+ function ChevronDownIcon() {
7294
+ return /* @__PURE__ */ jsx("svg", {
7295
+ width: "12",
7296
+ height: "12",
7297
+ viewBox: "0 0 12 12",
7298
+ fill: "none",
7299
+ "aria-hidden": "true",
7300
+ children: /* @__PURE__ */ jsx("path", {
7301
+ d: "m3 4.75 3 3 3-3",
7302
+ stroke: "currentColor",
7303
+ strokeWidth: "1.5",
7304
+ strokeLinecap: "round",
7305
+ strokeLinejoin: "round"
7306
+ })
7307
+ });
7308
+ }
7309
+ const STATE_LABEL = {
7310
+ pending: "planPending",
7311
+ approved: "planApproved",
7312
+ rejected: "planRejected"
7313
+ };
7314
+ /** The plan behind an `ExitPlanMode` approval, as the document it was written
7315
+ * as. The decision itself stays with the Host's approval dialog; this panel
7316
+ * is where the plan is actually read, under the reader's own prose palette. */
7317
+ function ClaudePlanPanel({ useClaudeProjection, t, sessionId, closeDetails, maximized, toggleMaximized, submitChanges = sendPlanForChanges }) {
7318
+ const markdownLabels = useClaudeMarkdownLabels(t);
7319
+ const owned = useClaudeProjection((projection) => projection.owned);
7320
+ const reviews = planReviews(useClaudeProjection((projection) => projection.activities));
7321
+ const [chosen, setChosen] = useState();
7322
+ const [open, setOpen] = useState(false);
7323
+ const picker = useRef(null);
7324
+ useDismissOnOutsidePointer(picker, open, (next) => {
7325
+ if (!next) setOpen(false);
7326
+ });
7327
+ const pendingId = reviews.find((item) => item.state === "pending")?.toolUseId;
7328
+ useEffect(() => {
7329
+ if (pendingId !== void 0) setChosen(pendingId);
7330
+ }, [pendingId]);
7331
+ const review = reviews.find((item) => item.toolUseId === chosen) ?? reviews.at(-1);
7332
+ const [notes, setNotes] = useState([]);
7333
+ const [draft, setDraft] = useState("");
7334
+ const [quote, setQuote] = useState();
7335
+ const [sending, setSending] = useState(false);
7336
+ const [failure, setFailure] = useState();
7337
+ const body = useRef(null);
7338
+ const composer = useRef(null);
7339
+ useEffect(() => {
7340
+ if (typeof document === "undefined") return;
7341
+ const read = () => {
7342
+ const selected = quotedSelection(document.getSelection(), body.current);
7343
+ if (selected !== void 0) setQuote(selected);
7344
+ };
7345
+ document.addEventListener("selectionchange", read);
7346
+ return () => {
7347
+ document.removeEventListener("selectionchange", read);
7348
+ };
7349
+ }, []);
7350
+ useEffect(() => {
7351
+ setNotes([]);
7352
+ setDraft("");
7353
+ setQuote(void 0);
7354
+ setFailure(void 0);
7355
+ }, [review?.toolUseId]);
7356
+ const addNote = useCallback(() => {
7357
+ const text = draft.trim();
7358
+ if (text.length === 0) return;
7359
+ setNotes((current) => [...current, quote === void 0 ? { text } : {
7360
+ quote,
7361
+ text
7362
+ }]);
7363
+ setDraft("");
7364
+ setQuote(void 0);
7365
+ }, [draft, quote]);
7366
+ const send = useCallback(() => {
7367
+ if (review === void 0 || sending) return;
7368
+ const text = draft.trim();
7369
+ const pending = text.length === 0 ? notes : [...notes, quote === void 0 ? { text } : {
7370
+ quote,
7371
+ text
7372
+ }];
7373
+ if (pending.length === 0) return;
7374
+ setSending(true);
7375
+ setFailure(void 0);
7376
+ submitChanges(sessionId, review.toolUseId, pending).then(() => {
7377
+ setNotes([]);
7378
+ setDraft("");
7379
+ setQuote(void 0);
7380
+ setSending(false);
7381
+ }, (error) => {
7382
+ setFailure(error instanceof Error && error.message === "planSettled" ? "planSettled" : "planFeedbackFailed");
7383
+ setSending(false);
7384
+ });
7385
+ }, [
7386
+ draft,
7387
+ notes,
7388
+ quote,
7389
+ review,
7390
+ sending,
7391
+ sessionId,
7392
+ submitChanges
7393
+ ]);
7394
+ useEffect(() => {
7395
+ if (!owned || review === void 0) closeDetails();
7396
+ }, [
7397
+ closeDetails,
7398
+ owned,
7399
+ review === void 0
7400
+ ]);
7401
+ if (!owned) return null;
7402
+ const index = review === void 0 ? -1 : reviews.findIndex((item) => item.toolUseId === review.toolUseId);
7403
+ const badge = (state) => ({
7404
+ ...planBadge,
7405
+ ...state === "pending" ? planBadgePending : state === "rejected" ? planBadgeRejected : {}
7406
+ });
7407
+ return /* @__PURE__ */ jsxs("div", {
7408
+ className: detailsCardClass,
7409
+ style: {
7410
+ ...tasksPanel,
7411
+ ...maximized ? diffPanelMaximized : {}
7412
+ },
7413
+ children: [
7414
+ /* @__PURE__ */ jsxs("style", {
7415
+ "data-dsh-claude-panel-icon-styles": true,
7416
+ children: [
7417
+ detailsCardCss,
7418
+ panelIconButtonCss,
7419
+ PICKER_CSS
7420
+ ]
7421
+ }),
7422
+ /* @__PURE__ */ jsxs("div", {
7423
+ style: tasksHeader,
7424
+ children: [/* @__PURE__ */ jsxs("div", {
7425
+ style: planHeaderStart,
7426
+ children: [reviews.length < 2 ? /* @__PURE__ */ jsx("span", {
7427
+ style: tasksHeading,
7428
+ children: t("planPanelTitle")
7429
+ }) : /* @__PURE__ */ jsxs("div", {
7430
+ className: "dsh-claude-plan-picker-root",
7431
+ ref: picker,
7432
+ children: [/* @__PURE__ */ jsxs("button", {
7433
+ type: "button",
7434
+ className: "dsh-claude-plan-picker",
7435
+ "aria-expanded": open,
7436
+ "aria-haspopup": "listbox",
7437
+ onClick: () => {
7438
+ setOpen((value) => !value);
7439
+ },
7440
+ children: [
7441
+ /* @__PURE__ */ jsx("span", {
7442
+ style: tasksHeading,
7443
+ children: t("planPanelTitle")
7444
+ }),
7445
+ /* @__PURE__ */ jsx("span", {
7446
+ style: planCount,
7447
+ children: t("planNth", {
7448
+ index: index + 1,
7449
+ total: reviews.length
7450
+ })
7451
+ }),
7452
+ /* @__PURE__ */ jsx(ChevronDownIcon, {})
7453
+ ]
7454
+ }), !open ? null : /* @__PURE__ */ jsx("div", {
7455
+ className: "dsh-claude-plan-picker-card",
7456
+ role: "listbox",
7457
+ "aria-label": t("planHistory"),
7458
+ children: [...reviews].reverse().map((item, offset) => /* @__PURE__ */ jsxs("button", {
7459
+ type: "button",
7460
+ role: "option",
7461
+ className: "dsh-claude-plan-picker-item",
7462
+ "aria-current": item.toolUseId === review?.toolUseId,
7463
+ "aria-selected": item.toolUseId === review?.toolUseId,
7464
+ onClick: () => {
7465
+ setChosen(item.toolUseId);
7466
+ setOpen(false);
7467
+ },
7468
+ children: [
7469
+ /* @__PURE__ */ jsx("span", {
7470
+ className: "dsh-claude-plan-picker-ordinal",
7471
+ children: reviews.length - offset
7472
+ }),
7473
+ /* @__PURE__ */ jsx("span", {
7474
+ className: "dsh-claude-plan-picker-title",
7475
+ children: planTitle(item.plan)
7476
+ }),
7477
+ /* @__PURE__ */ jsx("span", {
7478
+ style: badge(item.state),
7479
+ children: t(STATE_LABEL[item.state])
7480
+ })
7481
+ ]
7482
+ }, item.toolUseId))
7483
+ })]
7484
+ }), review === void 0 ? null : /* @__PURE__ */ jsx("span", {
7485
+ style: badge(review.state),
7486
+ children: t(STATE_LABEL[review.state])
7487
+ })]
7488
+ }), /* @__PURE__ */ jsxs("div", {
7489
+ style: planHeaderEnd,
7490
+ children: [/* @__PURE__ */ jsx("button", {
7491
+ type: "button",
7492
+ className: panelIconButtonClass,
7493
+ "aria-label": maximized ? t("planRestore") : t("planMaximize"),
7494
+ onClick: toggleMaximized,
7495
+ children: maximized ? /* @__PURE__ */ jsx(RestorePanelIcon$1, {}) : /* @__PURE__ */ jsx(IconFullscreenOutline16, {})
7496
+ }), /* @__PURE__ */ jsx("button", {
7497
+ type: "button",
7498
+ className: panelIconButtonClass,
7499
+ "aria-label": t("planClose"),
7500
+ onClick: closeDetails,
7501
+ children: /* @__PURE__ */ jsx(IconCloseOutline16, {})
7502
+ })]
7503
+ })]
7504
+ }),
7505
+ /* @__PURE__ */ jsx("div", {
7506
+ style: tasksBody,
7507
+ ref: body,
7508
+ children: review === void 0 ? /* @__PURE__ */ jsx("p", {
7509
+ style: tasksGroupEmpty,
7510
+ children: t("planEmpty")
7511
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [review.state === "pending" ? /* @__PURE__ */ jsx("p", {
7512
+ style: planHint,
7513
+ children: t("planPendingHint")
7514
+ }) : null, /* @__PURE__ */ jsx(ClaudeMarkdown, {
7515
+ text: review.plan,
7516
+ labels: markdownLabels
7517
+ }, review.toolUseId)] })
7518
+ }),
7519
+ review?.state !== "pending" ? null : /* @__PURE__ */ jsxs("div", {
7520
+ style: planComposer,
7521
+ children: [
7522
+ notes.length === 0 ? null : /* @__PURE__ */ jsx("ul", {
7523
+ style: planNoteList,
7524
+ children: notes.map((note, index) => /* @__PURE__ */ jsxs("li", {
7525
+ style: planNote,
7526
+ children: [
7527
+ note.quote === void 0 ? null : /* @__PURE__ */ jsx("p", {
7528
+ style: planNoteQuote,
7529
+ children: note.quote
7530
+ }),
7531
+ /* @__PURE__ */ jsx("p", {
7532
+ style: planNoteText,
7533
+ children: note.text
7534
+ }),
7535
+ /* @__PURE__ */ jsx("button", {
7536
+ type: "button",
7537
+ style: planNoteRemove,
7538
+ "aria-label": t("planNoteRemove"),
7539
+ onClick: () => {
7540
+ setNotes((current) => current.filter((_, at) => at !== index));
7541
+ },
7542
+ children: "×"
7543
+ })
7544
+ ]
7545
+ }, index))
7546
+ }),
7547
+ quote === void 0 ? null : /* @__PURE__ */ jsxs("div", {
7548
+ style: planQuoteChip,
7549
+ children: [/* @__PURE__ */ jsx("span", {
7550
+ style: planNoteQuote,
7551
+ children: quote
7552
+ }), /* @__PURE__ */ jsx("button", {
7553
+ type: "button",
7554
+ style: planNoteRemove,
7555
+ "aria-label": t("planQuoteClear"),
7556
+ onClick: () => {
7557
+ setQuote(void 0);
7558
+ },
7559
+ children: "×"
7560
+ })]
7561
+ }),
7562
+ /* @__PURE__ */ jsx("textarea", {
7563
+ ref: composer,
7564
+ value: draft,
7565
+ placeholder: t(quote === void 0 ? "planNotePlaceholder" : "planNoteQuotedPlaceholder"),
7566
+ style: planComposerInput,
7567
+ onChange: (event) => {
7568
+ setDraft(event.currentTarget.value);
7569
+ },
7570
+ onKeyDown: (event) => {
7571
+ if (event.key !== "Enter" || event.nativeEvent.isComposing) return;
7572
+ event.preventDefault();
7573
+ if (event.metaKey || event.ctrlKey) send();
7574
+ else addNote();
7575
+ }
7576
+ }),
7577
+ failure === void 0 ? null : /* @__PURE__ */ jsx("p", {
7578
+ role: "alert",
7579
+ style: planComposerError,
7580
+ children: t(failure)
7581
+ }),
7582
+ /* @__PURE__ */ jsxs("div", {
7583
+ style: planComposerActions,
7584
+ children: [/* @__PURE__ */ jsx("span", {
7585
+ style: planComposerHint,
7586
+ children: t("planNoteHint")
7587
+ }), /* @__PURE__ */ jsx("button", {
7588
+ type: "button",
7589
+ style: {
7590
+ ...askButton,
7591
+ ...askPrimaryButton
6455
7592
  },
6456
- disabled: updateBusy !== void 0 || updateStatus?.canUpdate !== true,
6457
- children: updateBusy === "update" ? t("updatingPlugin") : t("updatePlugin")
7593
+ disabled: sending || notes.length === 0 && draft.trim().length === 0,
7594
+ onClick: send,
7595
+ children: t(sending ? "planSending" : "planSendForChanges")
6458
7596
  })]
6459
7597
  })
6460
7598
  ]
6461
- }),
6462
- /* @__PURE__ */ jsxs("section", {
6463
- style: settingsCard,
6464
- children: [/* @__PURE__ */ jsx("h3", {
6465
- style: settingsSectionHeading,
6466
- children: t("security")
6467
- }), /* @__PURE__ */ jsx("p", {
6468
- style: settingsBody,
6469
- children: t("securityBody")
6470
- })]
6471
7599
  })
6472
7600
  ]
6473
7601
  });
6474
7602
  }
6475
7603
  //#endregion
7604
+ //#region src/client/ClaudePlanHeaderAction.tsx
7605
+ /** Same resting-quiet treatment as the diff action next to it, plus a dot for
7606
+ * a plan still waiting on its approval dialog. */
7607
+ const ACTION_CSS$1 = [
7608
+ ".dsh-claude-header-plan{position:relative;flex:none;display:inline-flex;align-items:center;justify-content:center;",
7609
+ "width:32px;height:32px;padding:0;border:0;border-radius:9px;background:transparent;",
7610
+ "color:var(--dsw-alias-label-secondary);cursor:pointer;",
7611
+ "transition:background .12s ease,color .12s ease}",
7612
+ ".dsh-claude-header-plan:hover,.dsh-claude-header-plan:focus-visible,.dsh-claude-header-plan[aria-pressed=\"true\"]{",
7613
+ "background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
7614
+ ".dsh-claude-header-plan:active{background:var(--dsw-alias-interactive-bg-active)}",
7615
+ ".dsh-claude-header-plan:focus-visible{outline:none}",
7616
+ ".dsh-claude-header-plan>*{flex:none}",
7617
+ ".dsh-claude-header-plan>svg.dsh-claude-header-plan-glyph{",
7618
+ "width:18px;height:18px;display:block;overflow:visible;opacity:1;",
7619
+ "fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}",
7620
+ ".dsh-claude-header-plan[data-pending]::after{content:\"\";position:absolute;top:6px;right:6px;",
7621
+ "width:6px;height:6px;border-radius:999px;background:var(--dsw-alias-state-warning-primary,#e2a03f);",
7622
+ "box-shadow:0 0 0 2px var(--dsw-alias-bg-base)}"
7623
+ ].join("");
7624
+ let cssInjected$3 = false;
7625
+ function ensureCss$3() {
7626
+ if (cssInjected$3 || typeof document === "undefined") return;
7627
+ cssInjected$3 = true;
7628
+ const element = document.createElement("style");
7629
+ element.dataset.dshClaudeHeaderPlan = "";
7630
+ element.textContent = ACTION_CSS$1;
7631
+ document.head.appendChild(element);
7632
+ }
7633
+ /** A written page, not a checklist: a plan is a document to read, and ticks
7634
+ * next to lines are what the task board means. Drawn inline for the same
7635
+ * reason the diff glyph is — the primitives set ships neither. */
7636
+ function PlanGlyph() {
7637
+ return /* @__PURE__ */ jsxs("svg", {
7638
+ className: "dsh-claude-header-plan-glyph",
7639
+ width: "18",
7640
+ height: "18",
7641
+ viewBox: "0 0 18 18",
7642
+ fill: "none",
7643
+ "aria-hidden": "true",
7644
+ focusable: "false",
7645
+ children: [
7646
+ /* @__PURE__ */ jsx("path", { d: "M10.5 2.25H5a1.75 1.75 0 0 0-1.75 1.75v10A1.75 1.75 0 0 0 5 15.75h8A1.75 1.75 0 0 0 14.75 14V6.5Z" }),
7647
+ /* @__PURE__ */ jsx("path", { d: "M10.5 2.25V6.5h4.25" }),
7648
+ /* @__PURE__ */ jsx("path", { d: "M6.25 9h5.5M6.25 12h3.5" })
7649
+ ]
7650
+ });
7651
+ }
7652
+ function ClaudePlanHeaderAction({ t, sessionId, togglePlan, planOpen, useClaudeProjection }) {
7653
+ const owned = useClaudeProjection((projection) => projection.owned);
7654
+ const review = parsePlanReviewKey(useClaudeProjection((projection) => planReviewKey(projection.activities)));
7655
+ const open = useSyncExternalStore(planOpen.subscribe, planOpen.getSnapshot, planOpen.getSnapshot);
7656
+ const opened = useRef();
7657
+ useEffect(() => {
7658
+ if (review?.state !== "pending" || opened.current === review.toolUseId) return;
7659
+ opened.current = review.toolUseId;
7660
+ if (!open) togglePlan();
7661
+ });
7662
+ if (!owned || review === void 0) return null;
7663
+ ensureCss$3();
7664
+ const label = t(open ? "planClose" : "planOpen");
7665
+ return /* @__PURE__ */ jsx(Tooltip, {
7666
+ label,
7667
+ side: "bottom",
7668
+ delayMs: 250,
7669
+ children: /* @__PURE__ */ jsx("button", {
7670
+ type: "button",
7671
+ className: "dsh-claude-header-plan",
7672
+ "aria-label": label,
7673
+ "aria-pressed": open,
7674
+ "data-pending": review.state === "pending" || void 0,
7675
+ "data-session": sessionId,
7676
+ onClick: togglePlan,
7677
+ children: /* @__PURE__ */ jsx(PlanGlyph, {})
7678
+ })
7679
+ });
7680
+ }
7681
+ //#endregion
6476
7682
  //#region src/client/repository-action-api.ts
6477
7683
  var RepositoryActionClientError = class extends Error {
6478
7684
  code;
@@ -6484,7 +7690,7 @@ window.__ModuleLoader__.load({
6484
7690
  if (commit !== void 0) this.commit = commit;
6485
7691
  }
6486
7692
  };
6487
- function record$4(value) {
7693
+ function record$3(value) {
6488
7694
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
6489
7695
  }
6490
7696
  /** The dialog branches on `code`, so a route refusal keeps arriving as this
@@ -6497,20 +7703,20 @@ window.__ModuleLoader__.load({
6497
7703
  return error instanceof PluginRequestError ? new RepositoryActionClientError(error.message, error.code) : error;
6498
7704
  }
6499
7705
  function preview(value) {
6500
- const input = record$4(value);
7706
+ const input = record$3(value);
6501
7707
  if (input === void 0 || typeof input.root !== "string" || typeof input.branch !== "string" || typeof input.head !== "string" || typeof input.fingerprint !== "string" || !Array.isArray(input.files) || typeof input.patch !== "string" || typeof input.truncated !== "boolean" || typeof input.hasStaged !== "boolean" || typeof input.hasUnstaged !== "boolean" || typeof input.hasUntracked !== "boolean" || input.upstream !== void 0 && typeof input.upstream !== "string" || !Array.isArray(input.unpushedCommits) || typeof input.unpushedTruncated !== "boolean") throw new Error("Invalid repository action preview.");
6502
7708
  for (const file of input.files) {
6503
- const item = record$4(file);
7709
+ const item = record$3(file);
6504
7710
  if (item === void 0 || typeof item.path !== "string" || typeof item.staged !== "boolean" || typeof item.unstaged !== "boolean" || typeof item.untracked !== "boolean") throw new Error("Invalid repository action file.");
6505
7711
  }
6506
7712
  for (const commit of input.unpushedCommits) {
6507
- const item = record$4(commit);
7713
+ const item = record$3(commit);
6508
7714
  if (item === void 0 || typeof item.hash !== "string" || typeof item.subject !== "string") throw new Error("Invalid repository action commit.");
6509
7715
  }
6510
7716
  return input;
6511
7717
  }
6512
7718
  function result(value) {
6513
- const input = record$4(value);
7719
+ const input = record$3(value);
6514
7720
  if (input === void 0 || typeof input.commit !== "string" || typeof input.pushed !== "boolean" || input.pullRequestUrl !== void 0 && typeof input.pullRequestUrl !== "string" || input.conflicts !== void 0 && (!Array.isArray(input.conflicts) || input.conflicts.some((item) => typeof item !== "string"))) throw new Error("Invalid repository action result.");
6515
7721
  return input;
6516
7722
  }
@@ -6524,7 +7730,7 @@ window.__ModuleLoader__.load({
6524
7730
  }
6525
7731
  async function generateCommitMessage(sessionId, fingerprint, signal) {
6526
7732
  try {
6527
- const value = record$4(await pluginWrite(`${CLAUDE_REPOSITORY_ACTION_PATH}/message`, "remote", signal, {
7733
+ const value = record$3(await pluginWrite(`${CLAUDE_REPOSITORY_ACTION_PATH}/message`, "remote", signal, {
6528
7734
  query: { sessionId },
6529
7735
  json: { fingerprint }
6530
7736
  }));
@@ -6587,16 +7793,16 @@ window.__ModuleLoader__.load({
6587
7793
  /** The draft only has to describe the work; the host truncates it again before
6588
7794
  * summarizing, and the setup route caps the whole body at 16 KiB. */
6589
7795
  const MAX_INTENT_CHARS = 2e3;
6590
- function record$3(value) {
7796
+ function record$2(value) {
6591
7797
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
6592
7798
  }
6593
7799
  function setupResult(value) {
6594
- const item = record$3(value);
7800
+ const item = record$2(value);
6595
7801
  if (item === void 0 || item.mode !== "checkout" && item.mode !== "worktree" || typeof item.root !== "string" || typeof item.path !== "string" || typeof item.branch !== "string" || item.leaseId !== void 0 && typeof item.leaseId !== "string") return void 0;
6596
7802
  return item;
6597
7803
  }
6598
7804
  function parseRepositorySetupEvent(line, onProgress) {
6599
- const event = record$3(JSON.parse(line));
7805
+ const event = record$2(JSON.parse(line));
6600
7806
  if (event?.type === "progress" && typeof event.stage === "string" && HOST_STAGES.has(event.stage)) {
6601
7807
  onProgress(event.stage);
6602
7808
  return;
@@ -6704,203 +7910,14 @@ window.__ModuleLoader__.load({
6704
7910
  /** Compact age of an ISO timestamp, in the shape the panels already use
6705
7911
  * ("<1h", "4h", "3d", "2mo"). `now` is a parameter so callers that re-render
6706
7912
  * on a clock — and tests — stay deterministic. */
6707
- function relativeAge(value, now = Date.now()) {
6708
- if (value === void 0) return void 0;
6709
- const elapsedHours = Math.max(0, Math.floor((now - Date.parse(value)) / 36e5));
6710
- if (!Number.isFinite(elapsedHours)) return void 0;
6711
- if (elapsedHours < 1) return "<1h";
6712
- if (elapsedHours < 24) return `${elapsedHours}h`;
6713
- const days = Math.floor(elapsedHours / 24);
6714
- return days < 30 ? `${days}d` : `${Math.floor(days / 30)}mo`;
6715
- }
6716
- //#endregion
6717
- //#region src/github-url.ts
6718
- /** Only GitHub's own image hosts; the browser loads these directly, so a URL
6719
- * the API did not vouch for must never become an outbound request. */
6720
- function githubAvatarUrl(value) {
6721
- if (typeof value !== "string" || value.length === 0 || value.length > 1024) return void 0;
6722
- try {
6723
- const url = new URL(value);
6724
- const allowed = url.hostname === "github.com" || url.hostname === "githubusercontent.com" || url.hostname.endsWith(".githubusercontent.com");
6725
- return url.protocol === "https:" && allowed ? url.href : void 0;
6726
- } catch {
6727
- return;
6728
- }
6729
- }
6730
- //#endregion
6731
- //#region src/client/pr-feedback-api.ts
6732
- function record$2(value) {
6733
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
6734
- }
6735
- function feedbackQuery(sessionId, pullNumber, extra) {
6736
- return {
6737
- sessionId,
6738
- number: String(pullNumber),
6739
- ...extra
6740
- };
6741
- }
6742
- function answer(value) {
6743
- const body = record$2(value);
6744
- if (body === void 0) throw new Error("Invalid pull request feedback response.");
6745
- return body;
6746
- }
6747
- /** Every arm of this route shells out to `gh`, so reads and writes alike take
6748
- * the remote budget. */
6749
- async function loadJson(path, sessionId, pullNumber, signal, extra) {
6750
- return answer(await pluginRead(`${CLAUDE_REPOSITORY_FEEDBACK_PATH}${path}`, "remote", signal, { query: feedbackQuery(sessionId, pullNumber, extra) }));
6751
- }
6752
- async function postJson(path, sessionId, pullNumber, input) {
6753
- return answer(await pluginWrite(`${CLAUDE_REPOSITORY_FEEDBACK_PATH}${path}`, "remote", void 0, {
6754
- query: feedbackQuery(sessionId, pullNumber),
6755
- json: input
6756
- }));
6757
- }
6758
- function reviewComment(value) {
6759
- const input = record$2(value);
6760
- if (input === void 0 || typeof input.id !== "number" || typeof input.path !== "string" || typeof input.author !== "string" || typeof input.body !== "string" || typeof input.url !== "string" || input.avatarUrl !== void 0 && githubAvatarUrl(input.avatarUrl) === void 0 || input.side !== "new" && input.side !== "old" || input.line !== void 0 && typeof input.line !== "number" || input.createdAt !== void 0 && typeof input.createdAt !== "string" || input.bot !== void 0 && typeof input.bot !== "boolean") return void 0;
6761
- return input;
6762
- }
6763
- async function loadPullRequestThreads(sessionId, pullNumber, signal) {
6764
- const body = await loadJson("/comments", sessionId, pullNumber, signal);
6765
- if (!Array.isArray(body.threads)) throw new Error("Invalid pull request comments response.");
6766
- const threads = [];
6767
- for (const item of body.threads) {
6768
- const input = record$2(item);
6769
- if (input === void 0 || typeof input.id !== "string" || typeof input.path !== "string" || input.side !== "new" && input.side !== "old" || input.line !== void 0 && typeof input.line !== "number" || !Array.isArray(input.comments)) continue;
6770
- const comments = input.comments.map(reviewComment).filter((value) => value !== void 0);
6771
- if (comments.length === 0) continue;
6772
- threads.push({
6773
- id: input.id,
6774
- path: input.path,
6775
- ...typeof input.line === "number" ? { line: input.line } : {},
6776
- side: input.side,
6777
- resolved: input.resolved === true,
6778
- outdated: input.outdated === true,
6779
- comments
6780
- });
6781
- }
6782
- return threads;
6783
- }
6784
- /** Post one reply into the thread that `commentId` belongs to. */
6785
- async function replyToReviewThread(sessionId, pullNumber, commentId, body) {
6786
- const comment = reviewComment((await postJson("/reply", sessionId, pullNumber, {
6787
- commentId,
6788
- body
6789
- })).comment);
6790
- if (comment === void 0) throw new Error("Invalid pull request reply response.");
6791
- return comment;
6792
- }
6793
- /** Resolve or reopen a thread; returns the state GitHub reports afterwards. */
6794
- async function setReviewThreadResolved(sessionId, pullNumber, threadId, resolved) {
6795
- const answer = await postJson("/resolve", sessionId, pullNumber, {
6796
- threadId,
6797
- resolved
6798
- });
6799
- if (typeof answer.resolved !== "boolean") throw new Error("Invalid pull request resolve response.");
6800
- return answer.resolved;
6801
- }
6802
- /** Logins GitHub would notify, for the reply composer's `@` completion. */
6803
- async function loadMentionableUsers(sessionId, pullNumber, query, signal) {
6804
- const body = await loadJson("/mentionables", sessionId, pullNumber, signal, { q: query });
6805
- if (!Array.isArray(body.users)) return [];
6806
- const users = [];
6807
- for (const item of body.users) {
6808
- const input = record$2(item);
6809
- if (input === void 0 || typeof input.login !== "string" || input.login.length === 0) continue;
6810
- const avatarUrl = githubAvatarUrl(input.avatarUrl);
6811
- users.push({
6812
- login: input.login,
6813
- ...avatarUrl === void 0 ? {} : { avatarUrl }
6814
- });
6815
- }
6816
- return users;
6817
- }
6818
- async function loadFailingChecks(sessionId, pullNumber, signal) {
6819
- const body = await loadJson("/checks", sessionId, pullNumber, signal);
6820
- if (!Array.isArray(body.checks)) throw new Error("Invalid pull request checks response.");
6821
- const checks = [];
6822
- for (const item of body.checks) {
6823
- const input = record$2(item);
6824
- if (input === void 0 || typeof input.name !== "string" || input.link !== void 0 && typeof input.link !== "string" || input.description !== void 0 && typeof input.description !== "string" || input.log !== void 0 && typeof input.log !== "string") continue;
6825
- checks.push(input);
6826
- }
6827
- return checks;
6828
- }
6829
- /** Draft handed to Claude when the user forwards GitHub review comments. A
6830
- * resolved thread is a settled conversation: forwarding it would ask Claude to
6831
- * redo work the reviewers already signed off. */
6832
- function composeCommentsPrompt(threads) {
6833
- const open = threads.filter((thread) => !thread.resolved);
6834
- if (open.length === 0) return "";
6835
- return `Please address the following GitHub pull request review comments. Make the requested changes, or explain briefly when a comment should not be applied.\n\n${open.map((thread) => {
6836
- const [first, ...rest] = thread.comments;
6837
- if (first === void 0) return "";
6838
- return [`- ${`${thread.path}${thread.line === void 0 ? "" : `:${thread.line}`}`} (@${first.author}): ${first.body.replaceAll("\n", "\n ")}`, ...rest.map((reply) => ` (@${reply.author}): ${reply.body.replaceAll("\n", "\n ")}`)].join("\n");
6839
- }).filter((block) => block.length > 0).join("\n")}`;
6840
- }
6841
- /** Draft handed to Claude when the user forwards failing CI checks. */
6842
- function composeChecksPrompt(checks) {
6843
- return `The following CI checks are failing on the current pull request. Investigate the failure logs, fix the underlying problems, and re-run the relevant commands locally when possible.\n\n${checks.map((check) => {
6844
- return `${`## ${check.name}${check.link === void 0 ? "" : ` (${check.link})`}`}${check.description === void 0 ? "" : `\n${check.description}`}${check.log === void 0 ? "" : `\n\n\`\`\`\n${check.log}\n\`\`\``}`;
6845
- }).join("\n\n")}`;
6846
- }
6847
- /** Draft handed to Claude after an update-branch merge left conflicts behind. */
6848
- function composeConflictsPrompt(baseBranch, conflicts, method = "merge") {
6849
- const list = conflicts.map((file) => `- ${file}`).join("\n");
6850
- if (method === "rebase") return `Rebasing the current branch onto origin/${baseBranch} stopped on conflicts in the files below. Resolve each conflict preserving the intent of both sides, stage the files, run \`git rebase --continue\` until the rebase finishes, then push with \`git push --force-with-lease\`.\n\n${list}`;
6851
- return `Merging origin/${baseBranch} into the current branch left merge conflicts in the files below. Resolve each conflict preserving the intent of both sides, then commit the merge.\n\n${list}`;
6852
- }
6853
- //#endregion
6854
- //#region src/client/auto-fix.ts
6855
- const AUTO_FIX_INTERVAL_MS = 3e4;
6856
- const AUTO_FIX_FOOTER = "This request was generated automatically by the pull request watcher. After making the changes, commit and push to the pull request branch so the checks re-run.";
6857
- const EMPTY_MEMORY = { handledCommentIds: /* @__PURE__ */ new Set() };
6858
- const sessions = /* @__PURE__ */ new Map();
6859
- function session(sessionId) {
6860
- let entry = sessions.get(sessionId);
6861
- if (entry === void 0) {
6862
- entry = {
6863
- enabled: false,
6864
- memory: EMPTY_MEMORY
6865
- };
6866
- sessions.set(sessionId, entry);
6867
- }
6868
- return entry;
6869
- }
6870
- function autoFixEnabled(sessionId) {
6871
- return session(sessionId).enabled;
6872
- }
6873
- function setAutoFixEnabled(sessionId, enabled) {
6874
- session(sessionId).enabled = enabled;
6875
- }
6876
- function autoFixMemory(sessionId) {
6877
- return session(sessionId).memory;
6878
- }
6879
- function rememberAutoFix(sessionId, memory) {
6880
- session(sessionId).memory = memory;
6881
- }
6882
- /** One failing CI run yields one fix attempt: run links change when CI re-runs. */
6883
- function checksSignature(checks) {
6884
- if (checks.length === 0) return void 0;
6885
- return checks.map((check) => `${check.name}|${check.link ?? ""}`).sort().join("\n");
6886
- }
6887
- function planAutoFix(memory, threads, checks) {
6888
- const unhandled = threads.filter((thread) => !thread.resolved).filter((thread) => thread.comments.some((comment) => !memory.handledCommentIds.has(comment.id)));
6889
- const fresh = unhandled.flatMap((thread) => thread.comments);
6890
- const signature = checksSignature(checks);
6891
- const checksChanged = signature !== void 0 && signature !== memory.handledChecksSignature;
6892
- const sections = [];
6893
- if (unhandled.length > 0) sections.push(composeCommentsPrompt(unhandled));
6894
- if (checksChanged) sections.push(composeChecksPrompt(checks));
6895
- if (sections.length === 0) return { memory };
6896
- const nextSignature = checksChanged ? signature : memory.handledChecksSignature;
6897
- return {
6898
- prompt: `${sections.join("\n\n")}\n\n${AUTO_FIX_FOOTER}`,
6899
- memory: {
6900
- handledCommentIds: /* @__PURE__ */ new Set([...memory.handledCommentIds, ...fresh.map((comment) => comment.id)]),
6901
- ...nextSignature === void 0 ? {} : { handledChecksSignature: nextSignature }
6902
- }
6903
- };
7913
+ function relativeAge(value, now = Date.now()) {
7914
+ if (value === void 0) return void 0;
7915
+ const elapsedHours = Math.max(0, Math.floor((now - Date.parse(value)) / 36e5));
7916
+ if (!Number.isFinite(elapsedHours)) return void 0;
7917
+ if (elapsedHours < 1) return "<1h";
7918
+ if (elapsedHours < 24) return `${elapsedHours}h`;
7919
+ const days = Math.floor(elapsedHours / 24);
7920
+ return days < 30 ? `${days}d` : `${Math.floor(days / 30)}mo`;
6904
7921
  }
6905
7922
  //#endregion
6906
7923
  //#region src/client/boot-check.ts
@@ -7094,7 +8111,7 @@ window.__ModuleLoader__.load({
7094
8111
  ]
7095
8112
  });
7096
8113
  }
7097
- function repositoryName$1(remote) {
8114
+ function repositoryName(remote) {
7098
8115
  return remote?.split("/").at(-1);
7099
8116
  }
7100
8117
  function PullRequestHoverCard({ repository, t }) {
@@ -7122,7 +8139,7 @@ window.__ModuleLoader__.load({
7122
8139
  /* @__PURE__ */ jsxs("span", {
7123
8140
  style: repositoryPrHoverRepo,
7124
8141
  children: [
7125
- repositoryName$1(repository.remote),
8142
+ repositoryName(repository.remote),
7126
8143
  " #",
7127
8144
  pullRequest.number,
7128
8145
  pullRequest.baseBranch === void 0 ? "" : ` → ${pullRequest.baseBranch}`
@@ -7900,7 +8917,7 @@ window.__ModuleLoader__.load({
7900
8917
  }),
7901
8918
  repository.remote === void 0 ? null : /* @__PURE__ */ jsx("span", {
7902
8919
  style: repositoryRemote,
7903
- children: repositoryName$1(repository.remote)
8920
+ children: repositoryName(repository.remote)
7904
8921
  }),
7905
8922
  /* @__PURE__ */ jsx(Tooltip, {
7906
8923
  label: branch,
@@ -12273,7 +13290,7 @@ window.__ModuleLoader__.load({
12273
13290
  ];
12274
13291
  const marked = new Z({
12275
13292
  gfm: true,
12276
- breaks: false,
13293
+ breaks: true,
12277
13294
  async: false
12278
13295
  });
12279
13296
  /** Parse one comment body into HTML. Bot machinery written as HTML comments
@@ -12568,7 +13585,7 @@ window.__ModuleLoader__.load({
12568
13585
  * the thread, plus the two things a reviewer expects to do with it — answer it
12569
13586
  * and close it. A resolved thread collapses, because the diff is about what
12570
13587
  * still needs attention. */
12571
- function ReviewThreadCard({ thread, t, now, anchorKey, active = false, suggest, onReply, onResolvedChange }) {
13588
+ function ReviewThreadCard({ thread, t, now, anchorKey, active = false, suggest, onReply, onResolvedChange, onSendToAi }) {
12572
13589
  const [expanded, setExpanded] = useState(false);
12573
13590
  const [replying, setReplying] = useState(false);
12574
13591
  const [draft, setDraft] = useState("");
@@ -12700,21 +13717,31 @@ window.__ModuleLoader__.load({
12700
13717
  ]
12701
13718
  }) : /* @__PURE__ */ jsxs("div", {
12702
13719
  style: diffCommentActions,
12703
- children: [/* @__PURE__ */ jsx("button", {
12704
- type: "button",
12705
- style: diffCommentActionButton,
12706
- disabled: busy,
12707
- onClick: () => {
12708
- setReplying(true);
12709
- },
12710
- children: t("reviewThreadReply")
12711
- }), /* @__PURE__ */ jsx("button", {
12712
- type: "button",
12713
- style: diffCommentActionButton,
12714
- disabled: busy,
12715
- onClick: toggleResolved,
12716
- children: thread.resolved ? t("reviewThreadUnresolve") : t("reviewThreadResolve")
12717
- })]
13720
+ children: [
13721
+ onSendToAi === void 0 || thread.resolved ? null : /* @__PURE__ */ jsx("button", {
13722
+ type: "button",
13723
+ style: diffCommentActionButton,
13724
+ disabled: busy,
13725
+ onClick: onSendToAi,
13726
+ children: t("reviewThreadSendToAi")
13727
+ }),
13728
+ /* @__PURE__ */ jsx("button", {
13729
+ type: "button",
13730
+ style: diffCommentActionButton,
13731
+ disabled: busy,
13732
+ onClick: () => {
13733
+ setReplying(true);
13734
+ },
13735
+ children: t("reviewThreadReply")
13736
+ }),
13737
+ /* @__PURE__ */ jsx("button", {
13738
+ type: "button",
13739
+ style: diffCommentActionButton,
13740
+ disabled: busy,
13741
+ onClick: toggleResolved,
13742
+ children: thread.resolved ? t("reviewThreadUnresolve") : t("reviewThreadResolve")
13743
+ })
13744
+ ]
12718
13745
  })
12719
13746
  ]
12720
13747
  });
@@ -13019,7 +14046,7 @@ window.__ModuleLoader__.load({
13019
14046
  children: /* @__PURE__ */ jsx("path", { d: "M14 9.5a2 2 0 0 1-2 2H6l-3.5 2.5V4a2 2 0 0 1 2-2h7.5a2 2 0 0 1 2 2z" })
13020
14047
  });
13021
14048
  }
13022
- function DiffFileSection({ file, root, open, t, comments, ghThreads, editorAnchor, editorNode, now, activeTargetKey, suggestMention, onOpenEditor, onOpenChange, onRemoveComment, onReplyToThread, onThreadResolvedChange }) {
14049
+ function DiffFileSection({ file, root, open, t, comments, ghThreads, editorAnchor, editorNode, now, activeTargetKey, suggestMention, onOpenEditor, onOpenChange, onRemoveComment, onReplyToThread, onThreadResolvedChange, onSendThreadToAi }) {
13023
14050
  const [revealed, setRevealed] = useState(() => /* @__PURE__ */ new Map());
13024
14051
  const [total, setTotal] = useState();
13025
14052
  const [expanding, setExpanding] = useState(false);
@@ -13174,7 +14201,10 @@ window.__ModuleLoader__.load({
13174
14201
  suggest: suggestMention,
13175
14202
  now,
13176
14203
  onReply: (body) => onReplyToThread(thread, body),
13177
- onResolvedChange: (resolved) => onThreadResolvedChange(thread, resolved)
14204
+ onResolvedChange: (resolved) => onThreadResolvedChange(thread, resolved),
14205
+ onSendToAi: onSendThreadToAi === void 0 ? void 0 : () => {
14206
+ onSendThreadToAi(thread);
14207
+ }
13178
14208
  }, thread.id)),
13179
14209
  editorOpen ? editorNode : null
13180
14210
  ] }, `${index}:${entry.line}`);
@@ -13728,14 +14758,20 @@ window.__ModuleLoader__.load({
13728
14758
  style: diffDelete,
13729
14759
  children: ["−", diff.deletions]
13730
14760
  }),
13731
- files.length === 0 ? null : /* @__PURE__ */ jsx("button", {
13732
- type: "button",
13733
- style: diffSummaryAction,
13734
- "aria-label": allFilesOpen ? t("diffCollapseAll") : t("diffExpandAll"),
13735
- onClick: () => {
13736
- setOpenFiles(new Map(files.map((file) => [file.path, !allFilesOpen])));
13737
- },
13738
- children: allFilesOpen ? t("diffCollapseAll") : t("diffExpandAll")
14761
+ files.length === 0 ? null : /* @__PURE__ */ jsx(Tooltip, {
14762
+ label: allFilesOpen ? t("diffCollapseAll") : t("diffExpandAll"),
14763
+ side: "bottom",
14764
+ delayMs: 250,
14765
+ children: /* @__PURE__ */ jsx("button", {
14766
+ type: "button",
14767
+ className: panelIconButtonClass,
14768
+ style: diffSummaryAction,
14769
+ "aria-label": allFilesOpen ? t("diffCollapseAll") : t("diffExpandAll"),
14770
+ onClick: () => {
14771
+ setOpenFiles(new Map(files.map((file) => [file.path, !allFilesOpen])));
14772
+ },
14773
+ children: allFilesOpen ? /* @__PURE__ */ jsx(IconChevronDownOutline14, {}) : /* @__PURE__ */ jsx(IconChevronRightOutline14, {})
14774
+ })
13739
14775
  })
13740
14776
  ]
13741
14777
  }),
@@ -13763,6 +14799,9 @@ window.__ModuleLoader__.load({
13763
14799
  activeTargetKey,
13764
14800
  onReplyToThread: replyToThread,
13765
14801
  onThreadResolvedChange: changeThreadResolved,
14802
+ onSendThreadToAi: submitPrompt === void 0 ? void 0 : (thread) => {
14803
+ submitPrompt(composeCommentsPrompt([thread]));
14804
+ },
13766
14805
  editorAnchor: commentEditor !== void 0 && commentEditor.path === file.path ? commentEditor : void 0,
13767
14806
  editorNode: commentEditorNode,
13768
14807
  onOpenEditor: (anchor) => openCommentEditor(file.path, anchor),
@@ -13923,7 +14962,7 @@ window.__ModuleLoader__.load({
13923
14962
  ] });
13924
14963
  }
13925
14964
  //#endregion
13926
- //#region src/client/ClaudeDiffOverlay.tsx
14965
+ //#region src/client/ClaudePanelOverlay.tsx
13927
14966
  const useClientLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
13928
14967
  function shouldRestoreFromEscape(event, root = document) {
13929
14968
  return event.key === "Escape" && root.querySelector("[role=\"dialog\"][aria-modal=\"true\"]") === null;
@@ -13971,7 +15010,7 @@ window.__ModuleLoader__.load({
13971
15010
  window.removeEventListener("resize", update);
13972
15011
  };
13973
15012
  }
13974
- function ClaudeDiffOverlay({ children, onRestore }) {
15013
+ function ClaudePanelOverlay({ children, onRestore }) {
13975
15014
  const ref = useRef(null);
13976
15015
  const [bounds, setBounds] = useState();
13977
15016
  useClientLayoutEffect(() => {
@@ -13990,7 +15029,7 @@ window.__ModuleLoader__.load({
13990
15029
  }, [onRestore]);
13991
15030
  return /* @__PURE__ */ jsx("div", {
13992
15031
  ref,
13993
- "data-dsh-claude-diff-overlay": true,
15032
+ "data-dsh-claude-panel-overlay": true,
13994
15033
  style: {
13995
15034
  position: "absolute",
13996
15035
  left: bounds?.left ?? 0,
@@ -14059,304 +15098,110 @@ window.__ModuleLoader__.load({
14059
15098
  label: hint ?? label,
14060
15099
  side: "bottom",
14061
15100
  delayMs: 400,
14062
- children: /* @__PURE__ */ jsx("button", {
14063
- type: "button",
14064
- className: panelIconButtonClass,
14065
- "aria-label": label,
14066
- disabled,
14067
- onClick,
14068
- children: icon
14069
- })
14070
- });
14071
- return /* @__PURE__ */ jsxs("div", {
14072
- style: repositoryBarFrame,
14073
- "data-claude-queue-dock": "",
14074
- children: [/* @__PURE__ */ jsx("style", {
14075
- "data-dsh-claude-queue-styles": true,
14076
- children: panelIconButtonCss
14077
- }), /* @__PURE__ */ jsxs("div", {
14078
- style: queueBar,
14079
- children: [queue.length > 1 ? /* @__PURE__ */ jsxs("button", {
14080
- type: "button",
14081
- style: queueHeader,
14082
- "aria-controls": listId,
14083
- "aria-expanded": expanded,
14084
- disabled: interacting,
14085
- onClick: () => {
14086
- setCollapsed((value) => !value);
14087
- },
14088
- children: [
14089
- /* @__PURE__ */ jsx("span", {
14090
- style: queueLead,
14091
- "aria-hidden": "true",
14092
- children: /* @__PURE__ */ jsx(IconQueueOutline14, {})
14093
- }),
14094
- /* @__PURE__ */ jsx("span", {
14095
- style: queueCount,
14096
- children: t("queueCount", { n: queue.length })
14097
- }),
14098
- /* @__PURE__ */ jsx("span", {
14099
- style: queueLead,
14100
- "aria-hidden": "true",
14101
- children: expanded ? /* @__PURE__ */ jsx(IconChevronDownOutline14, {}) : /* @__PURE__ */ jsx(IconChevronUpOutline14, {})
14102
- })
14103
- ]
14104
- }) : null, /* @__PURE__ */ jsx("ul", {
14105
- id: listId,
14106
- style: queueList,
14107
- hidden: !listVisible,
14108
- children: listVisible ? queue.map((row, index) => /* @__PURE__ */ jsxs("li", {
14109
- style: {
14110
- ...queueRow,
14111
- ...index > 0 ? queueRowDivider : {}
14112
- },
14113
- children: [
14114
- queue.length === 1 ? /* @__PURE__ */ jsx("span", {
14115
- style: queueLead,
14116
- "aria-hidden": "true",
14117
- children: /* @__PURE__ */ jsx(IconQueueOutline14, {})
14118
- }) : null,
14119
- editing?.id === row.id ? /* @__PURE__ */ jsx("input", {
14120
- autoFocus: true,
14121
- style: queueEditor,
14122
- "aria-label": t("queueEdit"),
14123
- value: editing.text,
14124
- onChange: (event) => {
14125
- setEditing({
14126
- id: row.id,
14127
- text: event.currentTarget.value
14128
- });
14129
- },
14130
- onKeyDown: (event) => {
14131
- if (event.key === "Escape") setEditing(void 0);
14132
- else if (event.key === "Enter" && !event.nativeEvent.isComposing) {
14133
- event.preventDefault();
14134
- saveEdit();
14135
- }
14136
- }
14137
- }) : /* @__PURE__ */ jsx("span", {
14138
- style: queuePreview,
14139
- children: row.preview
14140
- }),
14141
- mutable ? /* @__PURE__ */ jsx("span", {
14142
- style: queueActions,
14143
- children: editing?.id === row.id ? /* @__PURE__ */ jsxs(Fragment$1, { children: [action(t("queueSave"), /* @__PURE__ */ jsx(IconCheckOutline16, { size: 14 }), () => {
14144
- saveEdit();
14145
- }, busy !== void 0 || editing.text.trim() === ""), action(t("queueCancelEdit"), /* @__PURE__ */ jsx(IconCloseOutline16, { size: 14 }), () => {
14146
- setEditing(void 0);
14147
- }, busy !== void 0)] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
14148
- action(t("queueEdit"), /* @__PURE__ */ jsx(IconEditOutline16, { size: 14 }), () => {
14149
- if (row.text !== null) setEditing({
14150
- id: row.id,
14151
- text: row.text
14152
- });
14153
- }, busy !== void 0 || row.text === null, row.text === null ? t("queueEditUnsupported") : void 0),
14154
- action(t("queueRemove"), /* @__PURE__ */ jsx(IconTrashOutline16, { size: 14 }), () => {
14155
- apply(row.id, { kind: "remove" }, t("queueRemoveFailed"));
14156
- }, busy !== void 0),
14157
- action(t("queueSteer"), /* @__PURE__ */ jsx(IconSendOutline14, {}), () => {
14158
- apply(row.id, { kind: "steer" }, t("queueSteerFailed"));
14159
- }, busy !== void 0 || !running, running ? void 0 : t("queueSteerUnavailable"))
14160
- ] })
14161
- }) : null
14162
- ]
14163
- }, row.id)) : null
14164
- })]
14165
- })]
14166
- });
14167
- }
14168
- //#endregion
14169
- //#region src/client/session-preset.ts
14170
- /**
14171
- * Resolve one row's preset id, newest seat first.
14172
- * @param row - a session-list row, or undefined when the id is not listed.
14173
- * @returns the preset id, or undefined when neither source carries one.
14174
- */
14175
- function sessionRowPreset(row) {
14176
- return row?.agentPreset ?? row?.projectionValues?.agentPreset ?? void 0;
14177
- }
14178
- //#endregion
14179
- //#region src/client/ClaudePullRequestsPanel.tsx
14180
- const NO_WORKSPACE_STATE = {};
14181
- const NO_WORKSPACES = {
14182
- subscribe: () => () => {},
14183
- getSnapshot: () => NO_WORKSPACE_STATE
14184
- };
14185
- const OVERVIEW_REFRESH_MS = 3e4;
14186
- /** What a running session is blocked on: the latest permission or question
14187
- * activity that is still in its started phase. */
14188
- function overviewAttention(activities) {
14189
- for (let index = activities.length - 1; index >= 0; index -= 1) {
14190
- const activity = activities[index];
14191
- if (activity === void 0 || activity.kind !== "permission" && activity.kind !== "question") continue;
14192
- return activity.phase === "started" ? activity.kind : void 0;
14193
- }
14194
- }
14195
- /** Claude sessions worth listing: rows still in the host list (byId keeps
14196
- * deleted and breadcrumb rows), non-blank, non-subagent, with a checkout;
14197
- * running first. */
14198
- function claudeSessionRows(state, archivedSessionIds = []) {
14199
- const rows = state.ids === void 0 ? Object.values(state.byId) : state.ids.map((id) => state.byId[id]);
14200
- const archived = new Set(archivedSessionIds);
14201
- return rows.filter((row) => row !== void 0 && sessionRowPreset(row) === "claude" && row.blank !== true && row.origin !== "subagent" && !archived.has(row.id) && typeof row.cwd === "string").sort((left, right) => Number(right.running === true) - Number(left.running === true) || (left.displayTitle ?? left.id).localeCompare(right.displayTitle ?? right.id));
14202
- }
14203
- function Badge({ label, tone = "neutral" }) {
14204
- const toneStyle = tone === "success" ? repositoryItemSuccess : tone === "warning" ? repositoryItemWarning : tone === "error" ? repositoryItemError : tone === "merged" ? { color: "#a78bfa" } : {};
14205
- return /* @__PURE__ */ jsxs("span", {
14206
- style: {
14207
- ...repositoryItem,
14208
- ...toneStyle
14209
- },
14210
- children: [/* @__PURE__ */ jsx("span", {
14211
- style: repositoryItemDot,
14212
- "aria-hidden": "true"
14213
- }), /* @__PURE__ */ jsx("span", {
14214
- style: repositoryItemLabel,
14215
- children: label
14216
- })]
14217
- });
14218
- }
14219
- function repositoryName(remote) {
14220
- return remote?.split("/").at(-1);
14221
- }
14222
- function OverviewAttention({ source, running, t }) {
14223
- const snapshot = useSyncExternalStore(source.subscribe, source.getSnapshot, source.getSnapshot);
14224
- const attention = running ? overviewAttention(snapshot.activities) : void 0;
14225
- const usage = snapshot.contextUsage;
14226
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [
14227
- attention === "permission" ? /* @__PURE__ */ jsx(Badge, {
14228
- label: t("overviewNeedsPermission"),
14229
- tone: "warning"
14230
- }) : null,
14231
- attention === "question" ? /* @__PURE__ */ jsx(Badge, {
14232
- label: t("overviewNeedsAnswer"),
14233
- tone: "warning"
14234
- }) : null,
14235
- usage === void 0 ? null : /* @__PURE__ */ jsx("span", { children: t("overviewContextUsage", { percentage: usage.percentage }) })
14236
- ] });
14237
- }
14238
- function ClaudePullRequestsPanel({ t, closeDetails, openSession, loadStatus, sessions, workspaces, projectionFor }) {
14239
- const sessionStore = useMemo(() => ({
14240
- subscribe: (listener) => sessions.subscribe(listener),
14241
- getSnapshot: () => sessions.getSnapshot()
14242
- }), [sessions]);
14243
- const snapshot = useSyncExternalStore(sessionStore.subscribe, sessionStore.getSnapshot, sessionStore.getSnapshot);
14244
- const workspaceStore = useMemo(() => {
14245
- const source = workspaces ?? NO_WORKSPACES;
14246
- return {
14247
- subscribe: (listener) => source.subscribe(listener),
14248
- getSnapshot: () => source.getSnapshot()
14249
- };
14250
- }, [workspaces]);
14251
- const workspaceState = useSyncExternalStore(workspaceStore.subscribe, workspaceStore.getSnapshot, workspaceStore.getSnapshot);
14252
- const rows = useMemo(() => claudeSessionRows(snapshot, workspaceState.archivedSessionIds ?? []), [snapshot, workspaceState]);
14253
- const cwdKey = useMemo(() => [...new Set(rows.map((row) => row.cwd ?? ""))].sort().join("\0"), [rows]);
14254
- const [statuses, setStatuses] = useState({});
14255
- useEffect(() => {
14256
- const cwds = cwdKey.length === 0 ? [] : cwdKey.split("\0");
14257
- if (cwds.length === 0) return;
14258
- const controller = new AbortController();
14259
- const refresh = () => {
14260
- for (const cwd of cwds) loadStatus(cwd, controller.signal).then((status) => {
14261
- if (!controller.signal.aborted) setStatuses((previous) => ({
14262
- ...previous,
14263
- [cwd]: status
14264
- }));
14265
- }, () => void 0);
14266
- };
14267
- refresh();
14268
- const timer = setInterval(refresh, OVERVIEW_REFRESH_MS);
14269
- return () => {
14270
- controller.abort();
14271
- clearInterval(timer);
14272
- };
14273
- }, [cwdKey, loadStatus]);
15101
+ children: /* @__PURE__ */ jsx("button", {
15102
+ type: "button",
15103
+ className: panelIconButtonClass,
15104
+ "aria-label": label,
15105
+ disabled,
15106
+ onClick,
15107
+ children: icon
15108
+ })
15109
+ });
14274
15110
  return /* @__PURE__ */ jsxs("div", {
14275
- className: detailsCardClass,
14276
- style: tasksPanel,
14277
- children: [
14278
- /* @__PURE__ */ jsxs("style", {
14279
- "data-dsh-claude-overview-styles": true,
14280
- children: [detailsCardCss, panelIconButtonCss]
14281
- }),
14282
- /* @__PURE__ */ jsxs("header", {
14283
- style: tasksHeader,
14284
- children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
14285
- style: tasksHeading,
14286
- children: t("overviewTitle")
14287
- }), /* @__PURE__ */ jsx("span", {
14288
- style: tasksTurnMeta,
14289
- children: t("overviewBody")
14290
- })] }), /* @__PURE__ */ jsx("button", {
14291
- type: "button",
14292
- className: panelIconButtonClass,
14293
- "aria-label": t("diffClose"),
14294
- onClick: closeDetails,
14295
- children: /* @__PURE__ */ jsx(IconCloseOutline16, {})
14296
- })]
14297
- }),
14298
- /* @__PURE__ */ jsx("div", {
14299
- style: overviewBody,
14300
- children: rows.length === 0 ? /* @__PURE__ */ jsx("p", {
14301
- style: overviewEmpty,
14302
- children: t("overviewEmpty")
14303
- }) : rows.map((row) => {
14304
- const repository = row.cwd === void 0 ? void 0 : statuses[row.cwd];
14305
- const pullRequest = repository?.pullRequest;
14306
- const branch = repository?.status === "ready" ? repository.detached === true ? t("repositoryDetached") : repository.branch ?? t("repositoryUnknownBranch") : repository === void 0 ? t("overviewLoading") : t("repositoryUnavailable");
14307
- return /* @__PURE__ */ jsxs("button", {
14308
- type: "button",
14309
- style: overviewRow,
14310
- onClick: () => {
14311
- openSession(row.id);
14312
- },
14313
- children: [/* @__PURE__ */ jsxs("span", {
14314
- style: overviewRowTop,
14315
- children: [
14316
- row.running === true ? /* @__PURE__ */ jsx("span", {
14317
- style: overviewRunningDot,
14318
- "aria-label": t("overviewRunning")
14319
- }) : null,
14320
- /* @__PURE__ */ jsx("span", {
14321
- style: overviewTitle,
14322
- children: row.displayTitle ?? row.id
14323
- }),
14324
- pullRequest === void 0 ? /* @__PURE__ */ jsx(Badge, { label: t("overviewNoPr") }) : /* @__PURE__ */ jsx(Badge, {
14325
- label: `#${pullRequest.number} · ${t(`repositoryState_${pullRequest.state}`)}`,
14326
- tone: pullRequest.state === "merged" ? "merged" : pullRequest.state === "open" ? "success" : "neutral"
14327
- })
14328
- ]
14329
- }), /* @__PURE__ */ jsxs("span", {
14330
- style: overviewMeta,
14331
- children: [
14332
- repositoryName(repository?.remote) === void 0 ? null : /* @__PURE__ */ jsx("span", { children: repositoryName(repository?.remote) }),
14333
- /* @__PURE__ */ jsx("span", {
14334
- style: overviewBranch,
14335
- children: branch
14336
- }),
14337
- pullRequest?.state === "open" && pullRequest.checks !== "none" ? /* @__PURE__ */ jsx(Badge, {
14338
- label: t(`repositoryChecks_${pullRequest.checks}`),
14339
- tone: pullRequest.checks === "passing" ? "success" : pullRequest.checks === "failing" ? "error" : "warning"
14340
- }) : null,
14341
- pullRequest?.state === "open" && pullRequest.review !== "none" ? /* @__PURE__ */ jsx(Badge, {
14342
- label: t(`repositoryReview_${pullRequest.review}`),
14343
- tone: pullRequest.review === "approved" ? "success" : pullRequest.review === "changes-requested" ? "error" : "neutral"
14344
- }) : null,
14345
- autoFixEnabled(row.id) ? /* @__PURE__ */ jsx(Badge, {
14346
- label: t("overviewAutoFix"),
14347
- tone: "success"
14348
- }) : null,
14349
- projectionFor === void 0 ? null : /* @__PURE__ */ jsx(OverviewAttention, {
14350
- source: projectionFor(row.id),
14351
- running: row.running === true,
14352
- t
14353
- })
14354
- ]
14355
- })]
14356
- }, row.id);
14357
- })
14358
- })
14359
- ]
15111
+ style: repositoryBarFrame,
15112
+ "data-claude-queue-dock": "",
15113
+ children: [/* @__PURE__ */ jsx("style", {
15114
+ "data-dsh-claude-queue-styles": true,
15115
+ children: panelIconButtonCss
15116
+ }), /* @__PURE__ */ jsxs("div", {
15117
+ style: queueBar,
15118
+ children: [queue.length > 1 ? /* @__PURE__ */ jsxs("button", {
15119
+ type: "button",
15120
+ style: queueHeader,
15121
+ "aria-controls": listId,
15122
+ "aria-expanded": expanded,
15123
+ disabled: interacting,
15124
+ onClick: () => {
15125
+ setCollapsed((value) => !value);
15126
+ },
15127
+ children: [
15128
+ /* @__PURE__ */ jsx("span", {
15129
+ style: queueLead,
15130
+ "aria-hidden": "true",
15131
+ children: /* @__PURE__ */ jsx(IconQueueOutline14, {})
15132
+ }),
15133
+ /* @__PURE__ */ jsx("span", {
15134
+ style: queueCount,
15135
+ children: t("queueCount", { n: queue.length })
15136
+ }),
15137
+ /* @__PURE__ */ jsx("span", {
15138
+ style: queueLead,
15139
+ "aria-hidden": "true",
15140
+ children: expanded ? /* @__PURE__ */ jsx(IconChevronDownOutline14, {}) : /* @__PURE__ */ jsx(IconChevronUpOutline14, {})
15141
+ })
15142
+ ]
15143
+ }) : null, /* @__PURE__ */ jsx("ul", {
15144
+ id: listId,
15145
+ style: queueList,
15146
+ hidden: !listVisible,
15147
+ children: listVisible ? queue.map((row, index) => /* @__PURE__ */ jsxs("li", {
15148
+ style: {
15149
+ ...queueRow,
15150
+ ...index > 0 ? queueRowDivider : {}
15151
+ },
15152
+ children: [
15153
+ queue.length === 1 ? /* @__PURE__ */ jsx("span", {
15154
+ style: queueLead,
15155
+ "aria-hidden": "true",
15156
+ children: /* @__PURE__ */ jsx(IconQueueOutline14, {})
15157
+ }) : null,
15158
+ editing?.id === row.id ? /* @__PURE__ */ jsx("input", {
15159
+ autoFocus: true,
15160
+ style: queueEditor,
15161
+ "aria-label": t("queueEdit"),
15162
+ value: editing.text,
15163
+ onChange: (event) => {
15164
+ setEditing({
15165
+ id: row.id,
15166
+ text: event.currentTarget.value
15167
+ });
15168
+ },
15169
+ onKeyDown: (event) => {
15170
+ if (event.key === "Escape") setEditing(void 0);
15171
+ else if (event.key === "Enter" && !event.nativeEvent.isComposing) {
15172
+ event.preventDefault();
15173
+ saveEdit();
15174
+ }
15175
+ }
15176
+ }) : /* @__PURE__ */ jsx("span", {
15177
+ style: queuePreview,
15178
+ children: row.preview
15179
+ }),
15180
+ mutable ? /* @__PURE__ */ jsx("span", {
15181
+ style: queueActions,
15182
+ children: editing?.id === row.id ? /* @__PURE__ */ jsxs(Fragment$1, { children: [action(t("queueSave"), /* @__PURE__ */ jsx(IconCheckOutline16, { size: 14 }), () => {
15183
+ saveEdit();
15184
+ }, busy !== void 0 || editing.text.trim() === ""), action(t("queueCancelEdit"), /* @__PURE__ */ jsx(IconCloseOutline16, { size: 14 }), () => {
15185
+ setEditing(void 0);
15186
+ }, busy !== void 0)] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
15187
+ action(t("queueEdit"), /* @__PURE__ */ jsx(IconEditOutline16, { size: 14 }), () => {
15188
+ if (row.text !== null) setEditing({
15189
+ id: row.id,
15190
+ text: row.text
15191
+ });
15192
+ }, busy !== void 0 || row.text === null, row.text === null ? t("queueEditUnsupported") : void 0),
15193
+ action(t("queueRemove"), /* @__PURE__ */ jsx(IconTrashOutline16, { size: 14 }), () => {
15194
+ apply(row.id, { kind: "remove" }, t("queueRemoveFailed"));
15195
+ }, busy !== void 0),
15196
+ action(t("queueSteer"), /* @__PURE__ */ jsx(IconSendOutline14, {}), () => {
15197
+ apply(row.id, { kind: "steer" }, t("queueSteerFailed"));
15198
+ }, busy !== void 0 || !running, running ? void 0 : t("queueSteerUnavailable"))
15199
+ ] })
15200
+ }) : null
15201
+ ]
15202
+ }, row.id)) : null
15203
+ })]
15204
+ })]
14360
15205
  });
14361
15206
  }
14362
15207
  //#endregion
@@ -15096,13 +15941,19 @@ window.__ModuleLoader__.load({
15096
15941
  //#endregion
15097
15942
  //#region src/client/rewind-api.ts
15098
15943
  /** Drop one user message and everything after it: the rows are hidden from
15099
- * this session's transcript and Claude resumes before that turn. */
15100
- async function rewindSession(sessionId, seq) {
15944
+ * this session's transcript and Claude resumes before that turn.
15945
+ *
15946
+ * With `restoreFiles`, the checkout is put back to the tree that turn was
15947
+ * admitted against. The answer reports whether that half actually landed:
15948
+ * a turn from before this session captured trees, or a tree git has since
15949
+ * collected, still rewinds the conversation and leaves the files alone. */
15950
+ async function rewindSession(sessionId, seq, restoreFiles = false) {
15101
15951
  try {
15102
- await pluginWrite(CLAUDE_REWIND_PATH, "fast", void 0, { json: {
15952
+ return { filesRestored: (await pluginWrite(CLAUDE_REWIND_PATH, "fast", void 0, { json: {
15103
15953
  sessionId,
15104
- seq
15105
- } });
15954
+ seq,
15955
+ restoreFiles
15956
+ } }))?.filesRestored === true };
15106
15957
  } catch (error) {
15107
15958
  if (!(error instanceof PluginRequestError)) throw error;
15108
15959
  throw new Error(error.code ?? error.reason);
@@ -15171,6 +16022,8 @@ window.__ModuleLoader__.load({
15171
16022
  const [target, setTarget] = useState();
15172
16023
  const [submitting, setSubmitting] = useState(false);
15173
16024
  const [error, setError] = useState();
16025
+ const [restoreFiles, setRestoreFiles] = useState(true);
16026
+ const { toast, report } = useActionToast();
15174
16027
  const ranges = projection.rewind?.ranges ?? EMPTY_RANGES;
15175
16028
  const owned = projection.owned;
15176
16029
  const unavailable = snapshot.running;
@@ -15238,6 +16091,7 @@ window.__ModuleLoader__.load({
15238
16091
  setTarget(void 0);
15239
16092
  setSubmitting(false);
15240
16093
  setError(void 0);
16094
+ setRestoreFiles(true);
15241
16095
  }, [sessionId]);
15242
16096
  if (sessionId === void 0 || !owned) return /* @__PURE__ */ jsx("span", {
15243
16097
  "data-dsh-claude-rewind-armed": "armed",
@@ -15252,10 +16106,11 @@ window.__ModuleLoader__.load({
15252
16106
  if (target === void 0 || submitting) return;
15253
16107
  setSubmitting(true);
15254
16108
  setError(void 0);
15255
- rewindSession(sessionId, target.seq).then(() => {
16109
+ rewindSession(sessionId, target.seq, restoreFiles).then(({ filesRestored }) => {
15256
16110
  setSubmitting(false);
15257
16111
  setTarget(void 0);
15258
16112
  if (target.text !== "") setDraft?.(sessionId, target.text);
16113
+ if (restoreFiles && !filesRestored) report(t("rewindFilesUnavailable"));
15259
16114
  }, (reason) => {
15260
16115
  setSubmitting(false);
15261
16116
  const code = reason instanceof Error && reason.message !== "" ? reason.message : "unknown";
@@ -15263,6 +16118,7 @@ window.__ModuleLoader__.load({
15263
16118
  });
15264
16119
  };
15265
16120
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [
16121
+ toast,
15266
16122
  /* @__PURE__ */ jsx("style", {
15267
16123
  "data-dsh-claude-rewind-styles": true,
15268
16124
  children: `${rewindActionCss}${rewindHiddenCss(hiddenKeys)}`
@@ -15327,9 +16183,20 @@ window.__ModuleLoader__.load({
15327
16183
  style: rewindModalMessage,
15328
16184
  children: target.text.slice(0, 2e3)
15329
16185
  }),
16186
+ /* @__PURE__ */ jsxs("label", {
16187
+ style: diffModalCheckbox,
16188
+ children: [/* @__PURE__ */ jsx("input", {
16189
+ type: "checkbox",
16190
+ checked: restoreFiles,
16191
+ disabled: submitting,
16192
+ onChange: (event) => {
16193
+ setRestoreFiles(event.currentTarget.checked);
16194
+ }
16195
+ }), t("rewindRestoreFiles")]
16196
+ }),
15330
16197
  /* @__PURE__ */ jsx("p", {
15331
16198
  style: diffModalStatus,
15332
- children: t("rewindHint")
16199
+ children: restoreFiles ? t("rewindRestoreFilesHint") : t("rewindHint")
15333
16200
  }),
15334
16201
  error === void 0 ? null : /* @__PURE__ */ jsx("p", {
15335
16202
  style: diffModalError,
@@ -16658,19 +17525,19 @@ window.__ModuleLoader__.load({
16658
17525
  });
16659
17526
  }
16660
17527
  //#endregion
16661
- //#region src/client/diff-open-store.ts
16662
- var DiffOpenStore = class {
17528
+ //#region src/client/panel-open-store.ts
17529
+ var PanelOpenStore = class {
16663
17530
  #sessionId;
16664
17531
  #listeners = /* @__PURE__ */ new Set();
16665
- /** Mark the diff panel open for one session, replacing any previous holder. */
17532
+ /** Mark this panel open for one session, replacing any previous holder. */
16666
17533
  open(sessionId) {
16667
17534
  this.#set(sessionId);
16668
17535
  }
16669
- /** Mark the diff panel closed. */
17536
+ /** Mark this panel closed. */
16670
17537
  close() {
16671
17538
  this.#set(void 0);
16672
17539
  }
16673
- /** Whether the diff panel is currently open for this session. */
17540
+ /** Whether this panel is currently open for this session. */
16674
17541
  isOpen(sessionId) {
16675
17542
  return this.#sessionId === sessionId;
16676
17543
  }
@@ -17206,6 +18073,27 @@ window.__ModuleLoader__.load({
17206
18073
  yes: "是",
17207
18074
  no: "否",
17208
18075
  diffOpen: "查看分支改动",
18076
+ planPanelTitle: "Plan",
18077
+ planClose: "关闭方案面板",
18078
+ planOpen: "查看方案",
18079
+ planMaximize: "最大化方案面板",
18080
+ planRestore: "还原方案面板",
18081
+ planNth: "{index} / {total}",
18082
+ planHistory: "本会话的方案",
18083
+ planNotePlaceholder: "写下要改的地方,Enter 记一条,⌘/Ctrl+Enter 发送",
18084
+ planNoteQuotedPlaceholder: "针对选中的这段,要改成什么?",
18085
+ planNoteHint: "在正文里选中一段可以引用它",
18086
+ planNoteRemove: "删掉这条意见",
18087
+ planQuoteClear: "取消引用",
18088
+ planSendForChanges: "发送并让 AI 修改",
18089
+ planSending: "发送中…",
18090
+ planSettled: "这个方案已经在审批弹框里被决定了,意见没能发出。",
18091
+ planFeedbackFailed: "意见发送失败,请重试。",
18092
+ planPending: "待审批",
18093
+ planApproved: "已批准",
18094
+ planRejected: "已拒绝",
18095
+ planEmpty: "本会话还没有待查看的方案。",
18096
+ planPendingHint: "在 DSH 的审批弹框里批准或拒绝这个方案。",
17209
18097
  diffClose: "关闭 Diff 面板",
17210
18098
  sessionMenu: "会话菜单",
17211
18099
  sessionMenuOpenIn: "打开方式",
@@ -17351,6 +18239,7 @@ window.__ModuleLoader__.load({
17351
18239
  reviewCommentCancel: "取消",
17352
18240
  reviewCommentRemove: "删除评论",
17353
18241
  reviewThreadReply: "回复",
18242
+ reviewThreadSendToAi: "交给 AI 修复",
17354
18243
  reviewThreadBot: "Bot",
17355
18244
  reviewThreadAgo: "{age}前",
17356
18245
  reviewThreadReplyTo: "回复 @{author}",
@@ -17386,6 +18275,17 @@ window.__ModuleLoader__.load({
17386
18275
  rewindFailed: "回退失败({code})。",
17387
18276
  rewindBusy: "这个会话正在运行,请等本轮结束后再回退。",
17388
18277
  rewindStale: "宿主进程里的插件还是旧版本(没有回退接口)。重启 DSH 后再试。",
18278
+ alerts: "会话提醒",
18279
+ alertsOff: "关闭",
18280
+ alertsOn: "开启",
18281
+ alertsEffect: "当另一个 Claude 会话需要你——等待权限确认、等待回答,或者这一轮跑完了——弹出系统通知。正在看的那个会话不会提醒。点击通知会切到对应会话。首次使用时系统会询问通知权限;拒绝后不再提醒。改动立即生效。",
18282
+ alertNeedsPermission: "等待你确认权限",
18283
+ alertNeedsAnswer: "等待你回答",
18284
+ alertTurnFinished: "这一轮跑完了",
18285
+ alertFallbackTitle: "Claude 会话",
18286
+ rewindRestoreFiles: "同时把文件改回这一轮开始前的样子",
18287
+ rewindRestoreFilesHint: "被删除的内容会从这个会话中隐藏,Claude 也会忘记它们;工作区会回到这一轮开始前的状态——这之后新建的文件会被删除,被改动的文件会被还原,被 .gitignore 忽略的文件不受影响。消息原文会放回输入框,方便修改后重新发送。",
18288
+ rewindFilesUnavailable: "对话已回退,但文件没有还原:这一轮没有留下工作区快照。",
17389
18289
  turnUsage: "本回合用量",
17390
18290
  turnUsageTokens: "{count} tok",
17391
18291
  turnUsageCache: "缓存命中 {percent}%",
@@ -17583,6 +18483,27 @@ window.__ModuleLoader__.load({
17583
18483
  yes: "Yes",
17584
18484
  no: "No",
17585
18485
  diffOpen: "View branch changes",
18486
+ planPanelTitle: "Plan",
18487
+ planClose: "Close plan panel",
18488
+ planOpen: "View plan",
18489
+ planMaximize: "Maximize plan panel",
18490
+ planRestore: "Restore plan panel",
18491
+ planNth: "{index} / {total}",
18492
+ planHistory: "Plans in this session",
18493
+ planNotePlaceholder: "What should change? Enter files a note, ⌘/Ctrl+Enter sends",
18494
+ planNoteQuotedPlaceholder: "What should change about the selected passage?",
18495
+ planNoteHint: "Select a passage above to quote it",
18496
+ planNoteRemove: "Remove this note",
18497
+ planQuoteClear: "Clear the quote",
18498
+ planSendForChanges: "Send for changes",
18499
+ planSending: "Sending…",
18500
+ planSettled: "This plan was already decided in the approval dialog; the notes were not sent.",
18501
+ planFeedbackFailed: "The notes could not be sent. Try again.",
18502
+ planPending: "Awaiting approval",
18503
+ planApproved: "Approved",
18504
+ planRejected: "Rejected",
18505
+ planEmpty: "This session has no plan to read yet.",
18506
+ planPendingHint: "Approve or reject this plan in the DSH approval dialog.",
17586
18507
  diffClose: "Close diff panel",
17587
18508
  sessionMenu: "Session menu",
17588
18509
  sessionMenuOpenIn: "Open in",
@@ -17728,6 +18649,7 @@ window.__ModuleLoader__.load({
17728
18649
  reviewCommentCancel: "Cancel",
17729
18650
  reviewCommentRemove: "Remove comment",
17730
18651
  reviewThreadReply: "Reply",
18652
+ reviewThreadSendToAi: "Send to AI",
17731
18653
  reviewThreadBot: "Bot",
17732
18654
  reviewThreadAgo: "{age} ago",
17733
18655
  reviewThreadReplyTo: "Reply to @{author}",
@@ -17763,6 +18685,17 @@ window.__ModuleLoader__.load({
17763
18685
  rewindFailed: "The rewind failed ({code}).",
17764
18686
  rewindBusy: "This session is running; wait for the turn to finish before rewinding.",
17765
18687
  rewindStale: "The Host process is still running the previous plugin build, which has no rewind route. Restart DSH and try again.",
18688
+ alerts: "Session alerts",
18689
+ alertsOff: "Off",
18690
+ alertsOn: "On",
18691
+ alertsEffect: "Raises a desktop notification when another Claude session needs you — waiting on an approval, waiting on an answer, or finished its turn. The session on screen never raises one. Clicking a notification brings that session up. The system asks for notification permission the first time; refusing it turns alerts off. Takes effect immediately.",
18692
+ alertNeedsPermission: "Waiting on your approval",
18693
+ alertNeedsAnswer: "Waiting on your answer",
18694
+ alertTurnFinished: "Finished its turn",
18695
+ alertFallbackTitle: "Claude session",
18696
+ rewindRestoreFiles: "Also put the files back to where this turn found them",
18697
+ rewindRestoreFilesHint: "The removed entries are hidden from this session and Claude forgets them, and the checkout returns to the state this turn started from — files created since are deleted, changed files are restored, and files ignored by .gitignore are left alone. The message text goes back to the composer so you can edit and resend it.",
18698
+ rewindFilesUnavailable: "The conversation was rewound, but the files were left alone: that turn recorded no working-tree snapshot.",
17766
18699
  turnUsage: "Turn usage",
17767
18700
  turnUsageTokens: "{count} tok",
17768
18701
  turnUsageCache: "Cache hit {percent}%",
@@ -17774,7 +18707,7 @@ window.__ModuleLoader__.load({
17774
18707
  function MaximizedDiff({ source, t, sessionId, closeDetails, restore, submitPrompt }) {
17775
18708
  const snapshot = useSyncExternalStore(source.subscribe, source.getSnapshot, source.getSnapshot);
17776
18709
  const useClaudeProjection = (selector) => selector(snapshot);
17777
- return /* @__PURE__ */ jsx(ClaudeDiffOverlay, {
18710
+ return /* @__PURE__ */ jsx(ClaudePanelOverlay, {
17778
18711
  onRestore: restore,
17779
18712
  children: /* @__PURE__ */ jsx(ClaudeDiffPanel, {
17780
18713
  useClaudeProjection,
@@ -17788,6 +18721,21 @@ window.__ModuleLoader__.load({
17788
18721
  });
17789
18722
  }
17790
18723
  const name = "dsh-claude-client";
18724
+ function MaximizedPlan({ source, t, sessionId, closeDetails, restore }) {
18725
+ const snapshot = useSyncExternalStore(source.subscribe, source.getSnapshot, source.getSnapshot);
18726
+ const useClaudeProjection = (selector) => selector(snapshot);
18727
+ return /* @__PURE__ */ jsx(ClaudePanelOverlay, {
18728
+ onRestore: restore,
18729
+ children: /* @__PURE__ */ jsx(ClaudePlanPanel, {
18730
+ useClaudeProjection,
18731
+ t,
18732
+ sessionId,
18733
+ maximized: true,
18734
+ closeDetails,
18735
+ toggleMaximized: restore
18736
+ })
18737
+ });
18738
+ }
17791
18739
  const inject = [
17792
18740
  "slots",
17793
18741
  "locale",
@@ -17829,7 +18777,9 @@ window.__ModuleLoader__.load({
17829
18777
  const t = ctx.locale.bind(namespace);
17830
18778
  ctx.effect(() => restyleHostChrome(), "dsh-claude: Host chrome restyling");
17831
18779
  pluginRead(CLAUDE_GLOBAL_SETTINGS_PATH, "fast").then((payload) => {
17832
- if (isGlobalSettingsView(payload)) applyClaudeMarkdownTheme(proseModeOf(payload.settings));
18780
+ if (!isGlobalSettingsView(payload)) return;
18781
+ applyClaudeMarkdownTheme(proseModeOf(payload.settings));
18782
+ setClaudeAlertsEnabled(alertModeOf(payload.settings) === "on");
17833
18783
  }).catch(() => {});
17834
18784
  const projections = new ClaudeProjectionStore({ report: (kind, detail) => {
17835
18785
  diagnostics.report(kind, detail);
@@ -17874,6 +18824,17 @@ window.__ModuleLoader__.load({
17874
18824
  resolve: (binding) => ({ hooks: { claudeProjection: projections.source(binding.sessionId) } })
17875
18825
  }), "dsh-claude: sidecar projection provider");
17876
18826
  ctx.effect(() => () => projections.dispose(), "dsh-claude: sidecar projection lifecycle");
18827
+ if (sessions !== void 0) ctx.effect(() => startClaudeSessionAlerts({
18828
+ sessions: {
18829
+ subscribe: (listener) => sessions.list.subscribe(listener),
18830
+ getSnapshot: () => sessions.list.getSnapshot()
18831
+ },
18832
+ projectionFor: (id) => projections.source(id),
18833
+ open: (id) => {
18834
+ sessions.open(id);
18835
+ },
18836
+ t
18837
+ }), "dsh-claude: session alerts");
17877
18838
  const uiConversation = ctx.get("uiConversation");
17878
18839
  if (uiConversation !== void 0) {
17879
18840
  ctx.effect(() => uiConversation.events.register(claudeTurnDefinition), "dsh-claude: Claude turn marker");
@@ -17888,9 +18849,11 @@ window.__ModuleLoader__.load({
17888
18849
  const layout = ctx.get("layout");
17889
18850
  let disposePluginDetails;
17890
18851
  let disposeDiffOverlay;
18852
+ let disposePlanOverlay;
17891
18853
  let disposeExpandedDetailsResize;
17892
18854
  let detailsSessionId;
17893
- const diffOpen = new DiffOpenStore();
18855
+ const diffOpen = new PanelOpenStore();
18856
+ const planOpen = new PanelOpenStore();
17894
18857
  const restoreDiff = () => {
17895
18858
  if (disposeDiffOverlay === void 0) return;
17896
18859
  disposeDiffOverlay();
@@ -17898,16 +18861,26 @@ window.__ModuleLoader__.load({
17898
18861
  layout?.openDetails();
17899
18862
  disposeExpandedDetailsResize = enableExpandedDetailsResize();
17900
18863
  };
18864
+ const restorePlan = () => {
18865
+ if (disposePlanOverlay === void 0) return;
18866
+ disposePlanOverlay();
18867
+ disposePlanOverlay = void 0;
18868
+ layout?.openDetails();
18869
+ disposeExpandedDetailsResize = enableExpandedDetailsResize();
18870
+ };
17901
18871
  const closePluginDetails = () => {
17902
- if (disposePluginDetails === void 0 && disposeDiffOverlay === void 0 && disposeExpandedDetailsResize === void 0 && detailsSessionId === void 0) return;
18872
+ if (disposePluginDetails === void 0 && disposeDiffOverlay === void 0 && disposePlanOverlay === void 0 && disposeExpandedDetailsResize === void 0 && detailsSessionId === void 0) return;
17903
18873
  disposeDiffOverlay?.();
17904
18874
  disposeDiffOverlay = void 0;
18875
+ disposePlanOverlay?.();
18876
+ disposePlanOverlay = void 0;
17905
18877
  disposeExpandedDetailsResize?.();
17906
18878
  disposeExpandedDetailsResize = void 0;
17907
18879
  disposePluginDetails?.();
17908
18880
  disposePluginDetails = void 0;
17909
18881
  detailsSessionId = void 0;
17910
18882
  diffOpen.close();
18883
+ planOpen.close();
17911
18884
  layout?.closeDetails();
17912
18885
  };
17913
18886
  ctx.effect(() => ctx.slots.onEntryError((key, entry, error) => {
@@ -17916,6 +18889,7 @@ window.__ModuleLoader__.load({
17916
18889
  ${error.stack ?? ""}` : String(error);
17917
18890
  diagnostics.report("slot-entry-crashed", `slot "${key}"${id}: ${message}`);
17918
18891
  if (key === "shell.overlay" && entry.options.id === "claude-diff-overlay") restoreDiff();
18892
+ if (key === "shell.overlay" && entry.options.id === "claude-plan-overlay") restorePlan();
17919
18893
  }), "dsh-claude: Slot entry failure reporting");
17920
18894
  const openTasksPanel = (sessionId, turn) => {
17921
18895
  closePluginDetails();
@@ -17937,6 +18911,55 @@ window.__ModuleLoader__.load({
17937
18911
  layout?.openDetails();
17938
18912
  disposeExpandedDetailsResize = enableExpandedDetailsResize();
17939
18913
  };
18914
+ const openPlanPanel = (sessionId) => {
18915
+ closePluginDetails();
18916
+ const maximizePlan = () => {
18917
+ if (disposePlanOverlay !== void 0) {
18918
+ restorePlan();
18919
+ return;
18920
+ }
18921
+ disposeExpandedDetailsResize?.();
18922
+ disposeExpandedDetailsResize = void 0;
18923
+ layout?.closeDetails();
18924
+ try {
18925
+ disposePlanOverlay = ctx.slots.register({
18926
+ name: "shell.overlay",
18927
+ id: "claude-plan-overlay",
18928
+ locale: namespace
18929
+ }, () => /* @__PURE__ */ jsx(MaximizedPlan, {
18930
+ source: projections.source(sessionId),
18931
+ t,
18932
+ sessionId,
18933
+ closeDetails: closePluginDetails,
18934
+ restore: restorePlan
18935
+ }));
18936
+ } catch {
18937
+ disposePlanOverlay = void 0;
18938
+ layout?.openDetails();
18939
+ disposeExpandedDetailsResize = enableExpandedDetailsResize();
18940
+ }
18941
+ };
18942
+ try {
18943
+ disposePluginDetails = ctx.slots.register({
18944
+ name: "details",
18945
+ priority: -10,
18946
+ locale: namespace,
18947
+ inject: () => ({
18948
+ t,
18949
+ sessionId,
18950
+ closeDetails: closePluginDetails,
18951
+ maximized: false,
18952
+ toggleMaximized: maximizePlan
18953
+ })
18954
+ }, ClaudePlanPanel);
18955
+ } catch {
18956
+ return;
18957
+ }
18958
+ detailsSessionId = sessionId;
18959
+ planOpen.open(sessionId);
18960
+ layout?.openDetails();
18961
+ disposeExpandedDetailsResize = enableExpandedDetailsResize();
18962
+ };
17940
18963
  const openOverviewPanel = (sessionId) => {
17941
18964
  if (sessions === void 0) return;
17942
18965
  closePluginDetails();
@@ -18027,7 +19050,7 @@ window.__ModuleLoader__.load({
18027
19050
  ctx.effect(() => {
18028
19051
  if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => closePluginDetails();
18029
19052
  const observer = new MutationObserver(() => {
18030
- if (detailsSessionId !== void 0 && disposeDiffOverlay === void 0 && document.querySelector("[data-details-collapsed]") !== null) closePluginDetails();
19053
+ if (detailsSessionId !== void 0 && disposeDiffOverlay === void 0 && disposePlanOverlay === void 0 && document.querySelector("[data-details-collapsed]") !== null) closePluginDetails();
18031
19054
  });
18032
19055
  observer.observe(document.body, {
18033
19056
  attributes: true,
@@ -18073,6 +19096,20 @@ window.__ModuleLoader__.load({
18073
19096
  })
18074
19097
  }, ClaudeAgentPresetLabel));
18075
19098
  }
19099
+ ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
19100
+ name: "conversation.session.header.utilities",
19101
+ id: "claude-plan",
19102
+ order: 29,
19103
+ locale: namespace,
19104
+ inject: (sessionId) => ({
19105
+ t,
19106
+ togglePlan: () => {
19107
+ if (planOpen.isOpen(sessionId)) closePluginDetails();
19108
+ else openPlanPanel(sessionId);
19109
+ },
19110
+ planOpen: planOpen.sourceFor(sessionId)
19111
+ })
19112
+ }, ClaudePlanHeaderAction));
18076
19113
  ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
18077
19114
  name: "conversation.session.header.utilities",
18078
19115
  id: "claude-diff",