@dxos/react-ui-assistant 0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264 → 0.13.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 (96) hide show
  1. package/dist/lib/index.mjs +891 -353
  2. package/dist/lib/index.mjs.map +1 -1
  3. package/dist/lib/testing.mjs +22 -3
  4. package/dist/lib/testing.mjs.map +1 -1
  5. package/dist/lib/translations.mjs +19 -2
  6. package/dist/lib/translations.mjs.map +1 -1
  7. package/dist/lib/types.mjs +34 -1
  8. package/dist/lib/types.mjs.map +1 -1
  9. package/dist/types/src/components/ChatThread/ChatThread.d.ts +21 -11
  10. package/dist/types/src/components/ChatThread/ChatThread.d.ts.map +1 -1
  11. package/dist/types/src/components/ChatThread/ChatThread.stories.d.ts +21 -1
  12. package/dist/types/src/components/ChatThread/ChatThread.stories.d.ts.map +1 -1
  13. package/dist/types/src/components/ChatThread/index.d.ts +1 -1
  14. package/dist/types/src/components/ChatThread/index.d.ts.map +1 -1
  15. package/dist/types/src/components/MessageChrome/MessageChrome.d.ts +14 -11
  16. package/dist/types/src/components/MessageChrome/MessageChrome.d.ts.map +1 -1
  17. package/dist/types/src/components/MessageChrome/MessageChrome.stories.d.ts +27 -2
  18. package/dist/types/src/components/MessageChrome/MessageChrome.stories.d.ts.map +1 -1
  19. package/dist/types/src/components/MessageChrome/format-time.d.ts.map +1 -1
  20. package/dist/types/src/components/MessageChrome/index.d.ts +1 -1
  21. package/dist/types/src/components/MessageChrome/index.d.ts.map +1 -1
  22. package/dist/types/src/components/index.d.ts +2 -2
  23. package/dist/types/src/components/index.d.ts.map +1 -1
  24. package/dist/types/src/index.d.ts +5 -5
  25. package/dist/types/src/index.d.ts.map +1 -1
  26. package/dist/types/src/registry.d.ts +7 -1
  27. package/dist/types/src/registry.d.ts.map +1 -1
  28. package/dist/types/src/registry.stories.d.ts +63 -1
  29. package/dist/types/src/registry.stories.d.ts.map +1 -1
  30. package/dist/types/src/renderer.d.ts +18 -5
  31. package/dist/types/src/renderer.d.ts.map +1 -1
  32. package/dist/types/src/renderer.test.d.ts +2 -0
  33. package/dist/types/src/renderer.test.d.ts.map +1 -0
  34. package/dist/types/src/testing/index.d.ts +1 -1
  35. package/dist/types/src/testing/index.d.ts.map +1 -1
  36. package/dist/types/src/testing/test-generator.d.ts +6 -0
  37. package/dist/types/src/testing/test-generator.d.ts.map +1 -1
  38. package/dist/types/src/translations.d.ts +18 -1
  39. package/dist/types/src/translations.d.ts.map +1 -1
  40. package/dist/types/src/types.d.ts +37 -0
  41. package/dist/types/src/types.d.ts.map +1 -1
  42. package/dist/types/src/widgets/DeliveryWidget.d.ts +20 -0
  43. package/dist/types/src/widgets/DeliveryWidget.d.ts.map +1 -0
  44. package/dist/types/src/widgets/FallbackWidget.d.ts +2 -2
  45. package/dist/types/src/widgets/FallbackWidget.d.ts.map +1 -1
  46. package/dist/types/src/widgets/ReasoningWidget.d.ts.map +1 -1
  47. package/dist/types/src/widgets/ReferenceWidget.d.ts +2 -2
  48. package/dist/types/src/widgets/RequestWidget.d.ts +12 -0
  49. package/dist/types/src/widgets/RequestWidget.d.ts.map +1 -0
  50. package/dist/types/src/widgets/SelectWidget.d.ts.map +1 -1
  51. package/dist/types/src/widgets/SuggestionWidget.d.ts.map +1 -1
  52. package/dist/types/src/widgets/SummaryWidget.d.ts +2 -2
  53. package/dist/types/src/widgets/SummaryWidget.d.ts.map +1 -1
  54. package/dist/types/src/widgets/SyntheticWidget.d.ts +11 -0
  55. package/dist/types/src/widgets/SyntheticWidget.d.ts.map +1 -0
  56. package/dist/types/src/widgets/ToolWidget.d.ts +3 -3
  57. package/dist/types/src/widgets/ToolWidget.d.ts.map +1 -1
  58. package/dist/types/src/widgets/WidgetPanel.d.ts +20 -0
  59. package/dist/types/src/widgets/WidgetPanel.d.ts.map +1 -0
  60. package/dist/types/src/widgets/index.d.ts +12 -9
  61. package/dist/types/src/widgets/index.d.ts.map +1 -1
  62. package/dist/types/src/widgets/submit-button.d.ts +7 -0
  63. package/dist/types/src/widgets/submit-button.d.ts.map +1 -0
  64. package/dist/types/tsconfig.tsbuildinfo +1 -1
  65. package/package.json +22 -22
  66. package/src/components/ChatThread/ChatThread.stories.tsx +113 -17
  67. package/src/components/ChatThread/ChatThread.tsx +137 -20
  68. package/src/components/ChatThread/index.ts +1 -1
  69. package/src/components/MessageChrome/MessageChrome.stories.tsx +50 -11
  70. package/src/components/MessageChrome/MessageChrome.tsx +120 -44
  71. package/src/components/MessageChrome/format-time.ts +6 -1
  72. package/src/components/MessageChrome/index.ts +1 -1
  73. package/src/components/index.ts +2 -2
  74. package/src/index.ts +5 -5
  75. package/src/registry.stories.tsx +479 -26
  76. package/src/registry.tsx +65 -35
  77. package/src/renderer.test.ts +235 -0
  78. package/src/renderer.ts +182 -34
  79. package/src/testing/index.ts +1 -1
  80. package/src/testing/test-generator.test.ts +3 -5
  81. package/src/testing/test-generator.ts +28 -11
  82. package/src/translations.ts +20 -3
  83. package/src/types.ts +46 -3
  84. package/src/widgets/DeliveryWidget.ts +75 -0
  85. package/src/widgets/FallbackWidget.tsx +14 -16
  86. package/src/widgets/ReasoningWidget.ts +10 -5
  87. package/src/widgets/ReferenceWidget.ts +3 -3
  88. package/src/widgets/RequestWidget.tsx +69 -0
  89. package/src/widgets/SelectWidget.ts +9 -11
  90. package/src/widgets/SuggestionWidget.ts +7 -17
  91. package/src/widgets/SummaryWidget.tsx +10 -14
  92. package/src/widgets/SyntheticWidget.tsx +57 -0
  93. package/src/widgets/ToolWidget.tsx +365 -148
  94. package/src/widgets/WidgetPanel.tsx +74 -0
  95. package/src/widgets/index.ts +12 -9
  96. package/src/widgets/submit-button.ts +19 -0
@@ -1,31 +1,131 @@
1
+ import { ChatView, DeliveryAnnotation, DeliveryStatus, getDelivery, isUnread } from "./types.mjs";
1
2
  import { translationKey, translations } from "./translations.mjs";
2
- import { ChatView } from "./types.mjs";
3
- import { createContext } from "@radix-ui/react-context";
4
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
- import { MessageList, isPrompt } from "@dxos/react-ui-feed";
6
- import { AnchorWidget, getXmlTextChild } from "@dxos/ui-editor";
7
- import { NumericTabs, TogglePanel } from "@dxos/react-ui-components";
8
- import { JsonHighlighter } from "@dxos/react-ui-syntax-highlighter";
9
- import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { memo, useCallback, useEffect, useMemo, useState } from "react";
4
+ import { MessageList, isPrompt, useMessageList } from "@dxos/react-ui-feed";
5
+ import * as Button from "@dxos/react-ui/Button";
6
+ import * as Hooks from "@dxos/react-ui/Hooks";
7
+ import * as Schema from "effect/Schema";
8
+ import { getXmlInnerText, getXmlTextChild } from "@dxos/ui-editor";
10
9
  import { WidgetType } from "@codemirror/view";
11
10
  import { Domino, mx } from "@dxos/ui";
12
- import { Icon, IconBlock, IconButton, TextCrawl, useTranslation } from "@dxos/react-ui";
11
+ import { getSize, getStyles, mx as mx$1 } from "@dxos/ui-theme";
12
+ import { JsonHighlighter, SyntaxHighlighter } from "@dxos/react-ui-syntax-highlighter";
13
+ import { TogglePanel } from "@dxos/react-ui-components";
14
+ import * as Icon from "@dxos/react-ui/Icon";
15
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
16
+ import * as Card from "@dxos/react-ui/Card";
17
+ import * as Layout from "@dxos/react-ui/Layout";
18
+ import { ContentBlock, Message } from "@dxos/types";
13
19
  import { safeParseJson } from "@dxos/util";
14
- import { Message } from "@dxos/types";
15
- import { mx as mx$1 } from "@dxos/ui-theme";
16
- //#region src/widgets/FallbackWidget.tsx
17
- var FallbackWidget = ({ _tag, ...props }) => {
18
- return /* @__PURE__ */ jsx(TogglePanel.Root, { children: /* @__PURE__ */ jsxs(TogglePanel.Content, { children: [/* @__PURE__ */ jsx(TogglePanel.Header, {
19
- classNames: "dx-group-surface",
20
- children: _tag
21
- }), /* @__PURE__ */ jsx(TogglePanel.Body, {
22
- classNames: "dx-modal-surface",
23
- children: /* @__PURE__ */ jsx(TogglePanel.Viewport, { children: /* @__PURE__ */ jsx(JsonHighlighter, {
24
- classNames: "p-2! text-sm",
25
- data: props
26
- }) })
27
- })] }) });
20
+ import * as Accordion from "@dxos/react-ui/Accordion";
21
+ import * as SystemButton from "@dxos/react-ui/SystemButton";
22
+ //#region src/widgets/DeliveryWidget.ts
23
+ var ICONS = {
24
+ sent: "ph--check--regular",
25
+ delivered: "ph--checks--regular",
26
+ read: "ph--checks--bold",
27
+ failed: "ph--warning-circle--regular"
28
28
  };
29
+ /**
30
+ * The delivery ticks under a prompt still on its way to the agent, with its remove control.
31
+ *
32
+ * Every state is one line of the same height, so a prompt moving from sent to read never moves the
33
+ * rows below it. The controls are `data-action` buttons the thread's delegated listener turns into
34
+ * events, as the suggestion chips are, which keeps the widget renderable from its tag alone.
35
+ */
36
+ var DeliveryWidget = class extends WidgetType {
37
+ status;
38
+ messageId;
39
+ labels;
40
+ constructor(status, messageId, labels) {
41
+ super();
42
+ this.status = status;
43
+ this.messageId = messageId;
44
+ this.labels = labels;
45
+ }
46
+ eq(other) {
47
+ return this.status === other.status && this.messageId === other.messageId && this.labels === other.labels;
48
+ }
49
+ toDOM() {
50
+ const status = this.status;
51
+ const root = Domino.of("div").classNames(mx("flex justify-end items-center gap-2 pt-1 text-xs leading-4", status === "failed" ? "text-error-text" : status === "read" ? "text-accent-text" : "text-fg-subtle")).attributes({
52
+ "data-testid": "chat.delivery",
53
+ "data-delivery": status
54
+ });
55
+ if (status === "failed" || status === "delivered") root.append(this.#action("remove"));
56
+ root.append(Domino.of("dx-icon").classNames(getSize(4)).attributes({
57
+ "icon": ICONS[status],
58
+ "role": "img",
59
+ "aria-label": this.labels[status],
60
+ "title": this.labels[status]
61
+ }));
62
+ return root.root;
63
+ }
64
+ #action(action) {
65
+ return Domino.of("button").attributes({
66
+ "type": "button",
67
+ "data-action": action,
68
+ "data-value": this.messageId
69
+ }).classNames("underline underline-offset-2 hover:text-fg-default").text(this.labels[action]);
70
+ }
71
+ };
72
+ //#endregion
73
+ //#region src/widgets/WidgetPanel.tsx
74
+ /** The bordered box a disclosure opens onto. */
75
+ var PANEL_FRAME = "border border-separator rounded-md min-w-0";
76
+ /**
77
+ * A chat widget that reads as one line of prose with a disclosure caret at its end, opening onto
78
+ * its detail — the shared shape of every collapsible widget in the thread.
79
+ */
80
+ var WidgetPanel = ({ icon, label, suffix, error, testId, onChangeOpen, lazyMount, children }) => /* @__PURE__ */ jsxs(TogglePanel.Root, {
81
+ onChangeOpen,
82
+ lazyMount,
83
+ classNames: "w-0 min-w-full",
84
+ children: [/* @__PURE__ */ jsx(TogglePanel.Header, {
85
+ caret: "end",
86
+ "data-testid": testId,
87
+ classNames: "group/disclosure gap-1",
88
+ children: /* @__PURE__ */ jsxs("span", {
89
+ className: "flex min-w-0 items-center gap-2 text-fg-muted tabular-nums",
90
+ children: [
91
+ /* @__PURE__ */ jsx(Icon.Icon, {
92
+ icon,
93
+ size: "md"
94
+ }),
95
+ /* @__PURE__ */ jsx("span", {
96
+ className: mx$1("truncate group-hover/disclosure:text-fg", error && "text-error-text"),
97
+ children: label
98
+ }),
99
+ suffix
100
+ ]
101
+ })
102
+ }), /* @__PURE__ */ jsx(TogglePanel.Body, { children })]
103
+ });
104
+ /** The same row without a caret, for a widget with nothing to open onto. */
105
+ var WidgetPanelRow = ({ icon, label, testId }) => /* @__PURE__ */ jsxs("div", {
106
+ className: "flex items-center gap-2 p-1 text-fg-muted min-h-(--dx-control)",
107
+ "data-testid": testId,
108
+ children: [/* @__PURE__ */ jsx(Icon.Icon, {
109
+ icon,
110
+ size: "md"
111
+ }), /* @__PURE__ */ jsx("span", {
112
+ className: "truncate",
113
+ children: label
114
+ })]
115
+ });
116
+ //#endregion
117
+ //#region src/widgets/FallbackWidget.tsx
118
+ var FallbackWidget = ({ _tag, ...props }) => /* @__PURE__ */ jsx(WidgetPanel, {
119
+ icon: "ph--brackets-curly--regular",
120
+ label: _tag,
121
+ testId: "assistant.fallback",
122
+ lazyMount: true,
123
+ children: /* @__PURE__ */ jsx(JsonHighlighter, {
124
+ data: props,
125
+ scroll: "horizontal",
126
+ classNames: mx$1(PANEL_FRAME, "p-trim-sm text-xs bg-transparent")
127
+ })
128
+ });
29
129
  FallbackWidget.displayName = "Fallback";
30
130
  //#endregion
31
131
  //#region src/widgets/ReasoningWidget.ts
@@ -46,7 +146,7 @@ var ReasoningWidget = class extends WidgetType {
46
146
  return this.text === other.text && this.#pos === other.#pos;
47
147
  }
48
148
  toDOM() {
49
- return Domino.of("div").append(Domino.of("div").classNames("relative overflow-hidden p-px border border-subdued-separator rounded-md").attributes({ "data-trail-container": "" }).append(Domino.of("div").classNames("relative z-10 dx-base-surface rounded-md text-sm text-subdued py-1", "grid grid-cols-[24px_1fr] gap-x-0.5 items-start").append(Domino.of("div").classNames("flex h-5 w-full shrink-0 items-center justify-center self-start").append(Domino.svg("ph--brain--regular").classNames("shrink-0 size-4 opacity-70")), Domino.of("div").classNames("px-2 max-h-[5lh] overflow-y-auto dx-scrollbar-thin min-w-0").text(this.text).attributes({ "data-reasoning-text": "" })), Domino.of("div").attributes({ "data-id": this.#pos }))).root;
149
+ return Domino.of("div").append(Domino.of("div").classNames("relative overflow-hidden p-px border border-separator-subtle rounded-md").attributes({ "data-trail-container": "" }).append(Domino.of("div").classNames("relative z-10 dx-base-surface rounded-md text-sm p-1", "grid grid-cols-[24px_1fr] gap-x-0.5 items-start").append(Domino.of("div").classNames("flex h-[1lh] shrink-0 items-center justify-center self-start").append(Domino.svg("ph--brain--regular").classNames("shrink-0 size-4")), Domino.of("div").classNames("flex items-start max-h-[5lh] overflow-y-auto dx-scrollbar-thin text-fg-muted tabular-nums").text(this.text).attributes({ "data-reasoning-text": "" })), Domino.of("div").attributes({ "data-id": this.#pos }))).root;
50
150
  }
51
151
  updateDOM(dom) {
52
152
  dom.querySelector("[data-reasoning-text]")?.replaceChildren(this.text);
@@ -101,20 +201,66 @@ var createTrailLayers = () => [Domino.of("div").classNames(...trail, "animate-tr
101
201
  //#region src/widgets/ReferenceWidget.ts
102
202
  var ReferenceWidget = class extends WidgetType {
103
203
  text;
104
- dxn;
105
- constructor(text, dxn) {
204
+ eid;
205
+ constructor(text, eid) {
106
206
  super();
107
207
  this.text = text;
108
- this.dxn = dxn;
208
+ this.eid = eid;
109
209
  }
110
210
  eq(other) {
111
- return this.dxn === other.dxn;
211
+ return this.eid === other.eid;
112
212
  }
113
213
  toDOM() {
114
- return Domino.of("span").classNames("inline-flex align-baseline").append(Domino.of("dx-anchor").classNames("dx-tag--anchor").attributes({ dxn: this.dxn }).text(this.text)).root;
214
+ return Domino.of("span").classNames("inline-flex align-baseline").append(Domino.of("dx-anchor").classNames("dx-tag-anchor").attributes({ eid: this.eid }).text(this.text)).root;
115
215
  }
116
216
  };
117
217
  //#endregion
218
+ //#region src/widgets/RequestWidget.tsx
219
+ var decodeRequest = Schema.decodeUnknownOption(ContentBlock.Request);
220
+ /**
221
+ * An agent waiting for permission, as a card with the options it offered. A choice is reported
222
+ * through the thread's delegated `respond` action, like the select and suggestion widgets' `submit`.
223
+ * Once answered (or abandoned) the card shows the outcome instead of the buttons.
224
+ */
225
+ var RequestWidget = ({ children, message }) => {
226
+ const { t } = Hooks.useTranslation(translationKey);
227
+ const request = useMemo(() => decodeRequest(safeParseJson(getXmlTextChild(children ?? []) ?? "")), [children]);
228
+ if (request._tag === "None" || typeof message !== "string") return null;
229
+ const { requestId, title, options, resolution } = request.value;
230
+ const chosen = options.find((option) => option.id === resolution?.optionId);
231
+ return /* @__PURE__ */ jsxs(Card.Root, {
232
+ "data-testid": "assistant.request",
233
+ children: [/* @__PURE__ */ jsxs(Card.Header, { children: [/* @__PURE__ */ jsx(Layout.Block, { children: /* @__PURE__ */ jsx(Icon.Icon, { icon: "ph--shield-warning--regular" }) }), /* @__PURE__ */ jsx(Card.Title, { children: title })] }), /* @__PURE__ */ jsx(Card.Body, { children: resolution ? /* @__PURE__ */ jsx(Card.Description, { children: chosen ? t("request.answered.label", { option: chosen.label }) : t("request.cancelled.label") }) : /* @__PURE__ */ jsx(Layout.Flex, {
234
+ role: "group",
235
+ wrap: true,
236
+ gap: "sm",
237
+ children: options.map((option) => /* @__PURE__ */ jsx(Button.Root, {
238
+ variant: option.kind.startsWith("allow") ? "primary" : "default",
239
+ "data-action": "respond",
240
+ "data-message": message,
241
+ "data-request": requestId,
242
+ "data-option": option.id,
243
+ children: option.label
244
+ }, option.id))
245
+ }) })]
246
+ });
247
+ };
248
+ //#endregion
249
+ //#region src/widgets/submit-button.ts
250
+ /**
251
+ * A button that submits its text as the reader's next prompt, through the thread's delegated
252
+ * `submit` action — shared by the suggestion and select widgets so the two read as one control.
253
+ */
254
+ var submitButton = (value, icon) => {
255
+ const button = Domino.of("button").attributes({
256
+ "data-size": "md",
257
+ "data-action": "submit",
258
+ "data-value": value
259
+ }).classNames("dx-control dx-button dx-container-query-inline-size gap-2 min-w-0 overflow-hidden");
260
+ if (icon) button.append(Domino.svg(icon).classNames("shrink-0 size-4 text-fg-muted"));
261
+ return button.append(Domino.of("span").classNames("truncate min-w-0").text(value));
262
+ };
263
+ //#endregion
118
264
  //#region src/widgets/SelectWidget.ts
119
265
  /**
120
266
  * Simple prompt widget.
@@ -132,11 +278,7 @@ var SelectWidget = class extends WidgetType {
132
278
  * NOTE: Container must set var based on user's identity.
133
279
  */
134
280
  toDOM() {
135
- return Domino.of("div").attributes({ role: "group" }).classNames("flex flex-wrap gap-1").append(...this.options.map((option) => Domino.of("button").classNames("dx-button inline-block max-w-[100cqi]").attributes({
136
- "data-action": "submit",
137
- "data-value": option,
138
- "data-density": "md"
139
- }).text(option))).root;
281
+ return Domino.of("div").attributes({ role: "group" }).classNames("flex flex-wrap gap-x-2 gap-y-2").append(...this.options.map((option) => submitButton(option))).root;
140
282
  }
141
283
  };
142
284
  //#endregion
@@ -198,302 +340,124 @@ var SuggestionWidget = class extends WidgetType {
198
340
  return this.text === other.text;
199
341
  }
200
342
  toDOM() {
201
- return Domino.of("span").classNames(mx("inline-flex max-w-[calc(100cqi-8px)] overflow-hidden")).append(Domino.of("button").attributes({
202
- "data-density": "md",
203
- "data-action": "submit",
204
- "data-value": this.text
205
- }).classNames(mx("dx-button gap-2 w-full overflow-hidden")).append(Domino.of("dx-icon").attributes({ icon: "ph--lightning--regular" }), Domino.of("span").classNames("flex-1 truncate min-w-0").text(this.text))).root;
343
+ return Domino.of("span").classNames("dx-container-query-inline-size inline-flex overflow-hidden py-1 pe-2").append(submitButton(this.text, "ph--lightning--regular")).root;
206
344
  }
207
345
  };
208
346
  //#endregion
209
347
  //#region src/widgets/SummaryWidget.tsx
210
348
  var SummaryWidget = ({ children }) => {
211
- const { t } = useTranslation(translationKey);
212
- return /* @__PURE__ */ jsx(TogglePanel.Root, { children: /* @__PURE__ */ jsxs(TogglePanel.Content, {
213
- classNames: "border border-subdued-separator rounded-md",
214
- children: [/* @__PURE__ */ jsx(TogglePanel.Header, {
215
- classNames: "text-sm dx-group-surface",
216
- children: t("summary.label")
217
- }), /* @__PURE__ */ jsx(TogglePanel.Body, { children: /* @__PURE__ */ jsx("div", {
218
- className: "p-1 text-sm text-subdued",
349
+ const { t } = Hooks.useTranslation(translationKey);
350
+ return /* @__PURE__ */ jsx(WidgetPanel, {
351
+ icon: "ph--list-bullets--regular",
352
+ label: t("summary.label"),
353
+ testId: "assistant.summary",
354
+ children: /* @__PURE__ */ jsx("div", {
355
+ className: mx$1(PANEL_FRAME, "p-trim-sm text-sm text-fg-muted"),
219
356
  children: getXmlTextChild(children ?? [])
220
- }) })]
221
- }) });
357
+ })
358
+ });
222
359
  };
223
360
  //#endregion
224
- //#region src/widgets/ToolWidget.tsx
361
+ //#region src/widgets/SyntheticWidget.tsx
225
362
  /**
226
- * A run of tool blocks as one collapsible panel with a tab per call. The `<toolkit>` tag carries
227
- * the run as JSON — the item is rebuilt from its message alone, so there is no widget-state side
228
- * channel to accumulate blocks through.
363
+ * A system-generated turn: a trigger, an alarm wake-up, a continuation nudge, a tool result
364
+ * recovered across a reload. Collapsed to its first line behind a disclosure, like the model's own
365
+ * panels, so it reads as machinery rather than prose, and glyphed as input, because it is a prompt
366
+ * nobody typed.
229
367
  */
230
- var ToolWidget = ({ view, children }) => {
231
- const { t } = useTranslation(translationKey);
232
- const blocks = useMemo(() => {
233
- const parsed = safeParseJson(getXmlTextChild(children ?? []) ?? "");
234
- return Array.isArray(parsed) ? parsed : [];
235
- }, [children]);
236
- const items = useMemo(() => {
237
- let lastToolCall;
238
- const tools = [];
239
- const items = [];
240
- const indexByToolCallId = /* @__PURE__ */ new Map();
241
- const push = (id, item) => {
242
- const existing = id !== void 0 ? indexByToolCallId.get(id) : void 0;
243
- if (existing !== void 0) items[existing] = item;
244
- else {
245
- if (id !== void 0) indexByToolCallId.set(id, items.length);
246
- items.push(item);
247
- }
248
- };
249
- for (const block of blocks) switch (block._tag) {
250
- case "toolCall": {
251
- const tool = tools.find((tool) => tool.name === block.name);
252
- lastToolCall = {
253
- tool,
254
- block
255
- };
256
- push(block.toolCallId, {
257
- title: tool?.description ?? [t("tool-call.label"), block.name].filter(Boolean).join(" "),
258
- icon: block.operationIcon,
259
- content: safeParseJson(block.input) ?? (block.input || {})
260
- });
261
- break;
262
- }
263
- case "toolResult": {
264
- if (block.error) {
265
- push(void 0, {
266
- title: t("tool-error.label"),
267
- icon: lastToolCall?.block.operationIcon,
268
- content: block.error
269
- });
270
- break;
271
- }
272
- const title = lastToolCall?.tool?.description ?? [t("tool-result.label"), lastToolCall?.block.name].filter(Boolean).join(" ");
273
- const icon = lastToolCall?.block.operationIcon;
274
- lastToolCall = void 0;
275
- push(void 0, {
276
- title,
277
- icon,
278
- content: typeof block.result === "string" ? safeParseJson(block.result) ?? block.result : block.result
279
- });
280
- break;
281
- }
282
- case "stats":
283
- if (!lastToolCall) break;
284
- push(void 0, {
285
- title: t("stats.label"),
286
- icon: lastToolCall.block.operationIcon,
287
- content: block
288
- });
289
- }
290
- return items;
291
- }, [blocks, t]);
368
+ var SyntheticWidget = ({ view, children }) => {
369
+ const text = getXmlInnerText(children ?? [])?.trim();
370
+ useEffect(() => {
371
+ const frame = requestAnimationFrame(() => view?.requestMeasure());
372
+ return () => cancelAnimationFrame(frame);
373
+ }, [view, text]);
292
374
  const handleChangeOpen = useCallback(() => {
293
- setTimeout(() => {
294
- view?.requestMeasure();
295
- }, 1e3);
375
+ setTimeout(() => view?.requestMeasure(), 1e3);
296
376
  }, [view]);
297
- if (!items.length) return null;
298
- return /* @__PURE__ */ jsx(ToolPanel, {
299
- items,
300
- onChangeOpen: handleChangeOpen
301
- });
302
- };
303
- var DEFAULT_TOOL_ICON = "ph--wrench--regular";
304
- var ToolPanel = ({ items, onChangeOpen }) => {
305
- const tabsRef = useRef(null);
306
- const [selected, setSelected] = useState(0);
307
- const [open, setOpen] = useState(false);
308
- useEffect(() => {
309
- setSelected((prev) => Math.min(prev, Math.max(0, items.length - 1)));
310
- }, [items.length]);
311
- useEffect(() => {
312
- onChangeOpen?.(open);
313
- if (open) tabsRef.current?.focus();
314
- }, [open, onChangeOpen]);
315
- const handleSelect = useCallback((index) => {
316
- setSelected(index);
317
- }, []);
318
- const headerIcon = items[items.length - 1]?.icon ?? items[0]?.icon ?? DEFAULT_TOOL_ICON;
319
- return /* @__PURE__ */ jsx(TogglePanel.Root, {
320
- open,
321
- onChangeOpen: setOpen,
322
- children: /* @__PURE__ */ jsxs(TogglePanel.Content, { children: [/* @__PURE__ */ jsx(TogglePanel.Header, {
323
- classNames: "flex items-center gap-2 text-sm",
324
- children: /* @__PURE__ */ jsxs("div", {
325
- className: "w-full grid grid-cols-[1fr_auto] items-center gap-2",
326
- children: [/* @__PURE__ */ jsx(TextCrawl, {
327
- classNames: "text-description",
328
- lines: items.map((item) => item.title),
329
- autoAdvance: true,
330
- greedy: true
331
- }, "status-roll"), /* @__PURE__ */ jsx(IconBlock, { children: /* @__PURE__ */ jsx(Icon, {
332
- icon: headerIcon,
333
- size: 4
334
- }) })]
335
- })
336
- }), /* @__PURE__ */ jsx(TogglePanel.Body, { children: /* @__PURE__ */ jsxs(TogglePanel.Viewport, {
337
- classNames: "grid grid-cols-[32px_1fr]",
338
- children: [/* @__PURE__ */ jsx(NumericTabs, {
339
- ref: tabsRef,
340
- classNames: "p-1",
341
- length: items.length,
342
- selected,
343
- onSelect: handleSelect
344
- }), /* @__PURE__ */ jsx(JsonHighlighter, {
345
- data: items[selected]?.content,
346
- classNames: "p-1 text-xs bg-transparent",
347
- replacer: {
348
- maxDepth: 3,
349
- maxArrayLen: 10,
350
- maxStringLen: 128
351
- }
352
- })]
353
- }) })] })
377
+ if (!text) return null;
378
+ return /* @__PURE__ */ jsx(WidgetPanel, {
379
+ icon: "ph--lightning--regular",
380
+ label: text.split("\n")[0],
381
+ testId: "assistant.synthetic",
382
+ onChangeOpen: handleChangeOpen,
383
+ children: /* @__PURE__ */ jsx("div", {
384
+ className: mx$1(PANEL_FRAME, "p-trim-sm text-sm text-fg-muted whitespace-pre-wrap wrap-break-word tabular-nums"),
385
+ "data-synthetic-text": "",
386
+ children: text
387
+ })
354
388
  });
355
389
  };
356
390
  //#endregion
357
- //#region src/registry.tsx
358
- /**
359
- * Floor for a portaled panel's collapsed height, reserved before its content paints. `heightMode:
360
- * 'min'` makes it a floor rather than a pin — without it the row is measured at the height of an
361
- * empty box and grows a frame later, a row moving under a reader scrolling up.
362
- */
363
- var COLLAPSED_HEIGHT = 42;
364
- /**
365
- * Widgets for the tags {@link createRenderer} emits.
366
- *
367
- * Host-extensible by spreading: the plugin adds its `surface` widget (which renders an
368
- * app-framework Surface this package must not depend on) over the fallback registered here.
369
- * DOM widgets render synchronously from the tag alone; React widgets are portaled and declare a
370
- * collapsed-height floor so the row's first measurement is honest.
371
- */
372
- var assistantRegistry = {
373
- "prompt": { block: true },
374
- "link-preview": {
375
- block: false,
376
- urlSchemes: ["dxn:", "echo:"],
377
- factory: ({ label, dxn }) => typeof label === "string" && typeof dxn === "string" ? new AnchorWidget(label, dxn) : null
378
- },
379
- "synthetic": {
380
- block: true,
381
- factory: ({ children, range }) => {
382
- const text = getXmlTextChild(children ?? []);
383
- return text ? new ReasoningWidget(text, range?.from) : null;
384
- }
385
- },
386
- "reasoning": {
387
- block: true,
388
- streaming: true,
389
- factory: ({ children, range }) => {
390
- const text = getXmlTextChild(children ?? []);
391
- return text ? new ReasoningWidget(text, range?.from) : null;
392
- }
393
- },
394
- "status": {
395
- block: true,
396
- streaming: true,
397
- factory: ({ children }) => {
398
- const text = getXmlTextChild(children ?? []);
399
- return text ? new StatusWidget(text) : null;
400
- }
401
- },
402
- "reference": {
403
- block: false,
404
- factory: ({ children, ref }) => {
405
- const text = getXmlTextChild(children ?? []);
406
- return text && typeof ref === "string" ? new ReferenceWidget(text, ref) : null;
407
- }
408
- },
409
- "suggestion": {
410
- block: false,
411
- factory: ({ children }) => {
412
- const text = getXmlTextChild(children ?? []);
413
- return text ? new SuggestionWidget(text) : null;
414
- }
415
- },
416
- "select": {
417
- block: true,
418
- factory: ({ children }) => {
419
- const options = (children ?? []).map((option) => option?._tag === "option" && getXmlTextChild(option.children)).filter(Boolean);
420
- return options.length ? new SelectWidget(options) : null;
421
- }
422
- },
423
- "stats": {
424
- block: true,
425
- factory: ({ children }) => {
426
- const text = getXmlTextChild(children ?? []);
427
- return text ? new StatsWidget(text) : null;
428
- }
429
- },
430
- "toolkit": {
431
- block: true,
432
- streaming: true,
433
- estimatedHeight: () => COLLAPSED_HEIGHT,
434
- heightMode: "min",
435
- Component: ToolWidget
436
- },
437
- "summary": {
438
- block: true,
439
- estimatedHeight: () => COLLAPSED_HEIGHT,
440
- heightMode: "min",
441
- Component: SummaryWidget
442
- },
443
- "surface": {
444
- block: true,
445
- estimatedHeight: () => COLLAPSED_HEIGHT,
446
- heightMode: "min",
447
- Component: FallbackWidget
448
- },
449
- "json": {
450
- block: true,
451
- estimatedHeight: () => COLLAPSED_HEIGHT,
452
- heightMode: "min",
453
- Component: FallbackWidget
454
- }
455
- };
456
- //#endregion
457
391
  //#region src/renderer.ts
458
392
  /**
459
393
  * The assistant's projection of a message: each block flattened to markdown, everything that is not
460
394
  * prose emitted as an XML tag for the registry's widgets. The view type is a filter over blocks —
461
395
  * the model always carries everything, so switching views is a re-render, not a re-fetch.
462
396
  *
463
- * Consecutive tool blocks (`toolCall`/`toolResult`/`stats`) render as ONE `<toolkit>` tag carrying
464
- * the run as JSON: the panel widget shows a run of calls as tabs, and the item is rebuilt from its
465
- * message alone — there is no side channel accumulating widget state (the old `MessageSyncer`
466
- * machinery this package retires).
397
+ * Consecutive tool blocks (`toolCall`/`toolResult`/`stats`) — along with the `status` and `reasoning`
398
+ * blocks that narrate them — render as ONE `<toolkit>` tag carrying the run as JSON: the panel
399
+ * widget shows a run of calls as tabs, and the item is rebuilt from its message alone — there is no
400
+ * side channel accumulating widget state (the old `MessageSyncer` machinery this package retires).
401
+ *
402
+ * Narration opens a run of its own where no call follows, so a turn the model spends only saying
403
+ * what it is doing reads as one panel rather than a widget per block — and a call arriving later
404
+ * lands in the panel the status already opened instead of displacing it.
467
405
  */
468
406
  var createRenderer = (viewType, { getObjectLabel = () => "Object" } = {}) => {
469
407
  return (message) => {
470
408
  const blocks = message.blocks.filter((block) => isBlockVisible(viewType, block));
471
409
  const segments = [];
472
- let tools = [];
473
- const flushTools = () => {
474
- if (tools.length) {
475
- segments.push(toolkitTag(tools));
476
- tools = [];
410
+ let run = [];
411
+ let deferred = [];
412
+ const flushRun = () => {
413
+ if (run.length) {
414
+ segments.push(toolkitTag(run, viewType === "debug"));
415
+ run = [];
416
+ }
417
+ if (deferred.length) {
418
+ segments.push(...deferred);
419
+ deferred = [];
477
420
  }
478
421
  };
479
422
  for (const block of blocks) {
480
- if (block._tag === "toolCall" || block._tag === "toolResult" || block._tag === "stats" && tools.length) {
481
- tools.push(block);
423
+ if (block._tag === "toolCall" || block._tag === "toolResult") {
424
+ run.push(block);
425
+ continue;
426
+ }
427
+ const recovered = isPrompt(message) ? void 0 : recoveredToolResult(block);
428
+ if (recovered) {
429
+ run.push(recovered);
430
+ continue;
431
+ }
432
+ if (block._tag === "status" || block._tag === "reasoning") {
433
+ if (narrationText(block).length) run.push(block);
434
+ continue;
435
+ }
436
+ if (block._tag === "stats") {
437
+ const rendered = blockToMarkdown(message, block, getObjectLabel);
438
+ if (rendered) deferred.push(rendered);
482
439
  continue;
483
440
  }
484
- flushTools();
485
441
  const rendered = blockToMarkdown(message, block, getObjectLabel);
486
- if (rendered) segments.push(rendered);
442
+ if (!rendered) continue;
443
+ flushRun();
444
+ segments.push(rendered);
487
445
  }
488
- flushTools();
446
+ flushRun();
447
+ const text = segments.reduce((parts, segment) => {
448
+ const previous = parts[parts.length - 1];
449
+ if (previous?.startsWith("<suggestion") && segment.startsWith("<suggestion")) parts[parts.length - 1] = `${previous}${segment}`;
450
+ else parts.push(segment);
451
+ return parts;
452
+ }, []).join("\n\n");
453
+ const delivery = getDelivery(message);
454
+ if (delivery) return {
455
+ kind: "markdown",
456
+ text: `${text}\n<delivery status="${delivery}" id="${escapeAttribute(message.id)}" />`
457
+ };
489
458
  return {
490
459
  kind: "markdown",
491
- text: segments.reduce((parts, segment) => {
492
- const previous = parts[parts.length - 1];
493
- if (previous?.startsWith("<suggestion") && segment.startsWith("<suggestion")) parts[parts.length - 1] = `${previous} ${segment}`;
494
- else parts.push(segment);
495
- return parts;
496
- }, []).join("\n\n")
460
+ text
497
461
  };
498
462
  };
499
463
  };
@@ -508,10 +472,9 @@ var isBlockVisible = (viewType, block) => {
508
472
  var blockToMarkdown = (message, block, getObjectLabel) => {
509
473
  switch (block._tag) {
510
474
  case "text":
511
- if (message.sender.role === "user") return block.disposition === "synthetic" ? void 0 : tag("prompt", block.text, block);
512
- return block.text.trim() || void 0;
513
- case "reasoning": return tag("reasoning", block.reasoningText ?? block.redactedText ?? "", block);
514
- case "status": return tag("status", block.statusText, block);
475
+ if (block.disposition === "synthetic") return isPrompt(message) ? void 0 : tag("synthetic", block.text, block);
476
+ if (message.sender.role === "user") return tag("prompt", block.text, block);
477
+ return linkBareObjectUris(block.text, getObjectLabel).trim() || void 0;
515
478
  case "summary": return tag("summary", block.content, block);
516
479
  case "suggestion": return block.pending ? void 0 : tag("suggestion", block.text, block);
517
480
  case "select": return block.pending || !block.options.length ? void 0 : `<select>${block.options.map((option) => `<option>${escapeXml(option)}</option>`).join("")}</select>`;
@@ -521,13 +484,49 @@ var blockToMarkdown = (message, block, getObjectLabel) => {
521
484
  return `<reference ref="${escapeAttribute(uri.toString())}">${escapeXml(getObjectLabel(uri))}</reference>`;
522
485
  }
523
486
  case "stats": return;
487
+ case "request": return `<request message="${escapeAttribute(message.id)}">${escapeXml(JSON.stringify(block))}</request>`;
524
488
  case "surface": return block.pending ? void 0 : `<surface role="${escapeAttribute(block.role)}">${escapeXml(JSON.stringify(block.data ?? {}))}</surface>`;
525
489
  default: return tag("json", JSON.stringify(block), block);
526
490
  }
527
491
  };
492
+ /** The prose a narration block carries; blank means the widget would render no row for it. */
493
+ /** Tool name of a recovered background result; the widget names it, since the original call is gone. */
494
+ var BACKGROUND_TOOL = "background";
495
+ /** The agent runtime's `<result pid=…>` / `<error pid=…>` prompt for a background tool's outcome. */
496
+ var RECOVERED_RESULT = /^<(result|error) pid=([^>\s]+)>([\s\S]*)<\/\1>$/;
497
+ var recoveredToolResult = (block) => {
498
+ if (block._tag !== "text" || block.disposition !== "synthetic") return;
499
+ const match = block.text.trim().match(RECOVERED_RESULT);
500
+ if (!match) return;
501
+ const [, kind, pid, body] = match;
502
+ return {
503
+ _tag: "toolResult",
504
+ toolCallId: pid,
505
+ name: BACKGROUND_TOOL,
506
+ providerExecuted: false,
507
+ ...kind === "error" ? { error: body } : { result: body }
508
+ };
509
+ };
510
+ var narrationText = (block) => {
511
+ switch (block._tag) {
512
+ case "status": return block.statusText?.trim() ?? "";
513
+ case "reasoning": return (block.reasoningText ?? block.redactedText ?? "").trim();
514
+ default: return "";
515
+ }
516
+ };
528
517
  /** A run of tool blocks as one tag; the widget parses the payload back out. */
529
- var toolkitTag = (blocks) => {
530
- return `<toolkit${blocks.some((block) => block.pending) ? " pending=\"true\"" : ""}>${escapeXml(JSON.stringify(blocks))}</toolkit>`;
518
+ /**
519
+ * Debug shows the tag as text, so its JSON is indented, with JSON-string payloads expanded in place;
520
+ * the widget parses either form.
521
+ */
522
+ var toolkitTag = (blocks, pretty = false) => {
523
+ const open = `<toolkit${blocks.some((block) => block.pending) ? " pending=\"true\"" : ""}>`;
524
+ if (pretty) return `\`\`\`json\n${open}\n${JSON.stringify(blocks, expandJsonStrings, 2)}\n</toolkit>\n\`\`\``;
525
+ return `${open}${escapeXml(JSON.stringify(blocks))}</toolkit>`;
526
+ };
527
+ var expandJsonStrings = (key, value) => {
528
+ if ((key === "input" || key === "result") && typeof value === "string") return safeParseJson(value) ?? value;
529
+ return value;
531
530
  };
532
531
  /**
533
532
  * An XML tag holding block content.
@@ -557,7 +556,7 @@ var PANEL_HEIGHT = 50;
557
556
  * enough that measuring the row does not move the rows below it.
558
557
  */
559
558
  var estimateRow = (message) => {
560
- let height = ROW_CHROME;
559
+ let height = getDelivery(message) ? 66 : ROW_CHROME;
561
560
  for (const block of message.blocks) switch (block._tag) {
562
561
  case "text":
563
562
  if (block.disposition === "synthetic") {
@@ -578,6 +577,412 @@ var estimateRow = (message) => {
578
577
  }
579
578
  return height;
580
579
  };
580
+ /** Code fences and spans, which a URI rewrite must leave alone. */
581
+ var CODE = /(```[\s\S]*?```|~~~[\s\S]*?~~~|`[^`\n]*`)/g;
582
+ /** A bare object URI in prose, with the `@` the model is told it may prefix an in-text reference with. */
583
+ var BARE_OBJECT_URI = /(^|\s)@?(echo:\/\/[A-Za-z0-9]+(?:\/[A-Za-z0-9]+)?)(?=$|[\s.,;:!?)])/gm;
584
+ /**
585
+ * Bare `echo://…` URIs in the model's prose become the markdown forms the editor renders: one alone
586
+ * on its line is the object shown, so it becomes an embed; one inside a sentence becomes a link.
587
+ * The model writes the bare form at least as readily as either, and unrewritten it is just text.
588
+ */
589
+ var linkBareObjectUris = (text, getObjectLabel) => text.split(CODE).map((segment, index) => index % 2 === 1 ? segment : segment.replace(BARE_OBJECT_URI, (match, lead, uri, offset, whole) => {
590
+ const label = getObjectLabel(uri);
591
+ const lineStart = whole.lastIndexOf("\n", offset) + 1;
592
+ const lineEnd = whole.indexOf("\n", offset + match.length);
593
+ return `${lead}${whole.slice(lineStart, lineEnd === -1 ? void 0 : lineEnd).trim() === match.trim() ? "!" : ""}[${label}](${uri})`;
594
+ })).join("");
595
+ //#endregion
596
+ //#region src/widgets/ToolWidget.tsx
597
+ /**
598
+ * A run of tool blocks as one collapsible panel with a row per call. The `<toolkit>` tag carries
599
+ * the run as JSON — the item is rebuilt from its message alone, so there is no widget-state side
600
+ * channel to accumulate blocks through.
601
+ */
602
+ var ToolWidget = ({ view, children }) => {
603
+ const blocks = useMemo(() => {
604
+ const parsed = safeParseJson(getXmlTextChild(children ?? []) ?? "");
605
+ return Array.isArray(parsed) ? parsed : [];
606
+ }, [children]);
607
+ const entries = useMemo(() => toEntries(blocks), [blocks]);
608
+ useEffect(() => {
609
+ const frame = requestAnimationFrame(() => view?.requestMeasure());
610
+ return () => cancelAnimationFrame(frame);
611
+ }, [view, entries.length]);
612
+ const handleChangeOpen = useCallback(() => {
613
+ setTimeout(() => {
614
+ view?.requestMeasure();
615
+ }, 1e3);
616
+ }, [view]);
617
+ if (!entries.length) return null;
618
+ return /* @__PURE__ */ jsx(ToolPanel, {
619
+ entries,
620
+ onChangeOpen: handleChangeOpen
621
+ });
622
+ };
623
+ var TOOL_ICON = "ph--wrench--regular";
624
+ var STATUS_ICON = "ph--info--regular";
625
+ var REASONING_ICON = "ph--brain--regular";
626
+ /**
627
+ * The call's display label where its producer supplied one (a code-mode `eval`), else the operation's
628
+ * human-readable name; the raw tool name is the fallback for inline toolkit and MCP tools, which have
629
+ * no operation behind them.
630
+ */
631
+ var callTitle = (block) => block.displayName ?? block.operationName ?? block.name ?? "Tool";
632
+ /**
633
+ * Groups a run's blocks by row.
634
+ *
635
+ * Calls are keyed on `toolCallId` where the transport supplies one; a result without it answers the
636
+ * most recent unanswered call, which is the order the runtime emits them in.
637
+ */
638
+ var toEntries = (blocks) => {
639
+ const entries = [];
640
+ const indexById = /* @__PURE__ */ new Map();
641
+ const pending = () => {
642
+ for (let index = entries.length - 1; index >= 0; index--) if (entries[index].kind === "call" && entries[index].active) return entries[index];
643
+ };
644
+ for (const block of blocks) switch (block._tag) {
645
+ case "toolCall": {
646
+ const existing = block.toolCallId !== void 0 ? indexById.get(block.toolCallId) : void 0;
647
+ const entry = {
648
+ id: block.toolCallId ?? `call-${entries.length}`,
649
+ kind: "call",
650
+ active: true,
651
+ title: callTitle(block),
652
+ icon: block.displayIcon ?? block.operationIcon ?? TOOL_ICON,
653
+ input: safeParseJson(block.input) ?? (block.input || void 0)
654
+ };
655
+ if (existing !== void 0) entries[existing] = {
656
+ ...entries[existing],
657
+ ...entry
658
+ };
659
+ else {
660
+ if (block.toolCallId !== void 0) indexById.set(block.toolCallId, entries.length);
661
+ entries.push(entry);
662
+ }
663
+ break;
664
+ }
665
+ case "toolResult": {
666
+ let entry;
667
+ if (block.name === "background") {
668
+ const id = `background-${block.toolCallId}`;
669
+ entry = {
670
+ id,
671
+ kind: "call",
672
+ active: false,
673
+ background: true,
674
+ title: "",
675
+ icon: TOOL_ICON
676
+ };
677
+ const previous = entries.findIndex((existing) => existing.id === id);
678
+ if (previous === -1) entries.push(entry);
679
+ else entries[previous] = entry;
680
+ } else entry = pending();
681
+ if (!entry) break;
682
+ entry.active = false;
683
+ if (block.error) entry.error = block.error;
684
+ else entry.result = typeof block.result === "string" ? safeParseJson(block.result) ?? block.result : block.result;
685
+ break;
686
+ }
687
+ case "status": {
688
+ const text = block.statusText?.trim();
689
+ if (!text) break;
690
+ entries.push({
691
+ id: `status-${entries.length}`,
692
+ kind: "status",
693
+ active: block.pending === true,
694
+ title: text,
695
+ icon: STATUS_ICON
696
+ });
697
+ break;
698
+ }
699
+ case "reasoning": {
700
+ const text = (block.reasoningText ?? block.redactedText ?? "").trim();
701
+ if (!text) break;
702
+ entries.push({
703
+ id: `reasoning-${entries.length}`,
704
+ kind: "reasoning",
705
+ active: block.pending === true,
706
+ title: "",
707
+ icon: REASONING_ICON,
708
+ text
709
+ });
710
+ break;
711
+ }
712
+ }
713
+ return entries;
714
+ };
715
+ /**
716
+ * The row's own words. Reasoning names the kind instead of its prose: it runs to paragraphs, and a
717
+ * truncated first line reads as a broken title rather than a summary.
718
+ */
719
+ var entryLabel = (entry, t) => entry.kind === "reasoning" ? t("tool-thinking.label") : entry.background ? t("tool-background.label") : entry.title;
720
+ /** Whether the row carries anything an expansion could show. */
721
+ var hasDetail = (entry) => entry.text !== void 0 || entry.input !== void 0 || entry.error !== void 0 || entry.result !== void 0;
722
+ var ToolPanel = ({ entries, onChangeOpen }) => {
723
+ const { t } = Hooks.useTranslation(translationKey);
724
+ const calls = entries.filter((entry) => entry.kind === "call");
725
+ const status = entries.filter((entry) => entry.kind === "status").at(-1);
726
+ const failed = calls.filter((call) => call.error !== void 0).length;
727
+ const single = entries.length === 1 ? entries[0] : void 0;
728
+ const singleCall = single?.kind === "call" ? single : void 0;
729
+ const count = t("tool-run.label", { count: calls.length });
730
+ const narrating = calls.length === 0 ? status ?? entries[entries.length - 1] : void 0;
731
+ const reasoning = entries.filter((entry) => entry.kind === "reasoning").at(-1);
732
+ const running = calls.some((call) => call.active) ? status ?? reasoning : void 0;
733
+ const header = narrating ? entryLabel(narrating, t) : running ? `${entryLabel(running, t)} · ${t("tool-run-suffix.label", { count: calls.length })}` : singleCall ? entryLabel(singleCall, t) : count;
734
+ const icon = narrating?.icon ?? running?.icon ?? singleCall?.icon ?? TOOL_ICON;
735
+ if (single && !hasDetail(single)) return /* @__PURE__ */ jsx(WidgetPanelRow, {
736
+ icon,
737
+ label: header,
738
+ testId: `assistant.tool-${single.kind}`
739
+ });
740
+ return /* @__PURE__ */ jsx(WidgetPanel, {
741
+ icon,
742
+ label: header,
743
+ error: single?.error !== void 0,
744
+ suffix: failed > 0 && /* @__PURE__ */ jsxs("span", {
745
+ className: "shrink-0 text-error-text",
746
+ children: ["· ", t("tool-failed.label", { count: failed })]
747
+ }),
748
+ testId: singleCall ? "assistant.tool-call" : "assistant.tool-run",
749
+ onChangeOpen,
750
+ lazyMount: true,
751
+ children: single ? /* @__PURE__ */ jsx(ToolCallDetail, {
752
+ entry: single,
753
+ classNames: mx$1(PANEL_FRAME, "p-trim-sm")
754
+ }) : /* @__PURE__ */ jsx(ToolCallList, {
755
+ entries,
756
+ onOpen: onChangeOpen
757
+ })
758
+ });
759
+ };
760
+ /**
761
+ * The run's rows as an accordion, so each opens onto its own payload and the machine supplies the
762
+ * APG keymap the hand-rolled rows never had.
763
+ *
764
+ * Rows are collapsed by default: one that opened itself would change this item's height, and the
765
+ * feed measures that height as the row mounts.
766
+ */
767
+ var ToolCallList = ({ entries, onOpen }) => {
768
+ const { t } = Hooks.useTranslation(translationKey);
769
+ const label = (entry) => entryLabel(entry, t);
770
+ return /* @__PURE__ */ jsx(Accordion.Root, {
771
+ onValueChange: (value) => onOpen?.(value.length > 0),
772
+ children: entries.map((entry) => {
773
+ const detail = hasDetail(entry);
774
+ return /* @__PURE__ */ jsxs(Accordion.Item, {
775
+ value: entry.id,
776
+ disabled: !detail,
777
+ children: [/* @__PURE__ */ jsx(Accordion.ItemTrigger, {
778
+ icon: entry.icon,
779
+ "data-testid": `assistant.tool-${entry.kind}`,
780
+ classNames: mx$1("text-sm", entry.error !== void 0 && "text-error-text"),
781
+ children: /* @__PURE__ */ jsx("span", {
782
+ className: "flex items-center h-(--dx-control-sm) min-w-0",
783
+ children: /* @__PURE__ */ jsx("span", {
784
+ className: "truncate",
785
+ children: label(entry)
786
+ })
787
+ })
788
+ }), detail && /* @__PURE__ */ jsx(Accordion.ItemContent, {
789
+ classNames: "px-2",
790
+ children: /* @__PURE__ */ jsx(ToolCallDetail, { entry })
791
+ })]
792
+ }, entry.id);
793
+ })
794
+ });
795
+ };
796
+ /** What a row carries, in the order it happened. */
797
+ var ToolCallDetail = ({ entry, classNames }) => {
798
+ const { t } = Hooks.useTranslation(translationKey);
799
+ return /* @__PURE__ */ jsxs("div", {
800
+ className: mx$1("flex flex-col gap-1 min-w-0", classNames),
801
+ children: [
802
+ entry.text !== void 0 && /* @__PURE__ */ jsx("p", {
803
+ className: "text-sm text-fg-muted whitespace-pre-wrap px-1 py-trim-sm",
804
+ children: entry.text
805
+ }),
806
+ entry.input !== void 0 && /* @__PURE__ */ jsx(ToolSection, {
807
+ label: t("tool-input.label"),
808
+ data: entry.input
809
+ }),
810
+ entry.error !== void 0 && /* @__PURE__ */ jsx(ToolSection, {
811
+ label: t("tool-error.label"),
812
+ data: entry.error
813
+ }),
814
+ entry.result !== void 0 && /* @__PURE__ */ jsx(ToolSection, {
815
+ label: t("tool-result.label"),
816
+ data: entry.result
817
+ })
818
+ ]
819
+ });
820
+ };
821
+ /** Longer than this, JSON rendering truncates a string, so a text field is shown as text instead. */
822
+ var MAX_JSON_STRING_LENGTH = 128;
823
+ var ToolSection = ({ label, data }) => /* @__PURE__ */ jsxs("div", {
824
+ className: "flex flex-col",
825
+ children: [/* @__PURE__ */ jsxs("div", {
826
+ className: "flex items-center justify-between",
827
+ children: [/* @__PURE__ */ jsx("span", {
828
+ className: "text-sm text-fg-muted",
829
+ children: label
830
+ }), /* @__PURE__ */ jsx(SystemButton.Clipboard, {
831
+ variant: "ghost",
832
+ size: "sm",
833
+ iconOnly: true,
834
+ classNames: "-me-1",
835
+ onCopy: () => JSON.stringify(data)
836
+ })]
837
+ }), multilineFields(data)?.map(([key, value], _, fields) => /* @__PURE__ */ jsxs("div", {
838
+ className: "flex flex-col",
839
+ children: [fields.length > 1 && /* @__PURE__ */ jsx("span", {
840
+ className: "text-xs text-fg-muted",
841
+ children: key
842
+ }), /* @__PURE__ */ jsx(SyntaxHighlighter, {
843
+ language: key === "code" ? "js" : "text",
844
+ scroll: "horizontal",
845
+ classNames: "text-xs bg-transparent",
846
+ children: value
847
+ })]
848
+ }, key)) ?? /* @__PURE__ */ jsx(JsonHighlighter, {
849
+ data,
850
+ scroll: "horizontal",
851
+ classNames: "text-xs bg-transparent",
852
+ replacer: {
853
+ maxDepth: 3,
854
+ maxArrayLen: 10,
855
+ maxStringLen: MAX_JSON_STRING_LENGTH
856
+ }
857
+ })]
858
+ });
859
+ /**
860
+ * A string, or the entries of a record whose fields are all strings, when one spans lines or runs
861
+ * long — a code-mode `eval`'s `code` and its printed output. JSON would escape every newline onto
862
+ * one line and cut the string short, so these render as the text they are.
863
+ */
864
+ var multilineFields = (data) => {
865
+ if (typeof data === "string") return isMultiline(data) ? [["", data]] : void 0;
866
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return;
867
+ const entries = Object.entries(data);
868
+ const strings = entries.flatMap(([key, value]) => typeof value === "string" ? [[key, value]] : []);
869
+ return strings.length > 0 && strings.length === entries.length && strings.some(([, value]) => isMultiline(value)) ? strings : void 0;
870
+ };
871
+ var isMultiline = (value) => value.includes("\n") || value.length > MAX_JSON_STRING_LENGTH;
872
+ //#endregion
873
+ //#region src/registry.tsx
874
+ /**
875
+ * Floor for a portaled panel's collapsed height, reserved before its content paints. `heightMode:
876
+ * 'min'` makes it a floor rather than a pin — without it the row is measured at the height of an
877
+ * empty box and grows a frame later, a row moving under a reader scrolling up.
878
+ */
879
+ var COLLAPSED_HEIGHT = 42;
880
+ /** An `<option>` child of a `<select>` tag, as the XML parser emits it. */
881
+ var isOption = (node) => typeof node === "object" && node !== null && "_tag" in node && node._tag === "option";
882
+ /**
883
+ * Widgets for the tags {@link createRenderer} emits.
884
+ *
885
+ * Host-extensible by spreading: the plugin adds its `surface` widget (which renders an
886
+ * app-framework Surface this package must not depend on) over the fallback registered here.
887
+ * DOM widgets render synchronously from the tag alone; React widgets are portaled and declare a
888
+ * collapsed-height floor so the row's first measurement is honest.
889
+ */
890
+ var assistantRegistry = {
891
+ prompt: { block: true },
892
+ reasoning: {
893
+ block: true,
894
+ streaming: true,
895
+ factory: ({ children, range }) => {
896
+ const text = getXmlInnerText(children ?? []);
897
+ return text ? new ReasoningWidget(text, range?.from) : null;
898
+ }
899
+ },
900
+ status: {
901
+ block: true,
902
+ streaming: true,
903
+ factory: ({ children }) => {
904
+ const text = getXmlTextChild(children ?? []);
905
+ return text ? new StatusWidget(text) : null;
906
+ }
907
+ },
908
+ reference: {
909
+ block: false,
910
+ factory: ({ children, ref }) => {
911
+ const text = getXmlTextChild(children ?? []);
912
+ return text && typeof ref === "string" ? new ReferenceWidget(text, ref) : null;
913
+ }
914
+ },
915
+ suggestion: {
916
+ block: false,
917
+ factory: ({ children }) => {
918
+ const text = getXmlTextChild(children ?? []);
919
+ return text ? new SuggestionWidget(text) : null;
920
+ }
921
+ },
922
+ select: {
923
+ block: true,
924
+ factory: ({ children }) => {
925
+ const options = (children ?? []).flatMap((option) => {
926
+ const text = isOption(option) ? getXmlTextChild(option.children) : null;
927
+ return text ? [text] : [];
928
+ });
929
+ return options.length ? new SelectWidget(options) : null;
930
+ }
931
+ },
932
+ stats: {
933
+ block: true,
934
+ factory: ({ children }) => {
935
+ const text = getXmlTextChild(children ?? []);
936
+ return text ? new StatsWidget(text) : null;
937
+ }
938
+ },
939
+ toolkit: {
940
+ block: true,
941
+ streaming: true,
942
+ estimatedHeight: () => COLLAPSED_HEIGHT,
943
+ heightMode: "min",
944
+ Component: ToolWidget
945
+ },
946
+ synthetic: {
947
+ block: true,
948
+ estimatedHeight: () => COLLAPSED_HEIGHT,
949
+ heightMode: "min",
950
+ Component: SyntheticWidget
951
+ },
952
+ summary: {
953
+ block: true,
954
+ estimatedHeight: () => COLLAPSED_HEIGHT,
955
+ heightMode: "min",
956
+ Component: SummaryWidget
957
+ },
958
+ request: {
959
+ block: true,
960
+ estimatedHeight: () => COLLAPSED_HEIGHT,
961
+ heightMode: "min",
962
+ Component: RequestWidget
963
+ },
964
+ surface: {
965
+ block: true,
966
+ estimatedHeight: () => COLLAPSED_HEIGHT,
967
+ heightMode: "min",
968
+ Component: FallbackWidget
969
+ },
970
+ json: {
971
+ block: true,
972
+ estimatedHeight: () => COLLAPSED_HEIGHT,
973
+ heightMode: "min",
974
+ Component: FallbackWidget
975
+ }
976
+ };
977
+ var isDeliveryStatus = Schema.is(DeliveryStatus);
978
+ /**
979
+ * The delivery ticks under a prompt still on its way to the agent. Not in {@link assistantRegistry}
980
+ * because its labels are the reader's language, which only a rendered thread has.
981
+ */
982
+ var createDeliveryWidget = (labels) => ({
983
+ block: true,
984
+ factory: ({ status, id }) => isDeliveryStatus(status) && typeof id === "string" ? new DeliveryWidget(status, id, labels) : null
985
+ });
581
986
  //#endregion
582
987
  //#region src/components/MessageChrome/format-time.ts
583
988
  /**
@@ -587,6 +992,8 @@ var estimateRow = (message) => {
587
992
  * Recent prompts are the ones a reader places by elapsed time; older ones by date.
588
993
  */
589
994
  var DAY_MS = 864e5;
995
+ var relativeFormatter;
996
+ var relativeFormat = () => relativeFormatter ??= new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
590
997
  /**
591
998
  * Relative time for the message toolbars: `justNow` (caller-translated) under a minute, relative
592
999
  * ("5 min ago", "yesterday") within two days, an absolute date beyond that. `now` defaults to the
@@ -600,7 +1007,7 @@ var formatTime = (created, { now = Date.now(), justNow } = {}) => {
600
1007
  month: "short",
601
1008
  day: "numeric"
602
1009
  });
603
- const format = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
1010
+ const format = relativeFormat();
604
1011
  if (elapsed < 6e4) {
605
1012
  if (justNow) return justNow;
606
1013
  return format.format(-Math.max(1, Math.round(elapsed / 1e3)), "second");
@@ -614,34 +1021,43 @@ var formatTime = (created, { now = Date.now(), justNow } = {}) => {
614
1021
  //#endregion
615
1022
  //#region src/components/MessageChrome/MessageChrome.tsx
616
1023
  var MESSAGE_CHROME_NAME = "MessageChrome";
617
- var [MessageChromeProvider, useMessageChromeContext] = createContext(MESSAGE_CHROME_NAME, {});
1024
+ var [MessageChromeProvider, useMessageChromeContext] = Hooks.createContext(MESSAGE_CHROME_NAME, {});
618
1025
  /** Copies the message's extracted text — the model's truth, not the DOM's partial render. */
619
1026
  var CopyButton = ({ message }) => {
620
- const { t } = useTranslation(translationKey);
621
- return /* @__PURE__ */ jsx(IconButton, {
622
- icon: "ph--copy--regular",
1027
+ const { t } = Hooks.useTranslation(translationKey);
1028
+ return /* @__PURE__ */ jsx(SystemButton.Clipboard, {
623
1029
  iconOnly: true,
624
1030
  label: t("copy.label"),
625
1031
  variant: "ghost",
626
- density: "sm",
627
- onClick: () => void navigator.clipboard?.writeText(Message.extractText(message))
1032
+ size: "sm",
1033
+ onCopy: () => Message.extractText(message)
628
1034
  });
629
1035
  };
630
1036
  var MessageId = ({ message }) => {
631
1037
  const { debug } = useMessageChromeContext("MessageId");
632
1038
  if (!debug) return null;
633
1039
  return /* @__PURE__ */ jsx("span", {
634
- className: "font-mono text-subdued",
1040
+ className: "font-mono text-fg-subtle",
635
1041
  title: message.id,
636
1042
  children: message.id.slice(-8)
637
1043
  });
638
1044
  };
1045
+ /** How often a relative time is re-read; the coarsest label it shows changes by the minute. */
1046
+ var TIME_REFRESH_MS = 3e4;
639
1047
  var Time = ({ message }) => {
640
- const { t } = useTranslation(translationKey);
1048
+ const { t } = Hooks.useTranslation(translationKey);
1049
+ const [now, setNow] = useState(Date.now);
1050
+ useEffect(() => {
1051
+ const interval = setInterval(() => setNow(Date.now()), TIME_REFRESH_MS);
1052
+ return () => clearInterval(interval);
1053
+ }, []);
641
1054
  return /* @__PURE__ */ jsx("time", {
642
1055
  dateTime: message.created,
643
1056
  title: new Date(message.created).toLocaleString(),
644
- children: formatTime(message.created, { justNow: t("just-now.label") })
1057
+ children: formatTime(message.created, {
1058
+ now,
1059
+ justNow: t("just-now.label")
1060
+ })
645
1061
  });
646
1062
  };
647
1063
  /**
@@ -650,20 +1066,20 @@ var Time = ({ message }) => {
650
1066
  * disappears changes the row's height, and a pointer travelling down a scrolling list would then
651
1067
  * move every row below it.
652
1068
  */
653
- var PromptToolbar = ({ classNames, message }) => {
654
- const { t } = useTranslation(translationKey);
1069
+ var PromptToolbar = memo(({ classNames, message }) => {
1070
+ const { t } = Hooks.useTranslation(translationKey);
655
1071
  const { onRewind } = useMessageChromeContext("PromptToolbar");
656
1072
  return /* @__PURE__ */ jsxs("div", {
657
1073
  role: "toolbar",
658
- className: mx$1("flex items-center gap-1 text-xs text-description", classNames),
1074
+ className: mx$1("flex items-center gap-1 text-xs text-fg-muted", classNames),
659
1075
  children: [
660
1076
  /* @__PURE__ */ jsx(CopyButton, { message }),
661
- onRewind && /* @__PURE__ */ jsx(IconButton, {
1077
+ onRewind && !isUnread(getDelivery(message)) && /* @__PURE__ */ jsx(Button.Root, {
662
1078
  icon: "ph--clock-counter-clockwise--regular",
663
1079
  iconOnly: true,
664
1080
  label: t("rewind.label"),
665
1081
  variant: "ghost",
666
- density: "sm",
1082
+ size: "sm",
667
1083
  "data-testid": "chat.rewind",
668
1084
  onClick: () => onRewind(message.id)
669
1085
  }),
@@ -671,28 +1087,56 @@ var PromptToolbar = ({ classNames, message }) => {
671
1087
  /* @__PURE__ */ jsx(MessageId, { message })
672
1088
  ]
673
1089
  });
674
- };
1090
+ });
675
1091
  PromptToolbar.displayName = "PromptToolbar";
676
1092
  /** The controls under an answer: copy, and when the answer finished. */
677
- var AssistantToolbar = ({ classNames, message }) => {
1093
+ var AssistantToolbar = memo(({ classNames, message }) => {
678
1094
  return /* @__PURE__ */ jsxs("div", {
679
1095
  role: "toolbar",
680
- className: mx$1("flex items-center gap-1 text-xs text-description", classNames),
1096
+ className: mx$1("flex items-center gap-1 text-xs text-fg-muted", classNames),
681
1097
  children: [
682
1098
  /* @__PURE__ */ jsx(CopyButton, { message }),
683
1099
  /* @__PURE__ */ jsx(Time, { message }),
684
- /* @__PURE__ */ jsx(MessageId, { message })
1100
+ /* @__PURE__ */ jsx(MessageId, { message }),
1101
+ /* @__PURE__ */ jsx(Stats, {
1102
+ message,
1103
+ classNames: "ms-auto"
1104
+ })
685
1105
  ]
686
1106
  });
687
- };
1107
+ });
688
1108
  AssistantToolbar.displayName = "AssistantToolbar";
1109
+ /** Thousands abbreviated to one decimal, so the count stays one glance wide as a turn grows. */
1110
+ var formatTokens = (tokens) => tokens >= 1e3 ? `${(tokens / 1e3).toFixed(1)}k` : String(tokens);
1111
+ /**
1112
+ * What the turn cost, beside what it produced — the toolbar is where a reader looks for a message's
1113
+ * metadata, and a widget of its own would take a row of the thread to say one line.
1114
+ */
1115
+ var Stats = ({ classNames, message }) => {
1116
+ const stats = message.blocks.find((block) => block._tag === "stats");
1117
+ const tokens = stats?.usage?.totalTokens;
1118
+ const duration = stats?.duration;
1119
+ if (tokens === void 0 && duration === void 0) return null;
1120
+ return /* @__PURE__ */ jsxs("span", {
1121
+ className: mx$1("flex items-center gap-1 tabular-nums", classNames),
1122
+ children: [
1123
+ tokens !== void 0 && /* @__PURE__ */ jsxs("span", { children: [formatTokens(tokens), " tokens"] }),
1124
+ tokens !== void 0 && duration !== void 0 && /* @__PURE__ */ jsx("span", {
1125
+ "aria-hidden": true,
1126
+ children: "·"
1127
+ }),
1128
+ duration !== void 0 && /* @__PURE__ */ jsxs("span", { children: [(duration / 1e3).toFixed(1), "s"] })
1129
+ ]
1130
+ });
1131
+ };
1132
+ Stats.displayName = "Stats";
689
1133
  /**
690
1134
  * Synthetic context riding on a prompt (a document selection, an encoded event): system-generated
691
1135
  * input, so it renders as its own dimmed panel above the bubble — the bubble frames only the
692
1136
  * reader's words.
693
1137
  */
694
1138
  var SyntheticContext = ({ message }) => {
695
- const { t } = useTranslation(translationKey);
1139
+ const { t } = Hooks.useTranslation(translationKey);
696
1140
  const { showContext = true } = useMessageChromeContext("SyntheticContext");
697
1141
  const context = message.blocks.filter((block) => block._tag === "text" && block.disposition === "synthetic").map((block) => block.text).join("\n\n");
698
1142
  if (!showContext || !context.length) return null;
@@ -700,32 +1144,45 @@ var SyntheticContext = ({ message }) => {
700
1144
  className: "pb-1 opacity-60",
701
1145
  "data-testid": "chat.context",
702
1146
  children: /* @__PURE__ */ jsx(TogglePanel.Root, { children: /* @__PURE__ */ jsxs(TogglePanel.Content, {
703
- classNames: "border border-subdued-separator rounded-sm",
1147
+ classNames: "border border-separator-subtle rounded-sm",
704
1148
  children: [/* @__PURE__ */ jsxs(TogglePanel.Header, {
705
1149
  classNames: "flex items-center gap-2 px-2 py-1 text-sm",
706
1150
  children: [/* @__PURE__ */ jsx("span", {
707
- className: "grow text-description truncate",
1151
+ className: "grow text-fg-muted truncate",
708
1152
  children: t("context.label")
709
- }), /* @__PURE__ */ jsx(Icon, {
1153
+ }), /* @__PURE__ */ jsx(Icon.Icon, {
710
1154
  icon: "ph--brain--regular",
711
- size: 4,
712
- classNames: "text-description"
1155
+ size: "md",
1156
+ tone: "muted"
713
1157
  })]
714
1158
  }), /* @__PURE__ */ jsx(TogglePanel.Body, { children: /* @__PURE__ */ jsx(TogglePanel.Viewport, {
715
- classNames: "px-2 pb-1 max-h-40 overflow-y-auto text-sm text-description whitespace-pre-wrap",
1159
+ classNames: "px-2 pb-1 max-h-40 overflow-y-auto text-sm text-fg-muted whitespace-pre-wrap",
716
1160
  children: context
717
1161
  }) })]
718
1162
  }) })
719
1163
  });
720
1164
  };
721
- /** Shared hover reveal: present in flow at all times, visible when the row is under the pointer. */
722
- var reveal = "pt-1 opacity-0 transition-opacity";
723
- var revealOnHover = "group-hover/message:opacity-100";
724
- var Row = ({ children, classNames }) => /* @__PURE__ */ jsx("div", {
725
- className: mx$1("group/message relative px-2 py-2", classNames),
1165
+ /**
1166
+ * Shared hover reveal: present in flow at all times, visible when the row is under the pointer.
1167
+ *
1168
+ * A named group: the bare `group` variant matches ANY ancestor carrying `group`, and the app's
1169
+ * planks do — every toolbar lit up when the pointer was anywhere in the deck. Streaming hides it
1170
+ * from the row's attribute rather than the toolbar's props, so the memoized toolbars do not
1171
+ * re-render on every row each time a turn starts or ends.
1172
+ */
1173
+ var reveal = "pt-1 opacity-0 transition-opacity group-hover/message:opacity-100 group-data-[streaming]/message:invisible";
1174
+ var Row = ({ children, classNames, streaming }) => /* @__PURE__ */ jsx("div", {
1175
+ className: mx$1("group/message relative -mx-2 px-2 py-2 rounded-md", classNames),
1176
+ "data-streaming": streaming || void 0,
726
1177
  "data-testid": "feed.message",
727
1178
  children
728
1179
  });
1180
+ var promptReveal = mx$1("justify-end", reveal);
1181
+ /**
1182
+ * Whether an answer row carries the toolbar: only when it has prose to copy or stats to show. A row
1183
+ * of machinery alone (a synthetic prompt, a tool run) would otherwise reserve a blank toolbar line.
1184
+ */
1185
+ var hasToolbar = (message) => message.blocks.some((block) => block._tag === "stats" || block._tag === "text" && block.disposition !== "synthetic" && !!block.text.trim());
729
1186
  /**
730
1187
  * The assistant feed's per-message frame: the reader's prompts and the model's answers are framed
731
1188
  * differently because they are different kinds of thing — a prompt is an instruction the thread
@@ -736,10 +1193,11 @@ var Row = ({ children, classNames }) => /* @__PURE__ */ jsx("div", {
736
1193
  * bubble's edge.
737
1194
  */
738
1195
  var MessageChrome = ({ message, selected, children }) => {
739
- const { streaming } = useMessageChromeContext(MESSAGE_CHROME_NAME);
1196
+ const { streaming, userHue } = useMessageChromeContext(MESSAGE_CHROME_NAME);
740
1197
  const prompt = isPrompt(message);
741
1198
  return /* @__PURE__ */ jsx(Row, {
742
1199
  classNames: mx$1(selected && "bg-hover-surface"),
1200
+ streaming,
743
1201
  children: prompt ? /* @__PURE__ */ jsx("div", {
744
1202
  className: "min-w-0 flex flex-col items-end",
745
1203
  children: /* @__PURE__ */ jsxs("div", {
@@ -747,19 +1205,19 @@ var MessageChrome = ({ message, selected, children }) => {
747
1205
  children: [
748
1206
  /* @__PURE__ */ jsx(SyntheticContext, { message }),
749
1207
  /* @__PURE__ */ jsx("div", {
750
- className: "px-3 py-2 border-s-2 border-accent-bg rounded-sm bg-input-surface",
1208
+ className: mx$1("w-fit max-w-full ms-auto px-3 py-2 border-s-2 rounded-sm bg-input-surface", userHue ? getStyles(userHue).border : "border-accent-bg"),
751
1209
  children
752
1210
  }),
753
1211
  /* @__PURE__ */ jsx(PromptToolbar, {
754
- classNames: mx$1("justify-end", reveal, !streaming && revealOnHover),
1212
+ classNames: promptReveal,
755
1213
  message
756
1214
  })
757
1215
  ]
758
1216
  })
759
1217
  }) : /* @__PURE__ */ jsxs("div", {
760
1218
  className: "min-w-0",
761
- children: [children, /* @__PURE__ */ jsx(AssistantToolbar, {
762
- classNames: mx$1(reveal, !streaming && revealOnHover),
1219
+ children: [children, hasToolbar(message) && /* @__PURE__ */ jsx(AssistantToolbar, {
1220
+ classNames: reveal,
763
1221
  message
764
1222
  })]
765
1223
  })
@@ -768,19 +1226,34 @@ var MessageChrome = ({ message, selected, children }) => {
768
1226
  MessageChrome.displayName = MESSAGE_CHROME_NAME;
769
1227
  //#endregion
770
1228
  //#region src/components/ChatThread/ChatThread.tsx
771
- var [ChatThreadProvider, useChatThreadContext] = createContext("ChatThread");
1229
+ var [ChatThreadProvider, useChatThreadContext] = Hooks.createContext("ChatThread");
772
1230
  /**
773
1231
  * Headless root: the feed's `MessageList.Root` configured as an assistant thread — the view-typed
774
1232
  * renderer, the widget registry, the prompt/answer chrome, and the chat behaviours (follow the
775
1233
  * streaming tail; reserve room to bring the last prompt to the top). Composes with the feed's own
776
1234
  * parts: `MessageList.Nav`, `useMessageList`, and the rails all work inside it.
777
1235
  */
778
- var ChatThreadRoot = ({ children, model, viewType, registry, getObjectLabel, userHue, tailLines, debug, onEvent, onRangeChange, controllerRef }) => {
1236
+ /** Debug's registry: identity-stable, since the feed caches its extensions per registry. */
1237
+ var DEBUG_REGISTRY = {};
1238
+ var ChatThreadRoot = ({ children, model, viewType, registry, objectImage, getObjectLabel, userHue, tailLines, debug, rewind = true, onEvent, onRangeChange, controllerRef }) => {
1239
+ const { t } = Hooks.useTranslation(translationKey);
779
1240
  const renderer = useMemo(() => createRenderer(viewType, { getObjectLabel }), [viewType, getObjectLabel]);
780
- const merged = useMemo(() => viewType === "debug" ? void 0 : registry ? {
1241
+ const delivery = useMemo(() => createDeliveryWidget({
1242
+ sent: t("delivery-sent.label"),
1243
+ delivered: t("delivery-delivered.label"),
1244
+ read: t("delivery-read.label"),
1245
+ failed: t("delivery-failed.label"),
1246
+ remove: t("delivery-remove.label")
1247
+ }), [t]);
1248
+ const merged = useMemo(() => viewType === "debug" ? DEBUG_REGISTRY : {
781
1249
  ...assistantRegistry,
1250
+ delivery,
782
1251
  ...registry
783
- } : assistantRegistry, [registry, viewType]);
1252
+ }, [
1253
+ registry,
1254
+ viewType,
1255
+ delivery
1256
+ ]);
784
1257
  const handleRewind = useCallback((id) => onEvent?.({
785
1258
  type: "rewind",
786
1259
  id
@@ -792,16 +1265,19 @@ var ChatThreadRoot = ({ children, model, viewType, registry, getObjectLabel, use
792
1265
  }, [model]);
793
1266
  return /* @__PURE__ */ jsx(ChatThreadProvider, {
794
1267
  userHue,
1268
+ viewType,
795
1269
  onEvent,
796
1270
  children: /* @__PURE__ */ jsx(MessageChromeProvider, {
797
- onRewind: onEvent ? handleRewind : void 0,
1271
+ onRewind: onEvent && rewind ? handleRewind : void 0,
798
1272
  streaming,
799
1273
  showContext: viewType !== "summary",
800
1274
  debug,
1275
+ userHue,
801
1276
  children: /* @__PURE__ */ jsx(MessageList.Root, {
802
1277
  model,
803
1278
  renderer,
804
1279
  registry: merged,
1280
+ objectImage,
805
1281
  Chrome: MessageChrome,
806
1282
  estimateSize: estimateRow,
807
1283
  debug,
@@ -821,16 +1297,32 @@ var CHAT_THREAD_VIEWPORT_NAME = "ChatThread.Viewport";
821
1297
  * `data-action="submit"` buttons; one delegated listener here turns those clicks into `submit`
822
1298
  * events, which is what keeps the widgets renderable from the tag alone.
823
1299
  */
824
- var ChatThreadViewport = ({ children, classNames, ...props }) => {
825
- const { userHue, onEvent } = useChatThreadContext(CHAT_THREAD_VIEWPORT_NAME);
1300
+ var ChatThreadViewport = ({ children, classNames, overlay, ...props }) => {
1301
+ const { userHue, viewType, onEvent } = useChatThreadContext(CHAT_THREAD_VIEWPORT_NAME);
826
1302
  const handleClick = useCallback((event) => {
827
- const text = event.target.closest("[data-action=\"submit\"]")?.getAttribute("data-value");
828
- if (text) {
1303
+ const target = event.target;
1304
+ if (!(target instanceof Element)) return;
1305
+ const action = target.closest("[data-action]");
1306
+ const value = action?.getAttribute("data-value");
1307
+ const threadEvent = value ? toThreadEvent(action?.getAttribute("data-action"), value) : void 0;
1308
+ if (threadEvent) {
1309
+ event.preventDefault();
1310
+ event.stopPropagation();
1311
+ onEvent?.(threadEvent);
1312
+ return;
1313
+ }
1314
+ const response = target.closest("[data-action=\"respond\"]");
1315
+ const messageId = response?.getAttribute("data-message");
1316
+ const requestId = response?.getAttribute("data-request");
1317
+ const optionId = response?.getAttribute("data-option");
1318
+ if (messageId && requestId && optionId) {
829
1319
  event.preventDefault();
830
1320
  event.stopPropagation();
831
1321
  onEvent?.({
832
- type: "submit",
833
- text
1322
+ type: "respond",
1323
+ messageId,
1324
+ requestId,
1325
+ optionId
834
1326
  });
835
1327
  }
836
1328
  }, [onEvent]);
@@ -841,17 +1333,63 @@ var ChatThreadViewport = ({ children, classNames, ...props }) => {
841
1333
  onClickCapture: handleClick,
842
1334
  children: /* @__PURE__ */ jsx(MessageList.Viewport, {
843
1335
  ...props,
844
- classNames: ["grow min-h-0", classNames],
1336
+ classNames: [
1337
+ "dx-grow",
1338
+ viewType === "debug" && "[&_.cm-codeblock-line]:text-sm [&_.cm-xml-tag]:text-sm",
1339
+ classNames
1340
+ ],
1341
+ overlay: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ScrollToBottom, {}), overlay] }),
845
1342
  children
846
1343
  })
847
1344
  });
848
1345
  };
849
1346
  ChatThreadViewport.displayName = CHAT_THREAD_VIEWPORT_NAME;
1347
+ /** A widget's `data-action` button as the event it stands for; the value is the text or the message id. */
1348
+ var toThreadEvent = (action, value) => {
1349
+ switch (action) {
1350
+ case "submit": return {
1351
+ type: "submit",
1352
+ text: value
1353
+ };
1354
+ case "remove": return {
1355
+ type: "remove-prompt",
1356
+ id: value
1357
+ };
1358
+ default: return;
1359
+ }
1360
+ };
1361
+ var CHAT_THREAD_SCROLL_TO_BOTTOM_NAME = "ChatThread.ScrollToBottom";
1362
+ /**
1363
+ * Returns the reader to the tail, and re-arms the follow with it (`scrollToBottom` does both).
1364
+ *
1365
+ * Hidden by opacity rather than unmounted, because a control leaving the layout would move the
1366
+ * scroller's own box — which the placement measures; `disabled` and `aria-hidden` then keep the
1367
+ * invisible button out of the focus order and off the accessibility tree.
1368
+ */
1369
+ var ScrollToBottom = () => {
1370
+ const { t } = Hooks.useTranslation(translationKey);
1371
+ const { atEnd, following, scrollToBottom } = useMessageList(CHAT_THREAD_SCROLL_TO_BOTTOM_NAME);
1372
+ const hidden = atEnd || following;
1373
+ return /* @__PURE__ */ jsx(Button.Root, {
1374
+ variant: "primary",
1375
+ icon: "ph--arrow-line-down--regular",
1376
+ iconOnly: true,
1377
+ size: "lg",
1378
+ label: t("scroll-to-bottom.label"),
1379
+ disabled: hidden,
1380
+ "aria-hidden": hidden,
1381
+ classNames: ["absolute bottom-2 left-1/2 -translate-x-1/2 z-10 transition-opacity duration-300", hidden && "opacity-0 pointer-events-none"],
1382
+ "data-testid": "assistant.thread.scroll-to-bottom",
1383
+ onClick: () => scrollToBottom({ behavior: "smooth" })
1384
+ });
1385
+ };
1386
+ ScrollToBottom.displayName = CHAT_THREAD_SCROLL_TO_BOTTOM_NAME;
850
1387
  var ChatThread = {
851
1388
  Root: ChatThreadRoot,
852
- Viewport: ChatThreadViewport
1389
+ Viewport: ChatThreadViewport,
1390
+ ScrollToBottom
853
1391
  };
854
1392
  //#endregion
855
- export { AssistantToolbar, ChatThread, ChatView, MessageChrome, MessageChromeProvider, PromptToolbar, assistantRegistry, createRenderer, estimateRow, translationKey, translations };
1393
+ export { AssistantToolbar, BACKGROUND_TOOL, ChatThread, ChatView, DeliveryAnnotation, DeliveryStatus, MessageChrome, MessageChromeProvider, PromptToolbar, assistantRegistry, createDeliveryWidget, createRenderer, estimateRow, getDelivery, isUnread, linkBareObjectUris, translationKey, translations };
856
1394
 
857
1395
  //# sourceMappingURL=index.mjs.map