@dxos/react-ui-assistant 0.11.1

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 (91) hide show
  1. package/LICENSE +105 -0
  2. package/dist/lib/index.mjs +839 -0
  3. package/dist/lib/index.mjs.map +1 -0
  4. package/dist/lib/testing.mjs +214 -0
  5. package/dist/lib/testing.mjs.map +1 -0
  6. package/dist/types/src/components/ChatThread/ChatThread.d.ts +52 -0
  7. package/dist/types/src/components/ChatThread/ChatThread.d.ts.map +1 -0
  8. package/dist/types/src/components/ChatThread/ChatThread.stories.d.ts +38 -0
  9. package/dist/types/src/components/ChatThread/ChatThread.stories.d.ts.map +1 -0
  10. package/dist/types/src/components/ChatThread/index.d.ts +2 -0
  11. package/dist/types/src/components/ChatThread/index.d.ts.map +1 -0
  12. package/dist/types/src/components/MessageChrome/MessageChrome.d.ts +28 -0
  13. package/dist/types/src/components/MessageChrome/MessageChrome.d.ts.map +1 -0
  14. package/dist/types/src/components/MessageChrome/index.d.ts +2 -0
  15. package/dist/types/src/components/MessageChrome/index.d.ts.map +1 -0
  16. package/dist/types/src/components/index.d.ts +4 -0
  17. package/dist/types/src/components/index.d.ts.map +1 -0
  18. package/dist/types/src/components/widgets/FallbackWidget.d.ts +7 -0
  19. package/dist/types/src/components/widgets/FallbackWidget.d.ts.map +1 -0
  20. package/dist/types/src/components/widgets/ReasoningWidget.d.ts +25 -0
  21. package/dist/types/src/components/widgets/ReasoningWidget.d.ts.map +1 -0
  22. package/dist/types/src/components/widgets/ReasoningWidget.stories.d.ts +18 -0
  23. package/dist/types/src/components/widgets/ReasoningWidget.stories.d.ts.map +1 -0
  24. package/dist/types/src/components/widgets/ReferenceWidget.d.ts +9 -0
  25. package/dist/types/src/components/widgets/ReferenceWidget.d.ts.map +1 -0
  26. package/dist/types/src/components/widgets/SelectWidget.d.ts +14 -0
  27. package/dist/types/src/components/widgets/SelectWidget.d.ts.map +1 -0
  28. package/dist/types/src/components/widgets/StatsWidget.d.ts +12 -0
  29. package/dist/types/src/components/widgets/StatsWidget.d.ts.map +1 -0
  30. package/dist/types/src/components/widgets/StatusWidget.d.ts +12 -0
  31. package/dist/types/src/components/widgets/StatusWidget.d.ts.map +1 -0
  32. package/dist/types/src/components/widgets/SuggestionWidget.d.ts +11 -0
  33. package/dist/types/src/components/widgets/SuggestionWidget.d.ts.map +1 -0
  34. package/dist/types/src/components/widgets/SummaryWidget.d.ts +4 -0
  35. package/dist/types/src/components/widgets/SummaryWidget.d.ts.map +1 -0
  36. package/dist/types/src/components/widgets/ToolWidget.d.ts +10 -0
  37. package/dist/types/src/components/widgets/ToolWidget.d.ts.map +1 -0
  38. package/dist/types/src/components/widgets/ToolWidget.stories.d.ts +31 -0
  39. package/dist/types/src/components/widgets/ToolWidget.stories.d.ts.map +1 -0
  40. package/dist/types/src/components/widgets/defaults.d.ts +11 -0
  41. package/dist/types/src/components/widgets/defaults.d.ts.map +1 -0
  42. package/dist/types/src/components/widgets/format-time.d.ts +2 -0
  43. package/dist/types/src/components/widgets/format-time.d.ts.map +1 -0
  44. package/dist/types/src/components/widgets/index.d.ts +11 -0
  45. package/dist/types/src/components/widgets/index.d.ts.map +1 -0
  46. package/dist/types/src/index.d.ts +6 -0
  47. package/dist/types/src/index.d.ts.map +1 -0
  48. package/dist/types/src/registry.d.ts +11 -0
  49. package/dist/types/src/registry.d.ts.map +1 -0
  50. package/dist/types/src/renderer.d.ts +25 -0
  51. package/dist/types/src/renderer.d.ts.map +1 -0
  52. package/dist/types/src/testing/index.d.ts +2 -0
  53. package/dist/types/src/testing/index.d.ts.map +1 -0
  54. package/dist/types/src/testing/test-generator.d.ts +7 -0
  55. package/dist/types/src/testing/test-generator.d.ts.map +1 -0
  56. package/dist/types/src/testing/test-generator.test.d.ts +2 -0
  57. package/dist/types/src/testing/test-generator.test.d.ts.map +1 -0
  58. package/dist/types/src/translations.d.ts +19 -0
  59. package/dist/types/src/translations.d.ts.map +1 -0
  60. package/dist/types/src/types.d.ts +28 -0
  61. package/dist/types/src/types.d.ts.map +1 -0
  62. package/dist/types/tsconfig.tsbuildinfo +1 -0
  63. package/package.json +67 -0
  64. package/src/components/ChatThread/ChatThread.stories.tsx +419 -0
  65. package/src/components/ChatThread/ChatThread.tsx +188 -0
  66. package/src/components/ChatThread/index.ts +5 -0
  67. package/src/components/MessageChrome/MessageChrome.tsx +147 -0
  68. package/src/components/MessageChrome/index.ts +5 -0
  69. package/src/components/index.ts +7 -0
  70. package/src/components/widgets/FallbackWidget.tsx +26 -0
  71. package/src/components/widgets/ReasoningWidget.stories.tsx +68 -0
  72. package/src/components/widgets/ReasoningWidget.ts +139 -0
  73. package/src/components/widgets/ReferenceWidget.ts +28 -0
  74. package/src/components/widgets/SelectWidget.ts +39 -0
  75. package/src/components/widgets/StatsWidget.ts +31 -0
  76. package/src/components/widgets/StatusWidget.ts +53 -0
  77. package/src/components/widgets/SuggestionWidget.ts +50 -0
  78. package/src/components/widgets/SummaryWidget.tsx +27 -0
  79. package/src/components/widgets/ToolWidget.stories.tsx +55 -0
  80. package/src/components/widgets/ToolWidget.tsx +185 -0
  81. package/src/components/widgets/defaults.ts +14 -0
  82. package/src/components/widgets/format-time.ts +45 -0
  83. package/src/components/widgets/index.ts +14 -0
  84. package/src/index.ts +9 -0
  85. package/src/registry.tsx +144 -0
  86. package/src/renderer.ts +216 -0
  87. package/src/testing/index.ts +5 -0
  88. package/src/testing/test-generator.test.ts +75 -0
  89. package/src/testing/test-generator.ts +280 -0
  90. package/src/translations.ts +27 -0
  91. package/src/types.ts +32 -0
@@ -0,0 +1,839 @@
1
+ import { createContext } from "@radix-ui/react-context";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { Icon, IconBlock, IconButton, setRef, useTranslation } from "@dxos/react-ui";
4
+ import { MessageList, useMessageList } from "@dxos/react-ui-feed";
5
+ import { AnchorWidget, getXmlTextChild } from "@dxos/ui-editor";
6
+ import { NumericTabs, TextCrawl, TogglePanel } from "@dxos/react-ui-components";
7
+ import { JsonHighlighter } from "@dxos/react-ui-syntax-highlighter";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ import { WidgetType } from "@codemirror/view";
10
+ import { Domino, mx } from "@dxos/ui";
11
+ import { isNonNullable, safeParseJson } from "@dxos/util";
12
+ import { Message } from "@dxos/types";
13
+ import { mx as mx$1 } from "@dxos/ui-theme";
14
+ import * as Schema from "effect/Schema";
15
+ //#region src/components/widgets/format-time.ts
16
+ /**
17
+ * Relative ("5 min ago", "yesterday") within two days, absolute date beyond that. The exact
18
+ * timestamp is always available via the `title` tooltip.
19
+ *
20
+ * Recent prompts are the ones a reader places by elapsed time; older ones by date.
21
+ */
22
+ var DAY_MS = 864e5;
23
+ var formatTime = (created, now = Date.now()) => {
24
+ const date = new Date(created);
25
+ if (Number.isNaN(date.getTime())) return "";
26
+ const elapsed = now - date.getTime();
27
+ if (elapsed < 0 || elapsed >= 2 * DAY_MS) return date.toLocaleDateString(void 0, {
28
+ month: "short",
29
+ day: "numeric"
30
+ });
31
+ const format = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
32
+ const minutes = Math.round(elapsed / 6e4);
33
+ if (minutes < 1) return format.format(-Math.max(1, Math.round(elapsed / 1e3)), "second");
34
+ if (minutes < 60) return format.format(-minutes, "minute");
35
+ const hours = Math.round(minutes / 60);
36
+ if (hours < 24) return format.format(-hours, "hour");
37
+ return format.format(-Math.round(hours / 24), "day");
38
+ };
39
+ //#endregion
40
+ //#region src/components/widgets/FallbackWidget.tsx
41
+ var FallbackWidget = ({ _tag, ...props }) => {
42
+ return /* @__PURE__ */ jsx(TogglePanel.Root, { children: /* @__PURE__ */ jsxs(TogglePanel.Content, { children: [/* @__PURE__ */ jsx(TogglePanel.Header, {
43
+ classNames: "dx-group-surface",
44
+ children: _tag
45
+ }), /* @__PURE__ */ jsx(TogglePanel.Body, {
46
+ classNames: "dx-modal-surface",
47
+ children: /* @__PURE__ */ jsx(TogglePanel.Viewport, { children: /* @__PURE__ */ jsx(JsonHighlighter, {
48
+ classNames: "p-2! text-sm",
49
+ data: props
50
+ }) })
51
+ })] }) });
52
+ };
53
+ FallbackWidget.displayName = "Fallback";
54
+ //#endregion
55
+ //#region src/components/widgets/defaults.ts
56
+ var styles = {
57
+ /**
58
+ * Vertical rhythm for block widgets. Padding, never margin: CodeMirror measures a widget from its own
59
+ * box, so a margin sits outside what it can see and its height model drifts from what is rendered —
60
+ * the error accumulates down the document until the turn-fold gutter markers are visibly misaligned.
61
+ * Note padding does not collapse between adjacent siblings the way margins do
62
+ */
63
+ padding: "pt-2 pb-4",
64
+ border: "border border-subdued-separator rounded-sm"
65
+ };
66
+ //#endregion
67
+ //#region src/components/widgets/ReasoningWidget.ts
68
+ /**
69
+ * AI reasoning widget.
70
+ */
71
+ var ReasoningWidget = class extends WidgetType {
72
+ text;
73
+ #pos;
74
+ /** The timer id last registered for this instance (may differ from the map if superseded). */
75
+ #ownedTimerId = null;
76
+ constructor(text, pos) {
77
+ super();
78
+ this.text = text;
79
+ this.#pos = pos === void 0 || pos === "" ? "reasoning" : String(pos);
80
+ }
81
+ eq(other) {
82
+ return this.text === other.text && this.#pos === other.#pos;
83
+ }
84
+ toDOM() {
85
+ return Domino.of("div").classNames(styles.padding).append(Domino.of("div").classNames("relative overflow-hidden p-px", styles.border).attributes({ "data-trail-container": "" }).append(Domino.of("div").classNames("relative z-10 dx-base-surface rounded-sm 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;
86
+ }
87
+ updateDOM(dom) {
88
+ dom.querySelector("[data-reasoning-text]")?.replaceChildren(this.text);
89
+ const container = dom.querySelector("[data-trail-container]");
90
+ let trailHost = container?.querySelector("[data-id]");
91
+ if (container && !trailHost) {
92
+ trailHost = Domino.of("div").attributes({ "data-id": this.#pos }).root;
93
+ container.append(trailHost);
94
+ }
95
+ if (trailHost?.childElementCount === 0) trailHost.append(...createTrailLayers());
96
+ this.#scheduleTrailRemoval(dom);
97
+ return true;
98
+ }
99
+ destroy(_dom) {
100
+ this.#clearOwnedTrailTimer();
101
+ }
102
+ #scheduleTrailRemoval(dom) {
103
+ const previous = trailRemovalTimers.get(this.#pos);
104
+ if (previous !== void 0) clearTimeout(previous);
105
+ const timerId = setTimeout(() => {
106
+ if (trailRemovalTimers.get(this.#pos) !== timerId) return;
107
+ trailRemovalTimers.delete(this.#pos);
108
+ this.#ownedTimerId = null;
109
+ dom.querySelector("[data-id]")?.remove();
110
+ }, TRAIL_REMOVAL_DELAY_MS);
111
+ trailRemovalTimers.set(this.#pos, timerId);
112
+ this.#ownedTimerId = timerId;
113
+ }
114
+ #clearOwnedTrailTimer() {
115
+ const active = trailRemovalTimers.get(this.#pos);
116
+ if (active !== void 0 && active === this.#ownedTimerId) {
117
+ clearTimeout(active);
118
+ trailRemovalTimers.delete(this.#pos);
119
+ }
120
+ this.#ownedTimerId = null;
121
+ }
122
+ };
123
+ var trail = [
124
+ "absolute z-0 aspect-[2/1] w-16",
125
+ "bg-[radial-gradient(at_100%_50%,_theme(colors.green.700),_transparent_80%)]",
126
+ "[offset-anchor:100%_50%] [offset-path:border-box]"
127
+ ];
128
+ /** Delay after the last content update before hiding the trail. */
129
+ var TRAIL_REMOVAL_DELAY_MS = 1e3;
130
+ /**
131
+ * One pending removal timer per logical block. Streaming creates a new widget instance each tick;
132
+ * `destroy` may run after the next instance has already scheduled, so we must not clear a newer timer.
133
+ */
134
+ var trailRemovalTimers = /* @__PURE__ */ new Map();
135
+ var createTrailLayers = () => [Domino.of("div").classNames(...trail, "animate-trail").root, Domino.of("div").classNames(...trail, "animate-trail-offset").root];
136
+ //#endregion
137
+ //#region src/components/widgets/ReferenceWidget.ts
138
+ var ReferenceWidget = class extends WidgetType {
139
+ text;
140
+ dxn;
141
+ constructor(text, dxn) {
142
+ super();
143
+ this.text = text;
144
+ this.dxn = dxn;
145
+ }
146
+ eq(other) {
147
+ return this.dxn === other.dxn;
148
+ }
149
+ toDOM() {
150
+ return Domino.of("div").classNames(styles.padding).append(Domino.of("dx-anchor").classNames("dx-tag--anchor").attributes({ dxn: this.dxn }).text(this.text)).root;
151
+ }
152
+ };
153
+ //#endregion
154
+ //#region src/components/widgets/SelectWidget.ts
155
+ /**
156
+ * Simple prompt widget.
157
+ */
158
+ var SelectWidget = class extends WidgetType {
159
+ options;
160
+ constructor(options) {
161
+ super();
162
+ this.options = options;
163
+ }
164
+ eq(other) {
165
+ return JSON.stringify(this.options) === JSON.stringify(other.options);
166
+ }
167
+ /**
168
+ * NOTE: Container must set var based on user's identity.
169
+ */
170
+ toDOM() {
171
+ return Domino.of("div").attributes({ role: "group" }).classNames(styles.padding, "flex flex-wrap gap-1").append(...this.options.map((option) => Domino.of("button").classNames("dx-button inline-block max-w-[100cqi]").attributes({
172
+ "data-action": "submit",
173
+ "data-value": option,
174
+ "data-density": "md"
175
+ }).text(option))).root;
176
+ }
177
+ };
178
+ //#endregion
179
+ //#region src/components/widgets/StatsWidget.ts
180
+ /**
181
+ * Simple stats widget.
182
+ */
183
+ var StatsWidget = class extends WidgetType {
184
+ text;
185
+ constructor(text) {
186
+ super();
187
+ this.text = text;
188
+ }
189
+ eq(other) {
190
+ return this.text === other.text;
191
+ }
192
+ toDOM() {
193
+ return Domino.of("div").classNames(styles.padding, "text-sm text-placeholder").text(this.text).root;
194
+ }
195
+ updateDOM(dom) {
196
+ dom.textContent = this.text;
197
+ return true;
198
+ }
199
+ };
200
+ //#endregion
201
+ //#region src/components/widgets/StatusWidget.ts
202
+ /**
203
+ * AI status widget.
204
+ */
205
+ var StatusWidget = class extends WidgetType {
206
+ text;
207
+ constructor(text) {
208
+ super();
209
+ this.text = text;
210
+ }
211
+ eq(other) {
212
+ return this.text === other.text;
213
+ }
214
+ toDOM() {
215
+ return Domino.of("div").classNames(styles.padding).append(Domino.of("div").classNames("relative overflow-hidden rounded-sm").append(Domino.of("div").classNames("grid grid-cols-[24px_1fr] gap-x-0.5 gap-y-0 items-start px-0.5 py-0.5 text-placeholder").append(Domino.of("div").classNames("flex h-5 w-full shrink-0 items-center justify-center self-start").append(Domino.svg("ph--info--regular").classNames("shrink-0 size-4 opacity-70")), Domino.of("div").classNames("relative min-w-0").append(Domino.of("div").classNames("relative z-10 rounded-sm text-sm leading-5").attributes({ "data-status-text": "" }).text(this.text))))).root;
216
+ }
217
+ updateDOM(dom) {
218
+ dom.querySelector("[data-status-text]")?.replaceChildren(this.text);
219
+ return true;
220
+ }
221
+ };
222
+ //#endregion
223
+ //#region src/components/widgets/SuggestionWidget.ts
224
+ /**
225
+ * Simple prompt widget.
226
+ */
227
+ var SuggestionWidget = class extends WidgetType {
228
+ text;
229
+ constructor(text) {
230
+ super();
231
+ this.text = text;
232
+ }
233
+ eq(other) {
234
+ return this.text === other.text;
235
+ }
236
+ toDOM() {
237
+ return Domino.of("span").classNames(mx("inline-flex max-w-[calc(100cqi-8px)] py-1 pe-1 overflow-hidden")).append(Domino.of("button").attributes({
238
+ "data-density": "md",
239
+ "data-action": "submit",
240
+ "data-value": this.text
241
+ }).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;
242
+ }
243
+ };
244
+ //#endregion
245
+ //#region src/translations.ts
246
+ var translationKey = "@dxos/react-ui-assistant";
247
+ var translations = [{ "en-US": { [translationKey]: {
248
+ "copy.label": "Copy",
249
+ "rewind.label": "Rewind to this prompt",
250
+ "summary.label": "Summary",
251
+ "stats.label": "Stats",
252
+ "tool-call.label": "Calling",
253
+ "tool-result.label": "Result",
254
+ "tool-error.label": "Tool error",
255
+ "nav-first.label": "First message",
256
+ "nav-previous.label": "Previous message",
257
+ "nav-next.label": "Next message",
258
+ "nav-last.label": "Last message"
259
+ } } }];
260
+ //#endregion
261
+ //#region src/components/widgets/SummaryWidget.tsx
262
+ var SummaryWidget = ({ children }) => {
263
+ const { t } = useTranslation(translationKey);
264
+ return /* @__PURE__ */ jsx(TogglePanel.Root, { children: /* @__PURE__ */ jsxs(TogglePanel.Content, {
265
+ classNames: styles.border,
266
+ children: [/* @__PURE__ */ jsx(TogglePanel.Header, {
267
+ classNames: "text-sm dx-group-surface",
268
+ children: t("summary.label")
269
+ }), /* @__PURE__ */ jsx(TogglePanel.Body, { children: /* @__PURE__ */ jsx("div", {
270
+ className: "p-1 text-sm text-subdued",
271
+ children: getXmlTextChild(children ?? [])
272
+ }) })]
273
+ }) });
274
+ };
275
+ //#endregion
276
+ //#region src/components/widgets/ToolWidget.tsx
277
+ /**
278
+ * A run of tool blocks as one collapsible panel with a tab per call. The `<toolkit>` tag carries
279
+ * the run as JSON — the item is rebuilt from its message alone, so there is no widget-state side
280
+ * channel to accumulate blocks through.
281
+ */
282
+ var ToolWidget = ({ view, children }) => {
283
+ const { t } = useTranslation(translationKey);
284
+ const blocks = useMemo(() => {
285
+ const parsed = safeParseJson(getXmlTextChild(children ?? []) ?? "");
286
+ return Array.isArray(parsed) ? parsed : [];
287
+ }, [children]);
288
+ const items = useMemo(() => {
289
+ let lastToolCall;
290
+ const tools = [];
291
+ return blocks.filter((block) => block._tag === "toolCall" || block._tag === "toolResult" || block._tag === "stats").map((block) => {
292
+ switch (block._tag) {
293
+ case "toolCall": {
294
+ if (block.pending && lastToolCall?.block.toolCallId === block.toolCallId) return null;
295
+ const tool = tools.find((tool) => tool.name === block.name);
296
+ lastToolCall = {
297
+ tool,
298
+ block
299
+ };
300
+ return {
301
+ title: tool?.description ?? [t("tool-call.label"), block.name].filter(Boolean).join(" "),
302
+ icon: block.operationIcon,
303
+ content: {
304
+ ...block,
305
+ input: safeParseJson(block.input)
306
+ }
307
+ };
308
+ }
309
+ case "toolResult": {
310
+ if (block.error) return {
311
+ title: t("tool-error.label"),
312
+ icon: lastToolCall?.block.operationIcon,
313
+ content: block
314
+ };
315
+ const title = lastToolCall?.tool?.description ?? [t("tool-result.label"), lastToolCall?.block.name].filter(Boolean).join(" ");
316
+ const icon = lastToolCall?.block.operationIcon;
317
+ lastToolCall = void 0;
318
+ return {
319
+ title,
320
+ icon,
321
+ content: {
322
+ ...block,
323
+ result: typeof block.result === "string" ? safeParseJson(block.result) : block.result
324
+ }
325
+ };
326
+ }
327
+ case "stats":
328
+ if (!lastToolCall) return null;
329
+ return {
330
+ title: t("stats.label"),
331
+ icon: lastToolCall.block.operationIcon,
332
+ content: block
333
+ };
334
+ }
335
+ }).filter(isNonNullable);
336
+ }, [blocks, t]);
337
+ const handleChangeOpen = useCallback(() => {
338
+ setTimeout(() => {
339
+ view?.requestMeasure();
340
+ }, 1e3);
341
+ }, [view]);
342
+ if (!items.length) return null;
343
+ return /* @__PURE__ */ jsx(ToolPanel, {
344
+ items,
345
+ onChangeOpen: handleChangeOpen
346
+ });
347
+ };
348
+ var DEFAULT_TOOL_ICON = "ph--wrench--regular";
349
+ var ToolPanel = ({ items, onChangeOpen }) => {
350
+ const tabsRef = useRef(null);
351
+ const [selected, setSelected] = useState(0);
352
+ const [open, setOpen] = useState(false);
353
+ useEffect(() => {
354
+ setSelected((prev) => Math.min(prev, Math.max(0, items.length - 1)));
355
+ }, [items.length]);
356
+ useEffect(() => {
357
+ onChangeOpen?.(open);
358
+ if (open) tabsRef.current?.focus();
359
+ }, [open, onChangeOpen]);
360
+ const handleSelect = useCallback((index) => {
361
+ setSelected(index);
362
+ }, []);
363
+ const headerIcon = items[items.length - 1]?.icon ?? items[0]?.icon ?? DEFAULT_TOOL_ICON;
364
+ return /* @__PURE__ */ jsx(TogglePanel.Root, {
365
+ open,
366
+ onChangeOpen: setOpen,
367
+ children: /* @__PURE__ */ jsxs(TogglePanel.Content, { children: [/* @__PURE__ */ jsx(TogglePanel.Header, {
368
+ classNames: "flex items-center gap-2 text-sm",
369
+ children: /* @__PURE__ */ jsxs("div", {
370
+ className: "w-full grid grid-cols-[1fr_auto] items-center gap-2",
371
+ children: [/* @__PURE__ */ jsx(TextCrawl, {
372
+ classNames: "text-description",
373
+ lines: items.map((item) => item.title),
374
+ autoAdvance: true,
375
+ greedy: true
376
+ }, "status-roll"), /* @__PURE__ */ jsx(IconBlock, { children: /* @__PURE__ */ jsx(Icon, {
377
+ icon: headerIcon,
378
+ size: 4
379
+ }) })]
380
+ })
381
+ }), /* @__PURE__ */ jsx(TogglePanel.Body, { children: /* @__PURE__ */ jsxs(TogglePanel.Viewport, {
382
+ classNames: "grid grid-cols-[32px_1fr]",
383
+ children: [/* @__PURE__ */ jsx(NumericTabs, {
384
+ ref: tabsRef,
385
+ classNames: "p-1",
386
+ length: items.length,
387
+ selected,
388
+ onSelect: handleSelect
389
+ }), /* @__PURE__ */ jsx(JsonHighlighter, {
390
+ data: items[selected]?.content,
391
+ classNames: "p-1 text-xs bg-transparent",
392
+ replacer: {
393
+ maxDepth: 3,
394
+ maxArrayLen: 10,
395
+ maxStringLen: 128
396
+ }
397
+ })]
398
+ }) })] })
399
+ });
400
+ };
401
+ //#endregion
402
+ //#region src/registry.tsx
403
+ /**
404
+ * Floor for a portaled panel's collapsed height, reserved before its content paints. `heightMode:
405
+ * 'min'` makes it a floor rather than a pin — without it the row is measured at the height of an
406
+ * empty box and grows a frame later, a row moving under a reader scrolling up.
407
+ */
408
+ var COLLAPSED_HEIGHT = 42;
409
+ /**
410
+ * Widgets for the tags {@link createRenderer} emits.
411
+ *
412
+ * Host-extensible by spreading: the plugin adds its `surface` widget (which renders an
413
+ * app-framework Surface this package must not depend on) over the fallback registered here.
414
+ * DOM widgets render synchronously from the tag alone; React widgets are portaled and declare a
415
+ * collapsed-height floor so the row's first measurement is honest.
416
+ */
417
+ var assistantRegistry = {
418
+ "prompt": { block: true },
419
+ "link-preview": {
420
+ block: false,
421
+ urlSchemes: ["dxn:", "echo:"],
422
+ factory: ({ label, dxn }) => typeof label === "string" && typeof dxn === "string" ? new AnchorWidget(label, dxn) : null
423
+ },
424
+ "synthetic": {
425
+ block: true,
426
+ factory: ({ children, range }) => {
427
+ const text = getXmlTextChild(children ?? []);
428
+ return text ? new ReasoningWidget(text, range?.from) : null;
429
+ }
430
+ },
431
+ "reasoning": {
432
+ block: true,
433
+ streaming: true,
434
+ factory: ({ children, range }) => {
435
+ const text = getXmlTextChild(children ?? []);
436
+ return text ? new ReasoningWidget(text, range?.from) : null;
437
+ }
438
+ },
439
+ "status": {
440
+ block: true,
441
+ streaming: true,
442
+ factory: ({ children }) => {
443
+ const text = getXmlTextChild(children ?? []);
444
+ return text ? new StatusWidget(text) : null;
445
+ }
446
+ },
447
+ "reference": {
448
+ block: false,
449
+ factory: ({ children, ref }) => {
450
+ const text = getXmlTextChild(children ?? []);
451
+ return text && typeof ref === "string" ? new ReferenceWidget(text, ref) : null;
452
+ }
453
+ },
454
+ "suggestion": {
455
+ block: false,
456
+ factory: ({ children }) => {
457
+ const text = getXmlTextChild(children ?? []);
458
+ return text ? new SuggestionWidget(text) : null;
459
+ }
460
+ },
461
+ "select": {
462
+ block: true,
463
+ factory: ({ children }) => {
464
+ const options = (children ?? []).map((option) => option?._tag === "option" && getXmlTextChild(option.children)).filter(Boolean);
465
+ return options.length ? new SelectWidget(options) : null;
466
+ }
467
+ },
468
+ "stats": {
469
+ block: true,
470
+ factory: ({ children }) => {
471
+ const text = getXmlTextChild(children ?? []);
472
+ return text ? new StatsWidget(text) : null;
473
+ }
474
+ },
475
+ "toolkit": {
476
+ block: true,
477
+ streaming: true,
478
+ estimatedHeight: () => COLLAPSED_HEIGHT,
479
+ heightMode: "min",
480
+ Component: ToolWidget
481
+ },
482
+ "summary": {
483
+ block: true,
484
+ estimatedHeight: () => COLLAPSED_HEIGHT,
485
+ heightMode: "min",
486
+ Component: SummaryWidget
487
+ },
488
+ "surface": {
489
+ block: true,
490
+ estimatedHeight: () => COLLAPSED_HEIGHT,
491
+ heightMode: "min",
492
+ Component: FallbackWidget
493
+ },
494
+ "json": {
495
+ block: true,
496
+ estimatedHeight: () => COLLAPSED_HEIGHT,
497
+ heightMode: "min",
498
+ Component: FallbackWidget
499
+ }
500
+ };
501
+ //#endregion
502
+ //#region src/renderer.ts
503
+ /**
504
+ * The assistant's projection of a message: each block flattened to markdown, everything that is not
505
+ * prose emitted as an XML tag for the registry's widgets. The view type is a filter over blocks —
506
+ * the model always carries everything, so switching views is a re-render, not a re-fetch.
507
+ *
508
+ * Consecutive tool blocks (`toolCall`/`toolResult`/`stats`) render as ONE `<toolkit>` tag carrying
509
+ * the run as JSON: the panel widget shows a run of calls as tabs, and the item is rebuilt from its
510
+ * message alone — there is no side channel accumulating widget state (the old `MessageSyncer`
511
+ * machinery this package retires).
512
+ */
513
+ var createRenderer = (viewType, { getObjectLabel = () => "Object" } = {}) => {
514
+ return (message) => {
515
+ const blocks = message.blocks.filter((block) => isBlockVisible(viewType, block));
516
+ const segments = [];
517
+ let tools = [];
518
+ const flushTools = () => {
519
+ if (tools.length) {
520
+ segments.push(toolkitTag(tools));
521
+ tools = [];
522
+ }
523
+ };
524
+ for (const block of blocks) {
525
+ if (block._tag === "toolCall" || block._tag === "toolResult" || block._tag === "stats" && tools.length) {
526
+ tools.push(block);
527
+ continue;
528
+ }
529
+ flushTools();
530
+ const rendered = blockToMarkdown(message, block, getObjectLabel);
531
+ if (rendered) segments.push(rendered);
532
+ }
533
+ flushTools();
534
+ return {
535
+ kind: "markdown",
536
+ text: segments.reduce((parts, segment) => {
537
+ const previous = parts[parts.length - 1];
538
+ if (previous?.startsWith("<suggestion") && segment.startsWith("<suggestion")) parts[parts.length - 1] = `${previous} ${segment}`;
539
+ else parts.push(segment);
540
+ return parts;
541
+ }, []).join("\n\n")
542
+ };
543
+ };
544
+ };
545
+ var isBlockVisible = (viewType, block) => {
546
+ switch (viewType) {
547
+ case "debug": return true;
548
+ case "summary": return block._tag === "text" && block.disposition !== "synthetic";
549
+ case "normal": return block._tag !== "reasoning";
550
+ default: return true;
551
+ }
552
+ };
553
+ var blockToMarkdown = (message, block, getObjectLabel) => {
554
+ switch (block._tag) {
555
+ case "text":
556
+ if (message.sender.role === "user") return block.disposition === "synthetic" ? tag("synthetic", block.text, block) : tag("prompt", block.text, block);
557
+ return block.text.trim() || void 0;
558
+ case "reasoning": return tag("reasoning", block.reasoningText ?? block.redactedText ?? "", block);
559
+ case "status": return tag("status", block.statusText, block);
560
+ case "summary": return tag("summary", block.content, block);
561
+ case "suggestion": return block.pending ? void 0 : tag("suggestion", block.text, block);
562
+ case "select": return block.pending || !block.options.length ? void 0 : `<select>${block.options.map((option) => `<option>${escapeXml(option)}</option>`).join("")}</select>`;
563
+ case "reference": {
564
+ if (block.pending) return;
565
+ const uri = block.reference.uri;
566
+ return `<reference ref="${escapeAttribute(uri.toString())}">${escapeXml(getObjectLabel(uri))}</reference>`;
567
+ }
568
+ case "stats": return;
569
+ case "surface": return block.pending ? void 0 : `<surface role="${escapeAttribute(block.role)}">${escapeXml(JSON.stringify(block.data ?? {}))}</surface>`;
570
+ default: return tag("json", JSON.stringify(block), block);
571
+ }
572
+ };
573
+ /** A run of tool blocks as one tag; the widget parses the payload back out. */
574
+ var toolkitTag = (blocks) => {
575
+ return `<toolkit${blocks.some((block) => block.pending) ? " pending=\"true\"" : ""}>${escapeXml(JSON.stringify(blocks))}</toolkit>`;
576
+ };
577
+ /**
578
+ * An XML tag holding block content.
579
+ *
580
+ * A pending block is emitted **unclosed**, which is the streaming contract: the item reconciles by
581
+ * appending, so a closing tag written before the content is complete would be rewritten per chunk.
582
+ * Paragraph breaks inside tag content are collapsed, since a blank line would end the enclosing
583
+ * markdown block and leave the rest of the tag parsing as prose.
584
+ */
585
+ var tag = (name, content, block) => {
586
+ const text = escapeXml(content.replace(/\n\n+/g, " ").trim());
587
+ if (!text.length && !block.pending) return "";
588
+ const open = `<${name}>`;
589
+ return block.pending ? `${open}${text}` : `${open}${text}</${name}>`;
590
+ };
591
+ var escapeXml = (raw) => raw.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
592
+ var escapeAttribute = (raw) => escapeXml(raw).replace(/"/g, "&quot;");
593
+ /** Chrome around every row: the padding and the hover toolbar's reserved line. */
594
+ var ROW_CHROME = 46;
595
+ /** Height of a wrapped line of body text, and how many characters fit at typical thread width. */
596
+ var LINE_HEIGHT = 24;
597
+ var LINE_CHARS = 90;
598
+ /** A collapsed panel: reasoning, a tool run, a summary. */
599
+ var PANEL_HEIGHT = 50;
600
+ /**
601
+ * What a row will measure, from the message alone. Rough on purpose — its only job is to be close
602
+ * enough that measuring the row does not move the rows below it.
603
+ */
604
+ var estimateRow = (message) => {
605
+ let height = ROW_CHROME;
606
+ for (const block of message.blocks) switch (block._tag) {
607
+ case "text":
608
+ for (const paragraph of block.text.split("\n\n")) height += Math.max(1, Math.ceil(paragraph.length / LINE_CHARS)) * LINE_HEIGHT;
609
+ break;
610
+ case "suggestion":
611
+ height += 32;
612
+ break;
613
+ case "select":
614
+ height += block.options.length * 34;
615
+ break;
616
+ case "toolResult":
617
+ case "stats": break;
618
+ default: height += PANEL_HEIGHT;
619
+ }
620
+ return height;
621
+ };
622
+ //#endregion
623
+ //#region src/components/MessageChrome/MessageChrome.tsx
624
+ var MESSAGE_CHROME_NAME = "MessageChrome";
625
+ var [MessageChromeProvider, useMessageChromeContext] = createContext(MESSAGE_CHROME_NAME);
626
+ /** Copies the message's extracted text — the model's truth, not the DOM's partial render. */
627
+ var CopyButton = ({ message }) => {
628
+ const { t } = useTranslation(translationKey);
629
+ return /* @__PURE__ */ jsx(IconButton, {
630
+ icon: "ph--copy--regular",
631
+ iconOnly: true,
632
+ label: t("copy.label"),
633
+ variant: "ghost",
634
+ density: "sm",
635
+ onClick: () => void navigator.clipboard?.writeText(Message.extractText(message))
636
+ });
637
+ };
638
+ var timeOf = (message) => /* @__PURE__ */ jsx("time", {
639
+ dateTime: message.created,
640
+ title: new Date(message.created).toLocaleString(),
641
+ children: formatTime(message.created)
642
+ });
643
+ /**
644
+ * The controls under the reader's own prompt: copy, rewind (edit-and-resend), and when it was
645
+ * sent. Revealed on hover by the chrome, but never removed from flow — chrome that appears and
646
+ * disappears changes the row's height, and a pointer travelling down a scrolling list would then
647
+ * move every row below it.
648
+ */
649
+ var PromptToolbar = ({ classNames, message }) => {
650
+ const { t } = useTranslation(translationKey);
651
+ const { onRewind } = useMessageChromeContext("PromptToolbar");
652
+ return /* @__PURE__ */ jsxs("div", {
653
+ role: "toolbar",
654
+ className: mx$1("flex items-center gap-1 text-xs text-description", classNames),
655
+ children: [
656
+ /* @__PURE__ */ jsx(CopyButton, { message }),
657
+ onRewind && /* @__PURE__ */ jsx(IconButton, {
658
+ icon: "ph--clock-counter-clockwise--regular",
659
+ iconOnly: true,
660
+ label: t("rewind.label"),
661
+ variant: "ghost",
662
+ density: "sm",
663
+ "data-testid": "chat.rewind",
664
+ onClick: () => onRewind(message.id)
665
+ }),
666
+ timeOf(message)
667
+ ]
668
+ });
669
+ };
670
+ PromptToolbar.displayName = "PromptToolbar";
671
+ /** The controls under an answer: copy, and when the answer finished. */
672
+ var AssistantToolbar = ({ classNames, message }) => {
673
+ return /* @__PURE__ */ jsxs("div", {
674
+ role: "toolbar",
675
+ className: mx$1("flex items-center gap-1 text-xs text-description", classNames),
676
+ children: [/* @__PURE__ */ jsx(CopyButton, { message }), timeOf(message)]
677
+ });
678
+ };
679
+ AssistantToolbar.displayName = "AssistantToolbar";
680
+ /** Shared hover reveal: present in flow at all times, visible when the row is under the pointer. */
681
+ var reveal = "pt-1 opacity-0 transition-opacity group-hover:opacity-100";
682
+ var Row = ({ children, classNames }) => /* @__PURE__ */ jsx("div", {
683
+ className: mx$1("group relative px-2 py-2", classNames),
684
+ "data-testid": "feed.message",
685
+ children
686
+ });
687
+ /**
688
+ * The assistant feed's per-message frame: the reader's prompts and the model's answers are framed
689
+ * differently because they are different kinds of thing — a prompt is an instruction the thread
690
+ * can be rewound to, an answer is a result.
691
+ *
692
+ * A prompt sits apart from the model's prose: right-aligned, at most two thirds of the viewport —
693
+ * the chat convention that makes whose-turn legible at a glance — with its toolbar following the
694
+ * bubble's edge.
695
+ */
696
+ var MessageChrome = ({ message, selected, children }) => {
697
+ const prompt = message.sender.role === "user";
698
+ return /* @__PURE__ */ jsx(Row, {
699
+ classNames: mx$1(selected && "bg-hover-surface"),
700
+ children: prompt ? /* @__PURE__ */ jsx("div", {
701
+ className: "min-w-0 flex flex-col items-end",
702
+ children: /* @__PURE__ */ jsxs("div", {
703
+ className: "max-w-[66%] min-w-0",
704
+ children: [/* @__PURE__ */ jsx("div", {
705
+ className: "px-4 py-3 border-s-2 border-accent-bg rounded-sm bg-input-surface",
706
+ children
707
+ }), /* @__PURE__ */ jsx(PromptToolbar, {
708
+ classNames: mx$1("justify-end", reveal),
709
+ message
710
+ })]
711
+ })
712
+ }) : /* @__PURE__ */ jsxs("div", {
713
+ className: "min-w-0",
714
+ children: [children, /* @__PURE__ */ jsx(AssistantToolbar, {
715
+ classNames: reveal,
716
+ message
717
+ })]
718
+ })
719
+ });
720
+ };
721
+ MessageChrome.displayName = MESSAGE_CHROME_NAME;
722
+ //#endregion
723
+ //#region src/components/ChatThread/ChatThread.tsx
724
+ var [ChatThreadProvider, useChatThreadContext] = createContext("ChatThread");
725
+ /** Bridges the feed's context out to the host's ref; renders nothing. */
726
+ var ControllerBridge = ({ controllerRef }) => {
727
+ const { model, scrollToBottom, scrollToIndex, navigation } = useMessageList("ChatThread.Controller");
728
+ useEffect(() => {
729
+ setRef(controllerRef, {
730
+ model,
731
+ scrollToBottom,
732
+ scrollToIndex,
733
+ navigation
734
+ });
735
+ return () => {
736
+ setRef(controllerRef, null);
737
+ };
738
+ }, [
739
+ controllerRef,
740
+ model,
741
+ scrollToBottom,
742
+ scrollToIndex,
743
+ navigation
744
+ ]);
745
+ return null;
746
+ };
747
+ /**
748
+ * Headless root: the feed's `MessageList.Root` configured as an assistant thread — the view-typed
749
+ * renderer, the widget registry, the prompt/answer chrome, and the chat behaviours (follow the
750
+ * streaming tail; reserve room to bring the last prompt to the top). Composes with the feed's own
751
+ * parts: `MessageList.Nav`, `useMessageList`, and the rails all work inside it.
752
+ */
753
+ var ChatThreadRoot = ({ children, model, viewType, registry, getObjectLabel, userHue, debug, onEvent, onRangeChange, controllerRef }) => {
754
+ const renderer = useMemo(() => createRenderer(viewType, { getObjectLabel }), [viewType, getObjectLabel]);
755
+ const merged = useMemo(() => registry ? {
756
+ ...assistantRegistry,
757
+ ...registry
758
+ } : assistantRegistry, [registry]);
759
+ const handleRewind = useCallback((id) => onEvent?.({
760
+ type: "rewind",
761
+ id
762
+ }), [onEvent]);
763
+ return /* @__PURE__ */ jsx(ChatThreadProvider, {
764
+ userHue,
765
+ onEvent,
766
+ children: /* @__PURE__ */ jsx(MessageChromeProvider, {
767
+ onRewind: onEvent ? handleRewind : void 0,
768
+ children: /* @__PURE__ */ jsxs(MessageList.Root, {
769
+ model,
770
+ renderer,
771
+ registry: merged,
772
+ Chrome: MessageChrome,
773
+ estimateSize: estimateRow,
774
+ debug,
775
+ stickyBottom: true,
776
+ scrollPastEnd: true,
777
+ onRangeChange,
778
+ children: [controllerRef && /* @__PURE__ */ jsx(ControllerBridge, { controllerRef }), children]
779
+ })
780
+ })
781
+ });
782
+ };
783
+ ChatThreadRoot.displayName = "ChatThread.Root";
784
+ var CHAT_THREAD_VIEWPORT_NAME = "ChatThread.Viewport";
785
+ /**
786
+ * The scrolling thread. Suggestion and select widgets are DOM widgets carrying
787
+ * `data-action="submit"` buttons; one delegated listener here turns those clicks into `submit`
788
+ * events, which is what keeps the widgets renderable from the tag alone.
789
+ */
790
+ var ChatThreadViewport = ({ children, ...props }) => {
791
+ const { userHue, onEvent } = useChatThreadContext(CHAT_THREAD_VIEWPORT_NAME);
792
+ const handleClick = useCallback((event) => {
793
+ const text = event.target.closest("[data-action=\"submit\"]")?.getAttribute("data-value");
794
+ if (text) {
795
+ event.preventDefault();
796
+ event.stopPropagation();
797
+ onEvent?.({
798
+ type: "submit",
799
+ text
800
+ });
801
+ }
802
+ }, [onEvent]);
803
+ return /* @__PURE__ */ jsx("div", {
804
+ className: "contents",
805
+ "data-testid": "assistant.thread",
806
+ "data-hue": userHue,
807
+ onClickCapture: handleClick,
808
+ children: /* @__PURE__ */ jsx(MessageList.Viewport, {
809
+ ...props,
810
+ children
811
+ })
812
+ });
813
+ };
814
+ ChatThreadViewport.displayName = CHAT_THREAD_VIEWPORT_NAME;
815
+ var ChatThread = {
816
+ Root: ChatThreadRoot,
817
+ Viewport: ChatThreadViewport
818
+ };
819
+ //#endregion
820
+ //#region src/types.ts
821
+ /**
822
+ * Which projection of the thread the reader sees. A view is a filter over blocks, applied by the
823
+ * renderer — the model always carries everything.
824
+ *
825
+ * - `summary`: only conversational text (prompts and replies).
826
+ * - `normal`: everything except reasoning.
827
+ * - `thinking`: everything, reasoning included.
828
+ * - `debug`: every block, raw fallbacks visible.
829
+ */
830
+ var ChatView = Schema.Union([
831
+ Schema.Literal("normal").annotate({ title: "Normal" }),
832
+ Schema.Literal("summary").annotate({ title: "Summary" }),
833
+ Schema.Literal("thinking").annotate({ title: "Thinking" }),
834
+ Schema.Literal("debug").annotate({ title: "Debug" })
835
+ ]);
836
+ //#endregion
837
+ export { AssistantToolbar, ChatThread, ChatView, FallbackWidget, MessageChrome, MessageChromeProvider, PromptToolbar, ReasoningWidget, ReferenceWidget, SelectWidget, StatsWidget, StatusWidget, SuggestionWidget, SummaryWidget, ToolWidget, assistantRegistry, createRenderer, estimateRow, formatTime, translationKey, translations };
838
+
839
+ //# sourceMappingURL=index.mjs.map