@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.
- package/dist/lib/index.mjs +891 -353
- package/dist/lib/index.mjs.map +1 -1
- package/dist/lib/testing.mjs +22 -3
- package/dist/lib/testing.mjs.map +1 -1
- package/dist/lib/translations.mjs +19 -2
- package/dist/lib/translations.mjs.map +1 -1
- package/dist/lib/types.mjs +34 -1
- package/dist/lib/types.mjs.map +1 -1
- package/dist/types/src/components/ChatThread/ChatThread.d.ts +21 -11
- package/dist/types/src/components/ChatThread/ChatThread.d.ts.map +1 -1
- package/dist/types/src/components/ChatThread/ChatThread.stories.d.ts +21 -1
- package/dist/types/src/components/ChatThread/ChatThread.stories.d.ts.map +1 -1
- package/dist/types/src/components/ChatThread/index.d.ts +1 -1
- package/dist/types/src/components/ChatThread/index.d.ts.map +1 -1
- package/dist/types/src/components/MessageChrome/MessageChrome.d.ts +14 -11
- package/dist/types/src/components/MessageChrome/MessageChrome.d.ts.map +1 -1
- package/dist/types/src/components/MessageChrome/MessageChrome.stories.d.ts +27 -2
- package/dist/types/src/components/MessageChrome/MessageChrome.stories.d.ts.map +1 -1
- package/dist/types/src/components/MessageChrome/format-time.d.ts.map +1 -1
- package/dist/types/src/components/MessageChrome/index.d.ts +1 -1
- package/dist/types/src/components/MessageChrome/index.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +2 -2
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +5 -5
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/registry.d.ts +7 -1
- package/dist/types/src/registry.d.ts.map +1 -1
- package/dist/types/src/registry.stories.d.ts +63 -1
- package/dist/types/src/registry.stories.d.ts.map +1 -1
- package/dist/types/src/renderer.d.ts +18 -5
- package/dist/types/src/renderer.d.ts.map +1 -1
- package/dist/types/src/renderer.test.d.ts +2 -0
- package/dist/types/src/renderer.test.d.ts.map +1 -0
- package/dist/types/src/testing/index.d.ts +1 -1
- package/dist/types/src/testing/index.d.ts.map +1 -1
- package/dist/types/src/testing/test-generator.d.ts +6 -0
- package/dist/types/src/testing/test-generator.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +18 -1
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +37 -0
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/src/widgets/DeliveryWidget.d.ts +20 -0
- package/dist/types/src/widgets/DeliveryWidget.d.ts.map +1 -0
- package/dist/types/src/widgets/FallbackWidget.d.ts +2 -2
- package/dist/types/src/widgets/FallbackWidget.d.ts.map +1 -1
- package/dist/types/src/widgets/ReasoningWidget.d.ts.map +1 -1
- package/dist/types/src/widgets/ReferenceWidget.d.ts +2 -2
- package/dist/types/src/widgets/RequestWidget.d.ts +12 -0
- package/dist/types/src/widgets/RequestWidget.d.ts.map +1 -0
- package/dist/types/src/widgets/SelectWidget.d.ts.map +1 -1
- package/dist/types/src/widgets/SuggestionWidget.d.ts.map +1 -1
- package/dist/types/src/widgets/SummaryWidget.d.ts +2 -2
- package/dist/types/src/widgets/SummaryWidget.d.ts.map +1 -1
- package/dist/types/src/widgets/SyntheticWidget.d.ts +11 -0
- package/dist/types/src/widgets/SyntheticWidget.d.ts.map +1 -0
- package/dist/types/src/widgets/ToolWidget.d.ts +3 -3
- package/dist/types/src/widgets/ToolWidget.d.ts.map +1 -1
- package/dist/types/src/widgets/WidgetPanel.d.ts +20 -0
- package/dist/types/src/widgets/WidgetPanel.d.ts.map +1 -0
- package/dist/types/src/widgets/index.d.ts +12 -9
- package/dist/types/src/widgets/index.d.ts.map +1 -1
- package/dist/types/src/widgets/submit-button.d.ts +7 -0
- package/dist/types/src/widgets/submit-button.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +22 -22
- package/src/components/ChatThread/ChatThread.stories.tsx +113 -17
- package/src/components/ChatThread/ChatThread.tsx +137 -20
- package/src/components/ChatThread/index.ts +1 -1
- package/src/components/MessageChrome/MessageChrome.stories.tsx +50 -11
- package/src/components/MessageChrome/MessageChrome.tsx +120 -44
- package/src/components/MessageChrome/format-time.ts +6 -1
- package/src/components/MessageChrome/index.ts +1 -1
- package/src/components/index.ts +2 -2
- package/src/index.ts +5 -5
- package/src/registry.stories.tsx +479 -26
- package/src/registry.tsx +65 -35
- package/src/renderer.test.ts +235 -0
- package/src/renderer.ts +182 -34
- package/src/testing/index.ts +1 -1
- package/src/testing/test-generator.test.ts +3 -5
- package/src/testing/test-generator.ts +28 -11
- package/src/translations.ts +20 -3
- package/src/types.ts +46 -3
- package/src/widgets/DeliveryWidget.ts +75 -0
- package/src/widgets/FallbackWidget.tsx +14 -16
- package/src/widgets/ReasoningWidget.ts +10 -5
- package/src/widgets/ReferenceWidget.ts +3 -3
- package/src/widgets/RequestWidget.tsx +69 -0
- package/src/widgets/SelectWidget.ts +9 -11
- package/src/widgets/SuggestionWidget.ts +7 -17
- package/src/widgets/SummaryWidget.tsx +10 -14
- package/src/widgets/SyntheticWidget.tsx +57 -0
- package/src/widgets/ToolWidget.tsx +365 -148
- package/src/widgets/WidgetPanel.tsx +74 -0
- package/src/widgets/index.ts +12 -9
- package/src/widgets/submit-button.ts +19 -0
package/dist/lib/index.mjs
CHANGED
|
@@ -1,31 +1,131 @@
|
|
|
1
|
+
import { ChatView, DeliveryAnnotation, DeliveryStatus, getDelivery, isUnread } from "./types.mjs";
|
|
1
2
|
import { translationKey, translations } from "./translations.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
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 {
|
|
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
|
|
15
|
-
import
|
|
16
|
-
//#region src/widgets/
|
|
17
|
-
var
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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-
|
|
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
|
-
|
|
105
|
-
constructor(text,
|
|
204
|
+
eid;
|
|
205
|
+
constructor(text, eid) {
|
|
106
206
|
super();
|
|
107
207
|
this.text = text;
|
|
108
|
-
this.
|
|
208
|
+
this.eid = eid;
|
|
109
209
|
}
|
|
110
210
|
eq(other) {
|
|
111
|
-
return this.
|
|
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
|
|
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-
|
|
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(
|
|
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(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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/
|
|
361
|
+
//#region src/widgets/SyntheticWidget.tsx
|
|
225
362
|
/**
|
|
226
|
-
* A
|
|
227
|
-
*
|
|
228
|
-
*
|
|
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
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
return
|
|
235
|
-
}, [
|
|
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 (!
|
|
298
|
-
return /* @__PURE__ */ jsx(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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`)
|
|
464
|
-
*
|
|
465
|
-
*
|
|
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
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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"
|
|
481
|
-
|
|
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)
|
|
442
|
+
if (!rendered) continue;
|
|
443
|
+
flushRun();
|
|
444
|
+
segments.push(rendered);
|
|
487
445
|
}
|
|
488
|
-
|
|
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
|
|
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 (
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
|
|
530
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
627
|
-
|
|
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-
|
|
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, {
|
|
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-
|
|
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(
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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:
|
|
712
|
-
|
|
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-
|
|
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
|
-
/**
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
}
|
|
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
|
|
828
|
-
if (
|
|
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: "
|
|
833
|
-
|
|
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: [
|
|
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
|