@workerdeck/ui 0.20.0 → 0.22.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 (43) hide show
  1. package/README.md +3 -2
  2. package/build/{SessionPanel-CnU_IJ3-.d.mts → SessionPanel-13l25ubU.d.mts} +55 -7
  3. package/build/{SessionPanel-CHktI2CF.mjs → SessionPanel-DgwjH4Ve.mjs} +642 -231
  4. package/build/SessionPanel-DgwjH4Ve.mjs.map +1 -0
  5. package/build/index.d.mts +309 -53
  6. package/build/index.mjs +534 -397
  7. package/build/index.mjs.map +1 -1
  8. package/build/scoped.css +3547 -0
  9. package/build/workspace.d.mts +10 -1
  10. package/build/workspace.mjs +4 -2
  11. package/build/workspace.mjs.map +1 -1
  12. package/package.json +15 -7
  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/Scrubber.tsx +248 -0
  18. package/src/components/agent/SessionBrowser.tsx +113 -293
  19. package/src/components/agent/SessionItem.tsx +538 -0
  20. package/src/components/agent/SessionList.tsx +3 -1
  21. package/src/components/agent/SessionPanel.tsx +112 -21
  22. package/src/components/agent/SessionStatusIcon.tsx +43 -0
  23. package/src/components/agent/SessionSteps.tsx +118 -61
  24. package/src/components/agent/SessionWorkspace.tsx +11 -0
  25. package/src/components/agent/SkillsDialog.tsx +3 -2
  26. package/src/components/agent/StatusBar.tsx +1 -1
  27. package/src/components/agent/Transcript.tsx +147 -73
  28. package/src/components/agent/UsageDialog.tsx +3 -1
  29. package/src/components/agent/scrubber-marks.ts +277 -0
  30. package/src/components/terminal/PermissionPrompt.tsx +3 -0
  31. package/src/components/terminal/QuestionPrompt.tsx +3 -0
  32. package/src/components/terminal/StatusLine.tsx +3 -1
  33. package/src/components/ui/AlertDialog.tsx +23 -20
  34. package/src/components/ui/Dialog.tsx +26 -23
  35. package/src/components/ui/Menu.tsx +20 -17
  36. package/src/components/ui/PortalScope.tsx +27 -0
  37. package/src/components/ui/Select.tsx +19 -16
  38. package/src/components/ui/Tooltip.tsx +13 -10
  39. package/src/index.ts +5 -1
  40. package/src/styles/scoped.entry.css +16 -0
  41. package/src/styles/terminal.css +1 -1
  42. package/src/styles/theme.css +223 -8
  43. package/build/SessionPanel-CHktI2CF.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,
@@ -8650,7 +8688,7 @@ function Conversation({ className, children, ...props }) {
8650
8688
  function ConversationContent({ className, children, ...props }) {
8651
8689
  return /* @__PURE__ */ jsx(StickToBottom.Content, {
8652
8690
  "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),
8691
+ className: cn("mx-auto flex w-full max-w-[var(--wd-transcript-max-width)] flex-col gap-4 px-4 py-4", className),
8654
8692
  ...props,
8655
8693
  children
8656
8694
  });
@@ -9845,7 +9883,7 @@ function blockHeight(block, m) {
9845
9883
  //#region src/components/terminal/scrubber.tsx
9846
9884
  /** The member closest to a rail-space y — what a press or peek on a merged
9847
9885
  * cluster resolves to. */
9848
- function nearestMember(cluster, y) {
9886
+ function nearestMember$1(cluster, y) {
9849
9887
  let best;
9850
9888
  for (const member of cluster.marks) if (!best || Math.abs(member.y - y) < Math.abs(best.y - y)) best = member;
9851
9889
  return best?.mark;
@@ -9865,7 +9903,7 @@ function nearestMember(cluster, y) {
9865
9903
  * It also buys the marks their width back: three lanes in a 16px rail is 5px a
9866
9904
  * lane, which is a hard target to hit and a hard colour to see.
9867
9905
  */
9868
- const LANE = {
9906
+ const LANE$1 = {
9869
9907
  user: "l",
9870
9908
  subagent: "l",
9871
9909
  turn: "r",
@@ -9877,7 +9915,7 @@ const LANE = {
9877
9915
  bookmark: "f"
9878
9916
  };
9879
9917
  /** Who wins the colour when marks merge. */
9880
- const LOUDNESS = {
9918
+ const LOUDNESS$1 = {
9881
9919
  approval: 7,
9882
9920
  error: 6,
9883
9921
  turnFailed: 5,
@@ -9888,7 +9926,7 @@ const LOUDNESS = {
9888
9926
  subagent: 1,
9889
9927
  recap: 0
9890
9928
  };
9891
- const KIND_NAME = {
9929
+ const KIND_NAME$1 = {
9892
9930
  user: "you",
9893
9931
  subagent: "sub-agent",
9894
9932
  turn: "response · turn end",
@@ -9899,7 +9937,7 @@ const KIND_NAME = {
9899
9937
  recap: "catch-up boundary",
9900
9938
  bookmark: "bookmark"
9901
9939
  };
9902
- const MIN_MARK = 2;
9940
+ const MIN_MARK$1 = 2;
9903
9941
  /**
9904
9942
  * Rail pixels per content pixel — the one scale both the marks and the viewport
9905
9943
  * band are drawn at, so they cannot disagree about where a row sits.
@@ -9923,15 +9961,15 @@ const MIN_MARK = 2;
9923
9961
  function railScale(railH, totalSize, viewportH) {
9924
9962
  return totalSize > 0 ? railH / Math.max(totalSize, viewportH) : 0;
9925
9963
  }
9926
- const doneLine = (turn) => `${turn.isError ? turn.subtype : "done"} · ${formatDuration(turn.durationMs)} · ${formatCost(turn.totalCostUsd)}`;
9927
- function excerpt(item) {
9964
+ const doneLine$1 = (turn) => `${turn.isError ? turn.subtype : "done"} · ${formatDuration(turn.durationMs)} · ${formatCost(turn.totalCostUsd)}`;
9965
+ function excerpt$1(item) {
9928
9966
  switch (item.kind) {
9929
9967
  case "user":
9930
9968
  case "assistant_text":
9931
9969
  case "thinking":
9932
9970
  case "notice": return item.text;
9933
9971
  case "tool_call": return `${item.name}(${toolInputPreview(item.input)})`;
9934
- case "turn_result": return doneLine(item);
9972
+ case "turn_result": return doneLine$1(item);
9935
9973
  case "file_delivered": return item.path;
9936
9974
  default: return "";
9937
9975
  }
@@ -10023,9 +10061,9 @@ function buildClusters(props, railH) {
10023
10061
  for (const mark of marks) {
10024
10062
  const within = mark.itemIndex >= 0 ? positionInRow?.(mark.itemIndex) : void 0;
10025
10063
  const rowH = sizeOfRow(mark.rowIndex);
10026
- const h = within ? MIN_MARK : Math.max(MIN_MARK, Math.round(rowH * scale));
10064
+ const h = within ? MIN_MARK$1 : Math.max(MIN_MARK$1, Math.round(rowH * scale));
10027
10065
  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];
10066
+ const lane = LANE$1[mark.kind];
10029
10067
  const list = lanes.get(lane) ?? [];
10030
10068
  list.push({
10031
10069
  mark,
@@ -10040,7 +10078,7 @@ function buildClusters(props, railH) {
10040
10078
  let current = null;
10041
10079
  for (const { mark, y, h } of list) if (current && y <= current.y + current.h + 1) {
10042
10080
  current.h = Math.max(current.h, y + h - current.y);
10043
- if (LOUDNESS[mark.kind] > LOUDNESS[current.kind]) current.kind = mark.kind;
10081
+ if (LOUDNESS$1[mark.kind] > LOUDNESS$1[current.kind]) current.kind = mark.kind;
10044
10082
  current.marks.push({
10045
10083
  mark,
10046
10084
  y
@@ -10060,15 +10098,15 @@ function buildClusters(props, railH) {
10060
10098
  }
10061
10099
  }
10062
10100
  if (pendingApprovals.length > 0) clusters.push({
10063
- lane: LANE.approval,
10101
+ lane: LANE$1.approval,
10064
10102
  kind: "approval",
10065
- y: Math.max(0, railH - MIN_MARK),
10066
- h: MIN_MARK,
10103
+ y: Math.max(0, railH - MIN_MARK$1),
10104
+ h: MIN_MARK$1,
10067
10105
  marks: []
10068
10106
  });
10069
10107
  return clusters;
10070
10108
  }
10071
- function peekContent(cluster, first, { items, pendingApprovals, recapRow }) {
10109
+ function peekContent$1(cluster, first, { items, pendingApprovals, recapRow }) {
10072
10110
  const more = cluster.marks.length > 1 ? ` · ${cluster.marks.length} marks` : "";
10073
10111
  let body = null;
10074
10112
  if (cluster.kind === "approval") {
@@ -10102,7 +10140,7 @@ function peekContent(cluster, first, { items, pendingApprovals, recapRow }) {
10102
10140
  }), item.text]
10103
10141
  }) : null, turn?.kind === "turn_result" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
10104
10142
  "data-tone": turn.isError ? "red" : "faint",
10105
- children: doneLine(turn)
10143
+ children: doneLine$1(turn)
10106
10144
  }), turn.errors?.map((message, index) => /* @__PURE__ */ jsx("div", {
10107
10145
  "data-tone": "red",
10108
10146
  children: message
@@ -10115,7 +10153,7 @@ function peekContent(cluster, first, { items, pendingApprovals, recapRow }) {
10115
10153
  children: [first.kind === "user" ? /* @__PURE__ */ jsx("span", {
10116
10154
  "data-tone": "dim",
10117
10155
  children: "❯ "
10118
- }) : null, excerpt(item)]
10156
+ }) : null, excerpt$1(item)]
10119
10157
  }), failure ? /* @__PURE__ */ jsx("div", {
10120
10158
  className: "term-scrub-ex",
10121
10159
  "data-tone": "red",
@@ -10125,7 +10163,7 @@ function peekContent(cluster, first, { items, pendingApprovals, recapRow }) {
10125
10163
  }
10126
10164
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
10127
10165
  "data-tone": "faint",
10128
- children: [KIND_NAME[first?.kind ?? cluster.kind], more]
10166
+ children: [KIND_NAME$1[first?.kind ?? cluster.kind], more]
10129
10167
  }), body] });
10130
10168
  }
10131
10169
  function TerminalScrubber(props) {
@@ -10202,12 +10240,12 @@ function TerminalScrubber(props) {
10202
10240
  stick.scrollToBottom();
10203
10241
  return;
10204
10242
  }
10205
- const mark = nearestMember(cluster, railY(clientY));
10243
+ const mark = nearestMember$1(cluster, railY(clientY));
10206
10244
  if (mark) onJumpToRow(mark.rowIndex);
10207
10245
  };
10208
10246
  const showPeek = (cluster, clientY) => {
10209
10247
  const y = railY(clientY);
10210
- const mark = nearestMember(cluster, y);
10248
+ const mark = nearestMember$1(cluster, y);
10211
10249
  setPeek((previous) => previous && previous.cluster === cluster && previous.mark === mark ? previous : {
10212
10250
  cluster,
10213
10251
  mark,
@@ -10288,13 +10326,328 @@ function TerminalScrubber(props) {
10288
10326
  ref: peekRef,
10289
10327
  className: "term-scrub-peek",
10290
10328
  style: { top: peek.y },
10291
- children: peekContent(peek.cluster, peek.mark, props)
10329
+ children: peekContent$1(peek.cluster, peek.mark, props)
10292
10330
  }) : null
10293
10331
  ]
10294
10332
  })
10295
10333
  });
10296
10334
  }
10297
10335
  //#endregion
10336
+ //#region src/components/agent/scrubber-marks.ts
10337
+ /** The member closest to a rail-space y — what a press or peek on a merged
10338
+ * cluster resolves to. */
10339
+ function nearestMember(cluster, y) {
10340
+ let best;
10341
+ for (const member of cluster.marks) if (!best || Math.abs(member.y - y) < Math.abs(best.y - y)) best = member;
10342
+ return best?.mark;
10343
+ }
10344
+ /** The two lanes are channels, not classes: left is what went *in* (your
10345
+ * prompts, the sub-agents you dispatched), right is what came *out* (each
10346
+ * turn's answer, and everything that went wrong producing one). Full width is
10347
+ * for what is not a channel at all: the waiting approval, a bookmark, the
10348
+ * catch-up seam. See the terminal scrubber for the full argument. */
10349
+ const LANE = {
10350
+ user: "l",
10351
+ subagent: "l",
10352
+ turn: "r",
10353
+ turnFailed: "r",
10354
+ toolFailed: "r",
10355
+ error: "r",
10356
+ approval: "f",
10357
+ recap: "f",
10358
+ bookmark: "f"
10359
+ };
10360
+ /** Who wins the colour when marks merge. */
10361
+ const LOUDNESS = {
10362
+ approval: 7,
10363
+ error: 6,
10364
+ turnFailed: 5,
10365
+ toolFailed: 4,
10366
+ user: 3,
10367
+ turn: 2,
10368
+ bookmark: 1,
10369
+ subagent: 1,
10370
+ recap: 0
10371
+ };
10372
+ const KIND_NAME = {
10373
+ user: "you",
10374
+ subagent: "sub-agent",
10375
+ turn: "response · turn end",
10376
+ turnFailed: "turn failed",
10377
+ toolFailed: "tool failed",
10378
+ error: "error",
10379
+ approval: "pending approval",
10380
+ recap: "catch-up boundary",
10381
+ bookmark: "bookmark"
10382
+ };
10383
+ const doneLine = (turn) => `${turn.isError ? turn.subtype : "done"} · ${formatDuration(turn.durationMs)} · ${formatCost(turn.totalCostUsd)}`;
10384
+ function excerpt(item) {
10385
+ switch (item.kind) {
10386
+ case "user":
10387
+ case "assistant_text":
10388
+ case "thinking":
10389
+ case "notice": return item.text;
10390
+ case "tool_call": return `${item.name}(${toolInputPreview(item.input)})`;
10391
+ case "turn_result": return doneLine(item);
10392
+ case "file_delivered": return item.path;
10393
+ default: return "";
10394
+ }
10395
+ }
10396
+ function buildMarks(items, { frameParentId, bookmarks = [], recapItemIndex } = {}) {
10397
+ const marks = [];
10398
+ const subagentParents = /* @__PURE__ */ new Set();
10399
+ for (const item of items) {
10400
+ const parent = parentOf(item);
10401
+ if (parent !== void 0) subagentParents.add(parent);
10402
+ }
10403
+ let segment = {};
10404
+ const closeSegment = () => {
10405
+ const anchor = segment.response ?? segment.turn;
10406
+ if (anchor !== void 0) marks.push({
10407
+ kind: segment.failed ? "turnFailed" : "turn",
10408
+ itemIndex: anchor,
10409
+ turnIndex: segment.turn
10410
+ });
10411
+ segment = {};
10412
+ };
10413
+ items.forEach((item, index) => {
10414
+ if (item.kind === "tool_call" && subagentParents.has(item.id)) marks.push({
10415
+ kind: "subagent",
10416
+ itemIndex: index
10417
+ });
10418
+ if (item.kind === "user" && parentOf(item) === frameParentId) {
10419
+ closeSegment();
10420
+ marks.push({
10421
+ kind: "user",
10422
+ itemIndex: index
10423
+ });
10424
+ } else if (item.kind === "turn_result") {
10425
+ segment.turn = index;
10426
+ segment.failed = item.isError;
10427
+ closeSegment();
10428
+ } else if (item.kind === "notice" && item.level === "error") marks.push({
10429
+ kind: "error",
10430
+ itemIndex: index
10431
+ });
10432
+ else if (item.kind === "tool_call" && parentOf(item) === frameParentId && (item.status === "failed" || item.result?.isError === true)) marks.push({
10433
+ kind: "toolFailed",
10434
+ itemIndex: index
10435
+ });
10436
+ else if (item.kind === "assistant_text" && parentOf(item) === frameParentId) {
10437
+ if (frameParentId !== void 0) {
10438
+ marks.push({
10439
+ kind: "turn",
10440
+ itemIndex: index
10441
+ });
10442
+ return;
10443
+ }
10444
+ segment.response = index;
10445
+ }
10446
+ });
10447
+ closeSegment();
10448
+ for (const index of bookmarks) if (index >= 0 && index < items.length) marks.push({
10449
+ kind: "bookmark",
10450
+ itemIndex: index
10451
+ });
10452
+ if (recapItemIndex !== void 0) marks.push({
10453
+ kind: "recap",
10454
+ itemIndex: recapItemIndex
10455
+ });
10456
+ return marks;
10457
+ }
10458
+ /**
10459
+ * Place marks proportionally — y = `itemIndex / itemCount` of the rail, every
10460
+ * mark one item's share tall (floored at {@link MIN_MARK}) — then merge
10461
+ * adjacent ones per lane, the loudest colour winning. The same merge rule as
10462
+ * the terminal rail; only the position source differs.
10463
+ */
10464
+ function clusterMarks(marks, railH, itemCount) {
10465
+ const count = Math.max(1, itemCount);
10466
+ const h = Math.max(2, Math.round(railH / count));
10467
+ const lanes = /* @__PURE__ */ new Map();
10468
+ for (const mark of marks) {
10469
+ const y = Math.min(Math.max(0, railH - h), Math.round(mark.itemIndex / count * railH));
10470
+ const lane = LANE[mark.kind];
10471
+ const list = lanes.get(lane) ?? [];
10472
+ list.push({
10473
+ mark,
10474
+ y
10475
+ });
10476
+ lanes.set(lane, list);
10477
+ }
10478
+ const clusters = [];
10479
+ for (const [lane, list] of lanes) {
10480
+ list.sort((a, b) => a.y - b.y);
10481
+ let current = null;
10482
+ for (const { mark, y } of list) if (current && y <= current.y + current.h + 1) {
10483
+ current.h = Math.max(current.h, y + h - current.y);
10484
+ if (LOUDNESS[mark.kind] > LOUDNESS[current.kind]) current.kind = mark.kind;
10485
+ current.marks.push({
10486
+ mark,
10487
+ y
10488
+ });
10489
+ } else {
10490
+ current = {
10491
+ lane,
10492
+ kind: mark.kind,
10493
+ y,
10494
+ h,
10495
+ marks: [{
10496
+ mark,
10497
+ y
10498
+ }]
10499
+ };
10500
+ clusters.push(current);
10501
+ }
10502
+ }
10503
+ return clusters;
10504
+ }
10505
+ /** The approval is not an item — the prompt renders below the transcript — so
10506
+ * its mark pins at the rail's foot, where the prompt is. Built by hand (no
10507
+ * item to derive a position from), hence `marks: []`. */
10508
+ function approvalCluster(railH) {
10509
+ return {
10510
+ lane: LANE.approval,
10511
+ kind: "approval",
10512
+ y: Math.max(0, railH - 2),
10513
+ h: 2,
10514
+ marks: []
10515
+ };
10516
+ }
10517
+ //#endregion
10518
+ //#region src/components/agent/Scrubber.tsx
10519
+ function peekContent(cluster, first, items, pendingApprovals) {
10520
+ const more = cluster.marks.length > 1 ? ` · ${cluster.marks.length} marks` : "";
10521
+ let body = null;
10522
+ if (cluster.kind === "approval") {
10523
+ const request = pendingApprovals[0];
10524
+ body = request ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", { children: request.title ?? "Permission required" }), /* @__PURE__ */ jsx("div", {
10525
+ className: "wd-scrub-ex",
10526
+ "data-tone": "muted",
10527
+ children: `${request.displayName ?? request.toolName}(${toolInputPreview(request.input)})`
10528
+ })] }) : null;
10529
+ } else if (first && first.kind !== "recap") {
10530
+ const item = items[first.itemIndex];
10531
+ if (first.kind === "turn" || first.kind === "turnFailed") {
10532
+ const turn = first.turnIndex === void 0 ? void 0 : items[first.turnIndex];
10533
+ body = /* @__PURE__ */ jsxs(Fragment$1, { children: [item?.kind === "assistant_text" ? /* @__PURE__ */ jsx("div", {
10534
+ className: "wd-scrub-ex",
10535
+ children: item.text
10536
+ }) : null, turn?.kind === "turn_result" ? /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
10537
+ "data-tone": turn.isError ? "danger" : "muted",
10538
+ children: doneLine(turn)
10539
+ }), turn.errors?.map((message, index) => /* @__PURE__ */ jsx("div", {
10540
+ "data-tone": "danger",
10541
+ children: message
10542
+ }, index))] }) : null] });
10543
+ } else if (item) {
10544
+ const failure = first.kind === "toolFailed" && item.kind === "tool_call" ? item.result?.text.split("\n").find((line) => line.trim() !== "") : void 0;
10545
+ body = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
10546
+ className: "wd-scrub-ex",
10547
+ "data-tone": first.kind === "error" || first.kind === "toolFailed" ? "danger" : void 0,
10548
+ children: [first.kind === "user" ? /* @__PURE__ */ jsx("span", {
10549
+ "data-tone": "muted",
10550
+ children: "❯ "
10551
+ }) : null, excerpt(item)]
10552
+ }), failure ? /* @__PURE__ */ jsx("div", {
10553
+ className: "wd-scrub-ex",
10554
+ "data-tone": "danger",
10555
+ children: failure
10556
+ }) : null] });
10557
+ }
10558
+ }
10559
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
10560
+ "data-tone": "muted",
10561
+ children: [KIND_NAME[first?.kind ?? cluster.kind], more]
10562
+ }), body] });
10563
+ }
10564
+ function Scrubber({ items, pendingApprovals, recapItemIndex, bookmarks, frameParentId, interactive = false, onJumpToItem, className }) {
10565
+ const railRef = useRef(null);
10566
+ const peekRef = useRef(null);
10567
+ const [railH, setRailH] = useState(0);
10568
+ const [peek, setPeek] = useState(null);
10569
+ useEffect(() => {
10570
+ const element = railRef.current;
10571
+ if (!element) return;
10572
+ const observer = new ResizeObserver(() => setRailH(element.clientHeight));
10573
+ observer.observe(element);
10574
+ setRailH(element.clientHeight);
10575
+ return () => observer.disconnect();
10576
+ }, []);
10577
+ useEffect(() => {
10578
+ setPeek(null);
10579
+ }, [items]);
10580
+ useLayoutEffect(() => {
10581
+ const element = peekRef.current;
10582
+ if (!element || !peek) return;
10583
+ const height = element.offsetHeight;
10584
+ const railHeight = railRef.current?.clientHeight ?? 0;
10585
+ element.style.top = `${Math.max(4, Math.min(railHeight - height - 4, peek.y - height / 2))}px`;
10586
+ }, [peek]);
10587
+ const clusters = useMemo(() => {
10588
+ if (railH <= 0) return [];
10589
+ const built = clusterMarks(buildMarks(items, {
10590
+ frameParentId,
10591
+ bookmarks,
10592
+ recapItemIndex
10593
+ }), railH, items.length);
10594
+ if (pendingApprovals.length > 0) built.push(approvalCluster(railH));
10595
+ return built;
10596
+ }, [
10597
+ items,
10598
+ frameParentId,
10599
+ bookmarks,
10600
+ recapItemIndex,
10601
+ pendingApprovals,
10602
+ railH
10603
+ ]);
10604
+ /** A pointer's y in rail space. */
10605
+ const railY = (clientY) => clientY - (railRef.current?.getBoundingClientRect().top ?? 0);
10606
+ const activate = (cluster, clientY) => {
10607
+ if (cluster.kind === "approval" && cluster.marks.length === 0) {
10608
+ if (items.length > 0) onJumpToItem?.(items.length - 1);
10609
+ return;
10610
+ }
10611
+ const mark = nearestMember(cluster, railY(clientY));
10612
+ if (mark) onJumpToItem?.(mark.itemIndex);
10613
+ };
10614
+ const showPeek = (cluster, clientY) => {
10615
+ const y = railY(clientY);
10616
+ const mark = nearestMember(cluster, y);
10617
+ setPeek((previous) => previous && previous.cluster === cluster && previous.mark === mark ? previous : {
10618
+ cluster,
10619
+ mark,
10620
+ y: Math.min(Math.max(y, cluster.y), cluster.y + cluster.h)
10621
+ });
10622
+ };
10623
+ return /* @__PURE__ */ jsxs("div", {
10624
+ ref: railRef,
10625
+ className: cn("wd-scrubber", className),
10626
+ "data-interactive": interactive || void 0,
10627
+ "aria-hidden": true,
10628
+ children: [clusters.map((cluster, index) => /* @__PURE__ */ jsx("div", {
10629
+ className: "wd-scrub-mark",
10630
+ "data-lane": cluster.lane,
10631
+ "data-kind": cluster.kind,
10632
+ style: {
10633
+ top: cluster.y,
10634
+ height: cluster.h
10635
+ },
10636
+ ...interactive ? {
10637
+ onClick: (event) => activate(cluster, event.clientY),
10638
+ onPointerEnter: (event) => showPeek(cluster, event.clientY),
10639
+ onPointerMove: (event) => showPeek(cluster, event.clientY),
10640
+ onPointerLeave: () => setPeek(null)
10641
+ } : null
10642
+ }, index)), peek ? /* @__PURE__ */ jsx("div", {
10643
+ ref: peekRef,
10644
+ className: "wd-scrub-peek",
10645
+ style: { top: peek.y },
10646
+ children: peekContent(peek.cluster, peek.mark, items, pendingApprovals)
10647
+ }) : null]
10648
+ });
10649
+ }
10650
+ //#endregion
10298
10651
  //#region src/components/agent/transcript-rows.ts
10299
10652
  /** The item a row is spaced *as*. A run stands for the calls it folded, and a
10300
10653
  * task block for the `Task` call it absorbed into — all tool calls, so a run,
@@ -10962,36 +11315,69 @@ function nestedClass(item, frameParentId) {
10962
11315
  const parent = "parentToolUseId" in item ? item.parentToolUseId : void 0;
10963
11316
  return parent != null && parent !== frameParentId ? "border-l-2 border-border pl-3" : void 0;
10964
11317
  }
11318
+ /** The cards head's one line: the prompt as a plain run of text. Deliberately
11319
+ * not the Message component — a proportional card clipped by height is a
11320
+ * sliced bubble, and un-styling one from CSS is a fight (see theme.css's
11321
+ * sticky-prompt block for the other half of this decision). Prompt syntax
11322
+ * stays literal — a 28px bar is a reminder of what was asked, not a rendering
11323
+ * surface — and newlines collapse under the bar's `nowrap`. Attachment-only
11324
+ * prompts fall back to the attachments' names so the bar is never blank. */
11325
+ function promptHeadText(item) {
11326
+ return item.text || (item.attachments ?? []).map((attachment) => attachment.name).join(", ");
11327
+ }
10965
11328
  /**
10966
11329
  * A prompt row's sticky lane — the strip spanning its turn, leading with the
10967
11330
  * one-line pinned **head** (see the pinned-prompt comment in
10968
11331
  * {@link TranscriptRows}).
10969
11332
  *
11333
+ * The head's content is the variant's: the terminal passes the row again
11334
+ * (clipping it to one line is exact under a monospace grid), cards passes the
11335
+ * prompt as plain text for theme.css to draw as a compact bar. Only the
11336
+ * terminal head carries the row's gap class — its stuck geometry parks that
11337
+ * padding above the viewport edge so the visible line docks at zero
11338
+ * (terminal.css); the cards bar never aligns with the row in flow, so the gap
11339
+ * stays off it entirely and the 1st prompt and the Nth share one geometry.
11340
+ *
10970
11341
  * The head starts `visibility: hidden` and shows only while actually stuck —
10971
11342
  * an overlay that is visible in flow would sit on the real row's first line
10972
11343
  * and swallow its selection highlight, which reads as "the first line cannot
10973
11344
  * 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 }) {
11345
+ * engage threshold (the line's own y) answers it: sentinel above the
11346
+ * scrollport top → stuck, read by a passive scroll listener. It was an
11347
+ * IntersectionObserver once, for the "no per-scroll work" purity and that
11348
+ * was a real bug: IO is edge-triggered, and an *instant* jump (the open-at-
11349
+ * bottom pin, `jumpToRow`, a reveal) teleports the sentinel from below the
11350
+ * viewport to above it between two observations — ratio 0 → 0, no threshold
11351
+ * crossed, `isIntersecting` unchanged — so no entry is ever queued and the
11352
+ * flag strands, in whichever direction the jump left it (observed: a session
11353
+ * opened at the bottom, its prompt bar missing; the stale-true twin paints a
11354
+ * bar over the real bubble). The flag needs level-triggered truth. The cost
11355
+ * is two rect reads per scroll event per mounted lane — layout is clean
11356
+ * during scrolling, and the pin itself is still the compositor's; only the
11357
+ * bar's visibility rides the listener.
11358
+ */
11359
+ function StickyPromptLane({ top, height, gapClass, gapPx, terminal, scrollRoot, index, measureRef, head, content }) {
10979
11360
  const headRef = useRef(null);
10980
11361
  const sentinelRef = useRef(null);
10981
11362
  useEffect(() => {
10982
- const head = headRef.current;
11363
+ const headElement = headRef.current;
10983
11364
  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]);
11365
+ if (!headElement || !sentinel || !scrollRoot) return;
11366
+ const evaluate = () => {
11367
+ const stuck = sentinel.getBoundingClientRect().top < scrollRoot.getBoundingClientRect().top;
11368
+ headElement.toggleAttribute("data-stuck", stuck);
11369
+ };
11370
+ evaluate();
11371
+ scrollRoot.addEventListener("scroll", evaluate, { passive: true });
11372
+ return () => scrollRoot.removeEventListener("scroll", evaluate);
11373
+ }, [
11374
+ scrollRoot,
11375
+ top,
11376
+ height,
11377
+ gapClass
11378
+ ]);
10993
11379
  return /* @__PURE__ */ jsxs("div", {
10994
- "data-sticky-lane": "",
11380
+ "data-sticky-lane": terminal ? "terminal" : "cards",
10995
11381
  className: "absolute inset-x-0",
10996
11382
  style: {
10997
11383
  top,
@@ -11004,8 +11390,8 @@ function StickyPromptLane({ top, height, gapClass, scrollRoot, index, measureRef
11004
11390
  children: /* @__PURE__ */ jsx("div", {
11005
11391
  ref: headRef,
11006
11392
  "data-sticky-head": "",
11007
- className: gapClass || void 0,
11008
- children: content
11393
+ className: terminal && gapClass || void 0,
11394
+ children: head
11009
11395
  })
11010
11396
  }),
11011
11397
  /* @__PURE__ */ jsx("div", {
@@ -11013,7 +11399,7 @@ function StickyPromptLane({ top, height, gapClass, scrollRoot, index, measureRef
11013
11399
  "aria-hidden": true,
11014
11400
  className: "absolute left-0 w-px",
11015
11401
  style: {
11016
- top: gapClass ? "var(--term-line)" : 0,
11402
+ top: gapClass ? terminal ? "var(--term-line)" : gapPx : 0,
11017
11403
  height: 1
11018
11404
  }
11019
11405
  }),
@@ -11069,7 +11455,7 @@ function TranscriptRows({ rows, boundary, since, terminal, replaying, stickyProm
11069
11455
  promptRows: []
11070
11456
  });
11071
11457
  pinRef.current = {
11072
- enabled: terminal && stickyPrompt,
11458
+ enabled: stickyPrompt,
11073
11459
  promptRows
11074
11460
  };
11075
11461
  useEffect(() => {
@@ -11265,16 +11651,19 @@ function TranscriptRows({ rows, boundary, since, terminal, replaying, stickyProm
11265
11651
  onOpenSubagent
11266
11652
  })
11267
11653
  });
11268
- if (terminal && stickyPrompt && "item" in row && row.item.kind === "user" && parentOf(row.item) === void 0) {
11654
+ if (stickyPrompt && "item" in row && row.item.kind === "user" && parentOf(row.item) === void 0) {
11269
11655
  const next = promptRows.find((index) => index > virtualRow.index);
11270
11656
  const laneEnd = next === void 0 ? virtualizer.getTotalSize() : measurements[next]?.start ?? virtualRow.start;
11271
11657
  return /* @__PURE__ */ jsx(StickyPromptLane, {
11272
11658
  top: virtualRow.start,
11273
11659
  height: Math.max(laneEnd - virtualRow.start, 0),
11274
11660
  gapClass,
11661
+ gapPx: gap.px,
11662
+ terminal,
11275
11663
  scrollRoot: scrollElement,
11276
11664
  index: virtualRow.index,
11277
11665
  measureRef: virtualizer.measureElement,
11666
+ head: terminal ? content : promptHeadText(row.item),
11278
11667
  content
11279
11668
  }, row.key);
11280
11669
  }
@@ -11285,7 +11674,7 @@ function TranscriptRows({ rows, boundary, since, terminal, replaying, stickyProm
11285
11674
  style: { transform: `translateY(${virtualRow.start}px)` },
11286
11675
  children: content
11287
11676
  }, row.key);
11288
- }), terminal && scrubber && scrollElement?.parentElement ? createPortal(/* @__PURE__ */ jsx(TerminalScrubber, {
11677
+ }), scrubber && scrollElement?.parentElement ? createPortal(terminal ? /* @__PURE__ */ jsx(TerminalScrubber, {
11289
11678
  items,
11290
11679
  pendingApprovals,
11291
11680
  recapRow,
@@ -11302,10 +11691,18 @@ function TranscriptRows({ rows, boundary, since, terminal, replaying, stickyProm
11302
11691
  interactive: scrubInteractive,
11303
11692
  fontSize,
11304
11693
  lineHeight
11694
+ }) : /* @__PURE__ */ jsx(Scrubber, {
11695
+ items,
11696
+ pendingApprovals,
11697
+ recapItemIndex: recapIndex >= 0 ? boundary : void 0,
11698
+ bookmarks: scrubberMarks,
11699
+ frameParentId,
11700
+ interactive: scrubInteractive,
11701
+ onJumpToItem: (itemIndex) => jumpToRow(rowIndexForItem(rows, itemIndex), "start")
11305
11702
  }), scrollElement.parentElement) : null]
11306
11703
  });
11307
11704
  }
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 }) {
11705
+ 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
11706
  const terminal = variant === "terminal";
11310
11707
  const items = useMemo(() => frame ? subagentItems(state.items, frame.parentToolUseId) : state.items, [state.items, frame]);
11311
11708
  const frameTask = useMemo(() => frame ? state.items.find((item) => item.kind === "tool_call" && item.id === frame.parentToolUseId) : void 0, [state.items, frame]);
@@ -11356,7 +11753,7 @@ function Transcript({ state, fileUrl, attachmentUrl, canBrowseFiles, hostImage,
11356
11753
  frame ? frameTask === void 0 && !replaying ? /* @__PURE__ */ jsx("div", {
11357
11754
  className: cn(terminal ? "term-row text-fg-4" : "p-4 text-body-sm text-fg-4"),
11358
11755
  children: "This sub-agent's work is not in this transcript."
11359
- }) : null : items.length === 0 && state.status !== "starting" ? /* @__PURE__ */ jsx(SessionEmptyState, {
11756
+ }) : null : items.length === 0 && state.status !== "starting" ? emptyState !== void 0 ? emptyState : /* @__PURE__ */ jsx(SessionEmptyState, {
11360
11757
  cwd: state.cwd,
11361
11758
  hasCommands: !!state.commands?.length,
11362
11759
  hasSkills: !!state.skills?.some((s) => s.enabled),
@@ -11407,7 +11804,7 @@ function Transcript({ state, fileUrl, attachmentUrl, canBrowseFiles, hostImage,
11407
11804
  "aria-hidden": true,
11408
11805
  className: "wd-hold-appear visible pointer-events-none absolute inset-0 overflow-hidden",
11409
11806
  children: /* @__PURE__ */ jsx("div", {
11410
- className: cn("mx-auto w-full max-w-[var(--wd-content-max-w,48rem)]", !terminal && "px-4 py-4"),
11807
+ className: cn("mx-auto w-full max-w-[var(--wd-transcript-max-width)]", !terminal && "px-4 py-4"),
11411
11808
  children: terminal ? /* @__PURE__ */ jsx(TerminalSurface, {
11412
11809
  fontSize,
11413
11810
  lineHeight,
@@ -11531,42 +11928,45 @@ function UsageMeter({ window, now }) {
11531
11928
  * The session's view of the plan: every window as a meter (see
11532
11929
  * {@link UsageMeters}), the plan tier, and what this session itself has cost.
11533
11930
  */
11534
- function UsageDialog({ rateLimits, subscriptionType, engine, totalCostUsd, updatedAt, open, onOpenChange }) {
11931
+ function UsageDialog({ rateLimits, subscriptionType, engine, totalCostUsd, updatedAt, open, onOpenChange, className }) {
11535
11932
  const now = useMinuteClock(open);
11536
11933
  return /* @__PURE__ */ jsx(Dialog$1, {
11537
11934
  open,
11538
11935
  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
- ] })] })
11936
+ children: /* @__PURE__ */ jsxs(DialogContent, {
11937
+ className,
11938
+ children: [/* @__PURE__ */ jsx(DialogHeader, {
11939
+ title: "Usage",
11940
+ description: engine === "claude" ? "Claude Code" : engine,
11941
+ actions: subscriptionType ? /* @__PURE__ */ jsx(Badge, {
11942
+ variant: "accent",
11943
+ className: "mt-0.5 shrink-0 capitalize",
11944
+ children: subscriptionType
11945
+ }) : null
11946
+ }), /* @__PURE__ */ jsxs(DialogBody, { children: [
11947
+ rateLimits.length === 0 ? /* @__PURE__ */ jsx("p", {
11948
+ className: "py-6 text-center text-body-sm text-fg-4",
11949
+ 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.`
11950
+ }) : /* @__PURE__ */ jsx(UsageMeters, {
11951
+ windows: rateLimits,
11952
+ now
11953
+ }),
11954
+ /* @__PURE__ */ jsxs("div", {
11955
+ className: "mt-5 flex items-baseline justify-between gap-4 border-t border-border pt-3",
11956
+ children: [/* @__PURE__ */ jsx("span", {
11957
+ className: "text-label text-fg-3",
11958
+ children: "This session has cost"
11959
+ }), /* @__PURE__ */ jsx("span", {
11960
+ className: "font-mono text-body-sm text-fg-1",
11961
+ children: formatCost(totalCostUsd)
11962
+ })]
11963
+ }),
11964
+ updatedAt ? /* @__PURE__ */ jsxs("p", {
11965
+ className: "mt-2 text-label text-fg-4",
11966
+ children: ["Updated ", formatAgoPrecise(updatedAt, now)]
11967
+ }) : null
11968
+ ] })]
11969
+ })
11570
11970
  });
11571
11971
  }
11572
11972
  //#endregion
@@ -11581,7 +11981,7 @@ function UsageDialog({ rateLimits, subscriptionType, engine, totalCostUsd, updat
11581
11981
  */
11582
11982
  function PromptSurface({ terminal, metrics, affordances, children }) {
11583
11983
  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",
11984
+ className: "mx-auto flex w-full max-w-[var(--wd-transcript-max-width)] flex-col gap-2",
11585
11985
  children
11586
11986
  });
11587
11987
  return /* @__PURE__ */ jsx(TerminalSurface, {
@@ -11620,7 +12020,7 @@ const INTERACTIVE = [
11620
12020
  * the engine name — an absent capability hides the control instead of offering
11621
12021
  * one that can only fail.
11622
12022
  */
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, stickyPrompt = false, controlsSurface = "internal", onControls, focusComposerOnClick = false, unseen, readOnly = false, toolHost, cacheTranscript, className }) {
12023
+ 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, emptyState, className }) {
11624
12024
  const external = panelSurface === "external";
11625
12025
  const statusExternal = statusSurface === "external";
11626
12026
  const controlsInStatus = controlsSurface === "status" && !statusExternal;
@@ -11657,25 +12057,25 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11657
12057
  setSubagentId(void 0);
11658
12058
  setReturnReveal(void 0);
11659
12059
  }, [sessionId]);
12060
+ const leaveSubagent = useCallback(() => {
12061
+ setSubagentId((current) => {
12062
+ if (current !== void 0) setReturnReveal({
12063
+ toolUseId: current,
12064
+ nonce: Date.now()
12065
+ });
12066
+ });
12067
+ }, []);
11660
12068
  const openSubagentNonce = openSubagent?.nonce;
11661
12069
  const openSubagentId = openSubagent?.toolUseId;
11662
12070
  useEffect(() => {
11663
- if (openSubagentId === void 0) return;
11664
- setSubagentId(openSubagentId);
12071
+ if (openSubagentId === void 0) leaveSubagent();
12072
+ else setSubagentId(openSubagentId);
11665
12073
  }, [openSubagentNonce]);
11666
12074
  const revealNonce = reveal?.nonce;
11667
12075
  useEffect(() => {
11668
12076
  if (revealNonce === void 0) return;
11669
12077
  setSubagentId(void 0);
11670
12078
  }, [revealNonce]);
11671
- const leaveSubagent = useCallback(() => {
11672
- setSubagentId((current) => {
11673
- if (current !== void 0) setReturnReveal({
11674
- toolUseId: current,
11675
- nonce: Date.now()
11676
- });
11677
- });
11678
- }, []);
11679
12079
  useEffect(() => {
11680
12080
  if (subagentId === void 0) return;
11681
12081
  const onKey = (event) => {
@@ -11685,6 +12085,14 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11685
12085
  window.addEventListener("keydown", onKey);
11686
12086
  return () => window.removeEventListener("keydown", onKey);
11687
12087
  }, [subagentId, leaveSubagent]);
12088
+ const onSubagentChangeRef = useRef(onSubagentChange);
12089
+ onSubagentChangeRef.current = onSubagentChange;
12090
+ const reportedSubagentId = useRef(void 0);
12091
+ useEffect(() => {
12092
+ if (reportedSubagentId.current === subagentId) return;
12093
+ reportedSubagentId.current = subagentId;
12094
+ onSubagentChangeRef.current?.(subagentId);
12095
+ }, [subagentId]);
11688
12096
  const [caughtUp, setCaughtUp] = useState(false);
11689
12097
  useEffect(() => {
11690
12098
  setCaughtUp(false);
@@ -11743,7 +12151,8 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11743
12151
  cwd: state.cwd,
11744
12152
  contextUsage: state.contextUsage,
11745
12153
  rateLimits,
11746
- itemCount: state.items.length
12154
+ itemCount: state.items.length,
12155
+ totalCostUsd: state.totalCostUsd
11747
12156
  });
11748
12157
  }, [
11749
12158
  state.status,
@@ -11757,7 +12166,8 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11757
12166
  state.cwd,
11758
12167
  state.contextUsage,
11759
12168
  rateLimits,
11760
- state.items.length
12169
+ state.items.length,
12170
+ state.totalCostUsd
11761
12171
  ]);
11762
12172
  const onControlsRef = useRef(onControls);
11763
12173
  onControlsRef.current = onControls;
@@ -11946,6 +12356,7 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11946
12356
  reveal: returnReveal ?? reveal,
11947
12357
  frame: subagentId === void 0 ? void 0 : { parentToolUseId: subagentId },
11948
12358
  onOpenSubagent: setSubagentId,
12359
+ emptyState,
11949
12360
  jumpToRecapRef: jumpToRecap,
11950
12361
  repinRef: repinTranscript
11951
12362
  }, subagentId ?? "session"),
@@ -11953,7 +12364,7 @@ function SessionPanel({ client, sessionId, header, panelSurface = "internal", st
11953
12364
  className: "px-3 pb-1",
11954
12365
  children: /* @__PURE__ */ jsxs("div", {
11955
12366
  "data-slot": "catch-up",
11956
- className: "mx-auto flex w-full max-w-[var(--wd-content-max-w,48rem)] items-center gap-2 text-label text-fg-3",
12367
+ className: "mx-auto flex w-full max-w-[var(--wd-transcript-max-width)] items-center gap-2 text-label text-fg-3",
11957
12368
  children: [
11958
12369
  /* @__PURE__ */ jsx("span", {
11959
12370
  "aria-hidden": true,
@@ -12151,7 +12562,7 @@ function Notice({ level, onDismiss, children }) {
12151
12562
  className: "px-3 pt-2",
12152
12563
  children: /* @__PURE__ */ jsxs("div", {
12153
12564
  role: "alert",
12154
- 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"),
12565
+ 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"),
12155
12566
  children: [
12156
12567
  /* @__PURE__ */ jsx(TriangleAlert, { className: "mt-0.5 size-3.5 shrink-0" }),
12157
12568
  /* @__PURE__ */ jsx("span", {
@@ -12170,6 +12581,6 @@ function Notice({ level, onDismiss, children }) {
12170
12581
  });
12171
12582
  }
12172
12583
  //#endregion
12173
- 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 };
12584
+ 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 };
12174
12585
 
12175
- //# sourceMappingURL=SessionPanel-CHktI2CF.mjs.map
12586
+ //# sourceMappingURL=SessionPanel-DgwjH4Ve.mjs.map