@norman-else/dsh-claude 0.1.40 → 0.1.42

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
@@ -4,7 +4,7 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  module.exports;
6
6
  var { Fragment, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } = require("react");
7
- var { DiffBlock, DisclosureRow, HoverCard, IconAgentPresetOutline16, IconApiOutline14, IconBranchOutline16, IconCheckOutline14, IconCheckOutline16, IconChevronDownOutline14, IconChevronRightOutline14, IconChevronUpOutline14, IconCloseOutline16, IconEditOutline16, IconEllipsisOutline16, IconFullscreenOutline16, IconQueueOutline14, IconRefreshOutline14, IconRightUpOutline14, IconSearchOutline16, IconSendOutline14, IconThinkOutline14, IconTrashOutline16, MarkdownText, Menu, Modal, StateDot, Toast, Tooltip, useDismissOnOutsidePointer } = require("@deepseek-ai/dsh-client-ui-primitives");
7
+ var { Button, DiffBlock, DisclosureRow, HoverCard, IconAgentPresetOutline16, IconApiOutline14, IconBranchOutline16, IconCheckOutline14, IconCheckOutline16, IconChevronDownOutline14, IconChevronRightOutline14, IconChevronUpOutline14, IconCloseOutline16, IconEditOutline16, IconEllipsisOutline16, IconFullscreenOutline16, IconListPenOutline16, IconLoadingOutline16, IconQueueOutline14, IconRefreshOutline14, IconRefreshOutline16, IconRightUpOutline14, IconSearchOutline16, IconSendOutline14, IconSparkle16, IconThinkOutline14, IconTrashOutline16, MarkdownText, Menu, Modal, StateDot, Toast, Tooltip, useDismissOnOutsidePointer } = require("@deepseek-ai/dsh-client-ui-primitives");
8
8
  var { Fragment: Fragment$1, jsx, jsxs } = require("react/jsx-runtime");
9
9
  var { createPortal } = require("react-dom");
10
10
  /** Claude's subagent dispatch tools; rendered as plugin-owned group cards
@@ -27,6 +27,10 @@ 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";
31
+ const CLAUDE_PROMPTS_PATH = "/plugins/dsh-claude/prompts";
32
+ const CLAUDE_PROMPT_NAME_PATH = "/plugins/dsh-claude/prompts/name";
33
+ const CLAUDE_PROMPT_REFINE_PATH = "/plugins/dsh-claude/prompts/refine";
30
34
  function isClaudeRenderMode(value) {
31
35
  return value === "plugin" || value === "native";
32
36
  }
@@ -34,6 +38,9 @@ window.__ModuleLoader__.load({
34
38
  function isClaudeProseMode(value) {
35
39
  return value === "plain" || value === "enhanced";
36
40
  }
41
+ function isClaudeAlertMode(value) {
42
+ return value === "off" || value === "on";
43
+ }
37
44
  //#endregion
38
45
  //#region src/client/task-projection.ts
39
46
  /** Tasks UI is reserved for detached work and genuine Claude subagents. */
@@ -268,6 +275,10 @@ window.__ModuleLoader__.load({
268
275
  failedAction = description === void 0 ? `run ${command}` : description;
269
276
  break;
270
277
  }
278
+ case "ExitPlanMode":
279
+ completed = "Proposed a plan";
280
+ failedAction = "propose a plan";
281
+ break;
271
282
  default:
272
283
  completed = description ?? `${toolName}${target === void 0 ? "" : ` ${target}`}`;
273
284
  failedAction = completed.charAt(0).toLowerCase() + completed.slice(1);
@@ -1096,6 +1107,163 @@ window.__ModuleLoader__.load({
1096
1107
  fontSize: 12,
1097
1108
  lineHeight: "18px"
1098
1109
  };
1110
+ /** Title and state chip: one phrase at the header's left end. */
1111
+ const planHeaderStart = {
1112
+ display: "flex",
1113
+ alignItems: "center",
1114
+ gap: 8,
1115
+ minWidth: 0
1116
+ };
1117
+ /** "2 / 5" next to the title when the session has proposed more than one. */
1118
+ const planCount = {
1119
+ flex: "none",
1120
+ color: "var(--dsw-alias-label-tertiary)",
1121
+ fontSize: 12,
1122
+ lineHeight: "17px",
1123
+ fontVariantNumeric: "tabular-nums"
1124
+ };
1125
+ /** The review composer: notes filed so far, the pending quote, and the box.
1126
+ * Docked below the plan body so the plan itself keeps the scrolling room. */
1127
+ const planComposer = {
1128
+ flex: "none",
1129
+ display: "flex",
1130
+ flexDirection: "column",
1131
+ gap: 6,
1132
+ maxHeight: "45%",
1133
+ overflowY: "auto",
1134
+ padding: "10px 14px 12px",
1135
+ borderTop: "1px solid var(--dsw-alias-border-l2, color-mix(in srgb, currentColor 16%, transparent))",
1136
+ background: "var(--dsw-alias-bg-layer-1)"
1137
+ };
1138
+ const planNoteList = {
1139
+ display: "flex",
1140
+ flexDirection: "column",
1141
+ gap: 6,
1142
+ margin: 0,
1143
+ padding: 0,
1144
+ listStyle: "none"
1145
+ };
1146
+ const planNote = {
1147
+ position: "relative",
1148
+ padding: "6px 26px 6px 8px",
1149
+ borderRadius: 7,
1150
+ background: "var(--dsw-alias-bg-base)"
1151
+ };
1152
+ /** The quoted passage, marked as someone else's words by the rule down its
1153
+ * left edge rather than by quotation marks it may already contain. */
1154
+ const planNoteQuote = {
1155
+ display: "-webkit-box",
1156
+ WebkitLineClamp: 3,
1157
+ WebkitBoxOrient: "vertical",
1158
+ overflow: "hidden",
1159
+ margin: 0,
1160
+ paddingLeft: 8,
1161
+ borderLeft: "2px solid var(--dsw-alias-border-l2, color-mix(in srgb, currentColor 24%, transparent))",
1162
+ color: "var(--dsw-alias-label-tertiary)",
1163
+ fontSize: 11,
1164
+ lineHeight: "16px",
1165
+ whiteSpace: "pre-wrap"
1166
+ };
1167
+ const planNoteText = {
1168
+ margin: "4px 0 0",
1169
+ color: "var(--dsw-alias-label-primary)",
1170
+ fontSize: 12,
1171
+ lineHeight: "17px",
1172
+ whiteSpace: "pre-wrap"
1173
+ };
1174
+ const planNoteRemove = {
1175
+ position: "absolute",
1176
+ top: 4,
1177
+ right: 4,
1178
+ width: 18,
1179
+ height: 18,
1180
+ display: "grid",
1181
+ placeItems: "center",
1182
+ padding: 0,
1183
+ border: 0,
1184
+ borderRadius: 5,
1185
+ background: "transparent",
1186
+ color: "var(--dsw-alias-label-tertiary)",
1187
+ fontSize: 14,
1188
+ lineHeight: 1,
1189
+ cursor: "pointer"
1190
+ };
1191
+ /** The live selection, shown attached to the box it will be filed with. */
1192
+ const planQuoteChip = {
1193
+ position: "relative",
1194
+ padding: "6px 26px 6px 8px",
1195
+ borderRadius: 7,
1196
+ background: "var(--dsw-alias-interactive-bg-hover)"
1197
+ };
1198
+ const planComposerInput = {
1199
+ boxSizing: "border-box",
1200
+ width: "100%",
1201
+ minHeight: 56,
1202
+ padding: "7px 9px",
1203
+ border: "1px solid var(--dsw-alias-border-l2, color-mix(in srgb, currentColor 16%, transparent))",
1204
+ borderRadius: 8,
1205
+ background: "var(--dsw-alias-bg-base)",
1206
+ color: "var(--dsw-alias-label-primary)",
1207
+ fontFamily: "inherit",
1208
+ fontSize: 12,
1209
+ lineHeight: "18px",
1210
+ resize: "vertical"
1211
+ };
1212
+ const planComposerActions = {
1213
+ display: "flex",
1214
+ alignItems: "center",
1215
+ justifyContent: "space-between",
1216
+ gap: 8
1217
+ };
1218
+ const planComposerHint = {
1219
+ minWidth: 0,
1220
+ color: "var(--dsw-alias-label-tertiary)",
1221
+ fontSize: 11,
1222
+ lineHeight: "16px"
1223
+ };
1224
+ const planComposerError = {
1225
+ margin: 0,
1226
+ color: "var(--dsw-alias-state-error-primary)",
1227
+ fontSize: 11,
1228
+ lineHeight: "16px"
1229
+ };
1230
+ /** Maximize and close: the control group at the header's right end. */
1231
+ const planHeaderEnd = {
1232
+ flex: "none",
1233
+ display: "flex",
1234
+ alignItems: "center",
1235
+ gap: 2
1236
+ };
1237
+ const planBadge = {
1238
+ flex: "none",
1239
+ padding: "2px 8px",
1240
+ borderRadius: 999,
1241
+ background: "color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent)",
1242
+ color: "var(--dsw-alias-state-success-primary)",
1243
+ fontSize: 11,
1244
+ lineHeight: "16px",
1245
+ fontWeight: 600,
1246
+ whiteSpace: "nowrap"
1247
+ };
1248
+ const planBadgePending = {
1249
+ background: "var(--dsw-alias-interactive-bg-hover)",
1250
+ color: "var(--dsw-alias-label-secondary)"
1251
+ };
1252
+ const planBadgeRejected = {
1253
+ background: "color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent)",
1254
+ color: "var(--dsw-alias-state-error-primary)"
1255
+ };
1256
+ /** Says where the decision is made, since this panel deliberately does not
1257
+ * make it: the Host's approval dialog owns the buttons. */
1258
+ const planHint = {
1259
+ margin: "0 0 12px",
1260
+ padding: "8px 10px",
1261
+ borderRadius: 8,
1262
+ background: "var(--dsw-alias-interactive-bg-hover)",
1263
+ color: "var(--dsw-alias-label-secondary)",
1264
+ fontSize: 12,
1265
+ lineHeight: "18px"
1266
+ };
1099
1267
  const tasksFinishedSection = {
1100
1268
  marginTop: 12,
1101
1269
  paddingTop: 8,
@@ -1669,7 +1837,7 @@ window.__ModuleLoader__.load({
1669
1837
  whiteSpace: "nowrap"
1670
1838
  };
1671
1839
  const repositoryBarFrame = {
1672
- width: "calc(100% - 64px)",
1840
+ width: "calc(100% - 2 * var(--dsh-composer-side-clearance, 16px))",
1673
1841
  maxWidth: "var(--dsh-composer-card-max-width, 782px)",
1674
1842
  minWidth: 0,
1675
1843
  margin: "0 auto",
@@ -2829,7 +2997,7 @@ window.__ModuleLoader__.load({
2829
2997
  fontSize: 11
2830
2998
  };
2831
2999
  const reviewCommentBarFrame = {
2832
- width: "calc(100% - 64px)",
3000
+ width: "calc(100% - 2 * var(--dsh-composer-side-clearance, 16px))",
2833
3001
  maxWidth: "var(--dsh-composer-card-max-width, 782px)",
2834
3002
  margin: "0 auto",
2835
3003
  boxSizing: "border-box"
@@ -2920,6 +3088,113 @@ window.__ModuleLoader__.load({
2920
3088
  whiteSpace: "pre-wrap",
2921
3089
  overflowWrap: "anywhere"
2922
3090
  };
3091
+ const promptSaveTriggerClass = "dshClaudePromptSaveTrigger";
3092
+ const promptSpinClass = "dshClaudePromptSpin";
3093
+ const promptUndoClass = "dshClaudePromptUndo";
3094
+ const promptSaveFieldClass = "dshClaudePromptSaveField";
3095
+ /** The composer's own attach button geometry (InputBar.module.css `.add`:
3096
+ * 28x28 circle on --dsw-specific-selector, 14px glyph). The primitives'
3097
+ * Button has no icon-only form — its `sm` is a 36px-wide capsule around a
3098
+ * 16px slot, which is why this reads oversized beside its neighbours. */
3099
+ const promptSaveTriggerCss = `
3100
+ .${promptSaveTriggerClass} {
3101
+ width: 28px;
3102
+ height: 28px;
3103
+ flex: none;
3104
+ display: grid;
3105
+ place-items: center;
3106
+ padding: 0;
3107
+ border: none;
3108
+ border-radius: 999px;
3109
+ background: var(--dsw-specific-selector);
3110
+ color: var(--dsw-alias-label-primary);
3111
+ cursor: pointer;
3112
+ transition: background-color 120ms ease, color 120ms ease;
3113
+ }
3114
+ /* With no draft there is nothing to keep, so the control drops its seat
3115
+ * entirely rather than sitting there as a filled-but-dead circle. */
3116
+ .${promptSaveTriggerClass}:disabled {
3117
+ cursor: default;
3118
+ background: transparent;
3119
+ color: var(--dsw-alias-label-tertiary);
3120
+ }
3121
+ .${promptSaveTriggerClass}:focus-visible { outline: none; }
3122
+ .${promptSaveFieldClass} {
3123
+ box-sizing: border-box;
3124
+ width: 100%;
3125
+ height: 30px;
3126
+ padding: 0 9px;
3127
+ border: 1px solid var(--dsw-alias-border-inverted);
3128
+ border-radius: 7px;
3129
+ background: var(--dsw-alias-interactive-bg-hover);
3130
+ color: var(--dsw-alias-label-primary);
3131
+ font-family: var(--dsw-font-family);
3132
+ font-size: 13px;
3133
+ line-height: 20px;
3134
+ }
3135
+ /* Not --dsw-alias-brand-primary: in the dark theme that alias resolves to
3136
+ * neutral-bluish-50, so a focused field draws a near-white slab around
3137
+ * itself. The focus ring is a step up in the same greys the card is made of. */
3138
+ .${promptSaveFieldClass}:focus-visible {
3139
+ outline: none;
3140
+ border-color: var(--dsw-alias-label-tertiary);
3141
+ background: var(--dsw-alias-interactive-bg-active);
3142
+ }
3143
+ .${promptSaveFieldClass}:disabled { opacity: 0.6; }
3144
+ /* ic_ds_loading_outline_16 is a bare open ring and nothing in the primitives
3145
+ * turns it, so a four-second rewrite sat behind a frozen glyph that reads as
3146
+ * the letter C. The spin is what makes it a spinner. */
3147
+ .${promptSpinClass} { animation: dshClaudePromptSpin 900ms linear infinite; }
3148
+ @keyframes dshClaudePromptSpin { to { transform: rotate(360deg); } }
3149
+ /* The icon set has no undo arrow, and ic_ds_refresh_outline_16 turns
3150
+ * clockwise — the redo direction. Mirrored, it is the ordinary undo glyph. */
3151
+ .${promptUndoClass} { transform: scaleX(-1); }
3152
+ @media (prefers-reduced-motion: reduce) {
3153
+ .${promptSpinClass} { animation: none; }
3154
+ }
3155
+ `;
3156
+ /** The naming card hangs off the composer tool row, portaled so the composer
3157
+ * card cannot clip it and positioned by useAnchoredPosition. */
3158
+ const promptSaveCard = {
3159
+ position: "fixed",
3160
+ zIndex: 120,
3161
+ boxSizing: "border-box",
3162
+ display: "flex",
3163
+ flexDirection: "column",
3164
+ gap: 8,
3165
+ width: 340,
3166
+ maxWidth: "calc(100vw - 32px)",
3167
+ padding: "10px 12px",
3168
+ border: "1px solid var(--dsw-alias-border-inverted)",
3169
+ borderRadius: 11,
3170
+ background: "var(--dsw-specific-menu)",
3171
+ boxShadow: "var(--dsw-shadow-lv3)",
3172
+ color: "var(--dsw-alias-label-primary)",
3173
+ fontSize: 12,
3174
+ lineHeight: "20px"
3175
+ };
3176
+ const promptSaveHeading = {
3177
+ color: "var(--dsw-alias-label-tertiary)",
3178
+ fontSize: 11,
3179
+ lineHeight: "16px"
3180
+ };
3181
+ const promptSaveActions = {
3182
+ display: "flex",
3183
+ justifyContent: "flex-end",
3184
+ gap: 6
3185
+ };
3186
+ /** The saved file, wrapped rather than truncated: a path the user cannot read
3187
+ * in full does not tell them where to go and edit it. */
3188
+ const promptSaveLocation = {
3189
+ color: "var(--dsw-alias-label-tertiary)",
3190
+ fontSize: 11,
3191
+ lineHeight: "16px",
3192
+ overflowWrap: "anywhere"
3193
+ };
3194
+ const promptSaveError = {
3195
+ color: "var(--dsw-static-red-450, #d64545)",
3196
+ overflowWrap: "anywhere"
3197
+ };
2923
3198
  const diffAhead = { color: "var(--dsw-static-blue-450)" };
2924
3199
  const diffAheadMuted = { color: "color-mix(in srgb, var(--dsw-static-blue-450) 62%, var(--dsw-alias-label-tertiary))" };
2925
3200
  const diffAddMuted = { color: "color-mix(in srgb, var(--dsw-alias-state-success-primary) 62%, var(--dsw-alias-label-tertiary))" };
@@ -3843,6 +4118,16 @@ window.__ModuleLoader__.load({
3843
4118
  laneHeld[lane] -= 1;
3844
4119
  pump();
3845
4120
  }
4121
+ /** Runs at most once, so a release can be wired to every ending of a stream
4122
+ * without any of them having to know about the others. */
4123
+ function once(action) {
4124
+ let done = false;
4125
+ return () => {
4126
+ if (done) return;
4127
+ done = true;
4128
+ action();
4129
+ };
4130
+ }
3846
4131
  function acquire(lane) {
3847
4132
  let released = false;
3848
4133
  const releaseOnce = () => {
@@ -3946,11 +4231,53 @@ window.__ModuleLoader__.load({
3946
4231
  writeId += 1;
3947
4232
  return writeId;
3948
4233
  }
4234
+ /**
4235
+ * A reader that gives its permit back however the stream ends.
4236
+ *
4237
+ * The permit used to be released by one event only -- the caller's abort --
4238
+ * and a stream has three endings, two of which the caller does not cause: the
4239
+ * server closing the response, and the body failing mid-read. Either one left
4240
+ * the permit held. For the reserved carrier that permit is a single boolean,
4241
+ * so one such ending closed the transcript stream for the life of the page and
4242
+ * every reopen answered `starved`, silently, forever.
4243
+ *
4244
+ * Binding the release to the reader rather than to the signal is what makes
4245
+ * that unrepresentable: a caller cannot hold the permit past the stream it is
4246
+ * reading, because every way of leaving the read passes through here. `free`
4247
+ * is idempotent, so the abort listener below stays as the belt to this brace.
4248
+ */
4249
+ function releasingReader(reader, free) {
4250
+ return {
4251
+ get closed() {
4252
+ return reader.closed;
4253
+ },
4254
+ read: async () => {
4255
+ try {
4256
+ const chunk = await reader.read();
4257
+ if (chunk.done) free();
4258
+ return chunk;
4259
+ } catch (error) {
4260
+ free();
4261
+ throw error;
4262
+ }
4263
+ },
4264
+ cancel: async (reason) => {
4265
+ try {
4266
+ await reader.cancel(reason);
4267
+ } finally {
4268
+ free();
4269
+ }
4270
+ },
4271
+ releaseLock: () => {
4272
+ reader.releaseLock();
4273
+ }
4274
+ };
4275
+ }
3949
4276
  async function openStream(lane, path, cancel, options, reserved) {
3950
4277
  const url = withQuery(path, options?.query);
3951
- const free = reserved ? () => {
4278
+ const free = reserved ? once(() => {
3952
4279
  projectionHeld = false;
3953
- } : await acquire(lane);
4280
+ }) : await acquire(lane);
3954
4281
  try {
3955
4282
  const response = await send(url, {
3956
4283
  method: options?.method ?? "GET",
@@ -3968,7 +4295,7 @@ window.__ModuleLoader__.load({
3968
4295
  throw new PluginRequestError("http", `HTTP ${response.status}`, response.status);
3969
4296
  }
3970
4297
  cancel.addEventListener("abort", free, { once: true });
3971
- return response.body.getReader();
4298
+ return releasingReader(response.body.getReader(), free);
3972
4299
  } catch (error) {
3973
4300
  free();
3974
4301
  throw failureOf(error, cancel);
@@ -4387,9 +4714,8 @@ window.__ModuleLoader__.load({
4387
4714
  feed: applyLine,
4388
4715
  subscribe(listener) {
4389
4716
  if (disposed) return () => {};
4390
- const wasIdle = listeners.size === 0;
4391
4717
  listeners.add(listener);
4392
- if (wasIdle) onDemand(true);
4718
+ onDemand(true);
4393
4719
  return () => {
4394
4720
  listeners.delete(listener);
4395
4721
  if (listeners.size !== 0) return;
@@ -4405,6 +4731,13 @@ window.__ModuleLoader__.load({
4405
4731
  }
4406
4732
  };
4407
4733
  }
4734
+ /** Whether two lane sets carry the same sessions. Order is the LRU's business,
4735
+ * not the carrier's: the server reads `sessions=` as a set. */
4736
+ function sameLanes(before, after) {
4737
+ if (before.length !== after.length) return false;
4738
+ const held = new Set(before);
4739
+ return after.every((sessionId) => held.has(sessionId));
4740
+ }
4408
4741
  /**
4409
4742
  * Every session's projection over ONE connection.
4410
4743
  *
@@ -4427,6 +4760,10 @@ window.__ModuleLoader__.load({
4427
4760
  #report;
4428
4761
  #resyncCooldownMs;
4429
4762
  #resyncedAt = 0;
4763
+ /** Whether the carrier is in a run of failures. One report per outage: the
4764
+ * retry loop runs every couple of seconds and a beacon per attempt would
4765
+ * be the plugin reporting its own noise. */
4766
+ #carrierFailing = false;
4430
4767
  #controller;
4431
4768
  #settle;
4432
4769
  #running = false;
@@ -4477,10 +4814,12 @@ window.__ModuleLoader__.load({
4477
4814
  * a session list would otherwise reopen it once per row. */
4478
4815
  #demand(sessionId, active) {
4479
4816
  if (this.#disposed) return;
4817
+ const before = this.#lanes();
4480
4818
  if (active) {
4481
4819
  this.#wanted.delete(sessionId);
4482
4820
  this.#wanted.add(sessionId);
4483
4821
  } else if (!this.#wanted.delete(sessionId)) return;
4822
+ if (sameLanes(before, this.#lanes())) return;
4484
4823
  if (this.#settle !== void 0) clearTimeout(this.#settle);
4485
4824
  const timer = setTimeout(() => {
4486
4825
  this.#settle = void 0;
@@ -4510,6 +4849,7 @@ window.__ModuleLoader__.load({
4510
4849
  let superseded = false;
4511
4850
  try {
4512
4851
  const reader = await this.#open(`${CLAUDE_PROJECTION_PATH}/multi?sessions=${lanes.map(encodeURIComponent).join(",")}`, controller.signal);
4852
+ this.#carrierFailing = false;
4513
4853
  const stop = () => {
4514
4854
  superseded = true;
4515
4855
  reader.cancel().catch(() => void 0);
@@ -4532,6 +4872,10 @@ window.__ModuleLoader__.load({
4532
4872
  if (this.#controller !== controller) continue;
4533
4873
  return;
4534
4874
  }
4875
+ if (!this.#carrierFailing) {
4876
+ this.#carrierFailing = true;
4877
+ this.#report("projection-carrier-unavailable", error instanceof Error ? error.message : String(error));
4878
+ }
4535
4879
  } finally {
4536
4880
  if (this.#controller === controller) this.#controller = void 0;
4537
4881
  }
@@ -4624,10 +4968,10 @@ window.__ModuleLoader__.load({
4624
4968
  ".dsh-claude-act-running{animation:dsh-claude-act-pulse 1.2s ease-in-out infinite}",
4625
4969
  "@keyframes dsh-claude-act-pulse{0%,100%{opacity:1}50%{opacity:.3}}"
4626
4970
  ].join("");
4627
- let cssInjected$3 = false;
4628
- function ensureCss$3() {
4629
- if (cssInjected$3 || typeof document === "undefined") return;
4630
- cssInjected$3 = true;
4971
+ let cssInjected$4 = false;
4972
+ function ensureCss$4() {
4973
+ if (cssInjected$4 || typeof document === "undefined") return;
4974
+ cssInjected$4 = true;
4631
4975
  const element = document.createElement("style");
4632
4976
  element.dataset.dshClaudeActivity = "";
4633
4977
  element.textContent = ACTIVITY_CSS;
@@ -5055,7 +5399,7 @@ window.__ModuleLoader__.load({
5055
5399
  });
5056
5400
  }
5057
5401
  function ClaudeActivityNode({ node, useClaudeProjection, t }) {
5058
- ensureCss$3();
5402
+ ensureCss$4();
5059
5403
  const marker = node.data;
5060
5404
  const activities = useClaudeProjection((value) => selectStepActivities(value, marker.turn, marker.step));
5061
5405
  const tasks = useClaudeProjection((value) => value.tasks?.tasks ?? EMPTY_TASKS$1);
@@ -5521,50 +5865,551 @@ window.__ModuleLoader__.load({
5521
5865
  });
5522
5866
  }
5523
5867
  //#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;
5868
+ //#region src/github-url.ts
5869
+ /** Only GitHub's own image hosts; the browser loads these directly, so a URL
5870
+ * the API did not vouch for must never become an outbound request. */
5871
+ function githubAvatarUrl(value) {
5872
+ if (typeof value !== "string" || value.length === 0 || value.length > 1024) return void 0;
5873
+ try {
5874
+ const url = new URL(value);
5875
+ const allowed = url.hostname === "github.com" || url.hostname === "githubusercontent.com" || url.hostname.endsWith(".githubusercontent.com");
5876
+ return url.protocol === "https:" && allowed ? url.href : void 0;
5877
+ } catch {
5878
+ return;
5531
5879
  }
5532
- };
5880
+ }
5881
+ //#endregion
5882
+ //#region src/client/pr-feedback-api.ts
5533
5883
  function record$5(value) {
5534
5884
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
5535
5885
  }
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);
5886
+ function feedbackQuery(sessionId, pullNumber, extra) {
5887
+ return {
5888
+ sessionId,
5889
+ number: String(pullNumber),
5890
+ ...extra
5891
+ };
5552
5892
  }
5553
- function payload(value) {
5893
+ function answer(value) {
5554
5894
  const body = record$5(value);
5555
- if (body === void 0) throw new JiraClientError("Invalid Jira response.");
5895
+ if (body === void 0) throw new Error("Invalid pull request feedback response.");
5556
5896
  return body;
5557
5897
  }
5558
- function status(value) {
5559
- const body = payload(value);
5560
- if (typeof body.connected !== "boolean") throw new JiraClientError("Invalid Jira status response.");
5561
- return body;
5898
+ /** Every arm of this route shells out to `gh`, so reads and writes alike take
5899
+ * the remote budget. */
5900
+ async function loadJson(path, sessionId, pullNumber, signal, extra) {
5901
+ return answer(await pluginRead(`${CLAUDE_REPOSITORY_FEEDBACK_PATH}${path}`, "remote", signal, { query: feedbackQuery(sessionId, pullNumber, extra) }));
5562
5902
  }
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);
5903
+ async function postJson(path, sessionId, pullNumber, input) {
5904
+ return answer(await pluginWrite(`${CLAUDE_REPOSITORY_FEEDBACK_PATH}${path}`, "remote", void 0, {
5905
+ query: feedbackQuery(sessionId, pullNumber),
5906
+ json: input
5907
+ }));
5908
+ }
5909
+ function reviewComment(value) {
5910
+ const input = record$5(value);
5911
+ 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;
5912
+ return input;
5913
+ }
5914
+ async function loadPullRequestThreads(sessionId, pullNumber, signal) {
5915
+ const body = await loadJson("/comments", sessionId, pullNumber, signal);
5916
+ if (!Array.isArray(body.threads)) throw new Error("Invalid pull request comments response.");
5917
+ const threads = [];
5918
+ for (const item of body.threads) {
5919
+ const input = record$5(item);
5920
+ 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;
5921
+ const comments = input.comments.map(reviewComment).filter((value) => value !== void 0);
5922
+ if (comments.length === 0) continue;
5923
+ threads.push({
5924
+ id: input.id,
5925
+ path: input.path,
5926
+ ...typeof input.line === "number" ? { line: input.line } : {},
5927
+ side: input.side,
5928
+ resolved: input.resolved === true,
5929
+ outdated: input.outdated === true,
5930
+ comments
5931
+ });
5932
+ }
5933
+ return threads;
5934
+ }
5935
+ /** Post one reply into the thread that `commentId` belongs to. */
5936
+ async function replyToReviewThread(sessionId, pullNumber, commentId, body) {
5937
+ const comment = reviewComment((await postJson("/reply", sessionId, pullNumber, {
5938
+ commentId,
5939
+ body
5940
+ })).comment);
5941
+ if (comment === void 0) throw new Error("Invalid pull request reply response.");
5942
+ return comment;
5943
+ }
5944
+ /** Resolve or reopen a thread; returns the state GitHub reports afterwards. */
5945
+ async function setReviewThreadResolved(sessionId, pullNumber, threadId, resolved) {
5946
+ const answer = await postJson("/resolve", sessionId, pullNumber, {
5947
+ threadId,
5948
+ resolved
5949
+ });
5950
+ if (typeof answer.resolved !== "boolean") throw new Error("Invalid pull request resolve response.");
5951
+ return answer.resolved;
5952
+ }
5953
+ /** Logins GitHub would notify, for the reply composer's `@` completion. */
5954
+ async function loadMentionableUsers(sessionId, pullNumber, query, signal) {
5955
+ const body = await loadJson("/mentionables", sessionId, pullNumber, signal, { q: query });
5956
+ if (!Array.isArray(body.users)) return [];
5957
+ const users = [];
5958
+ for (const item of body.users) {
5959
+ const input = record$5(item);
5960
+ if (input === void 0 || typeof input.login !== "string" || input.login.length === 0) continue;
5961
+ const avatarUrl = githubAvatarUrl(input.avatarUrl);
5962
+ users.push({
5963
+ login: input.login,
5964
+ ...avatarUrl === void 0 ? {} : { avatarUrl }
5965
+ });
5966
+ }
5967
+ return users;
5968
+ }
5969
+ async function loadFailingChecks(sessionId, pullNumber, signal) {
5970
+ const body = await loadJson("/checks", sessionId, pullNumber, signal);
5971
+ if (!Array.isArray(body.checks)) throw new Error("Invalid pull request checks response.");
5972
+ const checks = [];
5973
+ for (const item of body.checks) {
5974
+ const input = record$5(item);
5975
+ 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;
5976
+ checks.push(input);
5977
+ }
5978
+ return checks;
5979
+ }
5980
+ /** Draft handed to Claude when the user forwards GitHub review comments. A
5981
+ * resolved thread is a settled conversation: forwarding it would ask Claude to
5982
+ * redo work the reviewers already signed off. */
5983
+ function composeCommentsPrompt(threads) {
5984
+ const open = threads.filter((thread) => !thread.resolved);
5985
+ if (open.length === 0) return "";
5986
+ 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) => {
5987
+ const [first, ...rest] = thread.comments;
5988
+ if (first === void 0) return "";
5989
+ 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");
5990
+ }).filter((block) => block.length > 0).join("\n")}`;
5991
+ }
5992
+ /** Draft handed to Claude when the user forwards failing CI checks. */
5993
+ function composeChecksPrompt(checks) {
5994
+ 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) => {
5995
+ 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\`\`\``}`;
5996
+ }).join("\n\n")}`;
5997
+ }
5998
+ /** Draft handed to Claude after an update-branch merge left conflicts behind. */
5999
+ function composeConflictsPrompt(baseBranch, conflicts, method = "merge") {
6000
+ const list = conflicts.map((file) => `- ${file}`).join("\n");
6001
+ 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}`;
6002
+ 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}`;
6003
+ }
6004
+ //#endregion
6005
+ //#region src/client/auto-fix.ts
6006
+ const AUTO_FIX_INTERVAL_MS = 3e4;
6007
+ 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.";
6008
+ const EMPTY_MEMORY = { handledCommentIds: /* @__PURE__ */ new Set() };
6009
+ const sessions = /* @__PURE__ */ new Map();
6010
+ function session(sessionId) {
6011
+ let entry = sessions.get(sessionId);
6012
+ if (entry === void 0) {
6013
+ entry = {
6014
+ enabled: false,
6015
+ memory: EMPTY_MEMORY
6016
+ };
6017
+ sessions.set(sessionId, entry);
6018
+ }
6019
+ return entry;
6020
+ }
6021
+ function autoFixEnabled(sessionId) {
6022
+ return session(sessionId).enabled;
6023
+ }
6024
+ function setAutoFixEnabled(sessionId, enabled) {
6025
+ session(sessionId).enabled = enabled;
6026
+ }
6027
+ function autoFixMemory(sessionId) {
6028
+ return session(sessionId).memory;
6029
+ }
6030
+ function rememberAutoFix(sessionId, memory) {
6031
+ session(sessionId).memory = memory;
6032
+ }
6033
+ /** One failing CI run yields one fix attempt: run links change when CI re-runs. */
6034
+ function checksSignature(checks) {
6035
+ if (checks.length === 0) return void 0;
6036
+ return checks.map((check) => `${check.name}|${check.link ?? ""}`).sort().join("\n");
6037
+ }
6038
+ function planAutoFix(memory, threads, checks) {
6039
+ const unhandled = threads.filter((thread) => !thread.resolved).filter((thread) => thread.comments.some((comment) => !memory.handledCommentIds.has(comment.id)));
6040
+ const fresh = unhandled.flatMap((thread) => thread.comments);
6041
+ const signature = checksSignature(checks);
6042
+ const checksChanged = signature !== void 0 && signature !== memory.handledChecksSignature;
6043
+ const sections = [];
6044
+ if (unhandled.length > 0) sections.push(composeCommentsPrompt(unhandled));
6045
+ if (checksChanged) sections.push(composeChecksPrompt(checks));
6046
+ if (sections.length === 0) return { memory };
6047
+ const nextSignature = checksChanged ? signature : memory.handledChecksSignature;
6048
+ return {
6049
+ prompt: `${sections.join("\n\n")}\n\n${AUTO_FIX_FOOTER}`,
6050
+ memory: {
6051
+ handledCommentIds: /* @__PURE__ */ new Set([...memory.handledCommentIds, ...fresh.map((comment) => comment.id)]),
6052
+ ...nextSignature === void 0 ? {} : { handledChecksSignature: nextSignature }
6053
+ }
6054
+ };
6055
+ }
6056
+ //#endregion
6057
+ //#region src/client/session-preset.ts
6058
+ /**
6059
+ * Resolve one row's preset id, newest seat first.
6060
+ * @param row - a session-list row, or undefined when the id is not listed.
6061
+ * @returns the preset id, or undefined when neither source carries one.
6062
+ */
6063
+ function sessionRowPreset(row) {
6064
+ return row?.agentPreset ?? row?.projectionValues?.agentPreset ?? void 0;
6065
+ }
6066
+ //#endregion
6067
+ //#region src/client/ClaudePullRequestsPanel.tsx
6068
+ const NO_WORKSPACE_STATE = {};
6069
+ const NO_WORKSPACES = {
6070
+ subscribe: () => () => {},
6071
+ getSnapshot: () => NO_WORKSPACE_STATE
6072
+ };
6073
+ const OVERVIEW_REFRESH_MS = 3e4;
6074
+ /** What a running session is blocked on: the latest permission or question
6075
+ * activity that is still in its started phase. */
6076
+ function overviewAttention(activities) {
6077
+ for (let index = activities.length - 1; index >= 0; index -= 1) {
6078
+ const activity = activities[index];
6079
+ if (activity === void 0 || activity.kind !== "permission" && activity.kind !== "question") continue;
6080
+ return activity.phase === "started" ? activity.kind : void 0;
6081
+ }
6082
+ }
6083
+ /** Claude sessions worth listing: rows still in the host list (byId keeps
6084
+ * deleted and breadcrumb rows), non-blank, non-subagent, with a checkout;
6085
+ * running first. */
6086
+ function claudeSessionRows(state, archivedSessionIds = []) {
6087
+ const rows = state.ids === void 0 ? Object.values(state.byId) : state.ids.map((id) => state.byId[id]);
6088
+ const archived = new Set(archivedSessionIds);
6089
+ 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));
6090
+ }
6091
+ function Badge({ label, tone = "neutral" }) {
6092
+ const toneStyle = tone === "success" ? repositoryItemSuccess : tone === "warning" ? repositoryItemWarning : tone === "error" ? repositoryItemError : tone === "merged" ? { color: "#a78bfa" } : {};
6093
+ return /* @__PURE__ */ jsxs("span", {
6094
+ style: {
6095
+ ...repositoryItem,
6096
+ ...toneStyle
6097
+ },
6098
+ children: [/* @__PURE__ */ jsx("span", {
6099
+ style: repositoryItemDot,
6100
+ "aria-hidden": "true"
6101
+ }), /* @__PURE__ */ jsx("span", {
6102
+ style: repositoryItemLabel,
6103
+ children: label
6104
+ })]
6105
+ });
6106
+ }
6107
+ function repositoryName$1(remote) {
6108
+ return remote?.split("/").at(-1);
6109
+ }
6110
+ function OverviewAttention({ source, running, t }) {
6111
+ const snapshot = useSyncExternalStore(source.subscribe, source.getSnapshot, source.getSnapshot);
6112
+ const attention = running ? overviewAttention(snapshot.activities) : void 0;
6113
+ const usage = snapshot.contextUsage;
6114
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
6115
+ attention === "permission" ? /* @__PURE__ */ jsx(Badge, {
6116
+ label: t("overviewNeedsPermission"),
6117
+ tone: "warning"
6118
+ }) : null,
6119
+ attention === "question" ? /* @__PURE__ */ jsx(Badge, {
6120
+ label: t("overviewNeedsAnswer"),
6121
+ tone: "warning"
6122
+ }) : null,
6123
+ usage === void 0 ? null : /* @__PURE__ */ jsx("span", { children: t("overviewContextUsage", { percentage: usage.percentage }) })
6124
+ ] });
6125
+ }
6126
+ function ClaudePullRequestsPanel({ t, closeDetails, openSession, loadStatus, sessions, workspaces, projectionFor }) {
6127
+ const sessionStore = useMemo(() => ({
6128
+ subscribe: (listener) => sessions.subscribe(listener),
6129
+ getSnapshot: () => sessions.getSnapshot()
6130
+ }), [sessions]);
6131
+ const snapshot = useSyncExternalStore(sessionStore.subscribe, sessionStore.getSnapshot, sessionStore.getSnapshot);
6132
+ const workspaceStore = useMemo(() => {
6133
+ const source = workspaces ?? NO_WORKSPACES;
6134
+ return {
6135
+ subscribe: (listener) => source.subscribe(listener),
6136
+ getSnapshot: () => source.getSnapshot()
6137
+ };
6138
+ }, [workspaces]);
6139
+ const workspaceState = useSyncExternalStore(workspaceStore.subscribe, workspaceStore.getSnapshot, workspaceStore.getSnapshot);
6140
+ const rows = useMemo(() => claudeSessionRows(snapshot, workspaceState.archivedSessionIds ?? []), [snapshot, workspaceState]);
6141
+ const cwdKey = useMemo(() => [...new Set(rows.map((row) => row.cwd ?? ""))].sort().join("\0"), [rows]);
6142
+ const [statuses, setStatuses] = useState({});
6143
+ useEffect(() => {
6144
+ const cwds = cwdKey.length === 0 ? [] : cwdKey.split("\0");
6145
+ if (cwds.length === 0) return;
6146
+ const controller = new AbortController();
6147
+ const refresh = () => {
6148
+ for (const cwd of cwds) loadStatus(cwd, controller.signal).then((status) => {
6149
+ if (!controller.signal.aborted) setStatuses((previous) => ({
6150
+ ...previous,
6151
+ [cwd]: status
6152
+ }));
6153
+ }, () => void 0);
6154
+ };
6155
+ refresh();
6156
+ const timer = setInterval(refresh, OVERVIEW_REFRESH_MS);
6157
+ return () => {
6158
+ controller.abort();
6159
+ clearInterval(timer);
6160
+ };
6161
+ }, [cwdKey, loadStatus]);
6162
+ return /* @__PURE__ */ jsxs("div", {
6163
+ className: detailsCardClass,
6164
+ style: tasksPanel,
6165
+ children: [
6166
+ /* @__PURE__ */ jsxs("style", {
6167
+ "data-dsh-claude-overview-styles": true,
6168
+ children: [detailsCardCss, panelIconButtonCss]
6169
+ }),
6170
+ /* @__PURE__ */ jsxs("header", {
6171
+ style: tasksHeader,
6172
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
6173
+ style: tasksHeading,
6174
+ children: t("overviewTitle")
6175
+ }), /* @__PURE__ */ jsx("span", {
6176
+ style: tasksTurnMeta,
6177
+ children: t("overviewBody")
6178
+ })] }), /* @__PURE__ */ jsx("button", {
6179
+ type: "button",
6180
+ className: panelIconButtonClass,
6181
+ "aria-label": t("diffClose"),
6182
+ onClick: closeDetails,
6183
+ children: /* @__PURE__ */ jsx(IconCloseOutline16, {})
6184
+ })]
6185
+ }),
6186
+ /* @__PURE__ */ jsx("div", {
6187
+ style: overviewBody,
6188
+ children: rows.length === 0 ? /* @__PURE__ */ jsx("p", {
6189
+ style: overviewEmpty,
6190
+ children: t("overviewEmpty")
6191
+ }) : rows.map((row) => {
6192
+ const repository = row.cwd === void 0 ? void 0 : statuses[row.cwd];
6193
+ const pullRequest = repository?.pullRequest;
6194
+ const branch = repository?.status === "ready" ? repository.detached === true ? t("repositoryDetached") : repository.branch ?? t("repositoryUnknownBranch") : repository === void 0 ? t("overviewLoading") : t("repositoryUnavailable");
6195
+ return /* @__PURE__ */ jsxs("button", {
6196
+ type: "button",
6197
+ style: overviewRow,
6198
+ onClick: () => {
6199
+ openSession(row.id);
6200
+ },
6201
+ children: [/* @__PURE__ */ jsxs("span", {
6202
+ style: overviewRowTop,
6203
+ children: [
6204
+ row.running === true ? /* @__PURE__ */ jsx("span", {
6205
+ style: overviewRunningDot,
6206
+ "aria-label": t("overviewRunning")
6207
+ }) : null,
6208
+ /* @__PURE__ */ jsx("span", {
6209
+ style: overviewTitle,
6210
+ children: row.displayTitle ?? row.id
6211
+ }),
6212
+ pullRequest === void 0 ? /* @__PURE__ */ jsx(Badge, { label: t("overviewNoPr") }) : /* @__PURE__ */ jsx(Badge, {
6213
+ label: `#${pullRequest.number} · ${t(`repositoryState_${pullRequest.state}`)}`,
6214
+ tone: pullRequest.state === "merged" ? "merged" : pullRequest.state === "open" ? "success" : "neutral"
6215
+ })
6216
+ ]
6217
+ }), /* @__PURE__ */ jsxs("span", {
6218
+ style: overviewMeta,
6219
+ children: [
6220
+ repositoryName$1(repository?.remote) === void 0 ? null : /* @__PURE__ */ jsx("span", { children: repositoryName$1(repository?.remote) }),
6221
+ /* @__PURE__ */ jsx("span", {
6222
+ style: overviewBranch,
6223
+ children: branch
6224
+ }),
6225
+ pullRequest?.state === "open" && pullRequest.checks !== "none" ? /* @__PURE__ */ jsx(Badge, {
6226
+ label: t(`repositoryChecks_${pullRequest.checks}`),
6227
+ tone: pullRequest.checks === "passing" ? "success" : pullRequest.checks === "failing" ? "error" : "warning"
6228
+ }) : null,
6229
+ pullRequest?.state === "open" && pullRequest.review !== "none" ? /* @__PURE__ */ jsx(Badge, {
6230
+ label: t(`repositoryReview_${pullRequest.review}`),
6231
+ tone: pullRequest.review === "approved" ? "success" : pullRequest.review === "changes-requested" ? "error" : "neutral"
6232
+ }) : null,
6233
+ autoFixEnabled(row.id) ? /* @__PURE__ */ jsx(Badge, {
6234
+ label: t("overviewAutoFix"),
6235
+ tone: "success"
6236
+ }) : null,
6237
+ projectionFor === void 0 ? null : /* @__PURE__ */ jsx(OverviewAttention, {
6238
+ source: projectionFor(row.id),
6239
+ running: row.running === true,
6240
+ t
6241
+ })
6242
+ ]
6243
+ })]
6244
+ }, row.id);
6245
+ })
6246
+ })
6247
+ ]
6248
+ });
6249
+ }
6250
+ //#endregion
6251
+ //#region src/client/session-alerts.ts
6252
+ /** Desktop notifications for the sessions the user is not looking at.
6253
+ *
6254
+ * The session board already knows which session is blocked on an approval or
6255
+ * a question and which has gone quiet — but only while it is open, which
6256
+ * makes the user the poller. Running several worktree sessions at once is the
6257
+ * workflow this plugin is built for, and it is the one thing that gets worse
6258
+ * the more of them there are.
6259
+ *
6260
+ * Everything here is derived from the two feeds the board already reads, so a
6261
+ * standing watcher costs one more subscriber on the shared projection
6262
+ * carrier rather than a stream per session.
6263
+ */
6264
+ /** ponytail: module state, like the auto-fix toggle next door. The Settings
6265
+ * panel and the boot read both write it, and the watcher reads it per alert,
6266
+ * so switching alerts off lands without a reload. */
6267
+ let alertsEnabled = true;
6268
+ function claudeAlertsEnabled() {
6269
+ return alertsEnabled;
6270
+ }
6271
+ function setClaudeAlertsEnabled(enabled) {
6272
+ alertsEnabled = enabled;
6273
+ }
6274
+ /** The alert one observation earns, or undefined when nothing happened that is
6275
+ * worth interrupting the user for.
6276
+ *
6277
+ * A session observed for the first time earns nothing: the watcher starts
6278
+ * with every session unknown, and announcing the state each one merely
6279
+ * happens to be in would greet a restart with a burst. */
6280
+ function sessionAlert(previous, next) {
6281
+ if (previous === void 0) return void 0;
6282
+ if (next.attention !== void 0 && next.attention !== previous.attention) return next.attention;
6283
+ if (previous.running && !next.running) return "idle";
6284
+ }
6285
+ /** Deliver one alert as a desktop notification.
6286
+ *
6287
+ * Best effort throughout: a Host without the Notification API, or a user who
6288
+ * has refused permission, simply gets no alerts. The tag collapses repeat
6289
+ * alerts for one session into a single banner rather than a stack. */
6290
+ function postSessionAlert(alert) {
6291
+ if (typeof Notification === "undefined" || Notification.permission === "denied") return;
6292
+ const show = () => {
6293
+ try {
6294
+ const notification = new Notification(alert.title, {
6295
+ body: alert.body,
6296
+ tag: `dsh-claude:${alert.sessionId}`
6297
+ });
6298
+ notification.onclick = () => {
6299
+ globalThis.focus?.();
6300
+ alert.open();
6301
+ };
6302
+ } catch {}
6303
+ };
6304
+ if (Notification.permission === "granted") show();
6305
+ else Notification.requestPermission().then((result) => {
6306
+ if (result === "granted") show();
6307
+ }, () => void 0);
6308
+ }
6309
+ const BODY_KEY = {
6310
+ permission: "alertNeedsPermission",
6311
+ question: "alertNeedsAnswer",
6312
+ idle: "alertTurnFinished"
6313
+ };
6314
+ /** Watch every Claude session and announce the ones that need the user.
6315
+ * Returns the unsubscriber. */
6316
+ function startClaudeSessionAlerts(deps) {
6317
+ const post = deps.post ?? postSessionAlert;
6318
+ const known = /* @__PURE__ */ new Map();
6319
+ const projections = /* @__PURE__ */ new Map();
6320
+ let disposed = false;
6321
+ const announce = (row, kind) => {
6322
+ if (!(deps.enabled ?? claudeAlertsEnabled)()) return;
6323
+ post({
6324
+ sessionId: row.id,
6325
+ title: row.displayTitle ?? deps.t("alertFallbackTitle"),
6326
+ body: deps.t(BODY_KEY[kind]),
6327
+ open: () => {
6328
+ deps.open(row.id);
6329
+ }
6330
+ });
6331
+ };
6332
+ const evaluate = () => {
6333
+ if (disposed) return;
6334
+ const snapshot = deps.sessions.getSnapshot();
6335
+ const rows = claudeSessionRows(snapshot);
6336
+ const live = new Set(rows.map((row) => row.id));
6337
+ for (const [sessionId, unsubscribe] of [...projections]) {
6338
+ if (live.has(sessionId)) continue;
6339
+ unsubscribe();
6340
+ projections.delete(sessionId);
6341
+ known.delete(sessionId);
6342
+ }
6343
+ for (const row of rows) {
6344
+ const source = deps.projectionFor(row.id);
6345
+ if (!projections.has(row.id)) projections.set(row.id, source.subscribe(evaluate));
6346
+ const running = row.running === true;
6347
+ const next = {
6348
+ running,
6349
+ attention: running ? overviewAttention(source.getSnapshot().activities) : void 0
6350
+ };
6351
+ const previous = known.get(row.id);
6352
+ known.set(row.id, next);
6353
+ if (row.id === snapshot.current) continue;
6354
+ const kind = sessionAlert(previous, next);
6355
+ if (kind !== void 0) announce(row, kind);
6356
+ }
6357
+ };
6358
+ const unsubscribe = deps.sessions.subscribe(evaluate);
6359
+ evaluate();
6360
+ return () => {
6361
+ disposed = true;
6362
+ unsubscribe();
6363
+ for (const dispose of projections.values()) dispose();
6364
+ projections.clear();
6365
+ known.clear();
6366
+ };
6367
+ }
6368
+ //#endregion
6369
+ //#region src/client/jira-api.ts
6370
+ var JiraClientError = class extends Error {
6371
+ code;
6372
+ constructor(message, code) {
6373
+ super(message);
6374
+ this.name = "JiraClientError";
6375
+ if (code !== void 0) this.code = code;
6376
+ }
6377
+ };
6378
+ function record$4(value) {
6379
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
6380
+ }
6381
+ /**
6382
+ * Every Jira failure the panels catch is a `JiraClientError`, whatever the
6383
+ * transport threw: `ClaudeHeroRepositoryControls` branches on `code` to tell
6384
+ * 'not-connected' apart from a real outage, and the settings card renders the
6385
+ * message verbatim.
6386
+ *
6387
+ * The routes answer `{ error, message }`, so `message` is already the sentence
6388
+ * to show. A body carrying only a code — a 405, a bad JSON body — used to read
6389
+ * 'Jira is unavailable.' rather than leaking the code as prose, and it still
6390
+ * does. Transport failures (a starved pool, an elapsed budget, an older Host
6391
+ * without the route) carry their own wording and keep it.
6392
+ */
6393
+ function jiraFailure(cause) {
6394
+ if (cause instanceof JiraClientError) return cause;
6395
+ if (!(cause instanceof PluginRequestError)) return new JiraClientError(cause instanceof Error ? cause.message : String(cause));
6396
+ return new JiraClientError(cause.message === cause.code ? "Jira is unavailable." : cause.message, cause.code);
6397
+ }
6398
+ function payload(value) {
6399
+ const body = record$4(value);
6400
+ if (body === void 0) throw new JiraClientError("Invalid Jira response.");
6401
+ return body;
6402
+ }
6403
+ function status(value) {
6404
+ const body = payload(value);
6405
+ if (typeof body.connected !== "boolean") throw new JiraClientError("Invalid Jira status response.");
6406
+ return body;
6407
+ }
6408
+ async function loadJiraStatus(signal) {
6409
+ try {
6410
+ return status(await pluginRead(`${CLAUDE_JIRA_PATH}/status`, "remote", signal));
6411
+ } catch (cause) {
6412
+ throw jiraFailure(cause);
5568
6413
  }
5569
6414
  }
5570
6415
  async function connectJira(input) {
@@ -5587,7 +6432,7 @@ window.__ModuleLoader__.load({
5587
6432
  if (!Array.isArray(body.tickets)) throw new JiraClientError("Invalid Jira search response.");
5588
6433
  const tickets = [];
5589
6434
  for (const item of body.tickets) {
5590
- const ticket = record$5(item);
6435
+ const ticket = record$4(item);
5591
6436
  if (ticket === void 0 || typeof ticket.key !== "string" || typeof ticket.summary !== "string" || typeof ticket.url !== "string") continue;
5592
6437
  tickets.push(ticket);
5593
6438
  }
@@ -5835,6 +6680,12 @@ window.__ModuleLoader__.load({
5835
6680
  const value = settings.find((setting) => setting.key === "prose")?.value;
5836
6681
  return isClaudeProseMode(value) ? value : DEFAULT_CLAUDE_PROSE_MODE;
5837
6682
  }
6683
+ /** The alert mode a settings payload carries, read the same way and for the
6684
+ * same reason as {@link proseModeOf}. */
6685
+ function alertModeOf(settings) {
6686
+ const value = settings.find((setting) => setting.key === "alerts")?.value;
6687
+ return isClaudeAlertMode(value) ? value : "on";
6688
+ }
5838
6689
  /** Settings whose row only makes sense under a particular value of another.
5839
6690
  * Filtering here rather than server-side keeps the descriptor list flat: the
5840
6691
  * server has no view of what the Client can paint. Fails OPEN — a payload
@@ -5860,6 +6711,10 @@ window.__ModuleLoader__.load({
5860
6711
  label: "prose",
5861
6712
  hint: "proseEffect"
5862
6713
  },
6714
+ alerts: {
6715
+ label: "alerts",
6716
+ hint: "alertsEffect"
6717
+ },
5863
6718
  worktreeBranchPrefix: {
5864
6719
  label: "worktreeBranchPrefix",
5865
6720
  hint: "worktreeBranchPrefixEffect"
@@ -5880,7 +6735,9 @@ window.__ModuleLoader__.load({
5880
6735
  "renderer:plugin": "rendererPlugin",
5881
6736
  "renderer:native": "rendererNative",
5882
6737
  "prose:plain": "prosePlain",
5883
- "prose:enhanced": "proseEnhanced"
6738
+ "prose:enhanced": "proseEnhanced",
6739
+ "alerts:off": "alertsOff",
6740
+ "alerts:on": "alertsOn"
5884
6741
  };
5885
6742
  function settingOptionLabel(settingKey, option, t) {
5886
6743
  const key = SETTING_OPTION_COPY[`${settingKey}:${option.value}`];
@@ -6120,6 +6977,7 @@ window.__ModuleLoader__.load({
6120
6977
  if (!isGlobalSettingsView(payload)) throw new Error("Invalid global settings response");
6121
6978
  setGlobalSettings(payload);
6122
6979
  applyClaudeMarkdownTheme(proseModeOf(payload.settings));
6980
+ setClaudeAlertsEnabled(alertModeOf(payload.settings) === "on");
6123
6981
  } catch (cause) {
6124
6982
  setGlobalSettingsError(cardFailure(cause));
6125
6983
  } finally {
@@ -6467,6 +7325,531 @@ window.__ModuleLoader__.load({
6467
7325
  });
6468
7326
  }
6469
7327
  //#endregion
7328
+ //#region src/client/plan-feedback-api.ts
7329
+ /** Send one plan back for changes. */
7330
+ async function sendPlanForChanges(sessionId, toolUseId, notes) {
7331
+ try {
7332
+ await pluginWrite(CLAUDE_PLAN_FEEDBACK_PATH, "fast", void 0, {
7333
+ query: { sessionId },
7334
+ json: {
7335
+ toolUseId,
7336
+ notes
7337
+ }
7338
+ });
7339
+ } catch (error) {
7340
+ const settled = error instanceof PluginRequestError && error.reason === "http" && error.status === 409;
7341
+ throw new Error(settled ? "planSettled" : "planFeedbackFailed");
7342
+ }
7343
+ }
7344
+ //#endregion
7345
+ //#region src/client/ClaudePlanPanel.tsx
7346
+ const PLAN_TOOL = "ExitPlanMode";
7347
+ /** Every plan this session handed over, oldest first, each with where its
7348
+ * approval stands.
7349
+ *
7350
+ * The permission bridge writes one `started` record carrying the plan and,
7351
+ * once the user decides, a second record under the same `toolUseId` whose
7352
+ * phase says which way it went. Both arrive on the ordinary activity stream,
7353
+ * so the panel needs no channel of its own — it reads the transcript the
7354
+ * session already has. */
7355
+ function planReviews(activities) {
7356
+ const plans = /* @__PURE__ */ new Map();
7357
+ const states = /* @__PURE__ */ new Map();
7358
+ for (const activity of activities) {
7359
+ const { toolUseId } = activity;
7360
+ if (activity.kind !== "permission" || toolUseId === void 0) continue;
7361
+ if (activity.toolName === PLAN_TOOL && activity.phase === "started" && activity.text !== void 0 && activity.text.length > 0) {
7362
+ plans.set(toolUseId, activity.text);
7363
+ if (!states.has(toolUseId)) states.set(toolUseId, "pending");
7364
+ }
7365
+ if (activity.phase === "completed") states.set(toolUseId, "approved");
7366
+ else if (activity.phase === "denied" || activity.phase === "failed") states.set(toolUseId, "rejected");
7367
+ }
7368
+ return [...plans].map(([toolUseId, plan]) => ({
7369
+ toolUseId,
7370
+ plan,
7371
+ state: states.get(toolUseId) ?? "pending"
7372
+ }));
7373
+ }
7374
+ /** The newest plan, for readers that only care what is on the table now. */
7375
+ function latestPlanReview(activities) {
7376
+ return planReviews(activities).at(-1);
7377
+ }
7378
+ /** A plan's own first heading, so a list of several can name them.
7379
+ *
7380
+ * Falls back to the opening line: a plan without a heading is unusual but
7381
+ * still has to be pickable. Fenced blocks are not scanned — a `#` comment in
7382
+ * the first code block of a heading-less plan is a worse label than the first
7383
+ * line, but not a wrong one, and the cost of getting it exactly right is a
7384
+ * fence-state machine for a fallback. */
7385
+ function planTitle(plan) {
7386
+ for (const raw of plan.split("\n")) {
7387
+ const line = raw.trim();
7388
+ if (line.length === 0) continue;
7389
+ const heading = /^#{1,6}\s+(.+?)\s*#*$/u.exec(line);
7390
+ if (heading?.[1] !== void 0) return heading[1].slice(0, MAX_TITLE_CHARS);
7391
+ return line.slice(0, MAX_TITLE_CHARS);
7392
+ }
7393
+ return "";
7394
+ }
7395
+ const MAX_TITLE_CHARS = 80;
7396
+ const MAX_QUOTE_CHARS = 1e3;
7397
+ /** The passage under the current selection, when it lies inside the plan body.
7398
+ *
7399
+ * Reads the live selection rather than mirroring the DOM: the body is the
7400
+ * Host's Markdown output, which this package renders but does not own, and
7401
+ * the only stable thing about it is that it is inside this element. */
7402
+ function quotedSelection(selection, body) {
7403
+ if (selection === null || body === null || selection.isCollapsed || selection.rangeCount === 0) return void 0;
7404
+ const range = selection.getRangeAt(0);
7405
+ if (!body.contains(range.startContainer) || !body.contains(range.endContainer)) return void 0;
7406
+ const text = selection.toString().trim();
7407
+ return text.length === 0 ? void 0 : text.slice(0, MAX_QUOTE_CHARS);
7408
+ }
7409
+ /** The newest review as one primitive, for readers that only need to know
7410
+ * whether it changed. A snapshot hook keeps its value only while the
7411
+ * selection compares equal, and a fresh object per snapshot would defeat
7412
+ * that. Empty string when the session has proposed nothing. */
7413
+ function planReviewKey(activities) {
7414
+ const review = latestPlanReview(activities);
7415
+ return review === void 0 ? "" : `${review.state}:${review.toolUseId}`;
7416
+ }
7417
+ /** Split what {@link planReviewKey} joined. */
7418
+ function parsePlanReviewKey(key) {
7419
+ const cut = key.indexOf(":");
7420
+ if (cut < 0) return void 0;
7421
+ const state = key.slice(0, cut);
7422
+ if (state !== "pending" && state !== "approved" && state !== "rejected") return void 0;
7423
+ return {
7424
+ state,
7425
+ toolUseId: key.slice(cut + 1)
7426
+ };
7427
+ }
7428
+ /** Restore-from-maximized: four corners pulling inward. Mirrors the diff
7429
+ * panel's own, which the primitives set has no counterpart for. */
7430
+ function RestorePanelIcon$1() {
7431
+ return /* @__PURE__ */ jsx("svg", {
7432
+ width: "16",
7433
+ height: "16",
7434
+ viewBox: "0 0 16 16",
7435
+ fill: "currentColor",
7436
+ "aria-hidden": "true",
7437
+ 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" })
7438
+ });
7439
+ }
7440
+ /** Card and rows reproduce the primitives' menu surface, like the session
7441
+ * menu next door: r7, inverted hairline, shadow-lv3, 2px inset. */
7442
+ const PICKER_CSS = [
7443
+ ".dsh-claude-plan-picker-root{position:relative;display:inline-flex;min-width:0}",
7444
+ ".dsh-claude-plan-picker{display:inline-flex;align-items:center;gap:4px;min-width:0;padding:2px 6px;",
7445
+ "margin:-2px -6px;border:0;border-radius:7px;background:transparent;color:inherit;font:inherit;cursor:pointer;",
7446
+ "transition:background .12s ease}",
7447
+ ".dsh-claude-plan-picker:hover,.dsh-claude-plan-picker[aria-expanded=\"true\"]{background:var(--dsw-alias-interactive-bg-hover)}",
7448
+ ".dsh-claude-plan-picker:focus-visible{outline:none;background:var(--dsw-alias-interactive-bg-hover)}",
7449
+ ".dsh-claude-plan-picker>svg{flex:none;transition:transform .12s ease}",
7450
+ ".dsh-claude-plan-picker[aria-expanded=\"true\"]>svg{transform:rotate(180deg)}",
7451
+ ".dsh-claude-plan-picker-card{box-sizing:border-box;position:absolute;top:calc(100% + 6px);left:-6px;z-index:100;",
7452
+ "display:flex;flex-direction:column;gap:1px;padding:2px;min-width:240px;max-width:min(420px,70vw);",
7453
+ "max-height:min(320px,50vh);overflow-y:auto;border:1px solid var(--dsw-alias-border-inverted);",
7454
+ "border-radius:7px;background:var(--dsw-specific-menu);box-shadow:var(--dsw-shadow-lv3)}",
7455
+ ".dsh-claude-plan-picker-item{display:flex;align-items:center;gap:8px;width:100%;min-height:26px;",
7456
+ "padding:5px 8px;border:0;border-radius:5px;background:transparent;color:var(--dsw-alias-label-primary);",
7457
+ "font:inherit;font-size:12px;line-height:17px;text-align:left;cursor:pointer}",
7458
+ ".dsh-claude-plan-picker-item:hover,.dsh-claude-plan-picker-item:focus-visible{outline:none;",
7459
+ "background:var(--dsw-alias-interactive-bg-hover)}",
7460
+ ".dsh-claude-plan-picker-item[aria-current=\"true\"]{background:var(--dsw-alias-interactive-bg-hover)}",
7461
+ ".dsh-claude-plan-picker-title{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
7462
+ ".dsh-claude-plan-picker-ordinal{flex:none;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}"
7463
+ ].join("");
7464
+ function ChevronDownIcon() {
7465
+ return /* @__PURE__ */ jsx("svg", {
7466
+ width: "12",
7467
+ height: "12",
7468
+ viewBox: "0 0 12 12",
7469
+ fill: "none",
7470
+ "aria-hidden": "true",
7471
+ children: /* @__PURE__ */ jsx("path", {
7472
+ d: "m3 4.75 3 3 3-3",
7473
+ stroke: "currentColor",
7474
+ strokeWidth: "1.5",
7475
+ strokeLinecap: "round",
7476
+ strokeLinejoin: "round"
7477
+ })
7478
+ });
7479
+ }
7480
+ const STATE_LABEL = {
7481
+ pending: "planPending",
7482
+ approved: "planApproved",
7483
+ rejected: "planRejected"
7484
+ };
7485
+ /** The plan behind an `ExitPlanMode` approval, as the document it was written
7486
+ * as. The decision itself stays with the Host's approval dialog; this panel
7487
+ * is where the plan is actually read, under the reader's own prose palette. */
7488
+ function ClaudePlanPanel({ useClaudeProjection, t, sessionId, closeDetails, maximized, toggleMaximized, submitChanges = sendPlanForChanges }) {
7489
+ const markdownLabels = useClaudeMarkdownLabels(t);
7490
+ const owned = useClaudeProjection((projection) => projection.owned);
7491
+ const reviews = planReviews(useClaudeProjection((projection) => projection.activities));
7492
+ const [chosen, setChosen] = useState();
7493
+ const [open, setOpen] = useState(false);
7494
+ const picker = useRef(null);
7495
+ useDismissOnOutsidePointer(picker, open, (next) => {
7496
+ if (!next) setOpen(false);
7497
+ });
7498
+ const pendingId = reviews.find((item) => item.state === "pending")?.toolUseId;
7499
+ useEffect(() => {
7500
+ if (pendingId !== void 0) setChosen(pendingId);
7501
+ }, [pendingId]);
7502
+ const review = reviews.find((item) => item.toolUseId === chosen) ?? reviews.at(-1);
7503
+ const [notes, setNotes] = useState([]);
7504
+ const [draft, setDraft] = useState("");
7505
+ const [quote, setQuote] = useState();
7506
+ const [sending, setSending] = useState(false);
7507
+ const [failure, setFailure] = useState();
7508
+ const body = useRef(null);
7509
+ const composer = useRef(null);
7510
+ useEffect(() => {
7511
+ if (typeof document === "undefined") return;
7512
+ const read = () => {
7513
+ const selected = quotedSelection(document.getSelection(), body.current);
7514
+ if (selected !== void 0) setQuote(selected);
7515
+ };
7516
+ document.addEventListener("selectionchange", read);
7517
+ return () => {
7518
+ document.removeEventListener("selectionchange", read);
7519
+ };
7520
+ }, []);
7521
+ useEffect(() => {
7522
+ setNotes([]);
7523
+ setDraft("");
7524
+ setQuote(void 0);
7525
+ setFailure(void 0);
7526
+ }, [review?.toolUseId]);
7527
+ const addNote = useCallback(() => {
7528
+ const text = draft.trim();
7529
+ if (text.length === 0) return;
7530
+ setNotes((current) => [...current, quote === void 0 ? { text } : {
7531
+ quote,
7532
+ text
7533
+ }]);
7534
+ setDraft("");
7535
+ setQuote(void 0);
7536
+ }, [draft, quote]);
7537
+ const send = useCallback(() => {
7538
+ if (review === void 0 || sending) return;
7539
+ const text = draft.trim();
7540
+ const pending = text.length === 0 ? notes : [...notes, quote === void 0 ? { text } : {
7541
+ quote,
7542
+ text
7543
+ }];
7544
+ if (pending.length === 0) return;
7545
+ setSending(true);
7546
+ setFailure(void 0);
7547
+ submitChanges(sessionId, review.toolUseId, pending).then(() => {
7548
+ setNotes([]);
7549
+ setDraft("");
7550
+ setQuote(void 0);
7551
+ setSending(false);
7552
+ }, (error) => {
7553
+ setFailure(error instanceof Error && error.message === "planSettled" ? "planSettled" : "planFeedbackFailed");
7554
+ setSending(false);
7555
+ });
7556
+ }, [
7557
+ draft,
7558
+ notes,
7559
+ quote,
7560
+ review,
7561
+ sending,
7562
+ sessionId,
7563
+ submitChanges
7564
+ ]);
7565
+ useEffect(() => {
7566
+ if (!owned || review === void 0) closeDetails();
7567
+ }, [
7568
+ closeDetails,
7569
+ owned,
7570
+ review === void 0
7571
+ ]);
7572
+ if (!owned) return null;
7573
+ const index = review === void 0 ? -1 : reviews.findIndex((item) => item.toolUseId === review.toolUseId);
7574
+ const badge = (state) => ({
7575
+ ...planBadge,
7576
+ ...state === "pending" ? planBadgePending : state === "rejected" ? planBadgeRejected : {}
7577
+ });
7578
+ return /* @__PURE__ */ jsxs("div", {
7579
+ className: detailsCardClass,
7580
+ style: {
7581
+ ...tasksPanel,
7582
+ ...maximized ? diffPanelMaximized : {}
7583
+ },
7584
+ children: [
7585
+ /* @__PURE__ */ jsxs("style", {
7586
+ "data-dsh-claude-panel-icon-styles": true,
7587
+ children: [
7588
+ detailsCardCss,
7589
+ panelIconButtonCss,
7590
+ PICKER_CSS
7591
+ ]
7592
+ }),
7593
+ /* @__PURE__ */ jsxs("div", {
7594
+ style: tasksHeader,
7595
+ children: [/* @__PURE__ */ jsxs("div", {
7596
+ style: planHeaderStart,
7597
+ children: [reviews.length < 2 ? /* @__PURE__ */ jsx("span", {
7598
+ style: tasksHeading,
7599
+ children: t("planPanelTitle")
7600
+ }) : /* @__PURE__ */ jsxs("div", {
7601
+ className: "dsh-claude-plan-picker-root",
7602
+ ref: picker,
7603
+ children: [/* @__PURE__ */ jsxs("button", {
7604
+ type: "button",
7605
+ className: "dsh-claude-plan-picker",
7606
+ "aria-expanded": open,
7607
+ "aria-haspopup": "listbox",
7608
+ onClick: () => {
7609
+ setOpen((value) => !value);
7610
+ },
7611
+ children: [
7612
+ /* @__PURE__ */ jsx("span", {
7613
+ style: tasksHeading,
7614
+ children: t("planPanelTitle")
7615
+ }),
7616
+ /* @__PURE__ */ jsx("span", {
7617
+ style: planCount,
7618
+ children: t("planNth", {
7619
+ index: index + 1,
7620
+ total: reviews.length
7621
+ })
7622
+ }),
7623
+ /* @__PURE__ */ jsx(ChevronDownIcon, {})
7624
+ ]
7625
+ }), !open ? null : /* @__PURE__ */ jsx("div", {
7626
+ className: "dsh-claude-plan-picker-card",
7627
+ role: "listbox",
7628
+ "aria-label": t("planHistory"),
7629
+ children: [...reviews].reverse().map((item, offset) => /* @__PURE__ */ jsxs("button", {
7630
+ type: "button",
7631
+ role: "option",
7632
+ className: "dsh-claude-plan-picker-item",
7633
+ "aria-current": item.toolUseId === review?.toolUseId,
7634
+ "aria-selected": item.toolUseId === review?.toolUseId,
7635
+ onClick: () => {
7636
+ setChosen(item.toolUseId);
7637
+ setOpen(false);
7638
+ },
7639
+ children: [
7640
+ /* @__PURE__ */ jsx("span", {
7641
+ className: "dsh-claude-plan-picker-ordinal",
7642
+ children: reviews.length - offset
7643
+ }),
7644
+ /* @__PURE__ */ jsx("span", {
7645
+ className: "dsh-claude-plan-picker-title",
7646
+ children: planTitle(item.plan)
7647
+ }),
7648
+ /* @__PURE__ */ jsx("span", {
7649
+ style: badge(item.state),
7650
+ children: t(STATE_LABEL[item.state])
7651
+ })
7652
+ ]
7653
+ }, item.toolUseId))
7654
+ })]
7655
+ }), review === void 0 ? null : /* @__PURE__ */ jsx("span", {
7656
+ style: badge(review.state),
7657
+ children: t(STATE_LABEL[review.state])
7658
+ })]
7659
+ }), /* @__PURE__ */ jsxs("div", {
7660
+ style: planHeaderEnd,
7661
+ children: [/* @__PURE__ */ jsx("button", {
7662
+ type: "button",
7663
+ className: panelIconButtonClass,
7664
+ "aria-label": maximized ? t("planRestore") : t("planMaximize"),
7665
+ onClick: toggleMaximized,
7666
+ children: maximized ? /* @__PURE__ */ jsx(RestorePanelIcon$1, {}) : /* @__PURE__ */ jsx(IconFullscreenOutline16, {})
7667
+ }), /* @__PURE__ */ jsx("button", {
7668
+ type: "button",
7669
+ className: panelIconButtonClass,
7670
+ "aria-label": t("planClose"),
7671
+ onClick: closeDetails,
7672
+ children: /* @__PURE__ */ jsx(IconCloseOutline16, {})
7673
+ })]
7674
+ })]
7675
+ }),
7676
+ /* @__PURE__ */ jsx("div", {
7677
+ style: tasksBody,
7678
+ ref: body,
7679
+ children: review === void 0 ? /* @__PURE__ */ jsx("p", {
7680
+ style: tasksGroupEmpty,
7681
+ children: t("planEmpty")
7682
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [review.state === "pending" ? /* @__PURE__ */ jsx("p", {
7683
+ style: planHint,
7684
+ children: t("planPendingHint")
7685
+ }) : null, /* @__PURE__ */ jsx(ClaudeMarkdown, {
7686
+ text: review.plan,
7687
+ labels: markdownLabels
7688
+ }, review.toolUseId)] })
7689
+ }),
7690
+ review?.state !== "pending" ? null : /* @__PURE__ */ jsxs("div", {
7691
+ style: planComposer,
7692
+ children: [
7693
+ notes.length === 0 ? null : /* @__PURE__ */ jsx("ul", {
7694
+ style: planNoteList,
7695
+ children: notes.map((note, index) => /* @__PURE__ */ jsxs("li", {
7696
+ style: planNote,
7697
+ children: [
7698
+ note.quote === void 0 ? null : /* @__PURE__ */ jsx("p", {
7699
+ style: planNoteQuote,
7700
+ children: note.quote
7701
+ }),
7702
+ /* @__PURE__ */ jsx("p", {
7703
+ style: planNoteText,
7704
+ children: note.text
7705
+ }),
7706
+ /* @__PURE__ */ jsx("button", {
7707
+ type: "button",
7708
+ style: planNoteRemove,
7709
+ "aria-label": t("planNoteRemove"),
7710
+ onClick: () => {
7711
+ setNotes((current) => current.filter((_, at) => at !== index));
7712
+ },
7713
+ children: "×"
7714
+ })
7715
+ ]
7716
+ }, index))
7717
+ }),
7718
+ quote === void 0 ? null : /* @__PURE__ */ jsxs("div", {
7719
+ style: planQuoteChip,
7720
+ children: [/* @__PURE__ */ jsx("span", {
7721
+ style: planNoteQuote,
7722
+ children: quote
7723
+ }), /* @__PURE__ */ jsx("button", {
7724
+ type: "button",
7725
+ style: planNoteRemove,
7726
+ "aria-label": t("planQuoteClear"),
7727
+ onClick: () => {
7728
+ setQuote(void 0);
7729
+ },
7730
+ children: "×"
7731
+ })]
7732
+ }),
7733
+ /* @__PURE__ */ jsx("textarea", {
7734
+ ref: composer,
7735
+ value: draft,
7736
+ placeholder: t(quote === void 0 ? "planNotePlaceholder" : "planNoteQuotedPlaceholder"),
7737
+ style: planComposerInput,
7738
+ onChange: (event) => {
7739
+ setDraft(event.currentTarget.value);
7740
+ },
7741
+ onKeyDown: (event) => {
7742
+ if (event.key !== "Enter" || event.nativeEvent.isComposing) return;
7743
+ event.preventDefault();
7744
+ if (event.metaKey || event.ctrlKey) send();
7745
+ else addNote();
7746
+ }
7747
+ }),
7748
+ failure === void 0 ? null : /* @__PURE__ */ jsx("p", {
7749
+ role: "alert",
7750
+ style: planComposerError,
7751
+ children: t(failure)
7752
+ }),
7753
+ /* @__PURE__ */ jsxs("div", {
7754
+ style: planComposerActions,
7755
+ children: [/* @__PURE__ */ jsx("span", {
7756
+ style: planComposerHint,
7757
+ children: t("planNoteHint")
7758
+ }), /* @__PURE__ */ jsx("button", {
7759
+ type: "button",
7760
+ style: {
7761
+ ...askButton,
7762
+ ...askPrimaryButton
7763
+ },
7764
+ disabled: sending || notes.length === 0 && draft.trim().length === 0,
7765
+ onClick: send,
7766
+ children: t(sending ? "planSending" : "planSendForChanges")
7767
+ })]
7768
+ })
7769
+ ]
7770
+ })
7771
+ ]
7772
+ });
7773
+ }
7774
+ //#endregion
7775
+ //#region src/client/ClaudePlanHeaderAction.tsx
7776
+ /** Same resting-quiet treatment as the diff action next to it, plus a dot for
7777
+ * a plan still waiting on its approval dialog. */
7778
+ const ACTION_CSS$1 = [
7779
+ ".dsh-claude-header-plan{position:relative;flex:none;display:inline-flex;align-items:center;justify-content:center;",
7780
+ "width:32px;height:32px;padding:0;border:0;border-radius:9px;background:transparent;",
7781
+ "color:var(--dsw-alias-label-secondary);cursor:pointer;",
7782
+ "transition:background .12s ease,color .12s ease}",
7783
+ ".dsh-claude-header-plan:hover,.dsh-claude-header-plan:focus-visible,.dsh-claude-header-plan[aria-pressed=\"true\"]{",
7784
+ "background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
7785
+ ".dsh-claude-header-plan:active{background:var(--dsw-alias-interactive-bg-active)}",
7786
+ ".dsh-claude-header-plan:focus-visible{outline:none}",
7787
+ ".dsh-claude-header-plan>*{flex:none}",
7788
+ ".dsh-claude-header-plan>svg.dsh-claude-header-plan-glyph{",
7789
+ "width:18px;height:18px;display:block;overflow:visible;opacity:1;",
7790
+ "fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}",
7791
+ ".dsh-claude-header-plan[data-pending]::after{content:\"\";position:absolute;top:6px;right:6px;",
7792
+ "width:6px;height:6px;border-radius:999px;background:var(--dsw-alias-state-warning-primary,#e2a03f);",
7793
+ "box-shadow:0 0 0 2px var(--dsw-alias-bg-base)}"
7794
+ ].join("");
7795
+ let cssInjected$3 = false;
7796
+ function ensureCss$3() {
7797
+ if (cssInjected$3 || typeof document === "undefined") return;
7798
+ cssInjected$3 = true;
7799
+ const element = document.createElement("style");
7800
+ element.dataset.dshClaudeHeaderPlan = "";
7801
+ element.textContent = ACTION_CSS$1;
7802
+ document.head.appendChild(element);
7803
+ }
7804
+ /** A written page, not a checklist: a plan is a document to read, and ticks
7805
+ * next to lines are what the task board means. Drawn inline for the same
7806
+ * reason the diff glyph is — the primitives set ships neither. */
7807
+ function PlanGlyph() {
7808
+ return /* @__PURE__ */ jsxs("svg", {
7809
+ className: "dsh-claude-header-plan-glyph",
7810
+ width: "18",
7811
+ height: "18",
7812
+ viewBox: "0 0 18 18",
7813
+ fill: "none",
7814
+ "aria-hidden": "true",
7815
+ focusable: "false",
7816
+ children: [
7817
+ /* @__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" }),
7818
+ /* @__PURE__ */ jsx("path", { d: "M10.5 2.25V6.5h4.25" }),
7819
+ /* @__PURE__ */ jsx("path", { d: "M6.25 9h5.5M6.25 12h3.5" })
7820
+ ]
7821
+ });
7822
+ }
7823
+ function ClaudePlanHeaderAction({ t, sessionId, togglePlan, planOpen, useClaudeProjection }) {
7824
+ const owned = useClaudeProjection((projection) => projection.owned);
7825
+ const review = parsePlanReviewKey(useClaudeProjection((projection) => planReviewKey(projection.activities)));
7826
+ const open = useSyncExternalStore(planOpen.subscribe, planOpen.getSnapshot, planOpen.getSnapshot);
7827
+ const opened = useRef();
7828
+ useEffect(() => {
7829
+ if (review?.state !== "pending" || opened.current === review.toolUseId) return;
7830
+ opened.current = review.toolUseId;
7831
+ if (!open) togglePlan();
7832
+ });
7833
+ if (!owned || review === void 0) return null;
7834
+ ensureCss$3();
7835
+ const label = t(open ? "planClose" : "planOpen");
7836
+ return /* @__PURE__ */ jsx(Tooltip, {
7837
+ label,
7838
+ side: "bottom",
7839
+ delayMs: 250,
7840
+ children: /* @__PURE__ */ jsx("button", {
7841
+ type: "button",
7842
+ className: "dsh-claude-header-plan",
7843
+ "aria-label": label,
7844
+ "aria-pressed": open,
7845
+ "data-pending": review.state === "pending" || void 0,
7846
+ "data-session": sessionId,
7847
+ onClick: togglePlan,
7848
+ children: /* @__PURE__ */ jsx(PlanGlyph, {})
7849
+ })
7850
+ });
7851
+ }
7852
+ //#endregion
6470
7853
  //#region src/client/repository-action-api.ts
6471
7854
  var RepositoryActionClientError = class extends Error {
6472
7855
  code;
@@ -6478,7 +7861,7 @@ window.__ModuleLoader__.load({
6478
7861
  if (commit !== void 0) this.commit = commit;
6479
7862
  }
6480
7863
  };
6481
- function record$4(value) {
7864
+ function record$3(value) {
6482
7865
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
6483
7866
  }
6484
7867
  /** The dialog branches on `code`, so a route refusal keeps arriving as this
@@ -6491,20 +7874,20 @@ window.__ModuleLoader__.load({
6491
7874
  return error instanceof PluginRequestError ? new RepositoryActionClientError(error.message, error.code) : error;
6492
7875
  }
6493
7876
  function preview(value) {
6494
- const input = record$4(value);
7877
+ const input = record$3(value);
6495
7878
  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
7879
  for (const file of input.files) {
6497
- const item = record$4(file);
7880
+ const item = record$3(file);
6498
7881
  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
7882
  }
6500
7883
  for (const commit of input.unpushedCommits) {
6501
- const item = record$4(commit);
7884
+ const item = record$3(commit);
6502
7885
  if (item === void 0 || typeof item.hash !== "string" || typeof item.subject !== "string") throw new Error("Invalid repository action commit.");
6503
7886
  }
6504
7887
  return input;
6505
7888
  }
6506
7889
  function result(value) {
6507
- const input = record$4(value);
7890
+ const input = record$3(value);
6508
7891
  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
7892
  return input;
6510
7893
  }
@@ -6518,7 +7901,7 @@ window.__ModuleLoader__.load({
6518
7901
  }
6519
7902
  async function generateCommitMessage(sessionId, fingerprint, signal) {
6520
7903
  try {
6521
- const value = record$4(await pluginWrite(`${CLAUDE_REPOSITORY_ACTION_PATH}/message`, "remote", signal, {
7904
+ const value = record$3(await pluginWrite(`${CLAUDE_REPOSITORY_ACTION_PATH}/message`, "remote", signal, {
6522
7905
  query: { sessionId },
6523
7906
  json: { fingerprint }
6524
7907
  }));
@@ -6581,16 +7964,16 @@ window.__ModuleLoader__.load({
6581
7964
  /** The draft only has to describe the work; the host truncates it again before
6582
7965
  * summarizing, and the setup route caps the whole body at 16 KiB. */
6583
7966
  const MAX_INTENT_CHARS = 2e3;
6584
- function record$3(value) {
7967
+ function record$2(value) {
6585
7968
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
6586
7969
  }
6587
7970
  function setupResult(value) {
6588
- const item = record$3(value);
7971
+ const item = record$2(value);
6589
7972
  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
7973
  return item;
6591
7974
  }
6592
7975
  function parseRepositorySetupEvent(line, onProgress) {
6593
- const event = record$3(JSON.parse(line));
7976
+ const event = record$2(JSON.parse(line));
6594
7977
  if (event?.type === "progress" && typeof event.stage === "string" && HOST_STAGES.has(event.stage)) {
6595
7978
  onProgress(event.stage);
6596
7979
  return;
@@ -6680,222 +8063,33 @@ window.__ModuleLoader__.load({
6680
8063
  cwd,
6681
8064
  path,
6682
8065
  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;
8066
+ to: String(to)
8067
+ } });
8068
+ if (!Array.isArray(body.lines) || typeof body.total !== "number") throw new Error("Invalid repository file response.");
6891
8069
  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
- }
8070
+ lines: body.lines.map(String),
8071
+ total: body.total
6897
8072
  };
6898
8073
  }
8074
+ async function loadRepositoryStatusFor(cwd, signal) {
8075
+ const body = await pluginRead(CLAUDE_REPOSITORY_STATUS_PATH, "git", signal, { query: { cwd } });
8076
+ if (typeof body.status !== "string" || typeof body.cwd !== "string") throw new Error("Invalid repository status response.");
8077
+ return body;
8078
+ }
8079
+ //#endregion
8080
+ //#region src/client/relative-age.ts
8081
+ /** Compact age of an ISO timestamp, in the shape the panels already use
8082
+ * ("<1h", "4h", "3d", "2mo"). `now` is a parameter so callers that re-render
8083
+ * on a clock — and tests — stay deterministic. */
8084
+ function relativeAge(value, now = Date.now()) {
8085
+ if (value === void 0) return void 0;
8086
+ const elapsedHours = Math.max(0, Math.floor((now - Date.parse(value)) / 36e5));
8087
+ if (!Number.isFinite(elapsedHours)) return void 0;
8088
+ if (elapsedHours < 1) return "<1h";
8089
+ if (elapsedHours < 24) return `${elapsedHours}h`;
8090
+ const days = Math.floor(elapsedHours / 24);
8091
+ return days < 30 ? `${days}d` : `${Math.floor(days / 30)}mo`;
8092
+ }
6899
8093
  //#endregion
6900
8094
  //#region src/client/boot-check.ts
6901
8095
  /** Boot-time assertions against the running Host.
@@ -7088,7 +8282,7 @@ window.__ModuleLoader__.load({
7088
8282
  ]
7089
8283
  });
7090
8284
  }
7091
- function repositoryName$1(remote) {
8285
+ function repositoryName(remote) {
7092
8286
  return remote?.split("/").at(-1);
7093
8287
  }
7094
8288
  function PullRequestHoverCard({ repository, t }) {
@@ -7116,7 +8310,7 @@ window.__ModuleLoader__.load({
7116
8310
  /* @__PURE__ */ jsxs("span", {
7117
8311
  style: repositoryPrHoverRepo,
7118
8312
  children: [
7119
- repositoryName$1(repository.remote),
8313
+ repositoryName(repository.remote),
7120
8314
  " #",
7121
8315
  pullRequest.number,
7122
8316
  pullRequest.baseBranch === void 0 ? "" : ` → ${pullRequest.baseBranch}`
@@ -7894,7 +9088,7 @@ window.__ModuleLoader__.load({
7894
9088
  }),
7895
9089
  repository.remote === void 0 ? null : /* @__PURE__ */ jsx("span", {
7896
9090
  style: repositoryRemote,
7897
- children: repositoryName$1(repository.remote)
9091
+ children: repositoryName(repository.remote)
7898
9092
  }),
7899
9093
  /* @__PURE__ */ jsx(Tooltip, {
7900
9094
  label: branch,
@@ -13939,7 +15133,7 @@ window.__ModuleLoader__.load({
13939
15133
  ] });
13940
15134
  }
13941
15135
  //#endregion
13942
- //#region src/client/ClaudeDiffOverlay.tsx
15136
+ //#region src/client/ClaudePanelOverlay.tsx
13943
15137
  const useClientLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
13944
15138
  function shouldRestoreFromEscape(event, root = document) {
13945
15139
  return event.key === "Escape" && root.querySelector("[role=\"dialog\"][aria-modal=\"true\"]") === null;
@@ -13987,7 +15181,7 @@ window.__ModuleLoader__.load({
13987
15181
  window.removeEventListener("resize", update);
13988
15182
  };
13989
15183
  }
13990
- function ClaudeDiffOverlay({ children, onRestore }) {
15184
+ function ClaudePanelOverlay({ children, onRestore }) {
13991
15185
  const ref = useRef(null);
13992
15186
  const [bounds, setBounds] = useState();
13993
15187
  useClientLayoutEffect(() => {
@@ -14006,7 +15200,7 @@ window.__ModuleLoader__.load({
14006
15200
  }, [onRestore]);
14007
15201
  return /* @__PURE__ */ jsx("div", {
14008
15202
  ref,
14009
- "data-dsh-claude-diff-overlay": true,
15203
+ "data-dsh-claude-panel-overlay": true,
14010
15204
  style: {
14011
15205
  position: "absolute",
14012
15206
  left: bounds?.left ?? 0,
@@ -14147,232 +15341,38 @@ window.__ModuleLoader__.load({
14147
15341
  if (event.key === "Escape") setEditing(void 0);
14148
15342
  else if (event.key === "Enter" && !event.nativeEvent.isComposing) {
14149
15343
  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]);
14290
- 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
- ]
15344
+ saveEdit();
15345
+ }
15346
+ }
15347
+ }) : /* @__PURE__ */ jsx("span", {
15348
+ style: queuePreview,
15349
+ children: row.preview
15350
+ }),
15351
+ mutable ? /* @__PURE__ */ jsx("span", {
15352
+ style: queueActions,
15353
+ children: editing?.id === row.id ? /* @__PURE__ */ jsxs(Fragment$1, { children: [action(t("queueSave"), /* @__PURE__ */ jsx(IconCheckOutline16, { size: 14 }), () => {
15354
+ saveEdit();
15355
+ }, busy !== void 0 || editing.text.trim() === ""), action(t("queueCancelEdit"), /* @__PURE__ */ jsx(IconCloseOutline16, { size: 14 }), () => {
15356
+ setEditing(void 0);
15357
+ }, busy !== void 0)] }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
15358
+ action(t("queueEdit"), /* @__PURE__ */ jsx(IconEditOutline16, { size: 14 }), () => {
15359
+ if (row.text !== null) setEditing({
15360
+ id: row.id,
15361
+ text: row.text
15362
+ });
15363
+ }, busy !== void 0 || row.text === null, row.text === null ? t("queueEditUnsupported") : void 0),
15364
+ action(t("queueRemove"), /* @__PURE__ */ jsx(IconTrashOutline16, { size: 14 }), () => {
15365
+ apply(row.id, { kind: "remove" }, t("queueRemoveFailed"));
15366
+ }, busy !== void 0),
15367
+ action(t("queueSteer"), /* @__PURE__ */ jsx(IconSendOutline14, {}), () => {
15368
+ apply(row.id, { kind: "steer" }, t("queueSteerFailed"));
15369
+ }, busy !== void 0 || !running, running ? void 0 : t("queueSteerUnavailable"))
15370
+ ] })
15371
+ }) : null
15372
+ ]
15373
+ }, row.id)) : null
15374
+ })]
15375
+ })]
14376
15376
  });
14377
15377
  }
14378
15378
  //#endregion
@@ -15112,13 +16112,19 @@ window.__ModuleLoader__.load({
15112
16112
  //#endregion
15113
16113
  //#region src/client/rewind-api.ts
15114
16114
  /** 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) {
16115
+ * this session's transcript and Claude resumes before that turn.
16116
+ *
16117
+ * With `restoreFiles`, the checkout is put back to the tree that turn was
16118
+ * admitted against. The answer reports whether that half actually landed:
16119
+ * a turn from before this session captured trees, or a tree git has since
16120
+ * collected, still rewinds the conversation and leaves the files alone. */
16121
+ async function rewindSession(sessionId, seq, restoreFiles = false) {
15117
16122
  try {
15118
- await pluginWrite(CLAUDE_REWIND_PATH, "fast", void 0, { json: {
16123
+ return { filesRestored: (await pluginWrite(CLAUDE_REWIND_PATH, "fast", void 0, { json: {
15119
16124
  sessionId,
15120
- seq
15121
- } });
16125
+ seq,
16126
+ restoreFiles
16127
+ } }))?.filesRestored === true };
15122
16128
  } catch (error) {
15123
16129
  if (!(error instanceof PluginRequestError)) throw error;
15124
16130
  throw new Error(error.code ?? error.reason);
@@ -15187,6 +16193,8 @@ window.__ModuleLoader__.load({
15187
16193
  const [target, setTarget] = useState();
15188
16194
  const [submitting, setSubmitting] = useState(false);
15189
16195
  const [error, setError] = useState();
16196
+ const [restoreFiles, setRestoreFiles] = useState(true);
16197
+ const { toast, report } = useActionToast();
15190
16198
  const ranges = projection.rewind?.ranges ?? EMPTY_RANGES;
15191
16199
  const owned = projection.owned;
15192
16200
  const unavailable = snapshot.running;
@@ -15254,6 +16262,7 @@ window.__ModuleLoader__.load({
15254
16262
  setTarget(void 0);
15255
16263
  setSubmitting(false);
15256
16264
  setError(void 0);
16265
+ setRestoreFiles(true);
15257
16266
  }, [sessionId]);
15258
16267
  if (sessionId === void 0 || !owned) return /* @__PURE__ */ jsx("span", {
15259
16268
  "data-dsh-claude-rewind-armed": "armed",
@@ -15268,10 +16277,11 @@ window.__ModuleLoader__.load({
15268
16277
  if (target === void 0 || submitting) return;
15269
16278
  setSubmitting(true);
15270
16279
  setError(void 0);
15271
- rewindSession(sessionId, target.seq).then(() => {
16280
+ rewindSession(sessionId, target.seq, restoreFiles).then(({ filesRestored }) => {
15272
16281
  setSubmitting(false);
15273
16282
  setTarget(void 0);
15274
16283
  if (target.text !== "") setDraft?.(sessionId, target.text);
16284
+ if (restoreFiles && !filesRestored) report(t("rewindFilesUnavailable"));
15275
16285
  }, (reason) => {
15276
16286
  setSubmitting(false);
15277
16287
  const code = reason instanceof Error && reason.message !== "" ? reason.message : "unknown";
@@ -15279,6 +16289,7 @@ window.__ModuleLoader__.load({
15279
16289
  });
15280
16290
  };
15281
16291
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [
16292
+ toast,
15282
16293
  /* @__PURE__ */ jsx("style", {
15283
16294
  "data-dsh-claude-rewind-styles": true,
15284
16295
  children: `${rewindActionCss}${rewindHiddenCss(hiddenKeys)}`
@@ -15343,9 +16354,20 @@ window.__ModuleLoader__.load({
15343
16354
  style: rewindModalMessage,
15344
16355
  children: target.text.slice(0, 2e3)
15345
16356
  }),
16357
+ /* @__PURE__ */ jsxs("label", {
16358
+ style: diffModalCheckbox,
16359
+ children: [/* @__PURE__ */ jsx("input", {
16360
+ type: "checkbox",
16361
+ checked: restoreFiles,
16362
+ disabled: submitting,
16363
+ onChange: (event) => {
16364
+ setRestoreFiles(event.currentTarget.checked);
16365
+ }
16366
+ }), t("rewindRestoreFiles")]
16367
+ }),
15346
16368
  /* @__PURE__ */ jsx("p", {
15347
16369
  style: diffModalStatus,
15348
- children: t("rewindHint")
16370
+ children: restoreFiles ? t("rewindRestoreFilesHint") : t("rewindHint")
15349
16371
  }),
15350
16372
  error === void 0 ? null : /* @__PURE__ */ jsx("p", {
15351
16373
  style: diffModalError,
@@ -16674,19 +17696,19 @@ window.__ModuleLoader__.load({
16674
17696
  });
16675
17697
  }
16676
17698
  //#endregion
16677
- //#region src/client/diff-open-store.ts
16678
- var DiffOpenStore = class {
17699
+ //#region src/client/panel-open-store.ts
17700
+ var PanelOpenStore = class {
16679
17701
  #sessionId;
16680
17702
  #listeners = /* @__PURE__ */ new Set();
16681
- /** Mark the diff panel open for one session, replacing any previous holder. */
17703
+ /** Mark this panel open for one session, replacing any previous holder. */
16682
17704
  open(sessionId) {
16683
17705
  this.#set(sessionId);
16684
17706
  }
16685
- /** Mark the diff panel closed. */
17707
+ /** Mark this panel closed. */
16686
17708
  close() {
16687
17709
  this.#set(void 0);
16688
17710
  }
16689
- /** Whether the diff panel is currently open for this session. */
17711
+ /** Whether this panel is currently open for this session. */
16690
17712
  isOpen(sessionId) {
16691
17713
  return this.#sessionId === sessionId;
16692
17714
  }
@@ -16791,6 +17813,428 @@ window.__ModuleLoader__.load({
16791
17813
  }
16792
17814
  }
16793
17815
  //#endregion
17816
+ //#region src/client/prompt-api.ts
17817
+ /** Prompt files live in one global directory, so one module-level cache serves
17818
+ * every session. The TTL is what lets a file added outside DSH appear without
17819
+ * a reload, while a burst of keystrokes through the menu still costs one
17820
+ * directory scan. */
17821
+ const TTL_MS = 5e3;
17822
+ let cached;
17823
+ function invalidateClaudePrompts() {
17824
+ cached = void 0;
17825
+ }
17826
+ /** The user's prompt snippets, at most one read per TTL window. */
17827
+ async function claudePrompts() {
17828
+ const at = Date.now();
17829
+ if (cached !== void 0 && at - cached.at < TTL_MS) return await cached.prompts;
17830
+ const prompts = pluginRead(CLAUDE_PROMPTS_PATH, "fast").then((payload) => payload.prompts).catch(() => {
17831
+ invalidateClaudePrompts();
17832
+ return [];
17833
+ });
17834
+ cached = {
17835
+ at,
17836
+ prompts
17837
+ };
17838
+ return await prompts;
17839
+ }
17840
+ /** Save one snippet, answering where it landed. Rejects with a
17841
+ * `PluginRequestError` whose `code` is `name-taken` when the file already
17842
+ * exists; nothing is overwritten. */
17843
+ async function saveClaudePrompt(name, body) {
17844
+ const saved = await pluginWrite(CLAUDE_PROMPTS_PATH, "fast", void 0, { json: {
17845
+ name,
17846
+ body
17847
+ } });
17848
+ invalidateClaudePrompts();
17849
+ return saved.prompt;
17850
+ }
17851
+ /** Ask the host to name a draft with Claude's cheapest model. Answers
17852
+ * undefined whenever no name could be had — the caller already holds the
17853
+ * locally derived one, so a failure here is never worth reporting. */
17854
+ async function suggestClaudePromptName(draft, cancel) {
17855
+ try {
17856
+ return (await pluginWrite(CLAUDE_PROMPT_NAME_PATH, "git", cancel, { json: { draft } })).name;
17857
+ } catch {
17858
+ return;
17859
+ }
17860
+ }
17861
+ /** Rewrite a draft into something an agent can act on. Unlike the name
17862
+ * suggestion this one throws: the user asked for it and is waiting, so a
17863
+ * failure is theirs to see rather than ours to swallow. */
17864
+ async function refineClaudePrompt(draft, cancel) {
17865
+ return (await pluginWrite(CLAUDE_PROMPT_REFINE_PATH, "git", cancel, { json: { draft } })).text;
17866
+ }
17867
+ /** Characters the host's `PROMPT_NAME` guard rejects; scrubbed rather than
17868
+ * re-implemented here, so a drift in the guard cannot let a bad name through
17869
+ * (the host still validates, and answers `invalid-name`). */
17870
+ const FOREIGN_NAME_CHARS = /[^\p{L}\p{M}\p{N} ._()\[\]-]/gu;
17871
+ const MAX_NAME_CHARS = 40;
17872
+ function stamp(now) {
17873
+ const pad = (value) => String(value).padStart(2, "0");
17874
+ return `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}`;
17875
+ }
17876
+ /** The file name to offer for a draft: its opening line, scrubbed to what a
17877
+ * file name may hold. A draft that opens with punctuation or an emoji leaves
17878
+ * nothing usable, so those fall back to a timestamp the user can rename. */
17879
+ function defaultPromptName(draft, now = /* @__PURE__ */ new Date()) {
17880
+ const collapsed = (draft.split("\n").map((line) => line.trim()).find((line) => line.length > 0) ?? "").replace(FOREIGN_NAME_CHARS, " ").replace(/\s+/gu, " ").trim();
17881
+ const cut = collapsed.slice(0, MAX_NAME_CHARS);
17882
+ const boundary = collapsed.length > MAX_NAME_CHARS ? cut.search(/[\s\-_][^\s\-_]*$/u) : -1;
17883
+ const scrubbed = (boundary > 0 ? cut.slice(0, boundary) : cut).trim();
17884
+ return /^[\p{L}\p{N}]/u.test(scrubbed) ? scrubbed : `prompt-${stamp(now)}`;
17885
+ }
17886
+ //#endregion
17887
+ //#region src/client/ClaudePromptSaveAction.tsx
17888
+ const CARD_GAP = 8;
17889
+ const CARD_MARGIN = 12;
17890
+ /**
17891
+ * Place the card above its trigger, clamped into the viewport, and dismiss it
17892
+ * on a pointer that lands outside both.
17893
+ *
17894
+ * The primitives ship `useAnchoredPosition` and `useDismissOnOutsidePointer`
17895
+ * for exactly this, and both are used here through their published types —
17896
+ * but the Host's build of that package predates the `side` option of one and
17897
+ * the portal argument of the other. Neither omission is visible to `tsc`,
17898
+ * which reads this checkout's newer copy, and both are silent at runtime: the
17899
+ * card hangs below a composer pinned to the bottom of the window, and every
17900
+ * pointer landing in the naming field counts as outside and closes it. Owning
17901
+ * twenty lines beats behaviour that depends on which Desktop the plugin was
17902
+ * installed into.
17903
+ */
17904
+ function useAnchoredCard(open, anchor, card, onDismiss) {
17905
+ const [position, setPosition] = useState();
17906
+ useLayoutEffect(() => {
17907
+ if (!open) {
17908
+ setPosition(void 0);
17909
+ return;
17910
+ }
17911
+ const place = () => {
17912
+ const trigger = anchor.current?.getBoundingClientRect();
17913
+ const panel = card.current?.getBoundingClientRect();
17914
+ if (trigger === void 0 || panel === void 0) return;
17915
+ const above = trigger.top - CARD_GAP - panel.height;
17916
+ setPosition({
17917
+ left: Math.max(CARD_MARGIN, Math.min(trigger.left, window.innerWidth - panel.width - CARD_MARGIN)),
17918
+ top: above >= CARD_MARGIN ? above : Math.min(trigger.bottom + CARD_GAP, window.innerHeight - panel.height - CARD_MARGIN)
17919
+ });
17920
+ };
17921
+ place();
17922
+ window.addEventListener("resize", place);
17923
+ window.addEventListener("scroll", place, true);
17924
+ return () => {
17925
+ window.removeEventListener("resize", place);
17926
+ window.removeEventListener("scroll", place, true);
17927
+ };
17928
+ }, [
17929
+ open,
17930
+ anchor,
17931
+ card
17932
+ ]);
17933
+ useEffect(() => {
17934
+ if (!open) return void 0;
17935
+ const dismiss = (event) => {
17936
+ const target = event.target;
17937
+ if (!(target instanceof Node)) return;
17938
+ if (anchor.current?.contains(target) === true || card.current?.contains(target) === true) return;
17939
+ onDismiss();
17940
+ };
17941
+ document.addEventListener("pointerdown", dismiss);
17942
+ return () => {
17943
+ document.removeEventListener("pointerdown", dismiss);
17944
+ };
17945
+ }, [
17946
+ open,
17947
+ anchor,
17948
+ card,
17949
+ onDismiss
17950
+ ]);
17951
+ return position;
17952
+ }
17953
+ /**
17954
+ * Keep the draft you just wrote, from the composer's own tool row.
17955
+ *
17956
+ * It sits beside the attach and access controls rather than in a row of its
17957
+ * own: a band above the composer moves the repository bar and the composer
17958
+ * itself every time a draft appears, and this is a once-in-a-while action that
17959
+ * has not earned that. The naming card is portaled and anchored, so opening it
17960
+ * displaces nothing either.
17961
+ *
17962
+ * The field opens on a name derived from the draft's first line, which costs
17963
+ * nothing and is there instantly, and a Claude-written name replaces it when
17964
+ * one arrives. That ordering is the whole naming design: the suggestion is an
17965
+ * improvement on a working answer, never something the user waits for.
17966
+ */
17967
+ function ClaudePromptSaveAction({ t, useClaudeProjection, input, savePrompt = saveClaudePrompt, suggestName = suggestClaudePromptName }) {
17968
+ const owned = useClaudeProjection((projection) => projection.owned);
17969
+ const anchor = useRef(null);
17970
+ const panelRef = useRef(null);
17971
+ const suggestion = useRef(void 0);
17972
+ const [panel, setPanel] = useState();
17973
+ const [saving, setSaving] = useState(false);
17974
+ const close = useCallback(() => {
17975
+ suggestion.current?.abort();
17976
+ suggestion.current = void 0;
17977
+ setPanel(void 0);
17978
+ }, []);
17979
+ const position = useAnchoredCard(panel !== void 0, anchor, panelRef, close);
17980
+ useEffect(() => () => {
17981
+ suggestion.current?.abort();
17982
+ }, []);
17983
+ if (!owned) return null;
17984
+ const draft = input?.draft ?? "";
17985
+ const label = t("promptSave");
17986
+ const open = () => {
17987
+ setPanel({
17988
+ kind: "naming",
17989
+ name: defaultPromptName(draft),
17990
+ touched: false,
17991
+ suggesting: true
17992
+ });
17993
+ const attempt = new AbortController();
17994
+ suggestion.current = attempt;
17995
+ suggestName(draft, attempt.signal).then((suggested) => {
17996
+ if (attempt.signal.aborted) return;
17997
+ setPanel((current) => current?.kind !== "naming" ? current : {
17998
+ ...current,
17999
+ suggesting: false,
18000
+ ...suggested === void 0 || current.touched ? {} : { name: suggested }
18001
+ });
18002
+ });
18003
+ };
18004
+ const save = () => {
18005
+ const name = panel?.kind === "naming" ? panel.name.trim() : "";
18006
+ if (saving || name === "" || draft.trim() === "") return;
18007
+ setSaving(true);
18008
+ savePrompt(name, draft).then((prompt) => {
18009
+ setPanel({
18010
+ kind: "saved",
18011
+ prompt
18012
+ });
18013
+ }, (error) => {
18014
+ setPanel({
18015
+ kind: "naming",
18016
+ name,
18017
+ touched: true,
18018
+ suggesting: false,
18019
+ failure: error instanceof PluginRequestError && error.code === "name-taken" ? t("promptSaveExists") : t("promptSaveFailed", { message: error instanceof Error ? error.message : String(error) })
18020
+ });
18021
+ }).finally(() => {
18022
+ setSaving(false);
18023
+ });
18024
+ };
18025
+ return /* @__PURE__ */ jsxs("span", {
18026
+ ref: anchor,
18027
+ style: {
18028
+ position: "relative",
18029
+ display: "inline-flex"
18030
+ },
18031
+ children: [
18032
+ /* @__PURE__ */ jsx("style", {
18033
+ "data-dsh-claude-prompt-save-styles": true,
18034
+ children: promptSaveTriggerCss
18035
+ }),
18036
+ /* @__PURE__ */ jsx(Tooltip, {
18037
+ label,
18038
+ side: "top",
18039
+ delayMs: 250,
18040
+ disabled: panel !== void 0,
18041
+ children: /* @__PURE__ */ jsx("button", {
18042
+ type: "button",
18043
+ className: promptSaveTriggerClass,
18044
+ "aria-label": label,
18045
+ "aria-haspopup": "dialog",
18046
+ "aria-expanded": panel !== void 0,
18047
+ disabled: draft.trim() === "",
18048
+ onClick: () => {
18049
+ if (panel === void 0) open();
18050
+ else close();
18051
+ },
18052
+ children: /* @__PURE__ */ jsx(IconListPenOutline16, { size: 14 })
18053
+ })
18054
+ }),
18055
+ panel === void 0 || typeof document === "undefined" ? null : createPortal(/* @__PURE__ */ jsx("div", {
18056
+ ref: panelRef,
18057
+ style: {
18058
+ ...promptSaveCard,
18059
+ ...position
18060
+ },
18061
+ role: "dialog",
18062
+ "aria-label": label,
18063
+ children: panel.kind === "saved" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
18064
+ /* @__PURE__ */ jsx("span", { children: t("promptSaved", { name: panel.prompt.name }) }),
18065
+ /* @__PURE__ */ jsx("span", {
18066
+ style: promptSaveLocation,
18067
+ children: panel.prompt.location
18068
+ }),
18069
+ /* @__PURE__ */ jsx("span", {
18070
+ style: promptSaveActions,
18071
+ children: /* @__PURE__ */ jsx(Button, {
18072
+ variant: "primary",
18073
+ size: "sm",
18074
+ onClick: close,
18075
+ children: t("promptSaveDone")
18076
+ })
18077
+ })
18078
+ ] }) : /* @__PURE__ */ jsxs("form", {
18079
+ style: { display: "contents" },
18080
+ onSubmit: (event) => {
18081
+ event.preventDefault();
18082
+ save();
18083
+ },
18084
+ children: [
18085
+ /* @__PURE__ */ jsx("span", {
18086
+ style: promptSaveHeading,
18087
+ children: panel.suggesting ? t("promptSaveNaming") : label
18088
+ }),
18089
+ /* @__PURE__ */ jsx("input", {
18090
+ className: promptSaveFieldClass,
18091
+ "aria-label": t("promptSaveName"),
18092
+ placeholder: t("promptSaveName"),
18093
+ value: panel.name,
18094
+ maxLength: 128,
18095
+ autoFocus: true,
18096
+ disabled: saving,
18097
+ onChange: (event) => setPanel({
18098
+ ...panel,
18099
+ name: event.currentTarget.value,
18100
+ touched: true
18101
+ }),
18102
+ onKeyDown: (event) => {
18103
+ if (event.key === "Escape") close();
18104
+ }
18105
+ }),
18106
+ panel.failure === void 0 ? null : /* @__PURE__ */ jsx("span", {
18107
+ style: promptSaveError,
18108
+ children: panel.failure
18109
+ }),
18110
+ /* @__PURE__ */ jsxs("span", {
18111
+ style: promptSaveActions,
18112
+ children: [/* @__PURE__ */ jsx(Button, {
18113
+ variant: "ghost",
18114
+ size: "sm",
18115
+ type: "button",
18116
+ disabled: saving,
18117
+ onClick: close,
18118
+ children: t("promptSaveCancel")
18119
+ }), /* @__PURE__ */ jsx(Button, {
18120
+ variant: "primary",
18121
+ size: "sm",
18122
+ type: "submit",
18123
+ disabled: saving,
18124
+ children: t("promptSaveConfirm")
18125
+ })]
18126
+ })
18127
+ ]
18128
+ })
18129
+ }), document.body)
18130
+ ]
18131
+ });
18132
+ }
18133
+ //#endregion
18134
+ //#region src/client/ClaudePromptRefineAction.tsx
18135
+ /**
18136
+ * Rewrite the draft in place, from the composer's own tool row.
18137
+ *
18138
+ * The rewrite replaces the draft outright, which is what makes the undo state
18139
+ * load-bearing rather than a nicety: `SessionInput.setDraft` documents itself
18140
+ * as "merged into history so a seed is not an undoable step of its own", so
18141
+ * Ctrl/Cmd+Z does NOT bring the original back. Without somewhere to put it,
18142
+ * one press of this button would silently destroy a draft the user may have
18143
+ * spent minutes on. So the original is held for exactly as long as it is still
18144
+ * recoverable — until the rewrite is edited or sent — and the same button
18145
+ * offers it back over that window.
18146
+ */
18147
+ function ClaudePromptRefineAction({ t, useClaudeProjection, input, replaceDraft, notify, refine = refineClaudePrompt }) {
18148
+ const owned = useClaudeProjection((projection) => projection.owned);
18149
+ const attempt = useRef(void 0);
18150
+ const [busy, setBusy] = useState(false);
18151
+ const [applied, setApplied] = useState();
18152
+ useEffect(() => () => {
18153
+ attempt.current?.abort();
18154
+ }, []);
18155
+ if (!owned || replaceDraft === void 0) return null;
18156
+ const draft = input?.draft ?? "";
18157
+ const undoable = applied !== void 0 && draft === applied.refined;
18158
+ const label = busy ? t("promptRefineBusy") : undoable ? t("promptRefineUndo") : t("promptRefine");
18159
+ const run = () => {
18160
+ if (busy || draft.trim() === "") return;
18161
+ setBusy(true);
18162
+ const running = new AbortController();
18163
+ attempt.current = running;
18164
+ refine(draft, running.signal).then((text) => {
18165
+ if (running.signal.aborted) return;
18166
+ setApplied({
18167
+ original: draft,
18168
+ refined: text
18169
+ });
18170
+ replaceDraft(text);
18171
+ }, (error) => {
18172
+ if (running.signal.aborted) return;
18173
+ notify?.("error", t("promptRefineFailed", { message: error instanceof Error ? error.message : String(error) }));
18174
+ }).finally(() => {
18175
+ setBusy(false);
18176
+ });
18177
+ };
18178
+ const undo = () => {
18179
+ if (applied === void 0) return;
18180
+ replaceDraft(applied.original);
18181
+ setApplied(void 0);
18182
+ };
18183
+ return /* @__PURE__ */ jsx(Tooltip, {
18184
+ label,
18185
+ side: "top",
18186
+ delayMs: 250,
18187
+ children: /* @__PURE__ */ jsxs("button", {
18188
+ type: "button",
18189
+ className: promptSaveTriggerClass,
18190
+ "aria-label": label,
18191
+ disabled: busy || !undoable && draft.trim() === "",
18192
+ onClick: undoable ? undo : run,
18193
+ children: [/* @__PURE__ */ jsx("style", {
18194
+ "data-dsh-claude-prompt-refine-styles": true,
18195
+ children: promptSaveTriggerCss
18196
+ }), busy ? /* @__PURE__ */ jsx(IconLoadingOutline16, {
18197
+ size: 14,
18198
+ className: promptSpinClass
18199
+ }) : undoable ? /* @__PURE__ */ jsx(IconRefreshOutline16, {
18200
+ size: 14,
18201
+ className: promptUndoClass
18202
+ }) : /* @__PURE__ */ jsx(IconSparkle16, { size: 14 })]
18203
+ })
18204
+ });
18205
+ }
18206
+ //#endregion
18207
+ //#region src/client/claude-prompt-source.ts
18208
+ /**
18209
+ * The user's own prompt snippets as a second `/` group.
18210
+ *
18211
+ * Unlike {@link createClaudeCommandSource}, a pick here settles as plain text:
18212
+ * the pipeline replaces the trigger token with the snippet body and leaves the
18213
+ * caret after it, so the draft stays editable and nothing is sent. A snippet is
18214
+ * a half-written message, not a command.
18215
+ */
18216
+ function createClaudePromptSource(groupName, load = claudePrompts) {
18217
+ let known = [];
18218
+ return {
18219
+ trigger: "/",
18220
+ name: groupName,
18221
+ order: 20,
18222
+ async candidates(_session, request) {
18223
+ if (request.position !== "leading") return [];
18224
+ known = await load();
18225
+ const query = request.query.toLocaleLowerCase();
18226
+ return known.filter((prompt) => prompt.name.toLocaleLowerCase().includes(query)).map((prompt) => ({
18227
+ name: prompt.name,
18228
+ description: prompt.description
18229
+ }));
18230
+ },
18231
+ onPick(pick) {
18232
+ const prompt = known.find((item) => item.name === pick.candidate.name);
18233
+ return prompt === void 0 ? void 0 : { text: prompt.body };
18234
+ }
18235
+ };
18236
+ }
18237
+ //#endregion
16794
18238
  //#region src/client/preset-seat-mark.ts
16795
18239
  /** Flags the Host's agent-preset seat while it names the Claude preset.
16796
18240
  *
@@ -17089,7 +18533,7 @@ window.__ModuleLoader__.load({
17089
18533
  worktreeBranchPrefixEffect: "分支前缀修改会应用于之后自动创建的 Worktree 分支;显式指定的完整分支名不会被修改。",
17090
18534
  maxProcessesSetting: "Claude 进程上限",
17091
18535
  idleTimeoutSetting: "闲置回收时长(分钟)",
17092
- maxProcessesEffect: "进程上限在下次启动 Claude 进程时生效。留空或非法值会被拒绝。",
18536
+ maxProcessesEffect: "保存后立即回收超额空闲进程;运行中的进程完成后再收敛。满载时,新会话会等待可用容量。留空或非法值会被拒绝。",
17093
18537
  idleTimeoutEffect: "闲置回收时长在下一次回合结束后生效。留空或非法值会被拒绝。",
17094
18538
  settingHint: "查看说明",
17095
18539
  pluginUpdate: "插件更新",
@@ -17222,10 +18666,45 @@ window.__ModuleLoader__.load({
17222
18666
  yes: "是",
17223
18667
  no: "否",
17224
18668
  diffOpen: "查看分支改动",
18669
+ planPanelTitle: "Plan",
18670
+ planClose: "关闭方案面板",
18671
+ planOpen: "查看方案",
18672
+ planMaximize: "最大化方案面板",
18673
+ planRestore: "还原方案面板",
18674
+ planNth: "{index} / {total}",
18675
+ planHistory: "本会话的方案",
18676
+ planNotePlaceholder: "写下要改的地方,Enter 记一条,⌘/Ctrl+Enter 发送",
18677
+ planNoteQuotedPlaceholder: "针对选中的这段,要改成什么?",
18678
+ planNoteHint: "在正文里选中一段可以引用它",
18679
+ planNoteRemove: "删掉这条意见",
18680
+ planQuoteClear: "取消引用",
18681
+ planSendForChanges: "发送并让 AI 修改",
18682
+ planSending: "发送中…",
18683
+ planSettled: "这个方案已经在审批弹框里被决定了,意见没能发出。",
18684
+ planFeedbackFailed: "意见发送失败,请重试。",
18685
+ planPending: "待审批",
18686
+ planApproved: "已批准",
18687
+ planRejected: "已拒绝",
18688
+ planEmpty: "本会话还没有待查看的方案。",
18689
+ planPendingHint: "在 DSH 的审批弹框里批准或拒绝这个方案。",
17225
18690
  diffClose: "关闭 Diff 面板",
17226
18691
  sessionMenu: "会话菜单",
17227
18692
  sessionMenuOpenIn: "打开方式",
17228
18693
  sessionMenuOpenFailed: "无法打开:{message}",
18694
+ promptSource: "常用提示词",
18695
+ promptSave: "存为常用提示词",
18696
+ promptSaveName: "名字",
18697
+ promptSaveNaming: "正在起名…",
18698
+ promptRefine: "AI 优化提示词",
18699
+ promptRefineBusy: "优化中…",
18700
+ promptRefineUndo: "还原成优化前的内容",
18701
+ promptRefineFailed: "优化失败:{message}",
18702
+ promptSaveConfirm: "保存",
18703
+ promptSaveCancel: "取消",
18704
+ promptSaveDone: "知道了",
18705
+ promptSaved: "已存为「{name}」",
18706
+ promptSaveExists: "已有同名提示词,换个名字。",
18707
+ promptSaveFailed: "保存失败:{message}",
17229
18708
  presetHeaderHint: "当前会话运行的 Agent 预设",
17230
18709
  diffWorkingTree: "分支改动",
17231
18710
  diffFiles: "{count} 个已修改文件",
@@ -17403,6 +18882,17 @@ window.__ModuleLoader__.load({
17403
18882
  rewindFailed: "回退失败({code})。",
17404
18883
  rewindBusy: "这个会话正在运行,请等本轮结束后再回退。",
17405
18884
  rewindStale: "宿主进程里的插件还是旧版本(没有回退接口)。重启 DSH 后再试。",
18885
+ alerts: "会话提醒",
18886
+ alertsOff: "关闭",
18887
+ alertsOn: "开启",
18888
+ alertsEffect: "当另一个 Claude 会话需要你——等待权限确认、等待回答,或者这一轮跑完了——弹出系统通知。正在看的那个会话不会提醒。点击通知会切到对应会话。首次使用时系统会询问通知权限;拒绝后不再提醒。改动立即生效。",
18889
+ alertNeedsPermission: "等待你确认权限",
18890
+ alertNeedsAnswer: "等待你回答",
18891
+ alertTurnFinished: "这一轮跑完了",
18892
+ alertFallbackTitle: "Claude 会话",
18893
+ rewindRestoreFiles: "同时把文件改回这一轮开始前的样子",
18894
+ rewindRestoreFilesHint: "被删除的内容会从这个会话中隐藏,Claude 也会忘记它们;工作区会回到这一轮开始前的状态——这之后新建的文件会被删除,被改动的文件会被还原,被 .gitignore 忽略的文件不受影响。消息原文会放回输入框,方便修改后重新发送。",
18895
+ rewindFilesUnavailable: "对话已回退,但文件没有还原:这一轮没有留下工作区快照。",
17406
18896
  turnUsage: "本回合用量",
17407
18897
  turnUsageTokens: "{count} tok",
17408
18898
  turnUsageCache: "缓存命中 {percent}%",
@@ -17467,7 +18957,7 @@ window.__ModuleLoader__.load({
17467
18957
  worktreeBranchPrefixEffect: "Prefix changes apply to subsequently generated Worktree branches. Explicit full branch names remain unchanged.",
17468
18958
  maxProcessesSetting: "Claude process limit",
17469
18959
  idleTimeoutSetting: "Idle timeout (minutes)",
17470
- maxProcessesEffect: "The process limit applies when the next Claude process is admitted. Blank or invalid values are rejected.",
18960
+ maxProcessesEffect: "Saving immediately reclaims excess idle processes; running processes converge after they finish. New sessions wait for capacity while every process is busy. Blank or invalid values are rejected.",
17471
18961
  idleTimeoutEffect: "The idle timeout applies after the next completed turn. Blank or invalid values are rejected.",
17472
18962
  settingHint: "Show details",
17473
18963
  pluginUpdate: "Plugin updates",
@@ -17600,10 +19090,45 @@ window.__ModuleLoader__.load({
17600
19090
  yes: "Yes",
17601
19091
  no: "No",
17602
19092
  diffOpen: "View branch changes",
19093
+ planPanelTitle: "Plan",
19094
+ planClose: "Close plan panel",
19095
+ planOpen: "View plan",
19096
+ planMaximize: "Maximize plan panel",
19097
+ planRestore: "Restore plan panel",
19098
+ planNth: "{index} / {total}",
19099
+ planHistory: "Plans in this session",
19100
+ planNotePlaceholder: "What should change? Enter files a note, ⌘/Ctrl+Enter sends",
19101
+ planNoteQuotedPlaceholder: "What should change about the selected passage?",
19102
+ planNoteHint: "Select a passage above to quote it",
19103
+ planNoteRemove: "Remove this note",
19104
+ planQuoteClear: "Clear the quote",
19105
+ planSendForChanges: "Send for changes",
19106
+ planSending: "Sending…",
19107
+ planSettled: "This plan was already decided in the approval dialog; the notes were not sent.",
19108
+ planFeedbackFailed: "The notes could not be sent. Try again.",
19109
+ planPending: "Awaiting approval",
19110
+ planApproved: "Approved",
19111
+ planRejected: "Rejected",
19112
+ planEmpty: "This session has no plan to read yet.",
19113
+ planPendingHint: "Approve or reject this plan in the DSH approval dialog.",
17603
19114
  diffClose: "Close diff panel",
17604
19115
  sessionMenu: "Session menu",
17605
19116
  sessionMenuOpenIn: "Open in",
17606
19117
  sessionMenuOpenFailed: "Could not open: {message}",
19118
+ promptSource: "Prompts",
19119
+ promptSave: "Save as a prompt",
19120
+ promptSaveName: "Name",
19121
+ promptSaveNaming: "Naming it…",
19122
+ promptRefine: "Rewrite with AI",
19123
+ promptRefineBusy: "Rewriting…",
19124
+ promptRefineUndo: "Put the original back",
19125
+ promptRefineFailed: "Could not rewrite: {message}",
19126
+ promptSaveConfirm: "Save",
19127
+ promptSaveCancel: "Cancel",
19128
+ promptSaveDone: "Got it",
19129
+ promptSaved: "Saved as \"{name}\"",
19130
+ promptSaveExists: "A prompt with that name already exists. Try another.",
19131
+ promptSaveFailed: "Could not save: {message}",
17607
19132
  presetHeaderHint: "The agent preset this session runs",
17608
19133
  diffWorkingTree: "Branch changes",
17609
19134
  diffFiles: "{count} modified file(s)",
@@ -17781,6 +19306,17 @@ window.__ModuleLoader__.load({
17781
19306
  rewindFailed: "The rewind failed ({code}).",
17782
19307
  rewindBusy: "This session is running; wait for the turn to finish before rewinding.",
17783
19308
  rewindStale: "The Host process is still running the previous plugin build, which has no rewind route. Restart DSH and try again.",
19309
+ alerts: "Session alerts",
19310
+ alertsOff: "Off",
19311
+ alertsOn: "On",
19312
+ 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.",
19313
+ alertNeedsPermission: "Waiting on your approval",
19314
+ alertNeedsAnswer: "Waiting on your answer",
19315
+ alertTurnFinished: "Finished its turn",
19316
+ alertFallbackTitle: "Claude session",
19317
+ rewindRestoreFiles: "Also put the files back to where this turn found them",
19318
+ 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.",
19319
+ rewindFilesUnavailable: "The conversation was rewound, but the files were left alone: that turn recorded no working-tree snapshot.",
17784
19320
  turnUsage: "Turn usage",
17785
19321
  turnUsageTokens: "{count} tok",
17786
19322
  turnUsageCache: "Cache hit {percent}%",
@@ -17792,7 +19328,7 @@ window.__ModuleLoader__.load({
17792
19328
  function MaximizedDiff({ source, t, sessionId, closeDetails, restore, submitPrompt }) {
17793
19329
  const snapshot = useSyncExternalStore(source.subscribe, source.getSnapshot, source.getSnapshot);
17794
19330
  const useClaudeProjection = (selector) => selector(snapshot);
17795
- return /* @__PURE__ */ jsx(ClaudeDiffOverlay, {
19331
+ return /* @__PURE__ */ jsx(ClaudePanelOverlay, {
17796
19332
  onRestore: restore,
17797
19333
  children: /* @__PURE__ */ jsx(ClaudeDiffPanel, {
17798
19334
  useClaudeProjection,
@@ -17806,6 +19342,21 @@ window.__ModuleLoader__.load({
17806
19342
  });
17807
19343
  }
17808
19344
  const name = "dsh-claude-client";
19345
+ function MaximizedPlan({ source, t, sessionId, closeDetails, restore }) {
19346
+ const snapshot = useSyncExternalStore(source.subscribe, source.getSnapshot, source.getSnapshot);
19347
+ const useClaudeProjection = (selector) => selector(snapshot);
19348
+ return /* @__PURE__ */ jsx(ClaudePanelOverlay, {
19349
+ onRestore: restore,
19350
+ children: /* @__PURE__ */ jsx(ClaudePlanPanel, {
19351
+ useClaudeProjection,
19352
+ t,
19353
+ sessionId,
19354
+ maximized: true,
19355
+ closeDetails,
19356
+ toggleMaximized: restore
19357
+ })
19358
+ });
19359
+ }
17809
19360
  const inject = [
17810
19361
  "slots",
17811
19362
  "locale",
@@ -17847,12 +19398,15 @@ window.__ModuleLoader__.load({
17847
19398
  const t = ctx.locale.bind(namespace);
17848
19399
  ctx.effect(() => restyleHostChrome(), "dsh-claude: Host chrome restyling");
17849
19400
  pluginRead(CLAUDE_GLOBAL_SETTINGS_PATH, "fast").then((payload) => {
17850
- if (isGlobalSettingsView(payload)) applyClaudeMarkdownTheme(proseModeOf(payload.settings));
19401
+ if (!isGlobalSettingsView(payload)) return;
19402
+ applyClaudeMarkdownTheme(proseModeOf(payload.settings));
19403
+ setClaudeAlertsEnabled(alertModeOf(payload.settings) === "on");
17851
19404
  }).catch(() => {});
17852
19405
  const projections = new ClaudeProjectionStore({ report: (kind, detail) => {
17853
19406
  diagnostics.report(kind, detail);
17854
19407
  } });
17855
19408
  ctx.effect(() => ctx.inputTriggers.registerSource(createClaudeCommandSource(ctx, projections)), "dsh-claude: Claude slash source");
19409
+ ctx.effect(() => ctx.inputTriggers.registerSource(createClaudePromptSource(t("promptSource"))), "dsh-claude: Claude prompt source");
17856
19410
  const sessions = ctx.get("sessions");
17857
19411
  const workspaces = ctx.get("workspaces");
17858
19412
  const uiWorkspace = ctx.get("uiWorkspace");
@@ -17892,6 +19446,17 @@ window.__ModuleLoader__.load({
17892
19446
  resolve: (binding) => ({ hooks: { claudeProjection: projections.source(binding.sessionId) } })
17893
19447
  }), "dsh-claude: sidecar projection provider");
17894
19448
  ctx.effect(() => () => projections.dispose(), "dsh-claude: sidecar projection lifecycle");
19449
+ if (sessions !== void 0) ctx.effect(() => startClaudeSessionAlerts({
19450
+ sessions: {
19451
+ subscribe: (listener) => sessions.list.subscribe(listener),
19452
+ getSnapshot: () => sessions.list.getSnapshot()
19453
+ },
19454
+ projectionFor: (id) => projections.source(id),
19455
+ open: (id) => {
19456
+ sessions.open(id);
19457
+ },
19458
+ t
19459
+ }), "dsh-claude: session alerts");
17895
19460
  const uiConversation = ctx.get("uiConversation");
17896
19461
  if (uiConversation !== void 0) {
17897
19462
  ctx.effect(() => uiConversation.events.register(claudeTurnDefinition), "dsh-claude: Claude turn marker");
@@ -17906,9 +19471,11 @@ window.__ModuleLoader__.load({
17906
19471
  const layout = ctx.get("layout");
17907
19472
  let disposePluginDetails;
17908
19473
  let disposeDiffOverlay;
19474
+ let disposePlanOverlay;
17909
19475
  let disposeExpandedDetailsResize;
17910
19476
  let detailsSessionId;
17911
- const diffOpen = new DiffOpenStore();
19477
+ const diffOpen = new PanelOpenStore();
19478
+ const planOpen = new PanelOpenStore();
17912
19479
  const restoreDiff = () => {
17913
19480
  if (disposeDiffOverlay === void 0) return;
17914
19481
  disposeDiffOverlay();
@@ -17916,16 +19483,26 @@ window.__ModuleLoader__.load({
17916
19483
  layout?.openDetails();
17917
19484
  disposeExpandedDetailsResize = enableExpandedDetailsResize();
17918
19485
  };
19486
+ const restorePlan = () => {
19487
+ if (disposePlanOverlay === void 0) return;
19488
+ disposePlanOverlay();
19489
+ disposePlanOverlay = void 0;
19490
+ layout?.openDetails();
19491
+ disposeExpandedDetailsResize = enableExpandedDetailsResize();
19492
+ };
17919
19493
  const closePluginDetails = () => {
17920
- if (disposePluginDetails === void 0 && disposeDiffOverlay === void 0 && disposeExpandedDetailsResize === void 0 && detailsSessionId === void 0) return;
19494
+ if (disposePluginDetails === void 0 && disposeDiffOverlay === void 0 && disposePlanOverlay === void 0 && disposeExpandedDetailsResize === void 0 && detailsSessionId === void 0) return;
17921
19495
  disposeDiffOverlay?.();
17922
19496
  disposeDiffOverlay = void 0;
19497
+ disposePlanOverlay?.();
19498
+ disposePlanOverlay = void 0;
17923
19499
  disposeExpandedDetailsResize?.();
17924
19500
  disposeExpandedDetailsResize = void 0;
17925
19501
  disposePluginDetails?.();
17926
19502
  disposePluginDetails = void 0;
17927
19503
  detailsSessionId = void 0;
17928
19504
  diffOpen.close();
19505
+ planOpen.close();
17929
19506
  layout?.closeDetails();
17930
19507
  };
17931
19508
  ctx.effect(() => ctx.slots.onEntryError((key, entry, error) => {
@@ -17934,6 +19511,7 @@ window.__ModuleLoader__.load({
17934
19511
  ${error.stack ?? ""}` : String(error);
17935
19512
  diagnostics.report("slot-entry-crashed", `slot "${key}"${id}: ${message}`);
17936
19513
  if (key === "shell.overlay" && entry.options.id === "claude-diff-overlay") restoreDiff();
19514
+ if (key === "shell.overlay" && entry.options.id === "claude-plan-overlay") restorePlan();
17937
19515
  }), "dsh-claude: Slot entry failure reporting");
17938
19516
  const openTasksPanel = (sessionId, turn) => {
17939
19517
  closePluginDetails();
@@ -17955,6 +19533,55 @@ window.__ModuleLoader__.load({
17955
19533
  layout?.openDetails();
17956
19534
  disposeExpandedDetailsResize = enableExpandedDetailsResize();
17957
19535
  };
19536
+ const openPlanPanel = (sessionId) => {
19537
+ closePluginDetails();
19538
+ const maximizePlan = () => {
19539
+ if (disposePlanOverlay !== void 0) {
19540
+ restorePlan();
19541
+ return;
19542
+ }
19543
+ disposeExpandedDetailsResize?.();
19544
+ disposeExpandedDetailsResize = void 0;
19545
+ layout?.closeDetails();
19546
+ try {
19547
+ disposePlanOverlay = ctx.slots.register({
19548
+ name: "shell.overlay",
19549
+ id: "claude-plan-overlay",
19550
+ locale: namespace
19551
+ }, () => /* @__PURE__ */ jsx(MaximizedPlan, {
19552
+ source: projections.source(sessionId),
19553
+ t,
19554
+ sessionId,
19555
+ closeDetails: closePluginDetails,
19556
+ restore: restorePlan
19557
+ }));
19558
+ } catch {
19559
+ disposePlanOverlay = void 0;
19560
+ layout?.openDetails();
19561
+ disposeExpandedDetailsResize = enableExpandedDetailsResize();
19562
+ }
19563
+ };
19564
+ try {
19565
+ disposePluginDetails = ctx.slots.register({
19566
+ name: "details",
19567
+ priority: -10,
19568
+ locale: namespace,
19569
+ inject: () => ({
19570
+ t,
19571
+ sessionId,
19572
+ closeDetails: closePluginDetails,
19573
+ maximized: false,
19574
+ toggleMaximized: maximizePlan
19575
+ })
19576
+ }, ClaudePlanPanel);
19577
+ } catch {
19578
+ return;
19579
+ }
19580
+ detailsSessionId = sessionId;
19581
+ planOpen.open(sessionId);
19582
+ layout?.openDetails();
19583
+ disposeExpandedDetailsResize = enableExpandedDetailsResize();
19584
+ };
17958
19585
  const openOverviewPanel = (sessionId) => {
17959
19586
  if (sessions === void 0) return;
17960
19587
  closePluginDetails();
@@ -18045,7 +19672,7 @@ window.__ModuleLoader__.load({
18045
19672
  ctx.effect(() => {
18046
19673
  if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => closePluginDetails();
18047
19674
  const observer = new MutationObserver(() => {
18048
- if (detailsSessionId !== void 0 && disposeDiffOverlay === void 0 && document.querySelector("[data-details-collapsed]") !== null) closePluginDetails();
19675
+ if (detailsSessionId !== void 0 && disposeDiffOverlay === void 0 && disposePlanOverlay === void 0 && document.querySelector("[data-details-collapsed]") !== null) closePluginDetails();
18049
19676
  });
18050
19677
  observer.observe(document.body, {
18051
19678
  attributes: true,
@@ -18091,6 +19718,20 @@ window.__ModuleLoader__.load({
18091
19718
  })
18092
19719
  }, ClaudeAgentPresetLabel));
18093
19720
  }
19721
+ ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
19722
+ name: "conversation.session.header.utilities",
19723
+ id: "claude-plan",
19724
+ order: 29,
19725
+ locale: namespace,
19726
+ inject: (sessionId) => ({
19727
+ t,
19728
+ togglePlan: () => {
19729
+ if (planOpen.isOpen(sessionId)) closePluginDetails();
19730
+ else openPlanPanel(sessionId);
19731
+ },
19732
+ planOpen: planOpen.sourceFor(sessionId)
19733
+ })
19734
+ }, ClaudePlanHeaderAction));
18094
19735
  ctx.slots.inject("conversation.session.header.utilities", () => ctx.slots.register({
18095
19736
  name: "conversation.session.header.utilities",
18096
19737
  id: "claude-diff",
@@ -18112,6 +19753,34 @@ window.__ModuleLoader__.load({
18112
19753
  locale: namespace,
18113
19754
  inject: () => ({ t })
18114
19755
  }, ClaudeSessionMenu));
19756
+ ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
19757
+ name: "conversation.input.left",
19758
+ id: "claude-prompt-save",
19759
+ order: 40,
19760
+ locale: namespace,
19761
+ inject: () => ({ t })
19762
+ }, ClaudePromptSaveAction));
19763
+ ctx.slots.inject("conversation.input.left", () => ctx.slots.register({
19764
+ name: "conversation.input.left",
19765
+ id: "claude-prompt-refine",
19766
+ order: 41,
19767
+ locale: namespace,
19768
+ inject: (sessionId) => {
19769
+ if (sessions === void 0 || conversation === void 0) return { t };
19770
+ const scope = sessions.scope(sessionId);
19771
+ if (scope === void 0) return { t };
19772
+ const facade = sessionInput(conversation, scope);
19773
+ return {
19774
+ t,
19775
+ replaceDraft: (text) => {
19776
+ facade.setDraft(text);
19777
+ },
19778
+ notify: (level, text) => {
19779
+ facade.notify(level, text);
19780
+ }
19781
+ };
19782
+ }
19783
+ }, ClaudePromptRefineAction));
18115
19784
  ctx.slots.inject("conversation.input.dock", () => ctx.slots.register({
18116
19785
  name: "conversation.input.dock",
18117
19786
  id: "claude-review-comments",