@norman-else/dsh-claude 0.1.35 → 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
@@ -30,6 +30,10 @@ window.__ModuleLoader__.load({
30
30
  function isClaudeRenderMode(value) {
31
31
  return value === "plugin" || value === "native";
32
32
  }
33
+ const DEFAULT_CLAUDE_PROSE_MODE = "plain";
34
+ function isClaudeProseMode(value) {
35
+ return value === "plain" || value === "enhanced";
36
+ }
33
37
  //#endregion
34
38
  //#region src/client/task-projection.ts
35
39
  /** Tasks UI is reserved for detached work and genuine Claude subagents. */
@@ -270,6 +274,62 @@ window.__ModuleLoader__.load({
270
274
  }
271
275
  return failed ? `Failed to ${failedAction}` : completed;
272
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
+ }
273
333
  /** Whether the Host drew this step with DSH's own renderer.
274
334
  *
275
335
  * The stamp rides on the records themselves rather than on a Client-side copy
@@ -342,6 +402,15 @@ window.__ModuleLoader__.load({
342
402
  });
343
403
  continue;
344
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
+ }
345
414
  if (activity.kind === "tool-call" && activity.toolUseId !== void 0 && activity.toolName !== void 0) {
346
415
  group ??= {
347
416
  ordinal: activity.ordinal,
@@ -1106,6 +1175,14 @@ window.__ModuleLoader__.load({
1106
1175
  cursor: "pointer"
1107
1176
  };
1108
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
+ };
1109
1186
  const taskActivityList = {
1110
1187
  display: "flex",
1111
1188
  flexDirection: "column",
@@ -3352,1079 +3429,718 @@ window.__ModuleLoader__.load({
3352
3429
  lineHeight: "20px"
3353
3430
  };
3354
3431
  //#endregion
3355
- //#region src/client/token-format.ts
3356
- function formatTokenCount(tokens) {
3357
- if (tokens >= 1e6) return `${Number((tokens / 1e6).toFixed(tokens >= 1e7 ? 0 : 1))}M`;
3358
- if (tokens >= 1e3) return `${Number((tokens / 1e3).toFixed(tokens >= 1e5 ? 0 : 1))}K`;
3359
- return String(tokens);
3360
- }
3361
- //#endregion
3362
- //#region src/client/ClaudeTasksPanel.tsx
3363
- const STATUS_LABEL = {
3364
- running: "tasksRunning",
3365
- completed: "tasksCompleted",
3366
- failed: "tasksFailed",
3367
- stopped: "tasksStopped",
3368
- killed: "tasksKilled"
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"
3369
3606
  };
3370
- function visibleTaskGroups(tasks, dismissedSettledIds) {
3371
- const projected = tasks.filter(isProjectedTask);
3372
- return {
3373
- running: projected.filter((task) => task.status === "running"),
3374
- finished: projected.filter((task) => task.status !== "running" && !dismissedSettledIds.has(task.taskId))
3375
- };
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("");
3376
3625
  }
3377
- function activitiesForTask(activities, taskId) {
3378
- return activities.filter((activity) => activity.taskId === taskId);
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;
3379
3681
  }
3380
- function tasksForTurn(tasks, turn) {
3381
- return tasks.filter((task) => task.originTurn === turn && isProjectedTask(task));
3682
+ /** Attach the sheet, keeping whatever mode is already set. Idempotent. */
3683
+ function ensureClaudeMarkdownTheme() {
3684
+ write();
3382
3685
  }
3383
- function summarizeTurnTasks(tasks) {
3384
- if (tasks.length === 0) return void 0;
3385
- const running = tasks.filter((task) => task.status === "running").length;
3386
- const failed = tasks.filter((task) => task.status === "failed" || task.status === "stopped" || task.status === "killed").length;
3387
- const completed = tasks.filter((task) => task.status === "completed").length;
3388
- return {
3389
- state: running > 0 ? "running" : failed > 0 ? "failed" : "completed",
3390
- count: tasks.length,
3391
- running,
3392
- failed,
3393
- completed
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();
3691
+ }
3692
+ //#endregion
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 }
3394
3716
  };
3717
+ const Renderer = MarkdownText;
3718
+ ensureClaudeMarkdownTheme();
3719
+ return /* @__PURE__ */ jsx("div", {
3720
+ className: CLAUDE_MARKDOWN_SCOPE,
3721
+ children: /* @__PURE__ */ jsx(Renderer, { ...props })
3722
+ });
3395
3723
  }
3396
- function statusGlyph(status) {
3397
- if (status === "running") return "●";
3398
- if (status === "completed") return "✓";
3399
- if (status === "stopped") return "–";
3400
- return "×";
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;
3401
3760
  }
3402
- function formatDuration(ms) {
3403
- if (ms < 1e3) return String(Math.max(1, Math.round(ms))) + "ms";
3404
- const seconds = Math.round(ms / 1e3);
3405
- if (seconds < 60) return String(seconds) + "s";
3406
- const minutes = Math.floor(seconds / 60);
3407
- return String(minutes) + "m " + String(seconds % 60) + "s";
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);
3408
3780
  }
3409
- function taskMeta(task, t) {
3410
- const parts = [];
3411
- if (task.subagentType !== void 0) parts.push(task.subagentType);
3412
- else if (task.taskType !== void 0) parts.push(task.taskType);
3413
- if (task.usage?.durationMs !== void 0) parts.push(formatDuration(task.usage.durationMs));
3414
- if (task.usage?.totalTokens !== void 0) parts.push(t("tokens", { count: formatTokenCount(task.usage.totalTokens) }));
3415
- if (task.usage?.toolUses !== void 0) parts.push(t("tasksToolUses", { count: task.usage.toolUses }));
3416
- if (task.lastToolName !== void 0) parts.push(t("tasksLastTool", { tool: task.lastToolName }));
3417
- return parts;
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];
3418
3834
  }
3419
- function TaskActivity({ activity, t }) {
3420
- return /* @__PURE__ */ jsxs("li", {
3421
- style: taskActivityItem,
3422
- children: [/* @__PURE__ */ jsx("span", {
3423
- style: taskActivityGlyph,
3424
- "aria-hidden": "true",
3425
- children: activity.isError === true ? "×" : "›"
3426
- }), /* @__PURE__ */ jsxs("div", {
3427
- style: taskActivityBody,
3428
- children: [
3429
- /* @__PURE__ */ jsx("p", {
3430
- style: taskActivityTitle,
3431
- children: activity.title ?? activity.kind
3432
- }),
3433
- activity.summary === void 0 ? null : /* @__PURE__ */ jsx("p", {
3434
- style: taskActivitySummary,
3435
- children: activity.summary
3436
- }),
3437
- activity.detail === void 0 ? null : /* @__PURE__ */ jsxs("details", {
3438
- style: taskActivityDetail,
3439
- children: [/* @__PURE__ */ jsx("summary", {
3440
- style: taskActivityDetailSummary,
3441
- children: t("detail")
3442
- }), /* @__PURE__ */ jsx("pre", {
3443
- style: detailCode,
3444
- children: activity.detail
3445
- })]
3446
- })
3447
- ]
3448
- })]
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
+ }
3846
+ }
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);
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);
3449
3875
  });
3450
3876
  }
3451
- function TaskCard(props) {
3452
- const { task, activities, t } = props;
3453
- const [activityOpen, setActivityOpen] = useState(false);
3454
- const running = task.status === "running";
3455
- const failed = task.status === "failed" || task.status === "killed";
3456
- const meta = taskMeta(task, t);
3457
- return /* @__PURE__ */ jsxs("article", {
3458
- style: {
3459
- ...taskCard,
3460
- ...running ? taskCardRunning : {}
3461
- },
3462
- children: [
3463
- /* @__PURE__ */ jsxs("div", {
3464
- style: taskCardTop,
3465
- children: [/* @__PURE__ */ jsx("span", {
3466
- className: running ? "dsh-claude-act-running" : void 0,
3467
- style: {
3468
- ...taskCardGlyph,
3469
- ...running ? iconChipRunning : {},
3470
- ...failed ? iconChipError : {}
3471
- },
3472
- "aria-hidden": "true",
3473
- children: statusGlyph(task.status)
3474
- }), /* @__PURE__ */ jsxs("div", {
3475
- style: taskCardBody,
3476
- children: [/* @__PURE__ */ jsx("p", {
3477
- style: {
3478
- ...taskTitle,
3479
- ...failed ? { color: "var(--dsw-alias-state-error-primary)" } : {}
3480
- },
3481
- children: task.description
3482
- }), /* @__PURE__ */ jsxs("p", {
3483
- style: taskStatusLine,
3484
- children: [/* @__PURE__ */ jsx("span", { children: t(STATUS_LABEL[task.status] ?? "tasksRunning") }), task.backgrounded === true ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
3485
- "aria-hidden": "true",
3486
- children: " · "
3487
- }), /* @__PURE__ */ jsx("span", { children: t("tasksBackground") })] }) : null]
3488
- })]
3489
- })]
3490
- }),
3491
- meta.length === 0 ? null : /* @__PURE__ */ jsx("p", {
3492
- style: taskMeta$1,
3493
- children: meta.join(" · ")
3494
- }),
3495
- task.summary === void 0 || running ? null : /* @__PURE__ */ jsx("p", {
3496
- style: taskSummary,
3497
- children: task.summary
3498
- }),
3499
- activities.length === 0 ? null : /* @__PURE__ */ jsxs("div", {
3500
- style: taskActivitySection,
3501
- children: [/* @__PURE__ */ jsx("button", {
3502
- type: "button",
3503
- style: taskTextButton,
3504
- "aria-expanded": activityOpen,
3505
- onClick: () => setActivityOpen((value) => !value),
3506
- children: activityOpen ? t("tasksHideActivity") : t("tasksViewActivity")
3507
- }), activityOpen ? /* @__PURE__ */ jsx("ul", {
3508
- style: taskActivityList,
3509
- children: activities.map((activity) => /* @__PURE__ */ jsx(TaskActivity, {
3510
- activity,
3511
- t
3512
- }, `${activity.turn}:${activity.step}:${activity.ordinal}`))
3513
- }) : null]
3514
- })
3515
- ]
3516
- });
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}`;
3517
3881
  }
3518
- function GroupHeading(props) {
3519
- const { label, count, collapsed, onToggle, action } = props;
3520
- const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", { children: label }), /* @__PURE__ */ jsx("span", {
3521
- style: tasksGroupCount,
3522
- children: count
3523
- })] });
3524
- return /* @__PURE__ */ jsxs("div", {
3525
- style: tasksGroupHeading,
3526
- children: [onToggle === void 0 ? /* @__PURE__ */ jsx("div", {
3527
- style: tasksGroupTitle,
3528
- children: content
3529
- }) : /* @__PURE__ */ jsxs("button", {
3530
- type: "button",
3531
- style: tasksGroupToggle,
3532
- "aria-expanded": !collapsed,
3533
- onClick: onToggle,
3534
- children: [/* @__PURE__ */ jsx("span", {
3535
- style: {
3536
- ...chevron,
3537
- ...collapsed === true ? {} : chevronOpen
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));
3888
+ }
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"
3538
3921
  },
3539
- children: "›"
3540
- }), content]
3541
- }), action === void 0 ? null : /* @__PURE__ */ jsx("button", {
3542
- type: "button",
3543
- style: taskTextButton,
3544
- onClick: action.onClick,
3545
- children: action.label
3546
- })]
3547
- });
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
+ }
3548
3936
  }
3549
- function ClaudeTasksPanel({ useClaudeProjection, t, closeDetails, turn }) {
3550
- const projection = useClaudeProjection((value) => value);
3551
- const tasks = useMemo(() => tasksForTurn(projection.tasks?.tasks ?? [], turn), [projection.tasks, turn]);
3552
- useEffect(() => {
3553
- if (!projection.owned || tasks.length === 0) closeDetails();
3554
- }, [
3555
- closeDetails,
3556
- projection.owned,
3557
- tasks.length
3558
- ]);
3559
- const [finishedCollapsed, setFinishedCollapsed] = useState(false);
3560
- const [dismissedSettledIds, setDismissedSettledIds] = useState(() => /* @__PURE__ */ new Set());
3561
- const groups = useMemo(() => visibleTaskGroups(tasks, dismissedSettledIds), [tasks, dismissedSettledIds]);
3562
- const taskActivities = useMemo(() => new Map(tasks.map((task) => [task.taskId, activitiesForTask(projection.activities, task.taskId)])), [projection.activities, tasks]);
3563
- const clearFinished = () => setDismissedSettledIds((previous) => /* @__PURE__ */ new Set([...previous, ...tasks.filter((task) => task.status !== "running").map((task) => task.taskId)]));
3564
- if (!projection.owned) return null;
3565
- return /* @__PURE__ */ jsxs("div", {
3566
- className: detailsCardClass,
3567
- style: tasksPanel,
3568
- children: [
3569
- /* @__PURE__ */ jsxs("style", {
3570
- "data-dsh-claude-panel-icon-styles": true,
3571
- children: [detailsCardCss, panelIconButtonCss]
3572
- }),
3573
- /* @__PURE__ */ jsxs("div", {
3574
- style: tasksHeader,
3575
- children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
3576
- style: tasksHeading,
3577
- children: t("tasksPanelTurn")
3578
- }), /* @__PURE__ */ jsx("span", {
3579
- style: tasksTurnMeta,
3580
- children: t("tasksTurnNumber", { turn })
3581
- })] }), /* @__PURE__ */ jsx("button", {
3582
- type: "button",
3583
- className: panelIconButtonClass,
3584
- "aria-label": t("tasksClose"),
3585
- onClick: closeDetails,
3586
- children: /* @__PURE__ */ jsx(IconCloseOutline16, {})
3587
- })]
3588
- }),
3589
- /* @__PURE__ */ jsxs("div", {
3590
- style: tasksBody,
3591
- children: [/* @__PURE__ */ jsxs("section", {
3592
- "aria-label": t("tasksRunning"),
3593
- children: [/* @__PURE__ */ jsx(GroupHeading, {
3594
- label: t("tasksRunning"),
3595
- count: groups.running.length
3596
- }), groups.running.length === 0 ? /* @__PURE__ */ jsx("p", {
3597
- style: tasksGroupEmpty,
3598
- children: t("tasksNoneRunning")
3599
- }) : /* @__PURE__ */ jsx("div", {
3600
- style: taskCardList,
3601
- children: groups.running.map((task) => /* @__PURE__ */ jsx(TaskCard, {
3602
- task,
3603
- activities: taskActivities.get(task.taskId) ?? [],
3604
- t
3605
- }, task.taskId))
3606
- })]
3607
- }), /* @__PURE__ */ jsxs("section", {
3608
- "aria-label": t("tasksSettled"),
3609
- style: tasksFinishedSection,
3610
- children: [/* @__PURE__ */ jsx(GroupHeading, {
3611
- label: t("tasksSettled"),
3612
- count: groups.finished.length,
3613
- collapsed: finishedCollapsed,
3614
- onToggle: () => setFinishedCollapsed((value) => !value),
3615
- ...groups.finished.length === 0 ? {} : { action: {
3616
- label: t("tasksClear"),
3617
- onClick: clearFinished
3618
- } }
3619
- }), finishedCollapsed || groups.finished.length === 0 ? null : /* @__PURE__ */ jsx("div", {
3620
- style: taskCardList,
3621
- children: groups.finished.map((task) => /* @__PURE__ */ jsx(TaskCard, {
3622
- task,
3623
- activities: taskActivities.get(task.taskId) ?? [],
3624
- t
3625
- }, task.taskId))
3626
- })]
3627
- })]
3628
- })
3629
- ]
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()}`
3630
3948
  });
3631
3949
  }
3632
- //#endregion
3633
- //#region src/client/ClaudeActivityTail.tsx
3634
- const MAX_HOVER_TASKS = 6;
3635
- function taskGlyph(status) {
3636
- if (status === "failed") return {
3637
- glyph: "×",
3638
- style: tasksHoverGlyphError
3639
- };
3640
- if (status === "completed") return {
3641
- glyph: "✓",
3642
- style: tasksHoverGlyphDone
3643
- };
3644
- return {
3645
- glyph: "●",
3646
- style: tasksHoverGlyphRunning
3647
- };
3950
+ let writeId = 0;
3951
+ function nextWriteId() {
3952
+ writeId += 1;
3953
+ return writeId;
3648
3954
  }
3649
- function ClaudeTaskLauncher({ turn, tasks, t, openTasks }) {
3650
- const [hovered, setHovered] = useState(false);
3651
- const closeTimer = useRef();
3652
- const turnTasks = useMemo(() => tasksForTurn(tasks, turn), [tasks, turn]);
3653
- const summary = useMemo(() => summarizeTurnTasks(turnTasks), [turnTasks]);
3654
- const open = () => {
3655
- if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
3656
- closeTimer.current = void 0;
3657
- setHovered(true);
3658
- };
3659
- const scheduleClose = () => {
3660
- if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
3661
- closeTimer.current = setTimeout(() => {
3662
- closeTimer.current = void 0;
3663
- setHovered(false);
3664
- }, 350);
3665
- };
3666
- useEffect(() => () => {
3667
- if (closeTimer.current !== void 0) clearTimeout(closeTimer.current);
3668
- }, []);
3669
- if (summary === void 0) return null;
3670
- const label = summary.state === "running" ? t("tasksTurnRunning", { count: summary.running }) : summary.state === "failed" ? t("tasksTurnFailed", {
3671
- failed: summary.failed,
3672
- completed: summary.completed
3673
- }) : t("tasksTurnCompleted", { count: summary.completed });
3674
- const stateStyle = summary.state === "completed" ? tasksBadgeDone : {};
3675
- const dotStyle = summary.state === "failed" ? tasksBadgeDotError : summary.state === "completed" ? tasksBadgeDotDone : {};
3676
- return /* @__PURE__ */ jsx("div", {
3677
- "data-claude-task-launcher": turn,
3678
- style: tasksBadgeWrap,
3679
- children: /* @__PURE__ */ jsxs("span", {
3680
- style: tasksBadgeSeat,
3681
- onMouseEnter: open,
3682
- onMouseLeave: scheduleClose,
3683
- onFocus: open,
3684
- onBlur: (event) => {
3685
- if (!event.currentTarget.contains(event.relatedTarget)) scheduleClose();
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"
3686
3968
  },
3687
- children: [hovered ? /* @__PURE__ */ jsxs("span", {
3688
- role: "tooltip",
3689
- style: tasksHoverCard,
3690
- onMouseEnter: open,
3691
- onMouseLeave: scheduleClose,
3692
- children: [
3693
- /* @__PURE__ */ jsx("span", {
3694
- style: tasksHoverHeader,
3695
- children: label
3696
- }),
3697
- turnTasks.slice(0, MAX_HOVER_TASKS).map((task) => {
3698
- const { glyph, style } = taskGlyph(task.status);
3699
- return /* @__PURE__ */ jsxs("span", {
3700
- style: tasksHoverRow,
3701
- children: [
3702
- /* @__PURE__ */ jsx("span", {
3703
- className: task.status === "running" ? "dsh-claude-act-running" : void 0,
3704
- style: {
3705
- ...tasksHoverGlyph,
3706
- ...style
3707
- },
3708
- "aria-hidden": "true",
3709
- children: glyph
3710
- }),
3711
- /* @__PURE__ */ jsx("span", {
3712
- style: tasksHoverDesc,
3713
- title: task.description,
3714
- children: task.description
3715
- }),
3716
- task.subagentType === void 0 ? null : /* @__PURE__ */ jsx("span", {
3717
- style: tasksHoverType,
3718
- children: task.subagentType
3719
- })
3720
- ]
3721
- }, task.taskId);
3722
- }),
3723
- turnTasks.length > MAX_HOVER_TASKS ? /* @__PURE__ */ jsxs("span", {
3724
- style: tasksHoverMore,
3725
- children: ["+", turnTasks.length - MAX_HOVER_TASKS]
3726
- }) : null,
3727
- /* @__PURE__ */ jsx("span", {
3728
- style: tasksHoverHint,
3729
- children: t("tasksOpen")
3730
- })
3731
- ]
3732
- }) : null, /* @__PURE__ */ jsxs("button", {
3733
- type: "button",
3734
- className: "dsh-claude-task-launcher",
3735
- style: {
3736
- ...tasksTurnBadge,
3737
- ...stateStyle,
3738
- ...hovered ? tasksBadgeHovered : {}
3739
- },
3740
- "aria-label": `${label} — ${t("tasksOpen")}`,
3741
- onClick: () => openTasks(turn),
3742
- children: [/* @__PURE__ */ jsx("span", {
3743
- className: summary.state === "running" ? "dsh-claude-act-running" : void 0,
3744
- style: {
3745
- ...tasksBadgeDot,
3746
- ...dotStyle
3747
- },
3748
- "aria-hidden": "true"
3749
- }), /* @__PURE__ */ jsx("span", {
3750
- style: tasksBadgeLabel,
3751
- children: label
3752
- })]
3753
- })]
3754
- })
3755
- });
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
+ }
3756
3982
  }
3757
- function ClaudeActivityTail({ matched, useClaudeProjection, t, openTasks }) {
3758
- const tasks = useClaudeProjection((value) => value.tasks?.tasks ?? []);
3759
- return /* @__PURE__ */ jsx(ClaudeTaskLauncher, {
3760
- turn: matched.turn,
3761
- tasks,
3762
- t,
3763
- openTasks
3764
- });
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);
3765
3988
  }
3766
- //#endregion
3767
- //#region src/client/ClaudeActiveTasksNode.tsx
3768
- const EMPTY_TASKS$1 = [];
3769
- /** Render the task launcher while the owning DSH turn is still open. */
3770
- function ClaudeActiveTasksNode({ node, useClaudeProjection, t, openTasks }) {
3771
- const tasks = useClaudeProjection((value) => value.tasks?.tasks ?? EMPTY_TASKS$1);
3772
- return /* @__PURE__ */ jsx(ClaudeTaskLauncher, {
3773
- turn: node.data.turn,
3774
- tasks,
3775
- t,
3776
- openTasks
3777
- });
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);
3778
4002
  }
3779
4003
  //#endregion
3780
- //#region src/client/markdown-theme.ts
3781
- /** Claude Code's code presentation over the Host's Markdown renderer.
3782
- *
3783
- * This package renders no Markdown of its own — prose, fenced blocks and the
3784
- * copy affordance all come from the Host's `MarkdownText` primitive. What it
3785
- * can own is what that primitive reads: the palette (custom properties) and
3786
- * the block's chrome (CSS over the primitive's own markup).
3787
- *
3788
- * PARITY IS PARTIAL BY CONSTRUCTION. Both renderers highlight with shiki, but
3789
- * Claude Code's desktop build loads a full TextMate theme (Pierre Dark /
3790
- * Pierre Light Soft: 248 tokenColor rules over 424 scopes) and bakes the
3791
- * resolved colour into every span, while the Host loads shiki's legacy
3792
- * `css-variables` theme, which collapses every scope into the eleven buckets
3793
- * below. Those eleven carry the colours that dominate a code block —
3794
- * keywords, strings, comments, functions, numbers — and nothing here can
3795
- * recover the rest: `constant.numeric` and `constant` are two different
3796
- * colours in Pierre and one bucket here, and Pierre's string-coloured string
3797
- * delimiters share this sheet's single punctuation bucket. Matching the rest
3798
- * means this package running its own shiki, which is a different decision
3799
- * with a different cost.
3800
- *
3801
- * Every rule fails open the way `host-chrome` does: the palette is
3802
- * declarations on this package's own wrapper, and the chrome rules match the
3803
- * primitive's CSS Module local names, so a Host that renames either simply
3804
- * stops matching and its stock presentation comes back.
3805
- */
3806
- /** Wrapper class carrying the palette and scoping the chrome rules.
3807
- * `display:contents` so the extra element generates no box — custom
3808
- * properties inherit through it regardless. */
3809
- const CLAUDE_MARKDOWN_SCOPE = "dsh-claude-markdown";
3810
- /** Pierre mapped onto the Host's eleven buckets. Each entry names the theme
3811
- * scope the value was taken from, because the mapping — not the colour — is
3812
- * the part a reader has to check. */
3813
- const PIERRE_DARK = [
3814
- [
3815
- "--shiki-background",
3816
- "#1a1a19",
3817
- "UI surface (--cds-surface-2)"
3818
- ],
3819
- [
3820
- "--shiki-foreground",
3821
- "#fafafa",
3822
- "editor.foreground"
3823
- ],
3824
- [
3825
- "--shiki-token-comment",
3826
- "#737373",
3827
- "comment"
3828
- ],
3829
- [
3830
- "--shiki-token-keyword",
3831
- "#ff678d",
3832
- "keyword, storage.type"
3833
- ],
3834
- [
3835
- "--shiki-token-string",
3836
- "#5ecc71",
3837
- "string"
3838
- ],
3839
- [
3840
- "--shiki-token-string-expression",
3841
- "#ffa359",
3842
- "punctuation.section.embedded"
3843
- ],
3844
- [
3845
- "--shiki-token-function",
3846
- "#9d6afb",
3847
- "entity.name.function"
3848
- ],
3849
- [
3850
- "--shiki-token-constant",
3851
- "#68cdf2",
3852
- "constant.numeric, constant.language"
3853
- ],
3854
- [
3855
- "--shiki-token-parameter",
3856
- "#a3a3a3",
3857
- "variable.parameter"
3858
- ],
3859
- [
3860
- "--shiki-token-punctuation",
3861
- "#636363",
3862
- "punctuation"
3863
- ],
3864
- [
3865
- "--shiki-token-link",
3866
- "#ff678d",
3867
- "markup.underline.link.markdown"
3868
- ]
3869
- ];
3870
- const PIERRE_LIGHT = [
3871
- [
3872
- "--shiki-background",
3873
- "#ffffff",
3874
- "UI surface (--cds-surface-2)"
3875
- ],
3876
- [
3877
- "--shiki-foreground",
3878
- "#525252",
3879
- "editor.foreground"
3880
- ],
3881
- [
3882
- "--shiki-token-comment",
3883
- "#8a8a8a",
3884
- "comment"
3885
- ],
3886
- [
3887
- "--shiki-token-keyword",
3888
- "#ff678d",
3889
- "keyword, storage.type"
3890
- ],
3891
- [
3892
- "--shiki-token-string",
3893
- "#0dbe4e",
3894
- "string"
3895
- ],
3896
- [
3897
- "--shiki-token-string-expression",
3898
- "#fe8c2c",
3899
- "punctuation.section.embedded"
3900
- ],
3901
- [
3902
- "--shiki-token-function",
3903
- "#9d6afb",
3904
- "entity.name.function"
3905
- ],
3906
- [
3907
- "--shiki-token-constant",
3908
- "#08c0ef",
3909
- "constant.numeric, constant.language"
3910
- ],
3911
- [
3912
- "--shiki-token-parameter",
3913
- "#737373",
3914
- "variable.parameter"
3915
- ],
3916
- [
3917
- "--shiki-token-punctuation",
3918
- "#737373",
3919
- "punctuation"
3920
- ],
3921
- [
3922
- "--shiki-token-link",
3923
- "#ff678d",
3924
- "markup.underline.link.markdown"
3925
- ]
3926
- ];
3927
- /** Claude's brand clay (`--cds-hsl-clay`), its inline-code TEXT colour; the
3928
- * emphasized ramp is the light-theme variant. The chip's fill is deliberately
3929
- * NOT tinted with it — see {@link INLINE_FILL_DARK}. */
3930
- const CLAY = "#d97757";
3931
- const CLAY_EMPHASIZED = "#c8603f";
3932
- /** The inline-code chip fill: a neutral 4% wash (Claude's `--t1`), not a tint
3933
- * of the text colour. A clay-tinted fill reads as a coloured box around every
3934
- * identifier; the neutral one disappears into the surface and lets the text
3935
- * carry the accent, which is what the chip is for. */
3936
- const INLINE_FILL_DARK = "hsl(0 0% 100% / .04)";
3937
- const INLINE_FILL_LIGHT = "hsl(0 0% 4.3% / .04)";
3938
- /** Block corner radius (Claude's `--r6`), against the Host's own 12px. */
3939
- const BLOCK_RADIUS = "8px";
3940
- /** @param banner - equal to `surface` on purpose: the bar is floated out of
3941
- * the way below, so this colour is only reached if those chrome selectors
3942
- * miss, and a bar that matches the block is the neutral fallback. */
3943
- function palette(entries, surface, banner, inlineFill) {
3944
- return [
3945
- ...entries.map(([name, value]) => `${name}:${value};`),
3946
- `--dsw-alias-markdown-code-block:${surface};`,
3947
- `--dsw-alias-markdown-code-block-banner:${banner};`,
3948
- `--dsw-alias-markdown-inline-code:${inlineFill}`
3949
- ].join("");
4004
+ //#region src/client/projection.ts
4005
+ const EMPTY_CLAUDE_PROJECTION = {
4006
+ schemaVersion: 1,
4007
+ revision: 0,
4008
+ owned: false,
4009
+ commands: [],
4010
+ activities: []
4011
+ };
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);
4020
+ /** Coalesce stream deltas into at most one React notification per frame. */
4021
+ const FRAME_MS = 16;
4022
+ /** Typewriter smoothing: drain newly arrived prose over roughly this window,
4023
+ * so the CLI's paragraph-sized deltas read as a continuous character flow. */
4024
+ const REVEAL_WINDOW_MS = 1200;
4025
+ /** A burst larger than this (redaction rewrite, reconnect catch-up) shows
4026
+ * instantly instead of animating for a long stretch. */
4027
+ const MAX_INSTANT_REVEAL = 4e3;
4028
+ const MAX_ACTIVITIES = 1e4;
4029
+ const MAX_COMMANDS = 2e3;
4030
+ const MAX_REPOSITORY_TEXT_CHARS = 1024;
4031
+ const MAX_DIFF_CHARS = 262144;
4032
+ const MAX_REVIEW_COMMENTS = 50;
4033
+ const MAX_REVIEW_COMMENT_CHARS = 2e3;
4034
+ const MAX_TRANSCRIPT_CHARS = 64e3;
4035
+ function record$7(value) {
4036
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
3950
4037
  }
3951
- const CLAUDE_MARKDOWN_THEME_CSS = [
3952
- `.${CLAUDE_MARKDOWN_SCOPE}{display:contents;`,
3953
- palette(PIERRE_LIGHT, "#ffffff", "#ffffff", INLINE_FILL_LIGHT),
3954
- "}",
3955
- `body[data-ds-dark-theme] .${CLAUDE_MARKDOWN_SCOPE}{`,
3956
- palette(PIERRE_DARK, "#1a1a19", "#1a1a19", INLINE_FILL_DARK),
3957
- "}",
3958
- `.${CLAUDE_MARKDOWN_SCOPE} :not(pre)>code{color:${CLAY_EMPHASIZED};border-radius:4px;padding:1px 2px}`,
3959
- `body[data-ds-dark-theme] .${CLAUDE_MARKDOWN_SCOPE} :not(pre)>code{color:${CLAY}}`,
3960
- `.${CLAUDE_MARKDOWN_SCOPE} [class*="bannerWrap"]{position:absolute;top:0;right:0;z-index:7;background:transparent;border-radius:0}`,
3961
- `.${CLAUDE_MARKDOWN_SCOPE} [class*="banner"]:not([class*="bannerWrap"]){background:transparent;padding:6px 8px}`,
3962
- `.${CLAUDE_MARKDOWN_SCOPE} [class*="infostring"]{display:none}`,
3963
- `.${CLAUDE_MARKDOWN_SCOPE} pre{white-space:pre;word-break:normal;overflow-x:auto}`,
3964
- `.${CLAUDE_MARKDOWN_SCOPE} [class*="block"]{--dsl-code-block-border-radius:${BLOCK_RADIUS}}`
3965
- ].join("");
3966
- let injected = false;
3967
- /** Attach the sheet once per page. */
3968
- function ensureClaudeMarkdownTheme() {
3969
- if (injected || typeof document === "undefined") return;
3970
- injected = true;
3971
- const element = document.createElement("style");
3972
- element.dataset.dshClaudeMarkdownTheme = "";
3973
- element.textContent = CLAUDE_MARKDOWN_THEME_CSS;
3974
- document.head.appendChild(element);
4038
+ function nonNegativeInteger(value) {
4039
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
3975
4040
  }
3976
- //#endregion
3977
- //#region src/client/markdown-labels.tsx
3978
- /** Stable across renders: MarkdownText keys its streaming renderer on this
3979
- * object's identity and reparses from scratch whenever it changes. */
3980
- function useClaudeMarkdownLabels(t) {
3981
- return useMemo(() => ({
3982
- code: {
3983
- copyLabel: t("markdownCopy"),
3984
- copiedLabel: t("markdownCopied")
3985
- },
3986
- footnotes: t("markdownFootnotes")
3987
- }), [t]);
4041
+ function optionalBoundedString(value) {
4042
+ return value === void 0 || typeof value === "string" && value.length <= MAX_REPOSITORY_TEXT_CHARS;
3988
4043
  }
3989
- /** MarkdownText with the labels the running Host demands.
3990
- *
3991
- * The published primitives package still types the old `codeLabels` prop, so
3992
- * the new shape cannot typecheck against it — the cast is confined here rather
3993
- * than repeated at every call site. Drop it once the installed
3994
- * @deepseek-ai/dsh-client-ui-primitives matches the Desktop build. */
3995
- function ClaudeMarkdown({ text, labels, streaming }) {
3996
- const props = {
3997
- text,
3998
- labels,
3999
- ...streaming === void 0 ? {} : { streaming }
4000
- };
4001
- const Renderer = MarkdownText;
4002
- ensureClaudeMarkdownTheme();
4003
- return /* @__PURE__ */ jsx("div", {
4004
- className: CLAUDE_MARKDOWN_SCOPE,
4005
- children: /* @__PURE__ */ jsx(Renderer, { ...props })
4044
+ function validateRepository(value) {
4045
+ const repository = record$7(value);
4046
+ if (repository === void 0 || ![
4047
+ "ready",
4048
+ "not-repository",
4049
+ "unavailable"
4050
+ ].includes(String(repository.status)) || typeof repository.cwd !== "string" || repository.cwd.length > MAX_REPOSITORY_TEXT_CHARS || !optionalBoundedString(repository.root) || !optionalBoundedString(repository.branch) || !optionalBoundedString(repository.remote) || repository.detached !== void 0 && typeof repository.detached !== "boolean" || repository.worktree !== void 0 && typeof repository.worktree !== "boolean" || repository.dirty !== void 0 && typeof repository.dirty !== "boolean" || repository.upstream !== void 0 && typeof repository.upstream !== "boolean" || repository.ahead !== void 0 && !nonNegativeInteger(repository.ahead) || repository.behind !== void 0 && !nonNegativeInteger(repository.behind)) return false;
4051
+ if (repository.diff !== void 0) {
4052
+ const diff = record$7(repository.diff);
4053
+ if (diff === void 0 || !nonNegativeInteger(diff.additions) || !nonNegativeInteger(diff.deletions) || !nonNegativeInteger(diff.files) || typeof diff.truncated !== "boolean" || diff.patch !== void 0 && (typeof diff.patch !== "string" || diff.patch.length > MAX_DIFF_CHARS)) return false;
4054
+ }
4055
+ if (repository.pullRequest === void 0) return true;
4056
+ const pullRequest = record$7(repository.pullRequest);
4057
+ if (pullRequest === void 0 || !Number.isSafeInteger(pullRequest.number) || Number(pullRequest.number) <= 0 || typeof pullRequest.title !== "string" || pullRequest.title.length > MAX_REPOSITORY_TEXT_CHARS || typeof pullRequest.url !== "string" || pullRequest.url.length > MAX_REPOSITORY_TEXT_CHARS || ![
4058
+ "open",
4059
+ "closed",
4060
+ "merged"
4061
+ ].includes(String(pullRequest.state)) || typeof pullRequest.draft !== "boolean" || ![
4062
+ "approved",
4063
+ "changes-requested",
4064
+ "review-required",
4065
+ "none"
4066
+ ].includes(String(pullRequest.review)) || ![
4067
+ "passing",
4068
+ "pending",
4069
+ "failing",
4070
+ "none"
4071
+ ].includes(String(pullRequest.checks)) || !optionalBoundedString(pullRequest.mergeState) || !optionalBoundedString(pullRequest.author) || !optionalBoundedString(pullRequest.baseBranch) || pullRequest.createdAt !== void 0 && (typeof pullRequest.createdAt !== "string" || !Number.isFinite(Date.parse(pullRequest.createdAt))) || pullRequest.mergedAt !== void 0 && (typeof pullRequest.mergedAt !== "string" || !Number.isFinite(Date.parse(pullRequest.mergedAt)))) return false;
4072
+ try {
4073
+ const url = new URL(pullRequest.url);
4074
+ return url.protocol === "https:" && url.hostname === "github.com";
4075
+ } catch {
4076
+ return false;
4077
+ }
4078
+ }
4079
+ /** Validate the public route envelope before publishing it to UI components. */
4080
+ function parseClaudeClientProjection(value) {
4081
+ const input = record$7(value);
4082
+ if (input === void 0 || input.schemaVersion !== 1 || !nonNegativeInteger(input.revision) || typeof input.owned !== "boolean" || !Array.isArray(input.commands) || input.commands.length > MAX_COMMANDS || !Array.isArray(input.activities) || input.activities.length > MAX_ACTIVITIES) throw new Error("invalid Claude sidecar projection");
4083
+ for (const item of input.commands) {
4084
+ const command = record$7(item);
4085
+ if (command === void 0 || typeof command.publicName !== "string" || typeof command.claudeName !== "string" || typeof command.description !== "string" || command.hint !== void 0 && typeof command.hint !== "string" || typeof command.prefixed !== "boolean") throw new Error("invalid Claude command projection");
4086
+ }
4087
+ for (const item of input.activities) {
4088
+ const activity = record$7(item);
4089
+ if (activity === void 0 || !nonNegativeInteger(activity.turn) || !nonNegativeInteger(activity.step) || !nonNegativeInteger(activity.ordinal) || typeof activity.kind !== "string") throw new Error("invalid Claude sidecar activity");
4090
+ }
4091
+ if (input.contextUsage !== void 0 && record$7(input.contextUsage) === void 0) throw new Error("invalid Claude context projection");
4092
+ const tasks = input.tasks === void 0 ? void 0 : record$7(input.tasks);
4093
+ if (tasks !== void 0 && !Array.isArray(tasks.tasks)) throw new Error("invalid Claude tasks projection");
4094
+ if (input.repository !== void 0 && !validateRepository(input.repository)) throw new Error("invalid Claude repository projection");
4095
+ if (input.reviewComments !== void 0) {
4096
+ if (!Array.isArray(input.reviewComments) || input.reviewComments.length > MAX_REVIEW_COMMENTS) throw new Error("invalid Claude review comment projection");
4097
+ for (const item of input.reviewComments) {
4098
+ const comment = record$7(item);
4099
+ if (comment === void 0 || typeof comment.id !== "string" || comment.id.length === 0 || comment.id.length > 128 || typeof comment.path !== "string" || comment.path.length === 0 || comment.path.length > MAX_REPOSITORY_TEXT_CHARS || !nonNegativeInteger(comment.line) || comment.side !== "old" && comment.side !== "new" || typeof comment.text !== "string" || comment.text.length > MAX_REVIEW_COMMENT_CHARS) throw new Error("invalid Claude review comment projection");
4100
+ }
4101
+ }
4102
+ if (input.rewind !== void 0) {
4103
+ const ranges = record$7(input.rewind)?.ranges;
4104
+ if (!Array.isArray(ranges) || ranges.length > 200) throw new Error("invalid Claude rewind projection");
4105
+ for (const item of ranges) {
4106
+ const range = record$7(item);
4107
+ if (range === void 0 || !nonNegativeInteger(range.start) || !nonNegativeInteger(range.end)) throw new Error("invalid Claude rewind projection");
4108
+ }
4109
+ }
4110
+ return input;
4111
+ }
4112
+ /** Validate one incremental delta payload with the same rules as a snapshot. */
4113
+ function validateEnvelopeFragment(fragment) {
4114
+ parseClaudeClientProjection({
4115
+ schemaVersion: 1,
4116
+ revision: 0,
4117
+ owned: false,
4118
+ commands: [],
4119
+ activities: [],
4120
+ ...fragment
4006
4121
  });
4007
4122
  }
4008
- //#endregion
4009
- //#region src/plugin-budget.ts
4010
- /**
4011
- * The plugin's connection budget, in one table.
4012
- *
4013
- * A browser opens a small fixed number of connections to one origin six for
4014
- * HTTP/1.1 in Chromium — and shares them with the Host's own traffic. Every
4015
- * response this plugin holds open costs one of them for its lifetime, and a
4016
- * request that cannot get one waits in the browser's queue where no server-side
4017
- * deadline can reach it. When the pool is exhausted the panels that would
4018
- * *diagnose* the problem are the first thing to stop answering, which is how
4019
- * this failure has always presented: four settings cards timing out at once
4020
- * against a Host that is demonstrably healthy.
4021
- *
4022
- * So the budget is a fixed constant rather than a function of how much work is
4023
- * in flight. Steady state is one connection (the multiplexed projection
4024
- * carrier); the peak is `PLUGIN_GLOBAL_PERMITS`, whatever the session count.
4025
- *
4026
- * Both halves read this file, which is the point: a route declares a budget
4027
- * class and the client derives its wait from the same entry, so a server
4028
- * deadline can never be quietly longer than the client's patience.
4029
- */
4030
- /** Server-side budget classes. A route declares a class, never a number. */
4031
- const ROUTE_BUDGET_MS = {
4032
- /** Answers from memory or a single bounded probe. */
4033
- fast: 5e3,
4034
- /** Chains local Git work. */
4035
- git: 45e3,
4036
- /** Reaches the network: remote Git, `gh`, the npm registry. */
4037
- remote: 15e4
4038
- };
4039
- /** The client waits one round trip longer, so the route's own 504 wins the
4040
- * race and the caller learns which budget elapsed instead of guessing. */
4041
- const CLIENT_GRACE_MS = 3e3;
4042
- function clientBudgetMs(budget) {
4043
- return ROUTE_BUDGET_MS[budget] + CLIENT_GRACE_MS;
4044
- }
4045
- /** A request that never got a permit fails fast and says so, rather than
4046
- * spending its whole budget queued behind work it cannot see. */
4047
- const QUEUE_WAIT_BUDGET_MS = 4e3;
4048
- /** Per-lane ceilings inside the global budget.
4049
- *
4050
- * The projection carrier holds a permanently reserved permit outside these,
4051
- * so three remain. `write + stream <= 2` leaves one permit that only a read
4052
- * can take: a diagnostic read always has somewhere to go, however many slow
4053
- * actions are in flight. That invariant is what stops a 150s repository
4054
- * action from reproducing the original symptom through a new mechanism. */
4055
- const PLUGIN_LANE_CAPS = {
4056
- read: 2,
4057
- write: 1,
4058
- stream: 1
4059
- };
4060
- //#endregion
4061
- //#region src/rewind.ts
4062
- function isRewound(ranges, seq) {
4063
- return ranges.some((range) => seq >= range.start && seq <= range.end);
4064
- }
4065
- //#endregion
4066
- //#region src/client/plugin-transport.ts
4067
- /**
4068
- * The plugin's only door to the network.
4069
- *
4070
- * Every request the Client makes goes through here, and the shape of the
4071
- * public functions is the point. There is no `signal` property, no `headers`,
4072
- * no `RequestInit` and no millisecond number in any parameter: a caller cannot
4073
- * hand this module an options object whose later spread silently overwrites
4074
- * the deadline sitting above it. That is not a hypothetical — it is exactly
4075
- * how four call sites lost their deadlines while looking completely ordinary,
4076
- * because `exactOptionalPropertyTypes` forbids writing `signal: undefined` and
4077
- * the spread idiom is what people reach for instead. `cancel` is positional
4078
- * here, so `undefined` is simply passable and the idiom has nothing to do.
4079
- *
4080
- * The second seal is arithmetic. The browser shares a small fixed connection
4081
- * budget between this plugin and the Host, and the plugin used to spend it
4082
- * proportionally to how many Claude sessions existed. Here every request takes
4083
- * a permit from a fixed pool first, so more call sites and more sessions
4084
- * cannot become more sockets — a saturated plugin queues, and says `starved`
4085
- * within `QUEUE_WAIT_BUDGET_MS` instead of dying silently at its full budget.
4086
- *
4087
- * Lane caps guarantee a read always has somewhere to go: the projection
4088
- * carrier holds a reserved permit, and `write + stream` can occupy at most two
4089
- * of the remaining three. The panel that diagnoses a saturated pool is
4090
- * therefore the one request that cannot be starved by it.
4091
- */
4092
- var PluginRequestError = class extends Error {
4093
- reason;
4094
- status;
4095
- code;
4096
- constructor(reason, message, status, code) {
4097
- super(message);
4098
- this.name = "PluginRequestError";
4099
- this.reason = reason;
4100
- if (status !== void 0) this.status = status;
4101
- if (code !== void 0) this.code = code;
4102
- }
4103
- };
4104
- let send = (...args) => fetch(...args);
4105
- let held = 0;
4106
- const laneHeld = {
4107
- read: 0,
4108
- write: 0,
4109
- stream: 0
4110
- };
4111
- let projectionHeld = false;
4112
- let queue = [];
4113
- const inFlight = /* @__PURE__ */ new Map();
4114
- /** The projection carrier owns a permit of its own, outside the lane caps, so
4115
- * the transcript stream and the panels never compete for the same slot. */
4116
- function laneHasRoom(lane) {
4117
- return held < 4 && laneHeld[lane] < PLUGIN_LANE_CAPS[lane];
4118
- }
4119
- function pump() {
4120
- for (let index = 0; index < queue.length; index += 1) {
4121
- const waiter = queue[index];
4122
- if (waiter === void 0 || !laneHasRoom(waiter.lane)) continue;
4123
- queue.splice(index, 1);
4124
- index -= 1;
4125
- clearTimeout(waiter.timer);
4126
- held += 1;
4127
- laneHeld[waiter.lane] += 1;
4128
- waiter.admit();
4129
- }
4130
- }
4131
- function release(lane) {
4132
- held -= 1;
4133
- laneHeld[lane] -= 1;
4134
- pump();
4135
- }
4136
- function acquire(lane) {
4137
- let released = false;
4138
- const releaseOnce = () => {
4139
- if (released) return;
4140
- released = true;
4141
- release(lane);
4142
- };
4143
- if (laneHasRoom(lane)) {
4144
- held += 1;
4145
- laneHeld[lane] += 1;
4146
- return Promise.resolve(releaseOnce);
4147
- }
4148
- return new Promise((resolve, reject) => {
4149
- const waiter = {
4150
- lane,
4151
- admit: () => resolve(releaseOnce),
4152
- reject,
4153
- timer: setTimeout(() => {
4154
- queue = queue.filter((item) => item !== waiter);
4155
- reject(new PluginRequestError("starved", "The plugin is holding every connection it is allowed to open."));
4156
- }, QUEUE_WAIT_BUDGET_MS)
4157
- };
4158
- queue.push(waiter);
4159
- });
4160
- }
4161
- function withQuery(path, query) {
4162
- if (query === void 0) return path;
4163
- const encoded = new URLSearchParams(query).toString();
4164
- return encoded.length === 0 ? path : `${path}?${encoded}`;
4165
- }
4166
- function failureOf(error, cancel) {
4167
- if (error instanceof PluginRequestError) return error;
4168
- if (cancel?.aborted === true) return new PluginRequestError("cancelled", "The caller cancelled the request.");
4169
- const name = error instanceof Error ? error.name : "";
4170
- if (name === "TimeoutError" || name === "AbortError") return new PluginRequestError("timeout", "The plugin route did not answer inside its budget.");
4171
- return new PluginRequestError("http", error instanceof Error ? error.message : String(error));
4172
- }
4173
- async function decode(response) {
4174
- let payload;
4175
- try {
4176
- payload = await response.json();
4177
- } catch {
4178
- if (response.ok) throw new PluginRequestError("shape", "The plugin route answered with a body this build cannot read.");
4179
- throw new PluginRequestError("http", `HTTP ${response.status}`, response.status);
4180
- }
4181
- if (response.ok) return payload;
4182
- if (response.status === 404) throw new PluginRequestError("route-missing", "The Host is running an older plugin build without this route.", 404);
4183
- const record = typeof payload === "object" && payload !== null ? payload : void 0;
4184
- const message = typeof record?.message === "string" ? record.message : typeof record?.error === "string" ? record.error : `HTTP ${response.status}`;
4185
- const code = typeof record?.error === "string" ? record.error : void 0;
4186
- throw new PluginRequestError("http", message, response.status, code);
4187
- }
4188
- async function dispatch(lane, method, path, budget, cancel, options) {
4189
- const url = withQuery(path, options?.query);
4190
- const key = options?.key ?? `${method} ${url}`;
4191
- const existing = inFlight.get(key);
4192
- if (existing !== void 0) return await existing;
4193
- const run = (async () => {
4194
- const free = await acquire(lane);
4195
- try {
4196
- const timeout = AbortSignal.timeout(clientBudgetMs(budget));
4197
- const signal = cancel === void 0 ? timeout : AbortSignal.any([cancel, timeout]);
4198
- return await decode(await send(url, {
4199
- method,
4200
- credentials: "same-origin",
4201
- signal,
4202
- headers: options?.json === void 0 ? { accept: "application/json" } : {
4203
- accept: "application/json",
4204
- "content-type": "application/json"
4205
- },
4206
- ...options?.json === void 0 ? {} : { body: JSON.stringify(options.json) }
4207
- }));
4208
- } catch (error) {
4209
- throw failureOf(error, cancel);
4210
- } finally {
4211
- free();
4212
- }
4213
- })();
4214
- inFlight.set(key, run);
4215
- try {
4216
- return await run;
4217
- } finally {
4218
- if (inFlight.get(key) === run) inFlight.delete(key);
4219
- }
4220
- }
4221
- /** A bounded read of a plugin route. */
4222
- function pluginRead(path, budget, cancel, options) {
4223
- return dispatch("read", "GET", path, budget, cancel, options);
4224
- }
4225
- /** A bounded write. Writes never coalesce by default: two of them are two
4226
- * intents, even when their bodies match. */
4227
- function pluginWrite(path, budget, cancel, options) {
4228
- const method = options?.method ?? "POST";
4229
- return dispatch("write", method, path, budget, cancel, {
4230
- ...options,
4231
- key: options?.key ?? `${method} ${withQuery(path, options?.query)} #${nextWriteId()}`
4232
- });
4233
- }
4234
- let writeId = 0;
4235
- function nextWriteId() {
4236
- writeId += 1;
4237
- return writeId;
4238
- }
4239
- async function openStream(lane, path, cancel, options, reserved) {
4240
- const url = withQuery(path, options?.query);
4241
- const free = reserved ? () => {
4242
- projectionHeld = false;
4243
- } : await acquire(lane);
4244
- try {
4245
- const response = await send(url, {
4246
- method: options?.method ?? "GET",
4247
- credentials: "same-origin",
4248
- signal: cancel,
4249
- headers: options?.json === void 0 ? { accept: "application/x-ndjson" } : {
4250
- accept: "application/x-ndjson",
4251
- "content-type": "application/json"
4252
- },
4253
- ...options?.json === void 0 ? {} : { body: JSON.stringify(options.json) }
4254
- });
4255
- if (!response.ok || response.body === null) {
4256
- free();
4257
- if (response.status === 404) throw new PluginRequestError("route-missing", "The Host is running an older plugin build without this route.", 404);
4258
- throw new PluginRequestError("http", `HTTP ${response.status}`, response.status);
4259
- }
4260
- cancel.addEventListener("abort", free, { once: true });
4261
- return response.body.getReader();
4262
- } catch (error) {
4263
- free();
4264
- throw failureOf(error, cancel);
4265
- }
4266
- }
4267
- /** A long-lived NDJSON response. No deadline — it is meant to stay open — but
4268
- * it takes a counted permit for its whole life, which is the bound that
4269
- * matters. */
4270
- function pluginNdjson(path, cancel, options) {
4271
- return openStream("stream", path, cancel, options, false);
4272
- }
4273
- /** The reserved projection carrier: exactly one live connection, ever,
4274
- * whatever the session count. */
4275
- function pluginProjectionStream(path, cancel, options) {
4276
- if (projectionHeld) return Promise.reject(new PluginRequestError("starved", "The projection carrier is already open."));
4277
- projectionHeld = true;
4278
- return openStream("stream", path, cancel, options, true);
4279
- }
4280
- /** Fire-and-forget diagnostics. Dropped rather than queued when saturated:
4281
- * the channel that reports the plugin's own failures must never be the
4282
- * traffic that causes them. */
4283
- function pluginBeacon(path, body) {
4284
- if (!laneHasRoom("write")) return;
4285
- pluginWrite(path, "fast", void 0, { json: body }).catch(() => void 0);
4286
- }
4287
- //#endregion
4288
- //#region src/client/projection.ts
4289
- const EMPTY_CLAUDE_PROJECTION = {
4290
- schemaVersion: 1,
4291
- revision: 0,
4292
- owned: false,
4293
- commands: [],
4294
- activities: []
4295
- };
4296
- const RETRY_DELAY_MS = 2e3;
4297
- /** Floor between carrier reopens forced by a desync. A carrier that is losing
4298
- * lines must not be answered with a reconnect per lost line. */
4299
- const RESYNC_COOLDOWN_MS = 5e3;
4300
- /** Wait for the subscribed set to stop moving before reopening the carrier:
4301
- * mounting a session list changes it once per row. */
4302
- const SUBSCRIPTION_SETTLE_MS = 250;
4303
- const NDJSON_SEPARATOR = String.fromCharCode(10);
4304
- /** Coalesce stream deltas into at most one React notification per frame. */
4305
- const FRAME_MS = 16;
4306
- /** Typewriter smoothing: drain newly arrived prose over roughly this window,
4307
- * so the CLI's paragraph-sized deltas read as a continuous character flow. */
4308
- const REVEAL_WINDOW_MS = 1200;
4309
- /** A burst larger than this (redaction rewrite, reconnect catch-up) shows
4310
- * instantly instead of animating for a long stretch. */
4311
- const MAX_INSTANT_REVEAL = 4e3;
4312
- const MAX_ACTIVITIES = 1e4;
4313
- const MAX_COMMANDS = 2e3;
4314
- const MAX_REPOSITORY_TEXT_CHARS = 1024;
4315
- const MAX_DIFF_CHARS = 262144;
4316
- const MAX_REVIEW_COMMENTS = 50;
4317
- const MAX_REVIEW_COMMENT_CHARS = 2e3;
4318
- const MAX_TRANSCRIPT_CHARS = 64e3;
4319
- function record$7(value) {
4320
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4321
- }
4322
- function nonNegativeInteger(value) {
4323
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
4324
- }
4325
- function optionalBoundedString(value) {
4326
- return value === void 0 || typeof value === "string" && value.length <= MAX_REPOSITORY_TEXT_CHARS;
4327
- }
4328
- function validateRepository(value) {
4329
- const repository = record$7(value);
4330
- if (repository === void 0 || ![
4331
- "ready",
4332
- "not-repository",
4333
- "unavailable"
4334
- ].includes(String(repository.status)) || typeof repository.cwd !== "string" || repository.cwd.length > MAX_REPOSITORY_TEXT_CHARS || !optionalBoundedString(repository.root) || !optionalBoundedString(repository.branch) || !optionalBoundedString(repository.remote) || repository.detached !== void 0 && typeof repository.detached !== "boolean" || repository.worktree !== void 0 && typeof repository.worktree !== "boolean" || repository.dirty !== void 0 && typeof repository.dirty !== "boolean" || repository.upstream !== void 0 && typeof repository.upstream !== "boolean" || repository.ahead !== void 0 && !nonNegativeInteger(repository.ahead) || repository.behind !== void 0 && !nonNegativeInteger(repository.behind)) return false;
4335
- if (repository.diff !== void 0) {
4336
- const diff = record$7(repository.diff);
4337
- if (diff === void 0 || !nonNegativeInteger(diff.additions) || !nonNegativeInteger(diff.deletions) || !nonNegativeInteger(diff.files) || typeof diff.truncated !== "boolean" || diff.patch !== void 0 && (typeof diff.patch !== "string" || diff.patch.length > MAX_DIFF_CHARS)) return false;
4338
- }
4339
- if (repository.pullRequest === void 0) return true;
4340
- const pullRequest = record$7(repository.pullRequest);
4341
- if (pullRequest === void 0 || !Number.isSafeInteger(pullRequest.number) || Number(pullRequest.number) <= 0 || typeof pullRequest.title !== "string" || pullRequest.title.length > MAX_REPOSITORY_TEXT_CHARS || typeof pullRequest.url !== "string" || pullRequest.url.length > MAX_REPOSITORY_TEXT_CHARS || ![
4342
- "open",
4343
- "closed",
4344
- "merged"
4345
- ].includes(String(pullRequest.state)) || typeof pullRequest.draft !== "boolean" || ![
4346
- "approved",
4347
- "changes-requested",
4348
- "review-required",
4349
- "none"
4350
- ].includes(String(pullRequest.review)) || ![
4351
- "passing",
4352
- "pending",
4353
- "failing",
4354
- "none"
4355
- ].includes(String(pullRequest.checks)) || !optionalBoundedString(pullRequest.mergeState) || !optionalBoundedString(pullRequest.author) || !optionalBoundedString(pullRequest.baseBranch) || pullRequest.createdAt !== void 0 && (typeof pullRequest.createdAt !== "string" || !Number.isFinite(Date.parse(pullRequest.createdAt))) || pullRequest.mergedAt !== void 0 && (typeof pullRequest.mergedAt !== "string" || !Number.isFinite(Date.parse(pullRequest.mergedAt)))) return false;
4356
- try {
4357
- const url = new URL(pullRequest.url);
4358
- return url.protocol === "https:" && url.hostname === "github.com";
4359
- } catch {
4360
- return false;
4361
- }
4362
- }
4363
- /** Validate the public route envelope before publishing it to UI components. */
4364
- function parseClaudeClientProjection(value) {
4365
- const input = record$7(value);
4366
- if (input === void 0 || input.schemaVersion !== 1 || !nonNegativeInteger(input.revision) || typeof input.owned !== "boolean" || !Array.isArray(input.commands) || input.commands.length > MAX_COMMANDS || !Array.isArray(input.activities) || input.activities.length > MAX_ACTIVITIES) throw new Error("invalid Claude sidecar projection");
4367
- for (const item of input.commands) {
4368
- const command = record$7(item);
4369
- if (command === void 0 || typeof command.publicName !== "string" || typeof command.claudeName !== "string" || typeof command.description !== "string" || command.hint !== void 0 && typeof command.hint !== "string" || typeof command.prefixed !== "boolean") throw new Error("invalid Claude command projection");
4370
- }
4371
- for (const item of input.activities) {
4372
- const activity = record$7(item);
4373
- if (activity === void 0 || !nonNegativeInteger(activity.turn) || !nonNegativeInteger(activity.step) || !nonNegativeInteger(activity.ordinal) || typeof activity.kind !== "string") throw new Error("invalid Claude sidecar activity");
4374
- }
4375
- if (input.contextUsage !== void 0 && record$7(input.contextUsage) === void 0) throw new Error("invalid Claude context projection");
4376
- const tasks = input.tasks === void 0 ? void 0 : record$7(input.tasks);
4377
- if (tasks !== void 0 && !Array.isArray(tasks.tasks)) throw new Error("invalid Claude tasks projection");
4378
- if (input.repository !== void 0 && !validateRepository(input.repository)) throw new Error("invalid Claude repository projection");
4379
- if (input.reviewComments !== void 0) {
4380
- if (!Array.isArray(input.reviewComments) || input.reviewComments.length > MAX_REVIEW_COMMENTS) throw new Error("invalid Claude review comment projection");
4381
- for (const item of input.reviewComments) {
4382
- const comment = record$7(item);
4383
- if (comment === void 0 || typeof comment.id !== "string" || comment.id.length === 0 || comment.id.length > 128 || typeof comment.path !== "string" || comment.path.length === 0 || comment.path.length > MAX_REPOSITORY_TEXT_CHARS || !nonNegativeInteger(comment.line) || comment.side !== "old" && comment.side !== "new" || typeof comment.text !== "string" || comment.text.length > MAX_REVIEW_COMMENT_CHARS) throw new Error("invalid Claude review comment projection");
4384
- }
4385
- }
4386
- if (input.rewind !== void 0) {
4387
- const ranges = record$7(input.rewind)?.ranges;
4388
- if (!Array.isArray(ranges) || ranges.length > 200) throw new Error("invalid Claude rewind projection");
4389
- for (const item of ranges) {
4390
- const range = record$7(item);
4391
- if (range === void 0 || !nonNegativeInteger(range.start) || !nonNegativeInteger(range.end)) throw new Error("invalid Claude rewind projection");
4392
- }
4393
- }
4394
- return input;
4395
- }
4396
- /** Validate one incremental delta payload with the same rules as a snapshot. */
4397
- function validateEnvelopeFragment(fragment) {
4398
- parseClaudeClientProjection({
4399
- schemaVersion: 1,
4400
- revision: 0,
4401
- owned: false,
4402
- commands: [],
4403
- activities: [],
4404
- ...fragment
4405
- });
4406
- }
4407
- function stepKeyOf(turn, step) {
4408
- return `${turn}:${step}`;
4409
- }
4410
- const EMPTY_ACTIVITIES = [];
4411
- /** Identity-stable per-step slice; untouched steps never re-render while
4412
- * another step streams. Falls back to filtering for snapshot-only hooks. */
4413
- function selectStepActivities(value, turn, step) {
4414
- const sliced = value.byStep?.get(stepKeyOf(turn, step));
4415
- if (sliced !== void 0) return sliced;
4416
- const filtered = value.activities.filter((activity) => activity.turn === turn && activity.step === step);
4417
- return filtered.length === 0 ? EMPTY_ACTIVITIES : filtered;
4418
- }
4419
- function isAbort(error) {
4420
- return error?.name === "AbortError";
4421
- }
4422
- function delay(ms) {
4423
- return new Promise((resolve) => {
4424
- setTimeout(resolve, ms).unref?.();
4425
- });
4426
- }
4427
- /** One session's reducer over the shared carrier's lines.
4123
+ function stepKeyOf(turn, step) {
4124
+ return `${turn}:${step}`;
4125
+ }
4126
+ const EMPTY_ACTIVITIES = [];
4127
+ /** Identity-stable per-step slice; untouched steps never re-render while
4128
+ * another step streams. Falls back to filtering for snapshot-only hooks. */
4129
+ function selectStepActivities(value, turn, step) {
4130
+ const sliced = value.byStep?.get(stepKeyOf(turn, step));
4131
+ if (sliced !== void 0) return sliced;
4132
+ const filtered = value.activities.filter((activity) => activity.turn === turn && activity.step === step);
4133
+ return filtered.length === 0 ? EMPTY_ACTIVITIES : filtered;
4134
+ }
4135
+ function isAbort(error) {
4136
+ return error?.name === "AbortError";
4137
+ }
4138
+ function delay(ms) {
4139
+ return new Promise((resolve) => {
4140
+ setTimeout(resolve, ms).unref?.();
4141
+ });
4142
+ }
4143
+ /** One session's reducer over the shared carrier's lines.
4428
4144
  *
4429
4145
  * A source opens nothing. It used to hold a stream of its own, which made the
4430
4146
  * plugin's connection count a function of how many sessions existed;
@@ -4695,586 +4411,1095 @@ window.__ModuleLoader__.load({
4695
4411
  }
4696
4412
  };
4697
4413
  }
4698
- /**
4699
- * Every session's projection over ONE connection.
4700
- *
4701
- * The plugin used to open an NDJSON stream per session, so its share of the
4702
- * browser's small per-origin connection budget grew with the number of Claude
4703
- * sessions — and the overview panel subscribes one per LISTED session, not per
4704
- * open one. Past a handful of sessions the plugin's own settings panel could no
4705
- * longer get a connection at all, which is the failure this class exists to
4706
- * make impossible: the carrier is one connection whatever the session count.
4707
- *
4708
- * `source(sessionId)` keeps its shape, so consumers are unaware of any of this.
4709
- */
4710
- var ClaudeProjectionStore = class {
4711
- #sources = /* @__PURE__ */ new Map();
4712
- /** Sessions with at least one live subscriber, newest interest last. */
4713
- #wanted = /* @__PURE__ */ new Set();
4714
- #open;
4715
- #retryDelayMs;
4716
- #settleMs;
4717
- #report;
4718
- #resyncCooldownMs;
4719
- #resyncedAt = 0;
4720
- #controller;
4721
- #settle;
4722
- #running = false;
4723
- #disposed = false;
4724
- constructor(options = {}) {
4725
- this.#open = options.open ?? ((path, cancel) => pluginProjectionStream(path, cancel));
4726
- this.#retryDelayMs = options.retryDelayMs ?? RETRY_DELAY_MS;
4727
- this.#settleMs = options.settleMs ?? SUBSCRIPTION_SETTLE_MS;
4728
- this.#report = options.report ?? (() => {});
4729
- this.#resyncCooldownMs = options.resyncCooldownMs ?? RESYNC_COOLDOWN_MS;
4730
- }
4731
- source(sessionId) {
4732
- let source = this.#sources.get(sessionId);
4733
- if (source === void 0) {
4734
- source = createClaudeProjectionSource(sessionId, (active) => {
4735
- this.#demand(sessionId, active);
4736
- }, (kind, detail) => {
4737
- this.#resync(kind, detail);
4738
- });
4739
- this.#sources.set(sessionId, source);
4740
- }
4741
- return source;
4742
- }
4743
- /** Reopen the carrier so every lane is restated from a fresh snapshot.
4744
- *
4745
- * One session noticed the hole, but the carrier is shared and a dropped
4746
- * line is a property of the carrier, so the others are suspect too --
4747
- * reopening restates all of them for the price of the one reconnect. */
4748
- #resync(kind, detail) {
4749
- if (this.#disposed) return;
4750
- this.#report(kind, detail);
4751
- const now = Date.now();
4752
- if (now - this.#resyncedAt < this.#resyncCooldownMs) return;
4753
- this.#resyncedAt = now;
4754
- this.#reopen();
4755
- }
4756
- dispose() {
4757
- this.#disposed = true;
4758
- if (this.#settle !== void 0) clearTimeout(this.#settle);
4759
- this.#settle = void 0;
4760
- this.#controller?.abort();
4761
- this.#controller = void 0;
4762
- for (const source of this.#sources.values()) source.dispose();
4763
- this.#sources.clear();
4764
- this.#wanted.clear();
4765
- }
4766
- /** Note interest and reopen the carrier once the set stops moving. Mounting
4767
- * a session list would otherwise reopen it once per row. */
4768
- #demand(sessionId, active) {
4769
- if (this.#disposed) return;
4770
- if (active) {
4771
- this.#wanted.delete(sessionId);
4772
- this.#wanted.add(sessionId);
4773
- } else if (!this.#wanted.delete(sessionId)) return;
4774
- if (this.#settle !== void 0) clearTimeout(this.#settle);
4775
- const timer = setTimeout(() => {
4776
- this.#settle = void 0;
4777
- this.#reopen();
4778
- }, this.#settleMs);
4779
- timer.unref?.();
4780
- this.#settle = timer;
4781
- }
4782
- #lanes() {
4783
- const wanted = [...this.#wanted];
4784
- return wanted.slice(Math.max(0, wanted.length - 16));
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
+ */
4426
+ var ClaudeProjectionStore = class {
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
+ }
4447
+ source(sessionId) {
4448
+ let source = this.#sources.get(sessionId);
4449
+ if (source === void 0) {
4450
+ source = createClaudeProjectionSource(sessionId, (active) => {
4451
+ this.#demand(sessionId, active);
4452
+ }, (kind, detail) => {
4453
+ this.#resync(kind, detail);
4454
+ });
4455
+ this.#sources.set(sessionId, source);
4456
+ }
4457
+ return source;
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
+ }
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;
4478
+ for (const source of this.#sources.values()) source.dispose();
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);
4564
+ }
4565
+ };
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
4574
+ //#region src/client/ClaudeActivityNode.tsx
4575
+ const EMPTY_TASKS$1 = [];
4576
+ const ACTIVITY_CSS = [
4577
+ ".dsh-claude-flow{display:flex;flex-direction:column;gap:10px}",
4578
+ ".dsh-claude-transcript-text{color:var(--dsw-alias-label-primary);font-size:15px;line-height:24px;overflow-wrap:anywhere}",
4579
+ ".dsh-claude-tool-group-native{overflow:visible}",
4580
+ ".dsh-claude-tool-group-native>.dsh-claude-flow-row{padding:0}",
4581
+ ".dsh-claude-tool-list{max-height:min(420px,calc(100vh - 320px));overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;margin-top:4px;border:1px solid var(--dsw-alias-border-l1, color-mix(in srgb, currentColor 12%, transparent));border-radius:10px;scrollbar-gutter:stable}",
4582
+ ".dsh-claude-tool-item{border-top:1px solid var(--dsw-alias-border-l1, color-mix(in srgb, currentColor 12%, transparent))}",
4583
+ ".dsh-claude-tool-item:first-child{border-top:0}",
4584
+ ".dsh-claude-tool-summary-row{display:flex;align-items:center;min-height:34px;gap:8px;padding:0 10px;cursor:pointer;list-style:none}",
4585
+ ".dsh-claude-tool-summary-row::-webkit-details-marker{display:none}",
4586
+ ".dsh-claude-tool-summary-row::after{content:\"›\";margin-left:2px;flex:none;align-self:center;color:var(--dsw-alias-label-secondary);font-size:22px;line-height:1;transform-origin:center;transition:transform .15s ease}",
4587
+ ".dsh-claude-tool-item[open]>.dsh-claude-tool-summary-row::after{transform:rotate(90deg)}",
4588
+ ".dsh-claude-tool-content{min-width:0;padding:0 10px 8px}",
4589
+ ".dsh-claude-tool-content>*{max-width:100%;box-sizing:border-box}",
4590
+ ".dsh-claude-tool-section{margin-top:8px}",
4591
+ ".dsh-claude-tool-section-title{margin-bottom:4px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;text-transform:uppercase;letter-spacing:.04em}",
4592
+ ".dsh-claude-tool-fields{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:3px 12px;font-size:13px;line-height:20px}",
4593
+ ".dsh-claude-tool-field-key{color:var(--dsw-alias-label-tertiary)}",
4594
+ ".dsh-claude-tool-field-value{min-width:0;color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere}",
4595
+ ".dsh-claude-tool-paths{display:flex;flex-direction:column;gap:2px;margin:0;padding:0;list-style:none;font:var(--dsw-font-markdown-code-block-small)}",
4596
+ ".dsh-claude-tool-path{overflow-wrap:anywhere;color:var(--dsw-alias-label-primary)}",
4597
+ ".dsh-claude-tool-code{max-height:260px;overflow:auto;margin:0;padding:8px 0;border-radius:8px;background:var(--dsw-alias-markdown-code-block);font:var(--dsw-font-markdown-code-block-small)}",
4598
+ ".dsh-claude-tool-code-line{display:grid;grid-template-columns:44px minmax(max-content,1fr);min-height:18px}",
4599
+ ".dsh-claude-tool-line-number{padding-right:10px;text-align:right;user-select:none;color:var(--dsw-alias-label-caption);border-right:1px solid var(--dsw-alias-border-l1, color-mix(in srgb, currentColor 12%, transparent))}",
4600
+ ".dsh-claude-tool-line-text{padding:0 10px;white-space:pre}",
4601
+ ".dsh-claude-tool-label{width:72px;flex:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary)}",
4602
+ ".dsh-claude-tool-description{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;line-height:20px;color:var(--dsw-alias-label-primary)}",
4603
+ ".dsh-claude-tool-stats{display:inline-flex;gap:4px;flex:none;font-size:13px;line-height:20px}",
4604
+ ".dsh-claude-diff-add{color:var(--dsw-alias-state-success-primary,#21c55d)}",
4605
+ ".dsh-claude-diff-delete{color:var(--dsw-alias-state-error-primary)}",
4606
+ ".dsh-claude-tool-name{font-size:14px;line-height:22px;color:var(--dsw-alias-label-primary)}",
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}",
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}",
4616
+ ".dsh-claude-flow-row{position:relative;overflow:hidden}",
4617
+ ".dsh-claude-flow-leading{flex-shrink:0}",
4618
+ ".dsh-claude-flow-title{font-weight:400}",
4619
+ ".dsh-claude-flow-chevron{color:var(--dsw-alias-label-secondary)}",
4620
+ ".dsh-claude-flow-separator{width:2px;height:2px;margin:0 8px;border-radius:1px;background:var(--dsw-alias-label-caption);flex:none}",
4621
+ ".dsh-claude-flow-summary{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-size:14px;line-height:24px;flex:auto}",
4622
+ ".dsh-claude-flow-summary[data-error]{color:var(--dsw-alias-state-error-primary)}",
4623
+ ".dsh-claude-flow-body{margin:4px 0 4px 22px;color:var(--dsw-alias-label-tertiary);font-size:14px;line-height:24px;white-space:pre-wrap;overflow-wrap:anywhere}",
4624
+ ".dsh-claude-flow-detail{max-height:260px;overflow:auto;margin:4px 0 4px 4px;padding:12px 16px;border:1px solid var(--dsw-alias-border-l1, color-mix(in srgb, currentColor 12%, transparent));border-radius:12px;background:var(--dsw-alias-markdown-code-block);color:var(--dsw-alias-label-primary);font:var(--dsw-font-markdown-code-block-small);white-space:pre-wrap}",
4625
+ ".dsh-claude-flow-subcalls{display:flex;flex-direction:column;gap:2px;margin:4px 0 4px 22px;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:22px}",
4626
+ ".dsh-claude-compaction{display:flex;align-items:center;gap:10px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}",
4627
+ ".dsh-claude-compaction::before,.dsh-claude-compaction::after{content:\"\";flex:1;height:1px;background:var(--dsw-alias-border-l1, color-mix(in srgb, currentColor 12%, transparent))}",
4628
+ ".dsh-claude-compaction-label{flex:none;white-space:nowrap}",
4629
+ ".dsh-claude-act-running{animation:dsh-claude-act-pulse 1.2s ease-in-out infinite}",
4630
+ "@keyframes dsh-claude-act-pulse{0%,100%{opacity:1}50%{opacity:.3}}"
4631
+ ].join("");
4632
+ let cssInjected$3 = false;
4633
+ function ensureCss$3() {
4634
+ if (cssInjected$3 || typeof document === "undefined") return;
4635
+ cssInjected$3 = true;
4636
+ const element = document.createElement("style");
4637
+ element.dataset.dshClaudeActivity = "";
4638
+ element.textContent = ACTIVITY_CSS;
4639
+ document.head.appendChild(element);
4640
+ }
4641
+ function subcallGlyph(subcall) {
4642
+ if (subcall.isError === true || subcall.phase === "failed") return "×";
4643
+ if (subcall.phase === "started" || subcall.phase === "updated") return "●";
4644
+ return "✓";
4645
+ }
4646
+ function title(activity) {
4647
+ return activity.toolName ?? activity.title ?? activity.kind.replaceAll("-", " ");
4648
+ }
4649
+ function activityState(activity, running) {
4650
+ if (activity.isError === true || activity.kind === "error" || activity.phase === "denied" || activity.phase === "failed") return "error";
4651
+ if (running) return "ongoing";
4652
+ if (activity.kind === "warning") return "warning";
4653
+ return "done";
4654
+ }
4655
+ function ActivityRow({ row, t }) {
4656
+ const { activity, running, subcalls } = row;
4657
+ const [open, setOpen] = useState(false);
4658
+ const state = activityState(activity, running);
4659
+ const detail = activity.detail;
4660
+ const expandable = detail !== void 0 || subcalls.length > 0 || activity.kind === "thinking";
4661
+ const summary = activity.summary ?? (running ? t("running") : state === "error" ? t("failed") : t("done"));
4662
+ const body = activity.kind === "thinking" ? activity.summary : detail;
4663
+ return /* @__PURE__ */ jsxs(DisclosureRow, {
4664
+ rowClassName: "dsh-claude-flow-row",
4665
+ leadingClassName: "dsh-claude-flow-leading",
4666
+ titleClassName: "dsh-claude-flow-title",
4667
+ chevronClassName: "dsh-claude-flow-chevron",
4668
+ icon: activity.kind === "thinking" ? /* @__PURE__ */ jsx(IconThinkOutline14, { size: 14 }) : state === "done" ? /* @__PURE__ */ jsx(IconApiOutline14, { size: 14 }) : /* @__PURE__ */ jsx(StateDot, { state }),
4669
+ title: activity.kind === "thinking" ? t("thinking") : title(activity),
4670
+ open,
4671
+ expandable,
4672
+ expandOnRowClick: true,
4673
+ keepContentWhenOpen: true,
4674
+ onToggle: () => setOpen((value) => !value),
4675
+ collapsedContent: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
4676
+ className: "dsh-claude-flow-separator",
4677
+ "aria-hidden": "true"
4678
+ }), /* @__PURE__ */ jsx("span", {
4679
+ className: "dsh-claude-flow-summary",
4680
+ "data-error": state === "error" || void 0,
4681
+ children: summary
4682
+ })] }),
4683
+ children: [subcalls.length === 0 ? null : /* @__PURE__ */ jsx("div", {
4684
+ className: "dsh-claude-flow-subcalls",
4685
+ children: subcalls.map((subcall) => /* @__PURE__ */ jsxs("div", { children: [
4686
+ subcallGlyph(subcall),
4687
+ " ",
4688
+ subcall.toolName ?? t("subagent"),
4689
+ subcall.summary === void 0 ? "" : ` · ${subcall.summary}`
4690
+ ] }, subcall.toolUseId))
4691
+ }), body === void 0 ? null : activity.kind === "thinking" ? /* @__PURE__ */ jsx("div", {
4692
+ className: "dsh-claude-flow-body",
4693
+ children: body
4694
+ }) : /* @__PURE__ */ jsx("pre", {
4695
+ className: "dsh-claude-flow-detail",
4696
+ children: body
4697
+ })]
4698
+ });
4699
+ }
4700
+ /** Compaction has no turn of its own to render, so the transcript marks it with
4701
+ * a rule instead of a row: it separates prose rather than reporting work. */
4702
+ function ClaudeCompactionDivider({ compaction, t }) {
4703
+ const { trigger, preTokens, postTokens } = compaction;
4704
+ const label = trigger === "auto" ? t("compactedAuto") : t("compacted");
4705
+ const shrink = preTokens === void 0 || postTokens === void 0 ? void 0 : `${formatTokenCount(preTokens)} → ${formatTokenCount(postTokens)}`;
4706
+ return /* @__PURE__ */ jsx("div", {
4707
+ className: "dsh-claude-compaction",
4708
+ role: "separator",
4709
+ children: /* @__PURE__ */ jsx("span", {
4710
+ className: "dsh-claude-compaction-label",
4711
+ children: shrink === void 0 ? label : `${label} · ${shrink}`
4712
+ })
4713
+ });
4714
+ }
4715
+ function parsedValue(value) {
4716
+ if (value === void 0 || value.length === 0) return void 0;
4717
+ try {
4718
+ return JSON.parse(value);
4719
+ } catch {
4720
+ return value;
4785
4721
  }
4786
- #reopen() {
4787
- this.#controller?.abort();
4788
- this.#controller = void 0;
4789
- if (this.#disposed || this.#wanted.size === 0) return;
4790
- this.#run();
4722
+ }
4723
+ function record$6(value) {
4724
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4725
+ }
4726
+ function text$1(value) {
4727
+ return typeof value === "string" && value.length > 0 ? value : void 0;
4728
+ }
4729
+ function numberValue(value) {
4730
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
4731
+ }
4732
+ function displayValue(value) {
4733
+ if (value === null) return "null";
4734
+ if (typeof value === "string") return value;
4735
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
4736
+ if (Array.isArray(value)) return value.map(displayValue).join(", ");
4737
+ return record$6(value) === void 0 ? String(value) : Object.entries(value).map(([key, item]) => `${key}: ${displayValue(item)}`).join("\n");
4738
+ }
4739
+ function Section({ title, children }) {
4740
+ return /* @__PURE__ */ jsxs("section", {
4741
+ className: "dsh-claude-tool-section",
4742
+ children: [/* @__PURE__ */ jsx("div", {
4743
+ className: "dsh-claude-tool-section-title",
4744
+ children: title
4745
+ }), children]
4746
+ });
4747
+ }
4748
+ function Fields({ value, omit = [] }) {
4749
+ const entries = Object.entries(value).filter(([key, item]) => !omit.includes(key) && item !== void 0 && item !== "");
4750
+ if (entries.length === 0) return null;
4751
+ return /* @__PURE__ */ jsx("div", {
4752
+ className: "dsh-claude-tool-fields",
4753
+ children: entries.map(([key, item]) => /* @__PURE__ */ jsxs("div", {
4754
+ style: { display: "contents" },
4755
+ children: [/* @__PURE__ */ jsx("span", {
4756
+ className: "dsh-claude-tool-field-key",
4757
+ children: key.replaceAll("_", " ")
4758
+ }), /* @__PURE__ */ jsx("span", {
4759
+ className: "dsh-claude-tool-field-value",
4760
+ children: displayValue(item)
4761
+ })]
4762
+ }, key))
4763
+ });
4764
+ }
4765
+ function Paths({ paths }) {
4766
+ if (paths.length === 0) return null;
4767
+ return /* @__PURE__ */ jsx("ul", {
4768
+ className: "dsh-claude-tool-paths",
4769
+ children: paths.map((path, index) => /* @__PURE__ */ jsx("li", {
4770
+ className: "dsh-claude-tool-path",
4771
+ children: path
4772
+ }, `${path}:${index}`))
4773
+ });
4774
+ }
4775
+ function Source({ content, start = 1 }) {
4776
+ return /* @__PURE__ */ jsx("div", {
4777
+ className: "dsh-claude-tool-code",
4778
+ children: content.split(/\r?\n/u).map((line, index) => /* @__PURE__ */ jsxs("div", {
4779
+ className: "dsh-claude-tool-code-line",
4780
+ children: [/* @__PURE__ */ jsx("span", {
4781
+ className: "dsh-claude-tool-line-number",
4782
+ children: start + index
4783
+ }), /* @__PURE__ */ jsx("span", {
4784
+ className: "dsh-claude-tool-line-text",
4785
+ children: line || " "
4786
+ })]
4787
+ }, index))
4788
+ });
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
+ }
4805
+ function TextDetail({ title, value }) {
4806
+ if (value === void 0 || value.length === 0) return null;
4807
+ return /* @__PURE__ */ jsx(Section, {
4808
+ title,
4809
+ children: /* @__PURE__ */ jsx("pre", {
4810
+ className: "dsh-claude-tool-detail",
4811
+ children: value
4812
+ })
4813
+ });
4814
+ }
4815
+ function filenameList(value) {
4816
+ if (!Array.isArray(value)) return [];
4817
+ return value.filter((item) => typeof item === "string");
4818
+ }
4819
+ function ToolPresentation({ tool, t }) {
4820
+ const inputValue = parsedValue(tool.input);
4821
+ const outputValue = parsedValue(tool.output);
4822
+ const input = record$6(inputValue);
4823
+ const output = record$6(outputValue);
4824
+ const outputTitle = tool.isError === true ? t("toolError") : t("toolOutput");
4825
+ if (tool.diffs !== void 0) return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(DiffBlock, { diffs: [...tool.diffs] }), /* @__PURE__ */ jsx(TextDetail, {
4826
+ title: outputTitle,
4827
+ value: typeof outputValue === "string" ? outputValue : void 0
4828
+ })] });
4829
+ if (tool.toolName === "Read") {
4830
+ const file = record$6(output?.file);
4831
+ const path = text$1(file?.filePath) ?? text$1(input?.file_path);
4832
+ const content = text$1(file?.content) ?? (typeof outputValue === "string" ? outputValue : void 0);
4833
+ const offset = numberValue(input?.offset) ?? 1;
4834
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
4835
+ path === void 0 ? null : /* @__PURE__ */ jsx(Section, {
4836
+ title: "File",
4837
+ children: /* @__PURE__ */ jsx("div", {
4838
+ className: "dsh-claude-tool-path",
4839
+ children: path
4840
+ })
4841
+ }),
4842
+ input === void 0 ? null : /* @__PURE__ */ jsx(Section, {
4843
+ title: t("toolInput"),
4844
+ children: /* @__PURE__ */ jsx(Fields, {
4845
+ value: input,
4846
+ omit: ["file_path"]
4847
+ })
4848
+ }),
4849
+ content === void 0 ? null : /* @__PURE__ */ jsx(Section, {
4850
+ title: outputTitle,
4851
+ children: /* @__PURE__ */ jsx(Source, {
4852
+ content,
4853
+ start: offset
4854
+ })
4855
+ })
4856
+ ] });
4791
4857
  }
4792
- async #run() {
4793
- if (this.#running) return;
4794
- this.#running = true;
4795
- try {
4796
- while (!this.#disposed && this.#wanted.size > 0) {
4797
- const controller = new AbortController();
4798
- this.#controller = controller;
4799
- const lanes = this.#lanes();
4800
- let superseded = false;
4801
- try {
4802
- const reader = await this.#open(`${CLAUDE_PROJECTION_PATH}/multi?sessions=${lanes.map(encodeURIComponent).join(",")}`, controller.signal);
4803
- const stop = () => {
4804
- superseded = true;
4805
- reader.cancel().catch(() => void 0);
4806
- };
4807
- controller.signal.addEventListener("abort", stop, { once: true });
4808
- const decoder = new TextDecoder();
4809
- let buffer = "";
4810
- while (!controller.signal.aborted) {
4811
- const chunk = await reader.read();
4812
- buffer += decoder.decode(chunk.value, { stream: !chunk.done });
4813
- const lines = buffer.split(NDJSON_SEPARATOR);
4814
- buffer = lines.pop() ?? "";
4815
- for (const line of lines) this.#dispatch(line);
4816
- if (chunk.done) break;
4817
- }
4818
- controller.signal.removeEventListener("abort", stop);
4819
- await reader.cancel().catch(() => void 0);
4820
- } catch (error) {
4821
- if (isAbort(error)) {
4822
- if (this.#controller !== controller) continue;
4823
- return;
4824
- }
4825
- } finally {
4826
- if (this.#controller === controller) this.#controller = void 0;
4827
- }
4828
- if (this.#disposed || this.#wanted.size === 0) return;
4829
- if (superseded) continue;
4830
- await delay(this.#retryDelayMs);
4831
- }
4832
- } finally {
4833
- this.#running = false;
4834
- }
4858
+ if (tool.toolName === "Grep" || tool.toolName === "Glob") {
4859
+ const filenames = filenameList(output?.filenames);
4860
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [input === void 0 ? /* @__PURE__ */ jsx(TextDetail, {
4861
+ title: t("toolInput"),
4862
+ value: typeof inputValue === "string" ? inputValue : void 0
4863
+ }) : /* @__PURE__ */ jsx(Section, {
4864
+ title: t("toolInput"),
4865
+ children: /* @__PURE__ */ jsx(Fields, { value: input })
4866
+ }), filenames.length === 0 ? /* @__PURE__ */ jsx(TextDetail, {
4867
+ title: outputTitle,
4868
+ value: typeof outputValue === "string" ? outputValue : output === void 0 ? void 0 : displayValue(output)
4869
+ }) : /* @__PURE__ */ jsx(Section, {
4870
+ title: outputTitle,
4871
+ children: /* @__PURE__ */ jsx(Paths, { paths: filenames })
4872
+ })] });
4835
4873
  }
4836
- /** Route one carrier line to the session that owns it. A line for a session
4837
- * nobody is watching any more is dropped rather than reviving its lane. */
4838
- #dispatch(line) {
4839
- if (line.length === 0) return;
4840
- let session;
4841
- try {
4842
- session = JSON.parse(line).session;
4843
- } catch {
4844
- return;
4845
- }
4846
- if (typeof session !== "string") return;
4847
- this.#sources.get(session)?.feed(line);
4874
+ if (tool.toolName === "Bash" || tool.toolName === "PowerShell") {
4875
+ const command = text$1(input?.command);
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);
4878
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
4879
+ typed === void 0 ? null : /* @__PURE__ */ jsx(Terminal, { command: typed }),
4880
+ input === void 0 ? null : /* @__PURE__ */ jsx(Section, {
4881
+ title: t("toolInput"),
4882
+ children: /* @__PURE__ */ jsx(Fields, {
4883
+ value: input,
4884
+ omit: ["command", "description"]
4885
+ })
4886
+ }),
4887
+ /* @__PURE__ */ jsx(TextDetail, {
4888
+ title: outputTitle,
4889
+ value: terminal || (typeof outputValue === "string" ? outputValue : void 0)
4890
+ })
4891
+ ] });
4848
4892
  }
4849
- };
4850
- //#endregion
4851
- //#region src/client/ClaudeActivityNode.tsx
4852
- const EMPTY_TASKS = [];
4853
- const ACTIVITY_CSS = [
4854
- ".dsh-claude-flow{display:flex;flex-direction:column;gap:10px}",
4855
- ".dsh-claude-transcript-text{color:var(--dsw-alias-label-primary);font-size:15px;line-height:24px;overflow-wrap:anywhere}",
4856
- ".dsh-claude-tool-group-native{overflow:visible}",
4857
- ".dsh-claude-tool-group-native>.dsh-claude-flow-row{padding:0}",
4858
- ".dsh-claude-tool-list{max-height:min(420px,calc(100vh - 320px));overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;margin-top:4px;border:1px solid var(--dsw-alias-border-l1, color-mix(in srgb, currentColor 12%, transparent));border-radius:10px;scrollbar-gutter:stable}",
4859
- ".dsh-claude-tool-item{border-top:1px solid var(--dsw-alias-border-l1, color-mix(in srgb, currentColor 12%, transparent))}",
4860
- ".dsh-claude-tool-item:first-child{border-top:0}",
4861
- ".dsh-claude-tool-summary-row{display:flex;align-items:center;min-height:34px;gap:8px;padding:0 10px;cursor:pointer;list-style:none}",
4862
- ".dsh-claude-tool-summary-row::-webkit-details-marker{display:none}",
4863
- ".dsh-claude-tool-summary-row::after{content:\"›\";margin-left:2px;flex:none;align-self:center;color:var(--dsw-alias-label-secondary);font-size:22px;line-height:1;transform-origin:center;transition:transform .15s ease}",
4864
- ".dsh-claude-tool-item[open]>.dsh-claude-tool-summary-row::after{transform:rotate(90deg)}",
4865
- ".dsh-claude-tool-content{min-width:0;padding:0 10px 8px}",
4866
- ".dsh-claude-tool-content>*{max-width:100%;box-sizing:border-box}",
4867
- ".dsh-claude-tool-section{margin-top:8px}",
4868
- ".dsh-claude-tool-section-title{margin-bottom:4px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;text-transform:uppercase;letter-spacing:.04em}",
4869
- ".dsh-claude-tool-fields{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:3px 12px;font-size:13px;line-height:20px}",
4870
- ".dsh-claude-tool-field-key{color:var(--dsw-alias-label-tertiary)}",
4871
- ".dsh-claude-tool-field-value{min-width:0;color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere}",
4872
- ".dsh-claude-tool-paths{display:flex;flex-direction:column;gap:2px;margin:0;padding:0;list-style:none;font:var(--dsw-font-markdown-code-block-small)}",
4873
- ".dsh-claude-tool-path{overflow-wrap:anywhere;color:var(--dsw-alias-label-primary)}",
4874
- ".dsh-claude-tool-code{max-height:260px;overflow:auto;margin:0;padding:8px 0;border-radius:8px;background:var(--dsw-alias-markdown-code-block);font:var(--dsw-font-markdown-code-block-small)}",
4875
- ".dsh-claude-tool-code-line{display:grid;grid-template-columns:44px minmax(max-content,1fr);min-height:18px}",
4876
- ".dsh-claude-tool-line-number{padding-right:10px;text-align:right;user-select:none;color:var(--dsw-alias-label-caption);border-right:1px solid var(--dsw-alias-border-l1, color-mix(in srgb, currentColor 12%, transparent))}",
4877
- ".dsh-claude-tool-line-text{padding:0 10px;white-space:pre}",
4878
- ".dsh-claude-tool-label{width:72px;flex:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary)}",
4879
- ".dsh-claude-tool-description{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;line-height:20px;color:var(--dsw-alias-label-primary)}",
4880
- ".dsh-claude-tool-stats{display:inline-flex;gap:4px;flex:none;font-size:13px;line-height:20px}",
4881
- ".dsh-claude-diff-add{color:var(--dsw-alias-state-success-primary,#21c55d)}",
4882
- ".dsh-claude-diff-delete{color:var(--dsw-alias-state-error-primary)}",
4883
- ".dsh-claude-tool-name{font-size:14px;line-height:22px;color:var(--dsw-alias-label-primary)}",
4884
- ".dsh-claude-tool-summary{margin-left:8px;color:var(--dsw-alias-label-tertiary)}",
4885
- ".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}",
4886
- ".dsh-claude-flow-row{position:relative;overflow:hidden}",
4887
- ".dsh-claude-flow-leading{flex-shrink:0}",
4888
- ".dsh-claude-flow-title{font-weight:400}",
4889
- ".dsh-claude-flow-chevron{color:var(--dsw-alias-label-secondary)}",
4890
- ".dsh-claude-flow-separator{width:2px;height:2px;margin:0 8px;border-radius:1px;background:var(--dsw-alias-label-caption);flex:none}",
4891
- ".dsh-claude-flow-summary{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-size:14px;line-height:24px;flex:auto}",
4892
- ".dsh-claude-flow-summary[data-error]{color:var(--dsw-alias-state-error-primary)}",
4893
- ".dsh-claude-flow-body{margin:4px 0 4px 22px;color:var(--dsw-alias-label-tertiary);font-size:14px;line-height:24px;white-space:pre-wrap;overflow-wrap:anywhere}",
4894
- ".dsh-claude-flow-detail{max-height:260px;overflow:auto;margin:4px 0 4px 4px;padding:12px 16px;border:1px solid var(--dsw-alias-border-l1, color-mix(in srgb, currentColor 12%, transparent));border-radius:12px;background:var(--dsw-alias-markdown-code-block);color:var(--dsw-alias-label-primary);font:var(--dsw-font-markdown-code-block-small);white-space:pre-wrap}",
4895
- ".dsh-claude-flow-subcalls{display:flex;flex-direction:column;gap:2px;margin:4px 0 4px 22px;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:22px}",
4896
- ".dsh-claude-compaction{display:flex;align-items:center;gap:10px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}",
4897
- ".dsh-claude-compaction::before,.dsh-claude-compaction::after{content:\"\";flex:1;height:1px;background:var(--dsw-alias-border-l1, color-mix(in srgb, currentColor 12%, transparent))}",
4898
- ".dsh-claude-compaction-label{flex:none;white-space:nowrap}",
4899
- ".dsh-claude-act-running{animation:dsh-claude-act-pulse 1.2s ease-in-out infinite}",
4900
- "@keyframes dsh-claude-act-pulse{0%,100%{opacity:1}50%{opacity:.3}}"
4901
- ].join("");
4902
- let cssInjected$3 = false;
4903
- function ensureCss$3() {
4904
- if (cssInjected$3 || typeof document === "undefined") return;
4905
- cssInjected$3 = true;
4906
- const element = document.createElement("style");
4907
- element.dataset.dshClaudeActivity = "";
4908
- element.textContent = ACTIVITY_CSS;
4909
- document.head.appendChild(element);
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
+ })] });
4910
4906
  }
4911
- function subcallGlyph(subcall) {
4912
- if (subcall.isError === true || subcall.phase === "failed") return "×";
4913
- if (subcall.phase === "started" || subcall.phase === "updated") return "●";
4914
- return "";
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
+ });
4915
4986
  }
4916
- function title(activity) {
4917
- return activity.toolName ?? activity.title ?? activity.kind.replaceAll("-", " ");
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`;
4918
4995
  }
4919
- function activityState(activity, running) {
4920
- if (activity.isError === true || activity.kind === "error" || activity.phase === "denied" || activity.phase === "failed") return "error";
4921
- if (running) return "ongoing";
4922
- if (activity.kind === "warning") return "warning";
4923
- return "done";
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;
4924
5018
  }
4925
- function ActivityRow({ row, t }) {
4926
- const { activity, running, subcalls } = row;
4927
- const [open, setOpen] = useState(false);
4928
- const state = activityState(activity, running);
4929
- const detail = activity.detail;
4930
- const expandable = detail !== void 0 || subcalls.length > 0 || activity.kind === "thinking";
4931
- const summary = activity.summary ?? (running ? t("running") : state === "error" ? t("failed") : t("done"));
4932
- const body = activity.kind === "thinking" ? activity.summary : detail;
4933
- return /* @__PURE__ */ jsxs(DisclosureRow, {
4934
- rowClassName: "dsh-claude-flow-row",
4935
- leadingClassName: "dsh-claude-flow-leading",
4936
- titleClassName: "dsh-claude-flow-title",
4937
- chevronClassName: "dsh-claude-flow-chevron",
4938
- icon: activity.kind === "thinking" ? /* @__PURE__ */ jsx(IconThinkOutline14, { size: 14 }) : state === "done" ? /* @__PURE__ */ jsx(IconApiOutline14, { size: 14 }) : /* @__PURE__ */ jsx(StateDot, { state }),
4939
- title: activity.kind === "thinking" ? t("thinking") : title(activity),
4940
- open,
4941
- expandable,
4942
- expandOnRowClick: true,
4943
- keepContentWhenOpen: true,
4944
- onToggle: () => setOpen((value) => !value),
4945
- collapsedContent: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
4946
- className: "dsh-claude-flow-separator",
4947
- "aria-hidden": "true"
4948
- }), /* @__PURE__ */ jsx("span", {
4949
- className: "dsh-claude-flow-summary",
4950
- "data-error": state === "error" || void 0,
4951
- children: summary
4952
- })] }),
4953
- children: [subcalls.length === 0 ? null : /* @__PURE__ */ jsx("div", {
4954
- className: "dsh-claude-flow-subcalls",
4955
- children: subcalls.map((subcall) => /* @__PURE__ */ jsxs("div", { children: [
4956
- subcallGlyph(subcall),
4957
- " ",
4958
- subcall.toolName ?? t("subagent"),
4959
- subcall.summary === void 0 ? "" : ` · ${subcall.summary}`
4960
- ] }, subcall.toolUseId))
4961
- }), body === void 0 ? null : activity.kind === "thinking" ? /* @__PURE__ */ jsx("div", {
4962
- className: "dsh-claude-flow-body",
4963
- children: body
4964
- }) : /* @__PURE__ */ jsx("pre", {
4965
- className: "dsh-claude-flow-detail",
4966
- children: body
4967
- })]
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
+ ]
4968
5037
  });
4969
5038
  }
4970
- /** Compaction has no turn of its own to render, so the transcript marks it with
4971
- * a rule instead of a row: it separates prose rather than reporting work. */
4972
- function ClaudeCompactionDivider({ compaction, t }) {
4973
- const { trigger, preTokens, postTokens } = compaction;
4974
- const label = trigger === "auto" ? t("compactedAuto") : t("compacted");
4975
- const shrink = preTokens === void 0 || postTokens === void 0 ? void 0 : `${formatTokenCount(preTokens)} → ${formatTokenCount(postTokens)}`;
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;
4976
5052
  return /* @__PURE__ */ jsx("div", {
4977
- className: "dsh-claude-compaction",
4978
- role: "separator",
4979
- children: /* @__PURE__ */ jsx("span", {
4980
- className: "dsh-claude-compaction-label",
4981
- children: shrink === void 0 ? label : `${label} · ${shrink}`
4982
- })
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}`))
4983
5076
  });
4984
5077
  }
4985
- function parsedValue(value) {
4986
- if (value === void 0 || value.length === 0) return void 0;
4987
- try {
4988
- return JSON.parse(value);
4989
- } catch {
4990
- return value;
4991
- }
4992
- }
4993
- function record$6(value) {
4994
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4995
- }
4996
- function text$1(value) {
4997
- return typeof value === "string" && value.length > 0 ? value : void 0;
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
+ };
4998
5093
  }
4999
- function numberValue(value) {
5000
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
5094
+ function activitiesForTask(activities, taskId) {
5095
+ return activities.filter((activity) => activity.taskId === taskId);
5001
5096
  }
5002
- function displayValue(value) {
5003
- if (value === null) return "null";
5004
- if (typeof value === "string") return value;
5005
- if (typeof value === "number" || typeof value === "boolean") return String(value);
5006
- if (Array.isArray(value)) return value.map(displayValue).join(", ");
5007
- return record$6(value) === void 0 ? String(value) : Object.entries(value).map(([key, item]) => `${key}: ${displayValue(item)}`).join("\n");
5097
+ function tasksForTurn(tasks, turn) {
5098
+ return tasks.filter((task) => task.originTurn === turn && isProjectedTask(task));
5008
5099
  }
5009
- function Section({ title, children }) {
5010
- return /* @__PURE__ */ jsxs("section", {
5011
- className: "dsh-claude-tool-section",
5012
- children: [/* @__PURE__ */ jsx("div", {
5013
- className: "dsh-claude-tool-section-title",
5014
- children: title
5015
- }), children]
5016
- });
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
+ };
5017
5112
  }
5018
- function Fields({ value, omit = [] }) {
5019
- const entries = Object.entries(value).filter(([key, item]) => !omit.includes(key) && item !== void 0 && item !== "");
5020
- if (entries.length === 0) return null;
5021
- return /* @__PURE__ */ jsx("div", {
5022
- className: "dsh-claude-tool-fields",
5023
- children: entries.map(([key, item]) => /* @__PURE__ */ jsxs("div", {
5024
- style: { display: "contents" },
5025
- children: [/* @__PURE__ */ jsx("span", {
5026
- className: "dsh-claude-tool-field-key",
5027
- children: key.replaceAll("_", " ")
5028
- }), /* @__PURE__ */ jsx("span", {
5029
- className: "dsh-claude-tool-field-value",
5030
- children: displayValue(item)
5031
- })]
5032
- }, key))
5033
- });
5113
+ function statusGlyph(status) {
5114
+ if (status === "running") return "";
5115
+ if (status === "completed") return "✓";
5116
+ if (status === "stopped") return "";
5117
+ return "×";
5034
5118
  }
5035
- function Paths({ paths }) {
5036
- if (paths.length === 0) return null;
5037
- return /* @__PURE__ */ jsx("ul", {
5038
- className: "dsh-claude-tool-paths",
5039
- children: paths.map((path, index) => /* @__PURE__ */ jsx("li", {
5040
- className: "dsh-claude-tool-path",
5041
- children: path
5042
- }, `${path}:${index}`))
5043
- });
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";
5044
5125
  }
5045
- function Source({ content, start = 1 }) {
5046
- return /* @__PURE__ */ jsx("div", {
5047
- className: "dsh-claude-tool-code",
5048
- children: content.split(/\r?\n/u).map((line, index) => /* @__PURE__ */ jsxs("div", {
5049
- className: "dsh-claude-tool-code-line",
5050
- children: [/* @__PURE__ */ jsx("span", {
5051
- className: "dsh-claude-tool-line-number",
5052
- children: start + index
5053
- }), /* @__PURE__ */ jsx("span", {
5054
- className: "dsh-claude-tool-line-text",
5055
- children: line || " "
5056
- })]
5057
- }, index))
5058
- });
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;
5059
5135
  }
5060
- function TextDetail({ title, value }) {
5061
- if (value === void 0 || value.length === 0) return null;
5062
- return /* @__PURE__ */ jsx(Section, {
5063
- title,
5064
- children: /* @__PURE__ */ jsx("pre", {
5065
- className: "dsh-claude-tool-detail",
5066
- children: value
5067
- })
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,
5145
+ children: [
5146
+ /* @__PURE__ */ jsx("p", {
5147
+ style: taskActivityTitle,
5148
+ children: activity.title ?? activity.kind
5149
+ }),
5150
+ activity.summary === void 0 ? null : /* @__PURE__ */ jsx("p", {
5151
+ style: taskActivitySummary,
5152
+ children: activity.summary
5153
+ }),
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
5162
+ })]
5163
+ })
5164
+ ]
5165
+ })]
5068
5166
  });
5069
5167
  }
5070
- function filenameList(value) {
5071
- if (!Array.isArray(value)) return [];
5072
- return value.filter((item) => typeof item === "string");
5073
- }
5074
- function ToolPresentation({ tool, t }) {
5075
- const inputValue = parsedValue(tool.input);
5076
- const outputValue = parsedValue(tool.output);
5077
- const input = record$6(inputValue);
5078
- const output = record$6(outputValue);
5079
- const outputTitle = tool.isError === true ? t("toolError") : t("toolOutput");
5080
- if (tool.diffs !== void 0) return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(DiffBlock, { diffs: [...tool.diffs] }), /* @__PURE__ */ jsx(TextDetail, {
5081
- title: outputTitle,
5082
- value: typeof outputValue === "string" ? outputValue : void 0
5083
- })] });
5084
- if (tool.toolName === "Read") {
5085
- const file = record$6(output?.file);
5086
- const path = text$1(file?.filePath) ?? text$1(input?.file_path);
5087
- const content = text$1(file?.content) ?? (typeof outputValue === "string" ? outputValue : void 0);
5088
- const offset = numberValue(input?.offset) ?? 1;
5089
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [
5090
- path === void 0 ? null : /* @__PURE__ */ jsx(Section, {
5091
- title: "File",
5092
- children: /* @__PURE__ */ jsx("div", {
5093
- className: "dsh-claude-tool-path",
5094
- children: path
5095
- })
5096
- }),
5097
- input === void 0 ? null : /* @__PURE__ */ jsx(Section, {
5098
- title: t("toolInput"),
5099
- children: /* @__PURE__ */ jsx(Fields, {
5100
- value: input,
5101
- omit: ["file_path"]
5102
- })
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
+ })]
5103
5208
  }),
5104
- content === void 0 ? null : /* @__PURE__ */ jsx(Section, {
5105
- title: outputTitle,
5106
- children: /* @__PURE__ */ jsx(Source, {
5107
- content,
5108
- start: offset
5109
- })
5110
- })
5111
- ] });
5112
- }
5113
- if (tool.toolName === "Grep" || tool.toolName === "Glob") {
5114
- const filenames = filenameList(output?.filenames);
5115
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [input === void 0 ? /* @__PURE__ */ jsx(TextDetail, {
5116
- title: t("toolInput"),
5117
- value: typeof inputValue === "string" ? inputValue : void 0
5118
- }) : /* @__PURE__ */ jsx(Section, {
5119
- title: t("toolInput"),
5120
- children: /* @__PURE__ */ jsx(Fields, { value: input })
5121
- }), filenames.length === 0 ? /* @__PURE__ */ jsx(TextDetail, {
5122
- title: outputTitle,
5123
- value: typeof outputValue === "string" ? outputValue : output === void 0 ? void 0 : displayValue(output)
5124
- }) : /* @__PURE__ */ jsx(Section, {
5125
- title: outputTitle,
5126
- children: /* @__PURE__ */ jsx(Paths, { paths: filenames })
5127
- })] });
5128
- }
5129
- if (tool.toolName === "Bash" || tool.toolName === "PowerShell") {
5130
- const command = text$1(input?.command);
5131
- const terminal = [text$1(output?.stdout), text$1(output?.stderr)].filter((value) => value !== void 0).join("\n");
5132
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [
5133
- /* @__PURE__ */ jsx(TextDetail, {
5134
- title: "Command",
5135
- value: command ?? (typeof inputValue === "string" ? inputValue : void 0)
5209
+ meta.length === 0 ? null : /* @__PURE__ */ jsx("p", {
5210
+ style: taskMeta$1,
5211
+ children: meta.join(" · ")
5136
5212
  }),
5137
- input === void 0 ? null : /* @__PURE__ */ jsx(Section, {
5138
- title: t("toolInput"),
5139
- children: /* @__PURE__ */ jsx(Fields, {
5140
- value: input,
5141
- omit: ["command"]
5142
- })
5213
+ task.summary === void 0 || running ? null : /* @__PURE__ */ jsx("p", {
5214
+ style: taskSummary,
5215
+ children: task.summary
5143
5216
  }),
5144
- /* @__PURE__ */ jsx(TextDetail, {
5145
- title: outputTitle,
5146
- value: terminal || (typeof outputValue === "string" ? outputValue : void 0)
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
+ })]
5147
5238
  })
5148
- ] });
5149
- }
5150
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [input === void 0 ? /* @__PURE__ */ jsx(TextDetail, {
5151
- title: t("toolInput"),
5152
- value: typeof inputValue === "string" ? inputValue : void 0
5153
- }) : /* @__PURE__ */ jsx(Section, {
5154
- title: t("toolInput"),
5155
- children: /* @__PURE__ */ jsx(Fields, { value: input })
5156
- }), output === void 0 ? /* @__PURE__ */ jsx(TextDetail, {
5157
- title: outputTitle,
5158
- value: typeof outputValue === "string" ? outputValue : void 0
5159
- }) : /* @__PURE__ */ jsx(Section, {
5160
- title: outputTitle,
5161
- children: /* @__PURE__ */ jsx(Fields, { value: output })
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
5162
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
+ });
5163
5272
  }
5164
- function ClaudeTranscriptToolItem({ tool, t }) {
5165
- return /* @__PURE__ */ jsxs("details", {
5166
- className: "dsh-claude-tool-item",
5167
- children: [/* @__PURE__ */ jsxs("summary", {
5168
- className: "dsh-claude-tool-summary-row",
5169
- children: [
5170
- /* @__PURE__ */ jsx("span", {
5171
- className: "dsh-claude-tool-label",
5172
- children: tool.toolName
5173
- }),
5174
- /* @__PURE__ */ jsx("span", {
5175
- className: "dsh-claude-tool-description",
5176
- children: tool.description
5177
- }),
5178
- tool.additions === void 0 && tool.deletions === void 0 ? null : /* @__PURE__ */ jsxs("span", {
5179
- className: "dsh-claude-tool-stats",
5180
- children: [/* @__PURE__ */ jsxs("span", {
5181
- className: "dsh-claude-diff-add",
5182
- children: ["+", tool.additions ?? 0]
5183
- }), /* @__PURE__ */ jsxs("span", {
5184
- className: "dsh-claude-diff-delete",
5185
- children: ["−", tool.deletions ?? 0]
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;
5289
+ return /* @__PURE__ */ jsxs("div", {
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, {})
5311
+ })]
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))
5186
5331
  })]
5187
- })
5188
- ]
5189
- }), /* @__PURE__ */ jsxs("div", {
5190
- className: "dsh-claude-tool-content",
5191
- children: [tool.subcalls.length === 0 ? null : /* @__PURE__ */ jsx("div", {
5192
- className: "dsh-claude-flow-subcalls",
5193
- children: tool.subcalls.map((subcall) => /* @__PURE__ */ jsxs("div", { children: [
5194
- subcallGlyph(subcall),
5195
- " ",
5196
- subcall.toolName ?? t("subagent"),
5197
- subcall.summary === void 0 ? "" : ` · ${subcall.summary}`
5198
- ] }, subcall.toolUseId))
5199
- }), /* @__PURE__ */ jsx(ToolPresentation, {
5200
- tool,
5201
- t
5202
- })]
5203
- })]
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
+ ]
5204
5356
  });
5205
5357
  }
5206
- function ClaudeTranscriptToolGroup({ tools, additions, deletions, files: _files, t }) {
5207
- const [open, setOpen] = useState(false);
5208
- const failed = tools.some((tool) => tool.isError === true || tool.phase === "failed");
5209
- const running = tools.some((tool) => tool.phase === "started" || tool.phase === "updated");
5210
- const summary = tools.length === 1 ? t("usedTool") : t("usedTools", { count: tools.length });
5211
- return /* @__PURE__ */ jsxs("div", {
5212
- className: "dsh-claude-tool-group-native",
5213
- children: [/* @__PURE__ */ jsx(DisclosureRow, {
5214
- rowClassName: "dsh-claude-flow-row",
5215
- leadingClassName: "dsh-claude-flow-leading",
5216
- titleClassName: "dsh-claude-flow-title",
5217
- chevronClassName: "dsh-claude-flow-chevron",
5218
- icon: failed ? /* @__PURE__ */ jsx(StateDot, { state: "error" }) : running ? /* @__PURE__ */ jsx(StateDot, { state: "ongoing" }) : /* @__PURE__ */ jsx(IconApiOutline14, { size: 14 }),
5219
- title: summary,
5220
- open,
5221
- expandable: true,
5222
- expandOnRowClick: true,
5223
- keepContentWhenOpen: true,
5224
- onToggle: () => setOpen((value) => !value),
5225
- collapsedContent: additions !== void 0 || deletions !== void 0 ? /* @__PURE__ */ jsxs("span", {
5226
- className: "dsh-claude-tool-stats",
5227
- children: [/* @__PURE__ */ jsxs("span", {
5228
- className: "dsh-claude-diff-add",
5229
- children: ["+", additions ?? 0]
5230
- }), /* @__PURE__ */ jsxs("span", {
5231
- className: "dsh-claude-diff-delete",
5232
- children: ["−", deletions ?? 0]
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 : {};
5402
+ return /* @__PURE__ */ jsx("div", {
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
5233
5478
  })]
5234
- }) : void 0
5235
- }), open ? /* @__PURE__ */ jsx("div", {
5236
- className: "dsh-claude-tool-list",
5237
- children: tools.map((tool) => /* @__PURE__ */ jsx(ClaudeTranscriptToolItem, {
5238
- tool,
5239
- t
5240
- }, tool.toolUseId))
5241
- }) : null]
5479
+ })]
5480
+ })
5242
5481
  });
5243
5482
  }
5244
- function ClaudeActivityNode({ node, useClaudeProjection, t }) {
5245
- ensureCss$3();
5246
- const marker = node.data;
5247
- const activities = useClaudeProjection((value) => selectStepActivities(value, marker.turn, marker.step));
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
5490
+ });
5491
+ }
5492
+ //#endregion
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 }) {
5248
5497
  const tasks = useClaudeProjection((value) => value.tasks?.tasks ?? EMPTY_TASKS);
5249
- const items = useMemo(() => transcriptItemsForStep(activities, marker.turn, marker.step, tasks), [
5250
- activities,
5251
- marker.step,
5252
- marker.turn,
5253
- tasks
5254
- ]);
5255
- const markdownLabels = useClaudeMarkdownLabels(t);
5256
- if (items.length === 0) return null;
5257
- return /* @__PURE__ */ jsx("div", {
5258
- className: "dsh-claude-flow",
5259
- children: items.map((item) => item.kind === "text" ? /* @__PURE__ */ jsx("div", {
5260
- className: "dsh-claude-transcript-text",
5261
- children: /* @__PURE__ */ jsx(ClaudeMarkdown, {
5262
- text: item.text,
5263
- labels: markdownLabels
5264
- })
5265
- }, `text:${item.ordinal}`) : item.kind === "compaction" ? /* @__PURE__ */ jsx(ClaudeCompactionDivider, {
5266
- compaction: item.compaction,
5267
- t
5268
- }, `compaction:${item.ordinal}`) : item.kind === "tools" ? /* @__PURE__ */ jsx(ClaudeTranscriptToolGroup, {
5269
- tools: item.tools,
5270
- ...item.additions === void 0 ? {} : { additions: item.additions },
5271
- ...item.deletions === void 0 ? {} : { deletions: item.deletions },
5272
- ...item.files === void 0 ? {} : { files: item.files },
5273
- t
5274
- }, `tools:${item.ordinal}`) : /* @__PURE__ */ jsx(ActivityRow, {
5275
- row: item.row,
5276
- t
5277
- }, `activity:${item.ordinal}`))
5498
+ return /* @__PURE__ */ jsx(ClaudeTaskLauncher, {
5499
+ turn: node.data.turn,
5500
+ tasks,
5501
+ t,
5502
+ openTasks
5278
5503
  });
5279
5504
  }
5280
5505
  //#endregion
@@ -5394,6 +5619,7 @@ window.__ModuleLoader__.load({
5394
5619
  if (typeof setting !== "object" || setting === null || Array.isArray(setting)) return false;
5395
5620
  const item = setting;
5396
5621
  if (typeof item.key !== "string" || typeof item.value !== "string" || ![
5622
+ "immediate",
5397
5623
  "new-session",
5398
5624
  "next-turn",
5399
5625
  "next-worktree",
@@ -5584,6 +5810,23 @@ window.__ModuleLoader__.load({
5584
5810
  "seven_day_opus",
5585
5811
  "seven_day_sonnet"
5586
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
+ }
5587
5830
  /** Per-setting label and the effect note that used to sit as a standalone
5588
5831
  * paragraph under the card; it now hangs off the label as a hover hint. */
5589
5832
  const SETTING_COPY = {
@@ -5595,6 +5838,10 @@ window.__ModuleLoader__.load({
5595
5838
  label: "renderer",
5596
5839
  hint: "rendererEffect"
5597
5840
  },
5841
+ prose: {
5842
+ label: "prose",
5843
+ hint: "proseEffect"
5844
+ },
5598
5845
  worktreeBranchPrefix: {
5599
5846
  label: "worktreeBranchPrefix",
5600
5847
  hint: "worktreeBranchPrefixEffect"
@@ -5613,7 +5860,9 @@ window.__ModuleLoader__.load({
5613
5860
  * names) carry no entry and keep the label the route reported. */
5614
5861
  const SETTING_OPTION_COPY = {
5615
5862
  "renderer:plugin": "rendererPlugin",
5616
- "renderer:native": "rendererNative"
5863
+ "renderer:native": "rendererNative",
5864
+ "prose:plain": "prosePlain",
5865
+ "prose:enhanced": "proseEnhanced"
5617
5866
  };
5618
5867
  function settingOptionLabel(settingKey, option, t) {
5619
5868
  const key = SETTING_OPTION_COPY[`${settingKey}:${option.value}`];
@@ -5852,6 +6101,7 @@ window.__ModuleLoader__.load({
5852
6101
  });
5853
6102
  if (!isGlobalSettingsView(payload)) throw new Error("Invalid global settings response");
5854
6103
  setGlobalSettings(payload);
6104
+ applyClaudeMarkdownTheme(proseModeOf(payload.settings));
5855
6105
  } catch (cause) {
5856
6106
  setGlobalSettingsError(cardFailure(cause));
5857
6107
  } finally {
@@ -5962,7 +6212,7 @@ window.__ModuleLoader__.load({
5962
6212
  globalSettings === void 0 ? /* @__PURE__ */ jsx("p", {
5963
6213
  style: notice,
5964
6214
  children: t("globalSettingsLoading")
5965
- }) : globalSettings.settings.map((setting) => {
6215
+ }) : visibleGlobalSettings(globalSettings.settings).map((setting) => {
5966
6216
  const copy = SETTING_COPY[setting.key];
5967
6217
  return /* @__PURE__ */ jsxs("div", {
5968
6218
  style: diagnosticGrid,
@@ -16702,6 +16952,10 @@ window.__ModuleLoader__.load({
16702
16952
  rendererPlugin: "插件渲染器",
16703
16953
  rendererNative: "DSH 原生渲染器",
16704
16954
  rendererEffect: "插件渲染器沿用本插件自带的转录视图:交错的正文、成组的工具卡片与活动行。DSH 原生渲染器改由 DSH 自身绘制:正文作为普通助手文本块,思考作为推理块,Claude 的顶层工具会镜像成原生工具卡片。修改从下一个回合起生效;已经产生的回合仍按记录时的渲染器显示,不会重绘。",
16955
+ prose: "Markdown 彩色高亮",
16956
+ prosePlain: "关闭",
16957
+ proseEnhanced: "开启",
16958
+ proseEffect: "开启后,正文的标题、加粗、斜体、行内代码、列表符号、引用与链接各自着色,代码块换成深色描边底。这会覆盖本插件默认对齐 Claude 桌面版的配色。仅在使用插件渲染器时有效,改动立即生效,无需等待下一回合。",
16705
16959
  worktreeBranchPrefix: "Worktree 分支前缀",
16706
16960
  worktreeBranchPrefixEffect: "分支前缀修改会应用于之后自动创建的 Worktree 分支;显式指定的完整分支名不会被修改。",
16707
16961
  maxProcessesSetting: "Claude 进程上限",
@@ -17014,7 +17268,12 @@ window.__ModuleLoader__.load({
17014
17268
  rewindSubmitting: "回退中…",
17015
17269
  rewindFailed: "回退失败({code})。",
17016
17270
  rewindBusy: "这个会话正在运行,请等本轮结束后再回退。",
17017
- rewindStale: "宿主进程里的插件还是旧版本(没有回退接口)。重启 DSH 后再试。"
17271
+ rewindStale: "宿主进程里的插件还是旧版本(没有回退接口)。重启 DSH 后再试。",
17272
+ turnUsage: "本回合用量",
17273
+ turnUsageTokens: "{count} tok",
17274
+ turnUsageCache: "缓存命中 {percent}%",
17275
+ turnUsageTtft: "首字 {duration}",
17276
+ turnUsageCost: "累计 ${cost}"
17018
17277
  };
17019
17278
  const en = {
17020
17279
  nav: "Claude Code",
@@ -17066,6 +17325,10 @@ window.__ModuleLoader__.load({
17066
17325
  rendererPlugin: "Plugin renderer",
17067
17326
  rendererNative: "DSH native renderer",
17068
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.",
17069
17332
  worktreeBranchPrefix: "Worktree branch prefix",
17070
17333
  worktreeBranchPrefixEffect: "Prefix changes apply to subsequently generated Worktree branches. Explicit full branch names remain unchanged.",
17071
17334
  maxProcessesSetting: "Claude process limit",
@@ -17378,7 +17641,12 @@ window.__ModuleLoader__.load({
17378
17641
  rewindSubmitting: "Rewinding…",
17379
17642
  rewindFailed: "The rewind failed ({code}).",
17380
17643
  rewindBusy: "This session is running; wait for the turn to finish before rewinding.",
17381
- 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"
17382
17650
  };
17383
17651
  //#endregion
17384
17652
  //#region src/client/index.tsx
@@ -17428,6 +17696,9 @@ window.__ModuleLoader__.load({
17428
17696
  }), "dsh-claude: client copy");
17429
17697
  const t = ctx.locale.bind(namespace);
17430
17698
  ctx.effect(() => restyleHostChrome(), "dsh-claude: Host chrome restyling");
17699
+ pluginRead(CLAUDE_GLOBAL_SETTINGS_PATH, "fast").then((payload) => {
17700
+ if (isGlobalSettingsView(payload)) applyClaudeMarkdownTheme(proseModeOf(payload.settings));
17701
+ }).catch(() => {});
17431
17702
  const projections = new ClaudeProjectionStore({ report: (kind, detail) => {
17432
17703
  diagnostics.report(kind, detail);
17433
17704
  } });