@norman-else/dsh-claude 0.1.34 → 0.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -27,6 +27,13 @@ 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
+ function isClaudeRenderMode(value) {
31
+ return value === "plugin" || value === "native";
32
+ }
33
+ const DEFAULT_CLAUDE_PROSE_MODE = "plain";
34
+ function isClaudeProseMode(value) {
35
+ return value === "plain" || value === "enhanced";
36
+ }
30
37
  //#endregion
31
38
  //#region src/client/task-projection.ts
32
39
  /** Tasks UI is reserved for detached work and genuine Claude subagents. */
@@ -239,7 +246,7 @@ window.__ModuleLoader__.load({
239
246
  case "WebFetch": {
240
247
  const url = inputString(input, "url") ?? "a web page";
241
248
  completed = `Fetched ${url}`;
242
- failedAction = `fetch ${url}`;
249
+ failedAction = `load ${url}`;
243
250
  break;
244
251
  }
245
252
  case "WebSearch":
@@ -267,8 +274,76 @@ window.__ModuleLoader__.load({
267
274
  }
268
275
  return failed ? `Failed to ${failedAction}` : completed;
269
276
  }
277
+ /** The call id a task's own lifecycle pings were dispatched from. */
278
+ function taskParentToolUseId(activities, taskId) {
279
+ for (const activity of activities) {
280
+ if (activity.taskId !== taskId) continue;
281
+ const id = inputRecord(activity.detail)?.tool_use_id;
282
+ if (typeof id === "string" && id.length > 0) return id;
283
+ }
284
+ }
285
+ /**
286
+ * The tools one task ran, in the shape the transcript's tool cards take.
287
+ *
288
+ * The task's own activities are lifecycle pings whose detail is the raw
289
+ * protocol message -- useful to nobody reading a panel. The work is in the
290
+ * activities addressed to the call that dispatched the task: a subagent's
291
+ * nested calls carry it as `parentToolUseId`, and a backgrounded command IS
292
+ * that call. Both are folded here the way {@link transcriptItemsForStep} folds
293
+ * a step, so one card renderer serves the transcript and the task panel.
294
+ */
295
+ function taskTools(activities, taskId) {
296
+ const parent = taskParentToolUseId(activities, taskId);
297
+ if (parent === void 0) return [];
298
+ const tools = /* @__PURE__ */ new Map();
299
+ const ordered = [...activities].sort((left, right) => left.ordinal - right.ordinal);
300
+ for (const activity of ordered) {
301
+ const own = activity.parentToolUseId === parent;
302
+ const isRoot = activity.parentToolUseId === void 0 && activity.toolUseId === parent;
303
+ if (!own && !isRoot) continue;
304
+ const toolUseId = activity.toolUseId;
305
+ if (toolUseId === void 0) continue;
306
+ const previous = tools.get(toolUseId);
307
+ if (previous === void 0) {
308
+ if (activity.toolName === void 0) continue;
309
+ const input = inputRecord(activity.detail);
310
+ tools.set(toolUseId, {
311
+ toolUseId,
312
+ toolName: activity.toolName,
313
+ description: toolDescription(activity.toolName, input),
314
+ ...activity.summary === void 0 ? {} : { summary: activity.summary },
315
+ ...activity.detail === void 0 ? {} : { input: activity.detail },
316
+ ...activity.phase === void 0 ? {} : { phase: activity.phase },
317
+ ...activity.isError === void 0 ? {} : { isError: activity.isError },
318
+ subcalls: []
319
+ });
320
+ continue;
321
+ }
322
+ const failed = activity.isError === true || activity.phase === "failed";
323
+ tools.set(toolUseId, {
324
+ ...previous,
325
+ description: toolDescription(previous.toolName, inputRecord(previous.input), failed),
326
+ ...activity.detail === void 0 ? {} : { output: activity.detail },
327
+ ...activity.phase === void 0 ? {} : { phase: activity.phase },
328
+ ...activity.isError === void 0 ? {} : { isError: activity.isError }
329
+ });
330
+ }
331
+ return [...tools.values()];
332
+ }
333
+ /** Whether the Host drew this step with DSH's own renderer.
334
+ *
335
+ * The stamp rides on the records themselves rather than on a Client-side copy
336
+ * of the setting, because the two can disagree: the Host switches on the next
337
+ * turn while a running Client keeps whatever it decided at boot, and the
338
+ * failure mode of disagreeing is drawing every step twice. Reading it back per
339
+ * step also keeps history honest in both directions -- a turn recorded under
340
+ * one renderer keeps it after the setting changes. */
341
+ function nativelyRenderedStep(activities, turn, step) {
342
+ return activities.some((activity) => activity.turn === turn && activity.step === step && activity.renderer === "native");
343
+ }
270
344
  /** Fold one step's shared ordinal stream into Claude Code-style prose and tool groups. */
271
345
  function transcriptItemsForStep(activities, turn, step, tasks = []) {
346
+ if (nativelyRenderedStep(activities, turn, step)) return [];
272
347
  const ordered = activities.filter((activity) => activity.turn === turn && activity.step === step && isProjectedTaskActivity(activity, tasks)).slice().sort((left, right) => left.ordinal - right.ordinal);
273
348
  const rows = foldedRows(ordered, tasks);
274
349
  const placed = /* @__PURE__ */ new Set();
@@ -327,6 +402,15 @@ window.__ModuleLoader__.load({
327
402
  });
328
403
  continue;
329
404
  }
405
+ if (activity.kind === "usage" && activity.usage !== void 0) {
406
+ flushGroup();
407
+ items.push({
408
+ kind: "usage",
409
+ ordinal: activity.ordinal,
410
+ usage: activity.usage
411
+ });
412
+ continue;
413
+ }
330
414
  if (activity.kind === "tool-call" && activity.toolUseId !== void 0 && activity.toolName !== void 0) {
331
415
  group ??= {
332
416
  ordinal: activity.ordinal,
@@ -674,30 +758,65 @@ window.__ModuleLoader__.load({
674
758
  fontSize: 14,
675
759
  lineHeight: "22px"
676
760
  };
677
- const settingSelectTrigger = {
678
- width: "100%",
679
- minHeight: 38,
680
- display: "flex",
681
- alignItems: "center",
682
- justifyContent: "space-between",
683
- gap: 12,
684
- padding: "7px 11px 7px 13px",
685
- border: "1px solid var(--dsw-alias-border-l2, color-mix(in srgb, currentColor 16%, transparent))",
686
- borderRadius: 10,
687
- background: "var(--dsw-alias-bg-layer-2)",
688
- color: "var(--dsw-alias-label-primary)",
689
- boxShadow: "0 1px 2px color-mix(in srgb, var(--dsw-alias-label-primary) 5%, transparent)",
690
- font: "inherit",
691
- fontSize: 14,
692
- lineHeight: "22px",
693
- textAlign: "left",
694
- cursor: "pointer",
695
- transition: "border-color 120ms ease, box-shadow 120ms ease, background 120ms ease"
696
- };
697
- const settingSelectTriggerOpen = {
698
- borderColor: "var(--dsw-static-blue-450)",
699
- boxShadow: "0 0 0 3px color-mix(in srgb, var(--dsw-static-blue-450) 15%, transparent)"
700
- };
761
+ const settingSelectTriggerClass = "dshClaudeSettingSelectTrigger";
762
+ const settingSelectChevronClass = "dshClaudeSettingSelectChevron";
763
+ /**
764
+ * The listbox trigger and its chevron, in a stylesheet rather than inline
765
+ * because the states it has to draw cannot be expressed inline.
766
+ *
767
+ * Focus is the reason. An inline style carries no pseudo-class, so the blue
768
+ * ring used to ride on the open state alone: choosing an option closed the
769
+ * menu and took the ring with it while the button kept DOM focus, leaving the
770
+ * UA's own focus ring -- white on a dark theme, drawn outside the radius --
771
+ * as the only indicator. Focus and open are separate states; both draw the
772
+ * ring here, and `outline: none` retires the UA's.
773
+ *
774
+ * The open state reads `aria-expanded` instead of a second class because the
775
+ * attribute is already on the element and already correct. Its value is left
776
+ * unquoted: a valid identifier needs no quotes, and the sheet then survives
777
+ * React's server escaping, which would otherwise write `"` into markup a
778
+ * browser reads as raw text.
779
+ */
780
+ const settingSelectCss = `
781
+ .${settingSelectTriggerClass} {
782
+ width: 100%;
783
+ min-height: 38px;
784
+ display: flex;
785
+ align-items: center;
786
+ justify-content: space-between;
787
+ gap: 12px;
788
+ padding: 7px 11px 7px 13px;
789
+ border: 1px solid var(--dsw-alias-border-l2, color-mix(in srgb, currentColor 16%, transparent));
790
+ border-radius: 10px;
791
+ background: var(--dsw-alias-bg-layer-2);
792
+ color: var(--dsw-alias-label-primary);
793
+ box-shadow: 0 1px 2px color-mix(in srgb, var(--dsw-alias-label-primary) 5%, transparent);
794
+ font: inherit;
795
+ font-size: 14px;
796
+ line-height: 22px;
797
+ text-align: left;
798
+ cursor: pointer;
799
+ transition: border-color 120ms ease, box-shadow 120ms ease, background 120ms ease;
800
+ }
801
+ .${settingSelectTriggerClass}:focus-visible,
802
+ .${settingSelectTriggerClass}[aria-expanded=true] {
803
+ outline: none;
804
+ border-color: var(--dsw-static-blue-450);
805
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-static-blue-450) 15%, transparent);
806
+ }
807
+ .${settingSelectChevronClass} {
808
+ flex: none;
809
+ display: grid;
810
+ place-items: center;
811
+ width: 16px;
812
+ height: 16px;
813
+ color: var(--dsw-alias-label-tertiary);
814
+ transition: transform 120ms ease;
815
+ }
816
+ .${settingSelectTriggerClass}[aria-expanded=true] .${settingSelectChevronClass} {
817
+ transform: rotate(180deg);
818
+ }
819
+ `;
701
820
  const settingSelectValue = {
702
821
  minWidth: 0,
703
822
  flex: 1,
@@ -705,15 +824,6 @@ window.__ModuleLoader__.load({
705
824
  textOverflow: "ellipsis",
706
825
  whiteSpace: "nowrap"
707
826
  };
708
- const settingSelectChevron = {
709
- flex: "none",
710
- color: "var(--dsw-alias-label-tertiary)",
711
- fontSize: 16,
712
- lineHeight: 1,
713
- transform: "translateY(-1px)",
714
- transition: "transform 120ms ease"
715
- };
716
- const settingSelectChevronOpen = { transform: "translateY(1px) rotate(180deg)" };
717
827
  const settingSelectMenu = {
718
828
  position: "absolute",
719
829
  zIndex: 20,
@@ -1065,6 +1175,14 @@ window.__ModuleLoader__.load({
1065
1175
  cursor: "pointer"
1066
1176
  };
1067
1177
  const taskActivitySection = { margin: "7px 0 0 27px" };
1178
+ /** The task's tool cards. No inset panel of their own: each card already
1179
+ * carries its own surface, and nesting two would read as a box in a box. */
1180
+ const taskToolList = {
1181
+ display: "flex",
1182
+ flexDirection: "column",
1183
+ gap: 2,
1184
+ marginTop: 7
1185
+ };
1068
1186
  const taskActivityList = {
1069
1187
  display: "flex",
1070
1188
  flexDirection: "column",
@@ -3311,460 +3429,576 @@ window.__ModuleLoader__.load({
3311
3429
  lineHeight: "20px"
3312
3430
  };
3313
3431
  //#endregion
3314
- //#region src/client/token-format.ts
3315
- function formatTokenCount(tokens) {
3316
- if (tokens >= 1e6) return `${Number((tokens / 1e6).toFixed(tokens >= 1e7 ? 0 : 1))}M`;
3317
- if (tokens >= 1e3) return `${Number((tokens / 1e3).toFixed(tokens >= 1e5 ? 0 : 1))}K`;
3318
- return String(tokens);
3432
+ //#region src/client/markdown-theme.ts
3433
+ /** Claude Code's code presentation over the Host's Markdown renderer.
3434
+ *
3435
+ * This package renders no Markdown of its own prose, fenced blocks and the
3436
+ * copy affordance all come from the Host's `MarkdownText` primitive. What it
3437
+ * can own is what that primitive reads: the palette (custom properties) and
3438
+ * the block's chrome (CSS over the primitive's own markup).
3439
+ *
3440
+ * PARITY IS PARTIAL BY CONSTRUCTION. Both renderers highlight with shiki, but
3441
+ * Claude Code's desktop build loads a full TextMate theme (Pierre Dark /
3442
+ * Pierre Light Soft: 248 tokenColor rules over 424 scopes) and bakes the
3443
+ * resolved colour into every span, while the Host loads shiki's legacy
3444
+ * `css-variables` theme, which collapses every scope into the eleven buckets
3445
+ * below. Those eleven carry the colours that dominate a code block —
3446
+ * keywords, strings, comments, functions, numbers — and nothing here can
3447
+ * recover the rest: `constant.numeric` and `constant` are two different
3448
+ * colours in Pierre and one bucket here, and Pierre's string-coloured string
3449
+ * delimiters share this sheet's single punctuation bucket. Matching the rest
3450
+ * means this package running its own shiki, which is a different decision
3451
+ * with a different cost.
3452
+ *
3453
+ * Every rule fails open the way `host-chrome` does: the palette is
3454
+ * declarations on this package's own wrapper, and the chrome rules match the
3455
+ * primitive's CSS Module local names, so a Host that renames either simply
3456
+ * stops matching and its stock presentation comes back.
3457
+ */
3458
+ /** Wrapper class carrying the palette and scoping the chrome rules.
3459
+ * `display:contents` so the extra element generates no box — custom
3460
+ * properties inherit through it regardless. */
3461
+ const CLAUDE_MARKDOWN_SCOPE = "dsh-claude-markdown";
3462
+ /** Pierre mapped onto the Host's eleven buckets. Each entry names the theme
3463
+ * scope the value was taken from, because the mapping — not the colour — is
3464
+ * the part a reader has to check. */
3465
+ const PIERRE_DARK = [
3466
+ [
3467
+ "--shiki-background",
3468
+ "#1a1a19",
3469
+ "UI surface (--cds-surface-2)"
3470
+ ],
3471
+ [
3472
+ "--shiki-foreground",
3473
+ "#fafafa",
3474
+ "editor.foreground"
3475
+ ],
3476
+ [
3477
+ "--shiki-token-comment",
3478
+ "#737373",
3479
+ "comment"
3480
+ ],
3481
+ [
3482
+ "--shiki-token-keyword",
3483
+ "#ff678d",
3484
+ "keyword, storage.type"
3485
+ ],
3486
+ [
3487
+ "--shiki-token-string",
3488
+ "#5ecc71",
3489
+ "string"
3490
+ ],
3491
+ [
3492
+ "--shiki-token-string-expression",
3493
+ "#ffa359",
3494
+ "punctuation.section.embedded"
3495
+ ],
3496
+ [
3497
+ "--shiki-token-function",
3498
+ "#9d6afb",
3499
+ "entity.name.function"
3500
+ ],
3501
+ [
3502
+ "--shiki-token-constant",
3503
+ "#68cdf2",
3504
+ "constant.numeric, constant.language"
3505
+ ],
3506
+ [
3507
+ "--shiki-token-parameter",
3508
+ "#a3a3a3",
3509
+ "variable.parameter"
3510
+ ],
3511
+ [
3512
+ "--shiki-token-punctuation",
3513
+ "#636363",
3514
+ "punctuation"
3515
+ ],
3516
+ [
3517
+ "--shiki-token-link",
3518
+ "#ff678d",
3519
+ "markup.underline.link.markdown"
3520
+ ]
3521
+ ];
3522
+ const PIERRE_LIGHT = [
3523
+ [
3524
+ "--shiki-background",
3525
+ "#ffffff",
3526
+ "UI surface (--cds-surface-2)"
3527
+ ],
3528
+ [
3529
+ "--shiki-foreground",
3530
+ "#525252",
3531
+ "editor.foreground"
3532
+ ],
3533
+ [
3534
+ "--shiki-token-comment",
3535
+ "#8a8a8a",
3536
+ "comment"
3537
+ ],
3538
+ [
3539
+ "--shiki-token-keyword",
3540
+ "#ff678d",
3541
+ "keyword, storage.type"
3542
+ ],
3543
+ [
3544
+ "--shiki-token-string",
3545
+ "#0dbe4e",
3546
+ "string"
3547
+ ],
3548
+ [
3549
+ "--shiki-token-string-expression",
3550
+ "#fe8c2c",
3551
+ "punctuation.section.embedded"
3552
+ ],
3553
+ [
3554
+ "--shiki-token-function",
3555
+ "#9d6afb",
3556
+ "entity.name.function"
3557
+ ],
3558
+ [
3559
+ "--shiki-token-constant",
3560
+ "#08c0ef",
3561
+ "constant.numeric, constant.language"
3562
+ ],
3563
+ [
3564
+ "--shiki-token-parameter",
3565
+ "#737373",
3566
+ "variable.parameter"
3567
+ ],
3568
+ [
3569
+ "--shiki-token-punctuation",
3570
+ "#737373",
3571
+ "punctuation"
3572
+ ],
3573
+ [
3574
+ "--shiki-token-link",
3575
+ "#ff678d",
3576
+ "markup.underline.link.markdown"
3577
+ ]
3578
+ ];
3579
+ /** Claude's brand clay (`--cds-hsl-clay`), its inline-code TEXT colour; the
3580
+ * emphasized ramp is the light-theme variant. The chip's fill is deliberately
3581
+ * NOT tinted with it — see {@link INLINE_FILL_DARK}. */
3582
+ const CLAY = "#d97757";
3583
+ const CLAY_EMPHASIZED = "#c8603f";
3584
+ /** Prose colours for the body of a Claude answer.
3585
+ *
3586
+ * These are NOT Pierre: the palette above exists to match Claude's desktop
3587
+ * build inside code blocks, and Claude paints prose in plain body text. This
3588
+ * block is a deliberate departure — headings, emphasis and links get their
3589
+ * own hues so a long answer is scannable, the way a Markdown-highlighting
3590
+ * editor shows it. Values are theme-independent by design (the same six read
3591
+ * acceptably on both surfaces); split them if the light theme ever needs its
3592
+ * own ramp.
3593
+ *
3594
+ * The inline-code entry REPLACES {@link CLAY} rather than sitting beside it:
3595
+ * two colours on the same chip is not a choice a stylesheet can make. */
3596
+ const PROSE = {
3597
+ heading: "#7C9EFF",
3598
+ bold: "#FFB454",
3599
+ italic: "#5BD6C0",
3600
+ inlineCode: "#FF7A93",
3601
+ listMarker: "#F2C94C",
3602
+ quote: "#9AA3B2",
3603
+ link: "#4DA3FF",
3604
+ codeBackground: "#0F1218",
3605
+ codeBorder: "#2E3546"
3606
+ };
3607
+ /** The inline-code chip fill: a neutral 4% wash (Claude's `--t1`), not a tint
3608
+ * of the text colour. A clay-tinted fill reads as a coloured box around every
3609
+ * identifier; the neutral one disappears into the surface and lets the text
3610
+ * carry the accent, which is what the chip is for. */
3611
+ const INLINE_FILL_DARK = "hsl(0 0% 100% / .04)";
3612
+ const INLINE_FILL_LIGHT = "hsl(0 0% 4.3% / .04)";
3613
+ /** Block corner radius (Claude's `--r6`), against the Host's own 12px. */
3614
+ const BLOCK_RADIUS = "8px";
3615
+ /** @param banner - equal to `surface` on purpose: the bar is floated out of
3616
+ * the way below, so this colour is only reached if those chrome selectors
3617
+ * miss, and a bar that matches the block is the neutral fallback. */
3618
+ function palette(entries, surface, banner, inlineFill) {
3619
+ return [
3620
+ ...entries.map(([name, value]) => `${name}:${value};`),
3621
+ `--dsw-alias-markdown-code-block:${surface};`,
3622
+ `--dsw-alias-markdown-code-block-banner:${banner};`,
3623
+ `--dsw-alias-markdown-inline-code:${inlineFill}`
3624
+ ].join("");
3625
+ }
3626
+ const CLAUDE_MARKDOWN_THEME_CSS = [
3627
+ `.${CLAUDE_MARKDOWN_SCOPE}{display:contents;`,
3628
+ palette(PIERRE_LIGHT, "#ffffff", "#ffffff", INLINE_FILL_LIGHT),
3629
+ "}",
3630
+ `body[data-ds-dark-theme] .${CLAUDE_MARKDOWN_SCOPE}{`,
3631
+ palette(PIERRE_DARK, "#1a1a19", "#1a1a19", INLINE_FILL_DARK),
3632
+ "}",
3633
+ `.${CLAUDE_MARKDOWN_SCOPE} :not(pre)>code{color:${CLAY_EMPHASIZED};border-radius:4px;padding:1px 2px}`,
3634
+ `body[data-ds-dark-theme] .${CLAUDE_MARKDOWN_SCOPE} :not(pre)>code{color:${CLAY}}`,
3635
+ `.${CLAUDE_MARKDOWN_SCOPE} [class*="bannerWrap"]{position:absolute;top:0;right:0;z-index:7;background:transparent;border-radius:0}`,
3636
+ `.${CLAUDE_MARKDOWN_SCOPE} [class*="banner"]:not([class*="bannerWrap"]){background:transparent;padding:6px 8px}`,
3637
+ `.${CLAUDE_MARKDOWN_SCOPE} [class*="infostring"]{display:none}`,
3638
+ `.${CLAUDE_MARKDOWN_SCOPE} pre{white-space:pre;word-break:normal;overflow-x:auto}`,
3639
+ `.${CLAUDE_MARKDOWN_SCOPE} [class*="block"]{--dsl-code-block-border-radius:${BLOCK_RADIUS}}`
3640
+ ].join("");
3641
+ /** The class the review-comment card puts on a rendered comment body. Declared
3642
+ * here rather than imported because it is a literal inside `styles.ts`'s CSS
3643
+ * string, not a constant; a rename there makes these rules miss and the
3644
+ * comment keeps its stock colours, which is the same fail-open the chrome
3645
+ * rules above rely on. */
3646
+ const COMMENT_BODY_SCOPE = "dshClaudeDiffCommentBody";
3647
+ /** Everything the `prose: 'enhanced'` setting adds, and nothing the base sheet
3648
+ * needs. Appended AFTER the base sheet so its rules win ties on source order —
3649
+ * see the note on the inline-code selector below. */
3650
+ const CLAUDE_MARKDOWN_ENHANCED_CSS = [
3651
+ `.${CLAUDE_MARKDOWN_SCOPE} :is(h1,h2,h3,h4,h5,h6){color:${PROSE.heading}}`,
3652
+ `.${CLAUDE_MARKDOWN_SCOPE} strong{color:${PROSE.bold}}`,
3653
+ `.${CLAUDE_MARKDOWN_SCOPE} em{color:${PROSE.italic}}`,
3654
+ `.${CLAUDE_MARKDOWN_SCOPE} li::marker{color:${PROSE.listMarker}}`,
3655
+ `.${CLAUDE_MARKDOWN_SCOPE} blockquote{border-left-color:${PROSE.quote};color:${PROSE.quote}}`,
3656
+ `.${CLAUDE_MARKDOWN_SCOPE} a{color:${PROSE.link}}`,
3657
+ `.${CLAUDE_MARKDOWN_SCOPE} :not(pre)>code,body[data-ds-dark-theme] .${CLAUDE_MARKDOWN_SCOPE} :not(pre)>code{color:${PROSE.inlineCode}}`,
3658
+ `.${CLAUDE_MARKDOWN_SCOPE} [class*="block"]{background:${PROSE.codeBackground};box-shadow:0 0 0 1px ${PROSE.codeBorder}}`,
3659
+ `.${CLAUDE_MARKDOWN_SCOPE} pre{background:${PROSE.codeBackground}}`,
3660
+ `.${COMMENT_BODY_SCOPE} :is(h1,h2,h3,h4,h5,h6){color:${PROSE.heading}}`,
3661
+ `.${COMMENT_BODY_SCOPE} strong{color:${PROSE.bold}}`,
3662
+ `.${COMMENT_BODY_SCOPE} em{color:${PROSE.italic}}`,
3663
+ `.${COMMENT_BODY_SCOPE} li::marker{color:${PROSE.listMarker}}`,
3664
+ `.${COMMENT_BODY_SCOPE} blockquote{border-left-color:${PROSE.quote};color:${PROSE.quote}}`,
3665
+ `.${COMMENT_BODY_SCOPE} a{color:${PROSE.link}}`,
3666
+ `.${COMMENT_BODY_SCOPE} :not(pre)>code{color:${PROSE.inlineCode}}`
3667
+ ].join("");
3668
+ /** Live reference to this package's sheet, and the mode it currently holds.
3669
+ * The mode is remembered so `ensureClaudeMarkdownTheme` — called on every
3670
+ * Markdown render — cannot undo a choice boot or the settings panel made. */
3671
+ let styleTag = null;
3672
+ let mode = DEFAULT_CLAUDE_PROSE_MODE;
3673
+ function write() {
3674
+ if (typeof document === "undefined") return;
3675
+ if (styleTag === null || styleTag.parentNode === null) {
3676
+ styleTag = document.createElement("style");
3677
+ styleTag.dataset.dshClaudeMarkdownTheme = "";
3678
+ document.head.appendChild(styleTag);
3679
+ }
3680
+ styleTag.textContent = mode === "enhanced" ? CLAUDE_MARKDOWN_THEME_CSS + CLAUDE_MARKDOWN_ENHANCED_CSS : CLAUDE_MARKDOWN_THEME_CSS;
3681
+ }
3682
+ /** Attach the sheet, keeping whatever mode is already set. Idempotent. */
3683
+ function ensureClaudeMarkdownTheme() {
3684
+ write();
3685
+ }
3686
+ /** Switch the prose palette. Rewriting one global sheet repaints every mounted
3687
+ * Markdown block at once, so no render state has to carry the setting. */
3688
+ function applyClaudeMarkdownTheme(next) {
3689
+ mode = next;
3690
+ write();
3319
3691
  }
3320
3692
  //#endregion
3321
- //#region src/client/ClaudeTasksPanel.tsx
3322
- const STATUS_LABEL = {
3323
- running: "tasksRunning",
3324
- completed: "tasksCompleted",
3325
- failed: "tasksFailed",
3326
- stopped: "tasksStopped",
3327
- killed: "tasksKilled"
3328
- };
3329
- function visibleTaskGroups(tasks, dismissedSettledIds) {
3330
- const projected = tasks.filter(isProjectedTask);
3331
- return {
3332
- running: projected.filter((task) => task.status === "running"),
3333
- finished: projected.filter((task) => task.status !== "running" && !dismissedSettledIds.has(task.taskId))
3693
+ //#region src/client/markdown-labels.tsx
3694
+ /** Stable across renders: MarkdownText keys its streaming renderer on this
3695
+ * object's identity and reparses from scratch whenever it changes. */
3696
+ function useClaudeMarkdownLabels(t) {
3697
+ return useMemo(() => ({
3698
+ code: {
3699
+ copyLabel: t("markdownCopy"),
3700
+ copiedLabel: t("markdownCopied")
3701
+ },
3702
+ footnotes: t("markdownFootnotes")
3703
+ }), [t]);
3704
+ }
3705
+ /** MarkdownText with the labels the running Host demands.
3706
+ *
3707
+ * The published primitives package still types the old `codeLabels` prop, so
3708
+ * the new shape cannot typecheck against it — the cast is confined here rather
3709
+ * than repeated at every call site. Drop it once the installed
3710
+ * @deepseek-ai/dsh-client-ui-primitives matches the Desktop build. */
3711
+ function ClaudeMarkdown({ text, labels, streaming }) {
3712
+ const props = {
3713
+ text,
3714
+ labels,
3715
+ ...streaming === void 0 ? {} : { streaming }
3334
3716
  };
3717
+ const Renderer = MarkdownText;
3718
+ ensureClaudeMarkdownTheme();
3719
+ return /* @__PURE__ */ jsx("div", {
3720
+ className: CLAUDE_MARKDOWN_SCOPE,
3721
+ children: /* @__PURE__ */ jsx(Renderer, { ...props })
3722
+ });
3335
3723
  }
3336
- function activitiesForTask(activities, taskId) {
3337
- return activities.filter((activity) => activity.taskId === taskId);
3724
+ //#endregion
3725
+ //#region src/plugin-budget.ts
3726
+ /**
3727
+ * The plugin's connection budget, in one table.
3728
+ *
3729
+ * A browser opens a small fixed number of connections to one origin — six for
3730
+ * HTTP/1.1 in Chromium — and shares them with the Host's own traffic. Every
3731
+ * response this plugin holds open costs one of them for its lifetime, and a
3732
+ * request that cannot get one waits in the browser's queue where no server-side
3733
+ * deadline can reach it. When the pool is exhausted the panels that would
3734
+ * *diagnose* the problem are the first thing to stop answering, which is how
3735
+ * this failure has always presented: four settings cards timing out at once
3736
+ * against a Host that is demonstrably healthy.
3737
+ *
3738
+ * So the budget is a fixed constant rather than a function of how much work is
3739
+ * in flight. Steady state is one connection (the multiplexed projection
3740
+ * carrier); the peak is `PLUGIN_GLOBAL_PERMITS`, whatever the session count.
3741
+ *
3742
+ * Both halves read this file, which is the point: a route declares a budget
3743
+ * class and the client derives its wait from the same entry, so a server
3744
+ * deadline can never be quietly longer than the client's patience.
3745
+ */
3746
+ /** Server-side budget classes. A route declares a class, never a number. */
3747
+ const ROUTE_BUDGET_MS = {
3748
+ /** Answers from memory or a single bounded probe. */
3749
+ fast: 5e3,
3750
+ /** Chains local Git work. */
3751
+ git: 45e3,
3752
+ /** Reaches the network: remote Git, `gh`, the npm registry. */
3753
+ remote: 15e4
3754
+ };
3755
+ /** The client waits one round trip longer, so the route's own 504 wins the
3756
+ * race and the caller learns which budget elapsed instead of guessing. */
3757
+ const CLIENT_GRACE_MS = 3e3;
3758
+ function clientBudgetMs(budget) {
3759
+ return ROUTE_BUDGET_MS[budget] + CLIENT_GRACE_MS;
3760
+ }
3761
+ /** A request that never got a permit fails fast and says so, rather than
3762
+ * spending its whole budget queued behind work it cannot see. */
3763
+ const QUEUE_WAIT_BUDGET_MS = 4e3;
3764
+ /** Per-lane ceilings inside the global budget.
3765
+ *
3766
+ * The projection carrier holds a permanently reserved permit outside these,
3767
+ * so three remain. `write + stream <= 2` leaves one permit that only a read
3768
+ * can take: a diagnostic read always has somewhere to go, however many slow
3769
+ * actions are in flight. That invariant is what stops a 150s repository
3770
+ * action from reproducing the original symptom through a new mechanism. */
3771
+ const PLUGIN_LANE_CAPS = {
3772
+ read: 2,
3773
+ write: 1,
3774
+ stream: 1
3775
+ };
3776
+ //#endregion
3777
+ //#region src/rewind.ts
3778
+ function isRewound(ranges, seq) {
3779
+ return ranges.some((range) => seq >= range.start && seq <= range.end);
3338
3780
  }
3339
- function tasksForTurn(tasks, turn) {
3340
- return tasks.filter((task) => task.originTurn === turn && isProjectedTask(task));
3781
+ //#endregion
3782
+ //#region src/client/plugin-transport.ts
3783
+ /**
3784
+ * The plugin's only door to the network.
3785
+ *
3786
+ * Every request the Client makes goes through here, and the shape of the
3787
+ * public functions is the point. There is no `signal` property, no `headers`,
3788
+ * no `RequestInit` and no millisecond number in any parameter: a caller cannot
3789
+ * hand this module an options object whose later spread silently overwrites
3790
+ * the deadline sitting above it. That is not a hypothetical — it is exactly
3791
+ * how four call sites lost their deadlines while looking completely ordinary,
3792
+ * because `exactOptionalPropertyTypes` forbids writing `signal: undefined` and
3793
+ * the spread idiom is what people reach for instead. `cancel` is positional
3794
+ * here, so `undefined` is simply passable and the idiom has nothing to do.
3795
+ *
3796
+ * The second seal is arithmetic. The browser shares a small fixed connection
3797
+ * budget between this plugin and the Host, and the plugin used to spend it
3798
+ * proportionally to how many Claude sessions existed. Here every request takes
3799
+ * a permit from a fixed pool first, so more call sites and more sessions
3800
+ * cannot become more sockets — a saturated plugin queues, and says `starved`
3801
+ * within `QUEUE_WAIT_BUDGET_MS` instead of dying silently at its full budget.
3802
+ *
3803
+ * Lane caps guarantee a read always has somewhere to go: the projection
3804
+ * carrier holds a reserved permit, and `write + stream` can occupy at most two
3805
+ * of the remaining three. The panel that diagnoses a saturated pool is
3806
+ * therefore the one request that cannot be starved by it.
3807
+ */
3808
+ var PluginRequestError = class extends Error {
3809
+ reason;
3810
+ status;
3811
+ code;
3812
+ constructor(reason, message, status, code) {
3813
+ super(message);
3814
+ this.name = "PluginRequestError";
3815
+ this.reason = reason;
3816
+ if (status !== void 0) this.status = status;
3817
+ if (code !== void 0) this.code = code;
3818
+ }
3819
+ };
3820
+ let send = (...args) => fetch(...args);
3821
+ let held = 0;
3822
+ const laneHeld = {
3823
+ read: 0,
3824
+ write: 0,
3825
+ stream: 0
3826
+ };
3827
+ let projectionHeld = false;
3828
+ let queue = [];
3829
+ const inFlight = /* @__PURE__ */ new Map();
3830
+ /** The projection carrier owns a permit of its own, outside the lane caps, so
3831
+ * the transcript stream and the panels never compete for the same slot. */
3832
+ function laneHasRoom(lane) {
3833
+ return held < 4 && laneHeld[lane] < PLUGIN_LANE_CAPS[lane];
3834
+ }
3835
+ function pump() {
3836
+ for (let index = 0; index < queue.length; index += 1) {
3837
+ const waiter = queue[index];
3838
+ if (waiter === void 0 || !laneHasRoom(waiter.lane)) continue;
3839
+ queue.splice(index, 1);
3840
+ index -= 1;
3841
+ clearTimeout(waiter.timer);
3842
+ held += 1;
3843
+ laneHeld[waiter.lane] += 1;
3844
+ waiter.admit();
3845
+ }
3341
3846
  }
3342
- function summarizeTurnTasks(tasks) {
3343
- if (tasks.length === 0) return void 0;
3344
- const running = tasks.filter((task) => task.status === "running").length;
3345
- const failed = tasks.filter((task) => task.status === "failed" || task.status === "stopped" || task.status === "killed").length;
3346
- const completed = tasks.filter((task) => task.status === "completed").length;
3347
- return {
3348
- state: running > 0 ? "running" : failed > 0 ? "failed" : "completed",
3349
- count: tasks.length,
3350
- running,
3351
- failed,
3352
- completed
3847
+ function release(lane) {
3848
+ held -= 1;
3849
+ laneHeld[lane] -= 1;
3850
+ pump();
3851
+ }
3852
+ function acquire(lane) {
3853
+ let released = false;
3854
+ const releaseOnce = () => {
3855
+ if (released) return;
3856
+ released = true;
3857
+ release(lane);
3353
3858
  };
3859
+ if (laneHasRoom(lane)) {
3860
+ held += 1;
3861
+ laneHeld[lane] += 1;
3862
+ return Promise.resolve(releaseOnce);
3863
+ }
3864
+ return new Promise((resolve, reject) => {
3865
+ const waiter = {
3866
+ lane,
3867
+ admit: () => resolve(releaseOnce),
3868
+ reject,
3869
+ timer: setTimeout(() => {
3870
+ queue = queue.filter((item) => item !== waiter);
3871
+ reject(new PluginRequestError("starved", "The plugin is holding every connection it is allowed to open."));
3872
+ }, QUEUE_WAIT_BUDGET_MS)
3873
+ };
3874
+ queue.push(waiter);
3875
+ });
3354
3876
  }
3355
- function statusGlyph(status) {
3356
- if (status === "running") return "●";
3357
- if (status === "completed") return "✓";
3358
- if (status === "stopped") return "–";
3359
- return "×";
3877
+ function withQuery(path, query) {
3878
+ if (query === void 0) return path;
3879
+ const encoded = new URLSearchParams(query).toString();
3880
+ return encoded.length === 0 ? path : `${path}?${encoded}`;
3360
3881
  }
3361
- function formatDuration(ms) {
3362
- if (ms < 1e3) return String(Math.max(1, Math.round(ms))) + "ms";
3363
- const seconds = Math.round(ms / 1e3);
3364
- if (seconds < 60) return String(seconds) + "s";
3365
- const minutes = Math.floor(seconds / 60);
3366
- return String(minutes) + "m " + String(seconds % 60) + "s";
3882
+ function failureOf(error, cancel) {
3883
+ if (error instanceof PluginRequestError) return error;
3884
+ if (cancel?.aborted === true) return new PluginRequestError("cancelled", "The caller cancelled the request.");
3885
+ const name = error instanceof Error ? error.name : "";
3886
+ if (name === "TimeoutError" || name === "AbortError") return new PluginRequestError("timeout", "The plugin route did not answer inside its budget.");
3887
+ return new PluginRequestError("http", error instanceof Error ? error.message : String(error));
3367
3888
  }
3368
- function taskMeta(task, t) {
3369
- const parts = [];
3370
- if (task.subagentType !== void 0) parts.push(task.subagentType);
3371
- else if (task.taskType !== void 0) parts.push(task.taskType);
3372
- if (task.usage?.durationMs !== void 0) parts.push(formatDuration(task.usage.durationMs));
3373
- if (task.usage?.totalTokens !== void 0) parts.push(t("tokens", { count: formatTokenCount(task.usage.totalTokens) }));
3374
- if (task.usage?.toolUses !== void 0) parts.push(t("tasksToolUses", { count: task.usage.toolUses }));
3375
- if (task.lastToolName !== void 0) parts.push(t("tasksLastTool", { tool: task.lastToolName }));
3376
- return parts;
3889
+ async function decode(response) {
3890
+ let payload;
3891
+ try {
3892
+ payload = await response.json();
3893
+ } catch {
3894
+ if (response.ok) throw new PluginRequestError("shape", "The plugin route answered with a body this build cannot read.");
3895
+ throw new PluginRequestError("http", `HTTP ${response.status}`, response.status);
3896
+ }
3897
+ if (response.ok) return payload;
3898
+ if (response.status === 404) throw new PluginRequestError("route-missing", "The Host is running an older plugin build without this route.", 404);
3899
+ const record = typeof payload === "object" && payload !== null ? payload : void 0;
3900
+ const message = typeof record?.message === "string" ? record.message : typeof record?.error === "string" ? record.error : `HTTP ${response.status}`;
3901
+ const code = typeof record?.error === "string" ? record.error : void 0;
3902
+ throw new PluginRequestError("http", message, response.status, code);
3903
+ }
3904
+ async function dispatch(lane, method, path, budget, cancel, options) {
3905
+ const url = withQuery(path, options?.query);
3906
+ const key = options?.key ?? `${method} ${url}`;
3907
+ const existing = inFlight.get(key);
3908
+ if (existing !== void 0) return await existing;
3909
+ const run = (async () => {
3910
+ const free = await acquire(lane);
3911
+ try {
3912
+ const timeout = AbortSignal.timeout(clientBudgetMs(budget));
3913
+ const signal = cancel === void 0 ? timeout : AbortSignal.any([cancel, timeout]);
3914
+ return await decode(await send(url, {
3915
+ method,
3916
+ credentials: "same-origin",
3917
+ signal,
3918
+ headers: options?.json === void 0 ? { accept: "application/json" } : {
3919
+ accept: "application/json",
3920
+ "content-type": "application/json"
3921
+ },
3922
+ ...options?.json === void 0 ? {} : { body: JSON.stringify(options.json) }
3923
+ }));
3924
+ } catch (error) {
3925
+ throw failureOf(error, cancel);
3926
+ } finally {
3927
+ free();
3928
+ }
3929
+ })();
3930
+ inFlight.set(key, run);
3931
+ try {
3932
+ return await run;
3933
+ } finally {
3934
+ if (inFlight.get(key) === run) inFlight.delete(key);
3935
+ }
3377
3936
  }
3378
- function TaskActivity({ activity, t }) {
3379
- return /* @__PURE__ */ jsxs("li", {
3380
- style: taskActivityItem,
3381
- children: [/* @__PURE__ */ jsx("span", {
3382
- style: taskActivityGlyph,
3383
- "aria-hidden": "true",
3384
- children: activity.isError === true ? "×" : "›"
3385
- }), /* @__PURE__ */ jsxs("div", {
3386
- style: taskActivityBody,
3387
- children: [
3388
- /* @__PURE__ */ jsx("p", {
3389
- style: taskActivityTitle,
3390
- children: activity.title ?? activity.kind
3391
- }),
3392
- activity.summary === void 0 ? null : /* @__PURE__ */ jsx("p", {
3393
- style: taskActivitySummary,
3394
- children: activity.summary
3395
- }),
3396
- activity.detail === void 0 ? null : /* @__PURE__ */ jsxs("details", {
3397
- style: taskActivityDetail,
3398
- children: [/* @__PURE__ */ jsx("summary", {
3399
- style: taskActivityDetailSummary,
3400
- children: t("detail")
3401
- }), /* @__PURE__ */ jsx("pre", {
3402
- style: detailCode,
3403
- children: activity.detail
3404
- })]
3405
- })
3406
- ]
3407
- })]
3937
+ /** A bounded read of a plugin route. */
3938
+ function pluginRead(path, budget, cancel, options) {
3939
+ return dispatch("read", "GET", path, budget, cancel, options);
3940
+ }
3941
+ /** A bounded write. Writes never coalesce by default: two of them are two
3942
+ * intents, even when their bodies match. */
3943
+ function pluginWrite(path, budget, cancel, options) {
3944
+ const method = options?.method ?? "POST";
3945
+ return dispatch("write", method, path, budget, cancel, {
3946
+ ...options,
3947
+ key: options?.key ?? `${method} ${withQuery(path, options?.query)} #${nextWriteId()}`
3408
3948
  });
3409
3949
  }
3410
- function TaskCard(props) {
3411
- const { task, activities, t } = props;
3412
- const [activityOpen, setActivityOpen] = useState(false);
3413
- const running = task.status === "running";
3414
- const failed = task.status === "failed" || task.status === "killed";
3415
- const meta = taskMeta(task, t);
3416
- return /* @__PURE__ */ jsxs("article", {
3417
- style: {
3418
- ...taskCard,
3419
- ...running ? taskCardRunning : {}
3420
- },
3421
- children: [
3422
- /* @__PURE__ */ jsxs("div", {
3423
- style: taskCardTop,
3424
- children: [/* @__PURE__ */ jsx("span", {
3425
- className: running ? "dsh-claude-act-running" : void 0,
3426
- style: {
3427
- ...taskCardGlyph,
3428
- ...running ? iconChipRunning : {},
3429
- ...failed ? iconChipError : {}
3430
- },
3431
- "aria-hidden": "true",
3432
- children: statusGlyph(task.status)
3433
- }), /* @__PURE__ */ jsxs("div", {
3434
- style: taskCardBody,
3435
- children: [/* @__PURE__ */ jsx("p", {
3436
- style: {
3437
- ...taskTitle,
3438
- ...failed ? { color: "var(--dsw-alias-state-error-primary)" } : {}
3439
- },
3440
- children: task.description
3441
- }), /* @__PURE__ */ jsxs("p", {
3442
- style: taskStatusLine,
3443
- children: [/* @__PURE__ */ jsx("span", { children: t(STATUS_LABEL[task.status] ?? "tasksRunning") }), task.backgrounded === true ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
3444
- "aria-hidden": "true",
3445
- children: " · "
3446
- }), /* @__PURE__ */ jsx("span", { children: t("tasksBackground") })] }) : null]
3447
- })]
3448
- })]
3449
- }),
3450
- meta.length === 0 ? null : /* @__PURE__ */ jsx("p", {
3451
- style: taskMeta$1,
3452
- children: meta.join(" · ")
3453
- }),
3454
- task.summary === void 0 || running ? null : /* @__PURE__ */ jsx("p", {
3455
- style: taskSummary,
3456
- children: task.summary
3457
- }),
3458
- activities.length === 0 ? null : /* @__PURE__ */ jsxs("div", {
3459
- style: taskActivitySection,
3460
- children: [/* @__PURE__ */ jsx("button", {
3461
- type: "button",
3462
- style: taskTextButton,
3463
- "aria-expanded": activityOpen,
3464
- onClick: () => setActivityOpen((value) => !value),
3465
- children: activityOpen ? t("tasksHideActivity") : t("tasksViewActivity")
3466
- }), activityOpen ? /* @__PURE__ */ jsx("ul", {
3467
- style: taskActivityList,
3468
- children: activities.map((activity) => /* @__PURE__ */ jsx(TaskActivity, {
3469
- activity,
3470
- t
3471
- }, `${activity.turn}:${activity.step}:${activity.ordinal}`))
3472
- }) : null]
3473
- })
3474
- ]
3475
- });
3476
- }
3477
- function GroupHeading(props) {
3478
- const { label, count, collapsed, onToggle, action } = props;
3479
- const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", { children: label }), /* @__PURE__ */ jsx("span", {
3480
- style: tasksGroupCount,
3481
- children: count
3482
- })] });
3483
- return /* @__PURE__ */ jsxs("div", {
3484
- style: tasksGroupHeading,
3485
- children: [onToggle === void 0 ? /* @__PURE__ */ jsx("div", {
3486
- style: tasksGroupTitle,
3487
- children: content
3488
- }) : /* @__PURE__ */ jsxs("button", {
3489
- type: "button",
3490
- style: tasksGroupToggle,
3491
- "aria-expanded": !collapsed,
3492
- onClick: onToggle,
3493
- children: [/* @__PURE__ */ jsx("span", {
3494
- style: {
3495
- ...chevron,
3496
- ...collapsed === true ? {} : chevronOpen
3497
- },
3498
- children: "›"
3499
- }), content]
3500
- }), action === void 0 ? null : /* @__PURE__ */ jsx("button", {
3501
- type: "button",
3502
- style: taskTextButton,
3503
- onClick: action.onClick,
3504
- children: action.label
3505
- })]
3506
- });
3507
- }
3508
- function ClaudeTasksPanel({ useClaudeProjection, t, closeDetails, turn }) {
3509
- const projection = useClaudeProjection((value) => value);
3510
- const tasks = useMemo(() => tasksForTurn(projection.tasks?.tasks ?? [], turn), [projection.tasks, turn]);
3511
- useEffect(() => {
3512
- if (!projection.owned || tasks.length === 0) closeDetails();
3513
- }, [
3514
- closeDetails,
3515
- projection.owned,
3516
- tasks.length
3517
- ]);
3518
- const [finishedCollapsed, setFinishedCollapsed] = useState(false);
3519
- const [dismissedSettledIds, setDismissedSettledIds] = useState(() => /* @__PURE__ */ new Set());
3520
- const groups = useMemo(() => visibleTaskGroups(tasks, dismissedSettledIds), [tasks, dismissedSettledIds]);
3521
- const taskActivities = useMemo(() => new Map(tasks.map((task) => [task.taskId, activitiesForTask(projection.activities, task.taskId)])), [projection.activities, tasks]);
3522
- const clearFinished = () => setDismissedSettledIds((previous) => /* @__PURE__ */ new Set([...previous, ...tasks.filter((task) => task.status !== "running").map((task) => task.taskId)]));
3523
- if (!projection.owned) return null;
3524
- return /* @__PURE__ */ jsxs("div", {
3525
- className: detailsCardClass,
3526
- style: tasksPanel,
3527
- children: [
3528
- /* @__PURE__ */ jsxs("style", {
3529
- "data-dsh-claude-panel-icon-styles": true,
3530
- children: [detailsCardCss, panelIconButtonCss]
3531
- }),
3532
- /* @__PURE__ */ jsxs("div", {
3533
- style: tasksHeader,
3534
- children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
3535
- style: tasksHeading,
3536
- children: t("tasksPanelTurn")
3537
- }), /* @__PURE__ */ jsx("span", {
3538
- style: tasksTurnMeta,
3539
- children: t("tasksTurnNumber", { turn })
3540
- })] }), /* @__PURE__ */ jsx("button", {
3541
- type: "button",
3542
- className: panelIconButtonClass,
3543
- "aria-label": t("tasksClose"),
3544
- onClick: closeDetails,
3545
- children: /* @__PURE__ */ jsx(IconCloseOutline16, {})
3546
- })]
3547
- }),
3548
- /* @__PURE__ */ jsxs("div", {
3549
- style: tasksBody,
3550
- children: [/* @__PURE__ */ jsxs("section", {
3551
- "aria-label": t("tasksRunning"),
3552
- children: [/* @__PURE__ */ jsx(GroupHeading, {
3553
- label: t("tasksRunning"),
3554
- count: groups.running.length
3555
- }), groups.running.length === 0 ? /* @__PURE__ */ jsx("p", {
3556
- style: tasksGroupEmpty,
3557
- children: t("tasksNoneRunning")
3558
- }) : /* @__PURE__ */ jsx("div", {
3559
- style: taskCardList,
3560
- children: groups.running.map((task) => /* @__PURE__ */ jsx(TaskCard, {
3561
- task,
3562
- activities: taskActivities.get(task.taskId) ?? [],
3563
- t
3564
- }, task.taskId))
3565
- })]
3566
- }), /* @__PURE__ */ jsxs("section", {
3567
- "aria-label": t("tasksSettled"),
3568
- style: tasksFinishedSection,
3569
- children: [/* @__PURE__ */ jsx(GroupHeading, {
3570
- label: t("tasksSettled"),
3571
- count: groups.finished.length,
3572
- collapsed: finishedCollapsed,
3573
- onToggle: () => setFinishedCollapsed((value) => !value),
3574
- ...groups.finished.length === 0 ? {} : { action: {
3575
- label: t("tasksClear"),
3576
- onClick: clearFinished
3577
- } }
3578
- }), finishedCollapsed || groups.finished.length === 0 ? null : /* @__PURE__ */ jsx("div", {
3579
- style: taskCardList,
3580
- children: groups.finished.map((task) => /* @__PURE__ */ jsx(TaskCard, {
3581
- task,
3582
- activities: taskActivities.get(task.taskId) ?? [],
3583
- t
3584
- }, task.taskId))
3585
- })]
3586
- })]
3587
- })
3588
- ]
3589
- });
3590
- }
3591
- //#endregion
3592
- //#region src/client/ClaudeActivityTail.tsx
3593
- const MAX_HOVER_TASKS = 6;
3594
- function taskGlyph(status) {
3595
- if (status === "failed") return {
3596
- glyph: "×",
3597
- style: tasksHoverGlyphError
3598
- };
3599
- if (status === "completed") return {
3600
- glyph: "✓",
3601
- style: tasksHoverGlyphDone
3602
- };
3603
- return {
3604
- glyph: "●",
3605
- style: tasksHoverGlyphRunning
3606
- };
3607
- }
3608
- function ClaudeTaskLauncher({ turn, tasks, t, openTasks }) {
3609
- const [hovered, setHovered] = useState(false);
3610
- const closeTimer = useRef();
3611
- const turnTasks = useMemo(() => tasksForTurn(tasks, turn), [tasks, turn]);
3612
- const summary = useMemo(() => summarizeTurnTasks(turnTasks), [turnTasks]);
3613
- const open = () => {
3614
- if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
3615
- closeTimer.current = void 0;
3616
- setHovered(true);
3617
- };
3618
- const scheduleClose = () => {
3619
- if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
3620
- closeTimer.current = setTimeout(() => {
3621
- closeTimer.current = void 0;
3622
- setHovered(false);
3623
- }, 350);
3624
- };
3625
- useEffect(() => () => {
3626
- if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
3627
- }, []);
3628
- if (summary === void 0) return null;
3629
- const label = summary.state === "running" ? t("tasksTurnRunning", { count: summary.running }) : summary.state === "failed" ? t("tasksTurnFailed", {
3630
- failed: summary.failed,
3631
- completed: summary.completed
3632
- }) : t("tasksTurnCompleted", { count: summary.completed });
3633
- const stateStyle = summary.state === "completed" ? tasksBadgeDone : {};
3634
- const dotStyle = summary.state === "failed" ? tasksBadgeDotError : summary.state === "completed" ? tasksBadgeDotDone : {};
3635
- return /* @__PURE__ */ jsx("div", {
3636
- "data-claude-task-launcher": turn,
3637
- style: tasksBadgeWrap,
3638
- children: /* @__PURE__ */ jsxs("span", {
3639
- style: tasksBadgeSeat,
3640
- onMouseEnter: open,
3641
- onMouseLeave: scheduleClose,
3642
- onFocus: open,
3643
- onBlur: (event) => {
3644
- if (!event.currentTarget.contains(event.relatedTarget)) scheduleClose();
3645
- },
3646
- children: [hovered ? /* @__PURE__ */ jsxs("span", {
3647
- role: "tooltip",
3648
- style: tasksHoverCard,
3649
- onMouseEnter: open,
3650
- onMouseLeave: scheduleClose,
3651
- children: [
3652
- /* @__PURE__ */ jsx("span", {
3653
- style: tasksHoverHeader,
3654
- children: label
3655
- }),
3656
- turnTasks.slice(0, MAX_HOVER_TASKS).map((task) => {
3657
- const { glyph, style } = taskGlyph(task.status);
3658
- return /* @__PURE__ */ jsxs("span", {
3659
- style: tasksHoverRow,
3660
- children: [
3661
- /* @__PURE__ */ jsx("span", {
3662
- className: task.status === "running" ? "dsh-claude-act-running" : void 0,
3663
- style: {
3664
- ...tasksHoverGlyph,
3665
- ...style
3666
- },
3667
- "aria-hidden": "true",
3668
- children: glyph
3669
- }),
3670
- /* @__PURE__ */ jsx("span", {
3671
- style: tasksHoverDesc,
3672
- title: task.description,
3673
- children: task.description
3674
- }),
3675
- task.subagentType === void 0 ? null : /* @__PURE__ */ jsx("span", {
3676
- style: tasksHoverType,
3677
- children: task.subagentType
3678
- })
3679
- ]
3680
- }, task.taskId);
3681
- }),
3682
- turnTasks.length > MAX_HOVER_TASKS ? /* @__PURE__ */ jsxs("span", {
3683
- style: tasksHoverMore,
3684
- children: ["+", turnTasks.length - MAX_HOVER_TASKS]
3685
- }) : null,
3686
- /* @__PURE__ */ jsx("span", {
3687
- style: tasksHoverHint,
3688
- children: t("tasksOpen")
3689
- })
3690
- ]
3691
- }) : null, /* @__PURE__ */ jsxs("button", {
3692
- type: "button",
3693
- className: "dsh-claude-task-launcher",
3694
- style: {
3695
- ...tasksTurnBadge,
3696
- ...stateStyle,
3697
- ...hovered ? tasksBadgeHovered : {}
3698
- },
3699
- "aria-label": `${label} — ${t("tasksOpen")}`,
3700
- onClick: () => openTasks(turn),
3701
- children: [/* @__PURE__ */ jsx("span", {
3702
- className: summary.state === "running" ? "dsh-claude-act-running" : void 0,
3703
- style: {
3704
- ...tasksBadgeDot,
3705
- ...dotStyle
3706
- },
3707
- "aria-hidden": "true"
3708
- }), /* @__PURE__ */ jsx("span", {
3709
- style: tasksBadgeLabel,
3710
- children: label
3711
- })]
3712
- })]
3713
- })
3714
- });
3715
- }
3716
- function ClaudeActivityTail({ matched, useClaudeProjection, t, openTasks }) {
3717
- const tasks = useClaudeProjection((value) => value.tasks?.tasks ?? []);
3718
- return /* @__PURE__ */ jsx(ClaudeTaskLauncher, {
3719
- turn: matched.turn,
3720
- tasks,
3721
- t,
3722
- openTasks
3723
- });
3724
- }
3725
- //#endregion
3726
- //#region src/client/ClaudeActiveTasksNode.tsx
3727
- const EMPTY_TASKS$1 = [];
3728
- /** Render the task launcher while the owning DSH turn is still open. */
3729
- function ClaudeActiveTasksNode({ node, useClaudeProjection, t, openTasks }) {
3730
- const tasks = useClaudeProjection((value) => value.tasks?.tasks ?? EMPTY_TASKS$1);
3731
- return /* @__PURE__ */ jsx(ClaudeTaskLauncher, {
3732
- turn: node.data.turn,
3733
- tasks,
3734
- t,
3735
- openTasks
3736
- });
3737
- }
3738
- //#endregion
3739
- //#region src/client/markdown-labels.tsx
3740
- /** Stable across renders: MarkdownText keys its streaming renderer on this
3741
- * object's identity and reparses from scratch whenever it changes. */
3742
- function useClaudeMarkdownLabels(t) {
3743
- return useMemo(() => ({
3744
- code: {
3745
- copyLabel: t("markdownCopy"),
3746
- copiedLabel: t("markdownCopied")
3747
- },
3748
- footnotes: t("markdownFootnotes")
3749
- }), [t]);
3950
+ let writeId = 0;
3951
+ function nextWriteId() {
3952
+ writeId += 1;
3953
+ return writeId;
3750
3954
  }
3751
- /** MarkdownText with the labels the running Host demands.
3752
- *
3753
- * The published primitives package still types the old `codeLabels` prop, so
3754
- * the new shape cannot typecheck against it — the cast is confined here rather
3755
- * than repeated at every call site. Drop it once the installed
3756
- * @deepseek-ai/dsh-client-ui-primitives matches the Desktop build. */
3757
- function ClaudeMarkdown({ text, labels, streaming }) {
3758
- return /* @__PURE__ */ jsx(MarkdownText, {
3759
- text,
3760
- labels,
3761
- ...streaming === void 0 ? {} : { streaming }
3762
- });
3955
+ async function openStream(lane, path, cancel, options, reserved) {
3956
+ const url = withQuery(path, options?.query);
3957
+ const free = reserved ? () => {
3958
+ projectionHeld = false;
3959
+ } : await acquire(lane);
3960
+ try {
3961
+ const response = await send(url, {
3962
+ method: options?.method ?? "GET",
3963
+ credentials: "same-origin",
3964
+ signal: cancel,
3965
+ headers: options?.json === void 0 ? { accept: "application/x-ndjson" } : {
3966
+ accept: "application/x-ndjson",
3967
+ "content-type": "application/json"
3968
+ },
3969
+ ...options?.json === void 0 ? {} : { body: JSON.stringify(options.json) }
3970
+ });
3971
+ if (!response.ok || response.body === null) {
3972
+ free();
3973
+ if (response.status === 404) throw new PluginRequestError("route-missing", "The Host is running an older plugin build without this route.", 404);
3974
+ throw new PluginRequestError("http", `HTTP ${response.status}`, response.status);
3975
+ }
3976
+ cancel.addEventListener("abort", free, { once: true });
3977
+ return response.body.getReader();
3978
+ } catch (error) {
3979
+ free();
3980
+ throw failureOf(error, cancel);
3981
+ }
3763
3982
  }
3764
- //#endregion
3765
- //#region src/rewind.ts
3766
- function isRewound(ranges, seq) {
3767
- return ranges.some((range) => seq >= range.start && seq <= range.end);
3983
+ /** A long-lived NDJSON response. No deadline — it is meant to stay open — but
3984
+ * it takes a counted permit for its whole life, which is the bound that
3985
+ * matters. */
3986
+ function pluginNdjson(path, cancel, options) {
3987
+ return openStream("stream", path, cancel, options, false);
3988
+ }
3989
+ /** The reserved projection carrier: exactly one live connection, ever,
3990
+ * whatever the session count. */
3991
+ function pluginProjectionStream(path, cancel, options) {
3992
+ if (projectionHeld) return Promise.reject(new PluginRequestError("starved", "The projection carrier is already open."));
3993
+ projectionHeld = true;
3994
+ return openStream("stream", path, cancel, options, true);
3995
+ }
3996
+ /** Fire-and-forget diagnostics. Dropped rather than queued when saturated:
3997
+ * the channel that reports the plugin's own failures must never be the
3998
+ * traffic that causes them. */
3999
+ function pluginBeacon(path, body) {
4000
+ if (!laneHasRoom("write")) return;
4001
+ pluginWrite(path, "fast", void 0, { json: body }).catch(() => void 0);
3768
4002
  }
3769
4003
  //#endregion
3770
4004
  //#region src/client/projection.ts
@@ -3776,6 +4010,13 @@ window.__ModuleLoader__.load({
3776
4010
  activities: []
3777
4011
  };
3778
4012
  const RETRY_DELAY_MS = 2e3;
4013
+ /** Floor between carrier reopens forced by a desync. A carrier that is losing
4014
+ * lines must not be answered with a reconnect per lost line. */
4015
+ const RESYNC_COOLDOWN_MS = 5e3;
4016
+ /** Wait for the subscribed set to stop moving before reopening the carrier:
4017
+ * mounting a session list changes it once per row. */
4018
+ const SUBSCRIPTION_SETTLE_MS = 250;
4019
+ const NDJSON_SEPARATOR = String.fromCharCode(10);
3779
4020
  /** Coalesce stream deltas into at most one React notification per frame. */
3780
4021
  const FRAME_MS = 16;
3781
4022
  /** Typewriter smoothing: drain newly arrived prose over roughly this window,
@@ -3899,11 +4140,19 @@ window.__ModuleLoader__.load({
3899
4140
  setTimeout(resolve, ms).unref?.();
3900
4141
  });
3901
4142
  }
3902
- /** Create one lazy source: active subscribers open the Host NDJSON stream, and
3903
- * a dropped stream reconnects with a fresh snapshot after a bounded delay. */
3904
- function createClaudeProjectionSource(sessionId, fetchProjection = fetch, retryDelayMs = RETRY_DELAY_MS) {
4143
+ /** One session's reducer over the shared carrier's lines.
4144
+ *
4145
+ * A source opens nothing. It used to hold a stream of its own, which made the
4146
+ * plugin's connection count a function of how many sessions existed;
4147
+ * {@link ClaudeProjectionStore} now owns the single carrier and feeds each
4148
+ * session's lines here. `onDemand` reports whether anyone is watching, which
4149
+ * is what the store uses to decide which sessions the carrier subscribes to. */
4150
+ function createClaudeProjectionSource(sessionId, onDemand = () => {}, onDesync = () => {}) {
3905
4151
  let snapshot = EMPTY_CLAUDE_PROJECTION;
3906
4152
  let revision = 0;
4153
+ /** Last carrier line this reducer applied, by the server's count. Undefined
4154
+ * until a snapshot states where the stream stands. */
4155
+ let seq;
3907
4156
  let owned = false;
3908
4157
  let commands = [];
3909
4158
  let contextUsage;
@@ -3916,8 +4165,6 @@ window.__ModuleLoader__.load({
3916
4165
  /** Streaming prose still being revealed: full arrived text plus shown chars. */
3917
4166
  const reveal = /* @__PURE__ */ new Map();
3918
4167
  let disposed = false;
3919
- let running = false;
3920
- let controller;
3921
4168
  let frame;
3922
4169
  let usedAnimationFrame = false;
3923
4170
  const listeners = /* @__PURE__ */ new Set();
@@ -4016,7 +4263,7 @@ window.__ModuleLoader__.load({
4016
4263
  }
4017
4264
  };
4018
4265
  const applyText = (event) => {
4019
- const { turn, step, ordinal, append, text } = event;
4266
+ const { turn, step, ordinal, append, text, renderer } = event;
4020
4267
  if (!nonNegativeInteger(turn) || !nonNegativeInteger(step) || !nonNegativeInteger(ordinal)) return false;
4021
4268
  if (append !== void 0 && (typeof append !== "string" || append.length > MAX_TRANSCRIPT_CHARS)) return false;
4022
4269
  if (text !== void 0 && (typeof text !== "string" || text.length > MAX_TRANSCRIPT_CHARS)) return false;
@@ -4033,7 +4280,8 @@ window.__ModuleLoader__.load({
4033
4280
  step,
4034
4281
  ordinal,
4035
4282
  kind: "text",
4036
- phase: "updated"
4283
+ phase: "updated",
4284
+ ...isClaudeRenderMode(renderer) ? { renderer } : {}
4037
4285
  },
4038
4286
  text: fullText
4039
4287
  };
@@ -4053,6 +4301,17 @@ window.__ModuleLoader__.load({
4053
4301
  });
4054
4302
  return true;
4055
4303
  };
4304
+ /** This reducer is behind the server and cannot catch up on its own: only a
4305
+ * fresh snapshot can. Reported as well as acted on, because a silent
4306
+ * self-heal hides how often the carrier is losing lines.
4307
+ *
4308
+ * Numbering stops until that snapshot arrives and states it again; every
4309
+ * line in between would only be measured against a count already known to
4310
+ * be wrong. */
4311
+ const desync = (kind, detail) => {
4312
+ seq = void 0;
4313
+ onDesync(kind, `${sessionId}: ${detail}`);
4314
+ };
4056
4315
  const applyLine = (line) => {
4057
4316
  const trimmed = line.trim();
4058
4317
  if (trimmed.length === 0) return;
@@ -4064,10 +4323,16 @@ window.__ModuleLoader__.load({
4064
4323
  }
4065
4324
  const event = record$7(value);
4066
4325
  if (event === void 0 || typeof event.type !== "string") return;
4326
+ if (event.type === "checkpoint") {
4327
+ if (nonNegativeInteger(event.seq) && seq !== void 0 && event.seq !== seq) desync("projection-gap", `checkpoint at ${event.seq}, applied ${seq}`);
4328
+ return;
4329
+ }
4330
+ if (event.type !== "snapshot" && nonNegativeInteger(event.seq) && seq !== void 0 && event.seq !== seq + 1) desync("projection-gap", `expected ${seq + 1}, received ${event.seq}`);
4067
4331
  try {
4068
4332
  switch (event.type) {
4069
4333
  case "snapshot": {
4070
4334
  const next = parseClaudeClientProjection(event);
4335
+ seq = nonNegativeInteger(event.seq) ? event.seq : void 0;
4071
4336
  revision = next.revision;
4072
4337
  owned = next.owned;
4073
4338
  commands = next.commands;
@@ -4080,7 +4345,10 @@ window.__ModuleLoader__.load({
4080
4345
  break;
4081
4346
  }
4082
4347
  case "text":
4083
- if (!applyText(event)) return;
4348
+ if (!applyText(event)) {
4349
+ if (seq !== void 0) desync("projection-delta-rejected", "text not applied");
4350
+ return;
4351
+ }
4084
4352
  revision += 1;
4085
4353
  break;
4086
4354
  case "activity":
@@ -4114,92 +4382,197 @@ window.__ModuleLoader__.load({
4114
4382
  default: return;
4115
4383
  }
4116
4384
  } catch {
4385
+ desync("projection-delta-rejected", `rejected ${event.type}`);
4117
4386
  return;
4118
4387
  }
4388
+ if (nonNegativeInteger(event.seq)) seq = event.seq;
4119
4389
  schedulePublish();
4120
4390
  };
4121
- const run = async () => {
4122
- if (running) return;
4123
- running = true;
4124
- try {
4125
- while (!disposed && listeners.size > 0) {
4126
- controller = new AbortController();
4127
- try {
4128
- const response = await fetchProjection(`${CLAUDE_PROJECTION_PATH}/${encodeURIComponent(sessionId)}/stream`, {
4129
- headers: { accept: "application/x-ndjson" },
4130
- signal: controller.signal
4131
- });
4132
- if (!response.ok) throw new Error(`Claude projection stream failed (${response.status})`);
4133
- if (response.body === null) throw new Error("Claude projection stream is unavailable");
4134
- const reader = response.body.getReader();
4135
- const decoder = new TextDecoder();
4136
- let buffer = "";
4137
- while (true) {
4138
- if (disposed || listeners.size === 0) {
4139
- await reader.cancel().catch(() => void 0);
4140
- break;
4141
- }
4142
- const chunk = await reader.read();
4143
- buffer += decoder.decode(chunk.value, { stream: !chunk.done });
4144
- const lines = buffer.split("\n");
4145
- buffer = lines.pop() ?? "";
4146
- for (const line of lines) applyLine(line);
4147
- if (chunk.done) break;
4148
- }
4149
- } catch (error) {
4150
- if (isAbort(error)) return;
4151
- } finally {
4152
- controller = void 0;
4153
- }
4154
- if (disposed || listeners.size === 0) return;
4155
- await delay(retryDelayMs);
4156
- }
4157
- } finally {
4158
- running = false;
4159
- }
4160
- };
4161
4391
  return {
4162
4392
  getSnapshot: () => snapshot,
4393
+ feed: applyLine,
4163
4394
  subscribe(listener) {
4164
4395
  if (disposed) return () => {};
4165
4396
  const wasIdle = listeners.size === 0;
4166
4397
  listeners.add(listener);
4167
- if (wasIdle) run();
4398
+ if (wasIdle) onDemand(true);
4168
4399
  return () => {
4169
4400
  listeners.delete(listener);
4170
4401
  if (listeners.size !== 0) return;
4171
- controller?.abort();
4172
- controller = void 0;
4173
4402
  cancelFrame();
4403
+ onDemand(false);
4174
4404
  };
4175
4405
  },
4176
4406
  dispose() {
4177
4407
  disposed = true;
4178
4408
  listeners.clear();
4179
- controller?.abort();
4180
- controller = void 0;
4181
4409
  cancelFrame();
4410
+ onDemand(false);
4182
4411
  }
4183
4412
  };
4184
4413
  }
4414
+ /**
4415
+ * Every session's projection over ONE connection.
4416
+ *
4417
+ * The plugin used to open an NDJSON stream per session, so its share of the
4418
+ * browser's small per-origin connection budget grew with the number of Claude
4419
+ * sessions — and the overview panel subscribes one per LISTED session, not per
4420
+ * open one. Past a handful of sessions the plugin's own settings panel could no
4421
+ * longer get a connection at all, which is the failure this class exists to
4422
+ * make impossible: the carrier is one connection whatever the session count.
4423
+ *
4424
+ * `source(sessionId)` keeps its shape, so consumers are unaware of any of this.
4425
+ */
4185
4426
  var ClaudeProjectionStore = class {
4186
4427
  #sources = /* @__PURE__ */ new Map();
4428
+ /** Sessions with at least one live subscriber, newest interest last. */
4429
+ #wanted = /* @__PURE__ */ new Set();
4430
+ #open;
4431
+ #retryDelayMs;
4432
+ #settleMs;
4433
+ #report;
4434
+ #resyncCooldownMs;
4435
+ #resyncedAt = 0;
4436
+ #controller;
4437
+ #settle;
4438
+ #running = false;
4439
+ #disposed = false;
4440
+ constructor(options = {}) {
4441
+ this.#open = options.open ?? ((path, cancel) => pluginProjectionStream(path, cancel));
4442
+ this.#retryDelayMs = options.retryDelayMs ?? RETRY_DELAY_MS;
4443
+ this.#settleMs = options.settleMs ?? SUBSCRIPTION_SETTLE_MS;
4444
+ this.#report = options.report ?? (() => {});
4445
+ this.#resyncCooldownMs = options.resyncCooldownMs ?? RESYNC_COOLDOWN_MS;
4446
+ }
4187
4447
  source(sessionId) {
4188
4448
  let source = this.#sources.get(sessionId);
4189
4449
  if (source === void 0) {
4190
- source = createClaudeProjectionSource(sessionId);
4450
+ source = createClaudeProjectionSource(sessionId, (active) => {
4451
+ this.#demand(sessionId, active);
4452
+ }, (kind, detail) => {
4453
+ this.#resync(kind, detail);
4454
+ });
4191
4455
  this.#sources.set(sessionId, source);
4192
4456
  }
4193
4457
  return source;
4194
4458
  }
4459
+ /** Reopen the carrier so every lane is restated from a fresh snapshot.
4460
+ *
4461
+ * One session noticed the hole, but the carrier is shared and a dropped
4462
+ * line is a property of the carrier, so the others are suspect too --
4463
+ * reopening restates all of them for the price of the one reconnect. */
4464
+ #resync(kind, detail) {
4465
+ if (this.#disposed) return;
4466
+ this.#report(kind, detail);
4467
+ const now = Date.now();
4468
+ if (now - this.#resyncedAt < this.#resyncCooldownMs) return;
4469
+ this.#resyncedAt = now;
4470
+ this.#reopen();
4471
+ }
4195
4472
  dispose() {
4473
+ this.#disposed = true;
4474
+ if (this.#settle !== void 0) clearTimeout(this.#settle);
4475
+ this.#settle = void 0;
4476
+ this.#controller?.abort();
4477
+ this.#controller = void 0;
4196
4478
  for (const source of this.#sources.values()) source.dispose();
4197
4479
  this.#sources.clear();
4480
+ this.#wanted.clear();
4481
+ }
4482
+ /** Note interest and reopen the carrier once the set stops moving. Mounting
4483
+ * a session list would otherwise reopen it once per row. */
4484
+ #demand(sessionId, active) {
4485
+ if (this.#disposed) return;
4486
+ if (active) {
4487
+ this.#wanted.delete(sessionId);
4488
+ this.#wanted.add(sessionId);
4489
+ } else if (!this.#wanted.delete(sessionId)) return;
4490
+ if (this.#settle !== void 0) clearTimeout(this.#settle);
4491
+ const timer = setTimeout(() => {
4492
+ this.#settle = void 0;
4493
+ this.#reopen();
4494
+ }, this.#settleMs);
4495
+ timer.unref?.();
4496
+ this.#settle = timer;
4497
+ }
4498
+ #lanes() {
4499
+ const wanted = [...this.#wanted];
4500
+ return wanted.slice(Math.max(0, wanted.length - 16));
4501
+ }
4502
+ #reopen() {
4503
+ this.#controller?.abort();
4504
+ this.#controller = void 0;
4505
+ if (this.#disposed || this.#wanted.size === 0) return;
4506
+ this.#run();
4507
+ }
4508
+ async #run() {
4509
+ if (this.#running) return;
4510
+ this.#running = true;
4511
+ try {
4512
+ while (!this.#disposed && this.#wanted.size > 0) {
4513
+ const controller = new AbortController();
4514
+ this.#controller = controller;
4515
+ const lanes = this.#lanes();
4516
+ let superseded = false;
4517
+ try {
4518
+ const reader = await this.#open(`${CLAUDE_PROJECTION_PATH}/multi?sessions=${lanes.map(encodeURIComponent).join(",")}`, controller.signal);
4519
+ const stop = () => {
4520
+ superseded = true;
4521
+ reader.cancel().catch(() => void 0);
4522
+ };
4523
+ controller.signal.addEventListener("abort", stop, { once: true });
4524
+ const decoder = new TextDecoder();
4525
+ let buffer = "";
4526
+ while (!controller.signal.aborted) {
4527
+ const chunk = await reader.read();
4528
+ buffer += decoder.decode(chunk.value, { stream: !chunk.done });
4529
+ const lines = buffer.split(NDJSON_SEPARATOR);
4530
+ buffer = lines.pop() ?? "";
4531
+ for (const line of lines) this.#dispatch(line);
4532
+ if (chunk.done) break;
4533
+ }
4534
+ controller.signal.removeEventListener("abort", stop);
4535
+ await reader.cancel().catch(() => void 0);
4536
+ } catch (error) {
4537
+ if (isAbort(error)) {
4538
+ if (this.#controller !== controller) continue;
4539
+ return;
4540
+ }
4541
+ } finally {
4542
+ if (this.#controller === controller) this.#controller = void 0;
4543
+ }
4544
+ if (this.#disposed || this.#wanted.size === 0) return;
4545
+ if (superseded) continue;
4546
+ await delay(this.#retryDelayMs);
4547
+ }
4548
+ } finally {
4549
+ this.#running = false;
4550
+ }
4551
+ }
4552
+ /** Route one carrier line to the session that owns it. A line for a session
4553
+ * nobody is watching any more is dropped rather than reviving its lane. */
4554
+ #dispatch(line) {
4555
+ if (line.length === 0) return;
4556
+ let session;
4557
+ try {
4558
+ session = JSON.parse(line).session;
4559
+ } catch {
4560
+ return;
4561
+ }
4562
+ if (typeof session !== "string") return;
4563
+ this.#sources.get(session)?.feed(line);
4198
4564
  }
4199
4565
  };
4200
4566
  //#endregion
4567
+ //#region src/client/token-format.ts
4568
+ function formatTokenCount(tokens) {
4569
+ if (tokens >= 1e6) return `${Number((tokens / 1e6).toFixed(tokens >= 1e7 ? 0 : 1))}M`;
4570
+ if (tokens >= 1e3) return `${Number((tokens / 1e3).toFixed(tokens >= 1e5 ? 0 : 1))}K`;
4571
+ return String(tokens);
4572
+ }
4573
+ //#endregion
4201
4574
  //#region src/client/ClaudeActivityNode.tsx
4202
- const EMPTY_TASKS = [];
4575
+ const EMPTY_TASKS$1 = [];
4203
4576
  const ACTIVITY_CSS = [
4204
4577
  ".dsh-claude-flow{display:flex;flex-direction:column;gap:10px}",
4205
4578
  ".dsh-claude-transcript-text{color:var(--dsw-alias-label-primary);font-size:15px;line-height:24px;overflow-wrap:anywhere}",
@@ -4232,6 +4605,13 @@ window.__ModuleLoader__.load({
4232
4605
  ".dsh-claude-diff-delete{color:var(--dsw-alias-state-error-primary)}",
4233
4606
  ".dsh-claude-tool-name{font-size:14px;line-height:22px;color:var(--dsw-alias-label-primary)}",
4234
4607
  ".dsh-claude-tool-summary{margin-left:8px;color:var(--dsw-alias-label-tertiary)}",
4608
+ ".dsh-claude-turn-usage{display:flex;flex-wrap:wrap;align-items:center;gap:5px;margin-top:10px;",
4609
+ "color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}",
4610
+ ".dsh-claude-turn-usage-label{color:var(--dsw-alias-label-caption)}",
4611
+ ".dsh-claude-tool-terminal{display:flex;gap:8px;margin:6px 0 0;padding:8px 10px;max-height:220px;overflow:auto;",
4612
+ "border-radius:8px;background:var(--dsw-alias-markdown-code-block);font:var(--dsw-font-markdown-code-block-small)}",
4613
+ ".dsh-claude-tool-prompt{flex:none;user-select:none;color:var(--dsw-alias-label-caption)}",
4614
+ ".dsh-claude-tool-command{min-width:0;margin:0;color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere}",
4235
4615
  ".dsh-claude-tool-detail{margin:6px 0 0;padding:8px 10px;max-height:220px;overflow:auto;border-radius:8px;background:var(--dsw-alias-markdown-code-block);font:var(--dsw-font-markdown-code-block-small);white-space:pre-wrap;overflow-wrap:anywhere}",
4236
4616
  ".dsh-claude-flow-row{position:relative;overflow:hidden}",
4237
4617
  ".dsh-claude-flow-leading{flex-shrink:0}",
@@ -4407,6 +4787,21 @@ window.__ModuleLoader__.load({
4407
4787
  }, index))
4408
4788
  });
4409
4789
  }
4790
+ /** The command as a shell prompt rather than a labelled field: it was typed
4791
+ * at one, and the prompt is what tells a reader that at a glance. */
4792
+ function Terminal({ command }) {
4793
+ return /* @__PURE__ */ jsxs("div", {
4794
+ className: "dsh-claude-tool-terminal",
4795
+ children: [/* @__PURE__ */ jsx("span", {
4796
+ className: "dsh-claude-tool-prompt",
4797
+ "aria-hidden": "true",
4798
+ children: "$"
4799
+ }), /* @__PURE__ */ jsx("pre", {
4800
+ className: "dsh-claude-tool-command",
4801
+ children: command
4802
+ })]
4803
+ });
4804
+ }
4410
4805
  function TextDetail({ title, value }) {
4411
4806
  if (value === void 0 || value.length === 0) return null;
4412
4807
  return /* @__PURE__ */ jsx(Section, {
@@ -4479,176 +4874,633 @@ window.__ModuleLoader__.load({
4479
4874
  if (tool.toolName === "Bash" || tool.toolName === "PowerShell") {
4480
4875
  const command = text$1(input?.command);
4481
4876
  const terminal = [text$1(output?.stdout), text$1(output?.stderr)].filter((value) => value !== void 0).join("\n");
4877
+ const typed = command ?? (typeof inputValue === "string" ? inputValue : void 0);
4482
4878
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [
4483
- /* @__PURE__ */ jsx(TextDetail, {
4484
- title: "Command",
4485
- value: command ?? (typeof inputValue === "string" ? inputValue : void 0)
4486
- }),
4879
+ typed === void 0 ? null : /* @__PURE__ */ jsx(Terminal, { command: typed }),
4487
4880
  input === void 0 ? null : /* @__PURE__ */ jsx(Section, {
4488
4881
  title: t("toolInput"),
4489
4882
  children: /* @__PURE__ */ jsx(Fields, {
4490
4883
  value: input,
4491
- omit: ["command"]
4884
+ omit: ["command", "description"]
4492
4885
  })
4493
4886
  }),
4494
4887
  /* @__PURE__ */ jsx(TextDetail, {
4495
4888
  title: outputTitle,
4496
4889
  value: terminal || (typeof outputValue === "string" ? outputValue : void 0)
4497
4890
  })
4498
- ] });
4499
- }
4500
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [input === void 0 ? /* @__PURE__ */ jsx(TextDetail, {
4501
- title: t("toolInput"),
4502
- value: typeof inputValue === "string" ? inputValue : void 0
4503
- }) : /* @__PURE__ */ jsx(Section, {
4504
- title: t("toolInput"),
4505
- children: /* @__PURE__ */ jsx(Fields, { value: input })
4506
- }), output === void 0 ? /* @__PURE__ */ jsx(TextDetail, {
4507
- title: outputTitle,
4508
- value: typeof outputValue === "string" ? outputValue : void 0
4509
- }) : /* @__PURE__ */ jsx(Section, {
4510
- title: outputTitle,
4511
- children: /* @__PURE__ */ jsx(Fields, { value: output })
4512
- })] });
4891
+ ] });
4892
+ }
4893
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [input === void 0 ? /* @__PURE__ */ jsx(TextDetail, {
4894
+ title: t("toolInput"),
4895
+ value: typeof inputValue === "string" ? inputValue : void 0
4896
+ }) : /* @__PURE__ */ jsx(Section, {
4897
+ title: t("toolInput"),
4898
+ children: /* @__PURE__ */ jsx(Fields, { value: input })
4899
+ }), output === void 0 ? /* @__PURE__ */ jsx(TextDetail, {
4900
+ title: outputTitle,
4901
+ value: typeof outputValue === "string" ? outputValue : void 0
4902
+ }) : /* @__PURE__ */ jsx(Section, {
4903
+ title: outputTitle,
4904
+ children: /* @__PURE__ */ jsx(Fields, { value: output })
4905
+ })] });
4906
+ }
4907
+ function ClaudeTranscriptToolItem({ tool, t }) {
4908
+ return /* @__PURE__ */ jsxs("details", {
4909
+ className: "dsh-claude-tool-item",
4910
+ children: [/* @__PURE__ */ jsxs("summary", {
4911
+ className: "dsh-claude-tool-summary-row",
4912
+ children: [
4913
+ /* @__PURE__ */ jsx("span", {
4914
+ className: "dsh-claude-tool-label",
4915
+ children: tool.toolName
4916
+ }),
4917
+ /* @__PURE__ */ jsx("span", {
4918
+ className: "dsh-claude-tool-description",
4919
+ children: tool.description
4920
+ }),
4921
+ tool.additions === void 0 && tool.deletions === void 0 ? null : /* @__PURE__ */ jsxs("span", {
4922
+ className: "dsh-claude-tool-stats",
4923
+ children: [/* @__PURE__ */ jsxs("span", {
4924
+ className: "dsh-claude-diff-add",
4925
+ children: ["+", tool.additions ?? 0]
4926
+ }), /* @__PURE__ */ jsxs("span", {
4927
+ className: "dsh-claude-diff-delete",
4928
+ children: ["−", tool.deletions ?? 0]
4929
+ })]
4930
+ })
4931
+ ]
4932
+ }), /* @__PURE__ */ jsxs("div", {
4933
+ className: "dsh-claude-tool-content",
4934
+ children: [tool.subcalls.length === 0 ? null : /* @__PURE__ */ jsx("div", {
4935
+ className: "dsh-claude-flow-subcalls",
4936
+ children: tool.subcalls.map((subcall) => /* @__PURE__ */ jsxs("div", { children: [
4937
+ subcallGlyph(subcall),
4938
+ " ",
4939
+ subcall.toolName ?? t("subagent"),
4940
+ subcall.summary === void 0 ? "" : ` · ${subcall.summary}`
4941
+ ] }, subcall.toolUseId))
4942
+ }), /* @__PURE__ */ jsx(ToolPresentation, {
4943
+ tool,
4944
+ t
4945
+ })]
4946
+ })]
4947
+ });
4948
+ }
4949
+ function ClaudeTranscriptToolGroup({ tools, additions, deletions, files: _files, t }) {
4950
+ const [open, setOpen] = useState(false);
4951
+ const failed = tools.some((tool) => tool.isError === true || tool.phase === "failed");
4952
+ const running = tools.some((tool) => tool.phase === "started" || tool.phase === "updated");
4953
+ const summary = tools.length === 1 ? t("usedTool") : t("usedTools", { count: tools.length });
4954
+ return /* @__PURE__ */ jsxs("div", {
4955
+ className: "dsh-claude-tool-group-native",
4956
+ children: [/* @__PURE__ */ jsx(DisclosureRow, {
4957
+ rowClassName: "dsh-claude-flow-row",
4958
+ leadingClassName: "dsh-claude-flow-leading",
4959
+ titleClassName: "dsh-claude-flow-title",
4960
+ chevronClassName: "dsh-claude-flow-chevron",
4961
+ icon: failed ? /* @__PURE__ */ jsx(StateDot, { state: "error" }) : running ? /* @__PURE__ */ jsx(StateDot, { state: "ongoing" }) : /* @__PURE__ */ jsx(IconApiOutline14, { size: 14 }),
4962
+ title: summary,
4963
+ open,
4964
+ expandable: true,
4965
+ expandOnRowClick: true,
4966
+ keepContentWhenOpen: true,
4967
+ onToggle: () => setOpen((value) => !value),
4968
+ collapsedContent: additions !== void 0 || deletions !== void 0 ? /* @__PURE__ */ jsxs("span", {
4969
+ className: "dsh-claude-tool-stats",
4970
+ children: [/* @__PURE__ */ jsxs("span", {
4971
+ className: "dsh-claude-diff-add",
4972
+ children: ["+", additions ?? 0]
4973
+ }), /* @__PURE__ */ jsxs("span", {
4974
+ className: "dsh-claude-diff-delete",
4975
+ children: ["−", deletions ?? 0]
4976
+ })]
4977
+ }) : void 0
4978
+ }), open ? /* @__PURE__ */ jsx("div", {
4979
+ className: "dsh-claude-tool-list",
4980
+ children: tools.map((tool) => /* @__PURE__ */ jsx(ClaudeTranscriptToolItem, {
4981
+ tool,
4982
+ t
4983
+ }, tool.toolUseId))
4984
+ }) : null]
4985
+ });
4986
+ }
4987
+ /** Round a duration the way a reader reads one: no more precision than the
4988
+ * number deserves. */
4989
+ function formatTurnDuration(ms) {
4990
+ if (ms < 1e3) return `${Math.max(1, Math.round(ms))}ms`;
4991
+ const seconds = ms / 1e3;
4992
+ if (seconds < 60) return `${seconds < 10 ? seconds.toFixed(1) : String(Math.round(seconds))}s`;
4993
+ const whole = Math.round(seconds);
4994
+ return `${Math.floor(whole / 60)}m ${String(whole % 60).padStart(2, "0")}s`;
4995
+ }
4996
+ /** Share of the prompt that was served from cache.
4997
+ *
4998
+ * Cache reads are counted against everything the prompt cost to assemble --
4999
+ * fresh input and cache writes included -- so a turn that read nothing scores
5000
+ * zero rather than dividing by nothing. */
5001
+ function cacheHitRate(usage) {
5002
+ const read = usage.cacheReadTokens ?? 0;
5003
+ const total = read + (usage.cacheCreationTokens ?? 0) + (usage.inputTokens ?? 0);
5004
+ return total === 0 ? void 0 : read / total;
5005
+ }
5006
+ /** The turn's accounting, in the order a reader wants it: size, then cost of
5007
+ * assembling it, then how long it took, then money. */
5008
+ function turnUsageParts(usage, t) {
5009
+ const tokens = (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0) + (usage.cacheReadTokens ?? 0) + (usage.cacheCreationTokens ?? 0);
5010
+ const cached = cacheHitRate(usage);
5011
+ const parts = [];
5012
+ if (tokens > 0) parts.push(t("turnUsageTokens", { count: formatTokenCount(tokens) }));
5013
+ if (cached !== void 0) parts.push(t("turnUsageCache", { percent: (cached * 100).toFixed(1) }));
5014
+ if (usage.durationMs !== void 0) parts.push(formatTurnDuration(usage.durationMs));
5015
+ if (usage.ttftMs !== void 0) parts.push(t("turnUsageTtft", { duration: formatTurnDuration(usage.ttftMs) }));
5016
+ if (usage.cumulativeCostUsd !== void 0) parts.push(t("turnUsageCost", { cost: usage.cumulativeCostUsd.toFixed(2) }));
5017
+ return parts;
5018
+ }
5019
+ /** The footer the Host draws under its own assistant message, drawn here for
5020
+ * the steps the Host never had a message for. */
5021
+ function ClaudeTurnUsage({ usage, t }) {
5022
+ const parts = turnUsageParts(usage, t);
5023
+ if (parts.length === 0) return null;
5024
+ return /* @__PURE__ */ jsxs("div", {
5025
+ className: "dsh-claude-turn-usage",
5026
+ children: [
5027
+ /* @__PURE__ */ jsx("span", {
5028
+ className: "dsh-claude-turn-usage-label",
5029
+ children: t("turnUsage")
5030
+ }),
5031
+ /* @__PURE__ */ jsx("span", {
5032
+ "aria-hidden": "true",
5033
+ children: "·"
5034
+ }),
5035
+ /* @__PURE__ */ jsx("span", { children: parts.join(" · ") })
5036
+ ]
5037
+ });
5038
+ }
5039
+ function ClaudeActivityNode({ node, useClaudeProjection, t }) {
5040
+ ensureCss$3();
5041
+ const marker = node.data;
5042
+ const activities = useClaudeProjection((value) => selectStepActivities(value, marker.turn, marker.step));
5043
+ const tasks = useClaudeProjection((value) => value.tasks?.tasks ?? EMPTY_TASKS$1);
5044
+ const items = useMemo(() => transcriptItemsForStep(activities, marker.turn, marker.step, tasks), [
5045
+ activities,
5046
+ marker.step,
5047
+ marker.turn,
5048
+ tasks
5049
+ ]);
5050
+ const markdownLabels = useClaudeMarkdownLabels(t);
5051
+ if (items.length === 0) return null;
5052
+ return /* @__PURE__ */ jsx("div", {
5053
+ className: "dsh-claude-flow",
5054
+ children: items.map((item) => item.kind === "text" ? /* @__PURE__ */ jsx("div", {
5055
+ className: "dsh-claude-transcript-text",
5056
+ children: /* @__PURE__ */ jsx(ClaudeMarkdown, {
5057
+ text: item.text,
5058
+ labels: markdownLabels
5059
+ })
5060
+ }, `text:${item.ordinal}`) : item.kind === "compaction" ? /* @__PURE__ */ jsx(ClaudeCompactionDivider, {
5061
+ compaction: item.compaction,
5062
+ t
5063
+ }, `compaction:${item.ordinal}`) : item.kind === "usage" ? /* @__PURE__ */ jsx(ClaudeTurnUsage, {
5064
+ usage: item.usage,
5065
+ t
5066
+ }, `usage:${item.ordinal}`) : item.kind === "tools" ? /* @__PURE__ */ jsx(ClaudeTranscriptToolGroup, {
5067
+ tools: item.tools,
5068
+ ...item.additions === void 0 ? {} : { additions: item.additions },
5069
+ ...item.deletions === void 0 ? {} : { deletions: item.deletions },
5070
+ ...item.files === void 0 ? {} : { files: item.files },
5071
+ t
5072
+ }, `tools:${item.ordinal}`) : /* @__PURE__ */ jsx(ActivityRow, {
5073
+ row: item.row,
5074
+ t
5075
+ }, `activity:${item.ordinal}`))
5076
+ });
4513
5077
  }
4514
- function ClaudeTranscriptToolItem({ tool, t }) {
4515
- return /* @__PURE__ */ jsxs("details", {
4516
- className: "dsh-claude-tool-item",
4517
- children: [/* @__PURE__ */ jsxs("summary", {
4518
- className: "dsh-claude-tool-summary-row",
5078
+ //#endregion
5079
+ //#region src/client/ClaudeTasksPanel.tsx
5080
+ const STATUS_LABEL = {
5081
+ running: "tasksRunning",
5082
+ completed: "tasksCompleted",
5083
+ failed: "tasksFailed",
5084
+ stopped: "tasksStopped",
5085
+ killed: "tasksKilled"
5086
+ };
5087
+ function visibleTaskGroups(tasks, dismissedSettledIds) {
5088
+ const projected = tasks.filter(isProjectedTask);
5089
+ return {
5090
+ running: projected.filter((task) => task.status === "running"),
5091
+ finished: projected.filter((task) => task.status !== "running" && !dismissedSettledIds.has(task.taskId))
5092
+ };
5093
+ }
5094
+ function activitiesForTask(activities, taskId) {
5095
+ return activities.filter((activity) => activity.taskId === taskId);
5096
+ }
5097
+ function tasksForTurn(tasks, turn) {
5098
+ return tasks.filter((task) => task.originTurn === turn && isProjectedTask(task));
5099
+ }
5100
+ function summarizeTurnTasks(tasks) {
5101
+ if (tasks.length === 0) return void 0;
5102
+ const running = tasks.filter((task) => task.status === "running").length;
5103
+ const failed = tasks.filter((task) => task.status === "failed" || task.status === "stopped" || task.status === "killed").length;
5104
+ const completed = tasks.filter((task) => task.status === "completed").length;
5105
+ return {
5106
+ state: running > 0 ? "running" : failed > 0 ? "failed" : "completed",
5107
+ count: tasks.length,
5108
+ running,
5109
+ failed,
5110
+ completed
5111
+ };
5112
+ }
5113
+ function statusGlyph(status) {
5114
+ if (status === "running") return "●";
5115
+ if (status === "completed") return "✓";
5116
+ if (status === "stopped") return "–";
5117
+ return "×";
5118
+ }
5119
+ function formatDuration(ms) {
5120
+ if (ms < 1e3) return String(Math.max(1, Math.round(ms))) + "ms";
5121
+ const seconds = Math.round(ms / 1e3);
5122
+ if (seconds < 60) return String(seconds) + "s";
5123
+ const minutes = Math.floor(seconds / 60);
5124
+ return String(minutes) + "m " + String(seconds % 60) + "s";
5125
+ }
5126
+ function taskMeta(task, t) {
5127
+ const parts = [];
5128
+ if (task.subagentType !== void 0) parts.push(task.subagentType);
5129
+ else if (task.taskType !== void 0) parts.push(task.taskType);
5130
+ if (task.usage?.durationMs !== void 0) parts.push(formatDuration(task.usage.durationMs));
5131
+ if (task.usage?.totalTokens !== void 0) parts.push(t("tokens", { count: formatTokenCount(task.usage.totalTokens) }));
5132
+ if (task.usage?.toolUses !== void 0) parts.push(t("tasksToolUses", { count: task.usage.toolUses }));
5133
+ if (task.lastToolName !== void 0) parts.push(t("tasksLastTool", { tool: task.lastToolName }));
5134
+ return parts;
5135
+ }
5136
+ function TaskActivity({ activity, t }) {
5137
+ return /* @__PURE__ */ jsxs("li", {
5138
+ style: taskActivityItem,
5139
+ children: [/* @__PURE__ */ jsx("span", {
5140
+ style: taskActivityGlyph,
5141
+ "aria-hidden": "true",
5142
+ children: activity.isError === true ? "×" : "›"
5143
+ }), /* @__PURE__ */ jsxs("div", {
5144
+ style: taskActivityBody,
4519
5145
  children: [
4520
- /* @__PURE__ */ jsx("span", {
4521
- className: "dsh-claude-tool-label",
4522
- children: tool.toolName
5146
+ /* @__PURE__ */ jsx("p", {
5147
+ style: taskActivityTitle,
5148
+ children: activity.title ?? activity.kind
4523
5149
  }),
4524
- /* @__PURE__ */ jsx("span", {
4525
- className: "dsh-claude-tool-description",
4526
- children: tool.description
5150
+ activity.summary === void 0 ? null : /* @__PURE__ */ jsx("p", {
5151
+ style: taskActivitySummary,
5152
+ children: activity.summary
4527
5153
  }),
4528
- tool.additions === void 0 && tool.deletions === void 0 ? null : /* @__PURE__ */ jsxs("span", {
4529
- className: "dsh-claude-tool-stats",
4530
- children: [/* @__PURE__ */ jsxs("span", {
4531
- className: "dsh-claude-diff-add",
4532
- children: ["+", tool.additions ?? 0]
4533
- }), /* @__PURE__ */ jsxs("span", {
4534
- className: "dsh-claude-diff-delete",
4535
- children: ["−", tool.deletions ?? 0]
5154
+ activity.detail === void 0 ? null : /* @__PURE__ */ jsxs("details", {
5155
+ style: taskActivityDetail,
5156
+ children: [/* @__PURE__ */ jsx("summary", {
5157
+ style: taskActivityDetailSummary,
5158
+ children: t("detail")
5159
+ }), /* @__PURE__ */ jsx("pre", {
5160
+ style: detailCode,
5161
+ children: activity.detail
4536
5162
  })]
4537
5163
  })
4538
5164
  ]
4539
- }), /* @__PURE__ */ jsxs("div", {
4540
- className: "dsh-claude-tool-content",
4541
- children: [tool.subcalls.length === 0 ? null : /* @__PURE__ */ jsx("div", {
4542
- className: "dsh-claude-flow-subcalls",
4543
- children: tool.subcalls.map((subcall) => /* @__PURE__ */ jsxs("div", { children: [
4544
- subcallGlyph(subcall),
4545
- " ",
4546
- subcall.toolName ?? t("subagent"),
4547
- subcall.summary === void 0 ? "" : ` · ${subcall.summary}`
4548
- ] }, subcall.toolUseId))
4549
- }), /* @__PURE__ */ jsx(ToolPresentation, {
4550
- tool,
4551
- t
4552
- })]
4553
5165
  })]
4554
5166
  });
4555
5167
  }
4556
- function ClaudeTranscriptToolGroup({ tools, additions, deletions, files: _files, t }) {
4557
- const [open, setOpen] = useState(false);
4558
- const failed = tools.some((tool) => tool.isError === true || tool.phase === "failed");
4559
- const running = tools.some((tool) => tool.phase === "started" || tool.phase === "updated");
4560
- const summary = tools.length === 1 ? t("usedTool") : t("usedTools", { count: tools.length });
5168
+ function TaskCard(props) {
5169
+ const { task, activities, allActivities, t } = props;
5170
+ const tools = useMemo(() => taskTools(allActivities, task.taskId), [allActivities, task.taskId]);
5171
+ const [activityOpen, setActivityOpen] = useState(false);
5172
+ const running = task.status === "running";
5173
+ const failed = task.status === "failed" || task.status === "killed";
5174
+ const meta = taskMeta(task, t);
5175
+ return /* @__PURE__ */ jsxs("article", {
5176
+ style: {
5177
+ ...taskCard,
5178
+ ...running ? taskCardRunning : {}
5179
+ },
5180
+ children: [
5181
+ /* @__PURE__ */ jsxs("div", {
5182
+ style: taskCardTop,
5183
+ children: [/* @__PURE__ */ jsx("span", {
5184
+ className: running ? "dsh-claude-act-running" : void 0,
5185
+ style: {
5186
+ ...taskCardGlyph,
5187
+ ...running ? iconChipRunning : {},
5188
+ ...failed ? iconChipError : {}
5189
+ },
5190
+ "aria-hidden": "true",
5191
+ children: statusGlyph(task.status)
5192
+ }), /* @__PURE__ */ jsxs("div", {
5193
+ style: taskCardBody,
5194
+ children: [/* @__PURE__ */ jsx("p", {
5195
+ style: {
5196
+ ...taskTitle,
5197
+ ...failed ? { color: "var(--dsw-alias-state-error-primary)" } : {}
5198
+ },
5199
+ children: task.description
5200
+ }), /* @__PURE__ */ jsxs("p", {
5201
+ style: taskStatusLine,
5202
+ children: [/* @__PURE__ */ jsx("span", { children: t(STATUS_LABEL[task.status] ?? "tasksRunning") }), task.backgrounded === true ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
5203
+ "aria-hidden": "true",
5204
+ children: " · "
5205
+ }), /* @__PURE__ */ jsx("span", { children: t("tasksBackground") })] }) : null]
5206
+ })]
5207
+ })]
5208
+ }),
5209
+ meta.length === 0 ? null : /* @__PURE__ */ jsx("p", {
5210
+ style: taskMeta$1,
5211
+ children: meta.join(" · ")
5212
+ }),
5213
+ task.summary === void 0 || running ? null : /* @__PURE__ */ jsx("p", {
5214
+ style: taskSummary,
5215
+ children: task.summary
5216
+ }),
5217
+ activities.length === 0 && tools.length === 0 ? null : /* @__PURE__ */ jsxs("div", {
5218
+ style: taskActivitySection,
5219
+ children: [/* @__PURE__ */ jsx("button", {
5220
+ type: "button",
5221
+ style: taskTextButton,
5222
+ "aria-expanded": activityOpen,
5223
+ onClick: () => setActivityOpen((value) => !value),
5224
+ children: activityOpen ? t("tasksHideActivity") : t("tasksViewActivity")
5225
+ }), !activityOpen ? null : tools.length > 0 ? /* @__PURE__ */ jsx("div", {
5226
+ style: taskToolList,
5227
+ children: tools.map((tool) => /* @__PURE__ */ jsx(ClaudeTranscriptToolItem, {
5228
+ tool,
5229
+ t
5230
+ }, tool.toolUseId))
5231
+ }) : /* @__PURE__ */ jsx("ul", {
5232
+ style: taskActivityList,
5233
+ children: activities.map((activity) => /* @__PURE__ */ jsx(TaskActivity, {
5234
+ activity,
5235
+ t
5236
+ }, `${activity.turn}:${activity.step}:${activity.ordinal}`))
5237
+ })]
5238
+ })
5239
+ ]
5240
+ });
5241
+ }
5242
+ function GroupHeading(props) {
5243
+ const { label, count, collapsed, onToggle, action } = props;
5244
+ const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", { children: label }), /* @__PURE__ */ jsx("span", {
5245
+ style: tasksGroupCount,
5246
+ children: count
5247
+ })] });
5248
+ return /* @__PURE__ */ jsxs("div", {
5249
+ style: tasksGroupHeading,
5250
+ children: [onToggle === void 0 ? /* @__PURE__ */ jsx("div", {
5251
+ style: tasksGroupTitle,
5252
+ children: content
5253
+ }) : /* @__PURE__ */ jsxs("button", {
5254
+ type: "button",
5255
+ style: tasksGroupToggle,
5256
+ "aria-expanded": !collapsed,
5257
+ onClick: onToggle,
5258
+ children: [/* @__PURE__ */ jsx("span", {
5259
+ style: {
5260
+ ...chevron,
5261
+ ...collapsed === true ? {} : chevronOpen
5262
+ },
5263
+ children: "›"
5264
+ }), content]
5265
+ }), action === void 0 ? null : /* @__PURE__ */ jsx("button", {
5266
+ type: "button",
5267
+ style: taskTextButton,
5268
+ onClick: action.onClick,
5269
+ children: action.label
5270
+ })]
5271
+ });
5272
+ }
5273
+ function ClaudeTasksPanel({ useClaudeProjection, t, closeDetails, turn }) {
5274
+ const projection = useClaudeProjection((value) => value);
5275
+ const tasks = useMemo(() => tasksForTurn(projection.tasks?.tasks ?? [], turn), [projection.tasks, turn]);
5276
+ useEffect(() => {
5277
+ if (!projection.owned || tasks.length === 0) closeDetails();
5278
+ }, [
5279
+ closeDetails,
5280
+ projection.owned,
5281
+ tasks.length
5282
+ ]);
5283
+ const [finishedCollapsed, setFinishedCollapsed] = useState(false);
5284
+ const [dismissedSettledIds, setDismissedSettledIds] = useState(() => /* @__PURE__ */ new Set());
5285
+ const groups = useMemo(() => visibleTaskGroups(tasks, dismissedSettledIds), [tasks, dismissedSettledIds]);
5286
+ const taskActivities = useMemo(() => new Map(tasks.map((task) => [task.taskId, activitiesForTask(projection.activities, task.taskId)])), [projection.activities, tasks]);
5287
+ const clearFinished = () => setDismissedSettledIds((previous) => /* @__PURE__ */ new Set([...previous, ...tasks.filter((task) => task.status !== "running").map((task) => task.taskId)]));
5288
+ if (!projection.owned) return null;
4561
5289
  return /* @__PURE__ */ jsxs("div", {
4562
- className: "dsh-claude-tool-group-native",
4563
- children: [/* @__PURE__ */ jsx(DisclosureRow, {
4564
- rowClassName: "dsh-claude-flow-row",
4565
- leadingClassName: "dsh-claude-flow-leading",
4566
- titleClassName: "dsh-claude-flow-title",
4567
- chevronClassName: "dsh-claude-flow-chevron",
4568
- icon: failed ? /* @__PURE__ */ jsx(StateDot, { state: "error" }) : running ? /* @__PURE__ */ jsx(StateDot, { state: "ongoing" }) : /* @__PURE__ */ jsx(IconApiOutline14, { size: 14 }),
4569
- title: summary,
4570
- open,
4571
- expandable: true,
4572
- expandOnRowClick: true,
4573
- keepContentWhenOpen: true,
4574
- onToggle: () => setOpen((value) => !value),
4575
- collapsedContent: additions !== void 0 || deletions !== void 0 ? /* @__PURE__ */ jsxs("span", {
4576
- className: "dsh-claude-tool-stats",
4577
- children: [/* @__PURE__ */ jsxs("span", {
4578
- className: "dsh-claude-diff-add",
4579
- children: ["+", additions ?? 0]
4580
- }), /* @__PURE__ */ jsxs("span", {
4581
- className: "dsh-claude-diff-delete",
4582
- children: ["−", deletions ?? 0]
5290
+ className: detailsCardClass,
5291
+ style: tasksPanel,
5292
+ children: [
5293
+ /* @__PURE__ */ jsxs("style", {
5294
+ "data-dsh-claude-panel-icon-styles": true,
5295
+ children: [detailsCardCss, panelIconButtonCss]
5296
+ }),
5297
+ /* @__PURE__ */ jsxs("div", {
5298
+ style: tasksHeader,
5299
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
5300
+ style: tasksHeading,
5301
+ children: t("tasksPanelTurn")
5302
+ }), /* @__PURE__ */ jsx("span", {
5303
+ style: tasksTurnMeta,
5304
+ children: t("tasksTurnNumber", { turn })
5305
+ })] }), /* @__PURE__ */ jsx("button", {
5306
+ type: "button",
5307
+ className: panelIconButtonClass,
5308
+ "aria-label": t("tasksClose"),
5309
+ onClick: closeDetails,
5310
+ children: /* @__PURE__ */ jsx(IconCloseOutline16, {})
4583
5311
  })]
4584
- }) : void 0
4585
- }), open ? /* @__PURE__ */ jsx("div", {
4586
- className: "dsh-claude-tool-list",
4587
- children: tools.map((tool) => /* @__PURE__ */ jsx(ClaudeTranscriptToolItem, {
4588
- tool,
4589
- t
4590
- }, tool.toolUseId))
4591
- }) : null]
5312
+ }),
5313
+ /* @__PURE__ */ jsxs("div", {
5314
+ style: tasksBody,
5315
+ children: [/* @__PURE__ */ jsxs("section", {
5316
+ "aria-label": t("tasksRunning"),
5317
+ children: [/* @__PURE__ */ jsx(GroupHeading, {
5318
+ label: t("tasksRunning"),
5319
+ count: groups.running.length
5320
+ }), groups.running.length === 0 ? /* @__PURE__ */ jsx("p", {
5321
+ style: tasksGroupEmpty,
5322
+ children: t("tasksNoneRunning")
5323
+ }) : /* @__PURE__ */ jsx("div", {
5324
+ style: taskCardList,
5325
+ children: groups.running.map((task) => /* @__PURE__ */ jsx(TaskCard, {
5326
+ task,
5327
+ activities: taskActivities.get(task.taskId) ?? [],
5328
+ allActivities: projection.activities,
5329
+ t
5330
+ }, task.taskId))
5331
+ })]
5332
+ }), /* @__PURE__ */ jsxs("section", {
5333
+ "aria-label": t("tasksSettled"),
5334
+ style: tasksFinishedSection,
5335
+ children: [/* @__PURE__ */ jsx(GroupHeading, {
5336
+ label: t("tasksSettled"),
5337
+ count: groups.finished.length,
5338
+ collapsed: finishedCollapsed,
5339
+ onToggle: () => setFinishedCollapsed((value) => !value),
5340
+ ...groups.finished.length === 0 ? {} : { action: {
5341
+ label: t("tasksClear"),
5342
+ onClick: clearFinished
5343
+ } }
5344
+ }), finishedCollapsed || groups.finished.length === 0 ? null : /* @__PURE__ */ jsx("div", {
5345
+ style: taskCardList,
5346
+ children: groups.finished.map((task) => /* @__PURE__ */ jsx(TaskCard, {
5347
+ task,
5348
+ activities: taskActivities.get(task.taskId) ?? [],
5349
+ allActivities: projection.activities,
5350
+ t
5351
+ }, task.taskId))
5352
+ })]
5353
+ })]
5354
+ })
5355
+ ]
4592
5356
  });
4593
5357
  }
4594
- function ClaudeActivityNode({ node, useClaudeProjection, t }) {
4595
- ensureCss$3();
4596
- const marker = node.data;
4597
- const activities = useClaudeProjection((value) => selectStepActivities(value, marker.turn, marker.step));
4598
- const tasks = useClaudeProjection((value) => value.tasks?.tasks ?? EMPTY_TASKS);
4599
- const items = useMemo(() => transcriptItemsForStep(activities, marker.turn, marker.step, tasks), [
4600
- activities,
4601
- marker.step,
4602
- marker.turn,
4603
- tasks
4604
- ]);
4605
- const markdownLabels = useClaudeMarkdownLabels(t);
4606
- if (items.length === 0) return null;
5358
+ //#endregion
5359
+ //#region src/client/ClaudeActivityTail.tsx
5360
+ const MAX_HOVER_TASKS = 6;
5361
+ function taskGlyph(status) {
5362
+ if (status === "failed") return {
5363
+ glyph: "×",
5364
+ style: tasksHoverGlyphError
5365
+ };
5366
+ if (status === "completed") return {
5367
+ glyph: "✓",
5368
+ style: tasksHoverGlyphDone
5369
+ };
5370
+ return {
5371
+ glyph: "●",
5372
+ style: tasksHoverGlyphRunning
5373
+ };
5374
+ }
5375
+ function ClaudeTaskLauncher({ turn, tasks, t, openTasks }) {
5376
+ const [hovered, setHovered] = useState(false);
5377
+ const closeTimer = useRef();
5378
+ const turnTasks = useMemo(() => tasksForTurn(tasks, turn), [tasks, turn]);
5379
+ const summary = useMemo(() => summarizeTurnTasks(turnTasks), [turnTasks]);
5380
+ const open = () => {
5381
+ if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
5382
+ closeTimer.current = void 0;
5383
+ setHovered(true);
5384
+ };
5385
+ const scheduleClose = () => {
5386
+ if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
5387
+ closeTimer.current = setTimeout(() => {
5388
+ closeTimer.current = void 0;
5389
+ setHovered(false);
5390
+ }, 350);
5391
+ };
5392
+ useEffect(() => () => {
5393
+ if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
5394
+ }, []);
5395
+ if (summary === void 0) return null;
5396
+ const label = summary.state === "running" ? t("tasksTurnRunning", { count: summary.running }) : summary.state === "failed" ? t("tasksTurnFailed", {
5397
+ failed: summary.failed,
5398
+ completed: summary.completed
5399
+ }) : t("tasksTurnCompleted", { count: summary.completed });
5400
+ const stateStyle = summary.state === "completed" ? tasksBadgeDone : {};
5401
+ const dotStyle = summary.state === "failed" ? tasksBadgeDotError : summary.state === "completed" ? tasksBadgeDotDone : {};
4607
5402
  return /* @__PURE__ */ jsx("div", {
4608
- className: "dsh-claude-flow",
4609
- children: items.map((item) => item.kind === "text" ? /* @__PURE__ */ jsx("div", {
4610
- className: "dsh-claude-transcript-text",
4611
- children: /* @__PURE__ */ jsx(ClaudeMarkdown, {
4612
- text: item.text,
4613
- labels: markdownLabels
4614
- })
4615
- }, `text:${item.ordinal}`) : item.kind === "compaction" ? /* @__PURE__ */ jsx(ClaudeCompactionDivider, {
4616
- compaction: item.compaction,
4617
- t
4618
- }, `compaction:${item.ordinal}`) : item.kind === "tools" ? /* @__PURE__ */ jsx(ClaudeTranscriptToolGroup, {
4619
- tools: item.tools,
4620
- ...item.additions === void 0 ? {} : { additions: item.additions },
4621
- ...item.deletions === void 0 ? {} : { deletions: item.deletions },
4622
- ...item.files === void 0 ? {} : { files: item.files },
4623
- t
4624
- }, `tools:${item.ordinal}`) : /* @__PURE__ */ jsx(ActivityRow, {
4625
- row: item.row,
4626
- t
4627
- }, `activity:${item.ordinal}`))
5403
+ "data-claude-task-launcher": turn,
5404
+ style: tasksBadgeWrap,
5405
+ children: /* @__PURE__ */ jsxs("span", {
5406
+ style: tasksBadgeSeat,
5407
+ onMouseEnter: open,
5408
+ onMouseLeave: scheduleClose,
5409
+ onFocus: open,
5410
+ onBlur: (event) => {
5411
+ if (!event.currentTarget.contains(event.relatedTarget)) scheduleClose();
5412
+ },
5413
+ children: [hovered ? /* @__PURE__ */ jsxs("span", {
5414
+ role: "tooltip",
5415
+ style: tasksHoverCard,
5416
+ onMouseEnter: open,
5417
+ onMouseLeave: scheduleClose,
5418
+ children: [
5419
+ /* @__PURE__ */ jsx("span", {
5420
+ style: tasksHoverHeader,
5421
+ children: label
5422
+ }),
5423
+ turnTasks.slice(0, MAX_HOVER_TASKS).map((task) => {
5424
+ const { glyph, style } = taskGlyph(task.status);
5425
+ return /* @__PURE__ */ jsxs("span", {
5426
+ style: tasksHoverRow,
5427
+ children: [
5428
+ /* @__PURE__ */ jsx("span", {
5429
+ className: task.status === "running" ? "dsh-claude-act-running" : void 0,
5430
+ style: {
5431
+ ...tasksHoverGlyph,
5432
+ ...style
5433
+ },
5434
+ "aria-hidden": "true",
5435
+ children: glyph
5436
+ }),
5437
+ /* @__PURE__ */ jsx("span", {
5438
+ style: tasksHoverDesc,
5439
+ title: task.description,
5440
+ children: task.description
5441
+ }),
5442
+ task.subagentType === void 0 ? null : /* @__PURE__ */ jsx("span", {
5443
+ style: tasksHoverType,
5444
+ children: task.subagentType
5445
+ })
5446
+ ]
5447
+ }, task.taskId);
5448
+ }),
5449
+ turnTasks.length > MAX_HOVER_TASKS ? /* @__PURE__ */ jsxs("span", {
5450
+ style: tasksHoverMore,
5451
+ children: ["+", turnTasks.length - MAX_HOVER_TASKS]
5452
+ }) : null,
5453
+ /* @__PURE__ */ jsx("span", {
5454
+ style: tasksHoverHint,
5455
+ children: t("tasksOpen")
5456
+ })
5457
+ ]
5458
+ }) : null, /* @__PURE__ */ jsxs("button", {
5459
+ type: "button",
5460
+ className: "dsh-claude-task-launcher",
5461
+ style: {
5462
+ ...tasksTurnBadge,
5463
+ ...stateStyle,
5464
+ ...hovered ? tasksBadgeHovered : {}
5465
+ },
5466
+ "aria-label": `${label} — ${t("tasksOpen")}`,
5467
+ onClick: () => openTasks(turn),
5468
+ children: [/* @__PURE__ */ jsx("span", {
5469
+ className: summary.state === "running" ? "dsh-claude-act-running" : void 0,
5470
+ style: {
5471
+ ...tasksBadgeDot,
5472
+ ...dotStyle
5473
+ },
5474
+ "aria-hidden": "true"
5475
+ }), /* @__PURE__ */ jsx("span", {
5476
+ style: tasksBadgeLabel,
5477
+ children: label
5478
+ })]
5479
+ })]
5480
+ })
5481
+ });
5482
+ }
5483
+ function ClaudeActivityTail({ matched, useClaudeProjection, t, openTasks }) {
5484
+ const tasks = useClaudeProjection((value) => value.tasks?.tasks ?? []);
5485
+ return /* @__PURE__ */ jsx(ClaudeTaskLauncher, {
5486
+ turn: matched.turn,
5487
+ tasks,
5488
+ t,
5489
+ openTasks
4628
5490
  });
4629
5491
  }
4630
5492
  //#endregion
4631
- //#region src/client/plugin-request.ts
4632
- /** Deadlines for the plugin's own HTTP routes.
4633
- *
4634
- * A browser opens at most six connections to one origin, and a request that
4635
- * never settles holds one of them for the life of the page. A handful of
4636
- * those and every plugin route — the projection stream included — stops
4637
- * answering, which reads as the whole plugin freezing until the Host is
4638
- * restarted. So no plain request is allowed to wait forever: the deadline is
4639
- * generous enough that a slow answer still arrives, and short enough that a
4640
- * wedged one gives its connection back.
4641
- *
4642
- * Streaming routes (projection, ask, repository setup) are legitimately
4643
- * long-lived and carry their own cancellation instead. */
4644
- /** Reads: the route answers from cache or a short-timeout Git call. */
4645
- const PLUGIN_READ_TIMEOUT_MS = 3e4;
4646
- /** Writes: the route may chain several remote Git/gh calls of its own. */
4647
- const PLUGIN_ACTION_TIMEOUT_MS = 18e4;
4648
- /** Combine a caller's cancellation with this deadline. */
4649
- function pluginRequestSignal(timeoutMs, signal) {
4650
- const timeout = AbortSignal.timeout(timeoutMs);
4651
- return signal === void 0 ? timeout : AbortSignal.any([signal, timeout]);
5493
+ //#region src/client/ClaudeActiveTasksNode.tsx
5494
+ const EMPTY_TASKS = [];
5495
+ /** Render the task launcher while the owning DSH turn is still open. */
5496
+ function ClaudeActiveTasksNode({ node, useClaudeProjection, t, openTasks }) {
5497
+ const tasks = useClaudeProjection((value) => value.tasks?.tasks ?? EMPTY_TASKS);
5498
+ return /* @__PURE__ */ jsx(ClaudeTaskLauncher, {
5499
+ turn: node.data.turn,
5500
+ tasks,
5501
+ t,
5502
+ openTasks
5503
+ });
4652
5504
  }
4653
5505
  //#endregion
4654
5506
  //#region src/client/jira-api.ts
@@ -4663,68 +5515,75 @@ window.__ModuleLoader__.load({
4663
5515
  function record$5(value) {
4664
5516
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4665
5517
  }
4666
- async function call(path, init) {
4667
- const response = await fetch(`${CLAUDE_JIRA_PATH}${path}`, {
4668
- credentials: "same-origin",
4669
- signal: pluginRequestSignal(PLUGIN_READ_TIMEOUT_MS),
4670
- ...init
4671
- });
4672
- const body = record$5(await response.json());
4673
- if (!response.ok) throw new JiraClientError(typeof body?.message === "string" ? body.message : "Jira is unavailable.", typeof body?.error === "string" ? body.error : void 0);
5518
+ /**
5519
+ * Every Jira failure the panels catch is a `JiraClientError`, whatever the
5520
+ * transport threw: `ClaudeHeroRepositoryControls` branches on `code` to tell
5521
+ * 'not-connected' apart from a real outage, and the settings card renders the
5522
+ * message verbatim.
5523
+ *
5524
+ * The routes answer `{ error, message }`, so `message` is already the sentence
5525
+ * to show. A body carrying only a code a 405, a bad JSON body used to read
5526
+ * 'Jira is unavailable.' rather than leaking the code as prose, and it still
5527
+ * does. Transport failures (a starved pool, an elapsed budget, an older Host
5528
+ * without the route) carry their own wording and keep it.
5529
+ */
5530
+ function jiraFailure(cause) {
5531
+ if (cause instanceof JiraClientError) return cause;
5532
+ if (!(cause instanceof PluginRequestError)) return new JiraClientError(cause instanceof Error ? cause.message : String(cause));
5533
+ return new JiraClientError(cause.message === cause.code ? "Jira is unavailable." : cause.message, cause.code);
5534
+ }
5535
+ function payload(value) {
5536
+ const body = record$5(value);
4674
5537
  if (body === void 0) throw new JiraClientError("Invalid Jira response.");
4675
5538
  return body;
4676
5539
  }
4677
- function status(body) {
5540
+ function status(value) {
5541
+ const body = payload(value);
4678
5542
  if (typeof body.connected !== "boolean") throw new JiraClientError("Invalid Jira status response.");
4679
5543
  return body;
4680
5544
  }
4681
5545
  async function loadJiraStatus(signal) {
4682
- return status(await call("/status", {
4683
- method: "GET",
4684
- headers: { accept: "application/json" },
4685
- ...signal === void 0 ? {} : { signal }
4686
- }));
5546
+ try {
5547
+ return status(await pluginRead(`${CLAUDE_JIRA_PATH}/status`, "remote", signal));
5548
+ } catch (cause) {
5549
+ throw jiraFailure(cause);
5550
+ }
4687
5551
  }
4688
5552
  async function connectJira(input) {
4689
- return status(await call("/connect", {
4690
- method: "POST",
4691
- headers: {
4692
- accept: "application/json",
4693
- "content-type": "application/json"
4694
- },
4695
- body: JSON.stringify(input)
4696
- }));
5553
+ try {
5554
+ return status(await pluginWrite(`${CLAUDE_JIRA_PATH}/connect`, "remote", void 0, { json: input }));
5555
+ } catch (cause) {
5556
+ throw jiraFailure(cause);
5557
+ }
4697
5558
  }
4698
5559
  async function disconnectJira() {
4699
- await call("/disconnect", {
4700
- method: "POST",
4701
- headers: { accept: "application/json" }
4702
- });
5560
+ try {
5561
+ await pluginWrite(`${CLAUDE_JIRA_PATH}/disconnect`, "remote");
5562
+ } catch (cause) {
5563
+ throw jiraFailure(cause);
5564
+ }
4703
5565
  }
4704
5566
  async function searchJiraTickets(query, signal) {
4705
- const body = await call(`/search?query=${encodeURIComponent(query)}`, {
4706
- method: "GET",
4707
- headers: { accept: "application/json" },
4708
- ...signal === void 0 ? {} : { signal }
4709
- });
4710
- if (!Array.isArray(body.tickets)) throw new JiraClientError("Invalid Jira search response.");
4711
- const tickets = [];
4712
- for (const item of body.tickets) {
4713
- const ticket = record$5(item);
4714
- if (ticket === void 0 || typeof ticket.key !== "string" || typeof ticket.summary !== "string" || typeof ticket.url !== "string") continue;
4715
- tickets.push(ticket);
5567
+ try {
5568
+ const body = payload(await pluginRead(`${CLAUDE_JIRA_PATH}/search`, "remote", signal, { query: { query } }));
5569
+ if (!Array.isArray(body.tickets)) throw new JiraClientError("Invalid Jira search response.");
5570
+ const tickets = [];
5571
+ for (const item of body.tickets) {
5572
+ const ticket = record$5(item);
5573
+ if (ticket === void 0 || typeof ticket.key !== "string" || typeof ticket.summary !== "string" || typeof ticket.url !== "string") continue;
5574
+ tickets.push(ticket);
5575
+ }
5576
+ return tickets;
5577
+ } catch (cause) {
5578
+ throw jiraFailure(cause);
4716
5579
  }
4717
- return tickets;
4718
5580
  }
4719
5581
  async function assignJiraTicket(key) {
4720
- await call("/assign", {
4721
- method: "POST",
4722
- headers: {
4723
- accept: "application/json",
4724
- "content-type": "application/json"
4725
- },
4726
- body: JSON.stringify({ key })
4727
- });
5582
+ try {
5583
+ await pluginWrite(`${CLAUDE_JIRA_PATH}/assign`, "remote", void 0, { json: { key } });
5584
+ } catch (cause) {
5585
+ throw jiraFailure(cause);
5586
+ }
4728
5587
  }
4729
5588
  /** Draft seeded into the composer when a session starts from a ticket. */
4730
5589
  function ticketPrompt(ticket) {
@@ -4760,7 +5619,9 @@ window.__ModuleLoader__.load({
4760
5619
  if (typeof setting !== "object" || setting === null || Array.isArray(setting)) return false;
4761
5620
  const item = setting;
4762
5621
  if (typeof item.key !== "string" || typeof item.value !== "string" || ![
5622
+ "immediate",
4763
5623
  "new-session",
5624
+ "next-turn",
4764
5625
  "next-worktree",
4765
5626
  "restart"
4766
5627
  ].includes(String(item.effect))) return false;
@@ -4775,6 +5636,27 @@ window.__ModuleLoader__.load({
4775
5636
  function value(status, detail) {
4776
5637
  return detail === void 0 ? status : `${status} · ${detail}`;
4777
5638
  }
5639
+ /** The trigger's disclosure chevron.
5640
+ *
5641
+ * Geometry rather than a character: the ink spans y 6 to 10 in a 16-unit box,
5642
+ * so it is centred on the box's own centre and the open state's 180-degree
5643
+ * flip lands exactly where the closed state sat. A text arrowhead carries its
5644
+ * ink below the centre of the em box, which is why this needed a hand-tuned
5645
+ * nudge that could only be right in one of the two states. */
5646
+ function SelectChevron() {
5647
+ return /* @__PURE__ */ jsx("svg", {
5648
+ width: "16",
5649
+ height: "16",
5650
+ viewBox: "0 0 16 16",
5651
+ fill: "none",
5652
+ stroke: "currentColor",
5653
+ strokeWidth: "1.8",
5654
+ strokeLinecap: "round",
5655
+ strokeLinejoin: "round",
5656
+ "aria-hidden": "true",
5657
+ children: /* @__PURE__ */ jsx("path", { d: "M4 6l4 4 4-4" })
5658
+ });
5659
+ }
4778
5660
  function GlobalSettingText({ setting, disabled, onChange }) {
4779
5661
  const [draft, setDraft] = useState(setting.value);
4780
5662
  useEffect(() => {
@@ -4805,7 +5687,7 @@ window.__ModuleLoader__.load({
4805
5687
  }
4806
5688
  });
4807
5689
  }
4808
- function GlobalSettingSelect({ setting, disabled, onChange }) {
5690
+ function GlobalSettingSelect({ setting, disabled, onChange, labelFor = (option) => option.label }) {
4809
5691
  const [open, setOpen] = useState(false);
4810
5692
  const [activeIndex, setActiveIndex] = useState(0);
4811
5693
  const rootRef = useRef(null);
@@ -4843,80 +5725,81 @@ window.__ModuleLoader__.load({
4843
5725
  onBlur: (event) => {
4844
5726
  if (!event.currentTarget.contains(event.relatedTarget)) setOpen(false);
4845
5727
  },
4846
- children: [/* @__PURE__ */ jsxs("button", {
4847
- ref: triggerRef,
4848
- type: "button",
4849
- "aria-haspopup": "listbox",
4850
- "aria-expanded": open,
4851
- "aria-controls": open ? listboxId : void 0,
4852
- "aria-activedescendant": open ? `${listboxId}-${activeIndex}` : void 0,
4853
- disabled: disabled || setting.options.length === 0,
4854
- style: {
4855
- ...settingSelectTrigger,
4856
- ...open ? settingSelectTriggerOpen : {}
4857
- },
4858
- onClick: () => {
4859
- if (open) setOpen(false);
4860
- else openMenu();
4861
- },
4862
- onKeyDown: (event) => {
4863
- if (event.key === "ArrowDown" || event.key === "ArrowUp") {
4864
- event.preventDefault();
4865
- if (!open) openMenu(event.key === "ArrowDown" ? selectedIndex : Math.max(0, setting.options.length - 1));
4866
- else move(event.key === "ArrowDown" ? 1 : -1);
4867
- } else if (event.key === "Home" && open) {
4868
- event.preventDefault();
4869
- setActiveIndex(0);
4870
- } else if (event.key === "End" && open) {
4871
- event.preventDefault();
4872
- setActiveIndex(Math.max(0, setting.options.length - 1));
4873
- } else if ((event.key === "Enter" || event.key === " ") && open) {
4874
- event.preventDefault();
4875
- choose(activeIndex);
4876
- } else if (event.key === "Escape" && open) {
4877
- event.preventDefault();
4878
- setOpen(false);
4879
- }
4880
- },
4881
- children: [/* @__PURE__ */ jsx("span", {
4882
- style: settingSelectValue,
4883
- children: selectedOption?.label ?? setting.value
4884
- }), /* @__PURE__ */ jsx("span", {
4885
- "aria-hidden": "true",
4886
- style: {
4887
- ...settingSelectChevron,
4888
- ...open ? settingSelectChevronOpen : {}
5728
+ children: [
5729
+ /* @__PURE__ */ jsx("style", {
5730
+ "data-dsh-claude-setting-select-styles": true,
5731
+ children: settingSelectCss
5732
+ }),
5733
+ /* @__PURE__ */ jsxs("button", {
5734
+ ref: triggerRef,
5735
+ type: "button",
5736
+ "aria-haspopup": "listbox",
5737
+ "aria-expanded": open,
5738
+ "aria-controls": open ? listboxId : void 0,
5739
+ "aria-activedescendant": open ? `${listboxId}-${activeIndex}` : void 0,
5740
+ disabled: disabled || setting.options.length === 0,
5741
+ className: settingSelectTriggerClass,
5742
+ onClick: () => {
5743
+ if (open) setOpen(false);
5744
+ else openMenu();
4889
5745
  },
4890
- children: "⌄"
4891
- })]
4892
- }), open ? /* @__PURE__ */ jsx("div", {
4893
- id: listboxId,
4894
- role: "listbox",
4895
- "aria-activedescendant": `${listboxId}-${activeIndex}`,
4896
- style: settingSelectMenu,
4897
- children: setting.options.map((option, index) => {
4898
- const selected = option.value === setting.value;
4899
- const active = index === activeIndex;
4900
- return /* @__PURE__ */ jsxs("button", {
4901
- id: `${listboxId}-${index}`,
4902
- type: "button",
4903
- role: "option",
4904
- "aria-selected": selected,
4905
- style: {
4906
- ...settingSelectOption,
4907
- ...active ? settingSelectOptionActive : {}
4908
- },
4909
- onMouseEnter: () => setActiveIndex(index),
4910
- onMouseDown: (event) => event.preventDefault(),
4911
- onClick: () => choose(index),
4912
- children: [/* @__PURE__ */ jsx("span", {
4913
- style: settingSelectCheck,
4914
- "aria-hidden": "true",
4915
- children: selected ? "✓" : ""
4916
- }), /* @__PURE__ */ jsx("span", { children: option.label })]
4917
- }, `${option.source}:${option.value}`);
4918
- })
4919
- }) : null]
5746
+ onKeyDown: (event) => {
5747
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
5748
+ event.preventDefault();
5749
+ if (!open) openMenu(event.key === "ArrowDown" ? selectedIndex : Math.max(0, setting.options.length - 1));
5750
+ else move(event.key === "ArrowDown" ? 1 : -1);
5751
+ } else if (event.key === "Home" && open) {
5752
+ event.preventDefault();
5753
+ setActiveIndex(0);
5754
+ } else if (event.key === "End" && open) {
5755
+ event.preventDefault();
5756
+ setActiveIndex(Math.max(0, setting.options.length - 1));
5757
+ } else if ((event.key === "Enter" || event.key === " ") && open) {
5758
+ event.preventDefault();
5759
+ choose(activeIndex);
5760
+ } else if (event.key === "Escape" && open) {
5761
+ event.preventDefault();
5762
+ setOpen(false);
5763
+ }
5764
+ },
5765
+ children: [/* @__PURE__ */ jsx("span", {
5766
+ style: settingSelectValue,
5767
+ children: selectedOption === void 0 ? setting.value : labelFor(selectedOption)
5768
+ }), /* @__PURE__ */ jsx("span", {
5769
+ "aria-hidden": "true",
5770
+ className: settingSelectChevronClass,
5771
+ children: /* @__PURE__ */ jsx(SelectChevron, {})
5772
+ })]
5773
+ }),
5774
+ open ? /* @__PURE__ */ jsx("div", {
5775
+ id: listboxId,
5776
+ role: "listbox",
5777
+ "aria-activedescendant": `${listboxId}-${activeIndex}`,
5778
+ style: settingSelectMenu,
5779
+ children: setting.options.map((option, index) => {
5780
+ const selected = option.value === setting.value;
5781
+ const active = index === activeIndex;
5782
+ return /* @__PURE__ */ jsxs("button", {
5783
+ id: `${listboxId}-${index}`,
5784
+ type: "button",
5785
+ role: "option",
5786
+ "aria-selected": selected,
5787
+ style: {
5788
+ ...settingSelectOption,
5789
+ ...active ? settingSelectOptionActive : {}
5790
+ },
5791
+ onMouseEnter: () => setActiveIndex(index),
5792
+ onMouseDown: (event) => event.preventDefault(),
5793
+ onClick: () => choose(index),
5794
+ children: [/* @__PURE__ */ jsx("span", {
5795
+ style: settingSelectCheck,
5796
+ "aria-hidden": "true",
5797
+ children: selected ? "✓" : ""
5798
+ }), /* @__PURE__ */ jsx("span", { children: labelFor(option) })]
5799
+ }, `${option.source}:${option.value}`);
5800
+ })
5801
+ }) : null
5802
+ ]
4920
5803
  });
4921
5804
  }
4922
5805
  /** Fixed windows carry a translated label; server-named model buckets (e.g.
@@ -4927,6 +5810,23 @@ window.__ModuleLoader__.load({
4927
5810
  "seven_day_opus",
4928
5811
  "seven_day_sonnet"
4929
5812
  ]);
5813
+ /** The prose mode a settings payload carries, or the default when it carries
5814
+ * none — an older Host, or a response this Client does not fully understand,
5815
+ * leaves the palette alone rather than guessing. */
5816
+ function proseModeOf(settings) {
5817
+ const value = settings.find((setting) => setting.key === "prose")?.value;
5818
+ return isClaudeProseMode(value) ? value : DEFAULT_CLAUDE_PROSE_MODE;
5819
+ }
5820
+ /** Settings whose row only makes sense under a particular value of another.
5821
+ * Filtering here rather than server-side keeps the descriptor list flat: the
5822
+ * server has no view of what the Client can paint. Fails OPEN — a payload
5823
+ * missing the setting a row depends on shows the row rather than hiding it,
5824
+ * so an older Host cannot make a setting unreachable. */
5825
+ function visibleGlobalSettings(settings) {
5826
+ const renderer = settings.find((setting) => setting.key === "renderer");
5827
+ if (renderer === void 0 || renderer.value !== "native") return settings;
5828
+ return settings.filter((setting) => setting.key !== "prose");
5829
+ }
4930
5830
  /** Per-setting label and the effect note that used to sit as a standalone
4931
5831
  * paragraph under the card; it now hangs off the label as a hover hint. */
4932
5832
  const SETTING_COPY = {
@@ -4934,6 +5834,14 @@ window.__ModuleLoader__.load({
4934
5834
  label: "outputStyle",
4935
5835
  hint: "globalSettingsNewSession"
4936
5836
  },
5837
+ renderer: {
5838
+ label: "renderer",
5839
+ hint: "rendererEffect"
5840
+ },
5841
+ prose: {
5842
+ label: "prose",
5843
+ hint: "proseEffect"
5844
+ },
4937
5845
  worktreeBranchPrefix: {
4938
5846
  label: "worktreeBranchPrefix",
4939
5847
  hint: "worktreeBranchPrefixEffect"
@@ -4947,6 +5855,19 @@ window.__ModuleLoader__.load({
4947
5855
  hint: "idleTimeoutEffect"
4948
5856
  }
4949
5857
  };
5858
+ /** Translated display text for the option vocabularies this plugin owns,
5859
+ * keyed `<setting>:<option>`. Options discovered on the machine (output style
5860
+ * names) carry no entry and keep the label the route reported. */
5861
+ const SETTING_OPTION_COPY = {
5862
+ "renderer:plugin": "rendererPlugin",
5863
+ "renderer:native": "rendererNative",
5864
+ "prose:plain": "prosePlain",
5865
+ "prose:enhanced": "proseEnhanced"
5866
+ };
5867
+ function settingOptionLabel(settingKey, option, t) {
5868
+ const key = SETTING_OPTION_COPY[`${settingKey}:${option.value}`];
5869
+ return key === void 0 ? option.label : t(key);
5870
+ }
4950
5871
  /** '?' badge that reveals a setting's effect note on hover or keyboard focus. */
4951
5872
  function SettingHint({ text, label }) {
4952
5873
  return /* @__PURE__ */ jsx(Tooltip, {
@@ -4968,8 +5889,9 @@ window.__ModuleLoader__.load({
4968
5889
  const report = value;
4969
5890
  return typeof report.available === "boolean" && typeof report.fetchedAt === "number" && Array.isArray(report.windows) && report.windows.every((entry) => typeof entry === "object" && entry !== null && typeof entry.id === "string");
4970
5891
  }
4971
- /** Coarse duration for a reset countdown or a fetch age: minutes below an
4972
- * hour, then hours, then days. Never negative — a passed reset reads '0m'. */
5892
+ /** Coarse duration for a reset countdown or the age of a reading: minutes
5893
+ * below an hour, then hours, then days. Never negative — a passed reset
5894
+ * reads '0m'. */
4973
5895
  function durationLabel(milliseconds) {
4974
5896
  const minutes = Math.max(0, Math.round(milliseconds / 6e4));
4975
5897
  if (minutes < 60) return `${minutes}m`;
@@ -4999,23 +5921,31 @@ window.__ModuleLoader__.load({
4999
5921
  })]
5000
5922
  });
5001
5923
  }
5002
- /** Every panel read is a local plugin route, so a request still outstanding
5003
- * after this long is a wedged host, not a slow one — and a settings card that
5004
- * says so beats one that says "Loading…" forever. */
5005
- const SETTINGS_REQUEST_TIMEOUT_MS = 2e4;
5006
- /** Fetch the plan usage report; POST forces the backend to re-read it. */
5007
- async function loadPlanUsage(refresh) {
5008
- const response = await fetch(CLAUDE_USAGE_PATH, {
5009
- method: refresh ? "POST" : "GET",
5010
- credentials: "same-origin",
5011
- signal: AbortSignal.timeout(SETTINGS_REQUEST_TIMEOUT_MS),
5012
- headers: { accept: "application/json" }
5924
+ function cardFailure(cause) {
5925
+ return {
5926
+ detail: cause instanceof Error ? cause.message : String(cause),
5927
+ starved: cause instanceof PluginRequestError && cause.reason === "starved"
5928
+ };
5929
+ }
5930
+ function FailureNotice({ label, failure }) {
5931
+ return /* @__PURE__ */ jsxs("p", {
5932
+ role: "alert",
5933
+ style: {
5934
+ ...notice,
5935
+ color: failure.starved ? "var(--dsw-alias-state-warning-primary, #d69e2e)" : "var(--dsw-alias-state-error-primary)"
5936
+ },
5937
+ children: [
5938
+ label,
5939
+ ": ",
5940
+ failure.detail
5941
+ ]
5013
5942
  });
5014
- const payload = await response.json();
5015
- if (!response.ok) {
5016
- const message = typeof payload === "object" && payload !== null && "error" in payload && typeof payload.error === "string" ? payload.error : `HTTP ${response.status}`;
5017
- throw new Error(message);
5018
- }
5943
+ }
5944
+ /** The plan usage report. A plain read serves the cached reading from memory;
5945
+ * the refresh spawns a probe process, which is why only it pays the remote
5946
+ * budget. */
5947
+ async function loadPlanUsage(refresh) {
5948
+ const payload = refresh ? await pluginWrite(CLAUDE_USAGE_PATH, "remote") : await pluginRead(CLAUDE_USAGE_PATH, "fast");
5019
5949
  if (!isPlanUsageReport(payload)) throw new Error("Invalid plan usage response");
5020
5950
  return payload;
5021
5951
  }
@@ -5029,7 +5959,7 @@ window.__ModuleLoader__.load({
5029
5959
  try {
5030
5960
  setReport(await load(refresh));
5031
5961
  } catch (cause) {
5032
- setError(cause instanceof Error ? cause.message : String(cause));
5962
+ setError(cardFailure(cause));
5033
5963
  } finally {
5034
5964
  setBusy(false);
5035
5965
  }
@@ -5086,17 +6016,9 @@ window.__ModuleLoader__.load({
5086
6016
  style: notice,
5087
6017
  children: t("planUsageUpdated", { age: durationLabel(now - report.fetchedAt) })
5088
6018
  }) : null,
5089
- error === void 0 ? null : /* @__PURE__ */ jsxs("p", {
5090
- role: "alert",
5091
- style: {
5092
- ...notice,
5093
- color: "var(--dsw-alias-state-error-primary)"
5094
- },
5095
- children: [
5096
- t("planUsageError"),
5097
- ": ",
5098
- error
5099
- ]
6019
+ error === void 0 ? null : /* @__PURE__ */ jsx(FailureNotice, {
6020
+ label: t("planUsageError"),
6021
+ failure: error
5100
6022
  })
5101
6023
  ]
5102
6024
  });
@@ -5120,7 +6042,7 @@ window.__ModuleLoader__.load({
5120
6042
  useEffect(() => {
5121
6043
  const controller = new AbortController();
5122
6044
  loadJiraStatus(controller.signal).then(setJiraStatus, (reason) => {
5123
- if (!controller.signal.aborted) setJiraError(reason instanceof Error ? reason.message : String(reason));
6045
+ if (!controller.signal.aborted) setJiraError(cardFailure(reason));
5124
6046
  });
5125
6047
  return () => {
5126
6048
  controller.abort();
@@ -5137,7 +6059,7 @@ window.__ModuleLoader__.load({
5137
6059
  }));
5138
6060
  setJiraToken("");
5139
6061
  } catch (cause) {
5140
- setJiraError(cause instanceof Error ? cause.message : String(cause));
6062
+ setJiraError(cardFailure(cause));
5141
6063
  } finally {
5142
6064
  setJiraBusy(false);
5143
6065
  }
@@ -5149,7 +6071,7 @@ window.__ModuleLoader__.load({
5149
6071
  await disconnectJira();
5150
6072
  setJiraStatus({ connected: false });
5151
6073
  } catch (cause) {
5152
- setJiraError(cause instanceof Error ? cause.message : String(cause));
6074
+ setJiraError(cardFailure(cause));
5153
6075
  } finally {
5154
6076
  setJiraBusy(false);
5155
6077
  }
@@ -5159,16 +6081,9 @@ window.__ModuleLoader__.load({
5159
6081
  setError(void 0);
5160
6082
  setReport(void 0);
5161
6083
  try {
5162
- const response = await fetch(CLAUDE_DOCTOR_PATH, {
5163
- credentials: "same-origin",
5164
- signal: AbortSignal.timeout(SETTINGS_REQUEST_TIMEOUT_MS),
5165
- headers: { accept: "application/json" }
5166
- });
5167
- const payload = await response.json();
5168
- if (!response.ok) throw new Error("error" in payload ? payload.error : `HTTP ${response.status}`);
5169
- setReport(payload);
6084
+ setReport(await pluginRead(CLAUDE_DOCTOR_PATH, "fast"));
5170
6085
  } catch (cause) {
5171
- setError(cause instanceof Error ? cause.message : String(cause));
6086
+ setError(cardFailure(cause));
5172
6087
  } finally {
5173
6088
  setBusy(false);
5174
6089
  }
@@ -5180,25 +6095,15 @@ window.__ModuleLoader__.load({
5180
6095
  setGlobalSettingsBusy(true);
5181
6096
  setGlobalSettingsError(void 0);
5182
6097
  try {
5183
- const response = await fetch(CLAUDE_GLOBAL_SETTINGS_PATH, {
5184
- method: changes === void 0 ? "GET" : "PATCH",
5185
- credentials: "same-origin",
5186
- signal: AbortSignal.timeout(SETTINGS_REQUEST_TIMEOUT_MS),
5187
- headers: {
5188
- accept: "application/json",
5189
- ...changes === void 0 ? {} : { "content-type": "application/json" }
5190
- },
5191
- ...changes === void 0 ? {} : { body: JSON.stringify({ changes }) }
6098
+ const payload = changes === void 0 ? await pluginRead(CLAUDE_GLOBAL_SETTINGS_PATH, "fast") : await pluginWrite(CLAUDE_GLOBAL_SETTINGS_PATH, "fast", void 0, {
6099
+ method: "PATCH",
6100
+ json: { changes }
5192
6101
  });
5193
- const payload = await response.json();
5194
- if (!response.ok) {
5195
- const message = typeof payload === "object" && payload !== null && "error" in payload && typeof payload.error === "string" ? payload.error : `HTTP ${response.status}`;
5196
- throw new Error(message);
5197
- }
5198
6102
  if (!isGlobalSettingsView(payload)) throw new Error("Invalid global settings response");
5199
6103
  setGlobalSettings(payload);
6104
+ applyClaudeMarkdownTheme(proseModeOf(payload.settings));
5200
6105
  } catch (cause) {
5201
- setGlobalSettingsError(cause instanceof Error ? cause.message : String(cause));
6106
+ setGlobalSettingsError(cardFailure(cause));
5202
6107
  } finally {
5203
6108
  setGlobalSettingsBusy(false);
5204
6109
  }
@@ -5210,20 +6115,11 @@ window.__ModuleLoader__.load({
5210
6115
  setUpdateBusy(action);
5211
6116
  setUpdateError(void 0);
5212
6117
  try {
5213
- const response = await fetch(action === "check" ? CLAUDE_UPDATE_CHECK_PATH : CLAUDE_UPDATE_PATH, {
5214
- method: action === "check" ? "GET" : "POST",
5215
- credentials: "same-origin",
5216
- headers: { accept: "application/json" }
5217
- });
5218
- const payload = await response.json();
5219
- if (!response.ok) {
5220
- const message = typeof payload === "object" && payload !== null && "error" in payload && typeof payload.error === "string" ? payload.error : `HTTP ${response.status}`;
5221
- throw new Error(message);
5222
- }
6118
+ const payload = action === "check" ? await pluginRead(CLAUDE_UPDATE_CHECK_PATH, "remote") : await pluginWrite(CLAUDE_UPDATE_PATH, "remote");
5223
6119
  if (!isPluginUpdateStatus(payload)) throw new Error("Invalid update response");
5224
6120
  setUpdateStatus(payload);
5225
6121
  } catch (cause) {
5226
- setUpdateError(cause instanceof Error ? cause.message : String(cause));
6122
+ setUpdateError(cardFailure(cause));
5227
6123
  } finally {
5228
6124
  setUpdateBusy(void 0);
5229
6125
  }
@@ -5293,17 +6189,9 @@ window.__ModuleLoader__.load({
5293
6189
  children: rowValue
5294
6190
  }, `${label}-value`)])
5295
6191
  }),
5296
- error === void 0 ? null : /* @__PURE__ */ jsxs("p", {
5297
- role: "alert",
5298
- style: {
5299
- ...notice,
5300
- color: "var(--dsw-alias-state-error-primary)"
5301
- },
5302
- children: [
5303
- t("error"),
5304
- ": ",
5305
- error
5306
- ]
6192
+ error === void 0 ? null : /* @__PURE__ */ jsx(FailureNotice, {
6193
+ label: t("error"),
6194
+ failure: error
5307
6195
  })
5308
6196
  ]
5309
6197
  }),
@@ -5324,7 +6212,7 @@ window.__ModuleLoader__.load({
5324
6212
  globalSettings === void 0 ? /* @__PURE__ */ jsx("p", {
5325
6213
  style: notice,
5326
6214
  children: t("globalSettingsLoading")
5327
- }) : globalSettings.settings.map((setting) => {
6215
+ }) : visibleGlobalSettings(globalSettings.settings).map((setting) => {
5328
6216
  const copy = SETTING_COPY[setting.key];
5329
6217
  return /* @__PURE__ */ jsxs("div", {
5330
6218
  style: diagnosticGrid,
@@ -5337,6 +6225,7 @@ window.__ModuleLoader__.load({
5337
6225
  }), setting.kind === "select" ? /* @__PURE__ */ jsx(GlobalSettingSelect, {
5338
6226
  setting,
5339
6227
  disabled: globalSettingsBusy,
6228
+ labelFor: (option) => settingOptionLabel(setting.key, option, t),
5340
6229
  onChange: (nextValue) => {
5341
6230
  requestGlobalSettings({ [setting.key]: nextValue });
5342
6231
  }
@@ -5349,17 +6238,9 @@ window.__ModuleLoader__.load({
5349
6238
  })]
5350
6239
  }, setting.key);
5351
6240
  }),
5352
- globalSettingsError === void 0 ? null : /* @__PURE__ */ jsxs("p", {
5353
- role: "alert",
5354
- style: {
5355
- ...notice,
5356
- color: "var(--dsw-alias-state-error-primary)"
5357
- },
5358
- children: [
5359
- t("globalSettingsError"),
5360
- ": ",
5361
- globalSettingsError
5362
- ]
6241
+ globalSettingsError === void 0 ? null : /* @__PURE__ */ jsx(FailureNotice, {
6242
+ label: t("globalSettingsError"),
6243
+ failure: globalSettingsError
5363
6244
  })
5364
6245
  ]
5365
6246
  }),
@@ -5465,17 +6346,9 @@ window.__ModuleLoader__.load({
5465
6346
  })]
5466
6347
  })
5467
6348
  ] }),
5468
- jiraError === void 0 ? null : /* @__PURE__ */ jsxs("p", {
5469
- role: "alert",
5470
- style: {
5471
- ...notice,
5472
- color: "var(--dsw-alias-state-error-primary)"
5473
- },
5474
- children: [
5475
- t("jiraError"),
5476
- ": ",
5477
- jiraError
5478
- ]
6349
+ jiraError === void 0 ? null : /* @__PURE__ */ jsx(FailureNotice, {
6350
+ label: t("jiraError"),
6351
+ failure: jiraError
5479
6352
  })
5480
6353
  ]
5481
6354
  }),
@@ -5536,17 +6409,9 @@ window.__ModuleLoader__.load({
5536
6409
  style: notice,
5537
6410
  children: t("restartRequired")
5538
6411
  }),
5539
- updateError === void 0 ? null : /* @__PURE__ */ jsxs("p", {
5540
- role: "alert",
5541
- style: {
5542
- ...notice,
5543
- color: "var(--dsw-alias-state-error-primary)"
5544
- },
5545
- children: [
5546
- t("updateError"),
5547
- ": ",
5548
- updateError
5549
- ]
6412
+ updateError === void 0 ? null : /* @__PURE__ */ jsx(FailureNotice, {
6413
+ label: t("updateError"),
6414
+ failure: updateError
5550
6415
  }),
5551
6416
  /* @__PURE__ */ jsxs("div", {
5552
6417
  style: settingsActions,
@@ -5598,14 +6463,14 @@ window.__ModuleLoader__.load({
5598
6463
  function record$4(value) {
5599
6464
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
5600
6465
  }
5601
- async function response$1(pending) {
5602
- const result = await pending;
5603
- const body = await result.json();
5604
- if (!result.ok) {
5605
- const error = record$4(body);
5606
- throw new RepositoryActionClientError(typeof error?.message === "string" ? error.message : "Repository action failed.", typeof error?.error === "string" ? error.error : void 0, typeof error?.commit === "string" ? error.commit : void 0);
5607
- }
5608
- return body;
6466
+ /** The dialog branches on `code`, so a route refusal keeps arriving as this
6467
+ * class rather than as the transport's own error.
6468
+ *
6469
+ * `commit` cannot be carried across: the transport forwards a failed route's
6470
+ * message and error code, not the rest of its body, so a commit that survived
6471
+ * a failed push no longer reaches the dialog that offers its hash. */
6472
+ function actionError(error) {
6473
+ return error instanceof PluginRequestError ? new RepositoryActionClientError(error.message, error.code) : error;
5609
6474
  }
5610
6475
  function preview(value) {
5611
6476
  const input = record$4(value);
@@ -5625,42 +6490,35 @@ window.__ModuleLoader__.load({
5625
6490
  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.");
5626
6491
  return input;
5627
6492
  }
5628
- function endpoint(path, sessionId) {
5629
- return `${CLAUDE_REPOSITORY_ACTION_PATH}${path}?sessionId=${encodeURIComponent(sessionId)}`;
5630
- }
6493
+ /** The preview only chains local Git; everything that writes may reach a remote. */
5631
6494
  async function loadRepositoryActionPreview(sessionId, signal) {
5632
- return preview(await response$1(fetch(endpoint("/preview", sessionId), {
5633
- method: "GET",
5634
- credentials: "same-origin",
5635
- headers: { accept: "application/json" },
5636
- signal: pluginRequestSignal(PLUGIN_READ_TIMEOUT_MS, signal)
5637
- })));
6495
+ try {
6496
+ return preview(await pluginRead(`${CLAUDE_REPOSITORY_ACTION_PATH}/preview`, "git", signal, { query: { sessionId } }));
6497
+ } catch (error) {
6498
+ throw actionError(error);
6499
+ }
5638
6500
  }
5639
6501
  async function generateCommitMessage(sessionId, fingerprint, signal) {
5640
- const value = record$4(await response$1(fetch(endpoint("/message", sessionId), {
5641
- method: "POST",
5642
- credentials: "same-origin",
5643
- headers: {
5644
- accept: "application/json",
5645
- "content-type": "application/json"
5646
- },
5647
- body: JSON.stringify({ fingerprint }),
5648
- signal: pluginRequestSignal(PLUGIN_ACTION_TIMEOUT_MS, signal)
5649
- })));
5650
- if (typeof value?.message !== "string") throw new Error("Invalid generated commit message.");
5651
- return value.message;
5652
- }
5653
- function executeRepositoryAction(sessionId, request) {
5654
- return response$1(fetch(endpoint("", sessionId), {
5655
- method: "POST",
5656
- credentials: "same-origin",
5657
- headers: {
5658
- accept: "application/json",
5659
- "content-type": "application/json"
5660
- },
5661
- body: JSON.stringify(request),
5662
- signal: pluginRequestSignal(PLUGIN_ACTION_TIMEOUT_MS)
5663
- })).then(result);
6502
+ try {
6503
+ const value = record$4(await pluginWrite(`${CLAUDE_REPOSITORY_ACTION_PATH}/message`, "remote", signal, {
6504
+ query: { sessionId },
6505
+ json: { fingerprint }
6506
+ }));
6507
+ if (typeof value?.message !== "string") throw new Error("Invalid generated commit message.");
6508
+ return value.message;
6509
+ } catch (error) {
6510
+ throw actionError(error);
6511
+ }
6512
+ }
6513
+ async function executeRepositoryAction(sessionId, request) {
6514
+ try {
6515
+ return result(await pluginWrite(CLAUDE_REPOSITORY_ACTION_PATH, "remote", void 0, {
6516
+ query: { sessionId },
6517
+ json: request
6518
+ }));
6519
+ } catch (error) {
6520
+ throw actionError(error);
6521
+ }
5664
6522
  }
5665
6523
  //#endregion
5666
6524
  //#region src/client/action-toast.tsx
@@ -5722,130 +6580,79 @@ window.__ModuleLoader__.load({
5722
6580
  if (event?.type === "error" && typeof event.message === "string") throw new Error(event.message);
5723
6581
  throw new Error("Invalid repository setup progress response.");
5724
6582
  }
5725
- async function response(pending) {
5726
- const result = await pending;
5727
- const body = await result.json();
5728
- if (!result.ok) {
5729
- const error = body;
5730
- throw new Error(error.message ?? error.error ?? "Repository setup failed.");
5731
- }
5732
- return body;
5733
- }
5734
- /** A wedged host must surface as an error; the hero has no other way out of its loading state. */
5735
- const BRANCH_LOAD_TIMEOUT_MS = 15e3;
5736
6583
  function loadRepositoryBranches(cwd, signal) {
5737
- return response(fetch(`${CLAUDE_REPOSITORY_SETUP_PATH}/branches?cwd=${encodeURIComponent(cwd)}`, {
5738
- method: "GET",
5739
- credentials: "same-origin",
5740
- headers: { accept: "application/json" },
5741
- signal: pluginRequestSignal(BRANCH_LOAD_TIMEOUT_MS, signal)
5742
- }));
6584
+ return pluginRead(`${CLAUDE_REPOSITORY_SETUP_PATH}/branches`, "git", signal, { query: { cwd } });
5743
6585
  }
5744
6586
  /** Pull remote refs down first, then list: a POST because `--prune` rewrites
5745
- * this checkout's remote-tracking refs, and on the action deadline because the
6587
+ * this checkout's remote-tracking refs, and on the remote budget because the
5746
6588
  * host's own `git fetch` runs for up to a minute. */
5747
6589
  async function refreshRepositoryBranches(cwd, signal) {
5748
- const result = await fetch(`${CLAUDE_REPOSITORY_SETUP_PATH}/branches/refresh`, {
5749
- method: "POST",
5750
- credentials: "same-origin",
5751
- headers: {
5752
- accept: "application/json",
5753
- "content-type": "application/json"
5754
- },
5755
- signal: pluginRequestSignal(PLUGIN_ACTION_TIMEOUT_MS, signal),
5756
- body: JSON.stringify({ cwd })
5757
- });
5758
- if (result.status === 404) throw new Error("route-missing");
5759
- return response(Promise.resolve(result));
6590
+ try {
6591
+ return await pluginWrite(`${CLAUDE_REPOSITORY_SETUP_PATH}/branches/refresh`, "remote", signal, { json: { cwd } });
6592
+ } catch (error) {
6593
+ if (error instanceof PluginRequestError && error.reason === "route-missing") throw new Error("route-missing");
6594
+ throw error;
6595
+ }
5760
6596
  }
5761
6597
  async function prepareRepository(cwd, branch, worktree, branchName, onProgress = () => {}) {
5762
- const result = await fetch(CLAUDE_REPOSITORY_SETUP_PATH, {
5763
- method: "POST",
5764
- credentials: "same-origin",
5765
- headers: {
5766
- accept: "application/x-ndjson",
5767
- "content-type": "application/json"
5768
- },
5769
- body: JSON.stringify({
5770
- cwd,
5771
- branch,
5772
- worktree,
5773
- ...branchName === void 0 ? {} : { branchName }
5774
- })
5775
- });
5776
- if (!result.ok) {
5777
- const body = await result.json();
5778
- throw new Error(body.message ?? body.error ?? "Repository setup failed.");
5779
- }
5780
- if (result.body === null) throw new Error("Repository setup progress stream is unavailable.");
5781
- const reader = result.body.getReader();
5782
- const decoder = new TextDecoder();
5783
- let buffer = "";
5784
- let completed;
5785
- while (true) {
5786
- const chunk = await reader.read();
5787
- buffer += decoder.decode(chunk.value, { stream: !chunk.done });
5788
- const lines = buffer.split("\n");
5789
- buffer = lines.pop() ?? "";
5790
- for (const line of lines) {
5791
- if (line.trim().length === 0) continue;
5792
- const value = parseRepositorySetupEvent(line, onProgress);
6598
+ const carrier = new AbortController();
6599
+ try {
6600
+ const reader = await pluginNdjson(CLAUDE_REPOSITORY_SETUP_PATH, carrier.signal, {
6601
+ method: "POST",
6602
+ json: {
6603
+ cwd,
6604
+ branch,
6605
+ worktree,
6606
+ ...branchName === void 0 ? {} : { branchName }
6607
+ }
6608
+ });
6609
+ const decoder = new TextDecoder();
6610
+ let buffer = "";
6611
+ let completed;
6612
+ while (true) {
6613
+ const chunk = await reader.read();
6614
+ buffer += decoder.decode(chunk.value, { stream: !chunk.done });
6615
+ const lines = buffer.split("\n");
6616
+ buffer = lines.pop() ?? "";
6617
+ for (const line of lines) {
6618
+ if (line.trim().length === 0) continue;
6619
+ const value = parseRepositorySetupEvent(line, onProgress);
6620
+ if (value !== void 0) completed = value;
6621
+ }
6622
+ if (chunk.done) break;
6623
+ }
6624
+ if (buffer.trim().length > 0) {
6625
+ const value = parseRepositorySetupEvent(buffer, onProgress);
5793
6626
  if (value !== void 0) completed = value;
5794
6627
  }
5795
- if (chunk.done) break;
6628
+ if (completed === void 0) throw new Error("Repository setup progress ended before completion.");
6629
+ return completed;
6630
+ } finally {
6631
+ carrier.abort();
5796
6632
  }
5797
- if (buffer.trim().length > 0) {
5798
- const value = parseRepositorySetupEvent(buffer, onProgress);
5799
- if (value !== void 0) completed = value;
5800
- }
5801
- if (completed === void 0) throw new Error("Repository setup progress ended before completion.");
5802
- return completed;
5803
6633
  }
5804
- /** Bookkeeping, not a gate: a wedged host must fail this instead of hanging on
5805
- * a connection the rest of the flow is waiting behind. */
5806
- const LEASE_BIND_TIMEOUT_MS = 1e4;
5807
6634
  async function bindRepositoryLease(leaseId, sessionId) {
5808
- await response(fetch(`${CLAUDE_REPOSITORY_SETUP_PATH}/bind`, {
5809
- method: "POST",
5810
- credentials: "same-origin",
5811
- signal: pluginRequestSignal(LEASE_BIND_TIMEOUT_MS),
5812
- headers: {
5813
- accept: "application/json",
5814
- "content-type": "application/json"
5815
- },
5816
- body: JSON.stringify({
5817
- leaseId,
5818
- sessionId
5819
- })
5820
- }));
6635
+ await pluginWrite(`${CLAUDE_REPOSITORY_SETUP_PATH}/bind`, "remote", void 0, { json: {
6636
+ leaseId,
6637
+ sessionId
6638
+ } });
5821
6639
  }
5822
6640
  async function cleanupMergedRepository(path, baseBranch) {
5823
- const body = await response(fetch(`${CLAUDE_REPOSITORY_SETUP_PATH}/cleanup`, {
5824
- signal: pluginRequestSignal(PLUGIN_ACTION_TIMEOUT_MS),
5825
- method: "POST",
5826
- credentials: "same-origin",
5827
- headers: {
5828
- accept: "application/json",
5829
- "content-type": "application/json"
5830
- },
5831
- body: JSON.stringify({
5832
- path,
5833
- baseBranch
5834
- })
5835
- }));
6641
+ const body = await pluginWrite(`${CLAUDE_REPOSITORY_SETUP_PATH}/cleanup`, "remote", void 0, { json: {
6642
+ path,
6643
+ baseBranch
6644
+ } });
5836
6645
  if (body.mode !== "worktree" && body.mode !== "checkout" || typeof body.root !== "string" || typeof body.branch !== "string") throw new Error("Invalid repository cleanup response.");
5837
6646
  return body;
5838
6647
  }
5839
6648
  /** Lines [from, to] of a working-tree file plus its total line count, for expanding unmodified diff context. */
5840
6649
  async function loadRepositoryFileLines(cwd, path, from, to, signal) {
5841
- const query = `cwd=${encodeURIComponent(cwd)}&path=${encodeURIComponent(path)}&from=${from}&to=${to}`;
5842
- const body = await response(fetch(`${CLAUDE_REPOSITORY_FILE_PATH}?${query}`, {
5843
- signal: pluginRequestSignal(PLUGIN_READ_TIMEOUT_MS),
5844
- method: "GET",
5845
- credentials: "same-origin",
5846
- headers: { accept: "application/json" },
5847
- ...signal === void 0 ? {} : { signal }
5848
- }));
6650
+ const body = await pluginRead(CLAUDE_REPOSITORY_FILE_PATH, "git", signal, { query: {
6651
+ cwd,
6652
+ path,
6653
+ from: String(from),
6654
+ to: String(to)
6655
+ } });
5849
6656
  if (!Array.isArray(body.lines) || typeof body.total !== "number") throw new Error("Invalid repository file response.");
5850
6657
  return {
5851
6658
  lines: body.lines.map(String),
@@ -5853,13 +6660,7 @@ window.__ModuleLoader__.load({
5853
6660
  };
5854
6661
  }
5855
6662
  async function loadRepositoryStatusFor(cwd, signal) {
5856
- const body = await response(fetch(`${CLAUDE_REPOSITORY_STATUS_PATH}?cwd=${encodeURIComponent(cwd)}`, {
5857
- signal: pluginRequestSignal(PLUGIN_READ_TIMEOUT_MS),
5858
- method: "GET",
5859
- credentials: "same-origin",
5860
- headers: { accept: "application/json" },
5861
- ...signal === void 0 ? {} : { signal }
5862
- }));
6663
+ const body = await pluginRead(CLAUDE_REPOSITORY_STATUS_PATH, "git", signal, { query: { cwd } });
5863
6664
  if (typeof body.status !== "string" || typeof body.cwd !== "string") throw new Error("Invalid repository status response.");
5864
6665
  return body;
5865
6666
  }
@@ -5896,34 +6697,27 @@ window.__ModuleLoader__.load({
5896
6697
  function record$2(value) {
5897
6698
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
5898
6699
  }
5899
- function feedbackUrl(path, sessionId, pullNumber, extra = "") {
5900
- return `${CLAUDE_REPOSITORY_FEEDBACK_PATH}${path}?sessionId=${encodeURIComponent(sessionId)}&number=${pullNumber}${extra}`;
6700
+ function feedbackQuery(sessionId, pullNumber, extra) {
6701
+ return {
6702
+ sessionId,
6703
+ number: String(pullNumber),
6704
+ ...extra
6705
+ };
5901
6706
  }
5902
- async function answer(response) {
5903
- const body = record$2(await response.json());
5904
- if (!response.ok) throw new Error(typeof body?.message === "string" ? body.message : "Pull request feedback is unavailable.");
6707
+ function answer(value) {
6708
+ const body = record$2(value);
5905
6709
  if (body === void 0) throw new Error("Invalid pull request feedback response.");
5906
6710
  return body;
5907
6711
  }
5908
- async function loadJson(path, sessionId, pullNumber, signal, extra = "") {
5909
- return answer(await fetch(feedbackUrl(path, sessionId, pullNumber, extra), {
5910
- method: "GET",
5911
- credentials: "same-origin",
5912
- headers: { accept: "application/json" },
5913
- signal: pluginRequestSignal(PLUGIN_READ_TIMEOUT_MS, signal)
5914
- }));
6712
+ /** Every arm of this route shells out to `gh`, so reads and writes alike take
6713
+ * the remote budget. */
6714
+ async function loadJson(path, sessionId, pullNumber, signal, extra) {
6715
+ return answer(await pluginRead(`${CLAUDE_REPOSITORY_FEEDBACK_PATH}${path}`, "remote", signal, { query: feedbackQuery(sessionId, pullNumber, extra) }));
5915
6716
  }
5916
- /** Writes reach GitHub through `gh`, so they get the action deadline. */
5917
6717
  async function postJson(path, sessionId, pullNumber, input) {
5918
- return answer(await fetch(feedbackUrl(path, sessionId, pullNumber), {
5919
- method: "POST",
5920
- credentials: "same-origin",
5921
- headers: {
5922
- accept: "application/json",
5923
- "content-type": "application/json"
5924
- },
5925
- signal: pluginRequestSignal(PLUGIN_ACTION_TIMEOUT_MS),
5926
- body: JSON.stringify(input)
6718
+ return answer(await pluginWrite(`${CLAUDE_REPOSITORY_FEEDBACK_PATH}${path}`, "remote", void 0, {
6719
+ query: feedbackQuery(sessionId, pullNumber),
6720
+ json: input
5927
6721
  }));
5928
6722
  }
5929
6723
  function reviewComment(value) {
@@ -5972,7 +6766,7 @@ window.__ModuleLoader__.load({
5972
6766
  }
5973
6767
  /** Logins GitHub would notify, for the reply composer's `@` completion. */
5974
6768
  async function loadMentionableUsers(sessionId, pullNumber, query, signal) {
5975
- const body = await loadJson("/mentionables", sessionId, pullNumber, signal, `&q=${encodeURIComponent(query)}`);
6769
+ const body = await loadJson("/mentionables", sessionId, pullNumber, signal, { q: query });
5976
6770
  if (!Array.isArray(body.users)) return [];
5977
6771
  const users = [];
5978
6772
  for (const item of body.users) {
@@ -7128,22 +7922,10 @@ window.__ModuleLoader__.load({
7128
7922
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
7129
7923
  }
7130
7924
  async function post(path, sessionId, body) {
7131
- const response = await fetch(`${CLAUDE_REVIEW_COMMENT_PATH}${path}?sessionId=${encodeURIComponent(sessionId)}`, {
7132
- method: "POST",
7133
- credentials: "same-origin",
7134
- headers: {
7135
- accept: "application/json",
7136
- "content-type": "application/json"
7137
- },
7138
- body: JSON.stringify(body),
7139
- signal: pluginRequestSignal(PLUGIN_READ_TIMEOUT_MS)
7925
+ return await pluginWrite(`${CLAUDE_REVIEW_COMMENT_PATH}${path}`, "fast", void 0, {
7926
+ query: { sessionId },
7927
+ json: body
7140
7928
  });
7141
- const value = await response.json();
7142
- if (!response.ok) {
7143
- const error = record$1(value);
7144
- throw new Error(typeof error?.message === "string" ? error.message : "Review comment request failed.");
7145
- }
7146
- return value;
7147
7929
  }
7148
7930
  async function addReviewComment(sessionId, comment) {
7149
7931
  const created = record$1(record$1(await post("", sessionId, comment))?.comment);
@@ -13302,6 +14084,16 @@ window.__ModuleLoader__.load({
13302
14084
  });
13303
14085
  }
13304
14086
  //#endregion
14087
+ //#region src/client/session-preset.ts
14088
+ /**
14089
+ * Resolve one row's preset id, newest seat first.
14090
+ * @param row - a session-list row, or undefined when the id is not listed.
14091
+ * @returns the preset id, or undefined when neither source carries one.
14092
+ */
14093
+ function sessionRowPreset(row) {
14094
+ return row?.agentPreset ?? row?.projectionValues?.agentPreset;
14095
+ }
14096
+ //#endregion
13305
14097
  //#region src/client/ClaudePullRequestsPanel.tsx
13306
14098
  const NO_WORKSPACE_STATE = {};
13307
14099
  const NO_WORKSPACES = {
@@ -13324,7 +14116,7 @@ window.__ModuleLoader__.load({
13324
14116
  function claudeSessionRows(state, archivedSessionIds = []) {
13325
14117
  const rows = state.ids === void 0 ? Object.values(state.byId) : state.ids.map((id) => state.byId[id]);
13326
14118
  const archived = new Set(archivedSessionIds);
13327
- return rows.filter((row) => row !== void 0 && row.agentPreset === "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));
14119
+ 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));
13328
14120
  }
13329
14121
  function Badge({ label, tone = "neutral" }) {
13330
14122
  const toneStyle = tone === "success" ? repositoryItemSuccess : tone === "warning" ? repositoryItemWarning : tone === "error" ? repositoryItemError : tone === "merged" ? { color: "#a78bfa" } : {};
@@ -13519,56 +14311,64 @@ window.__ModuleLoader__.load({
13519
14311
  };
13520
14312
  throw new Error("Invalid ask stream event.");
13521
14313
  }
14314
+ async function openAnswer(sessionId, request, cancel) {
14315
+ try {
14316
+ return await pluginNdjson(CLAUDE_ASK_PATH, cancel, {
14317
+ method: "POST",
14318
+ query: { sessionId },
14319
+ json: request
14320
+ });
14321
+ } catch (error) {
14322
+ if (error instanceof PluginRequestError && error.reason === "http") throw new Error("The question could not be sent.");
14323
+ throw error;
14324
+ }
14325
+ }
13522
14326
  /** Stream an answer about selected reply text; resolves when the answer completes. */
13523
14327
  async function askAboutSelection(sessionId, request, onProgress, signal) {
13524
- const response = await fetch(`${CLAUDE_ASK_PATH}?sessionId=${encodeURIComponent(sessionId)}`, {
13525
- method: "POST",
13526
- credentials: "same-origin",
13527
- headers: {
13528
- accept: "application/x-ndjson",
13529
- "content-type": "application/json"
13530
- },
13531
- body: JSON.stringify(request),
13532
- ...signal === void 0 ? {} : { signal }
13533
- });
13534
- if (!response.ok) {
13535
- const body = record(await response.json().catch(() => void 0));
13536
- throw new Error(typeof body?.message === "string" ? body.message : "The question could not be sent.");
13537
- }
13538
- if (response.body === null) throw new Error("The answer stream is unavailable.");
13539
- const reader = response.body.getReader();
13540
- const decoder = new TextDecoder();
13541
- let buffer = "";
13542
- let finished = false;
13543
- const handle = (line) => {
13544
- if (line.trim().length === 0) return;
13545
- const event = parseAskEvent(line);
13546
- if (event.type === "delta") onProgress({
13547
- type: "text",
13548
- text: event.text
13549
- });
13550
- else if (event.type === "thinking") onProgress({
13551
- type: "thinking",
13552
- text: event.text
13553
- });
13554
- else if (event.type === "status") onProgress({
13555
- type: "status",
13556
- text: event.text
13557
- });
13558
- else if (event.type === "tool") onProgress(event);
13559
- else if (event.type === "done") finished = true;
13560
- else throw new Error(event.message);
14328
+ const carrier = new AbortController();
14329
+ const stop = () => {
14330
+ carrier.abort();
13561
14331
  };
13562
- while (true) {
13563
- const chunk = await reader.read();
13564
- buffer += decoder.decode(chunk.value, { stream: !chunk.done });
13565
- const lines = buffer.split("\n");
13566
- buffer = lines.pop() ?? "";
13567
- for (const line of lines) handle(line);
13568
- if (chunk.done) break;
14332
+ signal?.addEventListener("abort", stop, { once: true });
14333
+ if (signal?.aborted === true) carrier.abort();
14334
+ try {
14335
+ const reader = await openAnswer(sessionId, request, carrier.signal);
14336
+ const decoder = new TextDecoder();
14337
+ let buffer = "";
14338
+ let finished = false;
14339
+ const handle = (line) => {
14340
+ if (line.trim().length === 0) return;
14341
+ const event = parseAskEvent(line);
14342
+ if (event.type === "delta") onProgress({
14343
+ type: "text",
14344
+ text: event.text
14345
+ });
14346
+ else if (event.type === "thinking") onProgress({
14347
+ type: "thinking",
14348
+ text: event.text
14349
+ });
14350
+ else if (event.type === "status") onProgress({
14351
+ type: "status",
14352
+ text: event.text
14353
+ });
14354
+ else if (event.type === "tool") onProgress(event);
14355
+ else if (event.type === "done") finished = true;
14356
+ else throw new Error(event.message);
14357
+ };
14358
+ while (true) {
14359
+ const chunk = await reader.read();
14360
+ buffer += decoder.decode(chunk.value, { stream: !chunk.done });
14361
+ const lines = buffer.split("\n");
14362
+ buffer = lines.pop() ?? "";
14363
+ for (const line of lines) handle(line);
14364
+ if (chunk.done) break;
14365
+ }
14366
+ handle(buffer);
14367
+ if (!finished) throw new Error("The answer ended unexpectedly.");
14368
+ } finally {
14369
+ signal?.removeEventListener("abort", stop);
14370
+ carrier.abort();
13569
14371
  }
13570
- handle(buffer);
13571
- if (!finished) throw new Error("The answer ended unexpectedly.");
13572
14372
  }
13573
14373
  //#endregion
13574
14374
  //#region src/client/ClaudeSelectionAsk.tsx
@@ -14124,13 +14924,12 @@ window.__ModuleLoader__.load({
14124
14924
  });
14125
14925
  return stop;
14126
14926
  }
14927
+ /** A beacon rather than a request: a finding is dropped when the connection
14928
+ * budget is spent, because the channel that reports the plugin's own failures
14929
+ * must never be the traffic that causes them — and must never queue behind
14930
+ * them either. */
14127
14931
  function postToHost(report) {
14128
- fetch(CLAUDE_CLIENT_DIAGNOSTICS_PATH, {
14129
- method: "POST",
14130
- credentials: "same-origin",
14131
- headers: { "content-type": "application/json" },
14132
- body: JSON.stringify(report)
14133
- }).catch(() => {});
14932
+ pluginBeacon(CLAUDE_CLIENT_DIAGNOSTICS_PATH, report);
14134
14933
  }
14135
14934
  /** Renderer-side findings reach the Host log through here.
14136
14935
  *
@@ -14217,23 +15016,15 @@ window.__ModuleLoader__.load({
14217
15016
  /** Drop one user message and everything after it: the rows are hidden from
14218
15017
  * this session's transcript and Claude resumes before that turn. */
14219
15018
  async function rewindSession(sessionId, seq) {
14220
- const result = await fetch(CLAUDE_REWIND_PATH, {
14221
- method: "POST",
14222
- credentials: "same-origin",
14223
- signal: pluginRequestSignal(PLUGIN_READ_TIMEOUT_MS),
14224
- headers: {
14225
- "content-type": "application/json",
14226
- accept: "application/json"
14227
- },
14228
- body: JSON.stringify({
15019
+ try {
15020
+ await pluginWrite(CLAUDE_REWIND_PATH, "fast", void 0, { json: {
14229
15021
  sessionId,
14230
15022
  seq
14231
- })
14232
- });
14233
- if (result.ok) return;
14234
- if (result.status === 404) throw new Error("route-missing");
14235
- const body = await result.json().catch(() => void 0);
14236
- throw new Error(typeof body?.error === "string" ? body.error : `http-${result.status}`);
15023
+ } });
15024
+ } catch (error) {
15025
+ if (!(error instanceof PluginRequestError)) throw error;
15026
+ throw new Error(error.code ?? error.reason);
15027
+ }
14237
15028
  }
14238
15029
  //#endregion
14239
15030
  //#region src/client/ClaudeRewind.tsx
@@ -15449,15 +16240,10 @@ window.__ModuleLoader__.load({
15449
16240
  //#region src/client/editor-open-api.ts
15450
16241
  /** Launch the session's project in a desktop editor on the host machine. */
15451
16242
  async function openProjectInEditor(sessionId, editor) {
15452
- const result = await fetch(`${CLAUDE_EDITOR_OPEN_PATH}?sessionId=${encodeURIComponent(sessionId)}&editor=${encodeURIComponent(editor)}`, {
15453
- method: "POST",
15454
- credentials: "same-origin",
15455
- signal: pluginRequestSignal(PLUGIN_READ_TIMEOUT_MS),
15456
- headers: { accept: "application/json" }
15457
- });
15458
- if (result.ok) return;
15459
- const body = await result.json().catch(() => void 0);
15460
- throw new Error(typeof body?.message === "string" ? body.message : "The editor could not be launched.");
16243
+ await pluginWrite(CLAUDE_EDITOR_OPEN_PATH, "fast", void 0, { query: {
16244
+ sessionId,
16245
+ editor
16246
+ } });
15461
16247
  }
15462
16248
  //#endregion
15463
16249
  //#region src/client/ClaudeSessionMenu.tsx
@@ -15729,7 +16515,7 @@ window.__ModuleLoader__.load({
15729
16515
  document.head.appendChild(element);
15730
16516
  }
15731
16517
  function ClaudeAgentPresetLabel({ t, hostT, roster, sessionId, useSessions }) {
15732
- const preset = useSessions((state) => state.byId[sessionId]?.agentPreset);
16518
+ const preset = useSessions((state) => sessionRowPreset(state.byId[sessionId]));
15733
16519
  const rows = useSyncExternalStore(roster.subscribe, roster.getSnapshot, roster.getSnapshot);
15734
16520
  const { load } = roster;
15735
16521
  useEffect(() => {
@@ -16162,6 +16948,14 @@ window.__ModuleLoader__.load({
16162
16948
  globalSettingsNewSession: "修改仅对新建 Claude 会话生效。",
16163
16949
  globalSettingsError: "全局设置保存失败",
16164
16950
  outputStyle: "Output Style",
16951
+ renderer: "AI 输出渲染器",
16952
+ rendererPlugin: "插件渲染器",
16953
+ rendererNative: "DSH 原生渲染器",
16954
+ rendererEffect: "插件渲染器沿用本插件自带的转录视图:交错的正文、成组的工具卡片与活动行。DSH 原生渲染器改由 DSH 自身绘制:正文作为普通助手文本块,思考作为推理块,Claude 的顶层工具会镜像成原生工具卡片。修改从下一个回合起生效;已经产生的回合仍按记录时的渲染器显示,不会重绘。",
16955
+ prose: "Markdown 彩色高亮",
16956
+ prosePlain: "关闭",
16957
+ proseEnhanced: "开启",
16958
+ proseEffect: "开启后,正文的标题、加粗、斜体、行内代码、列表符号、引用与链接各自着色,代码块换成深色描边底。这会覆盖本插件默认对齐 Claude 桌面版的配色。仅在使用插件渲染器时有效,改动立即生效,无需等待下一回合。",
16165
16959
  worktreeBranchPrefix: "Worktree 分支前缀",
16166
16960
  worktreeBranchPrefixEffect: "分支前缀修改会应用于之后自动创建的 Worktree 分支;显式指定的完整分支名不会被修改。",
16167
16961
  maxProcessesSetting: "Claude 进程上限",
@@ -16474,7 +17268,12 @@ window.__ModuleLoader__.load({
16474
17268
  rewindSubmitting: "回退中…",
16475
17269
  rewindFailed: "回退失败({code})。",
16476
17270
  rewindBusy: "这个会话正在运行,请等本轮结束后再回退。",
16477
- rewindStale: "宿主进程里的插件还是旧版本(没有回退接口)。重启 DSH 后再试。"
17271
+ rewindStale: "宿主进程里的插件还是旧版本(没有回退接口)。重启 DSH 后再试。",
17272
+ turnUsage: "本回合用量",
17273
+ turnUsageTokens: "{count} tok",
17274
+ turnUsageCache: "缓存命中 {percent}%",
17275
+ turnUsageTtft: "首字 {duration}",
17276
+ turnUsageCost: "累计 ${cost}"
16478
17277
  };
16479
17278
  const en = {
16480
17279
  nav: "Claude Code",
@@ -16522,6 +17321,14 @@ window.__ModuleLoader__.load({
16522
17321
  globalSettingsNewSession: "Changes apply only to new Claude sessions.",
16523
17322
  globalSettingsError: "Global settings save failed",
16524
17323
  outputStyle: "Output Style",
17324
+ renderer: "AI output renderer",
17325
+ rendererPlugin: "Plugin renderer",
17326
+ rendererNative: "DSH native renderer",
17327
+ rendererEffect: "The plugin renderer keeps this package’s own transcript: interleaved prose, grouped tool cards, and activity rows. The DSH native renderer hands the same turn to DSH itself — prose as ordinary assistant text blocks, thinking as reasoning blocks, and root Claude tools mirrored into native tool cards. A change applies from the next turn; turns already recorded keep the renderer they were recorded with and are not redrawn.",
17328
+ prose: "Markdown colour highlighting",
17329
+ prosePlain: "Off",
17330
+ proseEnhanced: "On",
17331
+ proseEffect: "Gives headings, bold, italics, inline code, list markers, quotes and links their own colours, and paints the code block on a dark outlined surface. This overrides the Claude-desktop palette this package matches by default. Only applies under the plugin renderer, and takes effect immediately rather than on the next turn.",
16525
17332
  worktreeBranchPrefix: "Worktree branch prefix",
16526
17333
  worktreeBranchPrefixEffect: "Prefix changes apply to subsequently generated Worktree branches. Explicit full branch names remain unchanged.",
16527
17334
  maxProcessesSetting: "Claude process limit",
@@ -16834,7 +17641,12 @@ window.__ModuleLoader__.load({
16834
17641
  rewindSubmitting: "Rewinding…",
16835
17642
  rewindFailed: "The rewind failed ({code}).",
16836
17643
  rewindBusy: "This session is running; wait for the turn to finish before rewinding.",
16837
- rewindStale: "The Host process is still running the previous plugin build, which has no rewind route. Restart DSH and try again."
17644
+ rewindStale: "The Host process is still running the previous plugin build, which has no rewind route. Restart DSH and try again.",
17645
+ turnUsage: "Turn usage",
17646
+ turnUsageTokens: "{count} tok",
17647
+ turnUsageCache: "Cache hit {percent}%",
17648
+ turnUsageTtft: "TTFT {duration}",
17649
+ turnUsageCost: "${cost} total"
16838
17650
  };
16839
17651
  //#endregion
16840
17652
  //#region src/client/index.tsx
@@ -16884,7 +17696,12 @@ window.__ModuleLoader__.load({
16884
17696
  }), "dsh-claude: client copy");
16885
17697
  const t = ctx.locale.bind(namespace);
16886
17698
  ctx.effect(() => restyleHostChrome(), "dsh-claude: Host chrome restyling");
16887
- const projections = new ClaudeProjectionStore();
17699
+ pluginRead(CLAUDE_GLOBAL_SETTINGS_PATH, "fast").then((payload) => {
17700
+ if (isGlobalSettingsView(payload)) applyClaudeMarkdownTheme(proseModeOf(payload.settings));
17701
+ }).catch(() => {});
17702
+ const projections = new ClaudeProjectionStore({ report: (kind, detail) => {
17703
+ diagnostics.report(kind, detail);
17704
+ } });
16888
17705
  ctx.effect(() => ctx.inputTriggers.registerSource(createClaudeCommandSource(ctx, projections)), "dsh-claude: Claude slash source");
16889
17706
  const sessions = ctx.get("sessions");
16890
17707
  const workspaces = ctx.get("workspaces");