agentic-ui-kit 0.2.0 → 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 +1417 -287
- 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
|
});
|
|
@@ -982,7 +1177,10 @@ function useSmoothText(text, streaming) {
|
|
|
982
1177
|
s,
|
|
983
1178
|
prefersReducedMotion
|
|
984
1179
|
]);
|
|
985
|
-
useEffect(() => () =>
|
|
1180
|
+
useEffect(() => () => {
|
|
1181
|
+
cancelAnimationFrame(s.frame);
|
|
1182
|
+
s.frame = 0;
|
|
1183
|
+
}, [s]);
|
|
986
1184
|
return shown;
|
|
987
1185
|
}
|
|
988
1186
|
/** Moves `index` forward to the end of the word it falls in, so words are never cut in half. */
|
|
@@ -1009,6 +1207,700 @@ const SidebarContext = createContext({
|
|
|
1009
1207
|
dismiss: () => {}
|
|
1010
1208
|
});
|
|
1011
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
|
+
};
|
|
1012
1904
|
// src/components/word-fade.ts
|
|
1013
1905
|
const OPAQUE_TAGS = /* @__PURE__ */ new Set([
|
|
1014
1906
|
"code",
|
|
@@ -1075,45 +1967,96 @@ function textOf(node) {
|
|
|
1075
1967
|
if (node.type === "text") return node.value ?? "";
|
|
1076
1968
|
return (node.children ?? []).map(textOf).join("");
|
|
1077
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
|
+
}
|
|
1078
1993
|
const components = {
|
|
1079
1994
|
pre: ({ children }) => /* @__PURE__ */ jsx(Fragment, { children }),
|
|
1080
|
-
code: ({ node, className, children }) => {
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
"data-chat-inline-code": true,
|
|
1086
|
-
children
|
|
1087
|
-
});
|
|
1088
|
-
return /* @__PURE__ */ jsx(CodeBlock, {
|
|
1089
|
-
variant: "boxed",
|
|
1090
|
-
code: textOf(hast).replace(/\n$/, ""),
|
|
1091
|
-
...language && { language }
|
|
1092
|
-
});
|
|
1093
|
-
}
|
|
1995
|
+
code: ({ node, className, children }) => /* @__PURE__ */ jsx(Code, {
|
|
1996
|
+
node,
|
|
1997
|
+
className,
|
|
1998
|
+
children
|
|
1999
|
+
})
|
|
1094
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
|
+
}
|
|
1095
2008
|
/** Message markdown: GFM, KaTeX math ($…$ and $$…$$), Shiki code blocks, smooth fade-in while streaming. */
|
|
1096
|
-
function Markdown({ children, streaming = false, className }) {
|
|
2009
|
+
function Markdown({ children, streaming = false, className, partId }) {
|
|
1097
2010
|
const text = useSmoothText(children, streaming);
|
|
1098
2011
|
const animating = streaming || text.length < children.length;
|
|
1099
2012
|
const [streamed, setStreamed] = useState(streaming);
|
|
1100
2013
|
if (streaming && !streamed) setStreamed(true);
|
|
1101
2014
|
return /* @__PURE__ */ jsx("div", {
|
|
1102
2015
|
className: clsx("chat-markdown", streamed && "chat-fade", className),
|
|
1103
|
-
children: /* @__PURE__ */ jsx(
|
|
1104
|
-
|
|
1105
|
-
animating,
|
|
1106
|
-
|
|
2016
|
+
children: /* @__PURE__ */ jsx(BlockScope, {
|
|
2017
|
+
id: partId,
|
|
2018
|
+
streaming: animating,
|
|
2019
|
+
children: /* @__PURE__ */ jsx(MarkdownBody, {
|
|
2020
|
+
text,
|
|
2021
|
+
animating,
|
|
2022
|
+
fade: streamed
|
|
2023
|
+
})
|
|
1107
2024
|
})
|
|
1108
2025
|
});
|
|
1109
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
|
+
}
|
|
1110
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]);
|
|
1111
2054
|
return /* @__PURE__ */ jsx(Streamdown, {
|
|
1112
2055
|
prefix: PREFIX,
|
|
1113
2056
|
mode: animating || fade ? "streaming" : "static",
|
|
1114
2057
|
...fade && { rehypePlugins: fadePlugins },
|
|
1115
2058
|
plugins: { math },
|
|
1116
|
-
components,
|
|
2059
|
+
components: withLinks,
|
|
1117
2060
|
controls: false,
|
|
1118
2061
|
lineNumbers: false,
|
|
1119
2062
|
linkSafety: { enabled: false },
|
|
@@ -1153,32 +2096,31 @@ const useChatTool = useAgentTool;
|
|
|
1153
2096
|
/** Streaming or running: the model or a tool is busy with it. */
|
|
1154
2097
|
const isWorking = (part) => part.status !== "done" && part.status !== "error" && part.status !== "awaiting";
|
|
1155
2098
|
/** Row label for any activity part. */
|
|
1156
|
-
function activityLabel(part, view) {
|
|
2099
|
+
function activityLabel(part, labels, view) {
|
|
1157
2100
|
if (part.type === "thinking") {
|
|
1158
2101
|
if (part.title) return part.title;
|
|
1159
|
-
if (part.status !== "done") return
|
|
1160
|
-
return part.durationMs ?
|
|
2102
|
+
if (part.status !== "done") return labels.thinking;
|
|
2103
|
+
return part.durationMs ? labels.thoughtFor(formatDuration(part.durationMs, labels)) : labels.thought;
|
|
1161
2104
|
}
|
|
1162
|
-
return view?.label?.(part) ?? part.title ?? part.name;
|
|
2105
|
+
return view?.label?.(part, labels) ?? part.title ?? part.name;
|
|
1163
2106
|
}
|
|
1164
2107
|
/**
|
|
1165
2108
|
* Heading of an activity group. While live it follows the latest step. Once done it lists what the tools
|
|
1166
2109
|
* did, in order ("Ran a command, searched the web"); a group of only thinking shows the first title.
|
|
1167
2110
|
*/
|
|
1168
|
-
function activityHeading(parts, views) {
|
|
2111
|
+
function activityHeading(parts, views, labels) {
|
|
1169
2112
|
const waiting = parts.find((p) => p.type === "tool_call" && p.status === "awaiting");
|
|
1170
|
-
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] });
|
|
1171
2114
|
if (parts.some(isWorking)) {
|
|
1172
2115
|
const last = parts.at(-1);
|
|
1173
|
-
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);
|
|
1174
2117
|
}
|
|
1175
2118
|
const counts = /* @__PURE__ */ new Map();
|
|
1176
2119
|
for (const part of parts) if (part.type === "tool_call") counts.set(part.name, (counts.get(part.name) ?? 0) + 1);
|
|
1177
|
-
if (counts.size === 0) return parts[0] ? activityLabel(parts[0]) : "";
|
|
1178
|
-
const
|
|
1179
|
-
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);
|
|
1180
2123
|
}
|
|
1181
|
-
const plural = (count, one, many) => count === 1 ? one : many(count);
|
|
1182
2124
|
function recordOf(value) {
|
|
1183
2125
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1184
2126
|
}
|
|
@@ -1187,10 +2129,11 @@ function stringOf(value) {
|
|
|
1187
2129
|
}
|
|
1188
2130
|
/** A shell command. Input `{ command, description? }`; result a string or `{ stdout, stderr, exitCode }`. */
|
|
1189
2131
|
const commandTool = {
|
|
1190
|
-
label: () =>
|
|
2132
|
+
label: (_, labels) => labels.ranCommandLabel,
|
|
1191
2133
|
hint: (part) => stringOf(recordOf(part.input).description),
|
|
1192
|
-
summary: (n
|
|
2134
|
+
summary: (n, labels) => labels.ranCommands(n),
|
|
1193
2135
|
Detail: function CommandDetail({ part }) {
|
|
2136
|
+
const labels = useLabels();
|
|
1194
2137
|
const command = stringOf(recordOf(part.input).command) ?? part.inputJson;
|
|
1195
2138
|
const result = recordOf(part.result);
|
|
1196
2139
|
const output = stringOf(part.result) ?? [stringOf(result.stdout), stringOf(result.stderr)].filter(Boolean).join("\n");
|
|
@@ -1202,31 +2145,37 @@ const commandTool = {
|
|
|
1202
2145
|
code: command
|
|
1203
2146
|
}),
|
|
1204
2147
|
(output || part.status === "done") && /* @__PURE__ */ jsx(CodeBlock, {
|
|
1205
|
-
label:
|
|
1206
|
-
code: output ||
|
|
2148
|
+
label: labels.output,
|
|
2149
|
+
code: output || labels.noOutput
|
|
1207
2150
|
}),
|
|
1208
2151
|
part.error && /* @__PURE__ */ jsx("p", {
|
|
1209
|
-
className: "text-
|
|
2152
|
+
className: "text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
|
|
1210
2153
|
children: part.error
|
|
1211
2154
|
})
|
|
1212
2155
|
]
|
|
1213
2156
|
});
|
|
1214
2157
|
}
|
|
1215
2158
|
};
|
|
1216
|
-
/**
|
|
2159
|
+
/**
|
|
2160
|
+
* A web search. Input `{ query }`; results (`{ results: SearchResult[] }` or `SearchResult[]`) from the
|
|
2161
|
+
* part's display `data`, else its `result`.
|
|
2162
|
+
*/
|
|
1217
2163
|
const webSearchTool = {
|
|
1218
|
-
label: () =>
|
|
2164
|
+
label: (_, labels) => labels.searchedWebLabel,
|
|
1219
2165
|
subtitle: (part) => stringOf(recordOf(part.input).query) ?? part.subtitle,
|
|
1220
|
-
summary: (n
|
|
2166
|
+
summary: (n, labels) => labels.searchedWeb(n),
|
|
1221
2167
|
Detail: function WebSearchDetail({ part }) {
|
|
1222
|
-
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;
|
|
1223
2172
|
if (part.error) return /* @__PURE__ */ jsx("p", {
|
|
1224
|
-
className: "text-
|
|
2173
|
+
className: "text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
|
|
1225
2174
|
children: part.error
|
|
1226
2175
|
});
|
|
1227
2176
|
if (!results?.length) return /* @__PURE__ */ jsx("p", {
|
|
1228
|
-
className: "text-
|
|
1229
|
-
children:
|
|
2177
|
+
className: "text-(length:--chat-ui-font-size) leading-5 text-chat-muted",
|
|
2178
|
+
children: labels.noResults
|
|
1230
2179
|
});
|
|
1231
2180
|
return /* @__PURE__ */ jsx("ul", {
|
|
1232
2181
|
className: "-mx-1",
|
|
@@ -1234,22 +2183,22 @@ const webSearchTool = {
|
|
|
1234
2183
|
href: result.url,
|
|
1235
2184
|
target: "_blank",
|
|
1236
2185
|
rel: "noreferrer",
|
|
1237
|
-
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",
|
|
1238
2187
|
children: [
|
|
1239
2188
|
result.favicon ? /* @__PURE__ */ jsx("img", {
|
|
1240
2189
|
src: result.favicon,
|
|
1241
2190
|
alt: "",
|
|
1242
2191
|
className: "size-4 shrink-0 rounded-sm"
|
|
1243
|
-
}) : /* @__PURE__ */ jsx(
|
|
1244
|
-
"
|
|
1245
|
-
|
|
2192
|
+
}) : /* @__PURE__ */ jsx(IconSlot, {
|
|
2193
|
+
className: "[&_svg]:size-4",
|
|
2194
|
+
children: icons.web
|
|
1246
2195
|
}),
|
|
1247
2196
|
/* @__PURE__ */ jsx("span", {
|
|
1248
2197
|
className: "min-w-0 flex-1 truncate",
|
|
1249
2198
|
children: result.title
|
|
1250
2199
|
}),
|
|
1251
2200
|
/* @__PURE__ */ jsx("span", {
|
|
1252
|
-
className: "shrink-0 text-
|
|
2201
|
+
className: "shrink-0 text-(length:--chat-ui-font-size) leading-5",
|
|
1253
2202
|
children: hostOf(result.url)
|
|
1254
2203
|
})
|
|
1255
2204
|
]
|
|
@@ -1257,18 +2206,44 @@ const webSearchTool = {
|
|
|
1257
2206
|
});
|
|
1258
2207
|
}
|
|
1259
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
|
+
};
|
|
1260
2234
|
/** Memory lookups, e.g. Anthropic's memory tool. Input `{ path }`. */
|
|
1261
2235
|
const memoryTool = {
|
|
1262
|
-
label: () =>
|
|
2236
|
+
label: (_, labels) => labels.recalledMemoryLabel,
|
|
1263
2237
|
subtitle: (part) => {
|
|
1264
2238
|
return stringOf(recordOf(part.input).path)?.split("/").at(-1) ?? part.subtitle;
|
|
1265
2239
|
},
|
|
1266
|
-
summary: () =>
|
|
2240
|
+
summary: (n, labels) => labels.recalledMemory(n)
|
|
1267
2241
|
};
|
|
1268
2242
|
const defaultToolViews = {
|
|
1269
2243
|
bash: commandTool,
|
|
1270
2244
|
shell: commandTool,
|
|
1271
2245
|
web_search: webSearchTool,
|
|
2246
|
+
web_fetch: webFetchTool,
|
|
1272
2247
|
memory: memoryTool,
|
|
1273
2248
|
memory_view: memoryTool
|
|
1274
2249
|
};
|
|
@@ -1280,9 +2255,8 @@ function hostOf(url) {
|
|
|
1280
2255
|
}
|
|
1281
2256
|
}
|
|
1282
2257
|
/** "9s", "1m 5s". */
|
|
1283
|
-
function formatDuration(ms) {
|
|
1284
|
-
|
|
1285
|
-
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)));
|
|
1286
2260
|
}
|
|
1287
2261
|
// src/components/token-counter.tsx
|
|
1288
2262
|
/** Compact token count: 526, 1.4k, 33k, 76.8k, 1M. */
|
|
@@ -1327,25 +2301,23 @@ function useCountUp(target) {
|
|
|
1327
2301
|
}
|
|
1328
2302
|
/** A token count that counts up as `tokens` grows and holds still when it stops growing. */
|
|
1329
2303
|
function TokenCounter({ tokens, variant = "standalone", className }) {
|
|
2304
|
+
const icons = useIcons();
|
|
1330
2305
|
const shown = useCountUp(tokens);
|
|
2306
|
+
const labels = useLabels();
|
|
1331
2307
|
if (variant === "inline") return /* @__PURE__ */ jsxs("span", {
|
|
1332
2308
|
className: clsx("text-chat-subtle tabular-nums", className),
|
|
1333
2309
|
children: [
|
|
1334
2310
|
"(",
|
|
1335
|
-
formatTokens(shown),
|
|
1336
|
-
"
|
|
2311
|
+
labels.tokenCount(formatTokens(shown)),
|
|
2312
|
+
")"
|
|
1337
2313
|
]
|
|
1338
2314
|
});
|
|
1339
2315
|
return /* @__PURE__ */ jsxs("span", {
|
|
1340
|
-
className: clsx("inline-flex items-center gap-1 font-chat-sans text-
|
|
1341
|
-
children: [
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
}),
|
|
1346
|
-
formatTokens(shown),
|
|
1347
|
-
" tokens"
|
|
1348
|
-
]
|
|
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))]
|
|
1349
2321
|
});
|
|
1350
2322
|
}
|
|
1351
2323
|
// src/components/activity-group.tsx
|
|
@@ -1356,6 +2328,7 @@ function TokenCounter({ tokens, variant = "standalone", className }) {
|
|
|
1356
2328
|
*/
|
|
1357
2329
|
function ActivityGroup({ parts, defaultExpanded = false, className }) {
|
|
1358
2330
|
const views = useToolViews();
|
|
2331
|
+
const labels = useLabels();
|
|
1359
2332
|
const [expanded, setExpanded] = useState(defaultExpanded);
|
|
1360
2333
|
const live = parts.some(isWorking);
|
|
1361
2334
|
const tokens = activityTokens(parts);
|
|
@@ -1377,9 +2350,9 @@ function ActivityGroup({ parts, defaultExpanded = false, className }) {
|
|
|
1377
2350
|
onExpandedChange: setExpanded,
|
|
1378
2351
|
summary: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Live, {
|
|
1379
2352
|
live,
|
|
1380
|
-
children: activityHeading(parts, views)
|
|
2353
|
+
children: activityHeading(parts, views, labels)
|
|
1381
2354
|
}), after] }),
|
|
1382
|
-
summaryClassName: "text-
|
|
2355
|
+
summaryClassName: "text-(length:--chat-activity-font-size)",
|
|
1383
2356
|
panelClassName: "pt-3",
|
|
1384
2357
|
children: /* @__PURE__ */ jsx("div", {
|
|
1385
2358
|
className: "divide-y divide-chat-divider rounded-chat-md border border-chat-divider",
|
|
@@ -1416,7 +2389,7 @@ function SingleStep({ part, live, after, expanded, onExpandedChange }) {
|
|
|
1416
2389
|
live,
|
|
1417
2390
|
children: summary
|
|
1418
2391
|
}), after] }),
|
|
1419
|
-
summaryClassName: "text-
|
|
2392
|
+
summaryClassName: "text-(length:--chat-activity-font-size)",
|
|
1420
2393
|
panelClassName: "pt-3",
|
|
1421
2394
|
children: /* @__PURE__ */ jsx("div", {
|
|
1422
2395
|
className: "rounded-chat-md border border-chat-divider px-3 py-2",
|
|
@@ -1429,7 +2402,7 @@ function Step({ part }) {
|
|
|
1429
2402
|
const live = isWorking(part);
|
|
1430
2403
|
return /* @__PURE__ */ jsx(Disclosure, {
|
|
1431
2404
|
className: "px-3 py-2",
|
|
1432
|
-
summaryClassName: "text-
|
|
2405
|
+
summaryClassName: "text-(length:--chat-activity-font-size)",
|
|
1433
2406
|
panelClassName: "pt-2 pb-1",
|
|
1434
2407
|
summary: /* @__PURE__ */ jsx(Live, {
|
|
1435
2408
|
live,
|
|
@@ -1440,12 +2413,13 @@ function Step({ part }) {
|
|
|
1440
2413
|
}
|
|
1441
2414
|
/** The label line and expanded body of one step. Tool calls go through their registered view. */
|
|
1442
2415
|
function useStepContent(part) {
|
|
2416
|
+
const labels = useLabels();
|
|
1443
2417
|
const view = useToolView(part.type === "tool_call" ? part.name : "");
|
|
1444
2418
|
if (part.type === "thinking") return {
|
|
1445
|
-
summary: activityLabel(part),
|
|
2419
|
+
summary: activityLabel(part, labels),
|
|
1446
2420
|
detail: part.visibility === "hidden" || !part.text ? /* @__PURE__ */ jsx("p", {
|
|
1447
|
-
className: "text-
|
|
1448
|
-
children:
|
|
2421
|
+
className: "text-(length:--chat-ui-font-size) leading-5 text-chat-subtle",
|
|
2422
|
+
children: labels.reasoningHidden
|
|
1449
2423
|
}) : /* @__PURE__ */ jsx("div", {
|
|
1450
2424
|
className: "max-h-96 overflow-y-auto [scrollbar-width:thin]",
|
|
1451
2425
|
children: /* @__PURE__ */ jsx(Markdown, {
|
|
@@ -1460,7 +2434,7 @@ function useStepContent(part) {
|
|
|
1460
2434
|
return {
|
|
1461
2435
|
summary: /* @__PURE__ */ jsxs("span", {
|
|
1462
2436
|
title: view.hint?.(part),
|
|
1463
|
-
children: [activityLabel(part, view), subtitle && /* @__PURE__ */ jsx("span", {
|
|
2437
|
+
children: [activityLabel(part, labels, view), subtitle && /* @__PURE__ */ jsx("span", {
|
|
1464
2438
|
className: "ml-1.5 text-chat-fg",
|
|
1465
2439
|
children: subtitle
|
|
1466
2440
|
})]
|
|
@@ -1470,16 +2444,16 @@ function useStepContent(part) {
|
|
|
1470
2444
|
}
|
|
1471
2445
|
function DefaultDetail({ part }) {
|
|
1472
2446
|
if (part.error) return /* @__PURE__ */ jsx("p", {
|
|
1473
|
-
className: "text-
|
|
2447
|
+
className: "text-(length:--chat-ui-font-size) leading-5 text-chat-danger",
|
|
1474
2448
|
children: part.error
|
|
1475
2449
|
});
|
|
1476
2450
|
const body = part.result ?? part.input ?? part.inputJson;
|
|
1477
2451
|
if (typeof body === "string") return /* @__PURE__ */ jsx("p", {
|
|
1478
|
-
className: "text-
|
|
2452
|
+
className: "text-(length:--chat-activity-font-size) leading-relaxed whitespace-pre-wrap text-chat-muted",
|
|
1479
2453
|
children: body
|
|
1480
2454
|
});
|
|
1481
2455
|
return /* @__PURE__ */ jsx("pre", {
|
|
1482
|
-
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",
|
|
1483
2457
|
children: JSON.stringify(body, null, 2)
|
|
1484
2458
|
});
|
|
1485
2459
|
}
|
|
@@ -1490,6 +2464,9 @@ const draftId = () => `att_${Date.now().toString(36)}_${(counter++).toString(36)
|
|
|
1490
2464
|
function useAttachments({ upload } = {}) {
|
|
1491
2465
|
const [attachments, setAttachments] = useState([]);
|
|
1492
2466
|
const uploads = useRef(/* @__PURE__ */ new Map());
|
|
2467
|
+
const labels = useLabels();
|
|
2468
|
+
const labelsRef = useRef(labels);
|
|
2469
|
+
labelsRef.current = labels;
|
|
1493
2470
|
const uploadRef = useRef(upload);
|
|
1494
2471
|
uploadRef.current = upload;
|
|
1495
2472
|
const patch = useCallback((id, fields) => {
|
|
@@ -1523,7 +2500,7 @@ function useAttachments({ upload } = {}) {
|
|
|
1523
2500
|
})).catch((error) => {
|
|
1524
2501
|
if (!controller.signal.aborted) patch(id, {
|
|
1525
2502
|
status: "error",
|
|
1526
|
-
error: error instanceof Error ? error.message :
|
|
2503
|
+
error: error instanceof Error ? error.message : labelsRef.current.uploadFailed
|
|
1527
2504
|
});
|
|
1528
2505
|
}).finally(() => uploads.current.delete(id));
|
|
1529
2506
|
}
|
|
@@ -1532,7 +2509,7 @@ function useAttachments({ upload } = {}) {
|
|
|
1532
2509
|
setAttachments((list) => [...list, {
|
|
1533
2510
|
id: draftId(),
|
|
1534
2511
|
kind: "text",
|
|
1535
|
-
name:
|
|
2512
|
+
name: labelsRef.current.pastedText,
|
|
1536
2513
|
mime: "text/plain",
|
|
1537
2514
|
size: new Blob([text]).size,
|
|
1538
2515
|
text,
|
|
@@ -1578,13 +2555,15 @@ function useAttachments({ upload } = {}) {
|
|
|
1578
2555
|
const extension = (name) => /\.([a-z0-9]{1,5})$/i.exec(name)?.[1]?.toUpperCase();
|
|
1579
2556
|
/** A square card: pasted text preview with a PASTED badge, an image thumbnail, or a file with its type. */
|
|
1580
2557
|
function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", className }) {
|
|
2558
|
+
const icons = useIcons();
|
|
1581
2559
|
const image = a.mime.startsWith("image/") && a.url;
|
|
1582
2560
|
const pasted = a.text !== void 0;
|
|
2561
|
+
const labels = useLabels();
|
|
1583
2562
|
const uploading = a.status === "uploading";
|
|
1584
2563
|
const failed = a.status === "error";
|
|
1585
2564
|
return /* @__PURE__ */ jsxs("div", {
|
|
1586
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),
|
|
1587
|
-
title: failed ? a.error ??
|
|
2566
|
+
title: failed ? a.error ?? labels.uploadFailed : a.name,
|
|
1588
2567
|
children: [
|
|
1589
2568
|
/* @__PURE__ */ jsx("div", {
|
|
1590
2569
|
className: clsx("absolute inset-0 overflow-hidden rounded-[inherit]", uploading && "chat-shimmer-surface"),
|
|
@@ -1597,11 +2576,11 @@ function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", classNam
|
|
|
1597
2576
|
children: a.text
|
|
1598
2577
|
}) : /* @__PURE__ */ jsxs("div", {
|
|
1599
2578
|
className: "flex h-full flex-col gap-1.5 p-2.5",
|
|
1600
|
-
children: [/* @__PURE__ */ jsx(
|
|
1601
|
-
"
|
|
1602
|
-
|
|
2579
|
+
children: [/* @__PURE__ */ jsx(IconSlot, {
|
|
2580
|
+
className: "text-chat-muted [&_svg]:size-5",
|
|
2581
|
+
children: icons.file
|
|
1603
2582
|
}), /* @__PURE__ */ jsx("span", {
|
|
1604
|
-
className: "line-clamp-3 text-
|
|
2583
|
+
className: "line-clamp-3 text-(length:--chat-ui-font-size-sm) leading-4 break-words text-chat-fg",
|
|
1605
2584
|
children: a.name
|
|
1606
2585
|
})]
|
|
1607
2586
|
})
|
|
@@ -1610,29 +2589,29 @@ function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", classNam
|
|
|
1610
2589
|
className: "absolute inset-x-2 bottom-2 flex items-end justify-between gap-1",
|
|
1611
2590
|
children: [/* @__PURE__ */ jsx("span", {
|
|
1612
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"),
|
|
1613
|
-
children: failed ?
|
|
2592
|
+
children: failed ? labels.failed : pasted ? labels.pasted : extension(a.name) ?? labels.file
|
|
1614
2593
|
}), pasted && onOpen && /* @__PURE__ */ jsx(IconButton, {
|
|
1615
|
-
label:
|
|
2594
|
+
label: labels.editPastedText,
|
|
1616
2595
|
size: "sm",
|
|
1617
2596
|
onPress: onOpen,
|
|
1618
2597
|
className: "size-6 border border-chat-border-strong bg-chat-bg [&_svg]:size-3.5 [@media(pointer:coarse)]:size-7!",
|
|
1619
|
-
children:
|
|
2598
|
+
children: icons.edit
|
|
1620
2599
|
})]
|
|
1621
2600
|
}),
|
|
1622
2601
|
pasted && onOpen && !onRemove && /* @__PURE__ */ jsx("button", {
|
|
1623
2602
|
type: "button",
|
|
1624
|
-
"aria-label":
|
|
2603
|
+
"aria-label": labels.viewPastedText,
|
|
1625
2604
|
onClick: onOpen,
|
|
1626
2605
|
className: "absolute inset-0 cursor-pointer rounded-[inherit] outline-none focus-visible:ring-2 focus-visible:ring-chat-focus"
|
|
1627
2606
|
}),
|
|
1628
2607
|
onRemove && /* @__PURE__ */ jsx(IconButton, {
|
|
1629
|
-
label:
|
|
2608
|
+
label: labels.removeAttachment(a.name),
|
|
1630
2609
|
variant: "subtle",
|
|
1631
2610
|
size: "sm",
|
|
1632
2611
|
tooltip: false,
|
|
1633
2612
|
onPress: onRemove,
|
|
1634
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"),
|
|
1635
|
-
children:
|
|
2614
|
+
children: icons.remove
|
|
1636
2615
|
})
|
|
1637
2616
|
]
|
|
1638
2617
|
});
|
|
@@ -1640,9 +2619,10 @@ function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", classNam
|
|
|
1640
2619
|
/** The composer's row of cards above the text. Renders nothing when empty. */
|
|
1641
2620
|
function AttachmentRow({ attachments, onRemove, onEditText }) {
|
|
1642
2621
|
const [editing, setEditing] = useState();
|
|
2622
|
+
const labels = useLabels();
|
|
1643
2623
|
if (attachments.length === 0) return null;
|
|
1644
2624
|
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("ul", {
|
|
1645
|
-
"aria-label":
|
|
2625
|
+
"aria-label": labels.attachments,
|
|
1646
2626
|
className: "flex gap-2.5 overflow-x-auto px-0.5 pt-2.5 pb-1 [scrollbar-width:thin]",
|
|
1647
2627
|
children: attachments.map((a) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(AttachmentCard, {
|
|
1648
2628
|
attachment: {
|
|
@@ -1659,7 +2639,9 @@ function AttachmentRow({ attachments, onRemove, onEditText }) {
|
|
|
1659
2639
|
})] });
|
|
1660
2640
|
}
|
|
1661
2641
|
/** Shows pasted text in a modal, editable when `onSave` is set. */
|
|
1662
|
-
function PastedTextDialog({ text, onClose, onSave, title
|
|
2642
|
+
function PastedTextDialog({ text, onClose, onSave, title }) {
|
|
2643
|
+
const labels = useLabels();
|
|
2644
|
+
title ??= labels.pastedText;
|
|
1663
2645
|
const [draft, setDraft] = useState(text ?? "");
|
|
1664
2646
|
useEffect(() => setDraft(text ?? ""), [text]);
|
|
1665
2647
|
return /* @__PURE__ */ jsxs(Modal, {
|
|
@@ -1671,34 +2653,34 @@ function PastedTextDialog({ text, onClose, onSave, title = "Pasted text" }) {
|
|
|
1671
2653
|
onChange: (e) => setDraft(e.target.value),
|
|
1672
2654
|
readOnly: !onSave,
|
|
1673
2655
|
"aria-label": title,
|
|
1674
|
-
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]"
|
|
1675
2657
|
}), /* @__PURE__ */ jsxs("div", {
|
|
1676
2658
|
className: "flex justify-end gap-2 px-5 py-4",
|
|
1677
2659
|
children: [/* @__PURE__ */ jsx(Button, {
|
|
1678
2660
|
variant: "subtle",
|
|
1679
2661
|
onPress: onClose,
|
|
1680
|
-
children: onSave ?
|
|
2662
|
+
children: onSave ? labels.cancel : labels.close
|
|
1681
2663
|
}), onSave && /* @__PURE__ */ jsx(Button, {
|
|
1682
2664
|
variant: "primary",
|
|
1683
2665
|
onPress: () => {
|
|
1684
2666
|
onSave(draft);
|
|
1685
2667
|
onClose();
|
|
1686
2668
|
},
|
|
1687
|
-
children:
|
|
2669
|
+
children: labels.save
|
|
1688
2670
|
})]
|
|
1689
2671
|
})]
|
|
1690
2672
|
});
|
|
1691
2673
|
}
|
|
1692
2674
|
// src/components/messages.tsx
|
|
1693
2675
|
/** "just now", "5 minutes ago", "21 hours ago", "yesterday", then a date. */
|
|
1694
|
-
function formatRelativeTime(iso, now = Date.now()) {
|
|
2676
|
+
function formatRelativeTime(iso, now = Date.now(), labels = defaultLabels) {
|
|
1695
2677
|
const seconds = (new Date(iso).getTime() - now) / 1e3;
|
|
1696
|
-
const rtf = new Intl.RelativeTimeFormat(
|
|
1697
|
-
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;
|
|
1698
2680
|
if (Math.abs(seconds) < 3600) return rtf.format(Math.round(seconds / 60), "minute");
|
|
1699
2681
|
if (Math.abs(seconds) < 86400) return rtf.format(Math.round(seconds / 3600), "hour");
|
|
1700
2682
|
if (Math.abs(seconds) < 604800) return rtf.format(Math.round(seconds / 86400), "day");
|
|
1701
|
-
return new Date(iso).toLocaleDateString(
|
|
2683
|
+
return new Date(iso).toLocaleDateString(labels.locale, {
|
|
1702
2684
|
month: "short",
|
|
1703
2685
|
day: "numeric"
|
|
1704
2686
|
});
|
|
@@ -1708,21 +2690,25 @@ function messageText(message) {
|
|
|
1708
2690
|
return message.parts.map((p) => p.type === "text" ? p.text : p.type === "custom" ? p.text ?? "" : "").filter(Boolean).join("\n\n");
|
|
1709
2691
|
}
|
|
1710
2692
|
function CopyButton({ text }) {
|
|
2693
|
+
const icons = useIcons();
|
|
2694
|
+
const labels = useLabels();
|
|
1711
2695
|
const [copied, setCopied] = useState(false);
|
|
1712
2696
|
return /* @__PURE__ */ jsx(IconButton, {
|
|
1713
|
-
label: copied ?
|
|
2697
|
+
label: copied ? labels.copied : labels.copy,
|
|
1714
2698
|
size: "sm",
|
|
1715
2699
|
onPress: () => {
|
|
1716
2700
|
navigator.clipboard?.writeText(text);
|
|
1717
2701
|
setCopied(true);
|
|
1718
2702
|
setTimeout(() => setCopied(false), 1500);
|
|
1719
2703
|
},
|
|
1720
|
-
children: copied ?
|
|
2704
|
+
children: copied ? icons.copied : icons.copy
|
|
1721
2705
|
});
|
|
1722
2706
|
}
|
|
1723
|
-
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";
|
|
1724
2708
|
/** The user's turn: a bubble on the right, with time, retry, edit and copy under it on hover. */
|
|
1725
2709
|
function UserMessage({ message, onRetry, onEdit, actions, className }) {
|
|
2710
|
+
const icons = useIcons();
|
|
2711
|
+
const labels = useLabels();
|
|
1726
2712
|
const text = messageText(message);
|
|
1727
2713
|
const attachments = message.parts.filter((p) => p.type === "attachment");
|
|
1728
2714
|
const [viewing, setViewing] = useState();
|
|
@@ -1730,7 +2716,7 @@ function UserMessage({ message, onRetry, onEdit, actions, className }) {
|
|
|
1730
2716
|
className: clsx("group/message flex flex-col items-end", className),
|
|
1731
2717
|
children: [
|
|
1732
2718
|
attachments.length > 0 && /* @__PURE__ */ jsx("ul", {
|
|
1733
|
-
"aria-label":
|
|
2719
|
+
"aria-label": labels.attachments,
|
|
1734
2720
|
className: "mb-2 flex max-w-[85%] flex-wrap justify-end gap-2",
|
|
1735
2721
|
children: attachments.map((a) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(AttachmentCard, {
|
|
1736
2722
|
attachment: a,
|
|
@@ -1742,28 +2728,25 @@ function UserMessage({ message, onRetry, onEdit, actions, className }) {
|
|
|
1742
2728
|
text: viewing,
|
|
1743
2729
|
onClose: () => setViewing(void 0)
|
|
1744
2730
|
}),
|
|
1745
|
-
text && /* @__PURE__ */ jsx(
|
|
1746
|
-
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",
|
|
1747
|
-
children: text
|
|
1748
|
-
}),
|
|
2731
|
+
text && /* @__PURE__ */ jsx(UserBubble, { text }),
|
|
1749
2732
|
/* @__PURE__ */ jsxs("div", {
|
|
1750
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",
|
|
1751
2734
|
children: [
|
|
1752
2735
|
/* @__PURE__ */ jsx("span", {
|
|
1753
2736
|
className: timeClass,
|
|
1754
|
-
children: formatRelativeTime(message.createdAt)
|
|
2737
|
+
children: formatRelativeTime(message.createdAt, void 0, labels)
|
|
1755
2738
|
}),
|
|
1756
2739
|
onRetry && /* @__PURE__ */ jsx(IconButton, {
|
|
1757
|
-
label:
|
|
2740
|
+
label: labels.retry,
|
|
1758
2741
|
size: "sm",
|
|
1759
2742
|
onPress: onRetry,
|
|
1760
|
-
children:
|
|
2743
|
+
children: icons.retry
|
|
1761
2744
|
}),
|
|
1762
2745
|
onEdit && /* @__PURE__ */ jsx(IconButton, {
|
|
1763
|
-
label:
|
|
2746
|
+
label: labels.edit,
|
|
1764
2747
|
size: "sm",
|
|
1765
2748
|
onPress: onEdit,
|
|
1766
|
-
children:
|
|
2749
|
+
children: icons.edit
|
|
1767
2750
|
}),
|
|
1768
2751
|
/* @__PURE__ */ jsx(CopyButton, { text }),
|
|
1769
2752
|
actions
|
|
@@ -1772,11 +2755,46 @@ function UserMessage({ message, onRetry, onEdit, actions, className }) {
|
|
|
1772
2755
|
]
|
|
1773
2756
|
});
|
|
1774
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
|
+
}
|
|
1775
2791
|
/**
|
|
1776
2792
|
* The assistant's turn: activity groups (each with its own token count), markdown, and other parts in
|
|
1777
2793
|
* order. When done, copy / read aloud / feedback / retry and the time sit under it.
|
|
1778
2794
|
*/
|
|
1779
2795
|
function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPart, actions, className }) {
|
|
2796
|
+
const icons = useIcons();
|
|
2797
|
+
const labels = useLabels();
|
|
1780
2798
|
const streaming = message.status === "streaming";
|
|
1781
2799
|
const views = useToolViews();
|
|
1782
2800
|
const blocks = toBlocks(message.parts, (part) => views[part.name]?.Block !== void 0);
|
|
@@ -1784,56 +2802,66 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
|
|
|
1784
2802
|
className: clsx("group/message flex flex-col gap-4", className),
|
|
1785
2803
|
children: [
|
|
1786
2804
|
blocks.map((block) => {
|
|
1787
|
-
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
|
+
}
|
|
1788
2814
|
if (block.kind === "tool") {
|
|
1789
2815
|
const ToolBlock = views[block.part.name].Block;
|
|
1790
2816
|
return /* @__PURE__ */ jsx(ToolBlock, { part: block.part }, block.id);
|
|
1791
2817
|
}
|
|
1792
2818
|
const part = block.part;
|
|
1793
2819
|
if (part.type === "text") return /* @__PURE__ */ jsx(Markdown, {
|
|
2820
|
+
partId: part.id,
|
|
1794
2821
|
streaming: part.status === "streaming",
|
|
1795
2822
|
children: part.text
|
|
1796
2823
|
}, part.id);
|
|
1797
2824
|
const custom = renderPart?.(part);
|
|
1798
2825
|
if (custom) return /* @__PURE__ */ jsx("div", { children: custom }, part.id);
|
|
2826
|
+
if (part.type === "widget") return /* @__PURE__ */ jsx(WidgetPartView, { part }, part.id);
|
|
1799
2827
|
if (part.type === "custom" && part.text) return /* @__PURE__ */ jsx(Markdown, { children: part.text }, part.id);
|
|
1800
2828
|
return null;
|
|
1801
2829
|
}),
|
|
1802
2830
|
message.status === "error" && /* @__PURE__ */ jsx("p", {
|
|
1803
|
-
className: "font-chat-sans text-
|
|
1804
|
-
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
|
|
1805
2833
|
}),
|
|
1806
2834
|
streaming ? /* @__PURE__ */ jsx("div", { className: "h-7" }) : /* @__PURE__ */ jsxs("div", {
|
|
1807
2835
|
className: "-ml-1.5 flex h-7 items-center gap-0.5",
|
|
1808
2836
|
children: [
|
|
1809
2837
|
/* @__PURE__ */ jsx(CopyButton, { text: messageText(message) }),
|
|
1810
2838
|
onReadAloud && /* @__PURE__ */ jsx(IconButton, {
|
|
1811
|
-
label:
|
|
2839
|
+
label: labels.readAloud,
|
|
1812
2840
|
size: "sm",
|
|
1813
2841
|
onPress: onReadAloud,
|
|
1814
|
-
children:
|
|
2842
|
+
children: icons.speak
|
|
1815
2843
|
}),
|
|
1816
2844
|
onFeedback && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(IconButton, {
|
|
1817
|
-
label:
|
|
2845
|
+
label: labels.goodResponse,
|
|
1818
2846
|
size: "sm",
|
|
1819
2847
|
onPress: () => onFeedback("good"),
|
|
1820
|
-
children:
|
|
2848
|
+
children: icons.thumbsUp
|
|
1821
2849
|
}), /* @__PURE__ */ jsx(IconButton, {
|
|
1822
|
-
label:
|
|
2850
|
+
label: labels.badResponse,
|
|
1823
2851
|
size: "sm",
|
|
1824
2852
|
onPress: () => onFeedback("bad"),
|
|
1825
|
-
children:
|
|
2853
|
+
children: icons.thumbsDown
|
|
1826
2854
|
})] }),
|
|
1827
2855
|
onRetry && /* @__PURE__ */ jsx(IconButton, {
|
|
1828
|
-
label:
|
|
2856
|
+
label: labels.retry,
|
|
1829
2857
|
size: "sm",
|
|
1830
2858
|
onPress: onRetry,
|
|
1831
|
-
children:
|
|
2859
|
+
children: icons.retry
|
|
1832
2860
|
}),
|
|
1833
2861
|
actions,
|
|
1834
2862
|
/* @__PURE__ */ jsx("span", {
|
|
1835
2863
|
className: timeClass,
|
|
1836
|
-
children: formatRelativeTime(message.createdAt)
|
|
2864
|
+
children: formatRelativeTime(message.createdAt, void 0, labels)
|
|
1837
2865
|
})
|
|
1838
2866
|
]
|
|
1839
2867
|
})
|
|
@@ -1846,7 +2874,9 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
|
|
|
1846
2874
|
* view's `Detail` when it has one, else the input. Replying in the composer instead declines it.
|
|
1847
2875
|
*/
|
|
1848
2876
|
function ApprovalCard({ approval, onRespond, className }) {
|
|
2877
|
+
const icons = useIcons();
|
|
1849
2878
|
const { part } = approval;
|
|
2879
|
+
const labels = useLabels();
|
|
1850
2880
|
const view = useToolView(part.name);
|
|
1851
2881
|
const Detail = view.Detail;
|
|
1852
2882
|
const preview = {
|
|
@@ -1855,21 +2885,17 @@ function ApprovalCard({ approval, onRespond, className }) {
|
|
|
1855
2885
|
};
|
|
1856
2886
|
return /* @__PURE__ */ jsxs("div", {
|
|
1857
2887
|
role: "alertdialog",
|
|
1858
|
-
"aria-label":
|
|
2888
|
+
"aria-label": labels.allowTool(part.name),
|
|
1859
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),
|
|
1860
2890
|
children: [
|
|
1861
2891
|
/* @__PURE__ */ jsxs("div", {
|
|
1862
|
-
className: "flex items-center gap-2 text-
|
|
1863
|
-
children: [/* @__PURE__ */ jsx(
|
|
1864
|
-
"
|
|
1865
|
-
|
|
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
|
|
1866
2896
|
}), /* @__PURE__ */ jsx("span", {
|
|
1867
2897
|
className: "min-w-0 flex-1 font-medium",
|
|
1868
|
-
children: view.ask?.(part) ??
|
|
1869
|
-
"Allow ",
|
|
1870
|
-
activityLabel(part, view),
|
|
1871
|
-
"?"
|
|
1872
|
-
] })
|
|
2898
|
+
children: view.ask?.(part) ?? labels.allowAction(activityLabel(part, labels, view))
|
|
1873
2899
|
})]
|
|
1874
2900
|
}),
|
|
1875
2901
|
/* @__PURE__ */ jsx("div", {
|
|
@@ -1886,12 +2912,12 @@ function ApprovalCard({ approval, onRespond, className }) {
|
|
|
1886
2912
|
variant: "ghost",
|
|
1887
2913
|
className: "border border-chat-border-strong",
|
|
1888
2914
|
onPress: () => onRespond(part.id, false),
|
|
1889
|
-
children:
|
|
2915
|
+
children: labels.deny
|
|
1890
2916
|
}), /* @__PURE__ */ jsx(Button, {
|
|
1891
2917
|
variant: "primary",
|
|
1892
2918
|
autoFocus: true,
|
|
1893
2919
|
onPress: () => onRespond(part.id, true),
|
|
1894
|
-
children:
|
|
2920
|
+
children: labels.allow
|
|
1895
2921
|
})]
|
|
1896
2922
|
})
|
|
1897
2923
|
]
|
|
@@ -1904,7 +2930,8 @@ const DEFAULT_COLOR = "var(--chat-subtle)";
|
|
|
1904
2930
|
* What fills the context window: a usage line, a segmented bar, one row per category with its share, and
|
|
1905
2931
|
* expandable lists (each MCP tool, each memory file). Deferred categories are listed but not counted.
|
|
1906
2932
|
*/
|
|
1907
|
-
function ContextPanel({ usage, title
|
|
2933
|
+
function ContextPanel({ usage, title, defaultExpanded = true, className }) {
|
|
2934
|
+
const labels = useLabels();
|
|
1908
2935
|
const counted = usage.categories.filter((c) => !c.deferred);
|
|
1909
2936
|
const deferred = usage.categories.filter((c) => c.deferred);
|
|
1910
2937
|
const used = counted.reduce((sum, c) => sum + (c.reserved ? 0 : c.tokens), 0);
|
|
@@ -1912,16 +2939,16 @@ function ContextPanel({ usage, title = "Context", defaultExpanded = true, classN
|
|
|
1912
2939
|
const free = Math.max(usage.limit - used - reserved, 0);
|
|
1913
2940
|
const share = (tokens) => formatShare(tokens / usage.limit);
|
|
1914
2941
|
return /* @__PURE__ */ jsxs(Disclosure, {
|
|
1915
|
-
summary: title,
|
|
2942
|
+
summary: title ?? labels.context,
|
|
1916
2943
|
defaultExpanded,
|
|
1917
2944
|
className: clsx("font-chat-sans text-chat-fg", className),
|
|
1918
|
-
summaryClassName: "text-
|
|
2945
|
+
summaryClassName: "text-(length:--chat-activity-font-size) text-chat-fg",
|
|
1919
2946
|
panelClassName: "pt-3",
|
|
1920
2947
|
children: [
|
|
1921
2948
|
/* @__PURE__ */ jsxs("div", {
|
|
1922
|
-
className: "mb-2 flex items-baseline justify-between text-
|
|
1923
|
-
children: [/* @__PURE__ */
|
|
1924
|
-
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",
|
|
1925
2952
|
children: [
|
|
1926
2953
|
formatTokens(used),
|
|
1927
2954
|
" / ",
|
|
@@ -1931,7 +2958,7 @@ function ContextPanel({ usage, title = "Context", defaultExpanded = true, classN
|
|
|
1931
2958
|
}),
|
|
1932
2959
|
/* @__PURE__ */ jsx("div", {
|
|
1933
2960
|
role: "meter",
|
|
1934
|
-
"aria-label":
|
|
2961
|
+
"aria-label": labels.contextUsed,
|
|
1935
2962
|
"aria-valuemin": 0,
|
|
1936
2963
|
"aria-valuemax": usage.limit,
|
|
1937
2964
|
"aria-valuenow": used,
|
|
@@ -1953,7 +2980,7 @@ function ContextPanel({ usage, title = "Context", defaultExpanded = true, classN
|
|
|
1953
2980
|
}, c.id)), /* @__PURE__ */ jsx(CategoryRow, {
|
|
1954
2981
|
category: {
|
|
1955
2982
|
id: "free",
|
|
1956
|
-
label:
|
|
2983
|
+
label: labels.freeSpace,
|
|
1957
2984
|
tokens: free,
|
|
1958
2985
|
color: FREE_COLOR
|
|
1959
2986
|
},
|
|
@@ -1984,7 +3011,7 @@ function CategoryRow({ category, share }) {
|
|
|
1984
3011
|
}) })
|
|
1985
3012
|
}) : category.label;
|
|
1986
3013
|
return /* @__PURE__ */ jsxs("li", {
|
|
1987
|
-
className: "flex items-center gap-2.5 text-
|
|
3014
|
+
className: "flex items-center gap-2.5 text-(length:--chat-ui-font-size) leading-5",
|
|
1988
3015
|
children: [
|
|
1989
3016
|
/* @__PURE__ */ jsx("span", {
|
|
1990
3017
|
"aria-hidden": true,
|
|
@@ -2010,16 +3037,16 @@ function GroupList({ group }) {
|
|
|
2010
3037
|
return /* @__PURE__ */ jsx(Disclosure, {
|
|
2011
3038
|
summary: group.label,
|
|
2012
3039
|
trailing: /* @__PURE__ */ jsx("span", {
|
|
2013
|
-
className: "text-
|
|
3040
|
+
className: "text-(length:--chat-ui-font-size) leading-5 text-chat-subtle tabular-nums",
|
|
2014
3041
|
children: group.items.length
|
|
2015
3042
|
}),
|
|
2016
3043
|
indicator: "chevron",
|
|
2017
|
-
summaryClassName: "text-
|
|
3044
|
+
summaryClassName: "text-(length:--chat-ui-font-size) leading-5",
|
|
2018
3045
|
panelClassName: "pt-1.5",
|
|
2019
3046
|
children: /* @__PURE__ */ jsx("ul", {
|
|
2020
3047
|
className: "max-h-72 space-y-1 overflow-y-auto pl-6 [scrollbar-color:var(--chat-border-strong)_transparent] [scrollbar-width:thin]",
|
|
2021
3048
|
children: group.items.map((item) => /* @__PURE__ */ jsxs("li", {
|
|
2022
|
-
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",
|
|
2023
3050
|
children: [/* @__PURE__ */ jsx("span", {
|
|
2024
3051
|
className: "min-w-0 flex-1 truncate",
|
|
2025
3052
|
children: item.label
|
|
@@ -2038,7 +3065,9 @@ function formatShare(fraction) {
|
|
|
2038
3065
|
/** Touch keyboards have no easy Shift+Enter, so there Enter adds a line and the button sends. */
|
|
2039
3066
|
const coarsePointer = () => typeof window !== "undefined" && window.matchMedia?.("(pointer: coarse)").matches;
|
|
2040
3067
|
/** The chat box: growing textarea, toolbar slots, send / stop. Enter sends, Shift+Enter adds a line. */
|
|
2041
|
-
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();
|
|
2042
3071
|
const [own, setOwn] = useState("");
|
|
2043
3072
|
const text = value ?? own;
|
|
2044
3073
|
const setText = (next) => {
|
|
@@ -2111,18 +3140,18 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
|
|
|
2111
3140
|
};
|
|
2112
3141
|
const compact = variant === "compact";
|
|
2113
3142
|
const action = busy ? /* @__PURE__ */ jsx(IconButton, {
|
|
2114
|
-
label:
|
|
3143
|
+
label: labels.stop,
|
|
2115
3144
|
variant: "primary",
|
|
2116
3145
|
onPress: onStop,
|
|
2117
3146
|
className: "rounded-chat-md [&_svg]:size-3.5 [&_svg]:fill-current",
|
|
2118
|
-
children:
|
|
3147
|
+
children: icons.stop
|
|
2119
3148
|
}) : /* @__PURE__ */ jsx(IconButton, {
|
|
2120
|
-
label:
|
|
3149
|
+
label: labels.send,
|
|
2121
3150
|
variant: "primary",
|
|
2122
3151
|
onPress: submit,
|
|
2123
3152
|
isDisabled: !canSend,
|
|
2124
3153
|
className: "rounded-chat-md",
|
|
2125
|
-
children:
|
|
3154
|
+
children: icons.send
|
|
2126
3155
|
});
|
|
2127
3156
|
return /* @__PURE__ */ jsxs("div", {
|
|
2128
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),
|
|
@@ -2144,8 +3173,8 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
|
|
|
2144
3173
|
onChange: (e) => setText(e.target.value),
|
|
2145
3174
|
onKeyDown,
|
|
2146
3175
|
onPaste,
|
|
2147
|
-
placeholder,
|
|
2148
|
-
"aria-label":
|
|
3176
|
+
placeholder: placeholder ?? labels.replyPlaceholder,
|
|
3177
|
+
"aria-label": labels.message,
|
|
2149
3178
|
disabled: isDisabled,
|
|
2150
3179
|
autoFocus,
|
|
2151
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")
|
|
@@ -2169,10 +3198,12 @@ function Composer({ onSubmit, onStop, busy = false, value, onChange, placeholder
|
|
|
2169
3198
|
});
|
|
2170
3199
|
}
|
|
2171
3200
|
/** The `+` button that opens the attach menu above the composer. */
|
|
2172
|
-
function AttachButton({ children, label
|
|
3201
|
+
function AttachButton({ children, label }) {
|
|
3202
|
+
const icons = useIcons();
|
|
3203
|
+
const labels = useLabels();
|
|
2173
3204
|
return /* @__PURE__ */ jsxs(MenuTrigger, { children: [/* @__PURE__ */ jsx(IconButton, {
|
|
2174
|
-
label,
|
|
2175
|
-
children:
|
|
3205
|
+
label: label ?? labels.addFiles,
|
|
3206
|
+
children: icons.attach
|
|
2176
3207
|
}), /* @__PURE__ */ jsx(Popover, {
|
|
2177
3208
|
placement: "top start",
|
|
2178
3209
|
children
|
|
@@ -2180,7 +3211,9 @@ function AttachButton({ children, label = "Add files and more" }) {
|
|
|
2180
3211
|
}
|
|
2181
3212
|
// src/components/model-picker.tsx
|
|
2182
3213
|
/** "Opus 5.5 High ⌄": the models, an Effort submenu for models that have levels, and More models. */
|
|
2183
|
-
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();
|
|
2184
3217
|
const current = models.find((m) => m.id === model);
|
|
2185
3218
|
const efforts = current?.efforts ?? [];
|
|
2186
3219
|
const currentEffort = efforts.find((e) => e.id === (effort ?? current?.defaultEffort));
|
|
@@ -2199,7 +3232,7 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
|
|
|
2199
3232
|
};
|
|
2200
3233
|
return /* @__PURE__ */ jsxs(MenuTrigger, { children: [/* @__PURE__ */ jsxs(Button, {
|
|
2201
3234
|
size: "sm",
|
|
2202
|
-
"aria-label":
|
|
3235
|
+
"aria-label": labels.modelButton(current?.label ?? model),
|
|
2203
3236
|
className: clsx("text-chat-muted data-hovered:text-chat-fg", className),
|
|
2204
3237
|
children: [
|
|
2205
3238
|
/* @__PURE__ */ jsx("span", {
|
|
@@ -2210,16 +3243,16 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
|
|
|
2210
3243
|
className: "chat-picker-detail text-chat-muted",
|
|
2211
3244
|
children: currentEffort.label
|
|
2212
3245
|
}),
|
|
2213
|
-
/* @__PURE__ */ jsx(
|
|
2214
|
-
"
|
|
2215
|
-
|
|
3246
|
+
/* @__PURE__ */ jsx(IconSlot, {
|
|
3247
|
+
className: "[&_svg]:size-3.5",
|
|
3248
|
+
children: icons.dropdown
|
|
2216
3249
|
})
|
|
2217
3250
|
]
|
|
2218
3251
|
}), /* @__PURE__ */ jsxs(Popover, {
|
|
2219
3252
|
placement: "top end",
|
|
2220
3253
|
className: "w-72",
|
|
2221
3254
|
children: [/* @__PURE__ */ jsxs(Menu, {
|
|
2222
|
-
"aria-label":
|
|
3255
|
+
"aria-label": labels.model,
|
|
2223
3256
|
children: [
|
|
2224
3257
|
/* @__PURE__ */ jsx(MenuSection, {
|
|
2225
3258
|
selectionMode: "single",
|
|
@@ -2231,12 +3264,12 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
|
|
|
2231
3264
|
efforts.length > 0 && onEffortChange && /* @__PURE__ */ jsxs(SubmenuTrigger, { children: [/* @__PURE__ */ jsx(MenuItem, {
|
|
2232
3265
|
id: "__effort",
|
|
2233
3266
|
detail: currentEffort?.label,
|
|
2234
|
-
children:
|
|
3267
|
+
children: labels.effort
|
|
2235
3268
|
}), /* @__PURE__ */ jsxs(Popover, {
|
|
2236
3269
|
placement: "end top",
|
|
2237
3270
|
className: "w-80",
|
|
2238
|
-
children: [/* @__PURE__ */ jsx(MenuNote, { children: effortNote }), /* @__PURE__ */ jsx(Menu, {
|
|
2239
|
-
"aria-label":
|
|
3271
|
+
children: [/* @__PURE__ */ jsx(MenuNote, { children: effortNote ?? labels.effortNote }), /* @__PURE__ */ jsx(Menu, {
|
|
3272
|
+
"aria-label": labels.effort,
|
|
2240
3273
|
selectionMode: "single",
|
|
2241
3274
|
selectedKeys: currentEffort ? [currentEffort.id] : [],
|
|
2242
3275
|
onSelectionChange: (keys) => {
|
|
@@ -2251,10 +3284,10 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
|
|
|
2251
3284
|
className: "flex items-center gap-2",
|
|
2252
3285
|
children: [
|
|
2253
3286
|
e.label,
|
|
2254
|
-
e.recommended && /* @__PURE__ */ jsx(Badge, { children:
|
|
3287
|
+
e.recommended && /* @__PURE__ */ jsx(Badge, { children: labels.recommended }),
|
|
2255
3288
|
e.warning && /* @__PURE__ */ jsx(Badge, {
|
|
2256
3289
|
tone: "warning",
|
|
2257
|
-
icon:
|
|
3290
|
+
icon: icons.warning,
|
|
2258
3291
|
children: e.warning
|
|
2259
3292
|
})
|
|
2260
3293
|
]
|
|
@@ -2264,12 +3297,12 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
|
|
|
2264
3297
|
})] }),
|
|
2265
3298
|
more.length > 0 && /* @__PURE__ */ jsxs(SubmenuTrigger, { children: [/* @__PURE__ */ jsx(MenuItem, {
|
|
2266
3299
|
id: "__more",
|
|
2267
|
-
children:
|
|
3300
|
+
children: labels.moreModels
|
|
2268
3301
|
}), /* @__PURE__ */ jsx(Popover, {
|
|
2269
3302
|
placement: "end top",
|
|
2270
3303
|
className: "w-72",
|
|
2271
3304
|
children: /* @__PURE__ */ jsx(Menu, {
|
|
2272
|
-
"aria-label":
|
|
3305
|
+
"aria-label": labels.moreModels,
|
|
2273
3306
|
selectionMode: "single",
|
|
2274
3307
|
selectedKeys: [model],
|
|
2275
3308
|
onSelectionChange: pick,
|
|
@@ -2278,7 +3311,7 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
|
|
|
2278
3311
|
})] })
|
|
2279
3312
|
]
|
|
2280
3313
|
}), footnote && /* @__PURE__ */ jsx(MenuNote, {
|
|
2281
|
-
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",
|
|
2282
3315
|
children: footnote
|
|
2283
3316
|
})]
|
|
2284
3317
|
})] });
|
|
@@ -2290,10 +3323,12 @@ const STICK_SLOP = 48;
|
|
|
2290
3323
|
* The scrolling conversation. It follows a streaming reply while the reader is at the bottom and leaves
|
|
2291
3324
|
* them alone once they scroll up, with a button to jump back. Sending a message always jumps down.
|
|
2292
3325
|
*/
|
|
2293
|
-
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();
|
|
2294
3328
|
const scroller = useRef(null);
|
|
2295
3329
|
const content = useRef(null);
|
|
2296
|
-
const stuck = useRef(
|
|
3330
|
+
const stuck = useRef(start === "bottom");
|
|
3331
|
+
const labels = useLabels();
|
|
2297
3332
|
const [atBottom, setAtBottom] = useState(true);
|
|
2298
3333
|
const toBottom = useCallback((behavior = "auto") => {
|
|
2299
3334
|
const el = scroller.current;
|
|
@@ -2319,7 +3354,13 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, footer
|
|
|
2319
3354
|
return () => observer.disconnect();
|
|
2320
3355
|
}, [toBottom]);
|
|
2321
3356
|
const lastUser = messages.findLast((m) => m.role === "user")?.id;
|
|
3357
|
+
const jumpedFor = useRef(start === "top" ? lastUser : null);
|
|
2322
3358
|
useLayoutEffect(() => {
|
|
3359
|
+
if (lastUser === jumpedFor.current) {
|
|
3360
|
+
onScroll();
|
|
3361
|
+
return;
|
|
3362
|
+
}
|
|
3363
|
+
jumpedFor.current = lastUser;
|
|
2323
3364
|
stuck.current = true;
|
|
2324
3365
|
setAtBottom(true);
|
|
2325
3366
|
toBottom();
|
|
@@ -2333,11 +3374,11 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, footer
|
|
|
2333
3374
|
children: /* @__PURE__ */ jsxs("div", {
|
|
2334
3375
|
ref: content,
|
|
2335
3376
|
className: "flex min-h-full flex-col",
|
|
2336
|
-
children: [/* @__PURE__ */
|
|
3377
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2337
3378
|
role: "log",
|
|
2338
|
-
"aria-label":
|
|
3379
|
+
"aria-label": labels.conversation,
|
|
2339
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",
|
|
2340
|
-
children: messages.map((m) => m.role === "user" ? /* @__PURE__ */ jsx(UserMessage, {
|
|
3381
|
+
children: [header, messages.map((m) => m.role === "user" ? /* @__PURE__ */ jsx(UserMessage, {
|
|
2341
3382
|
message: m,
|
|
2342
3383
|
...onEdit && { onEdit: () => onEdit(m.id) }
|
|
2343
3384
|
}, m.id) : /* @__PURE__ */ jsx(AssistantMessage, {
|
|
@@ -2345,27 +3386,28 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, renderPart, footer
|
|
|
2345
3386
|
...onRetry && { onRetry: () => onRetry(m.id) },
|
|
2346
3387
|
...onFeedback && { onFeedback: (f) => onFeedback(m.id, f) },
|
|
2347
3388
|
...renderPart && { renderPart }
|
|
2348
|
-
}, m.id))
|
|
3389
|
+
}, m.id))]
|
|
2349
3390
|
}), footer && /* @__PURE__ */ jsx("div", {
|
|
2350
3391
|
className: "sticky bottom-0",
|
|
2351
3392
|
children: footer
|
|
2352
3393
|
})]
|
|
2353
3394
|
})
|
|
2354
3395
|
}), !atBottom && /* @__PURE__ */ jsx(IconButton, {
|
|
2355
|
-
label:
|
|
3396
|
+
label: labels.scrollToBottom,
|
|
2356
3397
|
variant: "subtle",
|
|
2357
3398
|
onPress: () => {
|
|
2358
3399
|
stuck.current = true;
|
|
2359
3400
|
toBottom("smooth");
|
|
2360
3401
|
},
|
|
2361
3402
|
className: "absolute bottom-[min(10rem,40%)] left-1/2 -translate-x-1/2 rounded-full border border-chat-border shadow-chat-popover",
|
|
2362
|
-
children:
|
|
3403
|
+
children: icons.scrollToBottom
|
|
2363
3404
|
})]
|
|
2364
3405
|
});
|
|
2365
3406
|
}
|
|
2366
3407
|
// src/components/chat-header.tsx
|
|
2367
3408
|
/** The bar above a chat: breadcrumb and title with its menu, actions on the right. */
|
|
2368
3409
|
function ChatHeader({ title, menu, leading, actions, className }) {
|
|
3410
|
+
const icons = useIcons();
|
|
2369
3411
|
return /* @__PURE__ */ jsxs("header", {
|
|
2370
3412
|
className: clsx("flex h-12 shrink-0 items-center gap-1 px-3 font-chat-sans", className),
|
|
2371
3413
|
children: [
|
|
@@ -2376,15 +3418,15 @@ function ChatHeader({ title, menu, leading, actions, className }) {
|
|
|
2376
3418
|
children: [/* @__PURE__ */ jsx("span", {
|
|
2377
3419
|
className: "truncate",
|
|
2378
3420
|
children: title
|
|
2379
|
-
}), /* @__PURE__ */ jsx(
|
|
2380
|
-
"
|
|
2381
|
-
|
|
3421
|
+
}), /* @__PURE__ */ jsx(IconSlot, {
|
|
3422
|
+
className: "text-chat-muted [&_svg]:size-3.5",
|
|
3423
|
+
children: icons.dropdown
|
|
2382
3424
|
})]
|
|
2383
3425
|
}), /* @__PURE__ */ jsx(Popover, {
|
|
2384
3426
|
placement: "bottom start",
|
|
2385
3427
|
children: menu
|
|
2386
3428
|
})] }) : /* @__PURE__ */ jsx("h1", {
|
|
2387
|
-
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",
|
|
2388
3430
|
children: title
|
|
2389
3431
|
}),
|
|
2390
3432
|
/* @__PURE__ */ jsx("div", {
|
|
@@ -2396,6 +3438,8 @@ function ChatHeader({ title, menu, leading, actions, className }) {
|
|
|
2396
3438
|
}
|
|
2397
3439
|
/** Pin, Rename, Add to project (searchable), Share, Delete. Each shows only when its handler is set. */
|
|
2398
3440
|
function ChatTitleMenu({ pinned, onPin, onRename, projects, onAddToProject, onCreateProject, onShare, onDelete, children }) {
|
|
3441
|
+
const icons = useIcons();
|
|
3442
|
+
const labels = useLabels();
|
|
2399
3443
|
const act = (key) => {
|
|
2400
3444
|
if (key === "pin") onPin?.();
|
|
2401
3445
|
else if (key === "rename") onRename?.();
|
|
@@ -2403,39 +3447,39 @@ function ChatTitleMenu({ pinned, onPin, onRename, projects, onAddToProject, onCr
|
|
|
2403
3447
|
else if (key === "delete") onDelete?.();
|
|
2404
3448
|
};
|
|
2405
3449
|
return /* @__PURE__ */ jsxs(Menu, {
|
|
2406
|
-
"aria-label":
|
|
3450
|
+
"aria-label": labels.chatActions,
|
|
2407
3451
|
className: "min-w-44",
|
|
2408
3452
|
onAction: act,
|
|
2409
3453
|
children: [
|
|
2410
3454
|
onPin && /* @__PURE__ */ jsx(MenuItem, {
|
|
2411
3455
|
id: "pin",
|
|
2412
|
-
icon: pinned ?
|
|
3456
|
+
icon: pinned ? icons.unpin : icons.pin,
|
|
2413
3457
|
shortcut: "P",
|
|
2414
|
-
children: pinned ?
|
|
3458
|
+
children: pinned ? labels.unpin : labels.pin
|
|
2415
3459
|
}),
|
|
2416
3460
|
onRename && /* @__PURE__ */ jsx(MenuItem, {
|
|
2417
3461
|
id: "rename",
|
|
2418
|
-
icon:
|
|
3462
|
+
icon: icons.rename,
|
|
2419
3463
|
shortcut: "R",
|
|
2420
|
-
children:
|
|
3464
|
+
children: labels.rename
|
|
2421
3465
|
}),
|
|
2422
3466
|
onAddToProject && /* @__PURE__ */ jsxs(SubmenuTrigger, { children: [/* @__PURE__ */ jsx(MenuItem, {
|
|
2423
3467
|
id: "project",
|
|
2424
|
-
icon:
|
|
2425
|
-
children:
|
|
3468
|
+
icon: icons.addToProject,
|
|
3469
|
+
children: labels.addToProject
|
|
2426
3470
|
}), /* @__PURE__ */ jsx(Popover, {
|
|
2427
3471
|
placement: "end top",
|
|
2428
3472
|
className: "w-60",
|
|
2429
3473
|
children: /* @__PURE__ */ jsx(MenuSearch, {
|
|
2430
|
-
placeholder:
|
|
3474
|
+
placeholder: labels.searchProjects,
|
|
2431
3475
|
children: /* @__PURE__ */ jsxs(Menu, {
|
|
2432
|
-
"aria-label":
|
|
3476
|
+
"aria-label": labels.projects,
|
|
2433
3477
|
className: "max-h-72",
|
|
2434
3478
|
onAction: (key) => key === "__new" ? onCreateProject?.() : onAddToProject(String(key)),
|
|
2435
3479
|
children: [onCreateProject && /* @__PURE__ */ jsx(MenuItem, {
|
|
2436
3480
|
id: "__new",
|
|
2437
|
-
icon:
|
|
2438
|
-
children:
|
|
3481
|
+
icon: icons.add,
|
|
3482
|
+
children: labels.newProject
|
|
2439
3483
|
}), (projects ?? []).map((p) => /* @__PURE__ */ jsx(MenuItem, {
|
|
2440
3484
|
id: p.id,
|
|
2441
3485
|
children: p.name
|
|
@@ -2445,28 +3489,50 @@ function ChatTitleMenu({ pinned, onPin, onRename, projects, onAddToProject, onCr
|
|
|
2445
3489
|
})] }),
|
|
2446
3490
|
onShare && /* @__PURE__ */ jsx(MenuItem, {
|
|
2447
3491
|
id: "share",
|
|
2448
|
-
icon:
|
|
2449
|
-
children:
|
|
3492
|
+
icon: icons.share,
|
|
3493
|
+
children: labels.shareChat
|
|
2450
3494
|
}),
|
|
2451
3495
|
children,
|
|
2452
3496
|
onDelete && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuSeparator, {}), /* @__PURE__ */ jsx(MenuItem, {
|
|
2453
3497
|
id: "delete",
|
|
2454
|
-
icon:
|
|
3498
|
+
icon: icons.delete,
|
|
2455
3499
|
shortcut: "D",
|
|
2456
3500
|
tone: "danger",
|
|
2457
|
-
children:
|
|
3501
|
+
children: labels.delete
|
|
2458
3502
|
})] })
|
|
2459
3503
|
]
|
|
2460
3504
|
});
|
|
2461
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
|
+
}
|
|
2462
3526
|
// src/components/chat-list.tsx
|
|
2463
3527
|
const isProject = (item) => "type" in item && item.type === "project";
|
|
2464
|
-
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");
|
|
2465
3529
|
/**
|
|
2466
3530
|
* The chat picker: sections of chats and projects, collapsible, with an active row, running and unread
|
|
2467
3531
|
* markers and a per-row menu. Usable on its own (in a host's own sidebar) or inside `Sidebar`.
|
|
2468
3532
|
*/
|
|
2469
3533
|
function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }) {
|
|
3534
|
+
const icons = useIcons();
|
|
3535
|
+
const labels = useLabels();
|
|
2470
3536
|
const row = (chat, nested = false) => /* @__PURE__ */ jsx(ChatRow, {
|
|
2471
3537
|
chat,
|
|
2472
3538
|
active: chat.id === activeId,
|
|
@@ -2480,13 +3546,13 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
|
|
|
2480
3546
|
row
|
|
2481
3547
|
}, item.id) : row(item));
|
|
2482
3548
|
return /* @__PURE__ */ jsx("nav", {
|
|
2483
|
-
"aria-label": props["aria-label"] ??
|
|
3549
|
+
"aria-label": props["aria-label"] ?? labels.chats,
|
|
2484
3550
|
className: clsx("flex flex-col gap-3", className),
|
|
2485
3551
|
children: sections.map((section) => section.title ? /* @__PURE__ */ jsxs(Disclosure$1, {
|
|
2486
3552
|
defaultExpanded: section.defaultExpanded ?? true,
|
|
2487
3553
|
className: "group/section",
|
|
2488
3554
|
children: [/* @__PURE__ */ jsxs(Heading, {
|
|
2489
|
-
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",
|
|
2490
3556
|
children: [
|
|
2491
3557
|
/* @__PURE__ */ jsxs(Button$1, {
|
|
2492
3558
|
slot: "trigger",
|
|
@@ -2494,9 +3560,9 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
|
|
|
2494
3560
|
children: [/* @__PURE__ */ jsx("span", {
|
|
2495
3561
|
className: "truncate",
|
|
2496
3562
|
children: section.title
|
|
2497
|
-
}), /* @__PURE__ */ jsx(
|
|
2498
|
-
"aria-
|
|
2499
|
-
|
|
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
|
|
2500
3566
|
})]
|
|
2501
3567
|
}),
|
|
2502
3568
|
section.badge,
|
|
@@ -2511,7 +3577,7 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
|
|
|
2511
3577
|
}), section.onViewAll && /* @__PURE__ */ jsx(Button$1, {
|
|
2512
3578
|
onPress: section.onViewAll,
|
|
2513
3579
|
className: clsx(rowClass, "text-chat-muted"),
|
|
2514
|
-
children:
|
|
3580
|
+
children: labels.viewAll
|
|
2515
3581
|
})] })]
|
|
2516
3582
|
}, section.id) : /* @__PURE__ */ jsx("ul", {
|
|
2517
3583
|
className: "flex flex-col gap-px",
|
|
@@ -2520,16 +3586,18 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
|
|
|
2520
3586
|
});
|
|
2521
3587
|
}
|
|
2522
3588
|
function ChatRow({ chat, active, nested, onSelect, menu }) {
|
|
3589
|
+
const icons = useIcons();
|
|
2523
3590
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
2524
3591
|
const { dismiss } = useSidebar();
|
|
3592
|
+
const labels = useLabels();
|
|
2525
3593
|
const markerClass = "size-2 shrink-0 rounded-full";
|
|
2526
3594
|
const marker = chat.running ? /* @__PURE__ */ jsx("span", {
|
|
2527
3595
|
role: "img",
|
|
2528
|
-
"aria-label":
|
|
3596
|
+
"aria-label": labels.running,
|
|
2529
3597
|
className: clsx(markerClass, "animate-pulse bg-chat-accent motion-reduce:animate-none")
|
|
2530
3598
|
}) : chat.unread ? /* @__PURE__ */ jsx("span", {
|
|
2531
3599
|
role: "img",
|
|
2532
|
-
"aria-label":
|
|
3600
|
+
"aria-label": labels.unread,
|
|
2533
3601
|
className: clsx(markerClass, "bg-chat-check")
|
|
2534
3602
|
}) : null;
|
|
2535
3603
|
const content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
@@ -2554,11 +3622,11 @@ function ChatRow({ chat, active, nested, onSelect, menu }) {
|
|
|
2554
3622
|
isOpen: menuOpen,
|
|
2555
3623
|
onOpenChange: setMenuOpen,
|
|
2556
3624
|
children: [/* @__PURE__ */ jsx(IconButton, {
|
|
2557
|
-
label:
|
|
3625
|
+
label: labels.moreOptionsFor(chat.title),
|
|
2558
3626
|
size: "sm",
|
|
2559
3627
|
tooltip: false,
|
|
2560
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"),
|
|
2561
|
-
children:
|
|
3629
|
+
children: icons.more
|
|
2562
3630
|
}), /* @__PURE__ */ jsx(Popover, {
|
|
2563
3631
|
placement: "bottom start",
|
|
2564
3632
|
children: menu
|
|
@@ -2567,6 +3635,8 @@ function ChatRow({ chat, active, nested, onSelect, menu }) {
|
|
|
2567
3635
|
});
|
|
2568
3636
|
}
|
|
2569
3637
|
function ProjectRow({ project, activeId, row }) {
|
|
3638
|
+
const icons = useIcons();
|
|
3639
|
+
const labels = useLabels();
|
|
2570
3640
|
const holdsActive = project.chats.some((c) => c.id === activeId);
|
|
2571
3641
|
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Disclosure$1, {
|
|
2572
3642
|
defaultExpanded: project.defaultExpanded ?? holdsActive,
|
|
@@ -2576,18 +3646,15 @@ function ProjectRow({ project, activeId, row }) {
|
|
|
2576
3646
|
slot: "trigger",
|
|
2577
3647
|
className: rowClass,
|
|
2578
3648
|
children: [
|
|
2579
|
-
|
|
2580
|
-
"
|
|
2581
|
-
|
|
2582
|
-
}) : /* @__PURE__ */ jsx(Folder, {
|
|
2583
|
-
"aria-hidden": true,
|
|
2584
|
-
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
|
|
2585
3652
|
}),
|
|
2586
3653
|
/* @__PURE__ */ jsx("span", {
|
|
2587
3654
|
className: "min-w-0 flex-1 truncate",
|
|
2588
3655
|
children: project.name
|
|
2589
3656
|
}),
|
|
2590
|
-
!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 })
|
|
2591
3658
|
]
|
|
2592
3659
|
})
|
|
2593
3660
|
}), /* @__PURE__ */ jsx(DisclosurePanel, { children: /* @__PURE__ */ jsx("ul", {
|
|
@@ -2597,7 +3664,11 @@ function ProjectRow({ project, activeId, row }) {
|
|
|
2597
3664
|
}) });
|
|
2598
3665
|
}
|
|
2599
3666
|
// src/views/chat-view.tsx
|
|
2600
|
-
/**
|
|
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
|
+
*/
|
|
2601
3672
|
const densityVars = {
|
|
2602
3673
|
full: {},
|
|
2603
3674
|
panel: {
|
|
@@ -2612,14 +3683,20 @@ const densityVars = {
|
|
|
2612
3683
|
}
|
|
2613
3684
|
};
|
|
2614
3685
|
/** A whole chat: header, the conversation, the docked composer. An empty chat shows `emptyState`. */
|
|
2615
|
-
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 }) {
|
|
2616
3687
|
const style = {
|
|
2617
3688
|
...densityVars[density],
|
|
2618
3689
|
...contentWidth && { "--chat-content-width": contentWidth },
|
|
2619
3690
|
...contentPadding && { "--chat-content-padding": contentPadding }
|
|
2620
3691
|
};
|
|
3692
|
+
const host = useMemo(() => ({
|
|
3693
|
+
onPrompt: onSend,
|
|
3694
|
+
onOpenLink: openInNewTab,
|
|
3695
|
+
...widgets
|
|
3696
|
+
}), [onSend, widgets]);
|
|
2621
3697
|
const empty = messages.length === 0;
|
|
2622
3698
|
const [body, short] = useShortPane();
|
|
3699
|
+
const labels = useLabels();
|
|
2623
3700
|
const pending = onApprove && approvals?.[0];
|
|
2624
3701
|
const box = (variant) => /* @__PURE__ */ jsx(Composer, {
|
|
2625
3702
|
variant: density === "inline" || short && !empty ? "compact" : variant,
|
|
@@ -2627,7 +3704,7 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
|
|
|
2627
3704
|
onSubmit: onSend,
|
|
2628
3705
|
...onStop && { onStop },
|
|
2629
3706
|
...composer,
|
|
2630
|
-
...pending && { placeholder:
|
|
3707
|
+
...pending && { placeholder: labels.replyDirectly }
|
|
2631
3708
|
});
|
|
2632
3709
|
return /* @__PURE__ */ jsxs("div", {
|
|
2633
3710
|
"data-chat-density": density,
|
|
@@ -2642,28 +3719,31 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
|
|
|
2642
3719
|
className: "w-full max-w-(--chat-content-width)",
|
|
2643
3720
|
children: emptyState ? emptyState(box("centered")) : box("centered")
|
|
2644
3721
|
})
|
|
2645
|
-
}) : /* @__PURE__ */ jsx(
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
className: "
|
|
2655
|
-
children:
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
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
|
+
})
|
|
2667
3747
|
})
|
|
2668
3748
|
})
|
|
2669
3749
|
})
|
|
@@ -2700,6 +3780,51 @@ function EmptyState({ greeting, icon, children, footer }) {
|
|
|
2700
3780
|
]
|
|
2701
3781
|
});
|
|
2702
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
|
+
}
|
|
2703
3828
|
// src/views/app-shell.tsx
|
|
2704
3829
|
/**
|
|
2705
3830
|
* A page with a sidebar: pinned and collapsible to an icon rail from tablet width up, a drawer over the
|
|
@@ -2724,6 +3849,7 @@ function AppShell({ sidebar, children, defaultCollapsed, className }) {
|
|
|
2724
3849
|
toggle,
|
|
2725
3850
|
dismiss
|
|
2726
3851
|
]);
|
|
3852
|
+
const labels = useLabels();
|
|
2727
3853
|
const inDrawer = useMemo(() => ({
|
|
2728
3854
|
...pinned,
|
|
2729
3855
|
collapsed: false
|
|
@@ -2742,7 +3868,7 @@ function AppShell({ sidebar, children, defaultCollapsed, className }) {
|
|
|
2742
3868
|
}), drawer && /* @__PURE__ */ jsx(Drawer, {
|
|
2743
3869
|
isOpen: drawerOpen,
|
|
2744
3870
|
onOpenChange: setDrawerOpen,
|
|
2745
|
-
"aria-label":
|
|
3871
|
+
"aria-label": labels.sidebar,
|
|
2746
3872
|
children: /* @__PURE__ */ jsx(SidebarContext.Provider, {
|
|
2747
3873
|
value: inDrawer,
|
|
2748
3874
|
children: sidebar
|
|
@@ -2752,13 +3878,16 @@ function AppShell({ sidebar, children, defaultCollapsed, className }) {
|
|
|
2752
3878
|
}
|
|
2753
3879
|
/** Opens the drawer on phones, collapses / expands the pinned sidebar elsewhere. */
|
|
2754
3880
|
function SidebarToggle({ drawerOnly = false, className }) {
|
|
3881
|
+
const icons = useIcons();
|
|
2755
3882
|
const { drawer, drawerOpen, collapsed, toggle } = useSidebar();
|
|
3883
|
+
const labels = useLabels();
|
|
2756
3884
|
if (drawerOnly && !drawer) return null;
|
|
3885
|
+
const label = drawer ? drawerOpen ? labels.closeSidebar : labels.openSidebar : collapsed ? labels.expandSidebar : labels.collapseSidebar;
|
|
2757
3886
|
return /* @__PURE__ */ jsx(IconButton, {
|
|
2758
|
-
label
|
|
3887
|
+
label,
|
|
2759
3888
|
onPress: toggle,
|
|
2760
3889
|
className,
|
|
2761
|
-
children:
|
|
3890
|
+
children: icons.sidebar
|
|
2762
3891
|
});
|
|
2763
3892
|
}
|
|
2764
3893
|
// src/views/sidebar.tsx
|
|
@@ -2798,7 +3927,7 @@ function Sidebar({ brand, headerActions, nav, children, footer, className }) {
|
|
|
2798
3927
|
/** A nav row: icon and label; just the icon with a tooltip when collapsed. Closes the drawer when picked. */
|
|
2799
3928
|
function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
|
|
2800
3929
|
const { collapsed, dismiss } = useSidebar();
|
|
2801
|
-
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");
|
|
2802
3931
|
const press = () => {
|
|
2803
3932
|
onPress?.();
|
|
2804
3933
|
dismiss();
|
|
@@ -2813,7 +3942,7 @@ function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
|
|
|
2813
3942
|
children: label
|
|
2814
3943
|
}),
|
|
2815
3944
|
!collapsed && shortcut && /* @__PURE__ */ jsx("kbd", {
|
|
2816
|
-
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",
|
|
2817
3946
|
children: shortcut
|
|
2818
3947
|
})
|
|
2819
3948
|
] });
|
|
@@ -2839,6 +3968,7 @@ function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
|
|
|
2839
3968
|
const initials = (name) => name.split(/\s+/).slice(0, 2).map((w) => w[0]?.toUpperCase() ?? "").join("");
|
|
2840
3969
|
/** The account row at the bottom: avatar, name, plan, and a menu. */
|
|
2841
3970
|
function SidebarUser({ name, detail, avatarUrl, menu }) {
|
|
3971
|
+
const icons = useIcons();
|
|
2842
3972
|
const { collapsed } = useSidebar();
|
|
2843
3973
|
const avatar = avatarUrl ? /* @__PURE__ */ jsx("img", {
|
|
2844
3974
|
src: avatarUrl,
|
|
@@ -2846,7 +3976,7 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
|
|
|
2846
3976
|
className: "size-8 shrink-0 rounded-full object-cover"
|
|
2847
3977
|
}) : /* @__PURE__ */ jsx("span", {
|
|
2848
3978
|
"aria-hidden": true,
|
|
2849
|
-
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",
|
|
2850
3980
|
children: initials(name)
|
|
2851
3981
|
});
|
|
2852
3982
|
const button = /* @__PURE__ */ jsxs(Button$1, {
|
|
@@ -2855,15 +3985,15 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
|
|
|
2855
3985
|
children: [avatar, !collapsed && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
|
|
2856
3986
|
className: "flex min-w-0 flex-1 flex-col",
|
|
2857
3987
|
children: [/* @__PURE__ */ jsx("span", {
|
|
2858
|
-
className: "truncate text-
|
|
3988
|
+
className: "truncate text-(length:--chat-ui-font-size) leading-5 text-chat-fg",
|
|
2859
3989
|
children: name
|
|
2860
3990
|
}), detail && /* @__PURE__ */ jsx("span", {
|
|
2861
|
-
className: "truncate text-
|
|
3991
|
+
className: "truncate text-(length:--chat-ui-font-size-sm) leading-4 text-chat-muted",
|
|
2862
3992
|
children: detail
|
|
2863
3993
|
})]
|
|
2864
|
-
}), menu && /* @__PURE__ */ jsx(
|
|
2865
|
-
"
|
|
2866
|
-
|
|
3994
|
+
}), menu && /* @__PURE__ */ jsx(IconSlot, {
|
|
3995
|
+
className: "text-chat-muted [&_svg]:size-4",
|
|
3996
|
+
children: icons.switcher
|
|
2867
3997
|
})] })]
|
|
2868
3998
|
});
|
|
2869
3999
|
if (!menu) return button;
|
|
@@ -2872,4 +4002,4 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
|
|
|
2872
4002
|
children: menu
|
|
2873
4003
|
})] });
|
|
2874
4004
|
}
|
|
2875
|
-
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 };
|