@dxos/react-ui-assistant 0.11.1 → 0.12.0-next-73daef452f1464b62efdbca75fb0067c1066f264

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 (93) hide show
  1. package/dist/lib/index.mjs +201 -183
  2. package/dist/lib/index.mjs.map +1 -1
  3. package/dist/lib/testing.mjs +7 -0
  4. package/dist/lib/testing.mjs.map +1 -1
  5. package/dist/lib/translations.mjs +21 -0
  6. package/dist/lib/translations.mjs.map +1 -0
  7. package/dist/lib/types.mjs +21 -0
  8. package/dist/lib/types.mjs.map +1 -0
  9. package/dist/types/src/components/ChatThread/ChatThread.d.ts +27 -26
  10. package/dist/types/src/components/ChatThread/ChatThread.d.ts.map +1 -1
  11. package/dist/types/src/components/ChatThread/ChatThread.stories.d.ts.map +1 -1
  12. package/dist/types/src/components/MessageChrome/MessageChrome.d.ts +8 -1
  13. package/dist/types/src/components/MessageChrome/MessageChrome.d.ts.map +1 -1
  14. package/dist/types/src/components/MessageChrome/MessageChrome.stories.d.ts +58 -0
  15. package/dist/types/src/components/MessageChrome/MessageChrome.stories.d.ts.map +1 -0
  16. package/dist/types/src/components/MessageChrome/format-time.d.ts +12 -0
  17. package/dist/types/src/components/MessageChrome/format-time.d.ts.map +1 -0
  18. package/dist/types/src/components/index.d.ts +0 -1
  19. package/dist/types/src/components/index.d.ts.map +1 -1
  20. package/dist/types/src/registry.d.ts.map +1 -1
  21. package/dist/types/src/registry.stories.d.ts +55 -0
  22. package/dist/types/src/registry.stories.d.ts.map +1 -0
  23. package/dist/types/src/renderer.d.ts.map +1 -1
  24. package/dist/types/src/testing/test-generator.d.ts.map +1 -1
  25. package/dist/types/src/translations.d.ts +2 -0
  26. package/dist/types/src/translations.d.ts.map +1 -1
  27. package/dist/types/src/types.d.ts.map +1 -1
  28. package/dist/types/src/widgets/FallbackWidget.d.ts.map +1 -0
  29. package/dist/types/src/widgets/ReasoningWidget.d.ts.map +1 -0
  30. package/dist/types/src/{components/widgets → widgets}/ReferenceWidget.d.ts +1 -1
  31. package/dist/types/src/widgets/ReferenceWidget.d.ts.map +1 -0
  32. package/dist/types/src/widgets/SelectWidget.d.ts.map +1 -0
  33. package/dist/types/src/widgets/StatsWidget.d.ts.map +1 -0
  34. package/dist/types/src/widgets/StatusWidget.d.ts.map +1 -0
  35. package/dist/types/src/widgets/SuggestionWidget.d.ts.map +1 -0
  36. package/dist/types/src/widgets/SummaryWidget.d.ts.map +1 -0
  37. package/dist/types/src/widgets/ToolWidget.d.ts.map +1 -0
  38. package/dist/types/src/{components/widgets → widgets}/index.d.ts +0 -1
  39. package/dist/types/src/widgets/index.d.ts.map +1 -0
  40. package/dist/types/tsconfig.tsbuildinfo +1 -1
  41. package/package.json +27 -17
  42. package/src/components/ChatThread/ChatThread.stories.tsx +27 -7
  43. package/src/components/ChatThread/ChatThread.tsx +35 -37
  44. package/src/components/MessageChrome/MessageChrome.stories.tsx +135 -0
  45. package/src/components/MessageChrome/MessageChrome.tsx +91 -18
  46. package/src/components/{widgets → MessageChrome}/format-time.ts +19 -3
  47. package/src/components/index.ts +0 -1
  48. package/src/registry.stories.tsx +151 -0
  49. package/src/registry.tsx +10 -1
  50. package/src/renderer.ts +8 -5
  51. package/src/testing/test-generator.ts +14 -0
  52. package/src/translations.ts +2 -0
  53. package/src/types.ts +3 -0
  54. package/src/{components/widgets → widgets}/ReasoningWidget.ts +22 -26
  55. package/src/{components/widgets → widgets}/ReferenceWidget.ts +4 -4
  56. package/src/{components/widgets → widgets}/SelectWidget.ts +1 -3
  57. package/src/{components/widgets → widgets}/StatsWidget.ts +1 -3
  58. package/src/widgets/StatusWidget.ts +49 -0
  59. package/src/{components/widgets → widgets}/SuggestionWidget.ts +2 -4
  60. package/src/{components/widgets → widgets}/SummaryWidget.tsx +2 -3
  61. package/src/{components/widgets → widgets}/ToolWidget.tsx +69 -58
  62. package/src/{components/widgets → widgets}/index.ts +0 -1
  63. package/dist/types/src/components/widgets/FallbackWidget.d.ts.map +0 -1
  64. package/dist/types/src/components/widgets/ReasoningWidget.d.ts.map +0 -1
  65. package/dist/types/src/components/widgets/ReasoningWidget.stories.d.ts +0 -18
  66. package/dist/types/src/components/widgets/ReasoningWidget.stories.d.ts.map +0 -1
  67. package/dist/types/src/components/widgets/ReferenceWidget.d.ts.map +0 -1
  68. package/dist/types/src/components/widgets/SelectWidget.d.ts.map +0 -1
  69. package/dist/types/src/components/widgets/StatsWidget.d.ts.map +0 -1
  70. package/dist/types/src/components/widgets/StatusWidget.d.ts.map +0 -1
  71. package/dist/types/src/components/widgets/SuggestionWidget.d.ts.map +0 -1
  72. package/dist/types/src/components/widgets/SummaryWidget.d.ts.map +0 -1
  73. package/dist/types/src/components/widgets/ToolWidget.d.ts.map +0 -1
  74. package/dist/types/src/components/widgets/ToolWidget.stories.d.ts +0 -31
  75. package/dist/types/src/components/widgets/ToolWidget.stories.d.ts.map +0 -1
  76. package/dist/types/src/components/widgets/defaults.d.ts +0 -11
  77. package/dist/types/src/components/widgets/defaults.d.ts.map +0 -1
  78. package/dist/types/src/components/widgets/format-time.d.ts +0 -2
  79. package/dist/types/src/components/widgets/format-time.d.ts.map +0 -1
  80. package/dist/types/src/components/widgets/index.d.ts.map +0 -1
  81. package/src/components/widgets/ReasoningWidget.stories.tsx +0 -68
  82. package/src/components/widgets/StatusWidget.ts +0 -53
  83. package/src/components/widgets/ToolWidget.stories.tsx +0 -55
  84. package/src/components/widgets/defaults.ts +0 -14
  85. /package/dist/types/src/{components/widgets → widgets}/FallbackWidget.d.ts +0 -0
  86. /package/dist/types/src/{components/widgets → widgets}/ReasoningWidget.d.ts +0 -0
  87. /package/dist/types/src/{components/widgets → widgets}/SelectWidget.d.ts +0 -0
  88. /package/dist/types/src/{components/widgets → widgets}/StatsWidget.d.ts +0 -0
  89. /package/dist/types/src/{components/widgets → widgets}/StatusWidget.d.ts +0 -0
  90. /package/dist/types/src/{components/widgets → widgets}/SuggestionWidget.d.ts +0 -0
  91. /package/dist/types/src/{components/widgets → widgets}/SummaryWidget.d.ts +0 -0
  92. /package/dist/types/src/{components/widgets → widgets}/ToolWidget.d.ts +0 -0
  93. /package/src/{components/widgets → widgets}/FallbackWidget.tsx +0 -0
@@ -1,43 +1,19 @@
1
+ import { translationKey, translations } from "./translations.mjs";
2
+ import { ChatView } from "./types.mjs";
1
3
  import { createContext } from "@radix-ui/react-context";
2
4
  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 { MessageList, isPrompt } from "@dxos/react-ui-feed";
5
6
  import { AnchorWidget, getXmlTextChild } from "@dxos/ui-editor";
6
- import { NumericTabs, TextCrawl, TogglePanel } from "@dxos/react-ui-components";
7
+ import { NumericTabs, TogglePanel } from "@dxos/react-ui-components";
7
8
  import { JsonHighlighter } from "@dxos/react-ui-syntax-highlighter";
8
9
  import { jsx, jsxs } from "react/jsx-runtime";
9
10
  import { WidgetType } from "@codemirror/view";
10
11
  import { Domino, mx } from "@dxos/ui";
11
- import { isNonNullable, safeParseJson } from "@dxos/util";
12
+ import { Icon, IconBlock, IconButton, TextCrawl, useTranslation } from "@dxos/react-ui";
13
+ import { safeParseJson } from "@dxos/util";
12
14
  import { Message } from "@dxos/types";
13
15
  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
16
+ //#region src/widgets/FallbackWidget.tsx
41
17
  var FallbackWidget = ({ _tag, ...props }) => {
42
18
  return /* @__PURE__ */ jsx(TogglePanel.Root, { children: /* @__PURE__ */ jsxs(TogglePanel.Content, { children: [/* @__PURE__ */ jsx(TogglePanel.Header, {
43
19
  classNames: "dx-group-surface",
@@ -52,19 +28,7 @@ var FallbackWidget = ({ _tag, ...props }) => {
52
28
  };
53
29
  FallbackWidget.displayName = "Fallback";
54
30
  //#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
31
+ //#region src/widgets/ReasoningWidget.ts
68
32
  /**
69
33
  * AI reasoning widget.
70
34
  */
@@ -82,7 +46,7 @@ var ReasoningWidget = class extends WidgetType {
82
46
  return this.text === other.text && this.#pos === other.#pos;
83
47
  }
84
48
  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;
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;
86
50
  }
87
51
  updateDOM(dom) {
88
52
  dom.querySelector("[data-reasoning-text]")?.replaceChildren(this.text);
@@ -134,7 +98,7 @@ var TRAIL_REMOVAL_DELAY_MS = 1e3;
134
98
  var trailRemovalTimers = /* @__PURE__ */ new Map();
135
99
  var createTrailLayers = () => [Domino.of("div").classNames(...trail, "animate-trail").root, Domino.of("div").classNames(...trail, "animate-trail-offset").root];
136
100
  //#endregion
137
- //#region src/components/widgets/ReferenceWidget.ts
101
+ //#region src/widgets/ReferenceWidget.ts
138
102
  var ReferenceWidget = class extends WidgetType {
139
103
  text;
140
104
  dxn;
@@ -147,11 +111,11 @@ var ReferenceWidget = class extends WidgetType {
147
111
  return this.dxn === other.dxn;
148
112
  }
149
113
  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;
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;
151
115
  }
152
116
  };
153
117
  //#endregion
154
- //#region src/components/widgets/SelectWidget.ts
118
+ //#region src/widgets/SelectWidget.ts
155
119
  /**
156
120
  * Simple prompt widget.
157
121
  */
@@ -168,7 +132,7 @@ var SelectWidget = class extends WidgetType {
168
132
  * NOTE: Container must set var based on user's identity.
169
133
  */
170
134
  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({
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({
172
136
  "data-action": "submit",
173
137
  "data-value": option,
174
138
  "data-density": "md"
@@ -176,7 +140,7 @@ var SelectWidget = class extends WidgetType {
176
140
  }
177
141
  };
178
142
  //#endregion
179
- //#region src/components/widgets/StatsWidget.ts
143
+ //#region src/widgets/StatsWidget.ts
180
144
  /**
181
145
  * Simple stats widget.
182
146
  */
@@ -190,7 +154,7 @@ var StatsWidget = class extends WidgetType {
190
154
  return this.text === other.text;
191
155
  }
192
156
  toDOM() {
193
- return Domino.of("div").classNames(styles.padding, "text-sm text-placeholder").text(this.text).root;
157
+ return Domino.of("div").classNames("text-sm text-placeholder").text(this.text).root;
194
158
  }
195
159
  updateDOM(dom) {
196
160
  dom.textContent = this.text;
@@ -198,7 +162,7 @@ var StatsWidget = class extends WidgetType {
198
162
  }
199
163
  };
200
164
  //#endregion
201
- //#region src/components/widgets/StatusWidget.ts
165
+ //#region src/widgets/StatusWidget.ts
202
166
  /**
203
167
  * AI status widget.
204
168
  */
@@ -212,7 +176,7 @@ var StatusWidget = class extends WidgetType {
212
176
  return this.text === other.text;
213
177
  }
214
178
  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;
179
+ return Domino.of("div").append(Domino.of("div").classNames("relative overflow-hidden rounded-md").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-md text-sm leading-5").attributes({ "data-status-text": "" }).text(this.text))))).root;
216
180
  }
217
181
  updateDOM(dom) {
218
182
  dom.querySelector("[data-status-text]")?.replaceChildren(this.text);
@@ -220,7 +184,7 @@ var StatusWidget = class extends WidgetType {
220
184
  }
221
185
  };
222
186
  //#endregion
223
- //#region src/components/widgets/SuggestionWidget.ts
187
+ //#region src/widgets/SuggestionWidget.ts
224
188
  /**
225
189
  * Simple prompt widget.
226
190
  */
@@ -234,7 +198,7 @@ var SuggestionWidget = class extends WidgetType {
234
198
  return this.text === other.text;
235
199
  }
236
200
  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({
201
+ return Domino.of("span").classNames(mx("inline-flex max-w-[calc(100cqi-8px)] overflow-hidden")).append(Domino.of("button").attributes({
238
202
  "data-density": "md",
239
203
  "data-action": "submit",
240
204
  "data-value": this.text
@@ -242,27 +206,11 @@ var SuggestionWidget = class extends WidgetType {
242
206
  }
243
207
  };
244
208
  //#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
209
+ //#region src/widgets/SummaryWidget.tsx
262
210
  var SummaryWidget = ({ children }) => {
263
211
  const { t } = useTranslation(translationKey);
264
212
  return /* @__PURE__ */ jsx(TogglePanel.Root, { children: /* @__PURE__ */ jsxs(TogglePanel.Content, {
265
- classNames: styles.border,
213
+ classNames: "border border-subdued-separator rounded-md",
266
214
  children: [/* @__PURE__ */ jsx(TogglePanel.Header, {
267
215
  classNames: "text-sm dx-group-surface",
268
216
  children: t("summary.label")
@@ -273,7 +221,7 @@ var SummaryWidget = ({ children }) => {
273
221
  }) });
274
222
  };
275
223
  //#endregion
276
- //#region src/components/widgets/ToolWidget.tsx
224
+ //#region src/widgets/ToolWidget.tsx
277
225
  /**
278
226
  * A run of tool blocks as one collapsible panel with a tab per call. The `<toolkit>` tag carries
279
227
  * the run as JSON — the item is rebuilt from its message alone, so there is no widget-state side
@@ -288,51 +236,58 @@ var ToolWidget = ({ view, children }) => {
288
236
  const items = useMemo(() => {
289
237
  let lastToolCall;
290
238
  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 {
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, {
311
266
  title: t("tool-error.label"),
312
267
  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
- };
268
+ content: block.error
269
+ });
270
+ break;
326
271
  }
327
- case "stats":
328
- if (!lastToolCall) return null;
329
- return {
330
- title: t("stats.label"),
331
- icon: lastToolCall.block.operationIcon,
332
- content: block
333
- };
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;
334
281
  }
335
- }).filter(isNonNullable);
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;
336
291
  }, [blocks, t]);
337
292
  const handleChangeOpen = useCallback(() => {
338
293
  setTimeout(() => {
@@ -553,7 +508,7 @@ var isBlockVisible = (viewType, block) => {
553
508
  var blockToMarkdown = (message, block, getObjectLabel) => {
554
509
  switch (block._tag) {
555
510
  case "text":
556
- if (message.sender.role === "user") return block.disposition === "synthetic" ? tag("synthetic", block.text, block) : tag("prompt", block.text, block);
511
+ if (message.sender.role === "user") return block.disposition === "synthetic" ? void 0 : tag("prompt", block.text, block);
557
512
  return block.text.trim() || void 0;
558
513
  case "reasoning": return tag("reasoning", block.reasoningText ?? block.redactedText ?? "", block);
559
514
  case "status": return tag("status", block.statusText, block);
@@ -605,6 +560,10 @@ var estimateRow = (message) => {
605
560
  let height = ROW_CHROME;
606
561
  for (const block of message.blocks) switch (block._tag) {
607
562
  case "text":
563
+ if (block.disposition === "synthetic") {
564
+ height += PANEL_HEIGHT;
565
+ break;
566
+ }
608
567
  for (const paragraph of block.text.split("\n\n")) height += Math.max(1, Math.ceil(paragraph.length / LINE_CHARS)) * LINE_HEIGHT;
609
568
  break;
610
569
  case "suggestion":
@@ -620,9 +579,42 @@ var estimateRow = (message) => {
620
579
  return height;
621
580
  };
622
581
  //#endregion
582
+ //#region src/components/MessageChrome/format-time.ts
583
+ /**
584
+ * Relative ("5 min ago", "yesterday") within two days, absolute date beyond that. The exact
585
+ * timestamp is always available via the `title` tooltip.
586
+ *
587
+ * Recent prompts are the ones a reader places by elapsed time; older ones by date.
588
+ */
589
+ var DAY_MS = 864e5;
590
+ /**
591
+ * Relative time for the message toolbars: `justNow` (caller-translated) under a minute, relative
592
+ * ("5 min ago", "yesterday") within two days, an absolute date beyond that. `now` defaults to the
593
+ * wall clock; an invalid timestamp renders as the empty string.
594
+ */
595
+ var formatTime = (created, { now = Date.now(), justNow } = {}) => {
596
+ const date = new Date(created);
597
+ if (Number.isNaN(date.getTime())) return "";
598
+ const elapsed = now - date.getTime();
599
+ if (elapsed < 0 || elapsed >= 2 * DAY_MS) return date.toLocaleDateString(void 0, {
600
+ month: "short",
601
+ day: "numeric"
602
+ });
603
+ const format = new Intl.RelativeTimeFormat(void 0, { numeric: "auto" });
604
+ if (elapsed < 6e4) {
605
+ if (justNow) return justNow;
606
+ return format.format(-Math.max(1, Math.round(elapsed / 1e3)), "second");
607
+ }
608
+ const minutes = Math.round(elapsed / 6e4);
609
+ if (minutes < 60) return format.format(-minutes, "minute");
610
+ const hours = Math.round(minutes / 60);
611
+ if (hours < 24) return format.format(-hours, "hour");
612
+ return format.format(-Math.round(hours / 24), "day");
613
+ };
614
+ //#endregion
623
615
  //#region src/components/MessageChrome/MessageChrome.tsx
624
616
  var MESSAGE_CHROME_NAME = "MessageChrome";
625
- var [MessageChromeProvider, useMessageChromeContext] = createContext(MESSAGE_CHROME_NAME);
617
+ var [MessageChromeProvider, useMessageChromeContext] = createContext(MESSAGE_CHROME_NAME, {});
626
618
  /** Copies the message's extracted text — the model's truth, not the DOM's partial render. */
627
619
  var CopyButton = ({ message }) => {
628
620
  const { t } = useTranslation(translationKey);
@@ -635,11 +627,23 @@ var CopyButton = ({ message }) => {
635
627
  onClick: () => void navigator.clipboard?.writeText(Message.extractText(message))
636
628
  });
637
629
  };
638
- var timeOf = (message) => /* @__PURE__ */ jsx("time", {
639
- dateTime: message.created,
640
- title: new Date(message.created).toLocaleString(),
641
- children: formatTime(message.created)
642
- });
630
+ var MessageId = ({ message }) => {
631
+ const { debug } = useMessageChromeContext("MessageId");
632
+ if (!debug) return null;
633
+ return /* @__PURE__ */ jsx("span", {
634
+ className: "font-mono text-subdued",
635
+ title: message.id,
636
+ children: message.id.slice(-8)
637
+ });
638
+ };
639
+ var Time = ({ message }) => {
640
+ const { t } = useTranslation(translationKey);
641
+ return /* @__PURE__ */ jsx("time", {
642
+ dateTime: message.created,
643
+ title: new Date(message.created).toLocaleString(),
644
+ children: formatTime(message.created, { justNow: t("just-now.label") })
645
+ });
646
+ };
643
647
  /**
644
648
  * The controls under the reader's own prompt: copy, rewind (edit-and-resend), and when it was
645
649
  * sent. Revealed on hover by the chrome, but never removed from flow — chrome that appears and
@@ -663,7 +667,8 @@ var PromptToolbar = ({ classNames, message }) => {
663
667
  "data-testid": "chat.rewind",
664
668
  onClick: () => onRewind(message.id)
665
669
  }),
666
- timeOf(message)
670
+ /* @__PURE__ */ jsx(Time, { message }),
671
+ /* @__PURE__ */ jsx(MessageId, { message })
667
672
  ]
668
673
  });
669
674
  };
@@ -673,14 +678,51 @@ var AssistantToolbar = ({ classNames, message }) => {
673
678
  return /* @__PURE__ */ jsxs("div", {
674
679
  role: "toolbar",
675
680
  className: mx$1("flex items-center gap-1 text-xs text-description", classNames),
676
- children: [/* @__PURE__ */ jsx(CopyButton, { message }), timeOf(message)]
681
+ children: [
682
+ /* @__PURE__ */ jsx(CopyButton, { message }),
683
+ /* @__PURE__ */ jsx(Time, { message }),
684
+ /* @__PURE__ */ jsx(MessageId, { message })
685
+ ]
677
686
  });
678
687
  };
679
688
  AssistantToolbar.displayName = "AssistantToolbar";
689
+ /**
690
+ * Synthetic context riding on a prompt (a document selection, an encoded event): system-generated
691
+ * input, so it renders as its own dimmed panel above the bubble — the bubble frames only the
692
+ * reader's words.
693
+ */
694
+ var SyntheticContext = ({ message }) => {
695
+ const { t } = useTranslation(translationKey);
696
+ const { showContext = true } = useMessageChromeContext("SyntheticContext");
697
+ const context = message.blocks.filter((block) => block._tag === "text" && block.disposition === "synthetic").map((block) => block.text).join("\n\n");
698
+ if (!showContext || !context.length) return null;
699
+ return /* @__PURE__ */ jsx("div", {
700
+ className: "pb-1 opacity-60",
701
+ "data-testid": "chat.context",
702
+ children: /* @__PURE__ */ jsx(TogglePanel.Root, { children: /* @__PURE__ */ jsxs(TogglePanel.Content, {
703
+ classNames: "border border-subdued-separator rounded-sm",
704
+ children: [/* @__PURE__ */ jsxs(TogglePanel.Header, {
705
+ classNames: "flex items-center gap-2 px-2 py-1 text-sm",
706
+ children: [/* @__PURE__ */ jsx("span", {
707
+ className: "grow text-description truncate",
708
+ children: t("context.label")
709
+ }), /* @__PURE__ */ jsx(Icon, {
710
+ icon: "ph--brain--regular",
711
+ size: 4,
712
+ classNames: "text-description"
713
+ })]
714
+ }), /* @__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",
716
+ children: context
717
+ }) })]
718
+ }) })
719
+ });
720
+ };
680
721
  /** 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";
722
+ var reveal = "pt-1 opacity-0 transition-opacity";
723
+ var revealOnHover = "group-hover/message:opacity-100";
682
724
  var Row = ({ children, classNames }) => /* @__PURE__ */ jsx("div", {
683
- className: mx$1("group relative px-2 py-2", classNames),
725
+ className: mx$1("group/message relative px-2 py-2", classNames),
684
726
  "data-testid": "feed.message",
685
727
  children
686
728
  });
@@ -694,25 +736,30 @@ var Row = ({ children, classNames }) => /* @__PURE__ */ jsx("div", {
694
736
  * bubble's edge.
695
737
  */
696
738
  var MessageChrome = ({ message, selected, children }) => {
697
- const prompt = message.sender.role === "user";
739
+ const { streaming } = useMessageChromeContext(MESSAGE_CHROME_NAME);
740
+ const prompt = isPrompt(message);
698
741
  return /* @__PURE__ */ jsx(Row, {
699
742
  classNames: mx$1(selected && "bg-hover-surface"),
700
743
  children: prompt ? /* @__PURE__ */ jsx("div", {
701
744
  className: "min-w-0 flex flex-col items-end",
702
745
  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
- })]
746
+ className: "max-w-[70%] min-w-0",
747
+ children: [
748
+ /* @__PURE__ */ jsx(SyntheticContext, { message }),
749
+ /* @__PURE__ */ jsx("div", {
750
+ className: "px-3 py-2 border-s-2 border-accent-bg rounded-sm bg-input-surface",
751
+ children
752
+ }),
753
+ /* @__PURE__ */ jsx(PromptToolbar, {
754
+ classNames: mx$1("justify-end", reveal, !streaming && revealOnHover),
755
+ message
756
+ })
757
+ ]
711
758
  })
712
759
  }) : /* @__PURE__ */ jsxs("div", {
713
760
  className: "min-w-0",
714
761
  children: [children, /* @__PURE__ */ jsx(AssistantToolbar, {
715
- classNames: reveal,
762
+ classNames: mx$1(reveal, !streaming && revealOnHover),
716
763
  message
717
764
  })]
718
765
  })
@@ -722,50 +769,36 @@ MessageChrome.displayName = MESSAGE_CHROME_NAME;
722
769
  //#endregion
723
770
  //#region src/components/ChatThread/ChatThread.tsx
724
771
  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
772
  /**
748
773
  * Headless root: the feed's `MessageList.Root` configured as an assistant thread — the view-typed
749
774
  * renderer, the widget registry, the prompt/answer chrome, and the chat behaviours (follow the
750
775
  * streaming tail; reserve room to bring the last prompt to the top). Composes with the feed's own
751
776
  * parts: `MessageList.Nav`, `useMessageList`, and the rails all work inside it.
752
777
  */
753
- var ChatThreadRoot = ({ children, model, viewType, registry, getObjectLabel, userHue, debug, onEvent, onRangeChange, controllerRef }) => {
778
+ var ChatThreadRoot = ({ children, model, viewType, registry, getObjectLabel, userHue, tailLines, debug, onEvent, onRangeChange, controllerRef }) => {
754
779
  const renderer = useMemo(() => createRenderer(viewType, { getObjectLabel }), [viewType, getObjectLabel]);
755
- const merged = useMemo(() => registry ? {
780
+ const merged = useMemo(() => viewType === "debug" ? void 0 : registry ? {
756
781
  ...assistantRegistry,
757
782
  ...registry
758
- } : assistantRegistry, [registry]);
783
+ } : assistantRegistry, [registry, viewType]);
759
784
  const handleRewind = useCallback((id) => onEvent?.({
760
785
  type: "rewind",
761
786
  id
762
787
  }), [onEvent]);
788
+ const [streaming, setStreaming] = useState(!!model.streamingId);
789
+ useEffect(() => {
790
+ setStreaming(!!model.streamingId);
791
+ return model.subscribe(() => setStreaming(!!model.streamingId));
792
+ }, [model]);
763
793
  return /* @__PURE__ */ jsx(ChatThreadProvider, {
764
794
  userHue,
765
795
  onEvent,
766
796
  children: /* @__PURE__ */ jsx(MessageChromeProvider, {
767
797
  onRewind: onEvent ? handleRewind : void 0,
768
- children: /* @__PURE__ */ jsxs(MessageList.Root, {
798
+ streaming,
799
+ showContext: viewType !== "summary",
800
+ debug,
801
+ children: /* @__PURE__ */ jsx(MessageList.Root, {
769
802
  model,
770
803
  renderer,
771
804
  registry: merged,
@@ -773,9 +806,10 @@ var ChatThreadRoot = ({ children, model, viewType, registry, getObjectLabel, use
773
806
  estimateSize: estimateRow,
774
807
  debug,
775
808
  stickyBottom: true,
776
- scrollPastEnd: true,
809
+ tailLines,
777
810
  onRangeChange,
778
- children: [controllerRef && /* @__PURE__ */ jsx(ControllerBridge, { controllerRef }), children]
811
+ controllerRef,
812
+ children
779
813
  })
780
814
  })
781
815
  });
@@ -787,7 +821,7 @@ var CHAT_THREAD_VIEWPORT_NAME = "ChatThread.Viewport";
787
821
  * `data-action="submit"` buttons; one delegated listener here turns those clicks into `submit`
788
822
  * events, which is what keeps the widgets renderable from the tag alone.
789
823
  */
790
- var ChatThreadViewport = ({ children, ...props }) => {
824
+ var ChatThreadViewport = ({ children, classNames, ...props }) => {
791
825
  const { userHue, onEvent } = useChatThreadContext(CHAT_THREAD_VIEWPORT_NAME);
792
826
  const handleClick = useCallback((event) => {
793
827
  const text = event.target.closest("[data-action=\"submit\"]")?.getAttribute("data-value");
@@ -807,6 +841,7 @@ var ChatThreadViewport = ({ children, ...props }) => {
807
841
  onClickCapture: handleClick,
808
842
  children: /* @__PURE__ */ jsx(MessageList.Viewport, {
809
843
  ...props,
844
+ classNames: ["grow min-h-0", classNames],
810
845
  children
811
846
  })
812
847
  });
@@ -817,23 +852,6 @@ var ChatThread = {
817
852
  Viewport: ChatThreadViewport
818
853
  };
819
854
  //#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 };
855
+ export { AssistantToolbar, ChatThread, ChatView, MessageChrome, MessageChromeProvider, PromptToolbar, assistantRegistry, createRenderer, estimateRow, translationKey, translations };
838
856
 
839
857
  //# sourceMappingURL=index.mjs.map