agentic-ui-kit 0.2.1 → 0.3.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/README.md +109 -2
- package/dist/core.js +96 -1
- package/dist/index.js +1413 -286
- package/dist/styles.css +1 -1
- package/dist/tailwind.css +30 -2
- package/dist/types/core/src/index.d.ts +8 -1
- package/dist/types/core/src/partial-json.d.ts +8 -0
- package/dist/types/core/src/types.d.ts +5 -0
- package/dist/types/core/src/ui-markup.d.ts +92 -0
- package/dist/types/core/src/ui-tree.d.ts +110 -0
- package/dist/types/core/src/visualize.d.ts +60 -0
- package/dist/types/react/src/components/chat-notice.d.ts +11 -0
- package/dist/types/react/src/components/markdown.d.ts +19 -1
- package/dist/types/react/src/components/message-list.d.ts +5 -1
- package/dist/types/react/src/components/messages.d.ts +3 -2
- package/dist/types/react/src/components/tools.d.ts +20 -7
- package/dist/types/react/src/components/ui-tree.d.ts +87 -0
- package/dist/types/react/src/components/widget-frame.d.ts +97 -0
- package/dist/types/react/src/index.d.ts +15 -3
- package/dist/types/react/src/primitives/chat-theme.d.ts +13 -1
- package/dist/types/react/src/primitives/icons.d.ts +95 -0
- package/dist/types/react/src/primitives/labels.d.ts +193 -0
- package/dist/types/react/src/views/chat-transcript.d.ts +27 -0
- package/dist/types/react/src/views/chat-view.d.ts +15 -2
- package/dist/ui-markup-6xbP-myu.js +1082 -0
- package/package.json +1 -1
- package/dist/sse-BA_ebMcH.js +0 -408
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { A as salvageJson, C as awaitingToolCalls, O as settleToolCall, S as applyEvent, b as toBlocks, d as isStateRef, f as matchesSchema, g as uiActionSchema, i as parseUiMarkup, v as readEventStream, x as activityTokens } from "./ui-markup-6xbP-myu.js";
|
|
3
3
|
import { clsx } from "clsx";
|
|
4
4
|
import { Autocomplete, Button as Button$1, Dialog, Disclosure as Disclosure$1, DisclosurePanel, Focusable, Header, Heading, Input, Keyboard, Link, Menu as Menu$1, MenuItem as MenuItem$1, MenuSection as MenuSection$1, MenuTrigger, MenuTrigger as MenuTrigger$1, Modal as Modal$1, ModalOverlay, Popover as Popover$1, SearchField, Separator, SubmenuTrigger, Text, Tooltip as Tooltip$1, TooltipTrigger, useFilter } from "react-aria-components";
|
|
5
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
-
import { AlertTriangle, ArrowDown, ArrowUp, Check, ChevronDown, ChevronRight, ChevronsUpDown, Copy, FileText, Folder, FolderOpen, FolderPlus, Globe, MoreHorizontal, PanelLeft, Pencil, Pin, PinOff, Plus, RefreshCw, Share, ShieldQuestion, Square, ThumbsDown, ThumbsUp, Trash2, Volume2, X } from "lucide-react";
|
|
7
|
-
import { Fragment as Fragment$1, createContext, createElement, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
6
|
+
import { AlertTriangle, ArrowDown, ArrowUp, Check, ChevronDown, ChevronRight, ChevronsUpDown, Copy, FileText, Folder, FolderOpen, FolderPlus, Globe, Info, MoreHorizontal, PanelLeft, Pencil, Pin, PinOff, Plus, RefreshCw, Share, ShieldQuestion, Square, ThumbsDown, ThumbsUp, Trash2, Volume2, X } from "lucide-react";
|
|
7
|
+
import { Component, Fragment as Fragment$1, createContext, createElement, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
8
8
|
import { UNSAFE_PortalProvider } from "react-aria";
|
|
9
9
|
import { createMathPlugin } from "@streamdown/math";
|
|
10
10
|
import { Streamdown, defaultRehypePlugins } from "streamdown";
|
|
@@ -16,7 +16,7 @@ function Tooltip({ content, children, placement = "top", delay = 600 }) {
|
|
|
16
16
|
children: [children, /* @__PURE__ */ jsx(Tooltip$1, {
|
|
17
17
|
placement,
|
|
18
18
|
offset: 6,
|
|
19
|
-
className: "rounded-chat-sm bg-chat-raised px-2 py-1 font-chat-sans text-
|
|
19
|
+
className: "rounded-chat-sm bg-chat-raised px-2 py-1 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-fg shadow-chat-popover motion-safe:data-entering:animate-chat-in motion-safe:data-exiting:animate-chat-out",
|
|
20
20
|
children: content
|
|
21
21
|
})]
|
|
22
22
|
});
|
|
@@ -29,11 +29,11 @@ const variants = {
|
|
|
29
29
|
/** Filled with the surface color, e.g. the selected Chat/Cowork toggle. */
|
|
30
30
|
subtle: "bg-chat-surface text-chat-fg data-hovered:bg-chat-hover data-pressed:bg-chat-selected",
|
|
31
31
|
/** The send button. */
|
|
32
|
-
primary: "bg-chat-accent text-
|
|
32
|
+
primary: "bg-chat-accent text-chat-accent-fg data-hovered:brightness-110 data-pressed:brightness-95"
|
|
33
33
|
};
|
|
34
34
|
const sizes = {
|
|
35
|
-
sm: "h-7 gap-1.5 px-2 text-
|
|
36
|
-
md: "h-8 gap-2 px-2.5 text-
|
|
35
|
+
sm: "h-7 gap-1.5 px-2 text-(length:--chat-ui-font-size) leading-5 [@media(pointer:coarse)]:h-9",
|
|
36
|
+
md: "h-8 gap-2 px-2.5 text-(length:--chat-ui-font-size) leading-5 [@media(pointer:coarse)]:h-10"
|
|
37
37
|
};
|
|
38
38
|
function Button({ variant = "ghost", size = "md", className, ...props }) {
|
|
39
39
|
return /* @__PURE__ */ jsx(Button$1, {
|
|
@@ -61,7 +61,7 @@ const tones = {
|
|
|
61
61
|
};
|
|
62
62
|
function Badge({ children, tone = "outline", icon, className }) {
|
|
63
63
|
return /* @__PURE__ */ jsxs("span", {
|
|
64
|
-
className: clsx("inline-flex items-center gap-1 rounded-full px-2 py-px font-chat-sans text-
|
|
64
|
+
className: clsx("inline-flex items-center gap-1 rounded-full px-2 py-px font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 whitespace-nowrap [&_svg]:size-3", tones[tone], className),
|
|
65
65
|
children: [icon, children]
|
|
66
66
|
});
|
|
67
67
|
}
|
|
@@ -75,9 +75,77 @@ function Caret({ className }) {
|
|
|
75
75
|
children: /* @__PURE__ */ jsx("path", { d: "M2 1.2 6.2 4 2 6.8z" })
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
|
+
// src/primitives/icons.tsx
|
|
79
|
+
/** Lucide icons, plus the claude.ai caret. Unsized: the places that show them size them with CSS. */
|
|
80
|
+
const defaultIcons = {
|
|
81
|
+
copy: /* @__PURE__ */ jsx(Copy, {}),
|
|
82
|
+
copied: /* @__PURE__ */ jsx(Check, {}),
|
|
83
|
+
retry: /* @__PURE__ */ jsx(RefreshCw, {}),
|
|
84
|
+
edit: /* @__PURE__ */ jsx(Pencil, {}),
|
|
85
|
+
speak: /* @__PURE__ */ jsx(Volume2, {}),
|
|
86
|
+
thumbsUp: /* @__PURE__ */ jsx(ThumbsUp, {}),
|
|
87
|
+
thumbsDown: /* @__PURE__ */ jsx(ThumbsDown, {}),
|
|
88
|
+
send: /* @__PURE__ */ jsx(ArrowUp, {}),
|
|
89
|
+
stop: /* @__PURE__ */ jsx(Square, {}),
|
|
90
|
+
attach: /* @__PURE__ */ jsx(Plus, {}),
|
|
91
|
+
scrollToBottom: /* @__PURE__ */ jsx(ArrowDown, {}),
|
|
92
|
+
tokens: /* @__PURE__ */ jsx(ArrowDown, {}),
|
|
93
|
+
sidebar: /* @__PURE__ */ jsx(PanelLeft, {}),
|
|
94
|
+
switcher: /* @__PURE__ */ jsx(ChevronsUpDown, {}),
|
|
95
|
+
dropdown: /* @__PURE__ */ jsx(ChevronDown, {}),
|
|
96
|
+
expand: /* @__PURE__ */ jsx(ChevronRight, {}),
|
|
97
|
+
caret: /* @__PURE__ */ jsx(Caret, {}),
|
|
98
|
+
submenu: /* @__PURE__ */ jsx(ChevronRight, {}),
|
|
99
|
+
check: /* @__PURE__ */ jsx(Check, {}),
|
|
100
|
+
info: /* @__PURE__ */ jsx(Info, {}),
|
|
101
|
+
warning: /* @__PURE__ */ jsx(AlertTriangle, {}),
|
|
102
|
+
approval: /* @__PURE__ */ jsx(ShieldQuestion, {}),
|
|
103
|
+
web: /* @__PURE__ */ jsx(Globe, {}),
|
|
104
|
+
pin: /* @__PURE__ */ jsx(Pin, {}),
|
|
105
|
+
unpin: /* @__PURE__ */ jsx(PinOff, {}),
|
|
106
|
+
rename: /* @__PURE__ */ jsx(Pencil, {}),
|
|
107
|
+
addToProject: /* @__PURE__ */ jsx(FolderPlus, {}),
|
|
108
|
+
add: /* @__PURE__ */ jsx(Plus, {}),
|
|
109
|
+
share: /* @__PURE__ */ jsx(Share, {}),
|
|
110
|
+
delete: /* @__PURE__ */ jsx(Trash2, {}),
|
|
111
|
+
more: /* @__PURE__ */ jsx(MoreHorizontal, {}),
|
|
112
|
+
folder: /* @__PURE__ */ jsx(Folder, {}),
|
|
113
|
+
folderOpen: /* @__PURE__ */ jsx(FolderOpen, {}),
|
|
114
|
+
file: /* @__PURE__ */ jsx(FileText, {}),
|
|
115
|
+
remove: /* @__PURE__ */ jsx(X, {})
|
|
116
|
+
};
|
|
117
|
+
const IconsContext = createContext(defaultIcons);
|
|
118
|
+
/** Merges `icons` over the icons of the enclosing provider (or the defaults). */
|
|
119
|
+
function IconsProvider({ icons, children }) {
|
|
120
|
+
const parent = useIcons();
|
|
121
|
+
const merged = useMemo(() => icons ? {
|
|
122
|
+
...parent,
|
|
123
|
+
...icons
|
|
124
|
+
} : parent, [parent, icons]);
|
|
125
|
+
return /* @__PURE__ */ jsx(IconsContext.Provider, {
|
|
126
|
+
value: merged,
|
|
127
|
+
children
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/** The active icons: the enclosing `ChatTheme`'s `icons` over the defaults. */
|
|
131
|
+
function useIcons() {
|
|
132
|
+
return useContext(IconsContext);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Sizes and colors an icon where no button does it already. Pass the size as `[&_svg]:size-4` so it
|
|
136
|
+
* reaches whatever SVG the host supplies.
|
|
137
|
+
*/
|
|
138
|
+
function IconSlot({ children, className }) {
|
|
139
|
+
return /* @__PURE__ */ jsx("span", {
|
|
140
|
+
"aria-hidden": true,
|
|
141
|
+
className: clsx("inline-flex shrink-0 items-center justify-center", className),
|
|
142
|
+
children
|
|
143
|
+
});
|
|
144
|
+
}
|
|
78
145
|
// src/primitives/disclosure.tsx
|
|
79
146
|
/** A row that expands to show more, like "Recalled memory ▸". */
|
|
80
147
|
function Disclosure({ summary, children, className, summaryClassName, panelClassName, indicator = "caret", trailing, ...props }) {
|
|
148
|
+
const icons = useIcons();
|
|
81
149
|
return /* @__PURE__ */ jsxs(Disclosure$1, {
|
|
82
150
|
...props,
|
|
83
151
|
...className !== void 0 && { className },
|
|
@@ -87,15 +155,18 @@ function Disclosure({ summary, children, className, summaryClassName, panelClass
|
|
|
87
155
|
slot: "trigger",
|
|
88
156
|
className: clsx("group/trigger flex max-w-full cursor-pointer items-center gap-1.5 rounded-chat-sm text-left text-chat-muted outline-none", "data-hovered:text-chat-fg data-focus-visible:ring-2 data-focus-visible:ring-chat-focus", summaryClassName),
|
|
89
157
|
children: [
|
|
90
|
-
indicator === "chevron" && /* @__PURE__ */ jsx(
|
|
91
|
-
"aria-
|
|
92
|
-
|
|
158
|
+
indicator === "chevron" && /* @__PURE__ */ jsx(IconSlot, {
|
|
159
|
+
className: "transition-transform group-aria-expanded/trigger:rotate-90 [&_svg]:size-3.5",
|
|
160
|
+
children: icons.expand
|
|
93
161
|
}),
|
|
94
162
|
/* @__PURE__ */ jsx("span", {
|
|
95
163
|
className: "min-w-0 truncate",
|
|
96
164
|
children: summary
|
|
97
165
|
}),
|
|
98
|
-
indicator === "caret" && /* @__PURE__ */ jsx(
|
|
166
|
+
indicator === "caret" && /* @__PURE__ */ jsx(IconSlot, {
|
|
167
|
+
className: "transition-transform duration-150 group-aria-expanded/trigger:rotate-90",
|
|
168
|
+
children: icons.caret
|
|
169
|
+
}),
|
|
99
170
|
trailing && /* @__PURE__ */ jsx("span", {
|
|
100
171
|
className: "ml-auto shrink-0 pl-2",
|
|
101
172
|
children: trailing
|
|
@@ -122,7 +193,7 @@ function MenuSearch({ placeholder, children, value, onChange }) {
|
|
|
122
193
|
className: "border-b border-chat-popover-border",
|
|
123
194
|
children: /* @__PURE__ */ jsx(Input, {
|
|
124
195
|
placeholder,
|
|
125
|
-
className: "w-full bg-transparent px-3.5 py-2.5 text-
|
|
196
|
+
className: "w-full bg-transparent px-3.5 py-2.5 text-(length:--chat-ui-font-size) leading-5 text-chat-fg outline-none placeholder:text-chat-subtle"
|
|
126
197
|
})
|
|
127
198
|
}), children]
|
|
128
199
|
});
|
|
@@ -142,11 +213,12 @@ function Menu({ className, ...props }) {
|
|
|
142
213
|
});
|
|
143
214
|
}
|
|
144
215
|
function MenuItem({ children, icon, description, detail, shortcut, tone = "default", className, ...props }) {
|
|
216
|
+
const icons = useIcons();
|
|
145
217
|
const textValue = props.textValue ?? (typeof children === "string" ? children : void 0);
|
|
146
218
|
return /* @__PURE__ */ jsx(MenuItem$1, {
|
|
147
219
|
...props,
|
|
148
220
|
...textValue !== void 0 && { textValue },
|
|
149
|
-
className: clsx("group flex min-h-7 cursor-pointer items-center gap-2.5 rounded-chat-sm px-2 py-1 text-
|
|
221
|
+
className: clsx("group flex min-h-7 cursor-pointer items-center gap-2.5 rounded-chat-sm px-2 py-1 text-(length:--chat-ui-font-size) leading-5 text-chat-fg outline-none select-none", "data-focused:bg-chat-popover-hover data-open:bg-chat-popover-hover data-disabled:opacity-40", tone === "danger" && "text-chat-danger", className),
|
|
150
222
|
children: ({ isSelected, hasSubmenu, selectionMode }) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
151
223
|
icon && /* @__PURE__ */ jsx("span", {
|
|
152
224
|
className: clsx("flex size-4 shrink-0 items-center [&_svg]:size-4", tone === "danger" ? "text-chat-danger" : "text-chat-fg"),
|
|
@@ -160,25 +232,25 @@ function MenuItem({ children, icon, description, detail, shortcut, tone = "defau
|
|
|
160
232
|
children
|
|
161
233
|
}), description && /* @__PURE__ */ jsx(Text, {
|
|
162
234
|
slot: "description",
|
|
163
|
-
className: "text-
|
|
235
|
+
className: "text-(length:--chat-ui-font-size-sm) leading-4 text-chat-muted",
|
|
164
236
|
children: description
|
|
165
237
|
})]
|
|
166
238
|
}),
|
|
167
239
|
detail && /* @__PURE__ */ jsx("span", {
|
|
168
|
-
className: "shrink-0 text-
|
|
240
|
+
className: "shrink-0 text-(length:--chat-ui-font-size) leading-5 text-chat-muted",
|
|
169
241
|
children: detail
|
|
170
242
|
}),
|
|
171
243
|
shortcut && /* @__PURE__ */ jsx(Keyboard, {
|
|
172
|
-
className: "shrink-0 font-chat-sans text-
|
|
244
|
+
className: "shrink-0 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle",
|
|
173
245
|
children: shortcut
|
|
174
246
|
}),
|
|
175
|
-
selectionMode !== "none" && isSelected && /* @__PURE__ */ jsx(
|
|
176
|
-
"
|
|
177
|
-
|
|
247
|
+
selectionMode !== "none" && isSelected && /* @__PURE__ */ jsx(IconSlot, {
|
|
248
|
+
className: "text-chat-check [&_svg]:size-4",
|
|
249
|
+
children: icons.check
|
|
178
250
|
}),
|
|
179
|
-
hasSubmenu && /* @__PURE__ */ jsx(
|
|
180
|
-
"
|
|
181
|
-
|
|
251
|
+
hasSubmenu && /* @__PURE__ */ jsx(IconSlot, {
|
|
252
|
+
className: "text-chat-muted [&_svg]:size-4",
|
|
253
|
+
children: icons.submenu
|
|
182
254
|
})
|
|
183
255
|
] })
|
|
184
256
|
});
|
|
@@ -188,7 +260,7 @@ function MenuSection({ title, children, ...selection }) {
|
|
|
188
260
|
...selection,
|
|
189
261
|
className: "[&:not(:first-child)]:mt-1",
|
|
190
262
|
children: [title && /* @__PURE__ */ jsx(Header, {
|
|
191
|
-
className: "px-2 pt-1.5 pb-1 text-
|
|
263
|
+
className: "px-2 pt-1.5 pb-1 text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle",
|
|
192
264
|
children: title
|
|
193
265
|
}), children]
|
|
194
266
|
});
|
|
@@ -199,7 +271,7 @@ function MenuSeparator() {
|
|
|
199
271
|
/** Explanatory text inside a menu, above or below the items. Not focusable. */
|
|
200
272
|
function MenuNote({ children, className }) {
|
|
201
273
|
return /* @__PURE__ */ jsx("div", {
|
|
202
|
-
className: clsx("px-2 py-1.5 text-
|
|
274
|
+
className: clsx("px-2 py-1.5 text-(length:--chat-ui-font-size) leading-snug text-chat-muted", className),
|
|
203
275
|
children
|
|
204
276
|
});
|
|
205
277
|
}
|
|
@@ -260,7 +332,7 @@ function Modal({ isOpen, onOpenChange, title, children, className, ...props }) {
|
|
|
260
332
|
isOpen,
|
|
261
333
|
onOpenChange,
|
|
262
334
|
isDismissable: true,
|
|
263
|
-
className: "fixed inset-0 z-50 flex items-center justify-center bg-
|
|
335
|
+
className: "fixed inset-0 z-50 flex items-center justify-center bg-(--chat-backdrop) p-4 data-entering:animate-chat-fade-in data-exiting:animate-chat-fade-out",
|
|
264
336
|
children: /* @__PURE__ */ jsx(Modal$1, {
|
|
265
337
|
className: clsx("flex max-h-[85dvh] w-full max-w-2xl flex-col rounded-chat-lg border border-chat-popover-border bg-chat-popover font-chat-sans text-chat-fg shadow-chat-popover", "data-entering:animate-chat-in data-exiting:animate-chat-out", className),
|
|
266
338
|
children: /* @__PURE__ */ jsxs(Dialog, {
|
|
@@ -281,7 +353,7 @@ function Drawer({ isOpen, onOpenChange, side = "left", children, className, ...p
|
|
|
281
353
|
isOpen,
|
|
282
354
|
onOpenChange,
|
|
283
355
|
isDismissable: true,
|
|
284
|
-
className: "fixed inset-0 z-40 bg-
|
|
356
|
+
className: "fixed inset-0 z-40 bg-(--chat-backdrop) data-entering:animate-chat-fade-in data-exiting:animate-chat-fade-out",
|
|
285
357
|
children: /* @__PURE__ */ jsx(Modal$1, {
|
|
286
358
|
className: clsx("fixed inset-y-0 w-[min(20rem,85vw)] bg-chat-sidebar text-chat-fg shadow-chat-popover outline-none", side === "left" ? "left-0 data-entering:animate-chat-drawer-in data-exiting:animate-chat-drawer-out" : "right-0 data-entering:animate-chat-drawer-in-right data-exiting:animate-chat-drawer-out-right", className),
|
|
287
359
|
children: /* @__PURE__ */ jsx(Dialog, {
|
|
@@ -303,7 +375,7 @@ function Panel({ title, actions, footer, children, variant = "card", className,
|
|
|
303
375
|
className: clsx("flex min-h-0 min-w-0 flex-col overflow-hidden bg-chat-bg font-chat-sans text-chat-fg", variant === "card" && "rounded-chat-md border border-chat-border", className),
|
|
304
376
|
children: [
|
|
305
377
|
(title || actions) && /* @__PURE__ */ jsxs("header", {
|
|
306
|
-
className: "flex h-10 shrink-0 items-center gap-2 border-b border-chat-divider px-3 text-
|
|
378
|
+
className: "flex h-10 shrink-0 items-center gap-2 border-b border-chat-divider px-3 text-(length:--chat-ui-font-size) leading-5",
|
|
307
379
|
children: [/* @__PURE__ */ jsx("div", {
|
|
308
380
|
className: "min-w-0 flex-1 truncate font-medium",
|
|
309
381
|
children: title
|
|
@@ -460,10 +532,10 @@ function CodeBlock({ code, language, label = language, variant = "plain", classN
|
|
|
460
532
|
style: themeBackground ? result.vars : void 0,
|
|
461
533
|
className: clsx(variant === "boxed" && "my-4 rounded-chat-md border border-chat-code-border bg-chat-code-bg px-4 py-3.5", className),
|
|
462
534
|
children: [label && /* @__PURE__ */ jsx("div", {
|
|
463
|
-
className: "mb-3 font-chat-sans text-
|
|
535
|
+
className: "mb-3 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle",
|
|
464
536
|
children: label
|
|
465
537
|
}), /* @__PURE__ */ jsx("pre", {
|
|
466
|
-
className: clsx("overflow-auto font-chat-mono text-
|
|
538
|
+
className: clsx("overflow-auto font-chat-mono text-(length:--chat-code-font-size) leading-[1.75] whitespace-pre text-chat-fg", variant === "plain" && "max-h-56", "[scrollbar-color:var(--chat-border-strong)_transparent] [scrollbar-width:thin]"),
|
|
467
539
|
children: /* @__PURE__ */ jsxs("code", { children: [highlighted ? highlighted.map((line, i) => /* @__PURE__ */ jsxs(Fragment$1, { children: [i > 0 && "\n", line.map((token, j) => /* @__PURE__ */ jsx("span", {
|
|
468
540
|
className: "chat-code-token",
|
|
469
541
|
style: token.htmlStyle,
|
|
@@ -472,13 +544,130 @@ function CodeBlock({ code, language, label = language, variant = "plain", classN
|
|
|
472
544
|
})]
|
|
473
545
|
});
|
|
474
546
|
}
|
|
547
|
+
// src/primitives/labels.tsx
|
|
548
|
+
const plural = (count, one, many) => count === 1 ? one : many(count);
|
|
549
|
+
/** The English strings. */
|
|
550
|
+
const defaultLabels = {
|
|
551
|
+
thinking: "Thinking…",
|
|
552
|
+
thought: "Thought",
|
|
553
|
+
thoughtFor: (duration) => `Thought for ${duration}`,
|
|
554
|
+
waitingForYou: " · waiting for you",
|
|
555
|
+
usedTool: (name) => `used ${name}`,
|
|
556
|
+
joinSummaries: (phrases) => {
|
|
557
|
+
const text = phrases.join(", ");
|
|
558
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
559
|
+
},
|
|
560
|
+
duration: (seconds) => seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`,
|
|
561
|
+
ranCommandLabel: "Ran a command",
|
|
562
|
+
ranCommands: (count) => plural(count, "ran a command", (n) => `ran ${n} commands`),
|
|
563
|
+
searchedWebLabel: "Searched the web",
|
|
564
|
+
readPageLabel: "Read page",
|
|
565
|
+
readPages: (count) => plural(count, "read a page", (n) => `read ${n} pages`),
|
|
566
|
+
searchedWeb: (count) => plural(count, "searched the web", (n) => `searched the web ${n} times`),
|
|
567
|
+
recalledMemoryLabel: "Recalled memory",
|
|
568
|
+
recalledMemory: () => "recalled memory",
|
|
569
|
+
output: "Output",
|
|
570
|
+
noOutput: "(no output)",
|
|
571
|
+
noResults: "No results",
|
|
572
|
+
reasoningHidden: "The model's reasoning is not shown.",
|
|
573
|
+
allowTool: (name) => `Allow ${name}?`,
|
|
574
|
+
allowAction: (label) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
575
|
+
"Allow ",
|
|
576
|
+
label,
|
|
577
|
+
"?"
|
|
578
|
+
] }),
|
|
579
|
+
allow: "Allow",
|
|
580
|
+
deny: "Deny",
|
|
581
|
+
sidebar: "Sidebar",
|
|
582
|
+
closeSidebar: "Close sidebar",
|
|
583
|
+
openSidebar: "Open sidebar",
|
|
584
|
+
expandSidebar: "Expand sidebar",
|
|
585
|
+
collapseSidebar: "Collapse sidebar",
|
|
586
|
+
context: "Context",
|
|
587
|
+
contextUsed: "Context used",
|
|
588
|
+
percentUsed: (percent) => `${percent}% used`,
|
|
589
|
+
freeSpace: "Free space",
|
|
590
|
+
tokenCount: (count) => `${count} tokens`,
|
|
591
|
+
replyPlaceholder: "Reply…",
|
|
592
|
+
replyDirectly: "Or reply directly…",
|
|
593
|
+
message: "Message",
|
|
594
|
+
stop: "Stop",
|
|
595
|
+
send: "Send",
|
|
596
|
+
addFiles: "Add files and more",
|
|
597
|
+
attachments: "Attachments",
|
|
598
|
+
editPastedText: "Edit pasted text",
|
|
599
|
+
viewPastedText: "View pasted text",
|
|
600
|
+
removeAttachment: (name) => `Remove ${name}`,
|
|
601
|
+
pastedText: "Pasted text",
|
|
602
|
+
pasted: "Pasted",
|
|
603
|
+
failed: "Failed",
|
|
604
|
+
file: "File",
|
|
605
|
+
uploadFailed: "Upload failed",
|
|
606
|
+
cancel: "Cancel",
|
|
607
|
+
close: "Close",
|
|
608
|
+
save: "Save",
|
|
609
|
+
chats: "Chats",
|
|
610
|
+
running: "Running",
|
|
611
|
+
unread: "Unread",
|
|
612
|
+
moreOptionsFor: (title) => `More options for ${title}`,
|
|
613
|
+
new: "New",
|
|
614
|
+
viewAll: "View all",
|
|
615
|
+
chatActions: "Chat actions",
|
|
616
|
+
pin: "Pin",
|
|
617
|
+
unpin: "Unpin",
|
|
618
|
+
rename: "Rename",
|
|
619
|
+
addToProject: "Add to project",
|
|
620
|
+
searchProjects: "Search or create a project",
|
|
621
|
+
projects: "Projects",
|
|
622
|
+
newProject: "New project",
|
|
623
|
+
shareChat: "Share chat",
|
|
624
|
+
delete: "Delete",
|
|
625
|
+
justNow: "just now",
|
|
626
|
+
copy: "Copy",
|
|
627
|
+
copied: "Copied",
|
|
628
|
+
retry: "Retry",
|
|
629
|
+
edit: "Edit",
|
|
630
|
+
readAloud: "Read aloud",
|
|
631
|
+
goodResponse: "Good response",
|
|
632
|
+
badResponse: "Bad response",
|
|
633
|
+
somethingWentWrong: "Something went wrong.",
|
|
634
|
+
conversation: "Conversation",
|
|
635
|
+
scrollToBottom: "Scroll to bottom",
|
|
636
|
+
showMore: "Show more",
|
|
637
|
+
showLess: "Show less",
|
|
638
|
+
model: "Model",
|
|
639
|
+
modelButton: (label) => `Model: ${label}`,
|
|
640
|
+
effort: "Effort",
|
|
641
|
+
effortNote: "Higher effort means more thorough responses, but takes longer and uses your limits faster.",
|
|
642
|
+
recommended: "Recommended",
|
|
643
|
+
moreModels: "More models",
|
|
644
|
+
widget: "Widget",
|
|
645
|
+
drawingWidget: "Drawing"
|
|
646
|
+
};
|
|
647
|
+
const LabelsContext = createContext(defaultLabels);
|
|
648
|
+
/** Merges `labels` over the labels of the enclosing provider (or the defaults). */
|
|
649
|
+
function LabelsProvider({ labels, children }) {
|
|
650
|
+
const parent = useLabels();
|
|
651
|
+
const merged = useMemo(() => labels ? {
|
|
652
|
+
...parent,
|
|
653
|
+
...labels
|
|
654
|
+
} : parent, [parent, labels]);
|
|
655
|
+
return /* @__PURE__ */ jsx(LabelsContext.Provider, {
|
|
656
|
+
value: merged,
|
|
657
|
+
children
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
/** The active UI strings: the enclosing `ChatTheme`'s `labels` over the English defaults. */
|
|
661
|
+
function useLabels() {
|
|
662
|
+
return useContext(LabelsContext);
|
|
663
|
+
}
|
|
475
664
|
// src/primitives/chat-theme.tsx
|
|
476
665
|
/**
|
|
477
666
|
* Applies a chat theme to part of a page. Menus, tooltips and dialogs opened inside render into this
|
|
478
667
|
* element instead of `<body>`, so they get the same theme. An app themed as a whole (variables on
|
|
479
668
|
* `:root`) does not need it for colors, but still uses it for `codeThemes` and `motion`.
|
|
480
669
|
*/
|
|
481
|
-
function ChatTheme({ theme, codeThemes, motion, children, className }) {
|
|
670
|
+
function ChatTheme({ theme, codeThemes, motion, labels, icons, children, className }) {
|
|
482
671
|
const portal = useRef(null);
|
|
483
672
|
const getContainer = useCallback(() => portal.current, []);
|
|
484
673
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -488,9 +677,15 @@ function ChatTheme({ theme, codeThemes, motion, children, className }) {
|
|
|
488
677
|
children: [/* @__PURE__ */ jsx(AppearanceProvider, {
|
|
489
678
|
codeThemes,
|
|
490
679
|
motion,
|
|
491
|
-
children: /* @__PURE__ */ jsx(
|
|
492
|
-
|
|
493
|
-
children
|
|
680
|
+
children: /* @__PURE__ */ jsx(LabelsProvider, {
|
|
681
|
+
labels,
|
|
682
|
+
children: /* @__PURE__ */ jsx(IconsProvider, {
|
|
683
|
+
icons,
|
|
684
|
+
children: /* @__PURE__ */ jsx(UNSAFE_PortalProvider, {
|
|
685
|
+
getContainer,
|
|
686
|
+
children
|
|
687
|
+
})
|
|
688
|
+
})
|
|
494
689
|
})
|
|
495
690
|
}), /* @__PURE__ */ jsx("div", { ref: portal })]
|
|
496
691
|
});
|
|
@@ -1012,6 +1207,700 @@ const SidebarContext = createContext({
|
|
|
1012
1207
|
dismiss: () => {}
|
|
1013
1208
|
});
|
|
1014
1209
|
const useSidebar = () => useContext(SidebarContext);
|
|
1210
|
+
// src/components/widget-frame.tsx
|
|
1211
|
+
const WidgetHostContext = createContext({});
|
|
1212
|
+
/** Sets what widgets below may do. ChatView and ChatTranscript set it from their `widgets` prop. */
|
|
1213
|
+
function WidgetHostProvider({ host, children }) {
|
|
1214
|
+
return /* @__PURE__ */ jsx(WidgetHostContext.Provider, {
|
|
1215
|
+
value: host,
|
|
1216
|
+
children
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
function useWidgetHost() {
|
|
1220
|
+
return useContext(WidgetHostContext);
|
|
1221
|
+
}
|
|
1222
|
+
/** The tallest a widget may grow, in px. */
|
|
1223
|
+
const WIDGET_MAX_HEIGHT = 2e3;
|
|
1224
|
+
const PROTOCOL_VERSION = "2026-01-26";
|
|
1225
|
+
const THEME_VARS = [
|
|
1226
|
+
"bg",
|
|
1227
|
+
"fg",
|
|
1228
|
+
"muted",
|
|
1229
|
+
"subtle",
|
|
1230
|
+
"surface",
|
|
1231
|
+
"raised",
|
|
1232
|
+
"hover",
|
|
1233
|
+
"selected",
|
|
1234
|
+
"border",
|
|
1235
|
+
"border-strong",
|
|
1236
|
+
"divider",
|
|
1237
|
+
"accent",
|
|
1238
|
+
"accent-fg",
|
|
1239
|
+
"check",
|
|
1240
|
+
"warning",
|
|
1241
|
+
"danger",
|
|
1242
|
+
"code-bg",
|
|
1243
|
+
"font-sans",
|
|
1244
|
+
"font-serif",
|
|
1245
|
+
"font-mono",
|
|
1246
|
+
"radius-sm",
|
|
1247
|
+
"radius-md",
|
|
1248
|
+
"radius-lg",
|
|
1249
|
+
"ui-font-size",
|
|
1250
|
+
"ui-font-size-sm",
|
|
1251
|
+
"message-font-size"
|
|
1252
|
+
];
|
|
1253
|
+
const ORIGIN = /^(https?|wss?):\/\/(\*\.)?[a-z0-9-]+(\.[a-z0-9-]+)*(:\d{1,5})?$/;
|
|
1254
|
+
function origins(list) {
|
|
1255
|
+
return (list ?? []).map((d) => d.trim().toLowerCase().replace(/\/$/, "")).filter((d) => ORIGIN.test(d));
|
|
1256
|
+
}
|
|
1257
|
+
/** Whether `origin` is covered by `allowed`, which may be a wildcard like "https://*.example.com". */
|
|
1258
|
+
function covers(allowed, origin) {
|
|
1259
|
+
if (allowed === origin) return true;
|
|
1260
|
+
const wild = /^([a-z]+:\/\/)\*\.(.+)$/.exec(allowed);
|
|
1261
|
+
return !!wild && origin.startsWith(wild[1]) && origin.slice(wild[1].length).endsWith("." + wild[2]) && !origin.includes("*");
|
|
1262
|
+
}
|
|
1263
|
+
/** The part of a widget's request that the host allows. */
|
|
1264
|
+
function intersectAllow(request, allow = {}) {
|
|
1265
|
+
const keep = (asked, max) => origins(asked).filter((o) => origins(max).some((m) => covers(m, o)));
|
|
1266
|
+
return {
|
|
1267
|
+
resourceDomains: keep(request.resourceDomains, allow.resourceDomains),
|
|
1268
|
+
connectDomains: keep(request.connectDomains, allow.connectDomains)
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
/** The Content-Security-Policy of a widget's document. */
|
|
1272
|
+
function widgetCsp(allow = {}) {
|
|
1273
|
+
const res = origins(allow.resourceDomains).join(" ");
|
|
1274
|
+
const conn = origins(allow.connectDomains).join(" ");
|
|
1275
|
+
const with_ = (base) => res ? `${base} ${res}` : base;
|
|
1276
|
+
return [
|
|
1277
|
+
"default-src 'none'",
|
|
1278
|
+
with_("script-src 'unsafe-inline'"),
|
|
1279
|
+
with_("style-src 'unsafe-inline'"),
|
|
1280
|
+
with_("img-src data: blob:"),
|
|
1281
|
+
with_("font-src data:"),
|
|
1282
|
+
with_("media-src data: blob:"),
|
|
1283
|
+
`connect-src ${conn || "'none'"}`,
|
|
1284
|
+
"frame-src 'none'",
|
|
1285
|
+
"base-uri 'none'",
|
|
1286
|
+
"form-action 'none'"
|
|
1287
|
+
].join("; ");
|
|
1288
|
+
}
|
|
1289
|
+
/** Runs inside a fragment's frame: renders the markup, reports its height, offers sendPrompt and openLink. */
|
|
1290
|
+
const BOOT = `(() => {
|
|
1291
|
+
const root = document.getElementById("widget");
|
|
1292
|
+
let id = 0;
|
|
1293
|
+
const post = (method, params, request) => parent.postMessage(request ? { jsonrpc: "2.0", id: ++id, method, params } : { jsonrpc: "2.0", method, params }, "*");
|
|
1294
|
+
window.sendPrompt = (text) => post("ui/message", { role: "user", content: [{ type: "text", text: String(text) }] }, true);
|
|
1295
|
+
window.openLink = (url) => post("ui/open-link", { url: String(url) }, true);
|
|
1296
|
+
let done = false;
|
|
1297
|
+
const runScripts = async () => {
|
|
1298
|
+
for (const old of [...root.querySelectorAll("script")]) {
|
|
1299
|
+
const s = document.createElement("script");
|
|
1300
|
+
for (const a of old.attributes) s.setAttribute(a.name, a.value);
|
|
1301
|
+
s.textContent = old.textContent;
|
|
1302
|
+
const loaded = s.src ? new Promise((ok) => { s.onload = s.onerror = ok; }) : null;
|
|
1303
|
+
old.replaceWith(s);
|
|
1304
|
+
if (loaded) await loaded;
|
|
1305
|
+
}
|
|
1306
|
+
};
|
|
1307
|
+
const theme = (ctx) => {
|
|
1308
|
+
const vars = (ctx && ctx.styles && ctx.styles.variables) || {};
|
|
1309
|
+
for (const k in vars) if (k.startsWith("--chat-")) document.documentElement.style.setProperty(k, vars[k]);
|
|
1310
|
+
if (ctx && ctx.theme) document.documentElement.style.colorScheme = ctx.theme;
|
|
1311
|
+
};
|
|
1312
|
+
addEventListener("message", (e) => {
|
|
1313
|
+
if (e.source !== parent) return;
|
|
1314
|
+
const m = e.data || {};
|
|
1315
|
+
if (m.method === "ui/notifications/host-context-changed") return theme(m.params);
|
|
1316
|
+
const code = m.params && m.params.arguments && m.params.arguments.widget_code;
|
|
1317
|
+
if (typeof code !== "string" || done) return;
|
|
1318
|
+
if (m.method === "ui/notifications/tool-input-partial") root.innerHTML = code;
|
|
1319
|
+
if (m.method === "ui/notifications/tool-input") { done = true; root.innerHTML = code; runScripts(); }
|
|
1320
|
+
});
|
|
1321
|
+
let last = -1;
|
|
1322
|
+
new ResizeObserver(() => {
|
|
1323
|
+
const h = Math.ceil(root.getBoundingClientRect().height);
|
|
1324
|
+
if (h !== last) { last = h; post("ui/notifications/size-changed", { height: h }); }
|
|
1325
|
+
}).observe(root);
|
|
1326
|
+
const seen = new Set();
|
|
1327
|
+
const report = (text) => { if (!seen.has(text) && seen.size < 10) { seen.add(text); post("notifications/message", { level: "error", logger: "widget", data: text }); } };
|
|
1328
|
+
addEventListener("error", (e) => report(String(e.message) + (e.lineno ? " (line " + e.lineno + ")" : "")));
|
|
1329
|
+
addEventListener("unhandledrejection", (e) => report("Unhandled promise rejection: " + String(e.reason && e.reason.message || e.reason)));
|
|
1330
|
+
post("ui/notifications/initialized", {});
|
|
1331
|
+
})();`;
|
|
1332
|
+
function fragmentDoc(csp, theme) {
|
|
1333
|
+
return `<!doctype html><html><head><meta charset="utf-8">
|
|
1334
|
+
<meta http-equiv="Content-Security-Policy" content="${csp}">
|
|
1335
|
+
<style>
|
|
1336
|
+
:root { ${Object.entries(theme.vars).map(([k, v]) => `${k}: ${v};`).join(" ")} color-scheme: ${theme.scheme}; }
|
|
1337
|
+
html, body { margin: 0; background: transparent; color: var(--chat-fg); font-family: var(--chat-font-sans);
|
|
1338
|
+
font-size: var(--chat-ui-font-size); line-height: 1.5; }
|
|
1339
|
+
#widget { display: flow-root; }
|
|
1340
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
1341
|
+
button, input, select, textarea { font: inherit; color: inherit; }
|
|
1342
|
+
</style></head><body><div id="widget"></div><script>${BOOT}<\/script></body></html>`;
|
|
1343
|
+
}
|
|
1344
|
+
/** An app's own document with the host's CSP put first in its head (a second policy can only narrow it). */
|
|
1345
|
+
function appDoc(csp, html) {
|
|
1346
|
+
const meta = `<meta http-equiv="Content-Security-Policy" content="${csp}">`;
|
|
1347
|
+
const head = /<head(\s[^>]*)?>/i.exec(html);
|
|
1348
|
+
if (head) return html.slice(0, head.index + head[0].length) + meta + html.slice(head.index + head[0].length);
|
|
1349
|
+
const root = /<html(\s[^>]*)?>/i.exec(html);
|
|
1350
|
+
if (root) return html.slice(0, root.index + root[0].length) + `<head>${meta}</head>` + html.slice(root.index + root[0].length);
|
|
1351
|
+
return `<!doctype html><html><head>${meta}</head><body>${html}</body></html>`;
|
|
1352
|
+
}
|
|
1353
|
+
function readTheme(el) {
|
|
1354
|
+
const style = getComputedStyle(el);
|
|
1355
|
+
return {
|
|
1356
|
+
vars: Object.fromEntries(THEME_VARS.map((v) => [`--chat-${v}`, style.getPropertyValue(`--chat-${v}`).trim()])),
|
|
1357
|
+
scheme: /light/.test(style.colorScheme) && !/dark/.test(style.colorScheme) ? "light" : "dark"
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
/** Calls `onChange` when anything above `el` changes attributes (a theme switch) or the OS scheme flips. */
|
|
1361
|
+
function watchTheme(el, onChange) {
|
|
1362
|
+
const observer = new MutationObserver(onChange);
|
|
1363
|
+
for (let node = el; node; node = node.parentElement) observer.observe(node, {
|
|
1364
|
+
attributes: true,
|
|
1365
|
+
attributeFilter: [
|
|
1366
|
+
"class",
|
|
1367
|
+
"style",
|
|
1368
|
+
"data-chat-theme"
|
|
1369
|
+
]
|
|
1370
|
+
});
|
|
1371
|
+
const media = typeof matchMedia === "function" ? matchMedia("(prefers-color-scheme: dark)") : void 0;
|
|
1372
|
+
media?.addEventListener?.("change", onChange);
|
|
1373
|
+
return () => {
|
|
1374
|
+
observer.disconnect();
|
|
1375
|
+
media?.removeEventListener?.("change", onChange);
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
function WidgetFrame({ html, document: isDocument = false, streaming = false, title, loadingMessages, allow, csp: request, onPrompt, onOpenLink, onCallTool, onError, toolInput, toolResult, cancelled, className }) {
|
|
1379
|
+
const labels = useLabels();
|
|
1380
|
+
const frame = useRef(null);
|
|
1381
|
+
const host = useRef(null);
|
|
1382
|
+
const [ready, setReady] = useState(false);
|
|
1383
|
+
const [height, setHeight] = useState(0);
|
|
1384
|
+
const [initial, setInitial] = useState();
|
|
1385
|
+
const theme = useRef(void 0);
|
|
1386
|
+
const [tick, setTick] = useState(0);
|
|
1387
|
+
const effective = request ? intersectAllow(request, allow) : isDocument ? {} : allow ?? {};
|
|
1388
|
+
const policy = widgetCsp(effective);
|
|
1389
|
+
const doc = useMemo(() => initial && (isDocument ? appDoc(policy, html) : fragmentDoc(policy, initial)), [
|
|
1390
|
+
initial,
|
|
1391
|
+
policy,
|
|
1392
|
+
isDocument,
|
|
1393
|
+
isDocument && html
|
|
1394
|
+
]);
|
|
1395
|
+
const latest = useRef({
|
|
1396
|
+
onPrompt,
|
|
1397
|
+
onOpenLink,
|
|
1398
|
+
onCallTool,
|
|
1399
|
+
onError,
|
|
1400
|
+
policy,
|
|
1401
|
+
effective
|
|
1402
|
+
});
|
|
1403
|
+
latest.current = {
|
|
1404
|
+
onPrompt,
|
|
1405
|
+
onOpenLink,
|
|
1406
|
+
onCallTool,
|
|
1407
|
+
onError,
|
|
1408
|
+
policy,
|
|
1409
|
+
effective
|
|
1410
|
+
};
|
|
1411
|
+
const send = (message) => frame.current?.contentWindow?.postMessage({
|
|
1412
|
+
jsonrpc: "2.0",
|
|
1413
|
+
...message
|
|
1414
|
+
}, "*");
|
|
1415
|
+
const context = () => ({
|
|
1416
|
+
theme: theme.current?.scheme,
|
|
1417
|
+
styles: { variables: theme.current?.vars },
|
|
1418
|
+
displayMode: "inline",
|
|
1419
|
+
availableDisplayModes: ["inline"],
|
|
1420
|
+
containerDimensions: {
|
|
1421
|
+
maxHeight: WIDGET_MAX_HEIGHT,
|
|
1422
|
+
...host.current && { width: host.current.clientWidth }
|
|
1423
|
+
},
|
|
1424
|
+
platform: "web",
|
|
1425
|
+
...labels.locale && { locale: labels.locale }
|
|
1426
|
+
});
|
|
1427
|
+
useEffect(() => {
|
|
1428
|
+
const el = host.current;
|
|
1429
|
+
theme.current = readTheme(el);
|
|
1430
|
+
setInitial(theme.current);
|
|
1431
|
+
return watchTheme(el, () => {
|
|
1432
|
+
const next = readTheme(el);
|
|
1433
|
+
if (JSON.stringify(next) === JSON.stringify(theme.current)) return;
|
|
1434
|
+
theme.current = next;
|
|
1435
|
+
send({
|
|
1436
|
+
method: "ui/notifications/host-context-changed",
|
|
1437
|
+
params: {
|
|
1438
|
+
theme: next.scheme,
|
|
1439
|
+
styles: { variables: next.vars }
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
});
|
|
1443
|
+
}, []);
|
|
1444
|
+
useEffect(() => setReady(false), [doc]);
|
|
1445
|
+
useEffect(() => {
|
|
1446
|
+
const reply = (id, result) => id !== void 0 && send({
|
|
1447
|
+
id,
|
|
1448
|
+
result
|
|
1449
|
+
});
|
|
1450
|
+
const fail = (id, code, message) => id !== void 0 && send({
|
|
1451
|
+
id,
|
|
1452
|
+
error: {
|
|
1453
|
+
code,
|
|
1454
|
+
message
|
|
1455
|
+
}
|
|
1456
|
+
});
|
|
1457
|
+
const onMessage = async (e) => {
|
|
1458
|
+
if (!frame.current || e.source !== frame.current.contentWindow) return;
|
|
1459
|
+
const m = e.data ?? {};
|
|
1460
|
+
if (typeof m !== "object" || typeof m.method !== "string") return;
|
|
1461
|
+
const { onPrompt, onOpenLink, onCallTool, effective } = latest.current;
|
|
1462
|
+
switch (m.method) {
|
|
1463
|
+
case "ui/initialize": return reply(m.id, {
|
|
1464
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1465
|
+
hostInfo: {
|
|
1466
|
+
name: "agentic-ui-kit",
|
|
1467
|
+
version: "1"
|
|
1468
|
+
},
|
|
1469
|
+
hostCapabilities: {
|
|
1470
|
+
...onOpenLink && { openLinks: {} },
|
|
1471
|
+
...onCallTool && { serverTools: {} },
|
|
1472
|
+
logging: {},
|
|
1473
|
+
sandbox: { csp: effective }
|
|
1474
|
+
},
|
|
1475
|
+
hostContext: context()
|
|
1476
|
+
});
|
|
1477
|
+
case "ui/notifications/initialized": return setReady(true);
|
|
1478
|
+
case "ui/notifications/size-changed": return setHeight(Math.min(Math.max(0, Number(m.params?.height) || 0), WIDGET_MAX_HEIGHT));
|
|
1479
|
+
case "ui/message": {
|
|
1480
|
+
const text = (Array.isArray(m.params?.content) ? m.params.content : []).find((c) => c?.type === "text")?.text;
|
|
1481
|
+
if (!onPrompt) return fail(m.id, -32e3, "Sending messages is off");
|
|
1482
|
+
if (typeof text !== "string" || !text.trim()) return fail(m.id, -32602, "Empty message");
|
|
1483
|
+
onPrompt(text.slice(0, 4e3));
|
|
1484
|
+
return reply(m.id, {});
|
|
1485
|
+
}
|
|
1486
|
+
case "ui/open-link": {
|
|
1487
|
+
const url = String(m.params?.url ?? "");
|
|
1488
|
+
if (!onOpenLink) return fail(m.id, -32e3, "Opening links is off");
|
|
1489
|
+
if (!/^https?:\/\//i.test(url)) return fail(m.id, -32602, "Only http(s) links");
|
|
1490
|
+
onOpenLink(url);
|
|
1491
|
+
return reply(m.id, {});
|
|
1492
|
+
}
|
|
1493
|
+
case "tools/call": {
|
|
1494
|
+
if (!onCallTool) return fail(m.id, -32e3, "Calling tools is off");
|
|
1495
|
+
const name = String(m.params?.name ?? "");
|
|
1496
|
+
const args = m.params?.arguments ?? {};
|
|
1497
|
+
try {
|
|
1498
|
+
return reply(m.id, await onCallTool(name, args));
|
|
1499
|
+
} catch (error) {
|
|
1500
|
+
return fail(m.id, -32e3, error instanceof Error ? error.message : String(error));
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
case "ping": return reply(m.id, {});
|
|
1504
|
+
case "notifications/message": {
|
|
1505
|
+
const data = typeof m.params?.data === "string" ? m.params.data : JSON.stringify(m.params?.data);
|
|
1506
|
+
if (m.params?.level === "error" && data) latest.current.onError?.(data.slice(0, 1e3));
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1509
|
+
default: return fail(m.id, -32601, "Method not found");
|
|
1510
|
+
}
|
|
1511
|
+
};
|
|
1512
|
+
window.addEventListener("message", onMessage);
|
|
1513
|
+
return () => window.removeEventListener("message", onMessage);
|
|
1514
|
+
}, []);
|
|
1515
|
+
useEffect(() => {
|
|
1516
|
+
if (!ready || isDocument) return;
|
|
1517
|
+
send({
|
|
1518
|
+
method: streaming ? "ui/notifications/tool-input-partial" : "ui/notifications/tool-input",
|
|
1519
|
+
params: { arguments: { widget_code: html } }
|
|
1520
|
+
});
|
|
1521
|
+
}, [
|
|
1522
|
+
ready,
|
|
1523
|
+
isDocument,
|
|
1524
|
+
html,
|
|
1525
|
+
streaming
|
|
1526
|
+
]);
|
|
1527
|
+
const inputKey = isDocument && toolInput !== void 0 ? JSON.stringify(toolInput) : void 0;
|
|
1528
|
+
useEffect(() => {
|
|
1529
|
+
if (!ready || inputKey === void 0) return;
|
|
1530
|
+
send({
|
|
1531
|
+
method: streaming ? "ui/notifications/tool-input-partial" : "ui/notifications/tool-input",
|
|
1532
|
+
params: { arguments: JSON.parse(inputKey) }
|
|
1533
|
+
});
|
|
1534
|
+
}, [
|
|
1535
|
+
ready,
|
|
1536
|
+
inputKey,
|
|
1537
|
+
streaming
|
|
1538
|
+
]);
|
|
1539
|
+
useEffect(() => {
|
|
1540
|
+
if (ready && isDocument && toolResult !== void 0) send({
|
|
1541
|
+
method: "ui/notifications/tool-result",
|
|
1542
|
+
params: toolResult
|
|
1543
|
+
});
|
|
1544
|
+
}, [
|
|
1545
|
+
ready,
|
|
1546
|
+
isDocument,
|
|
1547
|
+
toolResult
|
|
1548
|
+
]);
|
|
1549
|
+
useEffect(() => {
|
|
1550
|
+
if (ready && isDocument && cancelled !== void 0) send({
|
|
1551
|
+
method: "ui/notifications/tool-cancelled",
|
|
1552
|
+
params: { reason: cancelled }
|
|
1553
|
+
});
|
|
1554
|
+
}, [
|
|
1555
|
+
ready,
|
|
1556
|
+
isDocument,
|
|
1557
|
+
cancelled
|
|
1558
|
+
]);
|
|
1559
|
+
const lines = loadingMessages?.length ? loadingMessages : [labels.drawingWidget];
|
|
1560
|
+
useEffect(() => {
|
|
1561
|
+
if (!streaming || lines.length < 2) return;
|
|
1562
|
+
const id = setInterval(() => setTick((t) => t + 1), 1800);
|
|
1563
|
+
return () => clearInterval(id);
|
|
1564
|
+
}, [streaming, lines.length]);
|
|
1565
|
+
return /* @__PURE__ */ jsxs("figure", {
|
|
1566
|
+
ref: host,
|
|
1567
|
+
className: className ?? "my-2",
|
|
1568
|
+
"aria-label": title ?? labels.widget,
|
|
1569
|
+
"aria-busy": streaming || void 0,
|
|
1570
|
+
children: [streaming && /* @__PURE__ */ jsx("div", {
|
|
1571
|
+
className: "mb-2 animate-pulse font-chat-sans text-(length:--chat-ui-font-size) text-chat-muted motion-reduce:animate-none",
|
|
1572
|
+
children: lines[tick % lines.length]
|
|
1573
|
+
}), doc && /* @__PURE__ */ jsx("iframe", {
|
|
1574
|
+
ref: frame,
|
|
1575
|
+
title: title ?? labels.widget,
|
|
1576
|
+
sandbox: "allow-scripts",
|
|
1577
|
+
srcDoc: doc,
|
|
1578
|
+
style: {
|
|
1579
|
+
height: height || (streaming ? 120 : 0),
|
|
1580
|
+
transition: "height 150ms ease-out"
|
|
1581
|
+
},
|
|
1582
|
+
className: "block w-full border-0 bg-transparent"
|
|
1583
|
+
})]
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
/** Pulls the host's abilities from context into frame props. */
|
|
1587
|
+
function useHostProps(partId) {
|
|
1588
|
+
const { allow, onPrompt, onOpenLink, onCallTool, onWidgetError } = useWidgetHost();
|
|
1589
|
+
return {
|
|
1590
|
+
...allow && { allow },
|
|
1591
|
+
...onPrompt && { onPrompt },
|
|
1592
|
+
...onOpenLink && { onOpenLink },
|
|
1593
|
+
...onCallTool && { onCallTool },
|
|
1594
|
+
...onWidgetError && { onError: (message) => onWidgetError({
|
|
1595
|
+
partId,
|
|
1596
|
+
message
|
|
1597
|
+
}) }
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
/** The default look of a `widget` part: inline html, or an app UI the host reads from `resourceUri`. */
|
|
1601
|
+
function WidgetPartView({ part }) {
|
|
1602
|
+
const host = useWidgetHost();
|
|
1603
|
+
const props = useHostProps(part.id);
|
|
1604
|
+
const [resource, setResource] = useState();
|
|
1605
|
+
const uri = part.html === void 0 ? part.resourceUri : void 0;
|
|
1606
|
+
useEffect(() => {
|
|
1607
|
+
if (!uri || !host.readResource) return;
|
|
1608
|
+
let live = true;
|
|
1609
|
+
host.readResource(uri).then((r) => live && setResource(r), (e) => console.warn("widget:", e));
|
|
1610
|
+
return () => {
|
|
1611
|
+
live = false;
|
|
1612
|
+
};
|
|
1613
|
+
}, [uri, host.readResource]);
|
|
1614
|
+
if (part.html !== void 0) return /* @__PURE__ */ jsx(WidgetFrame, {
|
|
1615
|
+
html: part.html,
|
|
1616
|
+
streaming: part.status === "streaming",
|
|
1617
|
+
...part.title && { title: part.title },
|
|
1618
|
+
...props
|
|
1619
|
+
});
|
|
1620
|
+
if (!resource) return null;
|
|
1621
|
+
return /* @__PURE__ */ jsx(WidgetFrame, {
|
|
1622
|
+
document: true,
|
|
1623
|
+
html: resource.html,
|
|
1624
|
+
...resource.csp && { csp: resource.csp },
|
|
1625
|
+
...part.title && { title: part.title },
|
|
1626
|
+
...props
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
const str = (v) => typeof v === "string" ? v : void 0;
|
|
1630
|
+
/** Reads the fields of the suggested `visualizeTool` (widget_code, title, loading_messages). */
|
|
1631
|
+
function readVisualizeInput(input) {
|
|
1632
|
+
const i = input ?? {};
|
|
1633
|
+
const loading = Array.isArray(i.loading_messages) ? i.loading_messages.filter((l) => typeof l === "string" && l !== "") : [];
|
|
1634
|
+
return {
|
|
1635
|
+
...str(i.widget_code) !== void 0 && { html: str(i.widget_code) },
|
|
1636
|
+
...str(i.title) && { title: str(i.title) },
|
|
1637
|
+
...loading.length && { loadingMessages: loading.slice(0, 4) }
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
/**
|
|
1641
|
+
* A tool view that draws a call's (streaming) input in a WidgetFrame, for whatever tool your harness uses
|
|
1642
|
+
* to write HTML. Register it under your tool's name: `{ show_widget: widgetToolView() }`. `read` maps your
|
|
1643
|
+
* tool's input (possibly cut short while it streams) to the frame's fields; the default reads the suggested
|
|
1644
|
+
* `visualizeTool`'s fields.
|
|
1645
|
+
*/
|
|
1646
|
+
function widgetToolView(read = readVisualizeInput) {
|
|
1647
|
+
function WidgetToolBlock({ part }) {
|
|
1648
|
+
const props = useHostProps(part.id);
|
|
1649
|
+
const { html = "", title, loadingMessages } = read(part.input ?? part.partialInput);
|
|
1650
|
+
if (part.status === "error" && !html) return null;
|
|
1651
|
+
return /* @__PURE__ */ jsx(WidgetFrame, {
|
|
1652
|
+
html,
|
|
1653
|
+
streaming: part.status === "streaming" || part.status === "input",
|
|
1654
|
+
...title && { title },
|
|
1655
|
+
...loadingMessages?.length && { loadingMessages },
|
|
1656
|
+
...props
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
return { Block: WidgetToolBlock };
|
|
1660
|
+
}
|
|
1661
|
+
/** The default `onOpenLink` of ChatView and ChatTranscript: a new tab with no opener. */
|
|
1662
|
+
function openInNewTab(url) {
|
|
1663
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
1664
|
+
}
|
|
1665
|
+
// src/components/ui-tree.tsx
|
|
1666
|
+
const RegistryContext = createContext(void 0);
|
|
1667
|
+
/**
|
|
1668
|
+
* The components the model may use: in ```ui blocks of its replies (see `uiMarkupGuide` in
|
|
1669
|
+
* agentic-ui-kit/core) and in tool calls drawn with `uiTreeToolView`. `persist` keeps each widget's state.
|
|
1670
|
+
*/
|
|
1671
|
+
function UiComponentsProvider({ components, persist, markup = "ui", children }) {
|
|
1672
|
+
const value = useMemo(() => ({
|
|
1673
|
+
components: new Map(components.map((c) => [c.name, c])),
|
|
1674
|
+
persist,
|
|
1675
|
+
markup
|
|
1676
|
+
}), [
|
|
1677
|
+
components,
|
|
1678
|
+
persist,
|
|
1679
|
+
markup
|
|
1680
|
+
]);
|
|
1681
|
+
return /* @__PURE__ */ jsx(RegistryContext.Provider, {
|
|
1682
|
+
value,
|
|
1683
|
+
children
|
|
1684
|
+
});
|
|
1685
|
+
}
|
|
1686
|
+
const TreeContext = createContext({
|
|
1687
|
+
state: {},
|
|
1688
|
+
run: () => {},
|
|
1689
|
+
streaming: false
|
|
1690
|
+
});
|
|
1691
|
+
/** Inside a registered component: runs an action from its props, e.g. `run(props.onPress)`. Bad actions do nothing. */
|
|
1692
|
+
function useUiAction() {
|
|
1693
|
+
const { run } = useContext(TreeContext);
|
|
1694
|
+
return useCallback((action) => matchesSchema(uiActionSchema, action) && run(action), [run]);
|
|
1695
|
+
}
|
|
1696
|
+
/** Inside a registered component: the widget's state and a setter, for inputs that write to it. */
|
|
1697
|
+
function useUiState() {
|
|
1698
|
+
const { state, run } = useContext(TreeContext);
|
|
1699
|
+
return [state, useCallback((key, value) => run({
|
|
1700
|
+
action: "set",
|
|
1701
|
+
key,
|
|
1702
|
+
value
|
|
1703
|
+
}), [run])];
|
|
1704
|
+
}
|
|
1705
|
+
/** Replaces `{ "$state": key }` anywhere in the props with the state's value. */
|
|
1706
|
+
function resolve(value, state) {
|
|
1707
|
+
if (isStateRef(value)) return state[value.$state];
|
|
1708
|
+
if (Array.isArray(value)) return value.map((v) => resolve(v, state));
|
|
1709
|
+
if (typeof value === "object" && value !== null) {
|
|
1710
|
+
const out = {};
|
|
1711
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1712
|
+
const r = resolve(v, state);
|
|
1713
|
+
if (r !== void 0) out[k] = r;
|
|
1714
|
+
}
|
|
1715
|
+
return out;
|
|
1716
|
+
}
|
|
1717
|
+
return value;
|
|
1718
|
+
}
|
|
1719
|
+
/** A component that throws renders nothing instead of taking the message down. */
|
|
1720
|
+
var Contain = class extends Component {
|
|
1721
|
+
state = { failed: false };
|
|
1722
|
+
static getDerivedStateFromError() {
|
|
1723
|
+
return { failed: true };
|
|
1724
|
+
}
|
|
1725
|
+
componentDidCatch(error) {
|
|
1726
|
+
console.warn("render_ui:", error);
|
|
1727
|
+
}
|
|
1728
|
+
render() {
|
|
1729
|
+
return this.state.failed ? null : this.props.children;
|
|
1730
|
+
}
|
|
1731
|
+
};
|
|
1732
|
+
function Node({ node, registry }) {
|
|
1733
|
+
const { state } = useContext(TreeContext);
|
|
1734
|
+
if (Array.isArray(node)) return node.map((child, i) => /* @__PURE__ */ jsx(Node, {
|
|
1735
|
+
node: child,
|
|
1736
|
+
registry
|
|
1737
|
+
}, i));
|
|
1738
|
+
if (typeof node !== "object" || node === null) return null;
|
|
1739
|
+
const { component, props = {}, children } = node;
|
|
1740
|
+
const spec = typeof component === "string" ? registry.components.get(component) : void 0;
|
|
1741
|
+
if (!spec || typeof props !== "object") return null;
|
|
1742
|
+
const resolved = resolve(props, state);
|
|
1743
|
+
if (!matchesSchema(spec.props ?? { type: "object" }, resolved)) return null;
|
|
1744
|
+
const Render = spec.render;
|
|
1745
|
+
const kids = spec.children && Array.isArray(children) ? children.map((child, i) => /* @__PURE__ */ jsx(Node, {
|
|
1746
|
+
node: child,
|
|
1747
|
+
registry
|
|
1748
|
+
}, i)) : void 0;
|
|
1749
|
+
return /* @__PURE__ */ jsx(Contain, { children: /* @__PURE__ */ jsx(Render, {
|
|
1750
|
+
...resolved,
|
|
1751
|
+
children: kids
|
|
1752
|
+
}) });
|
|
1753
|
+
}
|
|
1754
|
+
/** Draws a `render_ui` tree with the registered components. Unknown components and bad props render nothing. */
|
|
1755
|
+
function UiTree({ id, root, state: initial, streaming = false }) {
|
|
1756
|
+
const registry = useContext(RegistryContext);
|
|
1757
|
+
const host = useWidgetHost();
|
|
1758
|
+
const [local, setLocal] = useState(() => registry?.persist?.load(id) ?? {});
|
|
1759
|
+
const changed = useRef(false);
|
|
1760
|
+
const latest = useRef(host);
|
|
1761
|
+
latest.current = host;
|
|
1762
|
+
useEffect(() => {
|
|
1763
|
+
if (changed.current) registry?.persist?.save(id, local);
|
|
1764
|
+
}, [
|
|
1765
|
+
id,
|
|
1766
|
+
local,
|
|
1767
|
+
registry
|
|
1768
|
+
]);
|
|
1769
|
+
const run = useCallback((action) => {
|
|
1770
|
+
switch (action.action) {
|
|
1771
|
+
case "set":
|
|
1772
|
+
changed.current = true;
|
|
1773
|
+
return setLocal((s) => ({
|
|
1774
|
+
...s,
|
|
1775
|
+
[action.key]: action.value
|
|
1776
|
+
}));
|
|
1777
|
+
case "send": return latest.current.onPrompt?.(action.text);
|
|
1778
|
+
case "open": return /^https?:\/\//i.test(action.url) && latest.current.onOpenLink?.(action.url);
|
|
1779
|
+
case "copy":
|
|
1780
|
+
navigator.clipboard?.writeText(action.text).catch(() => {});
|
|
1781
|
+
return;
|
|
1782
|
+
}
|
|
1783
|
+
}, []);
|
|
1784
|
+
const state = useMemo(() => ({
|
|
1785
|
+
...typeof initial === "object" && initial ? initial : {},
|
|
1786
|
+
...local
|
|
1787
|
+
}), [initial, local]);
|
|
1788
|
+
const context = useMemo(() => ({
|
|
1789
|
+
state,
|
|
1790
|
+
run,
|
|
1791
|
+
streaming
|
|
1792
|
+
}), [
|
|
1793
|
+
state,
|
|
1794
|
+
run,
|
|
1795
|
+
streaming
|
|
1796
|
+
]);
|
|
1797
|
+
if (!registry) return null;
|
|
1798
|
+
return /* @__PURE__ */ jsx(TreeContext.Provider, {
|
|
1799
|
+
value: context,
|
|
1800
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1801
|
+
className: "chat-ui-tree my-2",
|
|
1802
|
+
"aria-busy": streaming || void 0,
|
|
1803
|
+
children: /* @__PURE__ */ jsx(Node, {
|
|
1804
|
+
node: root,
|
|
1805
|
+
registry
|
|
1806
|
+
})
|
|
1807
|
+
})
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
/** The fence language drawn as UI here, or undefined outside a `UiComponentsProvider`. */
|
|
1811
|
+
function useUiMarkupLanguage() {
|
|
1812
|
+
const registry = useContext(RegistryContext);
|
|
1813
|
+
return registry && registry.markup !== false ? registry.markup : void 0;
|
|
1814
|
+
}
|
|
1815
|
+
/** A ```ui block from a reply, drawn with the registered components. Safe on a block that is still streaming. */
|
|
1816
|
+
function UiMarkupBlock({ id, source, streaming = false }) {
|
|
1817
|
+
const { nodes, state } = useMemo(() => parseUiMarkup(source), [source]);
|
|
1818
|
+
return /* @__PURE__ */ jsx(UiTree, {
|
|
1819
|
+
id,
|
|
1820
|
+
root: nodes,
|
|
1821
|
+
state,
|
|
1822
|
+
streaming
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* A tool view that draws a call's (streaming) JSX markup with your registered components, for whatever tool
|
|
1827
|
+
* your harness uses; the suggested one is core's `showUiTool`. Register it under your tool's name:
|
|
1828
|
+
* `{ show_ui: uiMarkupToolView() }`. `read` maps your tool's input to the markup; the default reads `markup`.
|
|
1829
|
+
*/
|
|
1830
|
+
function uiMarkupToolView(read = (input) => {
|
|
1831
|
+
const markup = input?.markup;
|
|
1832
|
+
return typeof markup === "string" ? markup : void 0;
|
|
1833
|
+
}) {
|
|
1834
|
+
function UiMarkupToolBlock({ part }) {
|
|
1835
|
+
const markup = read(part.input ?? part.partialInput);
|
|
1836
|
+
if (!markup) return null;
|
|
1837
|
+
return /* @__PURE__ */ jsx(UiMarkupBlock, {
|
|
1838
|
+
id: part.id,
|
|
1839
|
+
source: markup,
|
|
1840
|
+
streaming: part.status === "streaming" || part.status === "input"
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
return { Block: UiMarkupToolBlock };
|
|
1844
|
+
}
|
|
1845
|
+
/** Reads `{ root, state }`. Models sometimes send `root` or `state` as a JSON string; those are parsed too. */
|
|
1846
|
+
function readTreeInput(input) {
|
|
1847
|
+
const i = input ?? {};
|
|
1848
|
+
const unwrap = (v) => typeof v === "string" ? salvageJson(v).value : v;
|
|
1849
|
+
const root = unwrap(i.root);
|
|
1850
|
+
const state = unwrap(i.state);
|
|
1851
|
+
return {
|
|
1852
|
+
...root !== void 0 && { root },
|
|
1853
|
+
...typeof state === "object" && state !== null && { state }
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
/**
|
|
1857
|
+
* A tool view that draws a call's (streaming) input as a tree of your registered components, for whatever
|
|
1858
|
+
* tool your harness uses. Register it under your tool's name: `{ render_ui: uiTreeToolView() }`. `read` maps
|
|
1859
|
+
* your tool's input to the tree and its starting state; the default reads `{ root, state }`, the shape of
|
|
1860
|
+
* the suggested `renderUiTool`.
|
|
1861
|
+
*/
|
|
1862
|
+
function uiTreeToolView(read = readTreeInput) {
|
|
1863
|
+
function UiTreeToolBlock({ part }) {
|
|
1864
|
+
const { root, state } = read(part.input ?? part.partialInput);
|
|
1865
|
+
if (!root) return null;
|
|
1866
|
+
const ok = typeof state === "object" && state !== null;
|
|
1867
|
+
return /* @__PURE__ */ jsx(UiTree, {
|
|
1868
|
+
id: part.id,
|
|
1869
|
+
root,
|
|
1870
|
+
...ok && { state },
|
|
1871
|
+
streaming: part.status === "streaming" || part.status === "input"
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
return { Block: UiTreeToolBlock };
|
|
1875
|
+
}
|
|
1876
|
+
/**
|
|
1877
|
+
* The escape hatch: raw HTML or SVG in a sandboxed WidgetFrame, as a node of the tree. Register it only if
|
|
1878
|
+
* the model may write HTML; it gets what the chat's `widgets` host allows.
|
|
1879
|
+
*/
|
|
1880
|
+
const htmlComponent = {
|
|
1881
|
+
name: "Html",
|
|
1882
|
+
description: "Raw HTML or <svg> in a sandboxed frame, only for what the other components cannot show. Follow the visual design guide.",
|
|
1883
|
+
props: {
|
|
1884
|
+
type: "object",
|
|
1885
|
+
properties: {
|
|
1886
|
+
html: { type: "string" },
|
|
1887
|
+
title: { type: "string" }
|
|
1888
|
+
},
|
|
1889
|
+
required: ["html"]
|
|
1890
|
+
},
|
|
1891
|
+
render: function Html({ html, title }) {
|
|
1892
|
+
const { streaming } = useContext(TreeContext);
|
|
1893
|
+
const { allow, onPrompt, onOpenLink } = useWidgetHost();
|
|
1894
|
+
return /* @__PURE__ */ jsx(WidgetFrame, {
|
|
1895
|
+
html,
|
|
1896
|
+
streaming,
|
|
1897
|
+
...title && { title },
|
|
1898
|
+
...allow && { allow },
|
|
1899
|
+
...onPrompt && { onPrompt },
|
|
1900
|
+
...onOpenLink && { onOpenLink }
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
};
|
|
1015
1904
|
// src/components/word-fade.ts
|
|
1016
1905
|
const OPAQUE_TAGS = /* @__PURE__ */ new Set([
|
|
1017
1906
|
"code",
|
|
@@ -1078,45 +1967,96 @@ function textOf(node) {
|
|
|
1078
1967
|
if (node.type === "text") return node.value ?? "";
|
|
1079
1968
|
return (node.children ?? []).map(textOf).join("");
|
|
1080
1969
|
}
|
|
1970
|
+
const BlockContext = createContext(void 0);
|
|
1971
|
+
function Code({ node, className, children }) {
|
|
1972
|
+
const hast = node;
|
|
1973
|
+
const language = /language-([\w+#-]+)/.exec(className ?? "")?.[1];
|
|
1974
|
+
const ui = useUiMarkupLanguage();
|
|
1975
|
+
const block = useContext(BlockContext);
|
|
1976
|
+
const multiline = hast?.position && hast.position.start.line !== hast.position.end.line;
|
|
1977
|
+
if (!language && !multiline) return /* @__PURE__ */ jsx("code", {
|
|
1978
|
+
"data-chat-inline-code": true,
|
|
1979
|
+
children
|
|
1980
|
+
});
|
|
1981
|
+
const code = textOf(hast).replace(/\n$/, "");
|
|
1982
|
+
if (block && ui && language === ui) return /* @__PURE__ */ jsx(UiMarkupBlock, {
|
|
1983
|
+
id: `${block.id}:${hast?.position?.start.line ?? 0}`,
|
|
1984
|
+
source: code,
|
|
1985
|
+
streaming: block.streaming
|
|
1986
|
+
});
|
|
1987
|
+
return /* @__PURE__ */ jsx(CodeBlock, {
|
|
1988
|
+
variant: "boxed",
|
|
1989
|
+
code,
|
|
1990
|
+
...language && { language }
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1081
1993
|
const components = {
|
|
1082
1994
|
pre: ({ children }) => /* @__PURE__ */ jsx(Fragment, { children }),
|
|
1083
|
-
code: ({ node, className, children }) => {
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
"data-chat-inline-code": true,
|
|
1089
|
-
children
|
|
1090
|
-
});
|
|
1091
|
-
return /* @__PURE__ */ jsx(CodeBlock, {
|
|
1092
|
-
variant: "boxed",
|
|
1093
|
-
code: textOf(hast).replace(/\n$/, ""),
|
|
1094
|
-
...language && { language }
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1995
|
+
code: ({ node, className, children }) => /* @__PURE__ */ jsx(Code, {
|
|
1996
|
+
node,
|
|
1997
|
+
className,
|
|
1998
|
+
children
|
|
1999
|
+
})
|
|
1097
2000
|
};
|
|
2001
|
+
const LinksContext = createContext(void 0);
|
|
2002
|
+
function MarkdownLinksProvider({ renderLink, children }) {
|
|
2003
|
+
return /* @__PURE__ */ jsx(LinksContext.Provider, {
|
|
2004
|
+
value: renderLink,
|
|
2005
|
+
children
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
1098
2008
|
/** Message markdown: GFM, KaTeX math ($…$ and $$…$$), Shiki code blocks, smooth fade-in while streaming. */
|
|
1099
|
-
function Markdown({ children, streaming = false, className }) {
|
|
2009
|
+
function Markdown({ children, streaming = false, className, partId }) {
|
|
1100
2010
|
const text = useSmoothText(children, streaming);
|
|
1101
2011
|
const animating = streaming || text.length < children.length;
|
|
1102
2012
|
const [streamed, setStreamed] = useState(streaming);
|
|
1103
2013
|
if (streaming && !streamed) setStreamed(true);
|
|
1104
2014
|
return /* @__PURE__ */ jsx("div", {
|
|
1105
2015
|
className: clsx("chat-markdown", streamed && "chat-fade", className),
|
|
1106
|
-
children: /* @__PURE__ */ jsx(
|
|
1107
|
-
|
|
1108
|
-
animating,
|
|
1109
|
-
|
|
2016
|
+
children: /* @__PURE__ */ jsx(BlockScope, {
|
|
2017
|
+
id: partId,
|
|
2018
|
+
streaming: animating,
|
|
2019
|
+
children: /* @__PURE__ */ jsx(MarkdownBody, {
|
|
2020
|
+
text,
|
|
2021
|
+
animating,
|
|
2022
|
+
fade: streamed
|
|
2023
|
+
})
|
|
1110
2024
|
})
|
|
1111
2025
|
});
|
|
1112
2026
|
}
|
|
2027
|
+
function BlockScope({ id, streaming, children }) {
|
|
2028
|
+
const value = useMemo(() => id ? {
|
|
2029
|
+
id,
|
|
2030
|
+
streaming
|
|
2031
|
+
} : void 0, [id, streaming]);
|
|
2032
|
+
return /* @__PURE__ */ jsx(BlockContext.Provider, {
|
|
2033
|
+
value,
|
|
2034
|
+
children
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
1113
2037
|
const MarkdownBody = memo(function MarkdownBody({ text, animating, fade }) {
|
|
2038
|
+
const renderLink = useContext(LinksContext);
|
|
2039
|
+
const withLinks = useMemo(() => renderLink ? {
|
|
2040
|
+
...components,
|
|
2041
|
+
a: ({ href, children }) => {
|
|
2042
|
+
const custom = href ? renderLink({
|
|
2043
|
+
href,
|
|
2044
|
+
children
|
|
2045
|
+
}) : void 0;
|
|
2046
|
+
return custom !== void 0 ? /* @__PURE__ */ jsx(Fragment, { children: custom }) : /* @__PURE__ */ jsx("a", {
|
|
2047
|
+
href,
|
|
2048
|
+
target: "_blank",
|
|
2049
|
+
rel: "noopener noreferrer",
|
|
2050
|
+
children
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
} : components, [renderLink]);
|
|
1114
2054
|
return /* @__PURE__ */ jsx(Streamdown, {
|
|
1115
2055
|
prefix: PREFIX,
|
|
1116
2056
|
mode: animating || fade ? "streaming" : "static",
|
|
1117
2057
|
...fade && { rehypePlugins: fadePlugins },
|
|
1118
2058
|
plugins: { math },
|
|
1119
|
-
components,
|
|
2059
|
+
components: withLinks,
|
|
1120
2060
|
controls: false,
|
|
1121
2061
|
lineNumbers: false,
|
|
1122
2062
|
linkSafety: { enabled: false },
|
|
@@ -1156,32 +2096,31 @@ const useChatTool = useAgentTool;
|
|
|
1156
2096
|
/** Streaming or running: the model or a tool is busy with it. */
|
|
1157
2097
|
const isWorking = (part) => part.status !== "done" && part.status !== "error" && part.status !== "awaiting";
|
|
1158
2098
|
/** Row label for any activity part. */
|
|
1159
|
-
function activityLabel(part, view) {
|
|
2099
|
+
function activityLabel(part, labels, view) {
|
|
1160
2100
|
if (part.type === "thinking") {
|
|
1161
2101
|
if (part.title) return part.title;
|
|
1162
|
-
if (part.status !== "done") return
|
|
1163
|
-
return part.durationMs ?
|
|
2102
|
+
if (part.status !== "done") return labels.thinking;
|
|
2103
|
+
return part.durationMs ? labels.thoughtFor(formatDuration(part.durationMs, labels)) : labels.thought;
|
|
1164
2104
|
}
|
|
1165
|
-
return view?.label?.(part) ?? part.title ?? part.name;
|
|
2105
|
+
return view?.label?.(part, labels) ?? part.title ?? part.name;
|
|
1166
2106
|
}
|
|
1167
2107
|
/**
|
|
1168
2108
|
* Heading of an activity group. While live it follows the latest step. Once done it lists what the tools
|
|
1169
2109
|
* did, in order ("Ran a command, searched the web"); a group of only thinking shows the first title.
|
|
1170
2110
|
*/
|
|
1171
|
-
function activityHeading(parts, views) {
|
|
2111
|
+
function activityHeading(parts, views, labels) {
|
|
1172
2112
|
const waiting = parts.find((p) => p.type === "tool_call" && p.status === "awaiting");
|
|
1173
|
-
if (waiting) return /* @__PURE__ */ jsxs(Fragment, { children: [activityLabel(waiting, views[waiting.name]),
|
|
2113
|
+
if (waiting) return /* @__PURE__ */ jsxs(Fragment, { children: [activityLabel(waiting, labels, views[waiting.name]), labels.waitingForYou] });
|
|
1174
2114
|
if (parts.some(isWorking)) {
|
|
1175
2115
|
const last = parts.at(-1);
|
|
1176
|
-
return activityLabel(last, last.type === "tool_call" ? views[last.name] : void 0);
|
|
2116
|
+
return activityLabel(last, labels, last.type === "tool_call" ? views[last.name] : void 0);
|
|
1177
2117
|
}
|
|
1178
2118
|
const counts = /* @__PURE__ */ new Map();
|
|
1179
2119
|
for (const part of parts) if (part.type === "tool_call") counts.set(part.name, (counts.get(part.name) ?? 0) + 1);
|
|
1180
|
-
if (counts.size === 0) return parts[0] ? activityLabel(parts[0]) : "";
|
|
1181
|
-
const
|
|
1182
|
-
return
|
|
2120
|
+
if (counts.size === 0) return parts[0] ? activityLabel(parts[0], labels) : "";
|
|
2121
|
+
const phrases = [...counts].map(([name, count]) => views[name]?.summary?.(count, labels) ?? labels.usedTool(name));
|
|
2122
|
+
return labels.joinSummaries(phrases);
|
|
1183
2123
|
}
|
|
1184
|
-
const plural = (count, one, many) => count === 1 ? one : many(count);
|
|
1185
2124
|
function recordOf(value) {
|
|
1186
2125
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1187
2126
|
}
|
|
@@ -1190,10 +2129,11 @@ function stringOf(value) {
|
|
|
1190
2129
|
}
|
|
1191
2130
|
/** A shell command. Input `{ command, description? }`; result a string or `{ stdout, stderr, exitCode }`. */
|
|
1192
2131
|
const commandTool = {
|
|
1193
|
-
label: () =>
|
|
2132
|
+
label: (_, labels) => labels.ranCommandLabel,
|
|
1194
2133
|
hint: (part) => stringOf(recordOf(part.input).description),
|
|
1195
|
-
summary: (n
|
|
2134
|
+
summary: (n, labels) => labels.ranCommands(n),
|
|
1196
2135
|
Detail: function CommandDetail({ part }) {
|
|
2136
|
+
const labels = useLabels();
|
|
1197
2137
|
const command = stringOf(recordOf(part.input).command) ?? part.inputJson;
|
|
1198
2138
|
const result = recordOf(part.result);
|
|
1199
2139
|
const output = stringOf(part.result) ?? [stringOf(result.stdout), stringOf(result.stderr)].filter(Boolean).join("\n");
|
|
@@ -1205,31 +2145,37 @@ const commandTool = {
|
|
|
1205
2145
|
code: command
|
|
1206
2146
|
}),
|
|
1207
2147
|
(output || part.status === "done") && /* @__PURE__ */ jsx(CodeBlock, {
|
|
1208
|
-
label:
|
|
1209
|
-
code: output ||
|
|
2148
|
+
label: labels.output,
|
|
2149
|
+
code: output || labels.noOutput
|
|
1210
2150
|
}),
|
|
1211
2151
|
part.error && /* @__PURE__ */ jsx("p", {
|
|
1212
|
-
className: "text-
|
|
2152
|
+
className: "text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
|
|
1213
2153
|
children: part.error
|
|
1214
2154
|
})
|
|
1215
2155
|
]
|
|
1216
2156
|
});
|
|
1217
2157
|
}
|
|
1218
2158
|
};
|
|
1219
|
-
/**
|
|
2159
|
+
/**
|
|
2160
|
+
* A web search. Input `{ query }`; results (`{ results: SearchResult[] }` or `SearchResult[]`) from the
|
|
2161
|
+
* part's display `data`, else its `result`.
|
|
2162
|
+
*/
|
|
1220
2163
|
const webSearchTool = {
|
|
1221
|
-
label: () =>
|
|
2164
|
+
label: (_, labels) => labels.searchedWebLabel,
|
|
1222
2165
|
subtitle: (part) => stringOf(recordOf(part.input).query) ?? part.subtitle,
|
|
1223
|
-
summary: (n
|
|
2166
|
+
summary: (n, labels) => labels.searchedWeb(n),
|
|
1224
2167
|
Detail: function WebSearchDetail({ part }) {
|
|
1225
|
-
const
|
|
2168
|
+
const labels = useLabels();
|
|
2169
|
+
const icons = useIcons();
|
|
2170
|
+
const shown = part.data ?? part.result;
|
|
2171
|
+
const results = Array.isArray(shown) ? shown : recordOf(shown).results;
|
|
1226
2172
|
if (part.error) return /* @__PURE__ */ jsx("p", {
|
|
1227
|
-
className: "text-
|
|
2173
|
+
className: "text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
|
|
1228
2174
|
children: part.error
|
|
1229
2175
|
});
|
|
1230
2176
|
if (!results?.length) return /* @__PURE__ */ jsx("p", {
|
|
1231
|
-
className: "text-
|
|
1232
|
-
children:
|
|
2177
|
+
className: "text-(length:--chat-ui-font-size) leading-5 text-chat-muted",
|
|
2178
|
+
children: labels.noResults
|
|
1233
2179
|
});
|
|
1234
2180
|
return /* @__PURE__ */ jsx("ul", {
|
|
1235
2181
|
className: "-mx-1",
|
|
@@ -1237,22 +2183,22 @@ const webSearchTool = {
|
|
|
1237
2183
|
href: result.url,
|
|
1238
2184
|
target: "_blank",
|
|
1239
2185
|
rel: "noreferrer",
|
|
1240
|
-
className: "flex items-center gap-2.5 rounded-chat-sm px-1 py-2 text-
|
|
2186
|
+
className: "flex items-center gap-2.5 rounded-chat-sm px-1 py-2 text-(length:--chat-activity-font-size) text-chat-muted outline-none hover:text-chat-fg focus-visible:ring-2 focus-visible:ring-chat-focus",
|
|
1241
2187
|
children: [
|
|
1242
2188
|
result.favicon ? /* @__PURE__ */ jsx("img", {
|
|
1243
2189
|
src: result.favicon,
|
|
1244
2190
|
alt: "",
|
|
1245
2191
|
className: "size-4 shrink-0 rounded-sm"
|
|
1246
|
-
}) : /* @__PURE__ */ jsx(
|
|
1247
|
-
"
|
|
1248
|
-
|
|
2192
|
+
}) : /* @__PURE__ */ jsx(IconSlot, {
|
|
2193
|
+
className: "[&_svg]:size-4",
|
|
2194
|
+
children: icons.web
|
|
1249
2195
|
}),
|
|
1250
2196
|
/* @__PURE__ */ jsx("span", {
|
|
1251
2197
|
className: "min-w-0 flex-1 truncate",
|
|
1252
2198
|
children: result.title
|
|
1253
2199
|
}),
|
|
1254
2200
|
/* @__PURE__ */ jsx("span", {
|
|
1255
|
-
className: "shrink-0 text-
|
|
2201
|
+
className: "shrink-0 text-(length:--chat-ui-font-size) leading-5",
|
|
1256
2202
|
children: hostOf(result.url)
|
|
1257
2203
|
})
|
|
1258
2204
|
]
|
|
@@ -1260,18 +2206,44 @@ const webSearchTool = {
|
|
|
1260
2206
|
});
|
|
1261
2207
|
}
|
|
1262
2208
|
};
|
|
2209
|
+
/** Reads a web page. Input `{ url }`; the row shows the page's host, the detail links to it. */
|
|
2210
|
+
const webFetchTool = {
|
|
2211
|
+
label: (_, labels) => labels.readPageLabel,
|
|
2212
|
+
subtitle: (part) => {
|
|
2213
|
+
const url = stringOf(recordOf(part.input).url);
|
|
2214
|
+
return url ? hostOf(url) : part.subtitle;
|
|
2215
|
+
},
|
|
2216
|
+
summary: (n, labels) => labels.readPages(n),
|
|
2217
|
+
Detail: function WebFetchDetail({ part }) {
|
|
2218
|
+
const url = stringOf(recordOf(part.input).url);
|
|
2219
|
+
const title = stringOf(recordOf(part.data).title);
|
|
2220
|
+
if (part.error) return /* @__PURE__ */ jsx("p", {
|
|
2221
|
+
className: "text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
|
|
2222
|
+
children: part.error
|
|
2223
|
+
});
|
|
2224
|
+
if (!url) return null;
|
|
2225
|
+
return /* @__PURE__ */ jsx("a", {
|
|
2226
|
+
href: url,
|
|
2227
|
+
target: "_blank",
|
|
2228
|
+
rel: "noopener noreferrer",
|
|
2229
|
+
className: "block truncate text-(length:--chat-ui-font-size) leading-5 text-chat-fg hover:underline",
|
|
2230
|
+
children: title ?? url
|
|
2231
|
+
});
|
|
2232
|
+
}
|
|
2233
|
+
};
|
|
1263
2234
|
/** Memory lookups, e.g. Anthropic's memory tool. Input `{ path }`. */
|
|
1264
2235
|
const memoryTool = {
|
|
1265
|
-
label: () =>
|
|
2236
|
+
label: (_, labels) => labels.recalledMemoryLabel,
|
|
1266
2237
|
subtitle: (part) => {
|
|
1267
2238
|
return stringOf(recordOf(part.input).path)?.split("/").at(-1) ?? part.subtitle;
|
|
1268
2239
|
},
|
|
1269
|
-
summary: () =>
|
|
2240
|
+
summary: (n, labels) => labels.recalledMemory(n)
|
|
1270
2241
|
};
|
|
1271
2242
|
const defaultToolViews = {
|
|
1272
2243
|
bash: commandTool,
|
|
1273
2244
|
shell: commandTool,
|
|
1274
2245
|
web_search: webSearchTool,
|
|
2246
|
+
web_fetch: webFetchTool,
|
|
1275
2247
|
memory: memoryTool,
|
|
1276
2248
|
memory_view: memoryTool
|
|
1277
2249
|
};
|
|
@@ -1283,9 +2255,8 @@ function hostOf(url) {
|
|
|
1283
2255
|
}
|
|
1284
2256
|
}
|
|
1285
2257
|
/** "9s", "1m 5s". */
|
|
1286
|
-
function formatDuration(ms) {
|
|
1287
|
-
|
|
1288
|
-
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
2258
|
+
function formatDuration(ms, labels = defaultLabels) {
|
|
2259
|
+
return labels.duration(Math.max(1, Math.round(ms / 1e3)));
|
|
1289
2260
|
}
|
|
1290
2261
|
// src/components/token-counter.tsx
|
|
1291
2262
|
/** Compact token count: 526, 1.4k, 33k, 76.8k, 1M. */
|
|
@@ -1330,25 +2301,23 @@ function useCountUp(target) {
|
|
|
1330
2301
|
}
|
|
1331
2302
|
/** A token count that counts up as `tokens` grows and holds still when it stops growing. */
|
|
1332
2303
|
function TokenCounter({ tokens, variant = "standalone", className }) {
|
|
2304
|
+
const icons = useIcons();
|
|
1333
2305
|
const shown = useCountUp(tokens);
|
|
2306
|
+
const labels = useLabels();
|
|
1334
2307
|
if (variant === "inline") return /* @__PURE__ */ jsxs("span", {
|
|
1335
2308
|
className: clsx("text-chat-subtle tabular-nums", className),
|
|
1336
2309
|
children: [
|
|
1337
2310
|
"(",
|
|
1338
|
-
formatTokens(shown),
|
|
1339
|
-
"
|
|
2311
|
+
labels.tokenCount(formatTokens(shown)),
|
|
2312
|
+
")"
|
|
1340
2313
|
]
|
|
1341
2314
|
});
|
|
1342
2315
|
return /* @__PURE__ */ jsxs("span", {
|
|
1343
|
-
className: clsx("inline-flex items-center gap-1 font-chat-sans text-
|
|
1344
|
-
children: [
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
}),
|
|
1349
|
-
formatTokens(shown),
|
|
1350
|
-
" tokens"
|
|
1351
|
-
]
|
|
2316
|
+
className: clsx("inline-flex items-center gap-1 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle tabular-nums", className),
|
|
2317
|
+
children: [/* @__PURE__ */ jsx(IconSlot, {
|
|
2318
|
+
className: "[&_svg]:size-3",
|
|
2319
|
+
children: icons.tokens
|
|
2320
|
+
}), labels.tokenCount(formatTokens(shown))]
|
|
1352
2321
|
});
|
|
1353
2322
|
}
|
|
1354
2323
|
// src/components/activity-group.tsx
|
|
@@ -1359,6 +2328,7 @@ function TokenCounter({ tokens, variant = "standalone", className }) {
|
|
|
1359
2328
|
*/
|
|
1360
2329
|
function ActivityGroup({ parts, defaultExpanded = false, className }) {
|
|
1361
2330
|
const views = useToolViews();
|
|
2331
|
+
const labels = useLabels();
|
|
1362
2332
|
const [expanded, setExpanded] = useState(defaultExpanded);
|
|
1363
2333
|
const live = parts.some(isWorking);
|
|
1364
2334
|
const tokens = activityTokens(parts);
|
|
@@ -1380,9 +2350,9 @@ function ActivityGroup({ parts, defaultExpanded = false, className }) {
|
|
|
1380
2350
|
onExpandedChange: setExpanded,
|
|
1381
2351
|
summary: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Live, {
|
|
1382
2352
|
live,
|
|
1383
|
-
children: activityHeading(parts, views)
|
|
2353
|
+
children: activityHeading(parts, views, labels)
|
|
1384
2354
|
}), after] }),
|
|
1385
|
-
summaryClassName: "text-
|
|
2355
|
+
summaryClassName: "text-(length:--chat-activity-font-size)",
|
|
1386
2356
|
panelClassName: "pt-3",
|
|
1387
2357
|
children: /* @__PURE__ */ jsx("div", {
|
|
1388
2358
|
className: "divide-y divide-chat-divider rounded-chat-md border border-chat-divider",
|
|
@@ -1419,7 +2389,7 @@ function SingleStep({ part, live, after, expanded, onExpandedChange }) {
|
|
|
1419
2389
|
live,
|
|
1420
2390
|
children: summary
|
|
1421
2391
|
}), after] }),
|
|
1422
|
-
summaryClassName: "text-
|
|
2392
|
+
summaryClassName: "text-(length:--chat-activity-font-size)",
|
|
1423
2393
|
panelClassName: "pt-3",
|
|
1424
2394
|
children: /* @__PURE__ */ jsx("div", {
|
|
1425
2395
|
className: "rounded-chat-md border border-chat-divider px-3 py-2",
|
|
@@ -1432,7 +2402,7 @@ function Step({ part }) {
|
|
|
1432
2402
|
const live = isWorking(part);
|
|
1433
2403
|
return /* @__PURE__ */ jsx(Disclosure, {
|
|
1434
2404
|
className: "px-3 py-2",
|
|
1435
|
-
summaryClassName: "text-
|
|
2405
|
+
summaryClassName: "text-(length:--chat-activity-font-size)",
|
|
1436
2406
|
panelClassName: "pt-2 pb-1",
|
|
1437
2407
|
summary: /* @__PURE__ */ jsx(Live, {
|
|
1438
2408
|
live,
|
|
@@ -1443,12 +2413,13 @@ function Step({ part }) {
|
|
|
1443
2413
|
}
|
|
1444
2414
|
/** The label line and expanded body of one step. Tool calls go through their registered view. */
|
|
1445
2415
|
function useStepContent(part) {
|
|
2416
|
+
const labels = useLabels();
|
|
1446
2417
|
const view = useToolView(part.type === "tool_call" ? part.name : "");
|
|
1447
2418
|
if (part.type === "thinking") return {
|
|
1448
|
-
summary: activityLabel(part),
|
|
2419
|
+
summary: activityLabel(part, labels),
|
|
1449
2420
|
detail: part.visibility === "hidden" || !part.text ? /* @__PURE__ */ jsx("p", {
|
|
1450
|
-
className: "text-
|
|
1451
|
-
children:
|
|
2421
|
+
className: "text-(length:--chat-ui-font-size) leading-5 text-chat-subtle",
|
|
2422
|
+
children: labels.reasoningHidden
|
|
1452
2423
|
}) : /* @__PURE__ */ jsx("div", {
|
|
1453
2424
|
className: "max-h-96 overflow-y-auto [scrollbar-width:thin]",
|
|
1454
2425
|
children: /* @__PURE__ */ jsx(Markdown, {
|
|
@@ -1463,7 +2434,7 @@ function useStepContent(part) {
|
|
|
1463
2434
|
return {
|
|
1464
2435
|
summary: /* @__PURE__ */ jsxs("span", {
|
|
1465
2436
|
title: view.hint?.(part),
|
|
1466
|
-
children: [activityLabel(part, view), subtitle && /* @__PURE__ */ jsx("span", {
|
|
2437
|
+
children: [activityLabel(part, labels, view), subtitle && /* @__PURE__ */ jsx("span", {
|
|
1467
2438
|
className: "ml-1.5 text-chat-fg",
|
|
1468
2439
|
children: subtitle
|
|
1469
2440
|
})]
|
|
@@ -1473,16 +2444,16 @@ function useStepContent(part) {
|
|
|
1473
2444
|
}
|
|
1474
2445
|
function DefaultDetail({ part }) {
|
|
1475
2446
|
if (part.error) return /* @__PURE__ */ jsx("p", {
|
|
1476
|
-
className: "text-
|
|
2447
|
+
className: "text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
|
|
1477
2448
|
children: part.error
|
|
1478
2449
|
});
|
|
1479
2450
|
const body = part.result ?? part.input ?? part.inputJson;
|
|
1480
2451
|
if (typeof body === "string") return /* @__PURE__ */ jsx("p", {
|
|
1481
|
-
className: "text-
|
|
2452
|
+
className: "text-(length:--chat-activity-font-size) leading-relaxed whitespace-pre-wrap text-chat-muted",
|
|
1482
2453
|
children: body
|
|
1483
2454
|
});
|
|
1484
2455
|
return /* @__PURE__ */ jsx("pre", {
|
|
1485
|
-
className: "max-h-56 overflow-auto font-chat-mono text-
|
|
2456
|
+
className: "max-h-56 overflow-auto font-chat-mono text-(length:--chat-code-font-size) leading-relaxed text-chat-muted",
|
|
1486
2457
|
children: JSON.stringify(body, null, 2)
|
|
1487
2458
|
});
|
|
1488
2459
|
}
|
|
@@ -1493,6 +2464,9 @@ const draftId = () => `att_${Date.now().toString(36)}_${(counter++).toString(36)
|
|
|
1493
2464
|
function useAttachments({ upload } = {}) {
|
|
1494
2465
|
const [attachments, setAttachments] = useState([]);
|
|
1495
2466
|
const uploads = useRef(/* @__PURE__ */ new Map());
|
|
2467
|
+
const labels = useLabels();
|
|
2468
|
+
const labelsRef = useRef(labels);
|
|
2469
|
+
labelsRef.current = labels;
|
|
1496
2470
|
const uploadRef = useRef(upload);
|
|
1497
2471
|
uploadRef.current = upload;
|
|
1498
2472
|
const patch = useCallback((id, fields) => {
|
|
@@ -1526,7 +2500,7 @@ function useAttachments({ upload } = {}) {
|
|
|
1526
2500
|
})).catch((error) => {
|
|
1527
2501
|
if (!controller.signal.aborted) patch(id, {
|
|
1528
2502
|
status: "error",
|
|
1529
|
-
error: error instanceof Error ? error.message :
|
|
2503
|
+
error: error instanceof Error ? error.message : labelsRef.current.uploadFailed
|
|
1530
2504
|
});
|
|
1531
2505
|
}).finally(() => uploads.current.delete(id));
|
|
1532
2506
|
}
|
|
@@ -1535,7 +2509,7 @@ function useAttachments({ upload } = {}) {
|
|
|
1535
2509
|
setAttachments((list) => [...list, {
|
|
1536
2510
|
id: draftId(),
|
|
1537
2511
|
kind: "text",
|
|
1538
|
-
name:
|
|
2512
|
+
name: labelsRef.current.pastedText,
|
|
1539
2513
|
mime: "text/plain",
|
|
1540
2514
|
size: new Blob([text]).size,
|
|
1541
2515
|
text,
|
|
@@ -1581,13 +2555,15 @@ function useAttachments({ upload } = {}) {
|
|
|
1581
2555
|
const extension = (name) => /\.([a-z0-9]{1,5})$/i.exec(name)?.[1]?.toUpperCase();
|
|
1582
2556
|
/** A square card: pasted text preview with a PASTED badge, an image thumbnail, or a file with its type. */
|
|
1583
2557
|
function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", className }) {
|
|
2558
|
+
const icons = useIcons();
|
|
1584
2559
|
const image = a.mime.startsWith("image/") && a.url;
|
|
1585
2560
|
const pasted = a.text !== void 0;
|
|
2561
|
+
const labels = useLabels();
|
|
1586
2562
|
const uploading = a.status === "uploading";
|
|
1587
2563
|
const failed = a.status === "error";
|
|
1588
2564
|
return /* @__PURE__ */ jsxs("div", {
|
|
1589
2565
|
className: clsx("group/card relative shrink-0 rounded-chat-md border bg-chat-bg font-chat-sans", failed ? "border-chat-danger" : "border-chat-border", size === "md" ? "size-30" : "size-24", className),
|
|
1590
|
-
title: failed ? a.error ??
|
|
2566
|
+
title: failed ? a.error ?? labels.uploadFailed : a.name,
|
|
1591
2567
|
children: [
|
|
1592
2568
|
/* @__PURE__ */ jsx("div", {
|
|
1593
2569
|
className: clsx("absolute inset-0 overflow-hidden rounded-[inherit]", uploading && "chat-shimmer-surface"),
|
|
@@ -1600,11 +2576,11 @@ function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", classNam
|
|
|
1600
2576
|
children: a.text
|
|
1601
2577
|
}) : /* @__PURE__ */ jsxs("div", {
|
|
1602
2578
|
className: "flex h-full flex-col gap-1.5 p-2.5",
|
|
1603
|
-
children: [/* @__PURE__ */ jsx(
|
|
1604
|
-
"
|
|
1605
|
-
|
|
2579
|
+
children: [/* @__PURE__ */ jsx(IconSlot, {
|
|
2580
|
+
className: "text-chat-muted [&_svg]:size-5",
|
|
2581
|
+
children: icons.file
|
|
1606
2582
|
}), /* @__PURE__ */ jsx("span", {
|
|
1607
|
-
className: "line-clamp-3 text-
|
|
2583
|
+
className: "line-clamp-3 text-(length:--chat-ui-font-size-sm) leading-4 break-words text-chat-fg",
|
|
1608
2584
|
children: a.name
|
|
1609
2585
|
})]
|
|
1610
2586
|
})
|
|
@@ -1613,29 +2589,29 @@ function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", classNam
|
|
|
1613
2589
|
className: "absolute inset-x-2 bottom-2 flex items-end justify-between gap-1",
|
|
1614
2590
|
children: [/* @__PURE__ */ jsx("span", {
|
|
1615
2591
|
className: clsx("rounded-[0.3rem] border bg-chat-bg px-1.5 py-0.5 text-[0.625rem] leading-none tracking-wide uppercase", failed ? "border-chat-danger text-chat-danger" : "border-chat-border-strong text-chat-muted"),
|
|
1616
|
-
children: failed ?
|
|
2592
|
+
children: failed ? labels.failed : pasted ? labels.pasted : extension(a.name) ?? labels.file
|
|
1617
2593
|
}), pasted && onOpen && /* @__PURE__ */ jsx(IconButton, {
|
|
1618
|
-
label:
|
|
2594
|
+
label: labels.editPastedText,
|
|
1619
2595
|
size: "sm",
|
|
1620
2596
|
onPress: onOpen,
|
|
1621
2597
|
className: "size-6 border border-chat-border-strong bg-chat-bg [&_svg]:size-3.5 [@media(pointer:coarse)]:size-7!",
|
|
1622
|
-
children:
|
|
2598
|
+
children: icons.edit
|
|
1623
2599
|
})]
|
|
1624
2600
|
}),
|
|
1625
2601
|
pasted && onOpen && !onRemove && /* @__PURE__ */ jsx("button", {
|
|
1626
2602
|
type: "button",
|
|
1627
|
-
"aria-label":
|
|
2603
|
+
"aria-label": labels.viewPastedText,
|
|
1628
2604
|
onClick: onOpen,
|
|
1629
2605
|
className: "absolute inset-0 cursor-pointer rounded-[inherit] outline-none focus-visible:ring-2 focus-visible:ring-chat-focus"
|
|
1630
2606
|
}),
|
|
1631
2607
|
onRemove && /* @__PURE__ */ jsx(IconButton, {
|
|
1632
|
-
label:
|
|
2608
|
+
label: labels.removeAttachment(a.name),
|
|
1633
2609
|
variant: "subtle",
|
|
1634
2610
|
size: "sm",
|
|
1635
2611
|
tooltip: false,
|
|
1636
2612
|
onPress: onRemove,
|
|
1637
2613
|
className: clsx("absolute -top-2 -right-2 size-6 rounded-full border border-chat-border-strong [&_svg]:size-3.5 [@media(pointer:coarse)]:size-7!", "opacity-0 group-hover/card:opacity-100 group-focus-within/card:opacity-100 data-focus-visible:opacity-100 [@media(hover:none)]:opacity-100"),
|
|
1638
|
-
children:
|
|
2614
|
+
children: icons.remove
|
|
1639
2615
|
})
|
|
1640
2616
|
]
|
|
1641
2617
|
});
|
|
@@ -1643,9 +2619,10 @@ function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", classNam
|
|
|
1643
2619
|
/** The composer's row of cards above the text. Renders nothing when empty. */
|
|
1644
2620
|
function AttachmentRow({ attachments, onRemove, onEditText }) {
|
|
1645
2621
|
const [editing, setEditing] = useState();
|
|
2622
|
+
const labels = useLabels();
|
|
1646
2623
|
if (attachments.length === 0) return null;
|
|
1647
2624
|
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("ul", {
|
|
1648
|
-
"aria-label":
|
|
2625
|
+
"aria-label": labels.attachments,
|
|
1649
2626
|
className: "flex gap-2.5 overflow-x-auto px-0.5 pt-2.5 pb-1 [scrollbar-width:thin]",
|
|
1650
2627
|
children: attachments.map((a) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(AttachmentCard, {
|
|
1651
2628
|
attachment: {
|
|
@@ -1662,7 +2639,9 @@ function AttachmentRow({ attachments, onRemove, onEditText }) {
|
|
|
1662
2639
|
})] });
|
|
1663
2640
|
}
|
|
1664
2641
|
/** Shows pasted text in a modal, editable when `onSave` is set. */
|
|
1665
|
-
function PastedTextDialog({ text, onClose, onSave, title
|
|
2642
|
+
function PastedTextDialog({ text, onClose, onSave, title }) {
|
|
2643
|
+
const labels = useLabels();
|
|
2644
|
+
title ??= labels.pastedText;
|
|
1666
2645
|
const [draft, setDraft] = useState(text ?? "");
|
|
1667
2646
|
useEffect(() => setDraft(text ?? ""), [text]);
|
|
1668
2647
|
return /* @__PURE__ */ jsxs(Modal, {
|
|
@@ -1674,34 +2653,34 @@ function PastedTextDialog({ text, onClose, onSave, title = "Pasted text" }) {
|
|
|
1674
2653
|
onChange: (e) => setDraft(e.target.value),
|
|
1675
2654
|
readOnly: !onSave,
|
|
1676
2655
|
"aria-label": title,
|
|
1677
|
-
className: "mx-5 min-h-64 flex-1 resize-none rounded-chat-md border border-chat-border bg-chat-bg p-3 font-chat-mono text-
|
|
2656
|
+
className: "mx-5 min-h-64 flex-1 resize-none rounded-chat-md border border-chat-border bg-chat-bg p-3 font-chat-mono text-(length:--chat-ui-font-size) leading-relaxed text-chat-fg outline-none focus:border-chat-border-strong [scrollbar-width:thin]"
|
|
1678
2657
|
}), /* @__PURE__ */ jsxs("div", {
|
|
1679
2658
|
className: "flex justify-end gap-2 px-5 py-4",
|
|
1680
2659
|
children: [/* @__PURE__ */ jsx(Button, {
|
|
1681
2660
|
variant: "subtle",
|
|
1682
2661
|
onPress: onClose,
|
|
1683
|
-
children: onSave ?
|
|
2662
|
+
children: onSave ? labels.cancel : labels.close
|
|
1684
2663
|
}), onSave && /* @__PURE__ */ jsx(Button, {
|
|
1685
2664
|
variant: "primary",
|
|
1686
2665
|
onPress: () => {
|
|
1687
2666
|
onSave(draft);
|
|
1688
2667
|
onClose();
|
|
1689
2668
|
},
|
|
1690
|
-
children:
|
|
2669
|
+
children: labels.save
|
|
1691
2670
|
})]
|
|
1692
2671
|
})]
|
|
1693
2672
|
});
|
|
1694
2673
|
}
|
|
1695
2674
|
// src/components/messages.tsx
|
|
1696
2675
|
/** "just now", "5 minutes ago", "21 hours ago", "yesterday", then a date. */
|
|
1697
|
-
function formatRelativeTime(iso, now = Date.now()) {
|
|
2676
|
+
function formatRelativeTime(iso, now = Date.now(), labels = defaultLabels) {
|
|
1698
2677
|
const seconds = (new Date(iso).getTime() - now) / 1e3;
|
|
1699
|
-
const rtf = new Intl.RelativeTimeFormat(
|
|
1700
|
-
if (Math.abs(seconds) < 60) return
|
|
2678
|
+
const rtf = new Intl.RelativeTimeFormat(labels.locale, { numeric: "auto" });
|
|
2679
|
+
if (Math.abs(seconds) < 60) return labels.justNow;
|
|
1701
2680
|
if (Math.abs(seconds) < 3600) return rtf.format(Math.round(seconds / 60), "minute");
|
|
1702
2681
|
if (Math.abs(seconds) < 86400) return rtf.format(Math.round(seconds / 3600), "hour");
|
|
1703
2682
|
if (Math.abs(seconds) < 604800) return rtf.format(Math.round(seconds / 86400), "day");
|
|
1704
|
-
return new Date(iso).toLocaleDateString(
|
|
2683
|
+
return new Date(iso).toLocaleDateString(labels.locale, {
|
|
1705
2684
|
month: "short",
|
|
1706
2685
|
day: "numeric"
|
|
1707
2686
|
});
|
|
@@ -1711,21 +2690,25 @@ function messageText(message) {
|
|
|
1711
2690
|
return message.parts.map((p) => p.type === "text" ? p.text : p.type === "custom" ? p.text ?? "" : "").filter(Boolean).join("\n\n");
|
|
1712
2691
|
}
|
|
1713
2692
|
function CopyButton({ text }) {
|
|
2693
|
+
const icons = useIcons();
|
|
2694
|
+
const labels = useLabels();
|
|
1714
2695
|
const [copied, setCopied] = useState(false);
|
|
1715
2696
|
return /* @__PURE__ */ jsx(IconButton, {
|
|
1716
|
-
label: copied ?
|
|
2697
|
+
label: copied ? labels.copied : labels.copy,
|
|
1717
2698
|
size: "sm",
|
|
1718
2699
|
onPress: () => {
|
|
1719
2700
|
navigator.clipboard?.writeText(text);
|
|
1720
2701
|
setCopied(true);
|
|
1721
2702
|
setTimeout(() => setCopied(false), 1500);
|
|
1722
2703
|
},
|
|
1723
|
-
children: copied ?
|
|
2704
|
+
children: copied ? icons.copied : icons.copy
|
|
1724
2705
|
});
|
|
1725
2706
|
}
|
|
1726
|
-
const timeClass = "px-1 font-chat-sans text-
|
|
2707
|
+
const timeClass = "px-1 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle";
|
|
1727
2708
|
/** The user's turn: a bubble on the right, with time, retry, edit and copy under it on hover. */
|
|
1728
2709
|
function UserMessage({ message, onRetry, onEdit, actions, className }) {
|
|
2710
|
+
const icons = useIcons();
|
|
2711
|
+
const labels = useLabels();
|
|
1729
2712
|
const text = messageText(message);
|
|
1730
2713
|
const attachments = message.parts.filter((p) => p.type === "attachment");
|
|
1731
2714
|
const [viewing, setViewing] = useState();
|
|
@@ -1733,7 +2716,7 @@ function UserMessage({ message, onRetry, onEdit, actions, className }) {
|
|
|
1733
2716
|
className: clsx("group/message flex flex-col items-end", className),
|
|
1734
2717
|
children: [
|
|
1735
2718
|
attachments.length > 0 && /* @__PURE__ */ jsx("ul", {
|
|
1736
|
-
"aria-label":
|
|
2719
|
+
"aria-label": labels.attachments,
|
|
1737
2720
|
className: "mb-2 flex max-w-[85%] flex-wrap justify-end gap-2",
|
|
1738
2721
|
children: attachments.map((a) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(AttachmentCard, {
|
|
1739
2722
|
attachment: a,
|
|
@@ -1745,28 +2728,25 @@ function UserMessage({ message, onRetry, onEdit, actions, className }) {
|
|
|
1745
2728
|
text: viewing,
|
|
1746
2729
|
onClose: () => setViewing(void 0)
|
|
1747
2730
|
}),
|
|
1748
|
-
text && /* @__PURE__ */ jsx(
|
|
1749
|
-
className: "max-w-[85%] rounded-chat-lg bg-chat-bubble px-4 py-2.5 font-chat-sans text-[0.9375rem] leading-relaxed whitespace-pre-wrap text-chat-fg",
|
|
1750
|
-
children: text
|
|
1751
|
-
}),
|
|
2731
|
+
text && /* @__PURE__ */ jsx(UserBubble, { text }),
|
|
1752
2732
|
/* @__PURE__ */ jsxs("div", {
|
|
1753
2733
|
className: "mt-1 flex items-center gap-0.5 opacity-0 transition-opacity group-hover/message:opacity-100 group-focus-within/message:opacity-100 [@media(hover:none)]:opacity-100",
|
|
1754
2734
|
children: [
|
|
1755
2735
|
/* @__PURE__ */ jsx("span", {
|
|
1756
2736
|
className: timeClass,
|
|
1757
|
-
children: formatRelativeTime(message.createdAt)
|
|
2737
|
+
children: formatRelativeTime(message.createdAt, void 0, labels)
|
|
1758
2738
|
}),
|
|
1759
2739
|
onRetry && /* @__PURE__ */ jsx(IconButton, {
|
|
1760
|
-
label:
|
|
2740
|
+
label: labels.retry,
|
|
1761
2741
|
size: "sm",
|
|
1762
2742
|
onPress: onRetry,
|
|
1763
|
-
children:
|
|
2743
|
+
children: icons.retry
|
|
1764
2744
|
}),
|
|
1765
2745
|
onEdit && /* @__PURE__ */ jsx(IconButton, {
|
|
1766
|
-
label:
|
|
2746
|
+
label: labels.edit,
|
|
1767
2747
|
size: "sm",
|
|
1768
2748
|
onPress: onEdit,
|
|
1769
|
-
children:
|
|
2749
|
+
children: icons.edit
|
|
1770
2750
|
}),
|
|
1771
2751
|
/* @__PURE__ */ jsx(CopyButton, { text }),
|
|
1772
2752
|
actions
|
|
@@ -1775,11 +2755,46 @@ function UserMessage({ message, onRetry, onEdit, actions, className }) {
|
|
|
1775
2755
|
]
|
|
1776
2756
|
});
|
|
1777
2757
|
}
|
|
2758
|
+
/** Taller than this (px), a user message folds to this height with "Show more", like a long paste. */
|
|
2759
|
+
const FOLD_HEIGHT = 240;
|
|
2760
|
+
function UserBubble({ text }) {
|
|
2761
|
+
const body = useRef(null);
|
|
2762
|
+
const [long, setLong] = useState(false);
|
|
2763
|
+
const [open, setOpen] = useState(false);
|
|
2764
|
+
const labels = useLabels();
|
|
2765
|
+
useLayoutEffect(() => {
|
|
2766
|
+
const el = body.current;
|
|
2767
|
+
if (!el) return;
|
|
2768
|
+
const measure = () => setLong(el.scrollHeight > 264);
|
|
2769
|
+
measure();
|
|
2770
|
+
const observer = new ResizeObserver(measure);
|
|
2771
|
+
observer.observe(el);
|
|
2772
|
+
return () => observer.disconnect();
|
|
2773
|
+
}, [text]);
|
|
2774
|
+
const folded = long && !open;
|
|
2775
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2776
|
+
className: "max-w-[85%] rounded-chat-lg bg-chat-bubble px-4 py-2.5 font-chat-sans text-(length:--chat-bubble-font-size) leading-relaxed text-chat-fg",
|
|
2777
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
2778
|
+
ref: body,
|
|
2779
|
+
style: folded ? { maxHeight: FOLD_HEIGHT } : void 0,
|
|
2780
|
+
className: clsx("whitespace-pre-wrap", folded && "overflow-hidden mask-b-from-60% mask-b-to-100%"),
|
|
2781
|
+
children: text
|
|
2782
|
+
}), long && /* @__PURE__ */ jsx("button", {
|
|
2783
|
+
type: "button",
|
|
2784
|
+
"aria-expanded": open,
|
|
2785
|
+
onClick: () => setOpen(!open),
|
|
2786
|
+
className: "mt-1.5 cursor-pointer rounded-chat-sm text-(length:--chat-ui-font-size) leading-5 text-chat-muted outline-none hover:text-chat-fg focus-visible:ring-2 focus-visible:ring-chat-focus",
|
|
2787
|
+
children: open ? labels.showLess : labels.showMore
|
|
2788
|
+
})]
|
|
2789
|
+
});
|
|
2790
|
+
}
|
|
1778
2791
|
/**
|
|
1779
2792
|
* The assistant's turn: activity groups (each with its own token count), markdown, and other parts in
|
|
1780
2793
|
* order. When done, copy / read aloud / feedback / retry and the time sit under it.
|
|
1781
2794
|
*/
|
|
1782
2795
|
function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPart, actions, className }) {
|
|
2796
|
+
const icons = useIcons();
|
|
2797
|
+
const labels = useLabels();
|
|
1783
2798
|
const streaming = message.status === "streaming";
|
|
1784
2799
|
const views = useToolViews();
|
|
1785
2800
|
const blocks = toBlocks(message.parts, (part) => views[part.name]?.Block !== void 0);
|
|
@@ -1787,56 +2802,66 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
|
|
|
1787
2802
|
className: clsx("group/message flex flex-col gap-4", className),
|
|
1788
2803
|
children: [
|
|
1789
2804
|
blocks.map((block) => {
|
|
1790
|
-
if (block.kind === "activity")
|
|
2805
|
+
if (block.kind === "activity") {
|
|
2806
|
+
const cards = block.parts.filter((p) => p.type === "tool_call" && views[p.name]?.Card);
|
|
2807
|
+
if (!cards.length) return /* @__PURE__ */ jsx(ActivityGroup, { parts: block.parts }, block.id);
|
|
2808
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ActivityGroup, { parts: block.parts }), cards.map((p) => {
|
|
2809
|
+
if (p.type !== "tool_call") return null;
|
|
2810
|
+
const Card = views[p.name].Card;
|
|
2811
|
+
return /* @__PURE__ */ jsx(Card, { part: p }, p.id);
|
|
2812
|
+
})] }, block.id);
|
|
2813
|
+
}
|
|
1791
2814
|
if (block.kind === "tool") {
|
|
1792
2815
|
const ToolBlock = views[block.part.name].Block;
|
|
1793
2816
|
return /* @__PURE__ */ jsx(ToolBlock, { part: block.part }, block.id);
|
|
1794
2817
|
}
|
|
1795
2818
|
const part = block.part;
|
|
1796
2819
|
if (part.type === "text") return /* @__PURE__ */ jsx(Markdown, {
|
|
2820
|
+
partId: part.id,
|
|
1797
2821
|
streaming: part.status === "streaming",
|
|
1798
2822
|
children: part.text
|
|
1799
2823
|
}, part.id);
|
|
1800
2824
|
const custom = renderPart?.(part);
|
|
1801
2825
|
if (custom) return /* @__PURE__ */ jsx("div", { children: custom }, part.id);
|
|
2826
|
+
if (part.type === "widget") return /* @__PURE__ */ jsx(WidgetPartView, { part }, part.id);
|
|
1802
2827
|
if (part.type === "custom" && part.text) return /* @__PURE__ */ jsx(Markdown, { children: part.text }, part.id);
|
|
1803
2828
|
return null;
|
|
1804
2829
|
}),
|
|
1805
2830
|
message.status === "error" && /* @__PURE__ */ jsx("p", {
|
|
1806
|
-
className: "font-chat-sans text-
|
|
1807
|
-
children: message.error ??
|
|
2831
|
+
className: "font-chat-sans text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
|
|
2832
|
+
children: message.error ?? labels.somethingWentWrong
|
|
1808
2833
|
}),
|
|
1809
2834
|
streaming ? /* @__PURE__ */ jsx("div", { className: "h-7" }) : /* @__PURE__ */ jsxs("div", {
|
|
1810
2835
|
className: "-ml-1.5 flex h-7 items-center gap-0.5",
|
|
1811
2836
|
children: [
|
|
1812
2837
|
/* @__PURE__ */ jsx(CopyButton, { text: messageText(message) }),
|
|
1813
2838
|
onReadAloud && /* @__PURE__ */ jsx(IconButton, {
|
|
1814
|
-
label:
|
|
2839
|
+
label: labels.readAloud,
|
|
1815
2840
|
size: "sm",
|
|
1816
2841
|
onPress: onReadAloud,
|
|
1817
|
-
children:
|
|
2842
|
+
children: icons.speak
|
|
1818
2843
|
}),
|
|
1819
2844
|
onFeedback && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(IconButton, {
|
|
1820
|
-
label:
|
|
2845
|
+
label: labels.goodResponse,
|
|
1821
2846
|
size: "sm",
|
|
1822
2847
|
onPress: () => onFeedback("good"),
|
|
1823
|
-
children:
|
|
2848
|
+
children: icons.thumbsUp
|
|
1824
2849
|
}), /* @__PURE__ */ jsx(IconButton, {
|
|
1825
|
-
label:
|
|
2850
|
+
label: labels.badResponse,
|
|
1826
2851
|
size: "sm",
|
|
1827
2852
|
onPress: () => onFeedback("bad"),
|
|
1828
|
-
children:
|
|
2853
|
+
children: icons.thumbsDown
|
|
1829
2854
|
})] }),
|
|
1830
2855
|
onRetry && /* @__PURE__ */ jsx(IconButton, {
|
|
1831
|
-
label:
|
|
2856
|
+
label: labels.retry,
|
|
1832
2857
|
size: "sm",
|
|
1833
2858
|
onPress: onRetry,
|
|
1834
|
-
children:
|
|
2859
|
+
children: icons.retry
|
|
1835
2860
|
}),
|
|
1836
2861
|
actions,
|
|
1837
2862
|
/* @__PURE__ */ jsx("span", {
|
|
1838
2863
|
className: timeClass,
|
|
1839
|
-
children: formatRelativeTime(message.createdAt)
|
|
2864
|
+
children: formatRelativeTime(message.createdAt, void 0, labels)
|
|
1840
2865
|
})
|
|
1841
2866
|
]
|
|
1842
2867
|
})
|
|
@@ -1849,7 +2874,9 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
|
|
|
1849
2874
|
* view's `Detail` when it has one, else the input. Replying in the composer instead declines it.
|
|
1850
2875
|
*/
|
|
1851
2876
|
function ApprovalCard({ approval, onRespond, className }) {
|
|
2877
|
+
const icons = useIcons();
|
|
1852
2878
|
const { part } = approval;
|
|
2879
|
+
const labels = useLabels();
|
|
1853
2880
|
const view = useToolView(part.name);
|
|
1854
2881
|
const Detail = view.Detail;
|
|
1855
2882
|
const preview = {
|
|
@@ -1858,21 +2885,17 @@ function ApprovalCard({ approval, onRespond, className }) {
|
|
|
1858
2885
|
};
|
|
1859
2886
|
return /* @__PURE__ */ jsxs("div", {
|
|
1860
2887
|
role: "alertdialog",
|
|
1861
|
-
"aria-label":
|
|
2888
|
+
"aria-label": labels.allowTool(part.name),
|
|
1862
2889
|
className: clsx("flex flex-col gap-3 rounded-chat-lg border border-chat-border-strong bg-chat-surface p-3 font-chat-sans text-chat-fg shadow-chat-popover", className),
|
|
1863
2890
|
children: [
|
|
1864
2891
|
/* @__PURE__ */ jsxs("div", {
|
|
1865
|
-
className: "flex items-center gap-2 text-
|
|
1866
|
-
children: [/* @__PURE__ */ jsx(
|
|
1867
|
-
"
|
|
1868
|
-
|
|
2892
|
+
className: "flex items-center gap-2 text-(length:--chat-ui-font-size) leading-5",
|
|
2893
|
+
children: [/* @__PURE__ */ jsx(IconSlot, {
|
|
2894
|
+
className: "text-chat-accent [&_svg]:size-4",
|
|
2895
|
+
children: icons.approval
|
|
1869
2896
|
}), /* @__PURE__ */ jsx("span", {
|
|
1870
2897
|
className: "min-w-0 flex-1 font-medium",
|
|
1871
|
-
children: view.ask?.(part) ??
|
|
1872
|
-
"Allow ",
|
|
1873
|
-
activityLabel(part, view),
|
|
1874
|
-
"?"
|
|
1875
|
-
] })
|
|
2898
|
+
children: view.ask?.(part) ?? labels.allowAction(activityLabel(part, labels, view))
|
|
1876
2899
|
})]
|
|
1877
2900
|
}),
|
|
1878
2901
|
/* @__PURE__ */ jsx("div", {
|
|
@@ -1889,12 +2912,12 @@ function ApprovalCard({ approval, onRespond, className }) {
|
|
|
1889
2912
|
variant: "ghost",
|
|
1890
2913
|
className: "border border-chat-border-strong",
|
|
1891
2914
|
onPress: () => onRespond(part.id, false),
|
|
1892
|
-
children:
|
|
2915
|
+
children: labels.deny
|
|
1893
2916
|
}), /* @__PURE__ */ jsx(Button, {
|
|
1894
2917
|
variant: "primary",
|
|
1895
2918
|
autoFocus: true,
|
|
1896
2919
|
onPress: () => onRespond(part.id, true),
|
|
1897
|
-
children:
|
|
2920
|
+
children: labels.allow
|
|
1898
2921
|
})]
|
|
1899
2922
|
})
|
|
1900
2923
|
]
|
|
@@ -1907,7 +2930,8 @@ const DEFAULT_COLOR = "var(--chat-subtle)";
|
|
|
1907
2930
|
* What fills the context window: a usage line, a segmented bar, one row per category with its share, and
|
|
1908
2931
|
* expandable lists (each MCP tool, each memory file). Deferred categories are listed but not counted.
|
|
1909
2932
|
*/
|
|
1910
|
-
function ContextPanel({ usage, title
|
|
2933
|
+
function ContextPanel({ usage, title, defaultExpanded = true, className }) {
|
|
2934
|
+
const labels = useLabels();
|
|
1911
2935
|
const counted = usage.categories.filter((c) => !c.deferred);
|
|
1912
2936
|
const deferred = usage.categories.filter((c) => c.deferred);
|
|
1913
2937
|
const used = counted.reduce((sum, c) => sum + (c.reserved ? 0 : c.tokens), 0);
|
|
@@ -1915,16 +2939,16 @@ function ContextPanel({ usage, title = "Context", defaultExpanded = true, classN
|
|
|
1915
2939
|
const free = Math.max(usage.limit - used - reserved, 0);
|
|
1916
2940
|
const share = (tokens) => formatShare(tokens / usage.limit);
|
|
1917
2941
|
return /* @__PURE__ */ jsxs(Disclosure, {
|
|
1918
|
-
summary: title,
|
|
2942
|
+
summary: title ?? labels.context,
|
|
1919
2943
|
defaultExpanded,
|
|
1920
2944
|
className: clsx("font-chat-sans text-chat-fg", className),
|
|
1921
|
-
summaryClassName: "text-
|
|
2945
|
+
summaryClassName: "text-(length:--chat-activity-font-size) text-chat-fg",
|
|
1922
2946
|
panelClassName: "pt-3",
|
|
1923
2947
|
children: [
|
|
1924
2948
|
/* @__PURE__ */ jsxs("div", {
|
|
1925
|
-
className: "mb-2 flex items-baseline justify-between text-
|
|
1926
|
-
children: [/* @__PURE__ */
|
|
1927
|
-
className: "text-
|
|
2949
|
+
className: "mb-2 flex items-baseline justify-between text-(length:--chat-activity-font-size)",
|
|
2950
|
+
children: [/* @__PURE__ */ jsx("span", { children: labels.percentUsed(Math.round(used / usage.limit * 100)) }), /* @__PURE__ */ jsxs("span", {
|
|
2951
|
+
className: "text-(length:--chat-ui-font-size) leading-5 text-chat-muted tabular-nums",
|
|
1928
2952
|
children: [
|
|
1929
2953
|
formatTokens(used),
|
|
1930
2954
|
" / ",
|
|
@@ -1934,7 +2958,7 @@ function ContextPanel({ usage, title = "Context", defaultExpanded = true, classN
|
|
|
1934
2958
|
}),
|
|
1935
2959
|
/* @__PURE__ */ jsx("div", {
|
|
1936
2960
|
role: "meter",
|
|
1937
|
-
"aria-label":
|
|
2961
|
+
"aria-label": labels.contextUsed,
|
|
1938
2962
|
"aria-valuemin": 0,
|
|
1939
2963
|
"aria-valuemax": usage.limit,
|
|
1940
2964
|
"aria-valuenow": used,
|
|
@@ -1956,7 +2980,7 @@ function ContextPanel({ usage, title = "Context", defaultExpanded = true, classN
|
|
|
1956
2980
|
}, c.id)), /* @__PURE__ */ jsx(CategoryRow, {
|
|
1957
2981
|
category: {
|
|
1958
2982
|
id: "free",
|
|
1959
|
-
label:
|
|
2983
|
+
label: labels.freeSpace,
|
|
1960
2984
|
tokens: free,
|
|
1961
2985
|
color: FREE_COLOR
|
|
1962
2986
|
},
|
|
@@ -1987,7 +3011,7 @@ function CategoryRow({ category, share }) {
|
|
|
1987
3011
|
}) })
|
|
1988
3012
|
}) : category.label;
|
|
1989
3013
|
return /* @__PURE__ */ jsxs("li", {
|
|
1990
|
-
className: "flex items-center gap-2.5 text-
|
|
3014
|
+
className: "flex items-center gap-2.5 text-(length:--chat-ui-font-size) leading-5",
|
|
1991
3015
|
children: [
|
|
1992
3016
|
/* @__PURE__ */ jsx("span", {
|
|
1993
3017
|
"aria-hidden": true,
|
|
@@ -2013,16 +3037,16 @@ function GroupList({ group }) {
|
|
|
2013
3037
|
return /* @__PURE__ */ jsx(Disclosure, {
|
|
2014
3038
|
summary: group.label,
|
|
2015
3039
|
trailing: /* @__PURE__ */ jsx("span", {
|
|
2016
|
-
className: "text-
|
|
3040
|
+
className: "text-(length:--chat-ui-font-size) leading-5 text-chat-subtle tabular-nums",
|
|
2017
3041
|
children: group.items.length
|
|
2018
3042
|
}),
|
|
2019
3043
|
indicator: "chevron",
|
|
2020
|
-
summaryClassName: "text-
|
|
3044
|
+
summaryClassName: "text-(length:--chat-ui-font-size) leading-5",
|
|
2021
3045
|
panelClassName: "pt-1.5",
|
|
2022
3046
|
children: /* @__PURE__ */ jsx("ul", {
|
|
2023
3047
|
className: "max-h-72 space-y-1 overflow-y-auto pl-6 [scrollbar-color:var(--chat-border-strong)_transparent] [scrollbar-width:thin]",
|
|
2024
3048
|
children: group.items.map((item) => /* @__PURE__ */ jsxs("li", {
|
|
2025
|
-
className: "flex items-center gap-2 text-
|
|
3049
|
+
className: "flex items-center gap-2 text-(length:--chat-ui-font-size) leading-5 text-chat-muted",
|
|
2026
3050
|
children: [/* @__PURE__ */ jsx("span", {
|
|
2027
3051
|
className: "min-w-0 flex-1 truncate",
|
|
2028
3052
|
children: item.label
|
|
@@ -2041,7 +3065,9 @@ function formatShare(fraction) {
|
|
|
2041
3065
|
/** Touch keyboards have no easy Shift+Enter, so there Enter adds a line and the button sends. */
|
|
2042
3066
|
const coarsePointer = () => typeof window !== "undefined" && window.matchMedia?.("(pointer: coarse)").matches;
|
|
2043
3067
|
/** The chat box: growing textarea, toolbar slots, send / stop. Enter sends, Shift+Enter adds a line. */
|
|
2044
|
-
function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
|
|
3068
|
+
function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder, variant = "docked", leading, trailing, above, onFiles, onLongPaste, longPasteChars = 2e3, allowEmpty = false, submitDisabled = false, isDisabled = false, autoFocus, className }) {
|
|
3069
|
+
const icons = useIcons();
|
|
3070
|
+
const labels = useLabels();
|
|
2045
3071
|
const [own, setOwn] = useState("");
|
|
2046
3072
|
const text = value ?? own;
|
|
2047
3073
|
const setText = (next) => {
|
|
@@ -2114,18 +3140,18 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
|
|
|
2114
3140
|
};
|
|
2115
3141
|
const compact = variant === "compact";
|
|
2116
3142
|
const action = busy ? /* @__PURE__ */ jsx(IconButton, {
|
|
2117
|
-
label:
|
|
3143
|
+
label: labels.stop,
|
|
2118
3144
|
variant: "primary",
|
|
2119
3145
|
onPress: onStop,
|
|
2120
3146
|
className: "rounded-chat-md [&_svg]:size-3.5 [&_svg]:fill-current",
|
|
2121
|
-
children:
|
|
3147
|
+
children: icons.stop
|
|
2122
3148
|
}) : /* @__PURE__ */ jsx(IconButton, {
|
|
2123
|
-
label:
|
|
3149
|
+
label: labels.send,
|
|
2124
3150
|
variant: "primary",
|
|
2125
3151
|
onPress: submit,
|
|
2126
3152
|
isDisabled: !canSend,
|
|
2127
3153
|
className: "rounded-chat-md",
|
|
2128
|
-
children:
|
|
3154
|
+
children: icons.send
|
|
2129
3155
|
});
|
|
2130
3156
|
return /* @__PURE__ */ jsxs("div", {
|
|
2131
3157
|
className: clsx("flex flex-col rounded-[1.25rem] border border-chat-border bg-chat-surface font-chat-sans shadow-chat-popover", "focus-within:border-chat-border-strong", dragging && "border-chat-accent! border-dashed", compact ? "gap-1 p-2" : "gap-2 p-3", className),
|
|
@@ -2147,8 +3173,8 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
|
|
|
2147
3173
|
onChange: (e) => setText(e.target.value),
|
|
2148
3174
|
onKeyDown,
|
|
2149
3175
|
onPaste,
|
|
2150
|
-
placeholder,
|
|
2151
|
-
"aria-label":
|
|
3176
|
+
placeholder: placeholder ?? labels.replyPlaceholder,
|
|
3177
|
+
"aria-label": labels.message,
|
|
2152
3178
|
disabled: isDisabled,
|
|
2153
3179
|
autoFocus,
|
|
2154
3180
|
className: clsx("w-full resize-none bg-transparent px-1.5 text-chat-fg outline-none placeholder:text-chat-subtle", "text-base leading-relaxed [scrollbar-width:thin]", compact ? "max-h-[min(10rem,30cqh)] py-1" : variant === "centered" ? "max-h-[min(24rem,45cqh)] min-h-12 pt-1" : "max-h-[min(18rem,35cqh)] min-h-6 pt-1")
|
|
@@ -2172,10 +3198,12 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
|
|
|
2172
3198
|
});
|
|
2173
3199
|
}
|
|
2174
3200
|
/** The `+` button that opens the attach menu above the composer. */
|
|
2175
|
-
function AttachButton({ children, label
|
|
3201
|
+
function AttachButton({ children, label }) {
|
|
3202
|
+
const icons = useIcons();
|
|
3203
|
+
const labels = useLabels();
|
|
2176
3204
|
return /* @__PURE__ */ jsxs(MenuTrigger, { children: [/* @__PURE__ */ jsx(IconButton, {
|
|
2177
|
-
label,
|
|
2178
|
-
children:
|
|
3205
|
+
label: label ?? labels.addFiles,
|
|
3206
|
+
children: icons.attach
|
|
2179
3207
|
}), /* @__PURE__ */ jsx(Popover, {
|
|
2180
3208
|
placement: "top start",
|
|
2181
3209
|
children
|
|
@@ -2183,7 +3211,9 @@ function AttachButton({ children, label = "Add files and more" }) {
|
|
|
2183
3211
|
}
|
|
2184
3212
|
// src/components/model-picker.tsx
|
|
2185
3213
|
/** "Opus 5.5 High ⌄": the models, an Effort submenu for models that have levels, and More models. */
|
|
2186
|
-
function ModelPicker({ models, model, onModelChange, effort, onEffortChange, effortNote
|
|
3214
|
+
function ModelPicker({ models, model, onModelChange, effort, onEffortChange, effortNote, footnote, className }) {
|
|
3215
|
+
const icons = useIcons();
|
|
3216
|
+
const labels = useLabels();
|
|
2187
3217
|
const current = models.find((m) => m.id === model);
|
|
2188
3218
|
const efforts = current?.efforts ?? [];
|
|
2189
3219
|
const currentEffort = efforts.find((e) => e.id === (effort ?? current?.defaultEffort));
|
|
@@ -2202,7 +3232,7 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
|
|
|
2202
3232
|
};
|
|
2203
3233
|
return /* @__PURE__ */ jsxs(MenuTrigger, { children: [/* @__PURE__ */ jsxs(Button, {
|
|
2204
3234
|
size: "sm",
|
|
2205
|
-
"aria-label":
|
|
3235
|
+
"aria-label": labels.modelButton(current?.label ?? model),
|
|
2206
3236
|
className: clsx("text-chat-muted data-hovered:text-chat-fg", className),
|
|
2207
3237
|
children: [
|
|
2208
3238
|
/* @__PURE__ */ jsx("span", {
|
|
@@ -2213,16 +3243,16 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
|
|
|
2213
3243
|
className: "chat-picker-detail text-chat-muted",
|
|
2214
3244
|
children: currentEffort.label
|
|
2215
3245
|
}),
|
|
2216
|
-
/* @__PURE__ */ jsx(
|
|
2217
|
-
"
|
|
2218
|
-
|
|
3246
|
+
/* @__PURE__ */ jsx(IconSlot, {
|
|
3247
|
+
className: "[&_svg]:size-3.5",
|
|
3248
|
+
children: icons.dropdown
|
|
2219
3249
|
})
|
|
2220
3250
|
]
|
|
2221
3251
|
}), /* @__PURE__ */ jsxs(Popover, {
|
|
2222
3252
|
placement: "top end",
|
|
2223
3253
|
className: "w-72",
|
|
2224
3254
|
children: [/* @__PURE__ */ jsxs(Menu, {
|
|
2225
|
-
"aria-label":
|
|
3255
|
+
"aria-label": labels.model,
|
|
2226
3256
|
children: [
|
|
2227
3257
|
/* @__PURE__ */ jsx(MenuSection, {
|
|
2228
3258
|
selectionMode: "single",
|
|
@@ -2234,12 +3264,12 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
|
|
|
2234
3264
|
efforts.length > 0 && onEffortChange && /* @__PURE__ */ jsxs(SubmenuTrigger, { children: [/* @__PURE__ */ jsx(MenuItem, {
|
|
2235
3265
|
id: "__effort",
|
|
2236
3266
|
detail: currentEffort?.label,
|
|
2237
|
-
children:
|
|
3267
|
+
children: labels.effort
|
|
2238
3268
|
}), /* @__PURE__ */ jsxs(Popover, {
|
|
2239
3269
|
placement: "end top",
|
|
2240
3270
|
className: "w-80",
|
|
2241
|
-
children: [/* @__PURE__ */ jsx(MenuNote, { children: effortNote }), /* @__PURE__ */ jsx(Menu, {
|
|
2242
|
-
"aria-label":
|
|
3271
|
+
children: [/* @__PURE__ */ jsx(MenuNote, { children: effortNote ?? labels.effortNote }), /* @__PURE__ */ jsx(Menu, {
|
|
3272
|
+
"aria-label": labels.effort,
|
|
2243
3273
|
selectionMode: "single",
|
|
2244
3274
|
selectedKeys: currentEffort ? [currentEffort.id] : [],
|
|
2245
3275
|
onSelectionChange: (keys) => {
|
|
@@ -2254,10 +3284,10 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
|
|
|
2254
3284
|
className: "flex items-center gap-2",
|
|
2255
3285
|
children: [
|
|
2256
3286
|
e.label,
|
|
2257
|
-
e.recommended && /* @__PURE__ */ jsx(Badge, { children:
|
|
3287
|
+
e.recommended && /* @__PURE__ */ jsx(Badge, { children: labels.recommended }),
|
|
2258
3288
|
e.warning && /* @__PURE__ */ jsx(Badge, {
|
|
2259
3289
|
tone: "warning",
|
|
2260
|
-
icon:
|
|
3290
|
+
icon: icons.warning,
|
|
2261
3291
|
children: e.warning
|
|
2262
3292
|
})
|
|
2263
3293
|
]
|
|
@@ -2267,12 +3297,12 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
|
|
|
2267
3297
|
})] }),
|
|
2268
3298
|
more.length > 0 && /* @__PURE__ */ jsxs(SubmenuTrigger, { children: [/* @__PURE__ */ jsx(MenuItem, {
|
|
2269
3299
|
id: "__more",
|
|
2270
|
-
children:
|
|
3300
|
+
children: labels.moreModels
|
|
2271
3301
|
}), /* @__PURE__ */ jsx(Popover, {
|
|
2272
3302
|
placement: "end top",
|
|
2273
3303
|
className: "w-72",
|
|
2274
3304
|
children: /* @__PURE__ */ jsx(Menu, {
|
|
2275
|
-
"aria-label":
|
|
3305
|
+
"aria-label": labels.moreModels,
|
|
2276
3306
|
selectionMode: "single",
|
|
2277
3307
|
selectedKeys: [model],
|
|
2278
3308
|
onSelectionChange: pick,
|
|
@@ -2281,7 +3311,7 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
|
|
|
2281
3311
|
})] })
|
|
2282
3312
|
]
|
|
2283
3313
|
}), footnote && /* @__PURE__ */ jsx(MenuNote, {
|
|
2284
|
-
className: "border-t border-chat-popover-border text-
|
|
3314
|
+
className: "border-t border-chat-popover-border text-(length:--chat-ui-font-size-sm) leading-4",
|
|
2285
3315
|
children: footnote
|
|
2286
3316
|
})]
|
|
2287
3317
|
})] });
|
|
@@ -2293,10 +3323,12 @@ const STICK_SLOP = 48;
|
|
|
2293
3323
|
* The scrolling conversation. It follows a streaming reply while the reader is at the bottom and leaves
|
|
2294
3324
|
* them alone once they scroll up, with a button to jump back. Sending a message always jumps down.
|
|
2295
3325
|
*/
|
|
2296
|
-
function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, footer, className }) {
|
|
3326
|
+
function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, header, footer, start = "bottom", className }) {
|
|
3327
|
+
const icons = useIcons();
|
|
2297
3328
|
const scroller = useRef(null);
|
|
2298
3329
|
const content = useRef(null);
|
|
2299
|
-
const stuck = useRef(
|
|
3330
|
+
const stuck = useRef(start === "bottom");
|
|
3331
|
+
const labels = useLabels();
|
|
2300
3332
|
const [atBottom, setAtBottom] = useState(true);
|
|
2301
3333
|
const toBottom = useCallback((behavior = "auto") => {
|
|
2302
3334
|
const el = scroller.current;
|
|
@@ -2322,7 +3354,13 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, footer
|
|
|
2322
3354
|
return () => observer.disconnect();
|
|
2323
3355
|
}, [toBottom]);
|
|
2324
3356
|
const lastUser = messages.findLast((m) => m.role === "user")?.id;
|
|
3357
|
+
const jumpedFor = useRef(start === "top" ? lastUser : null);
|
|
2325
3358
|
useLayoutEffect(() => {
|
|
3359
|
+
if (lastUser === jumpedFor.current) {
|
|
3360
|
+
onScroll();
|
|
3361
|
+
return;
|
|
3362
|
+
}
|
|
3363
|
+
jumpedFor.current = lastUser;
|
|
2326
3364
|
stuck.current = true;
|
|
2327
3365
|
setAtBottom(true);
|
|
2328
3366
|
toBottom();
|
|
@@ -2336,11 +3374,11 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, footer
|
|
|
2336
3374
|
children: /* @__PURE__ */ jsxs("div", {
|
|
2337
3375
|
ref: content,
|
|
2338
3376
|
className: "flex min-h-full flex-col",
|
|
2339
|
-
children: [/* @__PURE__ */
|
|
3377
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2340
3378
|
role: "log",
|
|
2341
|
-
"aria-label":
|
|
3379
|
+
"aria-label": labels.conversation,
|
|
2342
3380
|
className: "mx-auto flex w-full max-w-[calc(var(--chat-content-width)+2*var(--chat-gutter,var(--chat-content-padding)))] flex-1 flex-col gap-6 px-[var(--chat-gutter,var(--chat-content-padding))] pt-6 pb-4",
|
|
2343
|
-
children: messages.map((m) => m.role === "user" ? /* @__PURE__ */ jsx(UserMessage, {
|
|
3381
|
+
children: [header, messages.map((m) => m.role === "user" ? /* @__PURE__ */ jsx(UserMessage, {
|
|
2344
3382
|
message: m,
|
|
2345
3383
|
...onEdit && { onEdit: () => onEdit(m.id) }
|
|
2346
3384
|
}, m.id) : /* @__PURE__ */ jsx(AssistantMessage, {
|
|
@@ -2348,27 +3386,28 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, footer
|
|
|
2348
3386
|
...onRetry && { onRetry: () => onRetry(m.id) },
|
|
2349
3387
|
...onFeedback && { onFeedback: (f) => onFeedback(m.id, f) },
|
|
2350
3388
|
...renderPart && { renderPart }
|
|
2351
|
-
}, m.id))
|
|
3389
|
+
}, m.id))]
|
|
2352
3390
|
}), footer && /* @__PURE__ */ jsx("div", {
|
|
2353
3391
|
className: "sticky bottom-0",
|
|
2354
3392
|
children: footer
|
|
2355
3393
|
})]
|
|
2356
3394
|
})
|
|
2357
3395
|
}), !atBottom && /* @__PURE__ */ jsx(IconButton, {
|
|
2358
|
-
label:
|
|
3396
|
+
label: labels.scrollToBottom,
|
|
2359
3397
|
variant: "subtle",
|
|
2360
3398
|
onPress: () => {
|
|
2361
3399
|
stuck.current = true;
|
|
2362
3400
|
toBottom("smooth");
|
|
2363
3401
|
},
|
|
2364
3402
|
className: "absolute bottom-[min(10rem,40%)] left-1/2 -translate-x-1/2 rounded-full border border-chat-border shadow-chat-popover",
|
|
2365
|
-
children:
|
|
3403
|
+
children: icons.scrollToBottom
|
|
2366
3404
|
})]
|
|
2367
3405
|
});
|
|
2368
3406
|
}
|
|
2369
3407
|
// src/components/chat-header.tsx
|
|
2370
3408
|
/** The bar above a chat: breadcrumb and title with its menu, actions on the right. */
|
|
2371
3409
|
function ChatHeader({ title, menu, leading, actions, className }) {
|
|
3410
|
+
const icons = useIcons();
|
|
2372
3411
|
return /* @__PURE__ */ jsxs("header", {
|
|
2373
3412
|
className: clsx("flex h-12 shrink-0 items-center gap-1 px-3 font-chat-sans", className),
|
|
2374
3413
|
children: [
|
|
@@ -2379,15 +3418,15 @@ function ChatHeader({ title, menu, leading, actions, className }) {
|
|
|
2379
3418
|
children: [/* @__PURE__ */ jsx("span", {
|
|
2380
3419
|
className: "truncate",
|
|
2381
3420
|
children: title
|
|
2382
|
-
}), /* @__PURE__ */ jsx(
|
|
2383
|
-
"
|
|
2384
|
-
|
|
3421
|
+
}), /* @__PURE__ */ jsx(IconSlot, {
|
|
3422
|
+
className: "text-chat-muted [&_svg]:size-3.5",
|
|
3423
|
+
children: icons.dropdown
|
|
2385
3424
|
})]
|
|
2386
3425
|
}), /* @__PURE__ */ jsx(Popover, {
|
|
2387
3426
|
placement: "bottom start",
|
|
2388
3427
|
children: menu
|
|
2389
3428
|
})] }) : /* @__PURE__ */ jsx("h1", {
|
|
2390
|
-
className: "min-w-0 truncate px-2 text-
|
|
3429
|
+
className: "min-w-0 truncate px-2 text-(length:--chat-ui-font-size) leading-5 text-chat-fg",
|
|
2391
3430
|
children: title
|
|
2392
3431
|
}),
|
|
2393
3432
|
/* @__PURE__ */ jsx("div", {
|
|
@@ -2399,6 +3438,8 @@ function ChatHeader({ title, menu, leading, actions, className }) {
|
|
|
2399
3438
|
}
|
|
2400
3439
|
/** Pin, Rename, Add to project (searchable), Share, Delete. Each shows only when its handler is set. */
|
|
2401
3440
|
function ChatTitleMenu({ pinned, onPin, onRename, projects, onAddToProject, onCreateProject, onShare, onDelete, children }) {
|
|
3441
|
+
const icons = useIcons();
|
|
3442
|
+
const labels = useLabels();
|
|
2402
3443
|
const act = (key) => {
|
|
2403
3444
|
if (key === "pin") onPin?.();
|
|
2404
3445
|
else if (key === "rename") onRename?.();
|
|
@@ -2406,39 +3447,39 @@ function ChatTitleMenu({ pinned, onPin, onRename, projects, onAddToProject, onCr
|
|
|
2406
3447
|
else if (key === "delete") onDelete?.();
|
|
2407
3448
|
};
|
|
2408
3449
|
return /* @__PURE__ */ jsxs(Menu, {
|
|
2409
|
-
"aria-label":
|
|
3450
|
+
"aria-label": labels.chatActions,
|
|
2410
3451
|
className: "min-w-44",
|
|
2411
3452
|
onAction: act,
|
|
2412
3453
|
children: [
|
|
2413
3454
|
onPin && /* @__PURE__ */ jsx(MenuItem, {
|
|
2414
3455
|
id: "pin",
|
|
2415
|
-
icon: pinned ?
|
|
3456
|
+
icon: pinned ? icons.unpin : icons.pin,
|
|
2416
3457
|
shortcut: "P",
|
|
2417
|
-
children: pinned ?
|
|
3458
|
+
children: pinned ? labels.unpin : labels.pin
|
|
2418
3459
|
}),
|
|
2419
3460
|
onRename && /* @__PURE__ */ jsx(MenuItem, {
|
|
2420
3461
|
id: "rename",
|
|
2421
|
-
icon:
|
|
3462
|
+
icon: icons.rename,
|
|
2422
3463
|
shortcut: "R",
|
|
2423
|
-
children:
|
|
3464
|
+
children: labels.rename
|
|
2424
3465
|
}),
|
|
2425
3466
|
onAddToProject && /* @__PURE__ */ jsxs(SubmenuTrigger, { children: [/* @__PURE__ */ jsx(MenuItem, {
|
|
2426
3467
|
id: "project",
|
|
2427
|
-
icon:
|
|
2428
|
-
children:
|
|
3468
|
+
icon: icons.addToProject,
|
|
3469
|
+
children: labels.addToProject
|
|
2429
3470
|
}), /* @__PURE__ */ jsx(Popover, {
|
|
2430
3471
|
placement: "end top",
|
|
2431
3472
|
className: "w-60",
|
|
2432
3473
|
children: /* @__PURE__ */ jsx(MenuSearch, {
|
|
2433
|
-
placeholder:
|
|
3474
|
+
placeholder: labels.searchProjects,
|
|
2434
3475
|
children: /* @__PURE__ */ jsxs(Menu, {
|
|
2435
|
-
"aria-label":
|
|
3476
|
+
"aria-label": labels.projects,
|
|
2436
3477
|
className: "max-h-72",
|
|
2437
3478
|
onAction: (key) => key === "__new" ? onCreateProject?.() : onAddToProject(String(key)),
|
|
2438
3479
|
children: [onCreateProject && /* @__PURE__ */ jsx(MenuItem, {
|
|
2439
3480
|
id: "__new",
|
|
2440
|
-
icon:
|
|
2441
|
-
children:
|
|
3481
|
+
icon: icons.add,
|
|
3482
|
+
children: labels.newProject
|
|
2442
3483
|
}), (projects ?? []).map((p) => /* @__PURE__ */ jsx(MenuItem, {
|
|
2443
3484
|
id: p.id,
|
|
2444
3485
|
children: p.name
|
|
@@ -2448,28 +3489,50 @@ function ChatTitleMenu({ pinned, onPin, onRename, projects, onAddToProject, onCr
|
|
|
2448
3489
|
})] }),
|
|
2449
3490
|
onShare && /* @__PURE__ */ jsx(MenuItem, {
|
|
2450
3491
|
id: "share",
|
|
2451
|
-
icon:
|
|
2452
|
-
children:
|
|
3492
|
+
icon: icons.share,
|
|
3493
|
+
children: labels.shareChat
|
|
2453
3494
|
}),
|
|
2454
3495
|
children,
|
|
2455
3496
|
onDelete && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuSeparator, {}), /* @__PURE__ */ jsx(MenuItem, {
|
|
2456
3497
|
id: "delete",
|
|
2457
|
-
icon:
|
|
3498
|
+
icon: icons.delete,
|
|
2458
3499
|
shortcut: "D",
|
|
2459
3500
|
tone: "danger",
|
|
2460
|
-
children:
|
|
3501
|
+
children: labels.delete
|
|
2461
3502
|
})] })
|
|
2462
3503
|
]
|
|
2463
3504
|
});
|
|
2464
3505
|
}
|
|
3506
|
+
// src/components/chat-notice.tsx
|
|
3507
|
+
/** A bordered note in the conversation column: "Shared by Alex. This is a copy of a chat…" with actions under it. */
|
|
3508
|
+
function ChatNotice({ children, actions, icon, className }) {
|
|
3509
|
+
const icons = useIcons();
|
|
3510
|
+
const shown = icon === void 0 ? icons.info : icon;
|
|
3511
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3512
|
+
role: "note",
|
|
3513
|
+
className: clsx("flex gap-3 rounded-chat-lg border border-(--chat-notice-border,var(--chat-border)) bg-(--chat-notice-bg,transparent) px-4 py-3.5 font-chat-sans text-(length:--chat-ui-font-size) leading-relaxed text-chat-fg", className),
|
|
3514
|
+
children: [shown && /* @__PURE__ */ jsx("span", {
|
|
3515
|
+
className: "mt-0.5 shrink-0 text-chat-muted [&_svg]:size-4",
|
|
3516
|
+
children: shown
|
|
3517
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
3518
|
+
className: "min-w-0 flex-1",
|
|
3519
|
+
children: [/* @__PURE__ */ jsx("div", { children }), actions && /* @__PURE__ */ jsx("div", {
|
|
3520
|
+
className: "mt-2.5 flex flex-wrap gap-2",
|
|
3521
|
+
children: actions
|
|
3522
|
+
})]
|
|
3523
|
+
})]
|
|
3524
|
+
});
|
|
3525
|
+
}
|
|
2465
3526
|
// src/components/chat-list.tsx
|
|
2466
3527
|
const isProject = (item) => "type" in item && item.type === "project";
|
|
2467
|
-
const rowClass = clsx("group/row relative flex h-8 w-full min-w-0 cursor-pointer items-center gap-2 rounded-chat-sm px-2 text-left font-chat-sans text-
|
|
3528
|
+
const rowClass = clsx("group/row relative flex h-8 w-full min-w-0 cursor-pointer items-center gap-2 rounded-chat-sm px-2 text-left font-chat-sans text-(length:--chat-ui-font-size) leading-5 text-chat-fg outline-none", "data-hovered:bg-chat-hover data-pressed:bg-chat-selected data-focus-visible:ring-2 data-focus-visible:ring-chat-focus data-focus-visible:ring-inset", "[@media(pointer:coarse)]:h-10");
|
|
2468
3529
|
/**
|
|
2469
3530
|
* The chat picker: sections of chats and projects, collapsible, with an active row, running and unread
|
|
2470
3531
|
* markers and a per-row menu. Usable on its own (in a host's own sidebar) or inside `Sidebar`.
|
|
2471
3532
|
*/
|
|
2472
3533
|
function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }) {
|
|
3534
|
+
const icons = useIcons();
|
|
3535
|
+
const labels = useLabels();
|
|
2473
3536
|
const row = (chat, nested = false) => /* @__PURE__ */ jsx(ChatRow, {
|
|
2474
3537
|
chat,
|
|
2475
3538
|
active: chat.id === activeId,
|
|
@@ -2483,13 +3546,13 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
|
|
|
2483
3546
|
row
|
|
2484
3547
|
}, item.id) : row(item));
|
|
2485
3548
|
return /* @__PURE__ */ jsx("nav", {
|
|
2486
|
-
"aria-label": props["aria-label"] ??
|
|
3549
|
+
"aria-label": props["aria-label"] ?? labels.chats,
|
|
2487
3550
|
className: clsx("flex flex-col gap-3", className),
|
|
2488
3551
|
children: sections.map((section) => section.title ? /* @__PURE__ */ jsxs(Disclosure$1, {
|
|
2489
3552
|
defaultExpanded: section.defaultExpanded ?? true,
|
|
2490
3553
|
className: "group/section",
|
|
2491
3554
|
children: [/* @__PURE__ */ jsxs(Heading, {
|
|
2492
|
-
className: "m-0 flex h-7 items-center gap-1 pr-1 pl-2 font-chat-sans text-
|
|
3555
|
+
className: "m-0 flex h-7 items-center gap-1 pr-1 pl-2 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 font-normal text-chat-subtle",
|
|
2493
3556
|
children: [
|
|
2494
3557
|
/* @__PURE__ */ jsxs(Button$1, {
|
|
2495
3558
|
slot: "trigger",
|
|
@@ -2497,9 +3560,9 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
|
|
|
2497
3560
|
children: [/* @__PURE__ */ jsx("span", {
|
|
2498
3561
|
className: "truncate",
|
|
2499
3562
|
children: section.title
|
|
2500
|
-
}), /* @__PURE__ */ jsx(
|
|
2501
|
-
"aria-
|
|
2502
|
-
|
|
3563
|
+
}), /* @__PURE__ */ jsx(IconSlot, {
|
|
3564
|
+
className: "opacity-0 transition group-hover/section:opacity-100 group-aria-expanded/trigger:rotate-90 group-data-focus-visible/trigger:opacity-100 [@media(hover:none)]:opacity-100 [&_svg]:size-3",
|
|
3565
|
+
children: icons.expand
|
|
2503
3566
|
})]
|
|
2504
3567
|
}),
|
|
2505
3568
|
section.badge,
|
|
@@ -2514,7 +3577,7 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
|
|
|
2514
3577
|
}), section.onViewAll && /* @__PURE__ */ jsx(Button$1, {
|
|
2515
3578
|
onPress: section.onViewAll,
|
|
2516
3579
|
className: clsx(rowClass, "text-chat-muted"),
|
|
2517
|
-
children:
|
|
3580
|
+
children: labels.viewAll
|
|
2518
3581
|
})] })]
|
|
2519
3582
|
}, section.id) : /* @__PURE__ */ jsx("ul", {
|
|
2520
3583
|
className: "flex flex-col gap-px",
|
|
@@ -2523,16 +3586,18 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
|
|
|
2523
3586
|
});
|
|
2524
3587
|
}
|
|
2525
3588
|
function ChatRow({ chat, active, nested, onSelect, menu }) {
|
|
3589
|
+
const icons = useIcons();
|
|
2526
3590
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
2527
3591
|
const { dismiss } = useSidebar();
|
|
3592
|
+
const labels = useLabels();
|
|
2528
3593
|
const markerClass = "size-2 shrink-0 rounded-full";
|
|
2529
3594
|
const marker = chat.running ? /* @__PURE__ */ jsx("span", {
|
|
2530
3595
|
role: "img",
|
|
2531
|
-
"aria-label":
|
|
3596
|
+
"aria-label": labels.running,
|
|
2532
3597
|
className: clsx(markerClass, "animate-pulse bg-chat-accent motion-reduce:animate-none")
|
|
2533
3598
|
}) : chat.unread ? /* @__PURE__ */ jsx("span", {
|
|
2534
3599
|
role: "img",
|
|
2535
|
-
"aria-label":
|
|
3600
|
+
"aria-label": labels.unread,
|
|
2536
3601
|
className: clsx(markerClass, "bg-chat-check")
|
|
2537
3602
|
}) : null;
|
|
2538
3603
|
const content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
@@ -2557,11 +3622,11 @@ function ChatRow({ chat, active, nested, onSelect, menu }) {
|
|
|
2557
3622
|
isOpen: menuOpen,
|
|
2558
3623
|
onOpenChange: setMenuOpen,
|
|
2559
3624
|
children: [/* @__PURE__ */ jsx(IconButton, {
|
|
2560
|
-
label:
|
|
3625
|
+
label: labels.moreOptionsFor(chat.title),
|
|
2561
3626
|
size: "sm",
|
|
2562
3627
|
tooltip: false,
|
|
2563
3628
|
className: clsx("absolute top-1/2 right-1 size-6 -translate-y-1/2 [@media(pointer:coarse)]:size-8", menuOpen || active ? "opacity-100" : "opacity-0 group-hover/item:opacity-100 group-focus-within/item:opacity-100 [@media(hover:none)]:opacity-100"),
|
|
2564
|
-
children:
|
|
3629
|
+
children: icons.more
|
|
2565
3630
|
}), /* @__PURE__ */ jsx(Popover, {
|
|
2566
3631
|
placement: "bottom start",
|
|
2567
3632
|
children: menu
|
|
@@ -2570,6 +3635,8 @@ function ChatRow({ chat, active, nested, onSelect, menu }) {
|
|
|
2570
3635
|
});
|
|
2571
3636
|
}
|
|
2572
3637
|
function ProjectRow({ project, activeId, row }) {
|
|
3638
|
+
const icons = useIcons();
|
|
3639
|
+
const labels = useLabels();
|
|
2573
3640
|
const holdsActive = project.chats.some((c) => c.id === activeId);
|
|
2574
3641
|
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Disclosure$1, {
|
|
2575
3642
|
defaultExpanded: project.defaultExpanded ?? holdsActive,
|
|
@@ -2579,18 +3646,15 @@ function ProjectRow({ project, activeId, row }) {
|
|
|
2579
3646
|
slot: "trigger",
|
|
2580
3647
|
className: rowClass,
|
|
2581
3648
|
children: [
|
|
2582
|
-
|
|
2583
|
-
"
|
|
2584
|
-
|
|
2585
|
-
}) : /* @__PURE__ */ jsx(Folder, {
|
|
2586
|
-
"aria-hidden": true,
|
|
2587
|
-
className: "size-4 shrink-0 text-chat-muted"
|
|
3649
|
+
/* @__PURE__ */ jsx(IconSlot, {
|
|
3650
|
+
className: "text-chat-muted [&_svg]:size-4",
|
|
3651
|
+
children: isExpanded ? icons.folderOpen : icons.folder
|
|
2588
3652
|
}),
|
|
2589
3653
|
/* @__PURE__ */ jsx("span", {
|
|
2590
3654
|
className: "min-w-0 flex-1 truncate",
|
|
2591
3655
|
children: project.name
|
|
2592
3656
|
}),
|
|
2593
|
-
!isExpanded && project.chats.some((c) => c.unread || c.running) && /* @__PURE__ */ jsx(Badge, { children:
|
|
3657
|
+
!isExpanded && project.chats.some((c) => c.unread || c.running) && /* @__PURE__ */ jsx(Badge, { children: labels.new })
|
|
2594
3658
|
]
|
|
2595
3659
|
})
|
|
2596
3660
|
}), /* @__PURE__ */ jsx(DisclosurePanel, { children: /* @__PURE__ */ jsx("ul", {
|
|
@@ -2600,7 +3664,11 @@ function ProjectRow({ project, activeId, row }) {
|
|
|
2600
3664
|
}) });
|
|
2601
3665
|
}
|
|
2602
3666
|
// src/views/chat-view.tsx
|
|
2603
|
-
/**
|
|
3667
|
+
/**
|
|
3668
|
+
* Column defaults per density; `contentWidth` / `contentPadding` override them. Only the message size changes
|
|
3669
|
+
* with density: UI text is the same in all three. A theme can set the other text-size variables per density
|
|
3670
|
+
* with `[data-chat-density="panel"] { --chat-ui-font-size: … }`.
|
|
3671
|
+
*/
|
|
2604
3672
|
const densityVars = {
|
|
2605
3673
|
full: {},
|
|
2606
3674
|
panel: {
|
|
@@ -2615,14 +3683,20 @@ const densityVars = {
|
|
|
2615
3683
|
}
|
|
2616
3684
|
};
|
|
2617
3685
|
/** A whole chat: header, the conversation, the docked composer. An empty chat shows `emptyState`. */
|
|
2618
|
-
function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove, onRetry, onEdit, onFeedback, renderPart, header, composer, emptyState, disclaimer, density = "full", contentWidth, contentPadding, className }) {
|
|
3686
|
+
function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove, onRetry, onEdit, onFeedback, renderPart, widgets, header, composer, emptyState, disclaimer, density = "full", contentWidth, contentPadding, className }) {
|
|
2619
3687
|
const style = {
|
|
2620
3688
|
...densityVars[density],
|
|
2621
3689
|
...contentWidth && { "--chat-content-width": contentWidth },
|
|
2622
3690
|
...contentPadding && { "--chat-content-padding": contentPadding }
|
|
2623
3691
|
};
|
|
3692
|
+
const host = useMemo(() => ({
|
|
3693
|
+
onPrompt: onSend,
|
|
3694
|
+
onOpenLink: openInNewTab,
|
|
3695
|
+
...widgets
|
|
3696
|
+
}), [onSend, widgets]);
|
|
2624
3697
|
const empty = messages.length === 0;
|
|
2625
3698
|
const [body, short] = useShortPane();
|
|
3699
|
+
const labels = useLabels();
|
|
2626
3700
|
const pending = onApprove && approvals?.[0];
|
|
2627
3701
|
const box = (variant) => /* @__PURE__ */ jsx(Composer, {
|
|
2628
3702
|
variant: density === "inline" || short && !empty ? "compact" : variant,
|
|
@@ -2630,7 +3704,7 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
|
|
|
2630
3704
|
onSubmit: onSend,
|
|
2631
3705
|
...onStop && { onStop },
|
|
2632
3706
|
...composer,
|
|
2633
|
-
...pending && { placeholder:
|
|
3707
|
+
...pending && { placeholder: labels.replyDirectly }
|
|
2634
3708
|
});
|
|
2635
3709
|
return /* @__PURE__ */ jsxs("div", {
|
|
2636
3710
|
"data-chat-density": density,
|
|
@@ -2645,28 +3719,31 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
|
|
|
2645
3719
|
className: "w-full max-w-(--chat-content-width)",
|
|
2646
3720
|
children: emptyState ? emptyState(box("centered")) : box("centered")
|
|
2647
3721
|
})
|
|
2648
|
-
}) : /* @__PURE__ */ jsx(
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
className: "
|
|
2658
|
-
children:
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
3722
|
+
}) : /* @__PURE__ */ jsx(WidgetHostProvider, {
|
|
3723
|
+
host,
|
|
3724
|
+
children: /* @__PURE__ */ jsx(MessageList, {
|
|
3725
|
+
messages,
|
|
3726
|
+
...onRetry && { onRetry },
|
|
3727
|
+
...onEdit && { onEdit },
|
|
3728
|
+
...onFeedback && { onFeedback },
|
|
3729
|
+
...renderPart && { renderPart },
|
|
3730
|
+
footer: /* @__PURE__ */ jsx("div", {
|
|
3731
|
+
className: clsx("bg-linear-to-t from-chat-bg from-70% to-transparent px-(--chat-gutter)", short ? "pt-2" : "pt-4"),
|
|
3732
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
3733
|
+
className: "mx-auto w-full max-w-(--chat-content-width)",
|
|
3734
|
+
children: [
|
|
3735
|
+
pending && /* @__PURE__ */ jsx(ApprovalCard, {
|
|
3736
|
+
approval: pending,
|
|
3737
|
+
onRespond: onApprove,
|
|
3738
|
+
className: "mb-2"
|
|
3739
|
+
}, pending.part.id),
|
|
3740
|
+
box("docked"),
|
|
3741
|
+
/* @__PURE__ */ jsx("div", {
|
|
3742
|
+
className: clsx("flex items-center justify-center py-1 pb-[max(0.25rem,env(safe-area-inset-bottom))] text-center font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle", short ? "min-h-2" : "min-h-6"),
|
|
3743
|
+
children: disclaimer && !short && /* @__PURE__ */ jsx("span", { children: disclaimer })
|
|
3744
|
+
})
|
|
3745
|
+
]
|
|
3746
|
+
})
|
|
2670
3747
|
})
|
|
2671
3748
|
})
|
|
2672
3749
|
})
|
|
@@ -2703,6 +3780,51 @@ function EmptyState({ greeting, icon, children, footer }) {
|
|
|
2703
3780
|
]
|
|
2704
3781
|
});
|
|
2705
3782
|
}
|
|
3783
|
+
// src/views/chat-transcript.tsx
|
|
3784
|
+
/**
|
|
3785
|
+
* A conversation to read, not to continue: a shared chat, an export, a log. Same messages as ChatView,
|
|
3786
|
+
* with no composer, approvals, retry, edit or feedback. Copy stays.
|
|
3787
|
+
*/
|
|
3788
|
+
function ChatTranscript({ messages, renderPart, widgets, header, notice, footer, start = "top", density = "full", contentWidth, contentPadding, className }) {
|
|
3789
|
+
const style = {
|
|
3790
|
+
...densityVars[density],
|
|
3791
|
+
...contentWidth && { "--chat-content-width": contentWidth },
|
|
3792
|
+
...contentPadding && { "--chat-content-padding": contentPadding }
|
|
3793
|
+
};
|
|
3794
|
+
const host = useMemo(() => {
|
|
3795
|
+
const { allow, readResource } = { ...widgets };
|
|
3796
|
+
const onOpenLink = widgets && "onOpenLink" in widgets ? widgets.onOpenLink : openInNewTab;
|
|
3797
|
+
return {
|
|
3798
|
+
...allow && { allow },
|
|
3799
|
+
...onOpenLink && { onOpenLink },
|
|
3800
|
+
...readResource && { readResource }
|
|
3801
|
+
};
|
|
3802
|
+
}, [widgets]);
|
|
3803
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3804
|
+
"data-chat-density": density,
|
|
3805
|
+
style,
|
|
3806
|
+
className: clsx("chat-view flex h-full min-h-0 flex-col bg-chat-bg text-chat-fg", className),
|
|
3807
|
+
children: [header, /* @__PURE__ */ jsx("div", {
|
|
3808
|
+
className: "chat-view-body flex min-h-0 flex-1 flex-col",
|
|
3809
|
+
children: /* @__PURE__ */ jsx(WidgetHostProvider, {
|
|
3810
|
+
host,
|
|
3811
|
+
children: /* @__PURE__ */ jsx(MessageList, {
|
|
3812
|
+
messages,
|
|
3813
|
+
start,
|
|
3814
|
+
...notice && { header: notice },
|
|
3815
|
+
...renderPart && { renderPart },
|
|
3816
|
+
...footer && { footer: /* @__PURE__ */ jsx("div", {
|
|
3817
|
+
className: "bg-linear-to-t from-chat-bg from-70% to-transparent px-(--chat-gutter) pt-4 pb-[max(1rem,env(safe-area-inset-bottom))]",
|
|
3818
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
3819
|
+
className: "mx-auto w-full max-w-(--chat-content-width)",
|
|
3820
|
+
children: footer
|
|
3821
|
+
})
|
|
3822
|
+
}) }
|
|
3823
|
+
})
|
|
3824
|
+
})
|
|
3825
|
+
})]
|
|
3826
|
+
});
|
|
3827
|
+
}
|
|
2706
3828
|
// src/views/app-shell.tsx
|
|
2707
3829
|
/**
|
|
2708
3830
|
* A page with a sidebar: pinned and collapsible to an icon rail from tablet width up, a drawer over the
|
|
@@ -2727,6 +3849,7 @@ function AppShell({ sidebar, children, defaultCollapsed, className }) {
|
|
|
2727
3849
|
toggle,
|
|
2728
3850
|
dismiss
|
|
2729
3851
|
]);
|
|
3852
|
+
const labels = useLabels();
|
|
2730
3853
|
const inDrawer = useMemo(() => ({
|
|
2731
3854
|
...pinned,
|
|
2732
3855
|
collapsed: false
|
|
@@ -2745,7 +3868,7 @@ function AppShell({ sidebar, children, defaultCollapsed, className }) {
|
|
|
2745
3868
|
}), drawer && /* @__PURE__ */ jsx(Drawer, {
|
|
2746
3869
|
isOpen: drawerOpen,
|
|
2747
3870
|
onOpenChange: setDrawerOpen,
|
|
2748
|
-
"aria-label":
|
|
3871
|
+
"aria-label": labels.sidebar,
|
|
2749
3872
|
children: /* @__PURE__ */ jsx(SidebarContext.Provider, {
|
|
2750
3873
|
value: inDrawer,
|
|
2751
3874
|
children: sidebar
|
|
@@ -2755,13 +3878,16 @@ function AppShell({ sidebar, children, defaultCollapsed, className }) {
|
|
|
2755
3878
|
}
|
|
2756
3879
|
/** Opens the drawer on phones, collapses / expands the pinned sidebar elsewhere. */
|
|
2757
3880
|
function SidebarToggle({ drawerOnly = false, className }) {
|
|
3881
|
+
const icons = useIcons();
|
|
2758
3882
|
const { drawer, drawerOpen, collapsed, toggle } = useSidebar();
|
|
3883
|
+
const labels = useLabels();
|
|
2759
3884
|
if (drawerOnly && !drawer) return null;
|
|
3885
|
+
const label = drawer ? drawerOpen ? labels.closeSidebar : labels.openSidebar : collapsed ? labels.expandSidebar : labels.collapseSidebar;
|
|
2760
3886
|
return /* @__PURE__ */ jsx(IconButton, {
|
|
2761
|
-
label
|
|
3887
|
+
label,
|
|
2762
3888
|
onPress: toggle,
|
|
2763
3889
|
className,
|
|
2764
|
-
children:
|
|
3890
|
+
children: icons.sidebar
|
|
2765
3891
|
});
|
|
2766
3892
|
}
|
|
2767
3893
|
// src/views/sidebar.tsx
|
|
@@ -2801,7 +3927,7 @@ function Sidebar({ brand, headerActions, nav, children, footer, className }) {
|
|
|
2801
3927
|
/** A nav row: icon and label; just the icon with a tooltip when collapsed. Closes the drawer when picked. */
|
|
2802
3928
|
function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
|
|
2803
3929
|
const { collapsed, dismiss } = useSidebar();
|
|
2804
|
-
const className = clsx("group flex h-8 cursor-pointer items-center gap-2.5 rounded-chat-sm text-left text-
|
|
3930
|
+
const className = clsx("group flex h-8 cursor-pointer items-center gap-2.5 rounded-chat-sm text-left text-(length:--chat-ui-font-size) leading-5 text-chat-fg outline-none [@media(pointer:coarse)]:h-10", "data-hovered:bg-chat-hover data-pressed:bg-chat-selected data-focus-visible:ring-2 data-focus-visible:ring-chat-focus", "[&_svg]:size-4 [&_svg]:shrink-0", collapsed ? "w-9 justify-center [@media(pointer:coarse)]:w-10" : "w-full px-2", active && "bg-chat-selected");
|
|
2805
3931
|
const press = () => {
|
|
2806
3932
|
onPress?.();
|
|
2807
3933
|
dismiss();
|
|
@@ -2816,7 +3942,7 @@ function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
|
|
|
2816
3942
|
children: label
|
|
2817
3943
|
}),
|
|
2818
3944
|
!collapsed && shortcut && /* @__PURE__ */ jsx("kbd", {
|
|
2819
|
-
className: "font-chat-sans text-
|
|
3945
|
+
className: "font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle opacity-0 group-data-hovered:opacity-100",
|
|
2820
3946
|
children: shortcut
|
|
2821
3947
|
})
|
|
2822
3948
|
] });
|
|
@@ -2842,6 +3968,7 @@ function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
|
|
|
2842
3968
|
const initials = (name) => name.split(/\s+/).slice(0, 2).map((w) => w[0]?.toUpperCase() ?? "").join("");
|
|
2843
3969
|
/** The account row at the bottom: avatar, name, plan, and a menu. */
|
|
2844
3970
|
function SidebarUser({ name, detail, avatarUrl, menu }) {
|
|
3971
|
+
const icons = useIcons();
|
|
2845
3972
|
const { collapsed } = useSidebar();
|
|
2846
3973
|
const avatar = avatarUrl ? /* @__PURE__ */ jsx("img", {
|
|
2847
3974
|
src: avatarUrl,
|
|
@@ -2849,7 +3976,7 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
|
|
|
2849
3976
|
className: "size-8 shrink-0 rounded-full object-cover"
|
|
2850
3977
|
}) : /* @__PURE__ */ jsx("span", {
|
|
2851
3978
|
"aria-hidden": true,
|
|
2852
|
-
className: "flex size-8 shrink-0 items-center justify-center rounded-full bg-chat-fg text-
|
|
3979
|
+
className: "flex size-8 shrink-0 items-center justify-center rounded-full bg-chat-fg text-(length:--chat-ui-font-size-sm) leading-4 font-medium text-chat-bg",
|
|
2853
3980
|
children: initials(name)
|
|
2854
3981
|
});
|
|
2855
3982
|
const button = /* @__PURE__ */ jsxs(Button$1, {
|
|
@@ -2858,15 +3985,15 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
|
|
|
2858
3985
|
children: [avatar, !collapsed && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
|
|
2859
3986
|
className: "flex min-w-0 flex-1 flex-col",
|
|
2860
3987
|
children: [/* @__PURE__ */ jsx("span", {
|
|
2861
|
-
className: "truncate text-
|
|
3988
|
+
className: "truncate text-(length:--chat-ui-font-size) leading-5 text-chat-fg",
|
|
2862
3989
|
children: name
|
|
2863
3990
|
}), detail && /* @__PURE__ */ jsx("span", {
|
|
2864
|
-
className: "truncate text-
|
|
3991
|
+
className: "truncate text-(length:--chat-ui-font-size-sm) leading-4 text-chat-muted",
|
|
2865
3992
|
children: detail
|
|
2866
3993
|
})]
|
|
2867
|
-
}), menu && /* @__PURE__ */ jsx(
|
|
2868
|
-
"
|
|
2869
|
-
|
|
3994
|
+
}), menu && /* @__PURE__ */ jsx(IconSlot, {
|
|
3995
|
+
className: "text-chat-muted [&_svg]:size-4",
|
|
3996
|
+
children: icons.switcher
|
|
2870
3997
|
})] })]
|
|
2871
3998
|
});
|
|
2872
3999
|
if (!menu) return button;
|
|
@@ -2875,4 +4002,4 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
|
|
|
2875
4002
|
children: menu
|
|
2876
4003
|
})] });
|
|
2877
4004
|
}
|
|
2878
|
-
export { ActivityGroup, AgentProvider, AgentRegistry, AppShell, ApprovalCard, AssistantMessage, AttachButton, AttachmentCard, AttachmentRow, Badge, Button, Caret, ChatHeader, ChatList, ChatTheme, ChatTitleMenu, ChatView, CodeBlock, Composer, ContextMenu, ContextPanel, DRAWER_QUERY, Disclosure, Drawer, EmptyState, IconButton, Markdown, Menu, MenuItem, MenuNote, MenuSearch, MenuSection, MenuSeparator, MenuTrigger, MessageList, Modal, ModelPicker, Panel, PastedTextDialog, Popover, Sidebar, SidebarItem, SidebarToggle, SidebarUser, SubmenuTrigger, TokenCounter, ToolViewsProvider, Tooltip, UserMessage, activityHeading, activityLabel, commandTool, defaultToolViews, formatDuration, formatRelativeTime, formatTokens, memoryTool, messageText, sseTransport, useAgentContext, useAgentRegistry, useAgentSnapshot, useAgentTool, useAttachments, useChat, useChatTool, useCountUp, useMediaQuery, useSidebar, useSmoothText, useToolView, useToolViews, userMessage, webSearchTool };
|
|
4005
|
+
export { ActivityGroup, AgentProvider, AgentRegistry, AppShell, ApprovalCard, AssistantMessage, AttachButton, AttachmentCard, AttachmentRow, Badge, Button, Caret, ChatHeader, ChatList, ChatNotice, ChatTheme, ChatTitleMenu, ChatTranscript, ChatView, CodeBlock, Composer, ContextMenu, ContextPanel, DRAWER_QUERY, Disclosure, Drawer, EmptyState, IconButton, Markdown, MarkdownLinksProvider, Menu, MenuItem, MenuNote, MenuSearch, MenuSection, MenuSeparator, MenuTrigger, MessageList, Modal, ModelPicker, Panel, PastedTextDialog, Popover, Sidebar, SidebarItem, SidebarToggle, SidebarUser, SubmenuTrigger, TokenCounter, ToolViewsProvider, Tooltip, UiComponentsProvider, UiMarkupBlock, UiTree, UserMessage, WIDGET_MAX_HEIGHT, WidgetFrame, WidgetHostProvider, WidgetPartView, activityHeading, activityLabel, commandTool, defaultIcons, defaultLabels, defaultToolViews, formatDuration, formatRelativeTime, formatTokens, htmlComponent, intersectAllow, memoryTool, messageText, openInNewTab, readTreeInput, readVisualizeInput, sseTransport, uiMarkupToolView, uiTreeToolView, useAgentContext, useAgentRegistry, useAgentSnapshot, useAgentTool, useAttachments, useChat, useChatTool, useCountUp, useIcons, useLabels, useMediaQuery, useSidebar, useSmoothText, useToolView, useToolViews, useUiAction, useUiMarkupLanguage, useUiState, useWidgetHost, userMessage, webFetchTool, webSearchTool, widgetCsp, widgetToolView };
|