@norman-else/dsh-claude 0.1.40 → 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,
@@ -4387,9 +4552,8 @@ window.__ModuleLoader__.load({
4387
4552
  feed: applyLine,
4388
4553
  subscribe(listener) {
4389
4554
  if (disposed) return () => {};
4390
- const wasIdle = listeners.size === 0;
4391
4555
  listeners.add(listener);
4392
- if (wasIdle) onDemand(true);
4556
+ onDemand(true);
4393
4557
  return () => {
4394
4558
  listeners.delete(listener);
4395
4559
  if (listeners.size !== 0) return;
@@ -4405,6 +4569,13 @@ window.__ModuleLoader__.load({
4405
4569
  }
4406
4570
  };
4407
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
+ }
4408
4579
  /**
4409
4580
  * Every session's projection over ONE connection.
4410
4581
  *
@@ -4477,10 +4648,12 @@ window.__ModuleLoader__.load({
4477
4648
  * a session list would otherwise reopen it once per row. */
4478
4649
  #demand(sessionId, active) {
4479
4650
  if (this.#disposed) return;
4651
+ const before = this.#lanes();
4480
4652
  if (active) {
4481
4653
  this.#wanted.delete(sessionId);
4482
4654
  this.#wanted.add(sessionId);
4483
4655
  } else if (!this.#wanted.delete(sessionId)) return;
4656
+ if (sameLanes(before, this.#lanes())) return;
4484
4657
  if (this.#settle !== void 0) clearTimeout(this.#settle);
4485
4658
  const timer = setTimeout(() => {
4486
4659
  this.#settle = void 0;
@@ -4624,10 +4797,10 @@ window.__ModuleLoader__.load({
4624
4797
  ".dsh-claude-act-running{animation:dsh-claude-act-pulse 1.2s ease-in-out infinite}",
4625
4798
  "@keyframes dsh-claude-act-pulse{0%,100%{opacity:1}50%{opacity:.3}}"
4626
4799
  ].join("");
4627
- let cssInjected$3 = false;
4628
- function ensureCss$3() {
4629
- if (cssInjected$3 || typeof document === "undefined") return;
4630
- cssInjected$3 = true;
4800
+ let cssInjected$4 = false;
4801
+ function ensureCss$4() {
4802
+ if (cssInjected$4 || typeof document === "undefined") return;
4803
+ cssInjected$4 = true;
4631
4804
  const element = document.createElement("style");
4632
4805
  element.dataset.dshClaudeActivity = "";
4633
4806
  element.textContent = ACTIVITY_CSS;
@@ -5055,7 +5228,7 @@ window.__ModuleLoader__.load({
5055
5228
  });
5056
5229
  }
5057
5230
  function ClaudeActivityNode({ node, useClaudeProjection, t }) {
5058
- ensureCss$3();
5231
+ ensureCss$4();
5059
5232
  const marker = node.data;
5060
5233
  const activities = useClaudeProjection((value) => selectStepActivities(value, marker.turn, marker.step));
5061
5234
  const tasks = useClaudeProjection((value) => value.tasks?.tasks ?? EMPTY_TASKS$1);
@@ -5521,91 +5694,592 @@ window.__ModuleLoader__.load({
5521
5694
  });
5522
5695
  }
5523
5696
  //#endregion
5524
- //#region src/client/jira-api.ts
5525
- var JiraClientError = class extends Error {
5526
- code;
5527
- constructor(message, code) {
5528
- super(message);
5529
- this.name = "JiraClientError";
5530
- 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;
5531
5708
  }
5532
- };
5709
+ }
5710
+ //#endregion
5711
+ //#region src/client/pr-feedback-api.ts
5533
5712
  function record$5(value) {
5534
5713
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
5535
5714
  }
5536
- /**
5537
- * Every Jira failure the panels catch is a `JiraClientError`, whatever the
5538
- * transport threw: `ClaudeHeroRepositoryControls` branches on `code` to tell
5539
- * 'not-connected' apart from a real outage, and the settings card renders the
5540
- * message verbatim.
5541
- *
5542
- * The routes answer `{ error, message }`, so `message` is already the sentence
5543
- * to show. A body carrying only a code — a 405, a bad JSON body — used to read
5544
- * 'Jira is unavailable.' rather than leaking the code as prose, and it still
5545
- * does. Transport failures (a starved pool, an elapsed budget, an older Host
5546
- * without the route) carry their own wording and keep it.
5547
- */
5548
- function jiraFailure(cause) {
5549
- if (cause instanceof JiraClientError) return cause;
5550
- if (!(cause instanceof PluginRequestError)) return new JiraClientError(cause instanceof Error ? cause.message : String(cause));
5551
- 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
+ };
5552
5721
  }
5553
- function payload(value) {
5722
+ function answer(value) {
5554
5723
  const body = record$5(value);
5555
- if (body === void 0) throw new JiraClientError("Invalid Jira response.");
5556
- return body;
5557
- }
5558
- function status(value) {
5559
- const body = payload(value);
5560
- 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.");
5561
5725
  return body;
5562
5726
  }
5563
- async function loadJiraStatus(signal) {
5564
- try {
5565
- return status(await pluginRead(`${CLAUDE_JIRA_PATH}/status`, "remote", signal));
5566
- } catch (cause) {
5567
- throw jiraFailure(cause);
5568
- }
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) }));
5569
5731
  }
5570
- async function connectJira(input) {
5571
- try {
5572
- return status(await pluginWrite(`${CLAUDE_JIRA_PATH}/connect`, "remote", void 0, { json: input }));
5573
- } catch (cause) {
5574
- throw jiraFailure(cause);
5575
- }
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
+ }));
5576
5737
  }
5577
- async function disconnectJira() {
5578
- try {
5579
- await pluginWrite(`${CLAUDE_JIRA_PATH}/disconnect`, "remote");
5580
- } catch (cause) {
5581
- throw jiraFailure(cause);
5582
- }
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;
5583
5742
  }
5584
- async function searchJiraTickets(query, signal) {
5585
- try {
5586
- const body = payload(await pluginRead(`${CLAUDE_JIRA_PATH}/search`, "remote", signal, { query: { query } }));
5587
- if (!Array.isArray(body.tickets)) throw new JiraClientError("Invalid Jira search response.");
5588
- const tickets = [];
5589
- for (const item of body.tickets) {
5590
- const ticket = record$5(item);
5591
- if (ticket === void 0 || typeof ticket.key !== "string" || typeof ticket.summary !== "string" || typeof ticket.url !== "string") continue;
5592
- tickets.push(ticket);
5593
- }
5594
- return tickets;
5595
- } catch (cause) {
5596
- 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
+ });
5597
5761
  }
5762
+ return threads;
5598
5763
  }
5599
- async function assignJiraTicket(key) {
5600
- try {
5601
- await pluginWrite(`${CLAUDE_JIRA_PATH}/assign`, "remote", void 0, { json: { key } });
5602
- } catch (cause) {
5603
- throw jiraFailure(cause);
5604
- }
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;
5605
5772
  }
5606
- /** Draft seeded into the composer when a session starts from a ticket. */
5607
- function ticketPrompt(ticket) {
5608
- return `Work on Jira ticket ${ticket.key}: ${ticket.summary}\n${ticket.url}\n\nRead the ticket, implement what it asks for, and reference ${ticket.key} in the commit and pull request.`;
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
+ }
6279
+ }
6280
+ /** Draft seeded into the composer when a session starts from a ticket. */
6281
+ function ticketPrompt(ticket) {
6282
+ return `Work on Jira ticket ${ticket.key}: ${ticket.summary}\n${ticket.url}\n\nRead the ticket, implement what it asks for, and reference ${ticket.key} in the commit and pull request.`;
5609
6283
  }
5610
6284
  /** Appended to a user-written draft so the session still knows its ticket. */
5611
6285
  function ticketContext(ticket) {
@@ -5835,6 +6509,12 @@ window.__ModuleLoader__.load({
5835
6509
  const value = settings.find((setting) => setting.key === "prose")?.value;
5836
6510
  return isClaudeProseMode(value) ? value : DEFAULT_CLAUDE_PROSE_MODE;
5837
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
+ }
5838
6518
  /** Settings whose row only makes sense under a particular value of another.
5839
6519
  * Filtering here rather than server-side keeps the descriptor list flat: the
5840
6520
  * server has no view of what the Client can paint. Fails OPEN — a payload
@@ -5860,6 +6540,10 @@ window.__ModuleLoader__.load({
5860
6540
  label: "prose",
5861
6541
  hint: "proseEffect"
5862
6542
  },
6543
+ alerts: {
6544
+ label: "alerts",
6545
+ hint: "alertsEffect"
6546
+ },
5863
6547
  worktreeBranchPrefix: {
5864
6548
  label: "worktreeBranchPrefix",
5865
6549
  hint: "worktreeBranchPrefixEffect"
@@ -5880,7 +6564,9 @@ window.__ModuleLoader__.load({
5880
6564
  "renderer:plugin": "rendererPlugin",
5881
6565
  "renderer:native": "rendererNative",
5882
6566
  "prose:plain": "prosePlain",
5883
- "prose:enhanced": "proseEnhanced"
6567
+ "prose:enhanced": "proseEnhanced",
6568
+ "alerts:off": "alertsOff",
6569
+ "alerts:on": "alertsOn"
5884
6570
  };
5885
6571
  function settingOptionLabel(settingKey, option, t) {
5886
6572
  const key = SETTING_OPTION_COPY[`${settingKey}:${option.value}`];
@@ -6120,6 +6806,7 @@ window.__ModuleLoader__.load({
6120
6806
  if (!isGlobalSettingsView(payload)) throw new Error("Invalid global settings response");
6121
6807
  setGlobalSettings(payload);
6122
6808
  applyClaudeMarkdownTheme(proseModeOf(payload.settings));
6809
+ setClaudeAlertsEnabled(alertModeOf(payload.settings) === "on");
6123
6810
  } catch (cause) {
6124
6811
  setGlobalSettingsError(cardFailure(cause));
6125
6812
  } finally {
@@ -6467,6 +7154,531 @@ window.__ModuleLoader__.load({
6467
7154
  });
6468
7155
  }
6469
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
7592
+ },
7593
+ disabled: sending || notes.length === 0 && draft.trim().length === 0,
7594
+ onClick: send,
7595
+ children: t(sending ? "planSending" : "planSendForChanges")
7596
+ })]
7597
+ })
7598
+ ]
7599
+ })
7600
+ ]
7601
+ });
7602
+ }
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
6470
7682
  //#region src/client/repository-action-api.ts
6471
7683
  var RepositoryActionClientError = class extends Error {
6472
7684
  code;
@@ -6478,7 +7690,7 @@ window.__ModuleLoader__.load({
6478
7690
  if (commit !== void 0) this.commit = commit;
6479
7691
  }
6480
7692
  };
6481
- function record$4(value) {
7693
+ function record$3(value) {
6482
7694
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
6483
7695
  }
6484
7696
  /** The dialog branches on `code`, so a route refusal keeps arriving as this
@@ -6491,20 +7703,20 @@ window.__ModuleLoader__.load({
6491
7703
  return error instanceof PluginRequestError ? new RepositoryActionClientError(error.message, error.code) : error;
6492
7704
  }
6493
7705
  function preview(value) {
6494
- const input = record$4(value);
7706
+ const input = record$3(value);
6495
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.");
6496
7708
  for (const file of input.files) {
6497
- const item = record$4(file);
7709
+ const item = record$3(file);
6498
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.");
6499
7711
  }
6500
7712
  for (const commit of input.unpushedCommits) {
6501
- const item = record$4(commit);
7713
+ const item = record$3(commit);
6502
7714
  if (item === void 0 || typeof item.hash !== "string" || typeof item.subject !== "string") throw new Error("Invalid repository action commit.");
6503
7715
  }
6504
7716
  return input;
6505
7717
  }
6506
7718
  function result(value) {
6507
- const input = record$4(value);
7719
+ const input = record$3(value);
6508
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.");
6509
7721
  return input;
6510
7722
  }
@@ -6518,7 +7730,7 @@ window.__ModuleLoader__.load({
6518
7730
  }
6519
7731
  async function generateCommitMessage(sessionId, fingerprint, signal) {
6520
7732
  try {
6521
- 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, {
6522
7734
  query: { sessionId },
6523
7735
  json: { fingerprint }
6524
7736
  }));
@@ -6581,16 +7793,16 @@ window.__ModuleLoader__.load({
6581
7793
  /** The draft only has to describe the work; the host truncates it again before
6582
7794
  * summarizing, and the setup route caps the whole body at 16 KiB. */
6583
7795
  const MAX_INTENT_CHARS = 2e3;
6584
- function record$3(value) {
7796
+ function record$2(value) {
6585
7797
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
6586
7798
  }
6587
7799
  function setupResult(value) {
6588
- const item = record$3(value);
7800
+ const item = record$2(value);
6589
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;
6590
7802
  return item;
6591
7803
  }
6592
7804
  function parseRepositorySetupEvent(line, onProgress) {
6593
- const event = record$3(JSON.parse(line));
7805
+ const event = record$2(JSON.parse(line));
6594
7806
  if (event?.type === "progress" && typeof event.stage === "string" && HOST_STAGES.has(event.stage)) {
6595
7807
  onProgress(event.stage);
6596
7808
  return;
@@ -6667,235 +7879,46 @@ window.__ModuleLoader__.load({
6667
7879
  } });
6668
7880
  if (body.mode !== "worktree" && body.mode !== "checkout" || typeof body.root !== "string" || typeof body.branch !== "string") throw new Error("Invalid repository cleanup response.");
6669
7881
  return body;
6670
- }
6671
- /** Ask the Host to reconcile worktrees against the workspace registry now.
6672
- * Deleting a workspace only touches the registry, so this kick is what makes
6673
- * the worktree and its sessions go without waiting out the sweep interval. */
6674
- async function sweepWorktrees() {
6675
- await pluginWrite(`${CLAUDE_REPOSITORY_SETUP_PATH}/sweep`, "fast");
6676
- }
6677
- /** Lines [from, to] of a working-tree file plus its total line count, for expanding unmodified diff context. */
6678
- async function loadRepositoryFileLines(cwd, path, from, to, signal) {
6679
- const body = await pluginRead(CLAUDE_REPOSITORY_FILE_PATH, "git", signal, { query: {
6680
- cwd,
6681
- path,
6682
- from: String(from),
6683
- to: String(to)
6684
- } });
6685
- if (!Array.isArray(body.lines) || typeof body.total !== "number") throw new Error("Invalid repository file response.");
6686
- return {
6687
- lines: body.lines.map(String),
6688
- total: body.total
6689
- };
6690
- }
6691
- async function loadRepositoryStatusFor(cwd, signal) {
6692
- const body = await pluginRead(CLAUDE_REPOSITORY_STATUS_PATH, "git", signal, { query: { cwd } });
6693
- if (typeof body.status !== "string" || typeof body.cwd !== "string") throw new Error("Invalid repository status response.");
6694
- return body;
6695
- }
6696
- //#endregion
6697
- //#region src/client/relative-age.ts
6698
- /** Compact age of an ISO timestamp, in the shape the panels already use
6699
- * ("<1h", "4h", "3d", "2mo"). `now` is a parameter so callers that re-render
6700
- * on a clock — and tests — stay deterministic. */
6701
- function relativeAge(value, now = Date.now()) {
6702
- if (value === void 0) return void 0;
6703
- const elapsedHours = Math.max(0, Math.floor((now - Date.parse(value)) / 36e5));
6704
- if (!Number.isFinite(elapsedHours)) return void 0;
6705
- if (elapsedHours < 1) return "<1h";
6706
- if (elapsedHours < 24) return `${elapsedHours}h`;
6707
- const days = Math.floor(elapsedHours / 24);
6708
- return days < 30 ? `${days}d` : `${Math.floor(days / 30)}mo`;
6709
- }
6710
- //#endregion
6711
- //#region src/github-url.ts
6712
- /** Only GitHub's own image hosts; the browser loads these directly, so a URL
6713
- * the API did not vouch for must never become an outbound request. */
6714
- function githubAvatarUrl(value) {
6715
- if (typeof value !== "string" || value.length === 0 || value.length > 1024) return void 0;
6716
- try {
6717
- const url = new URL(value);
6718
- const allowed = url.hostname === "github.com" || url.hostname === "githubusercontent.com" || url.hostname.endsWith(".githubusercontent.com");
6719
- return url.protocol === "https:" && allowed ? url.href : void 0;
6720
- } catch {
6721
- return;
6722
- }
6723
- }
6724
- //#endregion
6725
- //#region src/client/pr-feedback-api.ts
6726
- function record$2(value) {
6727
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
6728
- }
6729
- function feedbackQuery(sessionId, pullNumber, extra) {
6730
- return {
6731
- sessionId,
6732
- number: String(pullNumber),
6733
- ...extra
6734
- };
6735
- }
6736
- function answer(value) {
6737
- const body = record$2(value);
6738
- if (body === void 0) throw new Error("Invalid pull request feedback response.");
6739
- return body;
6740
- }
6741
- /** Every arm of this route shells out to `gh`, so reads and writes alike take
6742
- * the remote budget. */
6743
- async function loadJson(path, sessionId, pullNumber, signal, extra) {
6744
- return answer(await pluginRead(`${CLAUDE_REPOSITORY_FEEDBACK_PATH}${path}`, "remote", signal, { query: feedbackQuery(sessionId, pullNumber, extra) }));
6745
- }
6746
- async function postJson(path, sessionId, pullNumber, input) {
6747
- return answer(await pluginWrite(`${CLAUDE_REPOSITORY_FEEDBACK_PATH}${path}`, "remote", void 0, {
6748
- query: feedbackQuery(sessionId, pullNumber),
6749
- json: input
6750
- }));
6751
- }
6752
- function reviewComment(value) {
6753
- const input = record$2(value);
6754
- 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;
6755
- return input;
6756
- }
6757
- async function loadPullRequestThreads(sessionId, pullNumber, signal) {
6758
- const body = await loadJson("/comments", sessionId, pullNumber, signal);
6759
- if (!Array.isArray(body.threads)) throw new Error("Invalid pull request comments response.");
6760
- const threads = [];
6761
- for (const item of body.threads) {
6762
- const input = record$2(item);
6763
- 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;
6764
- const comments = input.comments.map(reviewComment).filter((value) => value !== void 0);
6765
- if (comments.length === 0) continue;
6766
- threads.push({
6767
- id: input.id,
6768
- path: input.path,
6769
- ...typeof input.line === "number" ? { line: input.line } : {},
6770
- side: input.side,
6771
- resolved: input.resolved === true,
6772
- outdated: input.outdated === true,
6773
- comments
6774
- });
6775
- }
6776
- return threads;
6777
- }
6778
- /** Post one reply into the thread that `commentId` belongs to. */
6779
- async function replyToReviewThread(sessionId, pullNumber, commentId, body) {
6780
- const comment = reviewComment((await postJson("/reply", sessionId, pullNumber, {
6781
- commentId,
6782
- body
6783
- })).comment);
6784
- if (comment === void 0) throw new Error("Invalid pull request reply response.");
6785
- return comment;
6786
- }
6787
- /** Resolve or reopen a thread; returns the state GitHub reports afterwards. */
6788
- async function setReviewThreadResolved(sessionId, pullNumber, threadId, resolved) {
6789
- const answer = await postJson("/resolve", sessionId, pullNumber, {
6790
- threadId,
6791
- resolved
6792
- });
6793
- if (typeof answer.resolved !== "boolean") throw new Error("Invalid pull request resolve response.");
6794
- return answer.resolved;
6795
- }
6796
- /** Logins GitHub would notify, for the reply composer's `@` completion. */
6797
- async function loadMentionableUsers(sessionId, pullNumber, query, signal) {
6798
- const body = await loadJson("/mentionables", sessionId, pullNumber, signal, { q: query });
6799
- if (!Array.isArray(body.users)) return [];
6800
- const users = [];
6801
- for (const item of body.users) {
6802
- const input = record$2(item);
6803
- if (input === void 0 || typeof input.login !== "string" || input.login.length === 0) continue;
6804
- const avatarUrl = githubAvatarUrl(input.avatarUrl);
6805
- users.push({
6806
- login: input.login,
6807
- ...avatarUrl === void 0 ? {} : { avatarUrl }
6808
- });
6809
- }
6810
- return users;
6811
- }
6812
- async function loadFailingChecks(sessionId, pullNumber, signal) {
6813
- const body = await loadJson("/checks", sessionId, pullNumber, signal);
6814
- if (!Array.isArray(body.checks)) throw new Error("Invalid pull request checks response.");
6815
- const checks = [];
6816
- for (const item of body.checks) {
6817
- const input = record$2(item);
6818
- 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;
6819
- checks.push(input);
6820
- }
6821
- return checks;
6822
- }
6823
- /** Draft handed to Claude when the user forwards GitHub review comments. A
6824
- * resolved thread is a settled conversation: forwarding it would ask Claude to
6825
- * redo work the reviewers already signed off. */
6826
- function composeCommentsPrompt(threads) {
6827
- const open = threads.filter((thread) => !thread.resolved);
6828
- if (open.length === 0) return "";
6829
- 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) => {
6830
- const [first, ...rest] = thread.comments;
6831
- if (first === void 0) return "";
6832
- 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");
6833
- }).filter((block) => block.length > 0).join("\n")}`;
6834
- }
6835
- /** Draft handed to Claude when the user forwards failing CI checks. */
6836
- function composeChecksPrompt(checks) {
6837
- 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) => {
6838
- 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\`\`\``}`;
6839
- }).join("\n\n")}`;
6840
- }
6841
- /** Draft handed to Claude after an update-branch merge left conflicts behind. */
6842
- function composeConflictsPrompt(baseBranch, conflicts, method = "merge") {
6843
- const list = conflicts.map((file) => `- ${file}`).join("\n");
6844
- 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}`;
6845
- 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}`;
6846
- }
6847
- //#endregion
6848
- //#region src/client/auto-fix.ts
6849
- const AUTO_FIX_INTERVAL_MS = 3e4;
6850
- 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.";
6851
- const EMPTY_MEMORY = { handledCommentIds: /* @__PURE__ */ new Set() };
6852
- const sessions = /* @__PURE__ */ new Map();
6853
- function session(sessionId) {
6854
- let entry = sessions.get(sessionId);
6855
- if (entry === void 0) {
6856
- entry = {
6857
- enabled: false,
6858
- memory: EMPTY_MEMORY
6859
- };
6860
- sessions.set(sessionId, entry);
6861
- }
6862
- return entry;
6863
- }
6864
- function autoFixEnabled(sessionId) {
6865
- return session(sessionId).enabled;
6866
- }
6867
- function setAutoFixEnabled(sessionId, enabled) {
6868
- session(sessionId).enabled = enabled;
6869
- }
6870
- function autoFixMemory(sessionId) {
6871
- return session(sessionId).memory;
6872
- }
6873
- function rememberAutoFix(sessionId, memory) {
6874
- session(sessionId).memory = memory;
6875
- }
6876
- /** One failing CI run yields one fix attempt: run links change when CI re-runs. */
6877
- function checksSignature(checks) {
6878
- if (checks.length === 0) return void 0;
6879
- return checks.map((check) => `${check.name}|${check.link ?? ""}`).sort().join("\n");
6880
- }
6881
- function planAutoFix(memory, threads, checks) {
6882
- const unhandled = threads.filter((thread) => !thread.resolved).filter((thread) => thread.comments.some((comment) => !memory.handledCommentIds.has(comment.id)));
6883
- const fresh = unhandled.flatMap((thread) => thread.comments);
6884
- const signature = checksSignature(checks);
6885
- const checksChanged = signature !== void 0 && signature !== memory.handledChecksSignature;
6886
- const sections = [];
6887
- if (unhandled.length > 0) sections.push(composeCommentsPrompt(unhandled));
6888
- if (checksChanged) sections.push(composeChecksPrompt(checks));
6889
- if (sections.length === 0) return { memory };
6890
- const nextSignature = checksChanged ? signature : memory.handledChecksSignature;
7882
+ }
7883
+ /** Ask the Host to reconcile worktrees against the workspace registry now.
7884
+ * Deleting a workspace only touches the registry, so this kick is what makes
7885
+ * the worktree and its sessions go without waiting out the sweep interval. */
7886
+ async function sweepWorktrees() {
7887
+ await pluginWrite(`${CLAUDE_REPOSITORY_SETUP_PATH}/sweep`, "fast");
7888
+ }
7889
+ /** Lines [from, to] of a working-tree file plus its total line count, for expanding unmodified diff context. */
7890
+ async function loadRepositoryFileLines(cwd, path, from, to, signal) {
7891
+ const body = await pluginRead(CLAUDE_REPOSITORY_FILE_PATH, "git", signal, { query: {
7892
+ cwd,
7893
+ path,
7894
+ from: String(from),
7895
+ to: String(to)
7896
+ } });
7897
+ if (!Array.isArray(body.lines) || typeof body.total !== "number") throw new Error("Invalid repository file response.");
6891
7898
  return {
6892
- prompt: `${sections.join("\n\n")}\n\n${AUTO_FIX_FOOTER}`,
6893
- memory: {
6894
- handledCommentIds: /* @__PURE__ */ new Set([...memory.handledCommentIds, ...fresh.map((comment) => comment.id)]),
6895
- ...nextSignature === void 0 ? {} : { handledChecksSignature: nextSignature }
6896
- }
7899
+ lines: body.lines.map(String),
7900
+ total: body.total
6897
7901
  };
6898
7902
  }
7903
+ async function loadRepositoryStatusFor(cwd, signal) {
7904
+ const body = await pluginRead(CLAUDE_REPOSITORY_STATUS_PATH, "git", signal, { query: { cwd } });
7905
+ if (typeof body.status !== "string" || typeof body.cwd !== "string") throw new Error("Invalid repository status response.");
7906
+ return body;
7907
+ }
7908
+ //#endregion
7909
+ //#region src/client/relative-age.ts
7910
+ /** Compact age of an ISO timestamp, in the shape the panels already use
7911
+ * ("<1h", "4h", "3d", "2mo"). `now` is a parameter so callers that re-render
7912
+ * on a clock — and tests — stay deterministic. */
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`;
7921
+ }
6899
7922
  //#endregion
6900
7923
  //#region src/client/boot-check.ts
6901
7924
  /** Boot-time assertions against the running Host.
@@ -7088,7 +8111,7 @@ window.__ModuleLoader__.load({
7088
8111
  ]
7089
8112
  });
7090
8113
  }
7091
- function repositoryName$1(remote) {
8114
+ function repositoryName(remote) {
7092
8115
  return remote?.split("/").at(-1);
7093
8116
  }
7094
8117
  function PullRequestHoverCard({ repository, t }) {
@@ -7116,7 +8139,7 @@ window.__ModuleLoader__.load({
7116
8139
  /* @__PURE__ */ jsxs("span", {
7117
8140
  style: repositoryPrHoverRepo,
7118
8141
  children: [
7119
- repositoryName$1(repository.remote),
8142
+ repositoryName(repository.remote),
7120
8143
  " #",
7121
8144
  pullRequest.number,
7122
8145
  pullRequest.baseBranch === void 0 ? "" : ` → ${pullRequest.baseBranch}`
@@ -7894,7 +8917,7 @@ window.__ModuleLoader__.load({
7894
8917
  }),
7895
8918
  repository.remote === void 0 ? null : /* @__PURE__ */ jsx("span", {
7896
8919
  style: repositoryRemote,
7897
- children: repositoryName$1(repository.remote)
8920
+ children: repositoryName(repository.remote)
7898
8921
  }),
7899
8922
  /* @__PURE__ */ jsx(Tooltip, {
7900
8923
  label: branch,
@@ -13939,7 +14962,7 @@ window.__ModuleLoader__.load({
13939
14962
  ] });
13940
14963
  }
13941
14964
  //#endregion
13942
- //#region src/client/ClaudeDiffOverlay.tsx
14965
+ //#region src/client/ClaudePanelOverlay.tsx
13943
14966
  const useClientLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
13944
14967
  function shouldRestoreFromEscape(event, root = document) {
13945
14968
  return event.key === "Escape" && root.querySelector("[role=\"dialog\"][aria-modal=\"true\"]") === null;
@@ -13987,7 +15010,7 @@ window.__ModuleLoader__.load({
13987
15010
  window.removeEventListener("resize", update);
13988
15011
  };
13989
15012
  }
13990
- function ClaudeDiffOverlay({ children, onRestore }) {
15013
+ function ClaudePanelOverlay({ children, onRestore }) {
13991
15014
  const ref = useRef(null);
13992
15015
  const [bounds, setBounds] = useState();
13993
15016
  useClientLayoutEffect(() => {
@@ -14006,7 +15029,7 @@ window.__ModuleLoader__.load({
14006
15029
  }, [onRestore]);
14007
15030
  return /* @__PURE__ */ jsx("div", {
14008
15031
  ref,
14009
- "data-dsh-claude-diff-overlay": true,
15032
+ "data-dsh-claude-panel-overlay": true,
14010
15033
  style: {
14011
15034
  position: "absolute",
14012
15035
  left: bounds?.left ?? 0,
@@ -14075,304 +15098,110 @@ window.__ModuleLoader__.load({
14075
15098
  label: hint ?? label,
14076
15099
  side: "bottom",
14077
15100
  delayMs: 400,
14078
- children: /* @__PURE__ */ jsx("button", {
14079
- type: "button",
14080
- className: panelIconButtonClass,
14081
- "aria-label": label,
14082
- disabled,
14083
- onClick,
14084
- children: icon
14085
- })
14086
- });
14087
- return /* @__PURE__ */ jsxs("div", {
14088
- style: repositoryBarFrame,
14089
- "data-claude-queue-dock": "",
14090
- children: [/* @__PURE__ */ jsx("style", {
14091
- "data-dsh-claude-queue-styles": true,
14092
- children: panelIconButtonCss
14093
- }), /* @__PURE__ */ jsxs("div", {
14094
- style: queueBar,
14095
- children: [queue.length > 1 ? /* @__PURE__ */ jsxs("button", {
14096
- type: "button",
14097
- style: queueHeader,
14098
- "aria-controls": listId,
14099
- "aria-expanded": expanded,
14100
- disabled: interacting,
14101
- onClick: () => {
14102
- setCollapsed((value) => !value);
14103
- },
14104
- children: [
14105
- /* @__PURE__ */ jsx("span", {
14106
- style: queueLead,
14107
- "aria-hidden": "true",
14108
- children: /* @__PURE__ */ jsx(IconQueueOutline14, {})
14109
- }),
14110
- /* @__PURE__ */ jsx("span", {
14111
- style: queueCount,
14112
- children: t("queueCount", { n: queue.length })
14113
- }),
14114
- /* @__PURE__ */ jsx("span", {
14115
- style: queueLead,
14116
- "aria-hidden": "true",
14117
- children: expanded ? /* @__PURE__ */ jsx(IconChevronDownOutline14, {}) : /* @__PURE__ */ jsx(IconChevronUpOutline14, {})
14118
- })
14119
- ]
14120
- }) : null, /* @__PURE__ */ jsx("ul", {
14121
- id: listId,
14122
- style: queueList,
14123
- hidden: !listVisible,
14124
- children: listVisible ? queue.map((row, index) => /* @__PURE__ */ jsxs("li", {
14125
- style: {
14126
- ...queueRow,
14127
- ...index > 0 ? queueRowDivider : {}
14128
- },
14129
- children: [
14130
- queue.length === 1 ? /* @__PURE__ */ jsx("span", {
14131
- style: queueLead,
14132
- "aria-hidden": "true",
14133
- children: /* @__PURE__ */ jsx(IconQueueOutline14, {})
14134
- }) : null,
14135
- editing?.id === row.id ? /* @__PURE__ */ jsx("input", {
14136
- autoFocus: true,
14137
- style: queueEditor,
14138
- "aria-label": t("queueEdit"),
14139
- value: editing.text,
14140
- onChange: (event) => {
14141
- setEditing({
14142
- id: row.id,
14143
- text: event.currentTarget.value
14144
- });
14145
- },
14146
- onKeyDown: (event) => {
14147
- if (event.key === "Escape") setEditing(void 0);
14148
- else if (event.key === "Enter" && !event.nativeEvent.isComposing) {
14149
- event.preventDefault();
14150
- saveEdit();
14151
- }
14152
- }
14153
- }) : /* @__PURE__ */ jsx("span", {
14154
- style: queuePreview,
14155
- children: row.preview
14156
- }),
14157
- mutable ? /* @__PURE__ */ jsx("span", {
14158
- style: queueActions,
14159
- children: editing?.id === row.id ? /* @__PURE__ */ jsxs(Fragment$1, { children: [action(t("queueSave"), /* @__PURE__ */ jsx(IconCheckOutline16, { size: 14 }), () => {
14160
- saveEdit();
14161
- }, busy !== void 0 || editing.text.trim() === ""), action(t("queueCancelEdit"), /* @__PURE__ */ jsx(IconCloseOutline16, { size: 14 }), () => {
14162
- setEditing(void 0);
14163
- }, busy !== void 0)] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
14164
- action(t("queueEdit"), /* @__PURE__ */ jsx(IconEditOutline16, { size: 14 }), () => {
14165
- if (row.text !== null) setEditing({
14166
- id: row.id,
14167
- text: row.text
14168
- });
14169
- }, busy !== void 0 || row.text === null, row.text === null ? t("queueEditUnsupported") : void 0),
14170
- action(t("queueRemove"), /* @__PURE__ */ jsx(IconTrashOutline16, { size: 14 }), () => {
14171
- apply(row.id, { kind: "remove" }, t("queueRemoveFailed"));
14172
- }, busy !== void 0),
14173
- action(t("queueSteer"), /* @__PURE__ */ jsx(IconSendOutline14, {}), () => {
14174
- apply(row.id, { kind: "steer" }, t("queueSteerFailed"));
14175
- }, busy !== void 0 || !running, running ? void 0 : t("queueSteerUnavailable"))
14176
- ] })
14177
- }) : null
14178
- ]
14179
- }, row.id)) : null
14180
- })]
14181
- })]
14182
- });
14183
- }
14184
- //#endregion
14185
- //#region src/client/session-preset.ts
14186
- /**
14187
- * Resolve one row's preset id, newest seat first.
14188
- * @param row - a session-list row, or undefined when the id is not listed.
14189
- * @returns the preset id, or undefined when neither source carries one.
14190
- */
14191
- function sessionRowPreset(row) {
14192
- return row?.agentPreset ?? row?.projectionValues?.agentPreset ?? void 0;
14193
- }
14194
- //#endregion
14195
- //#region src/client/ClaudePullRequestsPanel.tsx
14196
- const NO_WORKSPACE_STATE = {};
14197
- const NO_WORKSPACES = {
14198
- subscribe: () => () => {},
14199
- getSnapshot: () => NO_WORKSPACE_STATE
14200
- };
14201
- const OVERVIEW_REFRESH_MS = 3e4;
14202
- /** What a running session is blocked on: the latest permission or question
14203
- * activity that is still in its started phase. */
14204
- function overviewAttention(activities) {
14205
- for (let index = activities.length - 1; index >= 0; index -= 1) {
14206
- const activity = activities[index];
14207
- if (activity === void 0 || activity.kind !== "permission" && activity.kind !== "question") continue;
14208
- return activity.phase === "started" ? activity.kind : void 0;
14209
- }
14210
- }
14211
- /** Claude sessions worth listing: rows still in the host list (byId keeps
14212
- * deleted and breadcrumb rows), non-blank, non-subagent, with a checkout;
14213
- * running first. */
14214
- function claudeSessionRows(state, archivedSessionIds = []) {
14215
- const rows = state.ids === void 0 ? Object.values(state.byId) : state.ids.map((id) => state.byId[id]);
14216
- const archived = new Set(archivedSessionIds);
14217
- 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));
14218
- }
14219
- function Badge({ label, tone = "neutral" }) {
14220
- const toneStyle = tone === "success" ? repositoryItemSuccess : tone === "warning" ? repositoryItemWarning : tone === "error" ? repositoryItemError : tone === "merged" ? { color: "#a78bfa" } : {};
14221
- return /* @__PURE__ */ jsxs("span", {
14222
- style: {
14223
- ...repositoryItem,
14224
- ...toneStyle
14225
- },
14226
- children: [/* @__PURE__ */ jsx("span", {
14227
- style: repositoryItemDot,
14228
- "aria-hidden": "true"
14229
- }), /* @__PURE__ */ jsx("span", {
14230
- style: repositoryItemLabel,
14231
- children: label
14232
- })]
14233
- });
14234
- }
14235
- function repositoryName(remote) {
14236
- return remote?.split("/").at(-1);
14237
- }
14238
- function OverviewAttention({ source, running, t }) {
14239
- const snapshot = useSyncExternalStore(source.subscribe, source.getSnapshot, source.getSnapshot);
14240
- const attention = running ? overviewAttention(snapshot.activities) : void 0;
14241
- const usage = snapshot.contextUsage;
14242
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [
14243
- attention === "permission" ? /* @__PURE__ */ jsx(Badge, {
14244
- label: t("overviewNeedsPermission"),
14245
- tone: "warning"
14246
- }) : null,
14247
- attention === "question" ? /* @__PURE__ */ jsx(Badge, {
14248
- label: t("overviewNeedsAnswer"),
14249
- tone: "warning"
14250
- }) : null,
14251
- usage === void 0 ? null : /* @__PURE__ */ jsx("span", { children: t("overviewContextUsage", { percentage: usage.percentage }) })
14252
- ] });
14253
- }
14254
- function ClaudePullRequestsPanel({ t, closeDetails, openSession, loadStatus, sessions, workspaces, projectionFor }) {
14255
- const sessionStore = useMemo(() => ({
14256
- subscribe: (listener) => sessions.subscribe(listener),
14257
- getSnapshot: () => sessions.getSnapshot()
14258
- }), [sessions]);
14259
- const snapshot = useSyncExternalStore(sessionStore.subscribe, sessionStore.getSnapshot, sessionStore.getSnapshot);
14260
- const workspaceStore = useMemo(() => {
14261
- const source = workspaces ?? NO_WORKSPACES;
14262
- return {
14263
- subscribe: (listener) => source.subscribe(listener),
14264
- getSnapshot: () => source.getSnapshot()
14265
- };
14266
- }, [workspaces]);
14267
- const workspaceState = useSyncExternalStore(workspaceStore.subscribe, workspaceStore.getSnapshot, workspaceStore.getSnapshot);
14268
- const rows = useMemo(() => claudeSessionRows(snapshot, workspaceState.archivedSessionIds ?? []), [snapshot, workspaceState]);
14269
- const cwdKey = useMemo(() => [...new Set(rows.map((row) => row.cwd ?? ""))].sort().join("\0"), [rows]);
14270
- const [statuses, setStatuses] = useState({});
14271
- useEffect(() => {
14272
- const cwds = cwdKey.length === 0 ? [] : cwdKey.split("\0");
14273
- if (cwds.length === 0) return;
14274
- const controller = new AbortController();
14275
- const refresh = () => {
14276
- for (const cwd of cwds) loadStatus(cwd, controller.signal).then((status) => {
14277
- if (!controller.signal.aborted) setStatuses((previous) => ({
14278
- ...previous,
14279
- [cwd]: status
14280
- }));
14281
- }, () => void 0);
14282
- };
14283
- refresh();
14284
- const timer = setInterval(refresh, OVERVIEW_REFRESH_MS);
14285
- return () => {
14286
- controller.abort();
14287
- clearInterval(timer);
14288
- };
14289
- }, [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
+ });
14290
15110
  return /* @__PURE__ */ jsxs("div", {
14291
- className: detailsCardClass,
14292
- style: tasksPanel,
14293
- children: [
14294
- /* @__PURE__ */ jsxs("style", {
14295
- "data-dsh-claude-overview-styles": true,
14296
- children: [detailsCardCss, panelIconButtonCss]
14297
- }),
14298
- /* @__PURE__ */ jsxs("header", {
14299
- style: tasksHeader,
14300
- children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
14301
- style: tasksHeading,
14302
- children: t("overviewTitle")
14303
- }), /* @__PURE__ */ jsx("span", {
14304
- style: tasksTurnMeta,
14305
- children: t("overviewBody")
14306
- })] }), /* @__PURE__ */ jsx("button", {
14307
- type: "button",
14308
- className: panelIconButtonClass,
14309
- "aria-label": t("diffClose"),
14310
- onClick: closeDetails,
14311
- children: /* @__PURE__ */ jsx(IconCloseOutline16, {})
14312
- })]
14313
- }),
14314
- /* @__PURE__ */ jsx("div", {
14315
- style: overviewBody,
14316
- children: rows.length === 0 ? /* @__PURE__ */ jsx("p", {
14317
- style: overviewEmpty,
14318
- children: t("overviewEmpty")
14319
- }) : rows.map((row) => {
14320
- const repository = row.cwd === void 0 ? void 0 : statuses[row.cwd];
14321
- const pullRequest = repository?.pullRequest;
14322
- const branch = repository?.status === "ready" ? repository.detached === true ? t("repositoryDetached") : repository.branch ?? t("repositoryUnknownBranch") : repository === void 0 ? t("overviewLoading") : t("repositoryUnavailable");
14323
- return /* @__PURE__ */ jsxs("button", {
14324
- type: "button",
14325
- style: overviewRow,
14326
- onClick: () => {
14327
- openSession(row.id);
14328
- },
14329
- children: [/* @__PURE__ */ jsxs("span", {
14330
- style: overviewRowTop,
14331
- children: [
14332
- row.running === true ? /* @__PURE__ */ jsx("span", {
14333
- style: overviewRunningDot,
14334
- "aria-label": t("overviewRunning")
14335
- }) : null,
14336
- /* @__PURE__ */ jsx("span", {
14337
- style: overviewTitle,
14338
- children: row.displayTitle ?? row.id
14339
- }),
14340
- pullRequest === void 0 ? /* @__PURE__ */ jsx(Badge, { label: t("overviewNoPr") }) : /* @__PURE__ */ jsx(Badge, {
14341
- label: `#${pullRequest.number} · ${t(`repositoryState_${pullRequest.state}`)}`,
14342
- tone: pullRequest.state === "merged" ? "merged" : pullRequest.state === "open" ? "success" : "neutral"
14343
- })
14344
- ]
14345
- }), /* @__PURE__ */ jsxs("span", {
14346
- style: overviewMeta,
14347
- children: [
14348
- repositoryName(repository?.remote) === void 0 ? null : /* @__PURE__ */ jsx("span", { children: repositoryName(repository?.remote) }),
14349
- /* @__PURE__ */ jsx("span", {
14350
- style: overviewBranch,
14351
- children: branch
14352
- }),
14353
- pullRequest?.state === "open" && pullRequest.checks !== "none" ? /* @__PURE__ */ jsx(Badge, {
14354
- label: t(`repositoryChecks_${pullRequest.checks}`),
14355
- tone: pullRequest.checks === "passing" ? "success" : pullRequest.checks === "failing" ? "error" : "warning"
14356
- }) : null,
14357
- pullRequest?.state === "open" && pullRequest.review !== "none" ? /* @__PURE__ */ jsx(Badge, {
14358
- label: t(`repositoryReview_${pullRequest.review}`),
14359
- tone: pullRequest.review === "approved" ? "success" : pullRequest.review === "changes-requested" ? "error" : "neutral"
14360
- }) : null,
14361
- autoFixEnabled(row.id) ? /* @__PURE__ */ jsx(Badge, {
14362
- label: t("overviewAutoFix"),
14363
- tone: "success"
14364
- }) : null,
14365
- projectionFor === void 0 ? null : /* @__PURE__ */ jsx(OverviewAttention, {
14366
- source: projectionFor(row.id),
14367
- running: row.running === true,
14368
- t
14369
- })
14370
- ]
14371
- })]
14372
- }, row.id);
14373
- })
14374
- })
14375
- ]
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
+ })]
14376
15205
  });
14377
15206
  }
14378
15207
  //#endregion
@@ -15112,13 +15941,19 @@ window.__ModuleLoader__.load({
15112
15941
  //#endregion
15113
15942
  //#region src/client/rewind-api.ts
15114
15943
  /** Drop one user message and everything after it: the rows are hidden from
15115
- * this session's transcript and Claude resumes before that turn. */
15116
- 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) {
15117
15951
  try {
15118
- await pluginWrite(CLAUDE_REWIND_PATH, "fast", void 0, { json: {
15952
+ return { filesRestored: (await pluginWrite(CLAUDE_REWIND_PATH, "fast", void 0, { json: {
15119
15953
  sessionId,
15120
- seq
15121
- } });
15954
+ seq,
15955
+ restoreFiles
15956
+ } }))?.filesRestored === true };
15122
15957
  } catch (error) {
15123
15958
  if (!(error instanceof PluginRequestError)) throw error;
15124
15959
  throw new Error(error.code ?? error.reason);
@@ -15187,6 +16022,8 @@ window.__ModuleLoader__.load({
15187
16022
  const [target, setTarget] = useState();
15188
16023
  const [submitting, setSubmitting] = useState(false);
15189
16024
  const [error, setError] = useState();
16025
+ const [restoreFiles, setRestoreFiles] = useState(true);
16026
+ const { toast, report } = useActionToast();
15190
16027
  const ranges = projection.rewind?.ranges ?? EMPTY_RANGES;
15191
16028
  const owned = projection.owned;
15192
16029
  const unavailable = snapshot.running;
@@ -15254,6 +16091,7 @@ window.__ModuleLoader__.load({
15254
16091
  setTarget(void 0);
15255
16092
  setSubmitting(false);
15256
16093
  setError(void 0);
16094
+ setRestoreFiles(true);
15257
16095
  }, [sessionId]);
15258
16096
  if (sessionId === void 0 || !owned) return /* @__PURE__ */ jsx("span", {
15259
16097
  "data-dsh-claude-rewind-armed": "armed",
@@ -15268,10 +16106,11 @@ window.__ModuleLoader__.load({
15268
16106
  if (target === void 0 || submitting) return;
15269
16107
  setSubmitting(true);
15270
16108
  setError(void 0);
15271
- rewindSession(sessionId, target.seq).then(() => {
16109
+ rewindSession(sessionId, target.seq, restoreFiles).then(({ filesRestored }) => {
15272
16110
  setSubmitting(false);
15273
16111
  setTarget(void 0);
15274
16112
  if (target.text !== "") setDraft?.(sessionId, target.text);
16113
+ if (restoreFiles && !filesRestored) report(t("rewindFilesUnavailable"));
15275
16114
  }, (reason) => {
15276
16115
  setSubmitting(false);
15277
16116
  const code = reason instanceof Error && reason.message !== "" ? reason.message : "unknown";
@@ -15279,6 +16118,7 @@ window.__ModuleLoader__.load({
15279
16118
  });
15280
16119
  };
15281
16120
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [
16121
+ toast,
15282
16122
  /* @__PURE__ */ jsx("style", {
15283
16123
  "data-dsh-claude-rewind-styles": true,
15284
16124
  children: `${rewindActionCss}${rewindHiddenCss(hiddenKeys)}`
@@ -15343,9 +16183,20 @@ window.__ModuleLoader__.load({
15343
16183
  style: rewindModalMessage,
15344
16184
  children: target.text.slice(0, 2e3)
15345
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
+ }),
15346
16197
  /* @__PURE__ */ jsx("p", {
15347
16198
  style: diffModalStatus,
15348
- children: t("rewindHint")
16199
+ children: restoreFiles ? t("rewindRestoreFilesHint") : t("rewindHint")
15349
16200
  }),
15350
16201
  error === void 0 ? null : /* @__PURE__ */ jsx("p", {
15351
16202
  style: diffModalError,
@@ -16674,19 +17525,19 @@ window.__ModuleLoader__.load({
16674
17525
  });
16675
17526
  }
16676
17527
  //#endregion
16677
- //#region src/client/diff-open-store.ts
16678
- var DiffOpenStore = class {
17528
+ //#region src/client/panel-open-store.ts
17529
+ var PanelOpenStore = class {
16679
17530
  #sessionId;
16680
17531
  #listeners = /* @__PURE__ */ new Set();
16681
- /** Mark the diff panel open for one session, replacing any previous holder. */
17532
+ /** Mark this panel open for one session, replacing any previous holder. */
16682
17533
  open(sessionId) {
16683
17534
  this.#set(sessionId);
16684
17535
  }
16685
- /** Mark the diff panel closed. */
17536
+ /** Mark this panel closed. */
16686
17537
  close() {
16687
17538
  this.#set(void 0);
16688
17539
  }
16689
- /** Whether the diff panel is currently open for this session. */
17540
+ /** Whether this panel is currently open for this session. */
16690
17541
  isOpen(sessionId) {
16691
17542
  return this.#sessionId === sessionId;
16692
17543
  }
@@ -17222,6 +18073,27 @@ window.__ModuleLoader__.load({
17222
18073
  yes: "是",
17223
18074
  no: "否",
17224
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 的审批弹框里批准或拒绝这个方案。",
17225
18097
  diffClose: "关闭 Diff 面板",
17226
18098
  sessionMenu: "会话菜单",
17227
18099
  sessionMenuOpenIn: "打开方式",
@@ -17403,6 +18275,17 @@ window.__ModuleLoader__.load({
17403
18275
  rewindFailed: "回退失败({code})。",
17404
18276
  rewindBusy: "这个会话正在运行,请等本轮结束后再回退。",
17405
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: "对话已回退,但文件没有还原:这一轮没有留下工作区快照。",
17406
18289
  turnUsage: "本回合用量",
17407
18290
  turnUsageTokens: "{count} tok",
17408
18291
  turnUsageCache: "缓存命中 {percent}%",
@@ -17600,6 +18483,27 @@ window.__ModuleLoader__.load({
17600
18483
  yes: "Yes",
17601
18484
  no: "No",
17602
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.",
17603
18507
  diffClose: "Close diff panel",
17604
18508
  sessionMenu: "Session menu",
17605
18509
  sessionMenuOpenIn: "Open in",
@@ -17781,6 +18685,17 @@ window.__ModuleLoader__.load({
17781
18685
  rewindFailed: "The rewind failed ({code}).",
17782
18686
  rewindBusy: "This session is running; wait for the turn to finish before rewinding.",
17783
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.",
17784
18699
  turnUsage: "Turn usage",
17785
18700
  turnUsageTokens: "{count} tok",
17786
18701
  turnUsageCache: "Cache hit {percent}%",
@@ -17792,7 +18707,7 @@ window.__ModuleLoader__.load({
17792
18707
  function MaximizedDiff({ source, t, sessionId, closeDetails, restore, submitPrompt }) {
17793
18708
  const snapshot = useSyncExternalStore(source.subscribe, source.getSnapshot, source.getSnapshot);
17794
18709
  const useClaudeProjection = (selector) => selector(snapshot);
17795
- return /* @__PURE__ */ jsx(ClaudeDiffOverlay, {
18710
+ return /* @__PURE__ */ jsx(ClaudePanelOverlay, {
17796
18711
  onRestore: restore,
17797
18712
  children: /* @__PURE__ */ jsx(ClaudeDiffPanel, {
17798
18713
  useClaudeProjection,
@@ -17806,6 +18721,21 @@ window.__ModuleLoader__.load({
17806
18721
  });
17807
18722
  }
17808
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
+ }
17809
18739
  const inject = [
17810
18740
  "slots",
17811
18741
  "locale",
@@ -17847,7 +18777,9 @@ window.__ModuleLoader__.load({
17847
18777
  const t = ctx.locale.bind(namespace);
17848
18778
  ctx.effect(() => restyleHostChrome(), "dsh-claude: Host chrome restyling");
17849
18779
  pluginRead(CLAUDE_GLOBAL_SETTINGS_PATH, "fast").then((payload) => {
17850
- if (isGlobalSettingsView(payload)) applyClaudeMarkdownTheme(proseModeOf(payload.settings));
18780
+ if (!isGlobalSettingsView(payload)) return;
18781
+ applyClaudeMarkdownTheme(proseModeOf(payload.settings));
18782
+ setClaudeAlertsEnabled(alertModeOf(payload.settings) === "on");
17851
18783
  }).catch(() => {});
17852
18784
  const projections = new ClaudeProjectionStore({ report: (kind, detail) => {
17853
18785
  diagnostics.report(kind, detail);
@@ -17892,6 +18824,17 @@ window.__ModuleLoader__.load({
17892
18824
  resolve: (binding) => ({ hooks: { claudeProjection: projections.source(binding.sessionId) } })
17893
18825
  }), "dsh-claude: sidecar projection provider");
17894
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");
17895
18838
  const uiConversation = ctx.get("uiConversation");
17896
18839
  if (uiConversation !== void 0) {
17897
18840
  ctx.effect(() => uiConversation.events.register(claudeTurnDefinition), "dsh-claude: Claude turn marker");
@@ -17906,9 +18849,11 @@ window.__ModuleLoader__.load({
17906
18849
  const layout = ctx.get("layout");
17907
18850
  let disposePluginDetails;
17908
18851
  let disposeDiffOverlay;
18852
+ let disposePlanOverlay;
17909
18853
  let disposeExpandedDetailsResize;
17910
18854
  let detailsSessionId;
17911
- const diffOpen = new DiffOpenStore();
18855
+ const diffOpen = new PanelOpenStore();
18856
+ const planOpen = new PanelOpenStore();
17912
18857
  const restoreDiff = () => {
17913
18858
  if (disposeDiffOverlay === void 0) return;
17914
18859
  disposeDiffOverlay();
@@ -17916,16 +18861,26 @@ window.__ModuleLoader__.load({
17916
18861
  layout?.openDetails();
17917
18862
  disposeExpandedDetailsResize = enableExpandedDetailsResize();
17918
18863
  };
18864
+ const restorePlan = () => {
18865
+ if (disposePlanOverlay === void 0) return;
18866
+ disposePlanOverlay();
18867
+ disposePlanOverlay = void 0;
18868
+ layout?.openDetails();
18869
+ disposeExpandedDetailsResize = enableExpandedDetailsResize();
18870
+ };
17919
18871
  const closePluginDetails = () => {
17920
- 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;
17921
18873
  disposeDiffOverlay?.();
17922
18874
  disposeDiffOverlay = void 0;
18875
+ disposePlanOverlay?.();
18876
+ disposePlanOverlay = void 0;
17923
18877
  disposeExpandedDetailsResize?.();
17924
18878
  disposeExpandedDetailsResize = void 0;
17925
18879
  disposePluginDetails?.();
17926
18880
  disposePluginDetails = void 0;
17927
18881
  detailsSessionId = void 0;
17928
18882
  diffOpen.close();
18883
+ planOpen.close();
17929
18884
  layout?.closeDetails();
17930
18885
  };
17931
18886
  ctx.effect(() => ctx.slots.onEntryError((key, entry, error) => {
@@ -17934,6 +18889,7 @@ window.__ModuleLoader__.load({
17934
18889
  ${error.stack ?? ""}` : String(error);
17935
18890
  diagnostics.report("slot-entry-crashed", `slot "${key}"${id}: ${message}`);
17936
18891
  if (key === "shell.overlay" && entry.options.id === "claude-diff-overlay") restoreDiff();
18892
+ if (key === "shell.overlay" && entry.options.id === "claude-plan-overlay") restorePlan();
17937
18893
  }), "dsh-claude: Slot entry failure reporting");
17938
18894
  const openTasksPanel = (sessionId, turn) => {
17939
18895
  closePluginDetails();
@@ -17955,6 +18911,55 @@ window.__ModuleLoader__.load({
17955
18911
  layout?.openDetails();
17956
18912
  disposeExpandedDetailsResize = enableExpandedDetailsResize();
17957
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
+ };
17958
18963
  const openOverviewPanel = (sessionId) => {
17959
18964
  if (sessions === void 0) return;
17960
18965
  closePluginDetails();
@@ -18045,7 +19050,7 @@ window.__ModuleLoader__.load({
18045
19050
  ctx.effect(() => {
18046
19051
  if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => closePluginDetails();
18047
19052
  const observer = new MutationObserver(() => {
18048
- 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();
18049
19054
  });
18050
19055
  observer.observe(document.body, {
18051
19056
  attributes: true,
@@ -18091,6 +19096,20 @@ window.__ModuleLoader__.load({
18091
19096
  })
18092
19097
  }, ClaudeAgentPresetLabel));
18093
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));
18094
19113
  ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
18095
19114
  name: "conversation.session.header.utilities",
18096
19115
  id: "claude-diff",