@workerdeck/ui 0.21.0 → 0.23.0

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.
Files changed (40) hide show
  1. package/README.md +3 -2
  2. package/build/{SessionPanel-BjNAnWMF.d.mts → SessionPanel-BobynUo4.d.mts} +75 -5
  3. package/build/{SessionPanel-DNuPKg5c.mjs → SessionPanel-DUt2VzXG.mjs} +672 -231
  4. package/build/SessionPanel-DUt2VzXG.mjs.map +1 -0
  5. package/build/index.d.mts +54 -21
  6. package/build/index.mjs +7 -6
  7. package/build/index.mjs.map +1 -1
  8. package/build/scoped.css +3611 -0
  9. package/build/workspace.d.mts +7 -1
  10. package/build/workspace.mjs +4 -2
  11. package/build/workspace.mjs.map +1 -1
  12. package/package.json +10 -6
  13. package/src/components/agent/Composer.tsx +10 -10
  14. package/src/components/agent/ContextDialog.tsx +3 -2
  15. package/src/components/agent/Conversation.tsx +1 -1
  16. package/src/components/agent/McpDialog.tsx +3 -1
  17. package/src/components/agent/Response.tsx +4 -0
  18. package/src/components/agent/Scrubber.tsx +248 -0
  19. package/src/components/agent/SessionList.tsx +3 -1
  20. package/src/components/agent/SessionPanel.tsx +132 -16
  21. package/src/components/agent/SessionSteps.tsx +10 -4
  22. package/src/components/agent/SessionWorkspace.tsx +8 -0
  23. package/src/components/agent/SkillsDialog.tsx +3 -2
  24. package/src/components/agent/StatusBar.tsx +3 -3
  25. package/src/components/agent/Transcript.tsx +147 -73
  26. package/src/components/agent/UsageDialog.tsx +3 -1
  27. package/src/components/agent/scrubber-marks.ts +277 -0
  28. package/src/components/terminal/PermissionPrompt.tsx +3 -0
  29. package/src/components/terminal/QuestionPrompt.tsx +3 -0
  30. package/src/components/terminal/StatusLine.tsx +3 -1
  31. package/src/components/ui/AlertDialog.tsx +23 -20
  32. package/src/components/ui/Dialog.tsx +26 -23
  33. package/src/components/ui/Menu.tsx +20 -17
  34. package/src/components/ui/PortalScope.tsx +27 -0
  35. package/src/components/ui/Select.tsx +19 -16
  36. package/src/components/ui/Tooltip.tsx +13 -10
  37. package/src/styles/scoped.entry.css +16 -0
  38. package/src/styles/terminal.css +1 -1
  39. package/src/styles/theme.css +213 -0
  40. package/build/SessionPanel-DNuPKg5c.mjs.map +0 -1
@@ -117,6 +117,33 @@ const Input = forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("
117
117
  }));
118
118
  Input.displayName = "Input";
119
119
  //#endregion
120
+ //#region src/components/ui/PortalScope.tsx
121
+ /**
122
+ * Re-establishes the `.wd-root` styling scope inside a portal.
123
+ *
124
+ * The scoped stylesheet (`@workerdeck/ui/scoped.css`) rewrites every rule to
125
+ * live under `.wd-root`, so an embedder's page keeps its own design system and
126
+ * the panel keeps this one. Base UI popups (Menu, Dialog, AlertDialog, Select,
127
+ * Tooltip) portal to `document.body` — OUTSIDE the embedder's wrapper — so
128
+ * without this they would render unstyled, or worse, styled by the host.
129
+ *
130
+ * `display: contents` (inline, so no stylesheet needs to load for it) makes the
131
+ * element generate no box: positioning, hit-testing and the popup's own layout
132
+ * are untouched. It still carries the class, which is all the scope needs —
133
+ * design tokens declared on `.wd-root` inherit through it, and every
134
+ * `.wd-root <x>` rule matches below it. Theme keeps working because the dark
135
+ * token block matches `[data-theme='dark']` on ANY ancestor, and `document.body`
136
+ * sits under the host's themed `<html>` just like the panel does.
137
+ *
138
+ * Under the classic `theme.css` integration the class matches no rule and the
139
+ * element is inert, so the wrapper is unconditional.
140
+ */
141
+ const PortalScope = ({ children }) => /* @__PURE__ */ jsx("div", {
142
+ className: "wd-root",
143
+ style: { display: "contents" },
144
+ children
145
+ });
146
+ //#endregion
120
147
  //#region src/components/ui/Select.tsx
121
148
  const Select$1 = Select.Root;
122
149
  const SelectValue = Select.Value;
@@ -130,7 +157,7 @@ const SelectTrigger = ({ className, children, ...props }) => /* @__PURE__ */ jsx
130
157
  children: /* @__PURE__ */ jsx(ChevronsUpDown, { className: "size-3.5" })
131
158
  })]
132
159
  });
133
- const SelectContent = ({ className, align = "start", alignItemWithTrigger = false, side = "bottom", sideOffset = 6, ...props }) => /* @__PURE__ */ jsx(Select.Portal, { children: /* @__PURE__ */ jsx(Select.Positioner, {
160
+ const SelectContent = ({ className, align = "start", alignItemWithTrigger = false, side = "bottom", sideOffset = 6, ...props }) => /* @__PURE__ */ jsx(Select.Portal, { children: /* @__PURE__ */ jsx(PortalScope, { children: /* @__PURE__ */ jsx(Select.Positioner, {
134
161
  align,
135
162
  alignItemWithTrigger,
136
163
  side,
@@ -141,7 +168,7 @@ const SelectContent = ({ className, align = "start", alignItemWithTrigger = fals
141
168
  className: cn("max-h-[min(24rem,var(--available-height))] min-w-[var(--anchor-width)] overflow-y-auto", "rounded-md border border-border bg-surface p-1 text-fg-1 shadow-(--shadow-lg) outline-none", className),
142
169
  ...props
143
170
  })
144
- }) });
171
+ }) }) });
145
172
  const SelectItem = ({ className, children, ...props }) => /* @__PURE__ */ jsxs(Select.Item, {
146
173
  "data-slot": "select-item",
147
174
  className: cn("flex cursor-pointer items-start gap-2 rounded-sm px-2 py-1.5 text-body-sm text-text outline-none select-none", "data-highlighted:bg-surface-hover", className),
@@ -158,7 +185,7 @@ const SelectItem = ({ className, children, ...props }) => /* @__PURE__ */ jsxs(S
158
185
  //#region src/components/ui/Menu.tsx
159
186
  const Menu$1 = Menu.Root;
160
187
  const MenuTrigger = Menu.Trigger;
161
- const MenuContent = ({ className, align = "end", side = "bottom", sideOffset = 6, ...props }) => /* @__PURE__ */ jsx(Menu.Portal, { children: /* @__PURE__ */ jsx(Menu.Positioner, {
188
+ const MenuContent = ({ className, align = "end", side = "bottom", sideOffset = 6, ...props }) => /* @__PURE__ */ jsx(Menu.Portal, { children: /* @__PURE__ */ jsx(PortalScope, { children: /* @__PURE__ */ jsx(Menu.Positioner, {
162
189
  align,
163
190
  side,
164
191
  sideOffset,
@@ -168,7 +195,7 @@ const MenuContent = ({ className, align = "end", side = "bottom", sideOffset = 6
168
195
  className: cn("min-w-48 rounded-md border border-border bg-surface p-1 text-fg-1 shadow-(--shadow-lg) outline-none", "transition-[opacity,transform] duration-(--motion-base)", "data-starting-style:scale-95 data-starting-style:opacity-0", "data-ending-style:scale-95 data-ending-style:opacity-0", className),
169
196
  ...props
170
197
  })
171
- }) });
198
+ }) }) });
172
199
  const MenuItem = ({ className, destructive, ...props }) => /* @__PURE__ */ jsx(Menu.Item, {
173
200
  "data-slot": "menu-item",
174
201
  className: cn("flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-body-sm outline-none select-none", "data-highlighted:bg-surface-hover", destructive ? "text-danger" : "text-text", className),
@@ -190,12 +217,12 @@ const DialogClose = Dialog.Close;
190
217
  * Taller than {@link AlertDialogContent} and scrollable inside, because these
191
218
  * carry lists whose length is the engine's business, not the layout's.
192
219
  */
193
- const DialogContent = ({ className, children, size = "md", ...props }) => /* @__PURE__ */ jsxs(Dialog.Portal, { children: [/* @__PURE__ */ jsx(Dialog.Backdrop, { className: cn("fixed inset-0 z-70 bg-black/40 backdrop-blur-[1px]", "transition-opacity duration-(--motion-base)", "data-starting-style:opacity-0 data-ending-style:opacity-0") }), /* @__PURE__ */ jsx(Dialog.Popup, {
220
+ const DialogContent = ({ className, children, size = "md", ...props }) => /* @__PURE__ */ jsx(Dialog.Portal, { children: /* @__PURE__ */ jsxs(PortalScope, { children: [/* @__PURE__ */ jsx(Dialog.Backdrop, { className: cn("fixed inset-0 z-70 bg-black/40 backdrop-blur-[1px]", "transition-opacity duration-(--motion-base)", "data-starting-style:opacity-0 data-ending-style:opacity-0") }), /* @__PURE__ */ jsx(Dialog.Popup, {
194
221
  "data-slot": "dialog-content",
195
222
  className: cn("fixed top-1/2 left-1/2 z-70 flex max-h-[min(42rem,calc(100dvh-3rem))] -translate-x-1/2 -translate-y-1/2 flex-col", size === "sm" && "w-[min(24rem,calc(100vw-2rem))]", size === "md" && "w-[min(32rem,calc(100vw-2rem))]", size === "lg" && "w-[min(46rem,calc(100vw-2rem))]", "rounded-lg border border-border bg-surface shadow-(--shadow-lg) outline-none", "transition-[opacity,transform] duration-(--motion-base)", "data-starting-style:scale-95 data-starting-style:opacity-0", "data-ending-style:scale-95 data-ending-style:opacity-0", className),
196
223
  ...props,
197
224
  children
198
- })] });
225
+ })] }) });
199
226
  /** Title row with the close button, pinned above the scrolling body. */
200
227
  const DialogHeader = ({ title, description, actions }) => /* @__PURE__ */ jsxs("div", {
201
228
  className: "flex items-start gap-2 border-b border-border px-4 py-3",
@@ -237,7 +264,7 @@ const DialogRow = ({ label, children, mono }) => /* @__PURE__ */ jsxs("div", {
237
264
  //#endregion
238
265
  //#region src/components/ui/Tooltip.tsx
239
266
  const TooltipProvider = Tooltip.Provider;
240
- const TooltipContent = ({ className, side = "top", sideOffset = 6, ...props }) => /* @__PURE__ */ jsx(Tooltip.Portal, { children: /* @__PURE__ */ jsx(Tooltip.Positioner, {
267
+ const TooltipContent = ({ className, side = "top", sideOffset = 6, ...props }) => /* @__PURE__ */ jsx(Tooltip.Portal, { children: /* @__PURE__ */ jsx(PortalScope, { children: /* @__PURE__ */ jsx(Tooltip.Positioner, {
241
268
  side,
242
269
  sideOffset,
243
270
  className: "isolate z-90",
@@ -246,7 +273,7 @@ const TooltipContent = ({ className, side = "top", sideOffset = 6, ...props }) =
246
273
  className: cn("rounded-md border border-border bg-surface px-2 py-1 text-label text-fg-2 shadow-(--shadow-md) outline-none", className),
247
274
  ...props
248
275
  })
249
- }) });
276
+ }) }) });
250
277
  /**
251
278
  * Convenience wrapper: `<Tip content="..."><Button/></Tip>`.
252
279
  *
@@ -6281,7 +6308,7 @@ function Composer({ onSend, onInterrupt, busy, disabled, placeholder = "Message
6281
6308
  }
6282
6309
  };
6283
6310
  const errorRow = attachments?.error ? /* @__PURE__ */ jsxs("div", {
6284
- className: cn("mx-auto mt-1 flex w-full max-w-[var(--wd-content-max-w,48rem)] items-center gap-2 text-label text-danger"),
6311
+ className: cn("mx-auto mt-1 flex w-full max-w-[var(--wd-transcript-max-width)] items-center gap-2 text-label text-danger"),
6285
6312
  children: [
6286
6313
  /* @__PURE__ */ jsx(TriangleAlert, { className: "size-3 shrink-0" }),
6287
6314
  /* @__PURE__ */ jsx("span", {
@@ -6354,10 +6381,10 @@ function Composer({ onSend, onInterrupt, busy, disabled, placeholder = "Message
6354
6381
  }
6355
6382
  return /* @__PURE__ */ jsxs("div", {
6356
6383
  "data-slot": "composer",
6357
- className: cn("px-3 pb-3", className),
6384
+ className: cn("px-[var(--wd-composer-padding)] pb-[var(--wd-composer-padding)]", className),
6358
6385
  children: [/* @__PURE__ */ jsxs("div", {
6359
6386
  ...dropHandlers,
6360
- className: cn("mx-auto w-full max-w-[var(--wd-content-max-w,48rem)] overflow-hidden border border-border bg-bg", "transition-colors rounded-lg shadow-(--shadow-xs)", "focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/30", dragging && "border-ring ring-2 ring-ring/30", disabled && "opacity-60"),
6387
+ className: cn("mx-auto w-full max-w-[var(--wd-transcript-max-width)] overflow-hidden border border-border bg-bg", "transition-colors rounded-lg shadow-(--shadow-xs)", "focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/30", dragging && "border-ring ring-2 ring-ring/30", disabled && "opacity-60"),
6361
6388
  children: [staged.length > 0 && attachments ? /* @__PURE__ */ jsx(AttachmentStrip, { attachments }) : null, inline ? /* @__PURE__ */ jsxs("div", {
6362
6389
  className: "flex items-end gap-1 p-1",
6363
6390
  children: [
@@ -6372,7 +6399,7 @@ function Composer({ onSend, onInterrupt, busy, disabled, placeholder = "Message
6372
6399
  minHeight: 20,
6373
6400
  maxHeight: 192,
6374
6401
  "aria-label": "Message the agent",
6375
- className: "min-w-0 flex-1 py-0.5 text-body-sm text-text",
6402
+ className: "min-w-0 flex-1 py-1 text-body-sm text-text",
6376
6403
  onImagePaste: (file) => attachments?.add([file])
6377
6404
  }),
6378
6405
  submitButton
@@ -6387,7 +6414,7 @@ function Composer({ onSend, onInterrupt, busy, disabled, placeholder = "Message
6387
6414
  minHeight: 28,
6388
6415
  maxHeight: 192,
6389
6416
  "aria-label": "Message the agent",
6390
- className: "px-3 pt-2.5 pb-1 text-body-sm text-text",
6417
+ className: "px-3 pt-2.5 pb-0 text-body-sm text-text",
6391
6418
  onImagePaste: (file) => attachments?.add([file])
6392
6419
  }), /* @__PURE__ */ jsxs("div", {
6393
6420
  className: "flex items-center justify-between gap-2 px-2 pb-2",
@@ -6494,81 +6521,84 @@ const usageTint$1 = (pct) => pct >= 90 ? "bg-danger" : pct >= 70 ? "bg-warning"
6494
6521
  * are different questions asked at different moments, so they are different
6495
6522
  * screens rather than one "details" list you scroll past two answers to reach.
6496
6523
  */
6497
- function ContextDialog({ usage, open, onOpenChange }) {
6524
+ function ContextDialog({ usage, open, onOpenChange, className }) {
6498
6525
  return /* @__PURE__ */ jsx(Dialog$1, {
6499
6526
  open,
6500
6527
  onOpenChange,
6501
- children: /* @__PURE__ */ jsxs(DialogContent, { children: [/* @__PURE__ */ jsx(DialogHeader, {
6502
- title: "Context",
6503
- description: usage?.model
6504
- }), /* @__PURE__ */ jsx(DialogBody, { children: !usage ? /* @__PURE__ */ jsx("p", {
6505
- className: "py-6 text-center text-body-sm text-fg-4",
6506
- children: "No reading yet the context window is measured after a turn completes."
6507
- }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
6508
- /* @__PURE__ */ jsxs("div", {
6509
- className: "flex items-baseline justify-between gap-4",
6510
- children: [/* @__PURE__ */ jsx("span", {
6511
- className: "text-label text-fg-3",
6512
- children: "Used"
6513
- }), /* @__PURE__ */ jsxs("span", {
6514
- className: "font-mono text-body-sm text-fg-1",
6515
- children: [
6516
- formatTokens(usage.totalTokens),
6517
- " / ",
6518
- formatTokens(usage.maxTokens),
6519
- " ·",
6520
- " ",
6521
- usage.percentage.toFixed(0),
6522
- "%"
6523
- ]
6524
- })]
6525
- }),
6526
- /* @__PURE__ */ jsx("div", {
6527
- className: "mt-2 h-2 overflow-hidden rounded-full bg-border",
6528
- children: /* @__PURE__ */ jsx("div", {
6529
- className: cn("h-full rounded-full", usageTint$1(usage.percentage)),
6530
- style: { width: `${Math.min(100, Math.max(2, usage.percentage))}%` }
6531
- })
6532
- }),
6533
- usage.categories.length > 0 ? /* @__PURE__ */ jsxs("div", {
6534
- className: "mt-5",
6535
- children: [/* @__PURE__ */ jsx("h3", {
6536
- className: "text-label font-medium text-fg-3",
6537
- children: "Breakdown"
6538
- }), /* @__PURE__ */ jsx("div", {
6539
- className: "mt-2 flex flex-col gap-3",
6540
- children: usage.categories.map((category) => {
6541
- const share = category.tokens / Math.max(usage.maxTokens, 1) * 100;
6542
- const color = cssColor$1(category.color);
6543
- return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
6544
- className: "flex items-baseline justify-between gap-3",
6545
- children: [/* @__PURE__ */ jsxs("span", {
6546
- className: "flex min-w-0 items-center gap-2",
6547
- children: [/* @__PURE__ */ jsx("span", {
6548
- className: "size-2 shrink-0 rounded-full bg-fg-4",
6549
- style: color ? { backgroundColor: color } : void 0
6528
+ children: /* @__PURE__ */ jsxs(DialogContent, {
6529
+ className,
6530
+ children: [/* @__PURE__ */ jsx(DialogHeader, {
6531
+ title: "Context",
6532
+ description: usage?.model
6533
+ }), /* @__PURE__ */ jsx(DialogBody, { children: !usage ? /* @__PURE__ */ jsx("p", {
6534
+ className: "py-6 text-center text-body-sm text-fg-4",
6535
+ children: "No reading yet — the context window is measured after a turn completes."
6536
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
6537
+ /* @__PURE__ */ jsxs("div", {
6538
+ className: "flex items-baseline justify-between gap-4",
6539
+ children: [/* @__PURE__ */ jsx("span", {
6540
+ className: "text-label text-fg-3",
6541
+ children: "Used"
6542
+ }), /* @__PURE__ */ jsxs("span", {
6543
+ className: "font-mono text-body-sm text-fg-1",
6544
+ children: [
6545
+ formatTokens(usage.totalTokens),
6546
+ " / ",
6547
+ formatTokens(usage.maxTokens),
6548
+ " ·",
6549
+ " ",
6550
+ usage.percentage.toFixed(0),
6551
+ "%"
6552
+ ]
6553
+ })]
6554
+ }),
6555
+ /* @__PURE__ */ jsx("div", {
6556
+ className: "mt-2 h-2 overflow-hidden rounded-full bg-border",
6557
+ children: /* @__PURE__ */ jsx("div", {
6558
+ className: cn("h-full rounded-full", usageTint$1(usage.percentage)),
6559
+ style: { width: `${Math.min(100, Math.max(2, usage.percentage))}%` }
6560
+ })
6561
+ }),
6562
+ usage.categories.length > 0 ? /* @__PURE__ */ jsxs("div", {
6563
+ className: "mt-5",
6564
+ children: [/* @__PURE__ */ jsx("h3", {
6565
+ className: "text-label font-medium text-fg-3",
6566
+ children: "Breakdown"
6567
+ }), /* @__PURE__ */ jsx("div", {
6568
+ className: "mt-2 flex flex-col gap-3",
6569
+ children: usage.categories.map((category) => {
6570
+ const share = category.tokens / Math.max(usage.maxTokens, 1) * 100;
6571
+ const color = cssColor$1(category.color);
6572
+ return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
6573
+ className: "flex items-baseline justify-between gap-3",
6574
+ children: [/* @__PURE__ */ jsxs("span", {
6575
+ className: "flex min-w-0 items-center gap-2",
6576
+ children: [/* @__PURE__ */ jsx("span", {
6577
+ className: "size-2 shrink-0 rounded-full bg-fg-4",
6578
+ style: color ? { backgroundColor: color } : void 0
6579
+ }), /* @__PURE__ */ jsx("span", {
6580
+ className: "truncate text-body-sm text-fg-1",
6581
+ children: category.name
6582
+ })]
6550
6583
  }), /* @__PURE__ */ jsx("span", {
6551
- className: "truncate text-body-sm text-fg-1",
6552
- children: category.name
6584
+ className: "shrink-0 font-mono text-label text-fg-3",
6585
+ children: formatTokens(category.tokens)
6553
6586
  })]
6554
- }), /* @__PURE__ */ jsx("span", {
6555
- className: "shrink-0 font-mono text-label text-fg-3",
6556
- children: formatTokens(category.tokens)
6557
- })]
6558
- }), /* @__PURE__ */ jsx("div", {
6559
- className: "mt-1 h-1 overflow-hidden rounded-full bg-border",
6560
- children: /* @__PURE__ */ jsx("div", {
6561
- className: "h-full rounded-full bg-fg-4",
6562
- style: {
6563
- width: `${Math.min(100, share)}%`,
6564
- backgroundColor: color
6565
- }
6566
- })
6567
- })] }, category.name);
6568
- })
6569
- })]
6570
- }) : null
6571
- ] }) })] })
6587
+ }), /* @__PURE__ */ jsx("div", {
6588
+ className: "mt-1 h-1 overflow-hidden rounded-full bg-border",
6589
+ children: /* @__PURE__ */ jsx("div", {
6590
+ className: "h-full rounded-full bg-fg-4",
6591
+ style: {
6592
+ width: `${Math.min(100, share)}%`,
6593
+ backgroundColor: color
6594
+ }
6595
+ })
6596
+ })] }, category.name);
6597
+ })
6598
+ })]
6599
+ }) : null
6600
+ ] }) })]
6601
+ })
6572
6602
  });
6573
6603
  }
6574
6604
  //#endregion
@@ -6765,7 +6795,7 @@ const STATUS_VARIANT = {
6765
6795
  * the tool view either renders it or says why it can't, rather than leaving a
6766
6796
  * silent gap or claiming the absence is universal.
6767
6797
  */
6768
- function McpDialog({ client, sessionId, open, onOpenChange, canManageServers = true }) {
6798
+ function McpDialog({ client, sessionId, open, onOpenChange, canManageServers = true, className }) {
6769
6799
  const [servers, setServers] = useState();
6770
6800
  const [error, setError] = useState();
6771
6801
  const [loading, setLoading] = useState(false);
@@ -6807,35 +6837,38 @@ function McpDialog({ client, sessionId, open, onOpenChange, canManageServers = t
6807
6837
  return /* @__PURE__ */ jsx(Dialog$1, {
6808
6838
  open,
6809
6839
  onOpenChange,
6810
- children: /* @__PURE__ */ jsxs(DialogContent, { children: [/* @__PURE__ */ jsx(DialogHeader, {
6811
- title: tool?.name ?? server?.name ?? "MCP servers",
6812
- description: tool ? `${server?.name} tool` : server ? server.serverInfo?.name : void 0,
6813
- actions: selectedServer ? /* @__PURE__ */ jsxs(Button, {
6814
- variant: "ghost",
6815
- size: "xs",
6816
- onClick: () => selectedTool ? setSelectedTool(void 0) : setSelectedServer(void 0),
6817
- children: [/* @__PURE__ */ jsx(ChevronLeft, { className: "size-3.5" }), "Back"]
6818
- }) : /* @__PURE__ */ jsxs(Button, {
6819
- variant: "ghost",
6820
- size: "xs",
6821
- onClick: () => void load(),
6822
- disabled: loading,
6823
- children: [loading ? /* @__PURE__ */ jsx(Spinner, { className: "size-3 text-current" }) : /* @__PURE__ */ jsx(RotateCw, { className: "size-3" }), "Refresh"]
6824
- })
6825
- }), /* @__PURE__ */ jsxs(DialogBody, { children: [error ? /* @__PURE__ */ jsx("div", {
6826
- className: "mb-3 rounded-md bg-danger-bg px-3 py-2 text-body-sm text-danger",
6827
- children: error
6828
- }) : null, tool ? /* @__PURE__ */ jsx(ToolView, { tool }) : server ? /* @__PURE__ */ jsx(ServerView, {
6829
- server,
6830
- busy: busyServer === server.name,
6831
- canManage: canManageServers,
6832
- onAct: (action) => void act(server.name, action),
6833
- onSelectTool: setSelectedTool
6834
- }) : error && !servers ? null : /* @__PURE__ */ jsx(ServerList, {
6835
- servers,
6836
- loading,
6837
- onSelect: setSelectedServer
6838
- })] })] })
6840
+ children: /* @__PURE__ */ jsxs(DialogContent, {
6841
+ className,
6842
+ children: [/* @__PURE__ */ jsx(DialogHeader, {
6843
+ title: tool?.name ?? server?.name ?? "MCP servers",
6844
+ description: tool ? `${server?.name} tool` : server ? server.serverInfo?.name : void 0,
6845
+ actions: selectedServer ? /* @__PURE__ */ jsxs(Button, {
6846
+ variant: "ghost",
6847
+ size: "xs",
6848
+ onClick: () => selectedTool ? setSelectedTool(void 0) : setSelectedServer(void 0),
6849
+ children: [/* @__PURE__ */ jsx(ChevronLeft, { className: "size-3.5" }), "Back"]
6850
+ }) : /* @__PURE__ */ jsxs(Button, {
6851
+ variant: "ghost",
6852
+ size: "xs",
6853
+ onClick: () => void load(),
6854
+ disabled: loading,
6855
+ children: [loading ? /* @__PURE__ */ jsx(Spinner, { className: "size-3 text-current" }) : /* @__PURE__ */ jsx(RotateCw, { className: "size-3" }), "Refresh"]
6856
+ })
6857
+ }), /* @__PURE__ */ jsxs(DialogBody, { children: [error ? /* @__PURE__ */ jsx("div", {
6858
+ className: "mb-3 rounded-md bg-danger-bg px-3 py-2 text-body-sm text-danger",
6859
+ children: error
6860
+ }) : null, tool ? /* @__PURE__ */ jsx(ToolView, { tool }) : server ? /* @__PURE__ */ jsx(ServerView, {
6861
+ server,
6862
+ busy: busyServer === server.name,
6863
+ canManage: canManageServers,
6864
+ onAct: (action) => void act(server.name, action),
6865
+ onSelectTool: setSelectedTool
6866
+ }) : error && !servers ? null : /* @__PURE__ */ jsx(ServerList, {
6867
+ servers,
6868
+ loading,
6869
+ onSelect: setSelectedServer
6870
+ })] })]
6871
+ })
6839
6872
  });
6840
6873
  }
6841
6874
  function ServerList({ servers, loading, onSelect }) {
@@ -7035,7 +7068,7 @@ const SCOPE_LABEL = {
7035
7068
  * Fed from the session's `skills` event rather than a REST route, because that
7036
7069
  * is the channel the engine refreshes on its own when a skill changes on disk.
7037
7070
  */
7038
- function SkillsDialog({ skills, open, onOpenChange, onUse }) {
7071
+ function SkillsDialog({ skills, open, onOpenChange, onUse, className }) {
7039
7072
  const [selected, setSelected] = useState();
7040
7073
  const skill = skills?.find((s) => s.name === selected);
7041
7074
  return /* @__PURE__ */ jsx(Dialog$1, {
@@ -7044,23 +7077,26 @@ function SkillsDialog({ skills, open, onOpenChange, onUse }) {
7044
7077
  if (!next) setSelected(void 0);
7045
7078
  onOpenChange(next);
7046
7079
  },
7047
- children: /* @__PURE__ */ jsxs(DialogContent, { children: [/* @__PURE__ */ jsx(DialogHeader, {
7048
- title: skill ? skill.displayName ?? skill.name : "Skills",
7049
- description: skill ? skill.name !== (skill.displayName ?? skill.name) ? skill.name : void 0 : "Capabilities the agent can choose to use. Not commands — the model picks them from their descriptions.",
7050
- actions: skill ? /* @__PURE__ */ jsxs(Button, {
7051
- variant: "ghost",
7052
- size: "xs",
7053
- onClick: () => setSelected(void 0),
7054
- children: [/* @__PURE__ */ jsx(ChevronLeft, { className: "size-3.5" }), "Back"]
7055
- }) : void 0
7056
- }), /* @__PURE__ */ jsx(DialogBody, { children: skill ? /* @__PURE__ */ jsx(SkillView, {
7057
- skill,
7058
- onUse,
7059
- onUsed: () => onOpenChange(false)
7060
- }) : /* @__PURE__ */ jsx(SkillList, {
7061
- skills,
7062
- onSelect: setSelected
7063
- }) })] })
7080
+ children: /* @__PURE__ */ jsxs(DialogContent, {
7081
+ className,
7082
+ children: [/* @__PURE__ */ jsx(DialogHeader, {
7083
+ title: skill ? skill.displayName ?? skill.name : "Skills",
7084
+ description: skill ? skill.name !== (skill.displayName ?? skill.name) ? skill.name : void 0 : "Capabilities the agent can choose to use. Not commands — the model picks them from their descriptions.",
7085
+ actions: skill ? /* @__PURE__ */ jsxs(Button, {
7086
+ variant: "ghost",
7087
+ size: "xs",
7088
+ onClick: () => setSelected(void 0),
7089
+ children: [/* @__PURE__ */ jsx(ChevronLeft, { className: "size-3.5" }), "Back"]
7090
+ }) : void 0
7091
+ }), /* @__PURE__ */ jsx(DialogBody, { children: skill ? /* @__PURE__ */ jsx(SkillView, {
7092
+ skill,
7093
+ onUse,
7094
+ onUsed: () => onOpenChange(false)
7095
+ }) : /* @__PURE__ */ jsx(SkillList, {
7096
+ skills,
7097
+ onSelect: setSelected
7098
+ }) })]
7099
+ })
7064
7100
  });
7065
7101
  }
7066
7102
  function SkillList({ skills, onSelect }) {
@@ -7940,7 +7976,7 @@ function TabStrip({ tabs, active, onSelect }) {
7940
7976
  }
7941
7977
  //#endregion
7942
7978
  //#region src/components/terminal/PermissionPrompt.tsx
7943
- function TerminalPermissionPrompt({ request, onApprove, onDeny }) {
7979
+ function TerminalPermissionPrompt({ request, onApprove, onDeny, className }) {
7944
7980
  const [focused, setFocused] = useState(0);
7945
7981
  const [denying, setDenying] = useState(false);
7946
7982
  const [reason, setReason] = useState("");
@@ -7978,6 +8014,7 @@ function TerminalPermissionPrompt({ request, onApprove, onDeny }) {
7978
8014
  ];
7979
8015
  return /* @__PURE__ */ jsxs("div", {
7980
8016
  "data-slot": "permission-prompt",
8017
+ className,
7981
8018
  onKeyDown: (event) => {
7982
8019
  if (event.key === "Escape") {
7983
8020
  event.preventDefault();
@@ -8035,7 +8072,7 @@ function answerFor(selection) {
8035
8072
  if (selection.otherActive && selection.other.trim()) parts.push(selection.other.trim());
8036
8073
  return parts.join(", ");
8037
8074
  }
8038
- function TerminalQuestionPrompt({ request, onAnswer, onDismiss }) {
8075
+ function TerminalQuestionPrompt({ request, onAnswer, onDismiss, className }) {
8039
8076
  const questions = parseUserQuestions(request.input);
8040
8077
  const [selections, setSelections] = useState(() => questions.map(() => EMPTY));
8041
8078
  const [cursors, setCursors] = useState(() => questions.map(() => 0));
@@ -8077,6 +8114,7 @@ function TerminalQuestionPrompt({ request, onAnswer, onDismiss }) {
8077
8114
  const move = (delta) => setTab((current) => Math.min(questions.length, Math.max(0, current + delta)));
8078
8115
  return /* @__PURE__ */ jsxs("div", {
8079
8116
  "data-slot": "question-prompt",
8117
+ className,
8080
8118
  onKeyDown: (event) => {
8081
8119
  if (event.key === "Escape") {
8082
8120
  event.preventDefault();
@@ -8576,7 +8614,7 @@ function StatusBar({ state, rateLimits, connected, connection, onOpenStatus, onO
8576
8614
  const terminal = useTranscriptVariant() === "terminal";
8577
8615
  return /* @__PURE__ */ jsxs("div", {
8578
8616
  "data-slot": "status-bar",
8579
- className: cn("flex h-[38px] items-baseline gap-2 border-border bg-surface p-1.5", placement === "bottom" ? terminal ? void 0 : "border-t" : "border-b", className),
8617
+ className: cn("flex h-[var(--wd-status-bar-height)] items-baseline gap-2 border-border bg-surface p-1.5", placement === "bottom" ? terminal ? void 0 : "border-t" : "border-b", className),
8580
8618
  children: [
8581
8619
  /* @__PURE__ */ jsx(Slot, {
8582
8620
  onClick: onOpenStatus,
@@ -8614,13 +8652,19 @@ function StatusBar({ state, rateLimits, connected, connection, onOpenStatus, onO
8614
8652
  }) : null]
8615
8653
  })
8616
8654
  }) : null,
8617
- controls,
8655
+ controls ? /* @__PURE__ */ jsx("span", {
8656
+ className: "self-center",
8657
+ children: controls
8658
+ }) : null,
8618
8659
  /* @__PURE__ */ jsx("span", { className: "flex-1" }),
8619
8660
  /* @__PURE__ */ jsx("span", {
8620
8661
  className: "font-mono text-label text-fg-3",
8621
8662
  children: formatCost(state.totalCostUsd)
8622
8663
  }),
8623
- actions
8664
+ actions ? /* @__PURE__ */ jsx("span", {
8665
+ className: "self-center",
8666
+ children: actions
8667
+ }) : null
8624
8668
  ]
8625
8669
  });
8626
8670
  }
@@ -8650,7 +8694,7 @@ function Conversation({ className, children, ...props }) {
8650
8694
  function ConversationContent({ className, children, ...props }) {
8651
8695
  return /* @__PURE__ */ jsx(StickToBottom.Content, {
8652
8696
  "data-slot": "conversation-content",
8653
- className: cn("mx-auto flex w-full max-w-[var(--wd-content-max-w,48rem)] flex-col gap-4 px-4 py-4", className),
8697
+ className: cn("mx-auto flex w-full max-w-[var(--wd-transcript-max-width)] flex-col gap-4 px-4 py-4", className),
8654
8698
  ...props,
8655
8699
  children
8656
8700
  });
@@ -8801,6 +8845,7 @@ const Response = memo(function Response({ children, streaming, className }) {
8801
8845
  mode: streaming ? "streaming" : "static",
8802
8846
  parseIncompleteMarkdown: streaming,
8803
8847
  shikiTheme: ["github-light", "github-dark"],
8848
+ linkSafety: { enabled: false },
8804
8849
  className: cn("size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0", className),
8805
8850
  children
8806
8851
  });
@@ -9845,7 +9890,7 @@ function blockHeight(block, m) {
9845
9890
  //#region src/components/terminal/scrubber.tsx
9846
9891
  /** The member closest to a rail-space y — what a press or peek on a merged
9847
9892
  * cluster resolves to. */
9848
- function nearestMember(cluster, y) {
9893
+ function nearestMember$1(cluster, y) {
9849
9894
  let best;
9850
9895
  for (const member of cluster.marks) if (!best || Math.abs(member.y - y) < Math.abs(best.y - y)) best = member;
9851
9896
  return best?.mark;
@@ -9865,7 +9910,7 @@ function nearestMember(cluster, y) {
9865
9910
  * It also buys the marks their width back: three lanes in a 16px rail is 5px a
9866
9911
  * lane, which is a hard target to hit and a hard colour to see.
9867
9912
  */
9868
- const LANE = {
9913
+ const LANE$1 = {
9869
9914
  user: "l",
9870
9915
  subagent: "l",
9871
9916
  turn: "r",
@@ -9877,7 +9922,7 @@ const LANE = {
9877
9922
  bookmark: "f"
9878
9923
  };
9879
9924
  /** Who wins the colour when marks merge. */
9880
- const LOUDNESS = {
9925
+ const LOUDNESS$1 = {
9881
9926
  approval: 7,
9882
9927
  error: 6,
9883
9928
  turnFailed: 5,
@@ -9888,7 +9933,7 @@ const LOUDNESS = {
9888
9933
  subagent: 1,
9889
9934
  recap: 0
9890
9935
  };
9891
- const KIND_NAME = {
9936
+ const KIND_NAME$1 = {
9892
9937
  user: "you",
9893
9938
  subagent: "sub-agent",
9894
9939
  turn: "response · turn end",
@@ -9899,7 +9944,7 @@ const KIND_NAME = {
9899
9944
  recap: "catch-up boundary",
9900
9945
  bookmark: "bookmark"
9901
9946
  };
9902
- const MIN_MARK = 2;
9947
+ const MIN_MARK$1 = 2;
9903
9948
  /**
9904
9949
  * Rail pixels per content pixel — the one scale both the marks and the viewport
9905
9950
  * band are drawn at, so they cannot disagree about where a row sits.
@@ -9923,15 +9968,15 @@ const MIN_MARK = 2;
9923
9968
  function railScale(railH, totalSize, viewportH) {
9924
9969
  return totalSize > 0 ? railH / Math.max(totalSize, viewportH) : 0;
9925
9970
  }
9926
- const doneLine = (turn) => `${turn.isError ? turn.subtype : "done"} · ${formatDuration(turn.durationMs)} · ${formatCost(turn.totalCostUsd)}`;
9927
- function excerpt(item) {
9971
+ const doneLine$1 = (turn) => `${turn.isError ? turn.subtype : "done"} · ${formatDuration(turn.durationMs)} · ${formatCost(turn.totalCostUsd)}`;
9972
+ function excerpt$1(item) {
9928
9973
  switch (item.kind) {
9929
9974
  case "user":
9930
9975
  case "assistant_text":
9931
9976
  case "thinking":
9932
9977
  case "notice": return item.text;
9933
9978
  case "tool_call": return `${item.name}(${toolInputPreview(item.input)})`;
9934
- case "turn_result": return doneLine(item);
9979
+ case "turn_result": return doneLine$1(item);
9935
9980
  case "file_delivered": return item.path;
9936
9981
  default: return "";
9937
9982
  }
@@ -10023,9 +10068,9 @@ function buildClusters(props, railH) {
10023
10068
  for (const mark of marks) {
10024
10069
  const within = mark.itemIndex >= 0 ? positionInRow?.(mark.itemIndex) : void 0;
10025
10070
  const rowH = sizeOfRow(mark.rowIndex);
10026
- const h = within ? MIN_MARK : Math.max(MIN_MARK, Math.round(rowH * scale));
10071
+ const h = within ? MIN_MARK$1 : Math.max(MIN_MARK$1, Math.round(rowH * scale));
10027
10072
  const y = Math.min(Math.max(0, railH - h), Math.round((offsetOfRow(mark.rowIndex) + (within ? within.ordinal / within.count * rowH : 0)) * scale));
10028
- const lane = LANE[mark.kind];
10073
+ const lane = LANE$1[mark.kind];
10029
10074
  const list = lanes.get(lane) ?? [];
10030
10075
  list.push({
10031
10076
  mark,
@@ -10040,7 +10085,7 @@ function buildClusters(props, railH) {
10040
10085
  let current = null;
10041
10086
  for (const { mark, y, h } of list) if (current && y <= current.y + current.h + 1) {
10042
10087
  current.h = Math.max(current.h, y + h - current.y);
10043
- if (LOUDNESS[mark.kind] > LOUDNESS[current.kind]) current.kind = mark.kind;
10088
+ if (LOUDNESS$1[mark.kind] > LOUDNESS$1[current.kind]) current.kind = mark.kind;
10044
10089
  current.marks.push({
10045
10090
  mark,
10046
10091
  y
@@ -10060,15 +10105,15 @@ function buildClusters(props, railH) {
10060
10105
  }
10061
10106
  }
10062
10107
  if (pendingApprovals.length > 0) clusters.push({
10063
- lane: LANE.approval,
10108
+ lane: LANE$1.approval,
10064
10109
  kind: "approval",
10065
- y: Math.max(0, railH - MIN_MARK),
10066
- h: MIN_MARK,
10110
+ y: Math.max(0, railH - MIN_MARK$1),
10111
+ h: MIN_MARK$1,
10067
10112
  marks: []
10068
10113
  });
10069
10114
  return clusters;
10070
10115
  }
10071
- function peekContent(cluster, first, { items, pendingApprovals, recapRow }) {
10116
+ function peekContent$1(cluster, first, { items, pendingApprovals, recapRow }) {
10072
10117
  const more = cluster.marks.length > 1 ? ` · ${cluster.marks.length} marks` : "";
10073
10118
  let body = null;
10074
10119
  if (cluster.kind === "approval") {
@@ -10102,7 +10147,7 @@ function peekContent(cluster, first, { items, pendingApprovals, recapRow }) {
10102
10147
  }), item.text]
10103
10148
  }) : null, turn?.kind === "turn_result" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
10104
10149
  "data-tone": turn.isError ? "red" : "faint",
10105
- children: doneLine(turn)
10150
+ children: doneLine$1(turn)
10106
10151
  }), turn.errors?.map((message, index) => /* @__PURE__ */ jsx("div", {
10107
10152
  "data-tone": "red",
10108
10153
  children: message
@@ -10115,7 +10160,7 @@ function peekContent(cluster, first, { items, pendingApprovals, recapRow }) {
10115
10160
  children: [first.kind === "user" ? /* @__PURE__ */ jsx("span", {
10116
10161
  "data-tone": "dim",
10117
10162
  children: "❯ "
10118
- }) : null, excerpt(item)]
10163
+ }) : null, excerpt$1(item)]
10119
10164
  }), failure ? /* @__PURE__ */ jsx("div", {
10120
10165
  className: "term-scrub-ex",
10121
10166
  "data-tone": "red",
@@ -10125,7 +10170,7 @@ function peekContent(cluster, first, { items, pendingApprovals, recapRow }) {
10125
10170
  }
10126
10171
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
10127
10172
  "data-tone": "faint",
10128
- children: [KIND_NAME[first?.kind ?? cluster.kind], more]
10173
+ children: [KIND_NAME$1[first?.kind ?? cluster.kind], more]
10129
10174
  }), body] });
10130
10175
  }
10131
10176
  function TerminalScrubber(props) {
@@ -10202,12 +10247,12 @@ function TerminalScrubber(props) {
10202
10247
  stick.scrollToBottom();
10203
10248
  return;
10204
10249
  }
10205
- const mark = nearestMember(cluster, railY(clientY));
10250
+ const mark = nearestMember$1(cluster, railY(clientY));
10206
10251
  if (mark) onJumpToRow(mark.rowIndex);
10207
10252
  };
10208
10253
  const showPeek = (cluster, clientY) => {
10209
10254
  const y = railY(clientY);
10210
- const mark = nearestMember(cluster, y);
10255
+ const mark = nearestMember$1(cluster, y);
10211
10256
  setPeek((previous) => previous && previous.cluster === cluster && previous.mark === mark ? previous : {
10212
10257
  cluster,
10213
10258
  mark,
@@ -10288,13 +10333,328 @@ function TerminalScrubber(props) {
10288
10333
  ref: peekRef,
10289
10334
  className: "term-scrub-peek",
10290
10335
  style: { top: peek.y },
10291
- children: peekContent(peek.cluster, peek.mark, props)
10336
+ children: peekContent$1(peek.cluster, peek.mark, props)
10292
10337
  }) : null
10293
10338
  ]
10294
10339
  })
10295
10340
  });
10296
10341
  }
10297
10342
  //#endregion
10343
+ //#region src/components/agent/scrubber-marks.ts
10344
+ /** The member closest to a rail-space y — what a press or peek on a merged
10345
+ * cluster resolves to. */
10346
+ function nearestMember(cluster, y) {
10347
+ let best;
10348
+ for (const member of cluster.marks) if (!best || Math.abs(member.y - y) < Math.abs(best.y - y)) best = member;
10349
+ return best?.mark;
10350
+ }
10351
+ /** The two lanes are channels, not classes: left is what went *in* (your
10352
+ * prompts, the sub-agents you dispatched), right is what came *out* (each
10353
+ * turn's answer, and everything that went wrong producing one). Full width is
10354
+ * for what is not a channel at all: the waiting approval, a bookmark, the
10355
+ * catch-up seam. See the terminal scrubber for the full argument. */
10356
+ const LANE = {
10357
+ user: "l",
10358
+ subagent: "l",
10359
+ turn: "r",
10360
+ turnFailed: "r",
10361
+ toolFailed: "r",
10362
+ error: "r",
10363
+ approval: "f",
10364
+ recap: "f",
10365
+ bookmark: "f"
10366
+ };
10367
+ /** Who wins the colour when marks merge. */
10368
+ const LOUDNESS = {
10369
+ approval: 7,
10370
+ error: 6,
10371
+ turnFailed: 5,
10372
+ toolFailed: 4,
10373
+ user: 3,
10374
+ turn: 2,
10375
+ bookmark: 1,
10376
+ subagent: 1,
10377
+ recap: 0
10378
+ };
10379
+ const KIND_NAME = {
10380
+ user: "you",
10381
+ subagent: "sub-agent",
10382
+ turn: "response · turn end",
10383
+ turnFailed: "turn failed",
10384
+ toolFailed: "tool failed",
10385
+ error: "error",
10386
+ approval: "pending approval",
10387
+ recap: "catch-up boundary",
10388
+ bookmark: "bookmark"
10389
+ };
10390
+ const doneLine = (turn) => `${turn.isError ? turn.subtype : "done"} · ${formatDuration(turn.durationMs)} · ${formatCost(turn.totalCostUsd)}`;
10391
+ function excerpt(item) {
10392
+ switch (item.kind) {
10393
+ case "user":
10394
+ case "assistant_text":
10395
+ case "thinking":
10396
+ case "notice": return item.text;
10397
+ case "tool_call": return `${item.name}(${toolInputPreview(item.input)})`;
10398
+ case "turn_result": return doneLine(item);
10399
+ case "file_delivered": return item.path;
10400
+ default: return "";
10401
+ }
10402
+ }
10403
+ function buildMarks(items, { frameParentId, bookmarks = [], recapItemIndex } = {}) {
10404
+ const marks = [];
10405
+ const subagentParents = /* @__PURE__ */ new Set();
10406
+ for (const item of items) {
10407
+ const parent = parentOf(item);
10408
+ if (parent !== void 0) subagentParents.add(parent);
10409
+ }
10410
+ let segment = {};
10411
+ const closeSegment = () => {
10412
+ const anchor = segment.response ?? segment.turn;
10413
+ if (anchor !== void 0) marks.push({
10414
+ kind: segment.failed ? "turnFailed" : "turn",
10415
+ itemIndex: anchor,
10416
+ turnIndex: segment.turn
10417
+ });
10418
+ segment = {};
10419
+ };
10420
+ items.forEach((item, index) => {
10421
+ if (item.kind === "tool_call" && subagentParents.has(item.id)) marks.push({
10422
+ kind: "subagent",
10423
+ itemIndex: index
10424
+ });
10425
+ if (item.kind === "user" && parentOf(item) === frameParentId) {
10426
+ closeSegment();
10427
+ marks.push({
10428
+ kind: "user",
10429
+ itemIndex: index
10430
+ });
10431
+ } else if (item.kind === "turn_result") {
10432
+ segment.turn = index;
10433
+ segment.failed = item.isError;
10434
+ closeSegment();
10435
+ } else if (item.kind === "notice" && item.level === "error") marks.push({
10436
+ kind: "error",
10437
+ itemIndex: index
10438
+ });
10439
+ else if (item.kind === "tool_call" && parentOf(item) === frameParentId && (item.status === "failed" || item.result?.isError === true)) marks.push({
10440
+ kind: "toolFailed",
10441
+ itemIndex: index
10442
+ });
10443
+ else if (item.kind === "assistant_text" && parentOf(item) === frameParentId) {
10444
+ if (frameParentId !== void 0) {
10445
+ marks.push({
10446
+ kind: "turn",
10447
+ itemIndex: index
10448
+ });
10449
+ return;
10450
+ }
10451
+ segment.response = index;
10452
+ }
10453
+ });
10454
+ closeSegment();
10455
+ for (const index of bookmarks) if (index >= 0 && index < items.length) marks.push({
10456
+ kind: "bookmark",
10457
+ itemIndex: index
10458
+ });
10459
+ if (recapItemIndex !== void 0) marks.push({
10460
+ kind: "recap",
10461
+ itemIndex: recapItemIndex
10462
+ });
10463
+ return marks;
10464
+ }
10465
+ /**
10466
+ * Place marks proportionally — y = `itemIndex / itemCount` of the rail, every
10467
+ * mark one item's share tall (floored at {@link MIN_MARK}) — then merge
10468
+ * adjacent ones per lane, the loudest colour winning. The same merge rule as
10469
+ * the terminal rail; only the position source differs.
10470
+ */
10471
+ function clusterMarks(marks, railH, itemCount) {
10472
+ const count = Math.max(1, itemCount);
10473
+ const h = Math.max(2, Math.round(railH / count));
10474
+ const lanes = /* @__PURE__ */ new Map();
10475
+ for (const mark of marks) {
10476
+ const y = Math.min(Math.max(0, railH - h), Math.round(mark.itemIndex / count * railH));
10477
+ const lane = LANE[mark.kind];
10478
+ const list = lanes.get(lane) ?? [];
10479
+ list.push({
10480
+ mark,
10481
+ y
10482
+ });
10483
+ lanes.set(lane, list);
10484
+ }
10485
+ const clusters = [];
10486
+ for (const [lane, list] of lanes) {
10487
+ list.sort((a, b) => a.y - b.y);
10488
+ let current = null;
10489
+ for (const { mark, y } of list) if (current && y <= current.y + current.h + 1) {
10490
+ current.h = Math.max(current.h, y + h - current.y);
10491
+ if (LOUDNESS[mark.kind] > LOUDNESS[current.kind]) current.kind = mark.kind;
10492
+ current.marks.push({
10493
+ mark,
10494
+ y
10495
+ });
10496
+ } else {
10497
+ current = {
10498
+ lane,
10499
+ kind: mark.kind,
10500
+ y,
10501
+ h,
10502
+ marks: [{
10503
+ mark,
10504
+ y
10505
+ }]
10506
+ };
10507
+ clusters.push(current);
10508
+ }
10509
+ }
10510
+ return clusters;
10511
+ }
10512
+ /** The approval is not an item — the prompt renders below the transcript — so
10513
+ * its mark pins at the rail's foot, where the prompt is. Built by hand (no
10514
+ * item to derive a position from), hence `marks: []`. */
10515
+ function approvalCluster(railH) {
10516
+ return {
10517
+ lane: LANE.approval,
10518
+ kind: "approval",
10519
+ y: Math.max(0, railH - 2),
10520
+ h: 2,
10521
+ marks: []
10522
+ };
10523
+ }
10524
+ //#endregion
10525
+ //#region src/components/agent/Scrubber.tsx
10526
+ function peekContent(cluster, first, items, pendingApprovals) {
10527
+ const more = cluster.marks.length > 1 ? ` · ${cluster.marks.length} marks` : "";
10528
+ let body = null;
10529
+ if (cluster.kind === "approval") {
10530
+ const request = pendingApprovals[0];
10531
+ body = request ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", { children: request.title ?? "Permission required" }), /* @__PURE__ */ jsx("div", {
10532
+ className: "wd-scrub-ex",
10533
+ "data-tone": "muted",
10534
+ children: `${request.displayName ?? request.toolName}(${toolInputPreview(request.input)})`
10535
+ })] }) : null;
10536
+ } else if (first && first.kind !== "recap") {
10537
+ const item = items[first.itemIndex];
10538
+ if (first.kind === "turn" || first.kind === "turnFailed") {
10539
+ const turn = first.turnIndex === void 0 ? void 0 : items[first.turnIndex];
10540
+ body = /* @__PURE__ */ jsxs(Fragment$1, { children: [item?.kind === "assistant_text" ? /* @__PURE__ */ jsx("div", {
10541
+ className: "wd-scrub-ex",
10542
+ children: item.text
10543
+ }) : null, turn?.kind === "turn_result" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
10544
+ "data-tone": turn.isError ? "danger" : "muted",
10545
+ children: doneLine(turn)
10546
+ }), turn.errors?.map((message, index) => /* @__PURE__ */ jsx("div", {
10547
+ "data-tone": "danger",
10548
+ children: message
10549
+ }, index))] }) : null] });
10550
+ } else if (item) {
10551
+ const failure = first.kind === "toolFailed" && item.kind === "tool_call" ? item.result?.text.split("\n").find((line) => line.trim() !== "") : void 0;
10552
+ body = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
10553
+ className: "wd-scrub-ex",
10554
+ "data-tone": first.kind === "error" || first.kind === "toolFailed" ? "danger" : void 0,
10555
+ children: [first.kind === "user" ? /* @__PURE__ */ jsx("span", {
10556
+ "data-tone": "muted",
10557
+ children: "❯ "
10558
+ }) : null, excerpt(item)]
10559
+ }), failure ? /* @__PURE__ */ jsx("div", {
10560
+ className: "wd-scrub-ex",
10561
+ "data-tone": "danger",
10562
+ children: failure
10563
+ }) : null] });
10564
+ }
10565
+ }
10566
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
10567
+ "data-tone": "muted",
10568
+ children: [KIND_NAME[first?.kind ?? cluster.kind], more]
10569
+ }), body] });
10570
+ }
10571
+ function Scrubber({ items, pendingApprovals, recapItemIndex, bookmarks, frameParentId, interactive = false, onJumpToItem, className }) {
10572
+ const railRef = useRef(null);
10573
+ const peekRef = useRef(null);
10574
+ const [railH, setRailH] = useState(0);
10575
+ const [peek, setPeek] = useState(null);
10576
+ useEffect(() => {
10577
+ const element = railRef.current;
10578
+ if (!element) return;
10579
+ const observer = new ResizeObserver(() => setRailH(element.clientHeight));
10580
+ observer.observe(element);
10581
+ setRailH(element.clientHeight);
10582
+ return () => observer.disconnect();
10583
+ }, []);
10584
+ useEffect(() => {
10585
+ setPeek(null);
10586
+ }, [items]);
10587
+ useLayoutEffect(() => {
10588
+ const element = peekRef.current;
10589
+ if (!element || !peek) return;
10590
+ const height = element.offsetHeight;
10591
+ const railHeight = railRef.current?.clientHeight ?? 0;
10592
+ element.style.top = `${Math.max(4, Math.min(railHeight - height - 4, peek.y - height / 2))}px`;
10593
+ }, [peek]);
10594
+ const clusters = useMemo(() => {
10595
+ if (railH <= 0) return [];
10596
+ const built = clusterMarks(buildMarks(items, {
10597
+ frameParentId,
10598
+ bookmarks,
10599
+ recapItemIndex
10600
+ }), railH, items.length);
10601
+ if (pendingApprovals.length > 0) built.push(approvalCluster(railH));
10602
+ return built;
10603
+ }, [
10604
+ items,
10605
+ frameParentId,
10606
+ bookmarks,
10607
+ recapItemIndex,
10608
+ pendingApprovals,
10609
+ railH
10610
+ ]);
10611
+ /** A pointer's y in rail space. */
10612
+ const railY = (clientY) => clientY - (railRef.current?.getBoundingClientRect().top ?? 0);
10613
+ const activate = (cluster, clientY) => {
10614
+ if (cluster.kind === "approval" && cluster.marks.length === 0) {
10615
+ if (items.length > 0) onJumpToItem?.(items.length - 1);
10616
+ return;
10617
+ }
10618
+ const mark = nearestMember(cluster, railY(clientY));
10619
+ if (mark) onJumpToItem?.(mark.itemIndex);
10620
+ };
10621
+ const showPeek = (cluster, clientY) => {
10622
+ const y = railY(clientY);
10623
+ const mark = nearestMember(cluster, y);
10624
+ setPeek((previous) => previous && previous.cluster === cluster && previous.mark === mark ? previous : {
10625
+ cluster,
10626
+ mark,
10627
+ y: Math.min(Math.max(y, cluster.y), cluster.y + cluster.h)
10628
+ });
10629
+ };
10630
+ return /* @__PURE__ */ jsxs("div", {
10631
+ ref: railRef,
10632
+ className: cn("wd-scrubber", className),
10633
+ "data-interactive": interactive || void 0,
10634
+ "aria-hidden": true,
10635
+ children: [clusters.map((cluster, index) => /* @__PURE__ */ jsx("div", {
10636
+ className: "wd-scrub-mark",
10637
+ "data-lane": cluster.lane,
10638
+ "data-kind": cluster.kind,
10639
+ style: {
10640
+ top: cluster.y,
10641
+ height: cluster.h
10642
+ },
10643
+ ...interactive ? {
10644
+ onClick: (event) => activate(cluster, event.clientY),
10645
+ onPointerEnter: (event) => showPeek(cluster, event.clientY),
10646
+ onPointerMove: (event) => showPeek(cluster, event.clientY),
10647
+ onPointerLeave: () => setPeek(null)
10648
+ } : null
10649
+ }, index)), peek ? /* @__PURE__ */ jsx("div", {
10650
+ ref: peekRef,
10651
+ className: "wd-scrub-peek",
10652
+ style: { top: peek.y },
10653
+ children: peekContent(peek.cluster, peek.mark, items, pendingApprovals)
10654
+ }) : null]
10655
+ });
10656
+ }
10657
+ //#endregion
10298
10658
  //#region src/components/agent/transcript-rows.ts
10299
10659
  /** The item a row is spaced *as*. A run stands for the calls it folded, and a
10300
10660
  * task block for the `Task` call it absorbed into — all tool calls, so a run,
@@ -10962,36 +11322,69 @@ function nestedClass(item, frameParentId) {
10962
11322
  const parent = "parentToolUseId" in item ? item.parentToolUseId : void 0;
10963
11323
  return parent != null && parent !== frameParentId ? "border-l-2 border-border pl-3" : void 0;
10964
11324
  }
11325
+ /** The cards head's one line: the prompt as a plain run of text. Deliberately
11326
+ * not the Message component — a proportional card clipped by height is a
11327
+ * sliced bubble, and un-styling one from CSS is a fight (see theme.css's
11328
+ * sticky-prompt block for the other half of this decision). Prompt syntax
11329
+ * stays literal — a 28px bar is a reminder of what was asked, not a rendering
11330
+ * surface — and newlines collapse under the bar's `nowrap`. Attachment-only
11331
+ * prompts fall back to the attachments' names so the bar is never blank. */
11332
+ function promptHeadText(item) {
11333
+ return item.text || (item.attachments ?? []).map((attachment) => attachment.name).join(", ");
11334
+ }
10965
11335
  /**
10966
11336
  * A prompt row's sticky lane — the strip spanning its turn, leading with the
10967
11337
  * one-line pinned **head** (see the pinned-prompt comment in
10968
11338
  * {@link TranscriptRows}).
10969
11339
  *
11340
+ * The head's content is the variant's: the terminal passes the row again
11341
+ * (clipping it to one line is exact under a monospace grid), cards passes the
11342
+ * prompt as plain text for theme.css to draw as a compact bar. Only the
11343
+ * terminal head carries the row's gap class — its stuck geometry parks that
11344
+ * padding above the viewport edge so the visible line docks at zero
11345
+ * (terminal.css); the cards bar never aligns with the row in flow, so the gap
11346
+ * stays off it entirely and the 1st prompt and the Nth share one geometry.
11347
+ *
10970
11348
  * The head starts `visibility: hidden` and shows only while actually stuck —
10971
11349
  * an overlay that is visible in flow would sit on the real row's first line
10972
11350
  * and swallow its selection highlight, which reads as "the first line cannot
10973
11351
  * be selected". CSS cannot ask "am I stuck?", so a 1px sentinel at the head's
10974
- * engage threshold (the line's own y) feeds an IntersectionObserver: sentinel
10975
- * above the scrollport top → stuck. Transition-only callbacks this adds no
10976
- * per-scroll work, and the pin itself is still the compositor's.
10977
- */
10978
- function StickyPromptLane({ top, height, gapClass, scrollRoot, index, measureRef, content }) {
11352
+ * engage threshold (the line's own y) answers it: sentinel above the
11353
+ * scrollport top → stuck, read by a passive scroll listener. It was an
11354
+ * IntersectionObserver once, for the "no per-scroll work" purity and that
11355
+ * was a real bug: IO is edge-triggered, and an *instant* jump (the open-at-
11356
+ * bottom pin, `jumpToRow`, a reveal) teleports the sentinel from below the
11357
+ * viewport to above it between two observations — ratio 0 → 0, no threshold
11358
+ * crossed, `isIntersecting` unchanged — so no entry is ever queued and the
11359
+ * flag strands, in whichever direction the jump left it (observed: a session
11360
+ * opened at the bottom, its prompt bar missing; the stale-true twin paints a
11361
+ * bar over the real bubble). The flag needs level-triggered truth. The cost
11362
+ * is two rect reads per scroll event per mounted lane — layout is clean
11363
+ * during scrolling, and the pin itself is still the compositor's; only the
11364
+ * bar's visibility rides the listener.
11365
+ */
11366
+ function StickyPromptLane({ top, height, gapClass, gapPx, terminal, scrollRoot, index, measureRef, head, content }) {
10979
11367
  const headRef = useRef(null);
10980
11368
  const sentinelRef = useRef(null);
10981
11369
  useEffect(() => {
10982
- const head = headRef.current;
11370
+ const headElement = headRef.current;
10983
11371
  const sentinel = sentinelRef.current;
10984
- if (!head || !sentinel || !scrollRoot) return;
10985
- const observer = new IntersectionObserver(([entry]) => {
10986
- if (!entry) return;
10987
- const stuck = !entry.isIntersecting && entry.boundingClientRect.top < (entry.rootBounds?.top ?? 0);
10988
- head.toggleAttribute("data-stuck", stuck);
10989
- }, { root: scrollRoot });
10990
- observer.observe(sentinel);
10991
- return () => observer.disconnect();
10992
- }, [scrollRoot]);
11372
+ if (!headElement || !sentinel || !scrollRoot) return;
11373
+ const evaluate = () => {
11374
+ const stuck = sentinel.getBoundingClientRect().top < scrollRoot.getBoundingClientRect().top;
11375
+ headElement.toggleAttribute("data-stuck", stuck);
11376
+ };
11377
+ evaluate();
11378
+ scrollRoot.addEventListener("scroll", evaluate, { passive: true });
11379
+ return () => scrollRoot.removeEventListener("scroll", evaluate);
11380
+ }, [
11381
+ scrollRoot,
11382
+ top,
11383
+ height,
11384
+ gapClass
11385
+ ]);
10993
11386
  return /* @__PURE__ */ jsxs("div", {
10994
- "data-sticky-lane": "",
11387
+ "data-sticky-lane": terminal ? "terminal" : "cards",
10995
11388
  className: "absolute inset-x-0",
10996
11389
  style: {
10997
11390
  top,
@@ -11004,8 +11397,8 @@ function StickyPromptLane({ top, height, gapClass, scrollRoot, index, measureRef
11004
11397
  children: /* @__PURE__ */ jsx("div", {
11005
11398
  ref: headRef,
11006
11399
  "data-sticky-head": "",
11007
- className: gapClass || void 0,
11008
- children: content
11400
+ className: terminal && gapClass || void 0,
11401
+ children: head
11009
11402
  })
11010
11403
  }),
11011
11404
  /* @__PURE__ */ jsx("div", {
@@ -11013,7 +11406,7 @@ function StickyPromptLane({ top, height, gapClass, scrollRoot, index, measureRef
11013
11406
  "aria-hidden": true,
11014
11407
  className: "absolute left-0 w-px",
11015
11408
  style: {
11016
- top: gapClass ? "var(--term-line)" : 0,
11409
+ top: gapClass ? terminal ? "var(--term-line)" : gapPx : 0,
11017
11410
  height: 1
11018
11411
  }
11019
11412
  }),
@@ -11069,7 +11462,7 @@ function TranscriptRows({ rows, boundary, since, terminal, replaying, stickyProm
11069
11462
  promptRows: []
11070
11463
  });
11071
11464
  pinRef.current = {
11072
- enabled: terminal && stickyPrompt,
11465
+ enabled: stickyPrompt,
11073
11466
  promptRows
11074
11467
  };
11075
11468
  useEffect(() => {
@@ -11265,16 +11658,19 @@ function TranscriptRows({ rows, boundary, since, terminal, replaying, stickyProm
11265
11658
  onOpenSubagent
11266
11659
  })
11267
11660
  });
11268
- if (terminal && stickyPrompt && "item" in row && row.item.kind === "user" && parentOf(row.item) === void 0) {
11661
+ if (stickyPrompt && "item" in row && row.item.kind === "user" && parentOf(row.item) === void 0) {
11269
11662
  const next = promptRows.find((index) => index > virtualRow.index);
11270
11663
  const laneEnd = next === void 0 ? virtualizer.getTotalSize() : measurements[next]?.start ?? virtualRow.start;
11271
11664
  return /* @__PURE__ */ jsx(StickyPromptLane, {
11272
11665
  top: virtualRow.start,
11273
11666
  height: Math.max(laneEnd - virtualRow.start, 0),
11274
11667
  gapClass,
11668
+ gapPx: gap.px,
11669
+ terminal,
11275
11670
  scrollRoot: scrollElement,
11276
11671
  index: virtualRow.index,
11277
11672
  measureRef: virtualizer.measureElement,
11673
+ head: terminal ? content : promptHeadText(row.item),
11278
11674
  content
11279
11675
  }, row.key);
11280
11676
  }
@@ -11285,7 +11681,7 @@ function TranscriptRows({ rows, boundary, since, terminal, replaying, stickyProm
11285
11681
  style: { transform: `translateY(${virtualRow.start}px)` },
11286
11682
  children: content
11287
11683
  }, row.key);
11288
- }), terminal && scrubber && scrollElement?.parentElement ? createPortal(/* @__PURE__ */ jsx(TerminalScrubber, {
11684
+ }), scrubber && scrollElement?.parentElement ? createPortal(terminal ? /* @__PURE__ */ jsx(TerminalScrubber, {
11289
11685
  items,
11290
11686
  pendingApprovals,
11291
11687
  recapRow,
@@ -11302,10 +11698,18 @@ function TranscriptRows({ rows, boundary, since, terminal, replaying, stickyProm
11302
11698
  interactive: scrubInteractive,
11303
11699
  fontSize,
11304
11700
  lineHeight
11701
+ }) : /* @__PURE__ */ jsx(Scrubber, {
11702
+ items,
11703
+ pendingApprovals,
11704
+ recapItemIndex: recapIndex >= 0 ? boundary : void 0,
11705
+ bookmarks: scrubberMarks,
11706
+ frameParentId,
11707
+ interactive: scrubInteractive,
11708
+ onJumpToItem: (itemIndex) => jumpToRow(rowIndexForItem(rows, itemIndex), "start")
11305
11709
  }), scrollElement.parentElement) : null]
11306
11710
  });
11307
11711
  }
11308
- function Transcript({ state, fileUrl, attachmentUrl, canBrowseFiles, hostImage, variant = "cards", density = "comfortable", fontSize, lineHeight, affordances, stickyPrompt = false, scrubber, scrubberMarks, replaying = false, catchUp, jumpToRecapRef, repinRef, reveal, frame, onOpenSubagent, className }) {
11712
+ function Transcript({ state, fileUrl, attachmentUrl, canBrowseFiles, hostImage, variant = "cards", density = "comfortable", fontSize, lineHeight, affordances, stickyPrompt = false, scrubber, scrubberMarks, replaying = false, catchUp, jumpToRecapRef, repinRef, reveal, frame, onOpenSubagent, emptyState, className }) {
11309
11713
  const terminal = variant === "terminal";
11310
11714
  const items = useMemo(() => frame ? subagentItems(state.items, frame.parentToolUseId) : state.items, [state.items, frame]);
11311
11715
  const frameTask = useMemo(() => frame ? state.items.find((item) => item.kind === "tool_call" && item.id === frame.parentToolUseId) : void 0, [state.items, frame]);
@@ -11356,7 +11760,7 @@ function Transcript({ state, fileUrl, attachmentUrl, canBrowseFiles, hostImage,
11356
11760
  frame ? frameTask === void 0 && !replaying ? /* @__PURE__ */ jsx("div", {
11357
11761
  className: cn(terminal ? "term-row text-fg-4" : "p-4 text-body-sm text-fg-4"),
11358
11762
  children: "This sub-agent's work is not in this transcript."
11359
- }) : null : items.length === 0 && state.status !== "starting" ? /* @__PURE__ */ jsx(SessionEmptyState, {
11763
+ }) : null : items.length === 0 && state.status !== "starting" ? emptyState !== void 0 ? emptyState : /* @__PURE__ */ jsx(SessionEmptyState, {
11360
11764
  cwd: state.cwd,
11361
11765
  hasCommands: !!state.commands?.length,
11362
11766
  hasSkills: !!state.skills?.some((s) => s.enabled),
@@ -11407,7 +11811,7 @@ function Transcript({ state, fileUrl, attachmentUrl, canBrowseFiles, hostImage,
11407
11811
  "aria-hidden": true,
11408
11812
  className: "wd-hold-appear visible pointer-events-none absolute inset-0 overflow-hidden",
11409
11813
  children: /* @__PURE__ */ jsx("div", {
11410
- className: cn("mx-auto w-full max-w-[var(--wd-content-max-w,48rem)]", !terminal && "px-4 py-4"),
11814
+ className: cn("mx-auto w-full max-w-[var(--wd-transcript-max-width)]", !terminal && "px-4 py-4"),
11411
11815
  children: terminal ? /* @__PURE__ */ jsx(TerminalSurface, {
11412
11816
  fontSize,
11413
11817
  lineHeight,
@@ -11531,42 +11935,45 @@ function UsageMeter({ window, now }) {
11531
11935
  * The session's view of the plan: every window as a meter (see
11532
11936
  * {@link UsageMeters}), the plan tier, and what this session itself has cost.
11533
11937
  */
11534
- function UsageDialog({ rateLimits, subscriptionType, engine, totalCostUsd, updatedAt, open, onOpenChange }) {
11938
+ function UsageDialog({ rateLimits, subscriptionType, engine, totalCostUsd, updatedAt, open, onOpenChange, className }) {
11535
11939
  const now = useMinuteClock(open);
11536
11940
  return /* @__PURE__ */ jsx(Dialog$1, {
11537
11941
  open,
11538
11942
  onOpenChange,
11539
- children: /* @__PURE__ */ jsxs(DialogContent, { children: [/* @__PURE__ */ jsx(DialogHeader, {
11540
- title: "Usage",
11541
- description: engine === "claude" ? "Claude Code" : engine,
11542
- actions: subscriptionType ? /* @__PURE__ */ jsx(Badge, {
11543
- variant: "accent",
11544
- className: "mt-0.5 shrink-0 capitalize",
11545
- children: subscriptionType
11546
- }) : null
11547
- }), /* @__PURE__ */ jsxs(DialogBody, { children: [
11548
- rateLimits.length === 0 ? /* @__PURE__ */ jsx("p", {
11549
- className: "py-6 text-center text-body-sm text-fg-4",
11550
- children: engine === "claude" ? "This session reports no plan windows — API-key sessions have none, and a subscription session reports them once a turn has run." : `Plan windows are a claude.ai subscription thing; this session runs on the ${engine} engine.`
11551
- }) : /* @__PURE__ */ jsx(UsageMeters, {
11552
- windows: rateLimits,
11553
- now
11554
- }),
11555
- /* @__PURE__ */ jsxs("div", {
11556
- className: "mt-5 flex items-baseline justify-between gap-4 border-t border-border pt-3",
11557
- children: [/* @__PURE__ */ jsx("span", {
11558
- className: "text-label text-fg-3",
11559
- children: "This session has cost"
11560
- }), /* @__PURE__ */ jsx("span", {
11561
- className: "font-mono text-body-sm text-fg-1",
11562
- children: formatCost(totalCostUsd)
11563
- })]
11564
- }),
11565
- updatedAt ? /* @__PURE__ */ jsxs("p", {
11566
- className: "mt-2 text-label text-fg-4",
11567
- children: ["Updated ", formatAgoPrecise(updatedAt, now)]
11568
- }) : null
11569
- ] })] })
11943
+ children: /* @__PURE__ */ jsxs(DialogContent, {
11944
+ className,
11945
+ children: [/* @__PURE__ */ jsx(DialogHeader, {
11946
+ title: "Usage",
11947
+ description: engine === "claude" ? "Claude Code" : engine,
11948
+ actions: subscriptionType ? /* @__PURE__ */ jsx(Badge, {
11949
+ variant: "accent",
11950
+ className: "mt-0.5 shrink-0 capitalize",
11951
+ children: subscriptionType
11952
+ }) : null
11953
+ }), /* @__PURE__ */ jsxs(DialogBody, { children: [
11954
+ rateLimits.length === 0 ? /* @__PURE__ */ jsx("p", {
11955
+ className: "py-6 text-center text-body-sm text-fg-4",
11956
+ children: engine === "claude" ? "This session reports no plan windows — API-key sessions have none, and a subscription session reports them once a turn has run." : `Plan windows are a claude.ai subscription thing; this session runs on the ${engine} engine.`
11957
+ }) : /* @__PURE__ */ jsx(UsageMeters, {
11958
+ windows: rateLimits,
11959
+ now
11960
+ }),
11961
+ /* @__PURE__ */ jsxs("div", {
11962
+ className: "mt-5 flex items-baseline justify-between gap-4 border-t border-border pt-3",
11963
+ children: [/* @__PURE__ */ jsx("span", {
11964
+ className: "text-label text-fg-3",
11965
+ children: "This session has cost"
11966
+ }), /* @__PURE__ */ jsx("span", {
11967
+ className: "font-mono text-body-sm text-fg-1",
11968
+ children: formatCost(totalCostUsd)
11969
+ })]
11970
+ }),
11971
+ updatedAt ? /* @__PURE__ */ jsxs("p", {
11972
+ className: "mt-2 text-label text-fg-4",
11973
+ children: ["Updated ", formatAgoPrecise(updatedAt, now)]
11974
+ }) : null
11975
+ ] })]
11976
+ })
11570
11977
  });
11571
11978
  }
11572
11979
  //#endregion
@@ -11581,7 +11988,7 @@ function UsageDialog({ rateLimits, subscriptionType, engine, totalCostUsd, updat
11581
11988
  */
11582
11989
  function PromptSurface({ terminal, metrics, affordances, children }) {
11583
11990
  if (!terminal) return /* @__PURE__ */ jsx("div", {
11584
- className: "mx-auto flex w-full max-w-[var(--wd-content-max-w,48rem)] flex-col gap-2",
11991
+ className: "mx-auto flex w-full max-w-[var(--wd-transcript-max-width)] flex-col gap-2",
11585
11992
  children
11586
11993
  });
11587
11994
  return /* @__PURE__ */ jsx(TerminalSurface, {
@@ -11620,7 +12027,9 @@ const INTERACTIVE = [
11620
12027
  * the engine name — an absent capability hides the control instead of offering
11621
12028
  * one that can only fail.
11622
12029
  */
11623
- function SessionPanel({ client, sessionId, header, panelSurface = "internal", statusSurface = "internal", statusPlacement = "top", onOpenPanel, onVitals, transcriptVariant = "cards", transcriptDensity = "comfortable", transcriptFont = "sans", affordances, terminalMetrics, scrubber = false, scrubberMarks, reveal, openSubagent, onSubagentChange, stickyPrompt = false, controlsSurface = "internal", onControls, focusComposerOnClick = false, unseen, readOnly = false, toolHost, cacheTranscript, className }) {
12030
+ function SessionPanel({ client, sessionId, header, panelSurface = "internal", statusSurface = "internal", statusPlacement = "top", onOpenPanel, onVitals, transcriptVariant = "cards", transcriptDensity = "comfortable", transcriptFont = "sans", affordances, terminalMetrics, scrubber = false, scrubberMarks, reveal, openSubagent, onSubagentChange, stickyPrompt = false, controlsSurface = "internal", onControls, focusComposerOnClick = false, unseen, readOnly = false, toolHost, clientTools, cacheTranscript, emptyState, onLinkClick, fontSize, className }) {
12031
+ const effectiveTermFontSize = terminalMetrics?.fontSize ?? fontSize;
12032
+ const effectiveTermLineHeight = terminalMetrics?.lineHeight ?? (fontSize !== void 0 ? Math.round(fontSize * (18 / 13)) : void 0);
11624
12033
  const external = panelSurface === "external";
11625
12034
  const statusExternal = statusSurface === "external";
11626
12035
  const controlsInStatus = controlsSurface === "status" && !statusExternal;
@@ -11711,7 +12120,13 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11711
12120
  document.addEventListener("visibilitychange", onVisible);
11712
12121
  return () => document.removeEventListener("visibilitychange", onVisible);
11713
12122
  }, [reconnectNow]);
11714
- useToolCallHost(handle, toolHost === false ? { enabled: false } : toolHost);
12123
+ useToolCallHost(handle, toolHost === false ? { enabled: false } : clientTools ? {
12124
+ ...toolHost,
12125
+ clientTools: {
12126
+ ...toolHost?.clientTools,
12127
+ ...clientTools
12128
+ }
12129
+ } : toolHost);
11715
12130
  const terminal = transcriptVariant === "terminal";
11716
12131
  const subagentFrameItems = useMemo(() => subagentId === void 0 ? [] : subagentItems(state.items, subagentId), [state.items, subagentId]);
11717
12132
  const subagentTask = useMemo(() => subagentId === void 0 ? void 0 : state.items.find((item) => item.kind === "tool_call" && item.id === subagentId), [state.items, subagentId]);
@@ -11751,7 +12166,8 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11751
12166
  cwd: state.cwd,
11752
12167
  contextUsage: state.contextUsage,
11753
12168
  rateLimits,
11754
- itemCount: state.items.length
12169
+ itemCount: state.items.length,
12170
+ totalCostUsd: state.totalCostUsd
11755
12171
  });
11756
12172
  }, [
11757
12173
  state.status,
@@ -11765,7 +12181,8 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11765
12181
  state.cwd,
11766
12182
  state.contextUsage,
11767
12183
  rateLimits,
11768
- state.items.length
12184
+ state.items.length,
12185
+ state.totalCostUsd
11769
12186
  ]);
11770
12187
  const onControlsRef = useRef(onControls);
11771
12188
  onControlsRef.current = onControls;
@@ -11886,6 +12303,24 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11886
12303
  onOpenUsage: external && !onOpenPanel ? void 0 : () => openPanel("usage"),
11887
12304
  actions: headerTakesActions ? void 0 : menu
11888
12305
  });
12306
+ const panelRef = useRef(null);
12307
+ useEffect(() => {
12308
+ if (!onLinkClick) return;
12309
+ const el = panelRef.current;
12310
+ if (!el) return;
12311
+ const handler = (e) => {
12312
+ const anchor = e.target?.closest?.("a[href]");
12313
+ if (!anchor) return;
12314
+ const href = anchor.getAttribute("href");
12315
+ if (!href) return;
12316
+ if (onLinkClick(href)) {
12317
+ e.preventDefault();
12318
+ e.stopPropagation();
12319
+ }
12320
+ };
12321
+ el.addEventListener("click", handler, true);
12322
+ return () => el.removeEventListener("click", handler, true);
12323
+ }, [onLinkClick]);
11889
12324
  const handleClick = (event) => {
11890
12325
  if (!focusComposerOnClick || readOnly) return;
11891
12326
  if (event.target?.closest(INTERACTIVE)) return;
@@ -11901,10 +12336,12 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11901
12336
  children: /* @__PURE__ */ jsx(ToolResultImageProvider, {
11902
12337
  value: resultImages,
11903
12338
  children: /* @__PURE__ */ jsxs("div", {
12339
+ ref: panelRef,
11904
12340
  "data-slot": "session-panel",
11905
12341
  "data-agent-font": transcriptFont,
11906
12342
  onClick: handleClick,
11907
12343
  className: cn("flex h-full min-h-0 flex-col overflow-hidden bg-bg", className),
12344
+ style: fontSize !== void 0 ? { "--wd-font-size": `${Math.round(fontSize)}px` } : void 0,
11908
12345
  children: [
11909
12346
  headerTakesActions ? header({ actions: menu }) : header,
11910
12347
  statusPlacement === "top" ? statusBar : null,
@@ -11929,8 +12366,8 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11929
12366
  label: subagentFallbackLabel,
11930
12367
  onBack: leaveSubagent,
11931
12368
  terminal,
11932
- fontSize: terminalMetrics?.fontSize,
11933
- lineHeight: terminalMetrics?.lineHeight
12369
+ fontSize: effectiveTermFontSize,
12370
+ lineHeight: effectiveTermLineHeight
11934
12371
  }) : null,
11935
12372
  /* @__PURE__ */ jsx(Transcript, {
11936
12373
  state,
@@ -11940,8 +12377,8 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11940
12377
  hostImage,
11941
12378
  variant: transcriptVariant,
11942
12379
  density: transcriptDensity,
11943
- fontSize: terminalMetrics?.fontSize,
11944
- lineHeight: terminalMetrics?.lineHeight,
12380
+ fontSize: effectiveTermFontSize,
12381
+ lineHeight: effectiveTermLineHeight,
11945
12382
  affordances,
11946
12383
  stickyPrompt,
11947
12384
  scrubber,
@@ -11954,6 +12391,7 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11954
12391
  reveal: returnReveal ?? reveal,
11955
12392
  frame: subagentId === void 0 ? void 0 : { parentToolUseId: subagentId },
11956
12393
  onOpenSubagent: setSubagentId,
12394
+ emptyState,
11957
12395
  jumpToRecapRef: jumpToRecap,
11958
12396
  repinRef: repinTranscript
11959
12397
  }, subagentId ?? "session"),
@@ -11961,7 +12399,7 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11961
12399
  className: "px-3 pb-1",
11962
12400
  children: /* @__PURE__ */ jsxs("div", {
11963
12401
  "data-slot": "catch-up",
11964
- className: "mx-auto flex w-full max-w-[var(--wd-content-max-w,48rem)] items-center gap-2 text-label text-fg-3",
12402
+ className: "mx-auto flex w-full max-w-[var(--wd-transcript-max-width)] items-center gap-2 text-label text-fg-3",
11965
12403
  children: [
11966
12404
  /* @__PURE__ */ jsx("span", {
11967
12405
  "aria-hidden": true,
@@ -11996,7 +12434,10 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11996
12434
  className: cn(terminal ? "pb-2" : "px-3 pb-2"),
11997
12435
  children: /* @__PURE__ */ jsx(PromptSurface, {
11998
12436
  terminal,
11999
- metrics: terminalMetrics,
12437
+ metrics: {
12438
+ fontSize: effectiveTermFontSize,
12439
+ lineHeight: effectiveTermLineHeight
12440
+ },
12000
12441
  affordances,
12001
12442
  children: state.pendingApprovals.map((request) => {
12002
12443
  const isQuestion = request.toolName === "AskUserQuestion" && parseUserQuestions(request.input).length > 0;
@@ -12036,8 +12477,8 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
12036
12477
  }) : void 0,
12037
12478
  layout: controlsExternal ? "inline" : "stacked",
12038
12479
  toolbar: controlsExternal ? void 0 : sessionControls,
12039
- fontSize: terminalMetrics?.fontSize,
12040
- lineHeight: terminalMetrics?.lineHeight,
12480
+ fontSize: effectiveTermFontSize,
12481
+ lineHeight: effectiveTermLineHeight,
12041
12482
  affordances
12042
12483
  }),
12043
12484
  statusPlacement === "bottom" ? statusBar : null,
@@ -12159,7 +12600,7 @@ function Notice({ level, onDismiss, children }) {
12159
12600
  className: "px-3 pt-2",
12160
12601
  children: /* @__PURE__ */ jsxs("div", {
12161
12602
  role: "alert",
12162
- className: cn("mx-auto flex w-full max-w-[var(--wd-content-max-w,48rem)] items-start gap-2 rounded-md border px-3 py-2 text-body-sm", level === "error" ? "border-danger/40 bg-danger-bg text-danger" : "border-warning/40 bg-warning-bg text-warning"),
12603
+ className: cn("mx-auto flex w-full max-w-[var(--wd-transcript-max-width)] items-start gap-2 rounded-md border px-3 py-2 text-body-sm", level === "error" ? "border-danger/40 bg-danger-bg text-danger" : "border-warning/40 bg-warning-bg text-warning"),
12163
12604
  children: [
12164
12605
  /* @__PURE__ */ jsx(TriangleAlert, { className: "mt-0.5 size-3.5 shrink-0" }),
12165
12606
  /* @__PURE__ */ jsx("span", {
@@ -12178,6 +12619,6 @@ function Notice({ level, onDismiss, children }) {
12178
12619
  });
12179
12620
  }
12180
12621
  //#endregion
12181
- export { toolIcon as $, PERMISSION_MODES as A, MenuSeparator as At, skillPrompt as B, badgeVariants as Bt, TerminalQuestionPrompt as C, DialogContent as Ct, parseUserQuestions as D, Menu$1 as Dt, QuestionPrompt as E, DialogTrigger as Et, SkillsDialog as F, SelectItemText as Ft, Band as G, TerminalMarkdown as H, buttonVariants as Ht, McpDialog as I, SelectTrigger as It, Row as J, Blank as K, HostFilesDialog as L, SelectValue as Lt, permissionModeChoices as M, Select$1 as Mt, permissionModeMeta as N, SelectContent as Nt, SubagentStrip as O, MenuContent as Ot, ModelSelect as P, SelectItem as Pt, isMutatingTool as Q, ContextDialog as R, Input as Rt, SessionInfoDialog as S, DialogClose as St, QUESTION_BEHAVIORS as T, DialogRow as Tt, TerminalDiff as U, cn as Ut, TerminalSurface as V, Button as Vt, previewPatch as W, WithActions as X, CopyAction as Y, useAffordances as Z, Conversation as _, Tip as _t, Transcript as a, hashtagTrigger as at, StatusBar as b, Dialog$1 as bt, ToolCallCard as c, PromptArea as ct, Response as d, ProgressRing as dt, TranscriptDensityProvider as et, PromptTokenText as f, Splitter as ft, FileCard as g, copyText as gt, Loader as h, CopyButton as ht, useMinuteClock as i, commandTrigger as it, PermissionModeSelect as j, MenuTrigger as jt, PermissionPrompt as k, MenuItem as kt, SessionEmptyState as l, plainTextToSegments as lt, MessageContent as m, Spinner as mt, UsageDialog as n, useTranscriptDensity as nt, TerminalItemView as o, mentionTrigger as ot, Message as p, CodeBlock as pt, Ink as q, UsageMeters as r, useTranscriptVariant as rt, TerminalTranscript as s, usePromptAreaState as st, SessionPanel as t, TranscriptVariantProvider as tt, Reasoning as u, segmentsToPlainText as ut, ConversationContent as v, TooltipContent as vt, TerminalPermissionPrompt as w, DialogHeader as wt, STATUS_META as x, DialogBody as xt, ConversationScrollButton as y, TooltipProvider as yt, Composer as z, Badge as zt };
12622
+ export { toolIcon as $, PERMISSION_MODES as A, MenuSeparator as At, skillPrompt as B, Badge as Bt, TerminalQuestionPrompt as C, DialogContent as Ct, parseUserQuestions as D, Menu$1 as Dt, QuestionPrompt as E, DialogTrigger as Et, SkillsDialog as F, SelectItemText as Ft, Band as G, TerminalMarkdown as H, Button as Ht, McpDialog as I, SelectTrigger as It, Row as J, Blank as K, HostFilesDialog as L, SelectValue as Lt, permissionModeChoices as M, Select$1 as Mt, permissionModeMeta as N, SelectContent as Nt, SubagentStrip as O, MenuContent as Ot, ModelSelect as P, SelectItem as Pt, isMutatingTool as Q, ContextDialog as R, PortalScope as Rt, SessionInfoDialog as S, DialogClose as St, QUESTION_BEHAVIORS as T, DialogRow as Tt, TerminalDiff as U, buttonVariants as Ut, TerminalSurface as V, badgeVariants as Vt, previewPatch as W, cn as Wt, WithActions as X, CopyAction as Y, useAffordances as Z, Conversation as _, Tip as _t, Transcript as a, hashtagTrigger as at, StatusBar as b, Dialog$1 as bt, ToolCallCard as c, PromptArea as ct, Response as d, ProgressRing as dt, TranscriptDensityProvider as et, PromptTokenText as f, Splitter as ft, FileCard as g, copyText as gt, Loader as h, CopyButton as ht, useMinuteClock as i, commandTrigger as it, PermissionModeSelect as j, MenuTrigger as jt, PermissionPrompt as k, MenuItem as kt, SessionEmptyState as l, plainTextToSegments as lt, MessageContent as m, Spinner as mt, UsageDialog as n, useTranscriptDensity as nt, TerminalItemView as o, mentionTrigger as ot, Message as p, CodeBlock as pt, Ink as q, UsageMeters as r, useTranscriptVariant as rt, TerminalTranscript as s, usePromptAreaState as st, SessionPanel as t, TranscriptVariantProvider as tt, Reasoning as u, segmentsToPlainText as ut, ConversationContent as v, TooltipContent as vt, TerminalPermissionPrompt as w, DialogHeader as wt, STATUS_META as x, DialogBody as xt, ConversationScrollButton as y, TooltipProvider as yt, Composer as z, Input as zt };
12182
12623
 
12183
- //# sourceMappingURL=SessionPanel-DNuPKg5c.mjs.map
12624
+ //# sourceMappingURL=SessionPanel-DUt2VzXG.mjs.map