agentic-ui-kit 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +719 -123
- package/dist/styles.css +1 -1
- package/dist/tailwind.css +49 -0
- package/dist/types/react/src/components/attachments.d.ts +4 -1
- package/dist/types/react/src/components/chat-list.d.ts +16 -0
- package/dist/types/react/src/components/context-meter.d.ts +24 -0
- package/dist/types/react/src/components/context-panel.d.ts +5 -0
- package/dist/types/react/src/components/copy-field.d.ts +15 -0
- package/dist/types/react/src/components/markdown.d.ts +19 -2
- package/dist/types/react/src/components/model-picker.d.ts +6 -1
- package/dist/types/react/src/components/tools.d.ts +7 -2
- package/dist/types/react/src/hooks/sidebar-state.d.ts +6 -0
- package/dist/types/react/src/hooks/sse-transport.d.ts +7 -1
- package/dist/types/react/src/hooks/use-chat.d.ts +14 -1
- package/dist/types/react/src/index.d.ts +11 -7
- package/dist/types/react/src/primitives/button.d.ts +11 -3
- package/dist/types/react/src/primitives/chat-theme.d.ts +3 -1
- package/dist/types/react/src/primitives/cx.d.ts +3 -0
- package/dist/types/react/src/primitives/icons.d.ts +2 -0
- package/dist/types/react/src/primitives/labels.d.ts +18 -0
- package/dist/types/react/src/primitives/menu.d.ts +23 -5
- package/dist/types/react/src/primitives/overlay.d.ts +13 -1
- package/dist/types/react/src/primitives/panel.d.ts +3 -1
- package/dist/types/react/src/primitives/segmented-control.d.ts +29 -4
- package/dist/types/react/src/views/app-shell.d.ts +31 -3
- package/dist/types/react/src/views/chat-view.d.ts +6 -2
- package/dist/types/react/src/views/sidebar.d.ts +35 -6
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,13 +1,40 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { A as awaitingToolCalls, C as widgetLoaderMessages, D as toBlocks, F as settleToolCall, L as salvageJson, O as activityTokens, T as readEventStream, _ as replacedId, b as resolveReplaces, d as isStateRef, f as matchesSchema, g as uiActionSchema, i as parseUiMarkup, k as applyEvent } from "./src-D01l3nBc.js";
|
|
3
3
|
import { clsx } from "clsx";
|
|
4
|
-
import {
|
|
5
|
-
import { Fragment,
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
4
|
+
import { extendTailwindMerge } from "tailwind-merge";
|
|
5
|
+
import { Children, Component, Fragment, createContext, createElement, isValidElement, memo, useCallback, useContext, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
6
|
+
import { Autocomplete, Button as Button$1, Dialog, DialogTrigger, DialogTrigger as DialogTrigger$1, 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, ToggleButton, ToggleButtonGroup, Tooltip as Tooltip$1, TooltipTrigger, useFilter } from "react-aria-components";
|
|
7
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { AlertTriangle, ArrowDown, ArrowUp, Check, ChevronDown, ChevronRight, ChevronsUpDown, Copy, FileText, Folder, FolderOpen, FolderPlus, Globe, Info, MessageSquare, Mic, MoreHorizontal, PanelLeft, Pencil, Pin, PinOff, Plus, RefreshCw, Share, ShieldQuestion, Square, ThumbsDown, ThumbsUp, Trash2, Volume2, X } from "lucide-react";
|
|
8
9
|
import { UNSAFE_PortalProvider } from "react-aria";
|
|
9
10
|
import { createMathPlugin } from "@streamdown/math";
|
|
10
|
-
import { Streamdown, defaultRehypePlugins } from "streamdown";
|
|
11
|
+
import { Streamdown, defaultComponents, defaultRehypePlugins } from "streamdown";
|
|
12
|
+
// src/primitives/cx.ts
|
|
13
|
+
const twMerge = extendTailwindMerge({ extend: { theme: {
|
|
14
|
+
radius: [
|
|
15
|
+
"chat-sm",
|
|
16
|
+
"chat-md",
|
|
17
|
+
"chat-lg",
|
|
18
|
+
"chat-popover"
|
|
19
|
+
],
|
|
20
|
+
shadow: ["chat-popover"],
|
|
21
|
+
animate: [
|
|
22
|
+
"chat-in",
|
|
23
|
+
"chat-out",
|
|
24
|
+
"chat-fade-in",
|
|
25
|
+
"chat-fade-out",
|
|
26
|
+
"chat-drawer-in",
|
|
27
|
+
"chat-drawer-out",
|
|
28
|
+
"chat-drawer-in-right",
|
|
29
|
+
"chat-drawer-out-right",
|
|
30
|
+
"chat-sheet-in",
|
|
31
|
+
"chat-sheet-out"
|
|
32
|
+
]
|
|
33
|
+
} } });
|
|
34
|
+
/** `clsx` plus tailwind-merge: later classes win over earlier ones that set the same property, so a caller's `h-7` or `rounded-none` beats the kit's. */
|
|
35
|
+
function cx(...inputs) {
|
|
36
|
+
return twMerge(clsx(inputs));
|
|
37
|
+
}
|
|
11
38
|
// src/primitives/tooltip.tsx
|
|
12
39
|
function Tooltip({ content, children, placement = "top", delay = 600 }) {
|
|
13
40
|
return /* @__PURE__ */ jsxs(TooltipTrigger, {
|
|
@@ -38,23 +65,88 @@ const variants = {
|
|
|
38
65
|
*/
|
|
39
66
|
danger: "bg-[var(--chat-danger-bg,color-mix(in_srgb,var(--chat-danger)_70%,black))] font-medium text-[var(--chat-danger-fg,#fff)] data-hovered:brightness-110 data-pressed:brightness-95"
|
|
40
67
|
};
|
|
41
|
-
const sizes = {
|
|
68
|
+
const sizes$1 = {
|
|
42
69
|
sm: "h-7 gap-1.5 px-2 text-(length:--chat-ui-font-size) leading-5 [@media(pointer:coarse)]:h-9",
|
|
43
70
|
md: "h-8 gap-2 px-2.5 text-(length:--chat-ui-font-size) leading-5 [@media(pointer:coarse)]:h-10",
|
|
44
71
|
/** Dialog and empty-state actions. */
|
|
45
72
|
lg: "h-9 gap-2 px-3.5 text-(length:--chat-ui-font-size) leading-5 [@media(pointer:coarse)]:h-11"
|
|
46
73
|
};
|
|
47
|
-
|
|
48
|
-
|
|
74
|
+
/** Press handlers dropped from a button that is disabled with a reason; it only looks and announces as disabled. */
|
|
75
|
+
const pressHandlers = [
|
|
76
|
+
"onPress",
|
|
77
|
+
"onPressStart",
|
|
78
|
+
"onPressEnd",
|
|
79
|
+
"onPressChange",
|
|
80
|
+
"onPressUp",
|
|
81
|
+
"onClick"
|
|
82
|
+
];
|
|
83
|
+
/**
|
|
84
|
+
* A disabled button that still explains itself. React Aria's `isDisabled` makes the button unfocusable and
|
|
85
|
+
* hoverable-less, so there'd be nothing to hang a tooltip on. With a reason the button instead stays focusable
|
|
86
|
+
* with `aria-disabled`, loses its press handlers and never submits a form, and the reason is shown as a tooltip
|
|
87
|
+
* and read as its description.
|
|
88
|
+
*/
|
|
89
|
+
function useDisabledReason(props, disabledReason) {
|
|
90
|
+
const id = useId();
|
|
91
|
+
const active = Boolean(props.isDisabled && disabledReason);
|
|
92
|
+
if (!active) return {
|
|
93
|
+
active,
|
|
94
|
+
props,
|
|
95
|
+
reasonId: void 0
|
|
96
|
+
};
|
|
97
|
+
const rest = {
|
|
49
98
|
...props,
|
|
50
|
-
|
|
99
|
+
isDisabled: false,
|
|
100
|
+
type: "button"
|
|
101
|
+
};
|
|
102
|
+
for (const h of pressHandlers) delete rest[h];
|
|
103
|
+
rest["aria-disabled"] = true;
|
|
104
|
+
rest["data-disabled"] = true;
|
|
105
|
+
rest["aria-describedby"] = [props["aria-describedby"], id].filter(Boolean).join(" ");
|
|
106
|
+
return {
|
|
107
|
+
active,
|
|
108
|
+
props: rest,
|
|
109
|
+
reasonId: id
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** Hover and press styles are dropped while a button is disabled with a reason, to look as disabled as one that is. */
|
|
113
|
+
const noInteraction = (classes) => classes.replace(/data-(hovered|pressed):\S+\s*/g, "");
|
|
114
|
+
function WithReason({ active, reasonId, reason, children }) {
|
|
115
|
+
if (!active) return children;
|
|
116
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Tooltip, {
|
|
117
|
+
content: reason,
|
|
118
|
+
children
|
|
119
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
120
|
+
id: reasonId,
|
|
121
|
+
hidden: true,
|
|
122
|
+
children: reason
|
|
123
|
+
})] });
|
|
124
|
+
}
|
|
125
|
+
function Button({ variant = "ghost", size = "md", className, disabledReason, ...rest }) {
|
|
126
|
+
const { active, props, reasonId } = useDisabledReason(rest, disabledReason);
|
|
127
|
+
const button = /* @__PURE__ */ jsx(Button$1, {
|
|
128
|
+
...props,
|
|
129
|
+
className: cx("inline-flex cursor-pointer items-center justify-center rounded-chat-sm font-chat-sans whitespace-nowrap transition-colors select-none [&_svg]:size-4 [&_svg]:shrink-0", "data-disabled:cursor-default data-disabled:opacity-40", focusRing, active ? noInteraction(variants[variant]) : variants[variant], sizes$1[size], className)
|
|
130
|
+
});
|
|
131
|
+
return /* @__PURE__ */ jsx(WithReason, {
|
|
132
|
+
active,
|
|
133
|
+
reasonId,
|
|
134
|
+
reason: disabledReason,
|
|
135
|
+
children: button
|
|
51
136
|
});
|
|
52
137
|
}
|
|
53
|
-
function IconButton({ label, variant = "ghost", size = "md", tooltip = true, className, ...
|
|
138
|
+
function IconButton({ label, variant = "ghost", size = "md", tooltip = true, className, disabledReason, ...rest }) {
|
|
139
|
+
const { active, props, reasonId } = useDisabledReason(rest, disabledReason);
|
|
54
140
|
const button = /* @__PURE__ */ jsx(Button$1, {
|
|
55
141
|
...props,
|
|
56
142
|
"aria-label": label,
|
|
57
|
-
className:
|
|
143
|
+
className: cx("inline-flex shrink-0 cursor-pointer items-center justify-center rounded-chat-sm transition-colors select-none", "data-disabled:cursor-default data-disabled:opacity-40 [&_svg]:size-4", focusRing, active ? noInteraction(variants[variant]) : variants[variant], variant === "ghost" && (active ? "text-chat-muted" : "text-chat-muted data-hovered:text-chat-fg"), size === "sm" ? "size-7 [@media(pointer:coarse)]:size-9" : "size-8 [@media(pointer:coarse)]:size-10", className)
|
|
144
|
+
});
|
|
145
|
+
if (active) return /* @__PURE__ */ jsx(WithReason, {
|
|
146
|
+
active: true,
|
|
147
|
+
reasonId,
|
|
148
|
+
reason: disabledReason,
|
|
149
|
+
children: button
|
|
58
150
|
});
|
|
59
151
|
return tooltip ? /* @__PURE__ */ jsx(Tooltip, {
|
|
60
152
|
content: label,
|
|
@@ -70,7 +162,7 @@ const tones = {
|
|
|
70
162
|
};
|
|
71
163
|
function Badge({ children, tone = "outline", icon, className }) {
|
|
72
164
|
return /* @__PURE__ */ jsxs("span", {
|
|
73
|
-
className:
|
|
165
|
+
className: cx("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),
|
|
74
166
|
children: [icon, children]
|
|
75
167
|
});
|
|
76
168
|
}
|
|
@@ -124,7 +216,8 @@ const defaultIcons = {
|
|
|
124
216
|
folder: /* @__PURE__ */ jsx(Folder, {}),
|
|
125
217
|
folderOpen: /* @__PURE__ */ jsx(FolderOpen, {}),
|
|
126
218
|
file: /* @__PURE__ */ jsx(FileText, {}),
|
|
127
|
-
remove: /* @__PURE__ */ jsx(X, {})
|
|
219
|
+
remove: /* @__PURE__ */ jsx(X, {}),
|
|
220
|
+
chatList: /* @__PURE__ */ jsx(MessageSquare, {})
|
|
128
221
|
};
|
|
129
222
|
const IconsContext = createContext(defaultIcons);
|
|
130
223
|
/** Merges `icons` over the icons of the enclosing provider (or the defaults). */
|
|
@@ -170,6 +263,9 @@ const defaultLabels = {
|
|
|
170
263
|
duration: (seconds) => seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`,
|
|
171
264
|
ranCommandLabel: "Ran a command",
|
|
172
265
|
ranCommands: (count) => plural(count, "ran a command", (n) => `ran ${n} commands`),
|
|
266
|
+
runCommandAsk: "Run this command?",
|
|
267
|
+
searchWebAsk: "Search the web?",
|
|
268
|
+
readPageAsk: "Read this page?",
|
|
173
269
|
searchedWebLabel: "Searched the web",
|
|
174
270
|
readPageLabel: "Read page",
|
|
175
271
|
readPages: (count) => plural(count, "read a page", (n) => `read ${n} pages`),
|
|
@@ -181,7 +277,7 @@ const defaultLabels = {
|
|
|
181
277
|
noResults: "No results",
|
|
182
278
|
reasoningHidden: "The model's reasoning is not shown.",
|
|
183
279
|
allowTool: (name) => `Allow ${name}?`,
|
|
184
|
-
allowAction: (label) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
280
|
+
allowAction: (label) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
185
281
|
"Allow ",
|
|
186
282
|
label,
|
|
187
283
|
"?"
|
|
@@ -193,9 +289,14 @@ const defaultLabels = {
|
|
|
193
289
|
openSidebar: "Open sidebar",
|
|
194
290
|
expandSidebar: "Expand sidebar",
|
|
195
291
|
collapseSidebar: "Collapse sidebar",
|
|
292
|
+
resizeSidebar: "Resize sidebar",
|
|
293
|
+
withShortcut: (label, keys) => `${label} (${keys})`,
|
|
294
|
+
ctrlKey: "Ctrl",
|
|
196
295
|
context: "Context",
|
|
197
296
|
contextUsed: "Context used",
|
|
198
297
|
percentUsed: (percent) => `${percent}% used`,
|
|
298
|
+
contextSummary: (used, limit, percent) => `Context: ${used} / ${limit} (${percent}%)`,
|
|
299
|
+
contextLoading: "Context: loading",
|
|
199
300
|
freeSpace: "Free space",
|
|
200
301
|
tokenCount: (count) => `${count} tokens`,
|
|
201
302
|
replyPlaceholder: "Reply…",
|
|
@@ -240,6 +341,7 @@ const defaultLabels = {
|
|
|
240
341
|
justNow: "just now",
|
|
241
342
|
copy: "Copy",
|
|
242
343
|
copied: "Copied",
|
|
344
|
+
copyFieldLabel: "Link",
|
|
243
345
|
retry: "Retry",
|
|
244
346
|
edit: "Edit",
|
|
245
347
|
readAloud: "Read aloud",
|
|
@@ -299,27 +401,40 @@ function CopyButton({ text, className }) {
|
|
|
299
401
|
});
|
|
300
402
|
}
|
|
301
403
|
// src/primitives/segmented-control.tsx
|
|
404
|
+
const sizes = {
|
|
405
|
+
sm: {
|
|
406
|
+
button: "h-6",
|
|
407
|
+
iconOnly: "w-7"
|
|
408
|
+
},
|
|
409
|
+
md: {
|
|
410
|
+
button: "h-8",
|
|
411
|
+
iconOnly: "w-9"
|
|
412
|
+
}
|
|
413
|
+
};
|
|
302
414
|
/**
|
|
303
415
|
* One of a few options, side by side on a track, similar to claude.ai's Chat / Cowork switch. Themed with
|
|
304
416
|
* `--chat-segment-track` (default: a shade darker than the surface it sits on) and `--chat-segment-selected`
|
|
305
417
|
* (default `--chat-raised`).
|
|
306
418
|
*/
|
|
307
|
-
function SegmentedControl({ label, options, value, onChange, className }) {
|
|
419
|
+
function SegmentedControl({ label, options, value, onChange, clearable, isDisabled, size = "sm", className }) {
|
|
308
420
|
return /* @__PURE__ */ jsx(ToggleButtonGroup, {
|
|
309
421
|
"aria-label": label,
|
|
310
422
|
selectionMode: "single",
|
|
311
|
-
disallowEmptySelection:
|
|
312
|
-
|
|
423
|
+
disallowEmptySelection: !clearable,
|
|
424
|
+
...isDisabled && { isDisabled },
|
|
425
|
+
selectedKeys: value === null ? [] : [value],
|
|
313
426
|
onSelectionChange: (keys) => {
|
|
314
427
|
const next = [...keys][0];
|
|
315
428
|
if (next !== void 0) onChange(next);
|
|
429
|
+
else if (clearable) onChange(null);
|
|
316
430
|
},
|
|
317
|
-
className:
|
|
431
|
+
className: cx("inline-flex shrink-0 items-center gap-0.5 rounded-chat-md p-0.5", "bg-[var(--chat-segment-track,color-mix(in_srgb,var(--chat-bg)_55%,transparent))]", className),
|
|
318
432
|
children: options.map((o) => {
|
|
319
433
|
const button = /* @__PURE__ */ jsxs(ToggleButton, {
|
|
320
434
|
id: o.id,
|
|
435
|
+
...o.isDisabled && { isDisabled: true },
|
|
321
436
|
...o.iconOnly && { "aria-label": o.label },
|
|
322
|
-
className:
|
|
437
|
+
className: cx("inline-flex cursor-pointer items-center justify-center gap-1.5 rounded-[calc(var(--chat-radius-md)-2px)] font-chat-sans text-(length:--chat-ui-font-size) leading-5 text-chat-muted outline-none transition-colors select-none [&_svg]:size-4", sizes[size].button, o.iconOnly ? sizes[size].iconOnly : "px-2", "data-disabled:cursor-default data-disabled:opacity-40", "data-hovered:text-chat-fg data-focus-visible:ring-2 data-focus-visible:ring-chat-focus", "data-selected:bg-[var(--chat-segment-selected,var(--chat-raised))] data-selected:text-chat-fg data-selected:shadow-[inset_0_0_0_1px_var(--chat-border-strong)]"),
|
|
323
438
|
children: [o.icon, !o.iconOnly && o.label]
|
|
324
439
|
}, o.id);
|
|
325
440
|
return o.iconOnly ? /* @__PURE__ */ jsx(Tooltip, {
|
|
@@ -340,7 +455,7 @@ function Disclosure({ summary, children, className, summaryClassName, panelClass
|
|
|
340
455
|
className: "m-0 font-normal",
|
|
341
456
|
children: /* @__PURE__ */ jsxs(Button$1, {
|
|
342
457
|
slot: "trigger",
|
|
343
|
-
className:
|
|
458
|
+
className: cx("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),
|
|
344
459
|
children: [
|
|
345
460
|
indicator === "chevron" && /* @__PURE__ */ jsx(IconSlot, {
|
|
346
461
|
className: "transition-transform group-aria-expanded/trigger:rotate-90 [&_svg]:size-3.5",
|
|
@@ -367,6 +482,11 @@ function Disclosure({ summary, children, className, summaryClassName, panelClass
|
|
|
367
482
|
});
|
|
368
483
|
}
|
|
369
484
|
// src/primitives/menu.tsx
|
|
485
|
+
/**
|
|
486
|
+
* Wraps a trigger button and a kit `Popover` (or `Modal`) so that pressing the button toggles it, without
|
|
487
|
+
* importing react-aria. The same component as `DialogTrigger`; use whichever name reads better.
|
|
488
|
+
*/
|
|
489
|
+
const PopoverTrigger = DialogTrigger$1;
|
|
370
490
|
/** A search box above a menu that filters its items, like "Search or create a project". */
|
|
371
491
|
function MenuSearch({ placeholder, children, value, onChange }) {
|
|
372
492
|
const { contains } = useFilter({ sensitivity: "base" });
|
|
@@ -385,41 +505,71 @@ function MenuSearch({ placeholder, children, value, onChange }) {
|
|
|
385
505
|
}), children]
|
|
386
506
|
});
|
|
387
507
|
}
|
|
388
|
-
/**
|
|
389
|
-
function
|
|
508
|
+
/** Viewport narrower than `max` px. False on the server and where `matchMedia` is missing. */
|
|
509
|
+
function useNarrower(max) {
|
|
510
|
+
const query = max === false ? null : `(max-width: ${max - .02}px)`;
|
|
511
|
+
return useSyncExternalStore((notify) => {
|
|
512
|
+
if (!query || typeof window === "undefined" || !window.matchMedia) return () => {};
|
|
513
|
+
const list = window.matchMedia(query);
|
|
514
|
+
list.addEventListener("change", notify);
|
|
515
|
+
return () => list.removeEventListener("change", notify);
|
|
516
|
+
}, () => query && typeof window !== "undefined" && window.matchMedia ? window.matchMedia(query).matches : false, () => false);
|
|
517
|
+
}
|
|
518
|
+
/** Floating panel for menus and pickers: surface color, rounded, soft shadow, fades in. On phones it's a bottom sheet. */
|
|
519
|
+
function Popover({ className, offset = 6, sheet = 768, width = "auto", fullWidthOnPhones, children, ...props }) {
|
|
520
|
+
if (useNarrower(sheet)) {
|
|
521
|
+
const { isOpen, onOpenChange } = props;
|
|
522
|
+
return /* @__PURE__ */ jsx(ModalOverlay, {
|
|
523
|
+
...isOpen !== void 0 && { isOpen },
|
|
524
|
+
...onOpenChange && { onOpenChange },
|
|
525
|
+
isDismissable: true,
|
|
526
|
+
className: "fixed inset-0 z-50 flex items-end bg-(--chat-backdrop) data-entering:animate-chat-fade-in data-exiting:animate-chat-fade-out",
|
|
527
|
+
children: /* @__PURE__ */ jsx(Modal$1, {
|
|
528
|
+
className: cx("flex max-h-[85dvh] w-full flex-col overflow-auto rounded-t-chat-lg border-t border-chat-popover-border bg-chat-popover pb-[env(safe-area-inset-bottom)] font-chat-sans text-chat-fg shadow-chat-popover outline-none", "data-entering:animate-chat-sheet-in data-exiting:animate-chat-sheet-out", className),
|
|
529
|
+
children: typeof children === "function" ? children({
|
|
530
|
+
placement: "bottom",
|
|
531
|
+
trigger: null,
|
|
532
|
+
isEntering: false,
|
|
533
|
+
isExiting: false,
|
|
534
|
+
defaultChildren: void 0
|
|
535
|
+
}) : children
|
|
536
|
+
})
|
|
537
|
+
});
|
|
538
|
+
}
|
|
390
539
|
return /* @__PURE__ */ jsx(Popover$1, {
|
|
391
540
|
offset,
|
|
392
541
|
...props,
|
|
393
|
-
className:
|
|
542
|
+
className: cx("rounded-chat-popover border border-chat-popover-border bg-chat-popover font-chat-sans text-chat-fg shadow-chat-popover outline-none", "motion-safe:data-entering:animate-chat-in motion-safe:data-exiting:animate-chat-out", width === "trigger" && "w-(--trigger-width) [&_[role=menu]]:min-w-0", fullWidthOnPhones && "max-sm:w-[calc(100vw-1.5rem)]", className),
|
|
543
|
+
children
|
|
394
544
|
});
|
|
395
545
|
}
|
|
396
546
|
function Menu({ className, ...props }) {
|
|
397
547
|
return /* @__PURE__ */ jsx(Menu$1, {
|
|
398
548
|
...props,
|
|
399
|
-
className:
|
|
549
|
+
className: cx("max-h-[inherit] min-w-52 overflow-auto p-1.5 outline-none", className)
|
|
400
550
|
});
|
|
401
551
|
}
|
|
402
|
-
function MenuItem({ children, icon, description, descriptionPlacement = "below", detail, shortcut, tone = "default", className, ...props }) {
|
|
552
|
+
function MenuItem({ children, icon, description, descriptionPlacement = "below", detail, shortcut, tone = "default", indicator = "check", className, ...props }) {
|
|
403
553
|
const icons = useIcons();
|
|
404
554
|
const textValue = props.textValue ?? (typeof children === "string" ? children : void 0);
|
|
405
555
|
return /* @__PURE__ */ jsx(MenuItem$1, {
|
|
406
556
|
...props,
|
|
407
557
|
...textValue !== void 0 && { textValue },
|
|
408
|
-
className:
|
|
409
|
-
children: ({ isSelected, hasSubmenu, selectionMode }) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
558
|
+
className: cx("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),
|
|
559
|
+
children: ({ isSelected, hasSubmenu, selectionMode }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
410
560
|
icon && /* @__PURE__ */ jsx("span", {
|
|
411
|
-
className:
|
|
561
|
+
className: cx("flex size-4 shrink-0 items-center [&_svg]:size-4", tone === "danger" ? "text-chat-danger" : "text-chat-fg"),
|
|
412
562
|
children: icon
|
|
413
563
|
}),
|
|
414
564
|
/* @__PURE__ */ jsxs("span", {
|
|
415
|
-
className:
|
|
565
|
+
className: cx("flex min-w-0 flex-1", descriptionPlacement === "beside" ? "items-baseline gap-2" : "flex-col"),
|
|
416
566
|
children: [/* @__PURE__ */ jsx(Text, {
|
|
417
567
|
slot: "label",
|
|
418
|
-
className:
|
|
568
|
+
className: cx("truncate", descriptionPlacement === "beside" && "shrink-0"),
|
|
419
569
|
children
|
|
420
570
|
}), description && /* @__PURE__ */ jsx(Text, {
|
|
421
571
|
slot: "description",
|
|
422
|
-
className:
|
|
572
|
+
className: cx("text-chat-muted", descriptionPlacement === "beside" ? "truncate text-(length:--chat-ui-font-size) leading-5" : "text-(length:--chat-ui-font-size-sm) leading-4"),
|
|
423
573
|
children: description
|
|
424
574
|
})]
|
|
425
575
|
}),
|
|
@@ -431,7 +581,12 @@ function MenuItem({ children, icon, description, descriptionPlacement = "below",
|
|
|
431
581
|
className: "shrink-0 font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle",
|
|
432
582
|
children: shortcut
|
|
433
583
|
}),
|
|
434
|
-
selectionMode !== "none" &&
|
|
584
|
+
selectionMode !== "none" && indicator === "switch" && /* @__PURE__ */ jsx("span", {
|
|
585
|
+
"aria-hidden": true,
|
|
586
|
+
"data-selected": isSelected || void 0,
|
|
587
|
+
className: "relative h-4 w-7 shrink-0 rounded-full bg-chat-border-strong transition-colors after:absolute after:top-0.5 after:left-0.5 after:size-3 after:rounded-full after:bg-chat-bg after:transition-transform data-selected:bg-chat-accent data-selected:after:translate-x-3 data-selected:after:bg-chat-accent-fg"
|
|
588
|
+
}),
|
|
589
|
+
selectionMode !== "none" && indicator === "check" && isSelected && /* @__PURE__ */ jsx(IconSlot, {
|
|
435
590
|
className: "text-chat-check [&_svg]:size-4",
|
|
436
591
|
children: icons.check
|
|
437
592
|
}),
|
|
@@ -458,7 +613,7 @@ function MenuSeparator() {
|
|
|
458
613
|
/** Explanatory text inside a menu, above or below the items. Not focusable. */
|
|
459
614
|
function MenuNote({ children, className }) {
|
|
460
615
|
return /* @__PURE__ */ jsx("div", {
|
|
461
|
-
className:
|
|
616
|
+
className: cx("px-2 py-1.5 text-(length:--chat-ui-font-size) leading-snug text-chat-muted", className),
|
|
462
617
|
children
|
|
463
618
|
});
|
|
464
619
|
}
|
|
@@ -492,7 +647,7 @@ function ContextMenu({ menu, children, className }) {
|
|
|
492
647
|
ref: root,
|
|
493
648
|
onContextMenu,
|
|
494
649
|
onKeyDown,
|
|
495
|
-
className:
|
|
650
|
+
className: cx("relative", className),
|
|
496
651
|
children: [children, /* @__PURE__ */ jsxs(MenuTrigger$1, {
|
|
497
652
|
isOpen: at !== void 0,
|
|
498
653
|
onOpenChange: (open) => !open && setAt(void 0),
|
|
@@ -513,15 +668,23 @@ function ContextMenu({ menu, children, className }) {
|
|
|
513
668
|
});
|
|
514
669
|
}
|
|
515
670
|
// src/primitives/overlay.tsx
|
|
671
|
+
const modalSizes = {
|
|
672
|
+
sm: "max-w-sm",
|
|
673
|
+
md: "max-w-lg",
|
|
674
|
+
lg: "max-w-2xl",
|
|
675
|
+
xl: "max-w-5xl",
|
|
676
|
+
/** Fills the page inside the overlay's padding. */
|
|
677
|
+
full: "h-full max-h-none max-w-none"
|
|
678
|
+
};
|
|
516
679
|
/** A centered dialog over a dimmed page. Escape and clicking outside close it; focus stays inside. */
|
|
517
|
-
function Modal({ isOpen, onOpenChange, title, children, className, ...props }) {
|
|
680
|
+
function Modal({ isOpen, onOpenChange, title, size = "lg", placement = "center", children, className, ...props }) {
|
|
518
681
|
return /* @__PURE__ */ jsx(ModalOverlay, {
|
|
519
682
|
isOpen,
|
|
520
683
|
onOpenChange,
|
|
521
684
|
isDismissable: true,
|
|
522
|
-
className: "fixed inset-0 z-50 flex
|
|
685
|
+
className: cx("fixed inset-0 z-50 flex justify-center bg-(--chat-backdrop) p-4 data-entering:animate-chat-fade-in data-exiting:animate-chat-fade-out", placement === "top" ? "items-start pt-[10dvh]" : "items-center"),
|
|
523
686
|
children: /* @__PURE__ */ jsx(Modal$1, {
|
|
524
|
-
className:
|
|
687
|
+
className: cx("flex max-h-[85dvh] w-full flex-col rounded-chat-lg border border-chat-popover-border bg-chat-popover font-chat-sans text-chat-fg shadow-chat-popover", modalSizes[size], "data-entering:animate-chat-in data-exiting:animate-chat-out", className),
|
|
525
688
|
children: /* @__PURE__ */ jsxs(Dialog, {
|
|
526
689
|
...props,
|
|
527
690
|
className: "flex min-h-0 flex-1 flex-col outline-none",
|
|
@@ -542,7 +705,7 @@ function Drawer({ isOpen, onOpenChange, side = "left", children, className, ...p
|
|
|
542
705
|
isDismissable: true,
|
|
543
706
|
className: "fixed inset-0 z-40 bg-(--chat-backdrop) data-entering:animate-chat-fade-in data-exiting:animate-chat-fade-out",
|
|
544
707
|
children: /* @__PURE__ */ jsx(Modal$1, {
|
|
545
|
-
className:
|
|
708
|
+
className: cx("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),
|
|
546
709
|
children: /* @__PURE__ */ jsx(Dialog, {
|
|
547
710
|
...props,
|
|
548
711
|
className: "h-full outline-none",
|
|
@@ -556,10 +719,10 @@ function Drawer({ isOpen, onOpenChange, side = "left", children, className, ...p
|
|
|
556
719
|
* A titled region that fills its container and lets the body scroll: a dock panel, an agent pane in a
|
|
557
720
|
* grid, a settings card. A `ChatView` placed in the body sizes itself to the panel.
|
|
558
721
|
*/
|
|
559
|
-
function Panel({ title, actions, footer, children, variant = "card", className, ...props }) {
|
|
722
|
+
function Panel({ title, actions, footer, children, variant = "card", padded = false, className, ...props }) {
|
|
560
723
|
return /* @__PURE__ */ jsxs("section", {
|
|
561
724
|
"aria-label": props["aria-label"],
|
|
562
|
-
className:
|
|
725
|
+
className: cx("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),
|
|
563
726
|
children: [
|
|
564
727
|
(title || actions) && /* @__PURE__ */ jsxs("header", {
|
|
565
728
|
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",
|
|
@@ -572,7 +735,7 @@ function Panel({ title, actions, footer, children, variant = "card", className,
|
|
|
572
735
|
})]
|
|
573
736
|
}),
|
|
574
737
|
/* @__PURE__ */ jsx("div", {
|
|
575
|
-
className: "min-h-0 flex-1 overflow-y-auto [scrollbar-width:thin]",
|
|
738
|
+
className: cx("min-h-0 flex-1 overflow-y-auto [scrollbar-width:thin]", padded && "p-3"),
|
|
576
739
|
children
|
|
577
740
|
}),
|
|
578
741
|
footer && /* @__PURE__ */ jsx("footer", {
|
|
@@ -632,8 +795,25 @@ function useReducedMotion() {
|
|
|
632
795
|
return useCallback(() => shouldReduceMotion(motion), [motion]);
|
|
633
796
|
}
|
|
634
797
|
// src/primitives/highlight.ts
|
|
798
|
+
/**
|
|
799
|
+
* Shiki with its JavaScript regex engine, not the default Oniguruma WebAssembly one, so a page needs no
|
|
800
|
+
* `'wasm-unsafe-eval'` in its Content Security Policy. Languages and themes load on demand.
|
|
801
|
+
*/
|
|
635
802
|
let shiki;
|
|
636
|
-
const loadShiki = () => shiki ??=
|
|
803
|
+
const loadShiki = () => shiki ??= (async () => {
|
|
804
|
+
const [lib, { createJavaScriptRegexEngine }] = await Promise.all([import("shiki"), import("shiki/engine/javascript")]);
|
|
805
|
+
return {
|
|
806
|
+
lib,
|
|
807
|
+
highlighter: await lib.createHighlighter({
|
|
808
|
+
themes: [],
|
|
809
|
+
langs: [],
|
|
810
|
+
engine: createJavaScriptRegexEngine({ forgiving: true })
|
|
811
|
+
})
|
|
812
|
+
};
|
|
813
|
+
})().catch((error) => {
|
|
814
|
+
shiki = void 0;
|
|
815
|
+
throw error;
|
|
816
|
+
});
|
|
637
817
|
const cache = /* @__PURE__ */ new Map();
|
|
638
818
|
const warned = /* @__PURE__ */ new Set();
|
|
639
819
|
/** The theme id if Shiki bundles it; otherwise the default, with one console.warn per unknown id. */
|
|
@@ -646,6 +826,13 @@ function pick(id, fallback, known) {
|
|
|
646
826
|
}
|
|
647
827
|
return fallback;
|
|
648
828
|
}
|
|
829
|
+
let failedWarned = false;
|
|
830
|
+
/** One console.warn when highlighting breaks (a blocked chunk, a CSP violation), instead of silently showing plain code. */
|
|
831
|
+
function warnFailed(error) {
|
|
832
|
+
if (failedWarned) return;
|
|
833
|
+
failedWarned = true;
|
|
834
|
+
console.warn("agentic-ui-kit: code highlighting failed, showing plain code.", error);
|
|
835
|
+
}
|
|
649
836
|
/** Parses "--a:#fff;--b:#000" into an object. */
|
|
650
837
|
function parseVars(css) {
|
|
651
838
|
const vars = {};
|
|
@@ -659,15 +846,17 @@ async function highlight(code, language, light, dark) {
|
|
|
659
846
|
const key = `${light}\n${dark}\n${language}\n${code}`;
|
|
660
847
|
const hit = cache.get(key);
|
|
661
848
|
if (hit) return hit;
|
|
662
|
-
const {
|
|
849
|
+
const { lib: { bundledLanguages, bundledLanguagesAlias, bundledThemes }, highlighter } = await loadShiki();
|
|
663
850
|
const lang = language.toLowerCase();
|
|
664
851
|
if (!(lang in bundledLanguages) && !(lang in bundledLanguagesAlias)) return void 0;
|
|
665
|
-
const
|
|
852
|
+
const themes = {
|
|
853
|
+
light: pick(light, DEFAULT_CODE_THEMES.light, bundledThemes),
|
|
854
|
+
dark: pick(dark, DEFAULT_CODE_THEMES.dark, bundledThemes)
|
|
855
|
+
};
|
|
856
|
+
await Promise.all([highlighter.loadLanguage(lang), highlighter.loadTheme(themes.light, themes.dark)]);
|
|
857
|
+
const { tokens, bg, fg } = highlighter.codeToTokens(code, {
|
|
666
858
|
lang,
|
|
667
|
-
themes
|
|
668
|
-
light: pick(light, DEFAULT_CODE_THEMES.light, bundledThemes),
|
|
669
|
-
dark: pick(dark, DEFAULT_CODE_THEMES.dark, bundledThemes)
|
|
670
|
-
},
|
|
859
|
+
themes,
|
|
671
860
|
defaultColor: false
|
|
672
861
|
});
|
|
673
862
|
const result = {
|
|
@@ -692,7 +881,10 @@ function useHighlight(code, language) {
|
|
|
692
881
|
useEffect(() => {
|
|
693
882
|
if (!language) return setResult(void 0);
|
|
694
883
|
let cancelled = false;
|
|
695
|
-
highlight(code, language, light, dark).then((r) => !cancelled && setResult(r), () =>
|
|
884
|
+
highlight(code, language, light, dark).then((r) => !cancelled && setResult(r), (error) => {
|
|
885
|
+
warnFailed(error);
|
|
886
|
+
if (!cancelled) setResult(void 0);
|
|
887
|
+
});
|
|
696
888
|
return () => {
|
|
697
889
|
cancelled = true;
|
|
698
890
|
};
|
|
@@ -726,7 +918,7 @@ function CodeBlock({ code, language, label = language, variant = "plain", copy =
|
|
|
726
918
|
}), copy && /* @__PURE__ */ jsx(CopyButton, { text: code })]
|
|
727
919
|
}), /* @__PURE__ */ jsx("pre", {
|
|
728
920
|
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]"),
|
|
729
|
-
children: /* @__PURE__ */ jsxs("code", { children: [highlighted ? highlighted.map((line, i) => /* @__PURE__ */ jsxs(Fragment
|
|
921
|
+
children: /* @__PURE__ */ jsxs("code", { children: [highlighted ? highlighted.map((line, i) => /* @__PURE__ */ jsxs(Fragment, { children: [i > 0 && "\n", line.map((token, j) => /* @__PURE__ */ jsx("span", {
|
|
730
922
|
className: "chat-code-token",
|
|
731
923
|
style: token.htmlStyle,
|
|
732
924
|
children: token.content
|
|
@@ -738,12 +930,15 @@ function CodeBlock({ code, language, label = language, variant = "plain", copy =
|
|
|
738
930
|
/**
|
|
739
931
|
* Applies a chat theme to part of a page. Menus, tooltips and dialogs opened inside render into this
|
|
740
932
|
* element instead of `<body>`, so they get the same theme. An app themed as a whole (variables on
|
|
741
|
-
* `:root`) does not need it for colors, but still uses it for `codeThemes` and `motion`.
|
|
933
|
+
* `:root`) does not need it for colors, but still uses it for `codeThemes` and `motion`. The element is
|
|
934
|
+
* `height: 100%` of its parent by default (so `ChatView` has a height without Tailwind); a `className` such
|
|
935
|
+
* as `h-screen`, or a style rule of your own, replaces that.
|
|
742
936
|
*/
|
|
743
937
|
function ChatTheme({ theme, codeThemes, motion, labels, icons, children, className }) {
|
|
744
938
|
const portal = useRef(null);
|
|
745
939
|
const getContainer = useCallback(() => portal.current, []);
|
|
746
940
|
return /* @__PURE__ */ jsxs("div", {
|
|
941
|
+
"data-chat-root": "",
|
|
747
942
|
"data-chat-theme": theme,
|
|
748
943
|
"data-chat-motion": motion,
|
|
749
944
|
className: clsx("bg-chat-bg font-chat-sans text-chat-fg", className),
|
|
@@ -881,7 +1076,7 @@ const DECLINED = "Declined by the user";
|
|
|
881
1076
|
* retries. With an `AgentProvider` it also runs the agent loop: client tools run in the page, approvals
|
|
882
1077
|
* wait for the user, then the reply continues; every finished tool call reaches its tool's `onResult`.
|
|
883
1078
|
*/
|
|
884
|
-
function useChat({ transport, conversationId, initialMessages = [], onTitle, agent, maxSteps = 12 }) {
|
|
1079
|
+
function useChat({ transport, conversationId, initialMessages = [], onTitle, agent, maxSteps = 12, resume = false }) {
|
|
885
1080
|
const contextAgent = useAgentRegistry();
|
|
886
1081
|
const registry = agent === void 0 ? contextAgent : agent;
|
|
887
1082
|
const [messages, setMessages] = useState(initialMessages);
|
|
@@ -949,7 +1144,7 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
|
|
|
949
1144
|
if (ended) commit(ended);
|
|
950
1145
|
}, [commit]);
|
|
951
1146
|
const advanceRef = useRef(() => {});
|
|
952
|
-
const start = useCallback((history, options, continuation) => {
|
|
1147
|
+
const start = useCallback((history, options, continuation, joining) => {
|
|
953
1148
|
const message = history.at(-1);
|
|
954
1149
|
const controller = new AbortController();
|
|
955
1150
|
const current = {
|
|
@@ -965,7 +1160,7 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
|
|
|
965
1160
|
try {
|
|
966
1161
|
const { transport, conversationId, registry } = latest.current;
|
|
967
1162
|
const tools = registry?.specs();
|
|
968
|
-
|
|
1163
|
+
const events = joining && conversationId !== void 0 && transport.resume ? transport.resume(conversationId, controller.signal) : transport.send({
|
|
969
1164
|
message,
|
|
970
1165
|
history,
|
|
971
1166
|
signal: controller.signal,
|
|
@@ -975,7 +1170,8 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
|
|
|
975
1170
|
...conversationId !== void 0 && { conversationId },
|
|
976
1171
|
...options.model !== void 0 && { model: options.model },
|
|
977
1172
|
...options.effort !== void 0 && { effort: options.effort }
|
|
978
|
-
})
|
|
1173
|
+
});
|
|
1174
|
+
for await (const event of events) {
|
|
979
1175
|
if (controller.signal.aborted) break;
|
|
980
1176
|
if (event.type === "title") {
|
|
981
1177
|
latest.current.onTitle?.(event.title);
|
|
@@ -1147,7 +1343,30 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
|
|
|
1147
1343
|
};
|
|
1148
1344
|
start(messages.slice(0, index), options);
|
|
1149
1345
|
}, [start]);
|
|
1150
|
-
|
|
1346
|
+
const joined = useRef(false);
|
|
1347
|
+
useEffect(() => {
|
|
1348
|
+
if (!resume || joined.current || run.current) return;
|
|
1349
|
+
const { transport, conversationId, messages } = latest.current;
|
|
1350
|
+
if (!transport.resume || conversationId === void 0) return;
|
|
1351
|
+
joined.current = true;
|
|
1352
|
+
const last = messages.at(-1);
|
|
1353
|
+
const history = last?.role === "assistant" && last.status === "streaming" ? messages.slice(0, -1) : messages;
|
|
1354
|
+
loop.current = {
|
|
1355
|
+
options: {},
|
|
1356
|
+
step: 0
|
|
1357
|
+
};
|
|
1358
|
+
start(history, {}, void 0, true);
|
|
1359
|
+
}, [resume, start]);
|
|
1360
|
+
const mounted = useRef(false);
|
|
1361
|
+
useEffect(() => {
|
|
1362
|
+
mounted.current = true;
|
|
1363
|
+
return () => {
|
|
1364
|
+
mounted.current = false;
|
|
1365
|
+
queueMicrotask(() => {
|
|
1366
|
+
if (!mounted.current) run.current?.controller.abort();
|
|
1367
|
+
});
|
|
1368
|
+
};
|
|
1369
|
+
}, []);
|
|
1151
1370
|
return {
|
|
1152
1371
|
messages,
|
|
1153
1372
|
busy,
|
|
@@ -1160,7 +1379,7 @@ function useChat({ transport, conversationId, initialMessages = [], onTitle, age
|
|
|
1160
1379
|
}
|
|
1161
1380
|
// src/hooks/sse-transport.ts
|
|
1162
1381
|
/** A `Transport` for backends that stream `StreamEvent`s over server-sent events (see `formatEvent`). */
|
|
1163
|
-
function sseTransport({ url, headers, stopUrl, fetch: fetchImpl = fetch }) {
|
|
1382
|
+
function sseTransport({ url, headers, stopUrl, resumeUrl, fetch: fetchImpl = fetch }) {
|
|
1164
1383
|
const headersFor = async () => ({
|
|
1165
1384
|
"content-type": "application/json",
|
|
1166
1385
|
...Object.fromEntries(new Headers(typeof headers === "function" ? await headers() : headers))
|
|
@@ -1176,6 +1395,20 @@ function sseTransport({ url, headers, stopUrl, fetch: fetchImpl = fetch }) {
|
|
|
1176
1395
|
if (!response.ok || !response.body) throw new Error(`${url} answered ${response.status} ${response.statusText}`.trim());
|
|
1177
1396
|
yield* readEventStream(response.body);
|
|
1178
1397
|
},
|
|
1398
|
+
...resumeUrl && { async *resume(conversationId, signal) {
|
|
1399
|
+
const target = typeof resumeUrl === "function" ? resumeUrl(conversationId) : `${resumeUrl}${resumeUrl.includes("?") ? "&" : "?"}conversationId=${encodeURIComponent(conversationId)}`;
|
|
1400
|
+
const { "content-type": _, ...headers } = await headersFor();
|
|
1401
|
+
const response = await fetchImpl(target, {
|
|
1402
|
+
method: "GET",
|
|
1403
|
+
headers: {
|
|
1404
|
+
...headers,
|
|
1405
|
+
accept: "text/event-stream"
|
|
1406
|
+
},
|
|
1407
|
+
signal
|
|
1408
|
+
});
|
|
1409
|
+
if (!response.ok || !response.body) throw new Error(`${target} answered ${response.status} ${response.statusText}`.trim());
|
|
1410
|
+
yield* readEventStream(response.body);
|
|
1411
|
+
} },
|
|
1179
1412
|
...stopUrl && { async stop(runId) {
|
|
1180
1413
|
await fetchImpl(stopUrl, {
|
|
1181
1414
|
method: "POST",
|
|
@@ -1280,6 +1513,30 @@ const SidebarContext = createContext({
|
|
|
1280
1513
|
dismiss: () => {}
|
|
1281
1514
|
});
|
|
1282
1515
|
const useSidebar = () => useContext(SidebarContext);
|
|
1516
|
+
/** State that the parent may own: `value` makes it controlled, otherwise `initial` seeds it. */
|
|
1517
|
+
function useControllable(value, initial, onChange) {
|
|
1518
|
+
const [inner, setInner] = useState(initial);
|
|
1519
|
+
const current = value ?? inner;
|
|
1520
|
+
const latest = useRef({
|
|
1521
|
+
current,
|
|
1522
|
+
value,
|
|
1523
|
+
onChange
|
|
1524
|
+
});
|
|
1525
|
+
latest.current = {
|
|
1526
|
+
current,
|
|
1527
|
+
value,
|
|
1528
|
+
onChange
|
|
1529
|
+
};
|
|
1530
|
+
return [current, useCallback((next) => {
|
|
1531
|
+
const { current, value, onChange } = latest.current;
|
|
1532
|
+
const resolved = typeof next === "function" ? next(current) : next;
|
|
1533
|
+
if (resolved === current) return;
|
|
1534
|
+
if (value === void 0) setInner(resolved);
|
|
1535
|
+
onChange?.(resolved);
|
|
1536
|
+
}, [])];
|
|
1537
|
+
}
|
|
1538
|
+
/** True on Apple platforms, where the shortcut modifier is ⌘ instead of Ctrl. */
|
|
1539
|
+
const isMacPlatform = () => typeof navigator !== "undefined" && /mac|iphone|ipad|ipod/i.test(navigator.platform || navigator.userAgent);
|
|
1283
1540
|
// src/components/replacements.tsx
|
|
1284
1541
|
/**
|
|
1285
1542
|
* In-place updates of drawn visuals: a tool call whose input has `replaces: <id>` takes the place of that
|
|
@@ -2193,7 +2450,7 @@ function Code({ node, className, children }) {
|
|
|
2193
2450
|
});
|
|
2194
2451
|
}
|
|
2195
2452
|
const components = {
|
|
2196
|
-
pre: ({ children }) => /* @__PURE__ */ jsx(Fragment, { children }),
|
|
2453
|
+
pre: ({ children }) => /* @__PURE__ */ jsx(Fragment$1, { children }),
|
|
2197
2454
|
code: ({ node, className, children }) => /* @__PURE__ */ jsx(Code, {
|
|
2198
2455
|
node,
|
|
2199
2456
|
className,
|
|
@@ -2201,12 +2458,39 @@ const components = {
|
|
|
2201
2458
|
})
|
|
2202
2459
|
};
|
|
2203
2460
|
const LinksContext = createContext(void 0);
|
|
2204
|
-
function MarkdownLinksProvider({ renderLink, children }) {
|
|
2461
|
+
function MarkdownLinksProvider({ renderLink, renderLinkList, children }) {
|
|
2462
|
+
const value = useMemo(() => ({
|
|
2463
|
+
renderLink,
|
|
2464
|
+
renderLinkList
|
|
2465
|
+
}), [renderLink, renderLinkList]);
|
|
2205
2466
|
return /* @__PURE__ */ jsx(LinksContext.Provider, {
|
|
2206
|
-
value
|
|
2467
|
+
value,
|
|
2207
2468
|
children
|
|
2208
2469
|
});
|
|
2209
2470
|
}
|
|
2471
|
+
/** The rendered link an item consists of: an `a` alone, or alone in its paragraph (a loose list). */
|
|
2472
|
+
function soleLink(children) {
|
|
2473
|
+
const nodes = Children.toArray(children).filter((c) => !(typeof c === "string" && !c.trim()));
|
|
2474
|
+
if (nodes.length !== 1 || !isValidElement(nodes[0])) return void 0;
|
|
2475
|
+
const props = nodes[0].props;
|
|
2476
|
+
if (typeof props.href === "string") return {
|
|
2477
|
+
href: props.href,
|
|
2478
|
+
children: props.children
|
|
2479
|
+
};
|
|
2480
|
+
return props.node?.tagName === "p" ? soleLink(props.children) : void 0;
|
|
2481
|
+
}
|
|
2482
|
+
function listOf(ordered, props, renderLinkList) {
|
|
2483
|
+
const Default = ordered ? defaultComponents.ol : defaultComponents.ul;
|
|
2484
|
+
const { children } = props;
|
|
2485
|
+
const links = Children.toArray(children).filter((c) => isValidElement(c)).map((item) => soleLink(item.props.children));
|
|
2486
|
+
const list = /* @__PURE__ */ jsx(Default, { ...props });
|
|
2487
|
+
if (links.length === 0 || links.some((l) => !l)) return list;
|
|
2488
|
+
return renderLinkList({
|
|
2489
|
+
ordered,
|
|
2490
|
+
links,
|
|
2491
|
+
children: list
|
|
2492
|
+
}) ?? list;
|
|
2493
|
+
}
|
|
2210
2494
|
/** Message markdown: GFM, KaTeX math ($…$ and $$…$$), Shiki code blocks, smooth fade-in while streaming. */
|
|
2211
2495
|
function Markdown({ children, streaming = false, className, partId }) {
|
|
2212
2496
|
const text = useSmoothText(children, streaming);
|
|
@@ -2237,22 +2521,26 @@ function BlockScope({ id, streaming, children }) {
|
|
|
2237
2521
|
});
|
|
2238
2522
|
}
|
|
2239
2523
|
const MarkdownBody = memo(function MarkdownBody({ text, animating, fade }) {
|
|
2240
|
-
const renderLink = useContext(LinksContext);
|
|
2241
|
-
const withLinks = useMemo(() =>
|
|
2524
|
+
const { renderLink, renderLinkList } = useContext(LinksContext) ?? {};
|
|
2525
|
+
const withLinks = useMemo(() => ({
|
|
2242
2526
|
...components,
|
|
2243
|
-
a: ({ href, children }) => {
|
|
2527
|
+
...renderLink && { a: ({ href, children }) => {
|
|
2244
2528
|
const custom = href ? renderLink({
|
|
2245
2529
|
href,
|
|
2246
2530
|
children
|
|
2247
2531
|
}) : void 0;
|
|
2248
|
-
return custom !== void 0 ? /* @__PURE__ */ jsx(Fragment, { children: custom }) : /* @__PURE__ */ jsx("a", {
|
|
2532
|
+
return custom !== void 0 ? /* @__PURE__ */ jsx(Fragment$1, { children: custom }) : /* @__PURE__ */ jsx("a", {
|
|
2249
2533
|
href,
|
|
2250
2534
|
target: "_blank",
|
|
2251
2535
|
rel: "noopener noreferrer",
|
|
2252
2536
|
children
|
|
2253
2537
|
});
|
|
2538
|
+
} },
|
|
2539
|
+
...renderLinkList && {
|
|
2540
|
+
ul: (props) => listOf(false, props, renderLinkList),
|
|
2541
|
+
ol: (props) => listOf(true, props, renderLinkList)
|
|
2254
2542
|
}
|
|
2255
|
-
}
|
|
2543
|
+
}), [renderLink, renderLinkList]);
|
|
2256
2544
|
return /* @__PURE__ */ jsx(Streamdown, {
|
|
2257
2545
|
prefix: PREFIX,
|
|
2258
2546
|
mode: animating || fade ? "streaming" : "static",
|
|
@@ -2312,7 +2600,7 @@ function activityLabel(part, labels, view) {
|
|
|
2312
2600
|
*/
|
|
2313
2601
|
function activityHeading(parts, views, labels) {
|
|
2314
2602
|
const waiting = parts.find((p) => p.type === "tool_call" && p.status === "awaiting");
|
|
2315
|
-
if (waiting) return /* @__PURE__ */ jsxs(Fragment, { children: [activityLabel(waiting, labels, views[waiting.name]), labels.waitingForYou] });
|
|
2603
|
+
if (waiting) return /* @__PURE__ */ jsxs(Fragment$1, { children: [activityLabel(waiting, labels, views[waiting.name]), labels.waitingForYou] });
|
|
2316
2604
|
if (parts.some(isWorking)) {
|
|
2317
2605
|
const last = parts.at(-1);
|
|
2318
2606
|
return activityLabel(last, labels, last.type === "tool_call" ? views[last.name] : void 0);
|
|
@@ -2331,6 +2619,7 @@ function stringOf(value) {
|
|
|
2331
2619
|
}
|
|
2332
2620
|
/** A shell command. Input `{ command, description? }`; result a string or `{ stdout, stderr, exitCode }`. */
|
|
2333
2621
|
const commandTool = {
|
|
2622
|
+
ask: (_, labels) => labels.runCommandAsk,
|
|
2334
2623
|
label: (_, labels) => labels.ranCommandLabel,
|
|
2335
2624
|
hint: (part) => stringOf(recordOf(part.input).description),
|
|
2336
2625
|
summary: (n, labels) => labels.ranCommands(n),
|
|
@@ -2363,6 +2652,7 @@ const commandTool = {
|
|
|
2363
2652
|
* part's display `data`, else its `result`.
|
|
2364
2653
|
*/
|
|
2365
2654
|
const webSearchTool = {
|
|
2655
|
+
ask: (_, labels) => labels.searchWebAsk,
|
|
2366
2656
|
label: (_, labels) => labels.searchedWebLabel,
|
|
2367
2657
|
subtitle: (part) => stringOf(recordOf(part.input).query) ?? part.subtitle,
|
|
2368
2658
|
summary: (n, labels) => labels.searchedWeb(n),
|
|
@@ -2410,6 +2700,7 @@ const webSearchTool = {
|
|
|
2410
2700
|
};
|
|
2411
2701
|
/** Reads a web page. Input `{ url }`; the row shows the page's host, the detail links to it. */
|
|
2412
2702
|
const webFetchTool = {
|
|
2703
|
+
ask: (_, labels) => labels.readPageAsk,
|
|
2413
2704
|
label: (_, labels) => labels.readPageLabel,
|
|
2414
2705
|
subtitle: (part) => {
|
|
2415
2706
|
const url = stringOf(recordOf(part.input).url);
|
|
@@ -2550,7 +2841,7 @@ function ActivityGroup({ parts, defaultExpanded = false, className }) {
|
|
|
2550
2841
|
}) : /* @__PURE__ */ jsx(Disclosure, {
|
|
2551
2842
|
isExpanded: expanded,
|
|
2552
2843
|
onExpandedChange: setExpanded,
|
|
2553
|
-
summary: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2844
|
+
summary: /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
2554
2845
|
/* @__PURE__ */ jsx(Lead, { live }),
|
|
2555
2846
|
/* @__PURE__ */ jsx(Live, {
|
|
2556
2847
|
live,
|
|
@@ -2584,7 +2875,7 @@ const SHIMMER_MS = 2e3;
|
|
|
2584
2875
|
* shared clock, so every live label sweeps in step and a remount does not restart it.
|
|
2585
2876
|
*/
|
|
2586
2877
|
function Live({ live, children }) {
|
|
2587
|
-
return live ? /* @__PURE__ */ jsx(Shimmer, { children }) : /* @__PURE__ */ jsx(Fragment, { children });
|
|
2878
|
+
return live ? /* @__PURE__ */ jsx(Shimmer, { children }) : /* @__PURE__ */ jsx(Fragment$1, { children });
|
|
2588
2879
|
}
|
|
2589
2880
|
function Shimmer({ children }) {
|
|
2590
2881
|
const [delay] = useState(() => `-${Math.round(performance.now() % SHIMMER_MS)}ms`);
|
|
@@ -2602,7 +2893,7 @@ function SingleStep({ part, live, after, expanded, onExpandedChange }) {
|
|
|
2602
2893
|
return /* @__PURE__ */ jsx(Disclosure, {
|
|
2603
2894
|
isExpanded: expanded,
|
|
2604
2895
|
onExpandedChange,
|
|
2605
|
-
summary: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2896
|
+
summary: /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
2606
2897
|
/* @__PURE__ */ jsx(Lead, { live }),
|
|
2607
2898
|
/* @__PURE__ */ jsx(Live, {
|
|
2608
2899
|
live,
|
|
@@ -2773,11 +3064,19 @@ function useAttachments({ upload } = {}) {
|
|
|
2773
3064
|
toParts
|
|
2774
3065
|
};
|
|
2775
3066
|
}
|
|
3067
|
+
const IMAGE_EXTENSION = /\.(png|jpe?g|gif|webp|avif|svg|bmp|ico)$/i;
|
|
3068
|
+
/** Whether the card draws a thumbnail: by mime when given, else by `kind`, else by the file extension of the url or name. */
|
|
3069
|
+
function isImage({ mime, kind, url, name }) {
|
|
3070
|
+
if (mime) return mime.startsWith("image/");
|
|
3071
|
+
if (kind) return kind === "image";
|
|
3072
|
+
const path = url && !url.startsWith("data:") ? url.split(/[?#]/)[0] : "";
|
|
3073
|
+
return url?.startsWith("data:image/") === true || IMAGE_EXTENSION.test(path) || IMAGE_EXTENSION.test(name);
|
|
3074
|
+
}
|
|
2776
3075
|
const extension = (name) => /\.([a-z0-9]{1,5})$/i.exec(name)?.[1]?.toUpperCase();
|
|
2777
3076
|
/** A square card: pasted text preview with a PASTED badge, an image thumbnail, or a file with its type. */
|
|
2778
3077
|
function AttachmentCard({ attachment: a, onRemove, onOpen, size = "md", className }) {
|
|
2779
3078
|
const icons = useIcons();
|
|
2780
|
-
const image = a.
|
|
3079
|
+
const image = a.url && isImage(a);
|
|
2781
3080
|
const pasted = a.text !== void 0;
|
|
2782
3081
|
const labels = useLabels();
|
|
2783
3082
|
const uploading = a.status === "uploading";
|
|
@@ -2842,7 +3141,7 @@ function AttachmentRow({ attachments, onRemove, onEditText }) {
|
|
|
2842
3141
|
const [editing, setEditing] = useState();
|
|
2843
3142
|
const labels = useLabels();
|
|
2844
3143
|
if (attachments.length === 0) return null;
|
|
2845
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("ul", {
|
|
3144
|
+
return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("ul", {
|
|
2846
3145
|
"aria-label": labels.attachments,
|
|
2847
3146
|
className: "flex gap-2.5 overflow-x-auto px-0.5 pt-2.5 pb-1 [scrollbar-width:thin]",
|
|
2848
3147
|
children: attachments.map((a) => /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(AttachmentCard, {
|
|
@@ -3012,7 +3311,7 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
|
|
|
3012
3311
|
if (block.kind === "activity") {
|
|
3013
3312
|
const cards = block.parts.filter((p) => p.type === "tool_call" && views[p.name]?.Card);
|
|
3014
3313
|
if (!cards.length) return /* @__PURE__ */ jsx(ActivityGroup, { parts: block.parts }, block.id);
|
|
3015
|
-
return /* @__PURE__ */ jsxs(Fragment
|
|
3314
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ActivityGroup, { parts: block.parts }), cards.map((p) => {
|
|
3016
3315
|
if (p.type !== "tool_call") return null;
|
|
3017
3316
|
const Card = views[p.name].Card;
|
|
3018
3317
|
return /* @__PURE__ */ jsx(Card, { part: p }, p.id);
|
|
@@ -3054,7 +3353,7 @@ function AssistantMessage({ message, onRetry, onFeedback, onReadAloud, renderPar
|
|
|
3054
3353
|
onPress: onReadAloud,
|
|
3055
3354
|
children: icons.speak
|
|
3056
3355
|
}),
|
|
3057
|
-
onFeedback && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(IconButton, {
|
|
3356
|
+
onFeedback && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(IconButton, {
|
|
3058
3357
|
label: labels.goodResponse,
|
|
3059
3358
|
size: "sm",
|
|
3060
3359
|
onPress: () => onFeedback("good"),
|
|
@@ -3108,7 +3407,7 @@ function ApprovalCard({ approval, onRespond, className }) {
|
|
|
3108
3407
|
children: icons.approval
|
|
3109
3408
|
}), /* @__PURE__ */ jsx("span", {
|
|
3110
3409
|
className: "min-w-0 flex-1 font-medium",
|
|
3111
|
-
children: view.ask?.(part) ?? labels.allowAction(activityLabel(part, labels, view))
|
|
3410
|
+
children: view.ask?.(part, labels) ?? labels.allowAction(view.askLabel?.(part, labels) ?? activityLabel(part, labels, view))
|
|
3112
3411
|
})]
|
|
3113
3412
|
}),
|
|
3114
3413
|
/* @__PURE__ */ jsx("div", {
|
|
@@ -3139,6 +3438,16 @@ function ApprovalCard({ approval, onRespond, className }) {
|
|
|
3139
3438
|
// src/components/context-panel.tsx
|
|
3140
3439
|
const FREE_COLOR = "var(--chat-raised)";
|
|
3141
3440
|
const DEFAULT_COLOR = "var(--chat-subtle)";
|
|
3441
|
+
/** Tokens used (reserved and deferred categories excluded) and tokens still free. */
|
|
3442
|
+
function contextTotals(usage) {
|
|
3443
|
+
const counted = usage.categories.filter((c) => !c.deferred);
|
|
3444
|
+
const used = counted.reduce((sum, c) => sum + (c.reserved ? 0 : c.tokens), 0);
|
|
3445
|
+
const reserved = counted.reduce((sum, c) => sum + (c.reserved ? c.tokens : 0), 0);
|
|
3446
|
+
return {
|
|
3447
|
+
used,
|
|
3448
|
+
free: Math.max(usage.limit - used - reserved, 0)
|
|
3449
|
+
};
|
|
3450
|
+
}
|
|
3142
3451
|
/**
|
|
3143
3452
|
* What fills the context window: a usage line, a segmented bar, one row per category with its share, and
|
|
3144
3453
|
* expandable lists (each MCP tool, each memory file). Deferred categories are listed but not counted.
|
|
@@ -3147,9 +3456,7 @@ function ContextPanel({ usage, title, defaultExpanded = true, className }) {
|
|
|
3147
3456
|
const labels = useLabels();
|
|
3148
3457
|
const counted = usage.categories.filter((c) => !c.deferred);
|
|
3149
3458
|
const deferred = usage.categories.filter((c) => c.deferred);
|
|
3150
|
-
const used
|
|
3151
|
-
const reserved = counted.reduce((sum, c) => sum + (c.reserved ? c.tokens : 0), 0);
|
|
3152
|
-
const free = Math.max(usage.limit - used - reserved, 0);
|
|
3459
|
+
const { used, free } = contextTotals(usage);
|
|
3153
3460
|
const share = (tokens) => formatShare(tokens / usage.limit);
|
|
3154
3461
|
return /* @__PURE__ */ jsxs(Disclosure, {
|
|
3155
3462
|
summary: title ?? labels.context,
|
|
@@ -3274,6 +3581,121 @@ function GroupList({ group }) {
|
|
|
3274
3581
|
function formatShare(fraction) {
|
|
3275
3582
|
return `${(fraction * 100).toFixed(1)}%`;
|
|
3276
3583
|
}
|
|
3584
|
+
// src/components/context-meter.tsx
|
|
3585
|
+
const SIZE = 16;
|
|
3586
|
+
const STROKE = 2;
|
|
3587
|
+
const RADIUS = 7;
|
|
3588
|
+
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
|
|
3589
|
+
/**
|
|
3590
|
+
* A small progress ring for the composer toolbar, filled to the share of the context window in use (accent,
|
|
3591
|
+
* then warning, then danger). Hover for the numbers; click or Enter opens the ContextPanel above it.
|
|
3592
|
+
*/
|
|
3593
|
+
function ContextMeter({ usage, tooltip, tooltipNote, isLoading = false, warnAt = .75, dangerAt = .9, placement = "top end", className }) {
|
|
3594
|
+
const labels = useLabels();
|
|
3595
|
+
const { used } = contextTotals(usage);
|
|
3596
|
+
const fraction = usage.limit > 0 ? Math.min(used / usage.limit, 1) : 0;
|
|
3597
|
+
const summary = isLoading ? labels.contextLoading : labels.contextSummary(formatTokens(used), formatTokens(usage.limit), Math.round(fraction * 100));
|
|
3598
|
+
const color = fraction >= dangerAt ? "text-chat-danger" : fraction >= warnAt ? "text-chat-warning" : "text-chat-accent";
|
|
3599
|
+
const button = /* @__PURE__ */ jsx(Button, {
|
|
3600
|
+
variant: "ghost",
|
|
3601
|
+
size: "sm",
|
|
3602
|
+
"aria-label": summary,
|
|
3603
|
+
isDisabled: isLoading,
|
|
3604
|
+
className: clsx("size-7 justify-center px-0 text-chat-muted", isLoading && "data-disabled:opacity-100", className),
|
|
3605
|
+
children: /* @__PURE__ */ jsxs("svg", {
|
|
3606
|
+
"aria-hidden": true,
|
|
3607
|
+
viewBox: `0 0 ${SIZE} ${SIZE}`,
|
|
3608
|
+
className: clsx("size-4", isLoading && "motion-safe:animate-spin"),
|
|
3609
|
+
fill: "none",
|
|
3610
|
+
strokeWidth: STROKE,
|
|
3611
|
+
children: [/* @__PURE__ */ jsx("circle", {
|
|
3612
|
+
cx: SIZE / 2,
|
|
3613
|
+
cy: SIZE / 2,
|
|
3614
|
+
r: RADIUS,
|
|
3615
|
+
stroke: "var(--chat-raised)"
|
|
3616
|
+
}), /* @__PURE__ */ jsx("circle", {
|
|
3617
|
+
cx: SIZE / 2,
|
|
3618
|
+
cy: SIZE / 2,
|
|
3619
|
+
r: RADIUS,
|
|
3620
|
+
stroke: "currentColor",
|
|
3621
|
+
strokeLinecap: "round",
|
|
3622
|
+
strokeDasharray: CIRCUMFERENCE,
|
|
3623
|
+
strokeDashoffset: CIRCUMFERENCE * (1 - (isLoading ? .25 : fraction)),
|
|
3624
|
+
transform: `rotate(-90 ${SIZE / 2} ${SIZE / 2})`,
|
|
3625
|
+
className: clsx(color, "transition-[stroke-dashoffset]")
|
|
3626
|
+
})]
|
|
3627
|
+
})
|
|
3628
|
+
});
|
|
3629
|
+
return /* @__PURE__ */ jsxs(DialogTrigger$1, { children: [/* @__PURE__ */ jsx(Tooltip, {
|
|
3630
|
+
content: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", { children: tooltip ?? summary }), tooltipNote && /* @__PURE__ */ jsx("div", {
|
|
3631
|
+
className: "text-chat-muted",
|
|
3632
|
+
children: tooltipNote
|
|
3633
|
+
})] }),
|
|
3634
|
+
children: button
|
|
3635
|
+
}), /* @__PURE__ */ jsx(Popover, {
|
|
3636
|
+
placement,
|
|
3637
|
+
className: "w-80 overflow-auto p-3",
|
|
3638
|
+
children: /* @__PURE__ */ jsx(Dialog, {
|
|
3639
|
+
"aria-label": labels.context,
|
|
3640
|
+
className: "outline-none",
|
|
3641
|
+
children: /* @__PURE__ */ jsx(ContextPanel, { usage })
|
|
3642
|
+
})
|
|
3643
|
+
})] });
|
|
3644
|
+
}
|
|
3645
|
+
// src/components/copy-field.tsx
|
|
3646
|
+
/** How long "Copied" stays before the button reads "Copy" again. */
|
|
3647
|
+
const COPIED_MS = 2e3;
|
|
3648
|
+
/**
|
|
3649
|
+
* A read-only, selectable field with a Copy button, for share links. The button says "Copied" for about two
|
|
3650
|
+
* seconds (and announces it to screen readers). Where the clipboard is unavailable or refuses, it selects the
|
|
3651
|
+
* text instead so the reader can copy it by hand.
|
|
3652
|
+
*/
|
|
3653
|
+
function CopyField({ value, "aria-label": ariaLabel, onCopy, className }) {
|
|
3654
|
+
const icons = useIcons();
|
|
3655
|
+
const labels = useLabels();
|
|
3656
|
+
const inputRef = useRef(null);
|
|
3657
|
+
const timer = useRef(void 0);
|
|
3658
|
+
const [copied, setCopied] = useState(false);
|
|
3659
|
+
useEffect(() => () => clearTimeout(timer.current), []);
|
|
3660
|
+
const copy = async () => {
|
|
3661
|
+
try {
|
|
3662
|
+
if (!navigator.clipboard) throw new Error("no clipboard");
|
|
3663
|
+
await navigator.clipboard.writeText(value);
|
|
3664
|
+
} catch {
|
|
3665
|
+
inputRef.current?.focus();
|
|
3666
|
+
inputRef.current?.select();
|
|
3667
|
+
return;
|
|
3668
|
+
}
|
|
3669
|
+
setCopied(true);
|
|
3670
|
+
onCopy?.();
|
|
3671
|
+
clearTimeout(timer.current);
|
|
3672
|
+
timer.current = setTimeout(() => setCopied(false), COPIED_MS);
|
|
3673
|
+
};
|
|
3674
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3675
|
+
className: clsx("flex items-center gap-2 font-chat-sans", className),
|
|
3676
|
+
children: [
|
|
3677
|
+
/* @__PURE__ */ jsx("input", {
|
|
3678
|
+
ref: inputRef,
|
|
3679
|
+
readOnly: true,
|
|
3680
|
+
value,
|
|
3681
|
+
"aria-label": ariaLabel ?? labels.copyFieldLabel,
|
|
3682
|
+
onFocus: (e) => e.currentTarget.select(),
|
|
3683
|
+
className: "h-9 min-w-0 flex-1 rounded-chat-md border border-chat-border bg-chat-bg px-3 text-(length:--chat-ui-font-size) text-chat-fg outline-none focus:border-chat-border-strong"
|
|
3684
|
+
}),
|
|
3685
|
+
/* @__PURE__ */ jsxs(Button, {
|
|
3686
|
+
variant: "subtle",
|
|
3687
|
+
size: "lg",
|
|
3688
|
+
onPress: () => void copy(),
|
|
3689
|
+
children: [/* @__PURE__ */ jsx(IconSlot, { children: copied ? icons.copied : icons.copy }), copied ? labels.copied : labels.copy]
|
|
3690
|
+
}),
|
|
3691
|
+
/* @__PURE__ */ jsx("span", {
|
|
3692
|
+
role: "status",
|
|
3693
|
+
className: "sr-only",
|
|
3694
|
+
children: copied ? labels.copied : ""
|
|
3695
|
+
})
|
|
3696
|
+
]
|
|
3697
|
+
});
|
|
3698
|
+
}
|
|
3277
3699
|
// src/components/dictation.tsx
|
|
3278
3700
|
/**
|
|
3279
3701
|
* Voice dictation in the composer, similar to claude.ai's: while listening the words fill the box as they are
|
|
@@ -3773,7 +4195,7 @@ function EffortSlider({ efforts, value, onChange, label, className }) {
|
|
|
3773
4195
|
function EffortButton({ icon, children, className, ...slider }) {
|
|
3774
4196
|
const current = slider.efforts.find((e) => e.id === slider.value) ?? slider.efforts[0];
|
|
3775
4197
|
const labels = useLabels();
|
|
3776
|
-
return /* @__PURE__ */ jsxs(DialogTrigger, { children: [/* @__PURE__ */ jsxs(Button, {
|
|
4198
|
+
return /* @__PURE__ */ jsxs(DialogTrigger$1, { children: [/* @__PURE__ */ jsxs(Button, {
|
|
3777
4199
|
variant: "ghost",
|
|
3778
4200
|
size: "sm",
|
|
3779
4201
|
className: clsx("text-chat-muted data-hovered:text-chat-fg", className),
|
|
@@ -3801,6 +4223,7 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
|
|
|
3801
4223
|
const more = models.filter((m) => m.secondary);
|
|
3802
4224
|
const item = (m) => /* @__PURE__ */ jsx(MenuItem, {
|
|
3803
4225
|
id: m.id,
|
|
4226
|
+
...m.icon && { icon: m.icon },
|
|
3804
4227
|
description: m.disabledReason ?? m.description,
|
|
3805
4228
|
isDisabled: m.disabled ?? false,
|
|
3806
4229
|
children: m.label
|
|
@@ -3815,6 +4238,10 @@ function ModelPicker({ models, model, onModelChange, effort, onEffortChange, eff
|
|
|
3815
4238
|
"aria-label": labels.modelButton(current?.label ?? model),
|
|
3816
4239
|
className: clsx("text-chat-muted data-hovered:text-chat-fg", className),
|
|
3817
4240
|
children: [
|
|
4241
|
+
current?.icon && /* @__PURE__ */ jsx(IconSlot, {
|
|
4242
|
+
className: "text-chat-fg [&_svg]:size-3.5",
|
|
4243
|
+
children: current.icon
|
|
4244
|
+
}),
|
|
3818
4245
|
/* @__PURE__ */ jsx("span", {
|
|
3819
4246
|
className: "min-w-0 truncate text-chat-fg",
|
|
3820
4247
|
children: current?.label ?? model
|
|
@@ -3918,10 +4345,12 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, onReadAloud, actio
|
|
|
3918
4345
|
behavior
|
|
3919
4346
|
});
|
|
3920
4347
|
}, []);
|
|
3921
|
-
const
|
|
4348
|
+
const nearBottom = () => {
|
|
3922
4349
|
const el = scroller.current;
|
|
3923
|
-
|
|
3924
|
-
|
|
4350
|
+
return !el || el.scrollHeight - el.scrollTop - el.clientHeight < STICK_SLOP;
|
|
4351
|
+
};
|
|
4352
|
+
const onScroll = () => {
|
|
4353
|
+
const bottom = nearBottom();
|
|
3925
4354
|
stuck.current = bottom;
|
|
3926
4355
|
setAtBottom(bottom);
|
|
3927
4356
|
};
|
|
@@ -3938,7 +4367,7 @@ function MessageList({ messages, onRetry, onEdit, onFeedback, onReadAloud, actio
|
|
|
3938
4367
|
const jumpedFor = useRef(start === "top" ? lastUser : null);
|
|
3939
4368
|
useLayoutEffect(() => {
|
|
3940
4369
|
if (lastUser === jumpedFor.current) {
|
|
3941
|
-
|
|
4370
|
+
setAtBottom(nearBottom());
|
|
3942
4371
|
return;
|
|
3943
4372
|
}
|
|
3944
4373
|
jumpedFor.current = lastUser;
|
|
@@ -4084,7 +4513,7 @@ function ChatTitleMenu({ pinned, onPin, onRename, projects, onAddToProject, onCr
|
|
|
4084
4513
|
children: labels.shareChat
|
|
4085
4514
|
}),
|
|
4086
4515
|
children,
|
|
4087
|
-
onDelete && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(MenuSeparator, {}), /* @__PURE__ */ jsx(MenuItem, {
|
|
4516
|
+
onDelete && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(MenuSeparator, {}), /* @__PURE__ */ jsx(MenuItem, {
|
|
4088
4517
|
id: "delete",
|
|
4089
4518
|
icon: icons.delete,
|
|
4090
4519
|
shortcut: "D",
|
|
@@ -4176,6 +4605,34 @@ function ChatList({ sections, activeId, onSelect, rowMenu, className, ...props }
|
|
|
4176
4605
|
}, section.id))
|
|
4177
4606
|
});
|
|
4178
4607
|
}
|
|
4608
|
+
/**
|
|
4609
|
+
* An icon button that opens a `ChatList` in a popover, for `Sidebar`'s `railActions`: the list itself is
|
|
4610
|
+
* hidden on the collapsed rail, so this keeps recent chats reachable. Picking a chat closes the popover.
|
|
4611
|
+
*/
|
|
4612
|
+
function ChatListRailButton({ label, popoverProps, onSelect, className, ...list }) {
|
|
4613
|
+
const icons = useIcons();
|
|
4614
|
+
const labels = useLabels();
|
|
4615
|
+
const name = label ?? labels.chats;
|
|
4616
|
+
return /* @__PURE__ */ jsxs(DialogTrigger, { children: [/* @__PURE__ */ jsx(IconButton, {
|
|
4617
|
+
label: name,
|
|
4618
|
+
children: icons.chatList
|
|
4619
|
+
}), /* @__PURE__ */ jsx(Popover, {
|
|
4620
|
+
placement: "right top",
|
|
4621
|
+
...popoverProps,
|
|
4622
|
+
children: /* @__PURE__ */ jsx(Dialog, {
|
|
4623
|
+
"aria-label": name,
|
|
4624
|
+
className: "max-h-[70dvh] w-64 overflow-y-auto p-2 outline-none [scrollbar-width:thin]",
|
|
4625
|
+
children: ({ close }) => /* @__PURE__ */ jsx(ChatList, {
|
|
4626
|
+
...list,
|
|
4627
|
+
className,
|
|
4628
|
+
onSelect: (id) => {
|
|
4629
|
+
onSelect?.(id);
|
|
4630
|
+
close();
|
|
4631
|
+
}
|
|
4632
|
+
})
|
|
4633
|
+
})
|
|
4634
|
+
})] });
|
|
4635
|
+
}
|
|
4179
4636
|
function ChatRow({ chat, active, nested, onSelect, menu }) {
|
|
4180
4637
|
const icons = useIcons();
|
|
4181
4638
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
@@ -4186,15 +4643,22 @@ function ChatRow({ chat, active, nested, onSelect, menu }) {
|
|
|
4186
4643
|
role: "img",
|
|
4187
4644
|
"aria-label": labels.running,
|
|
4188
4645
|
className: clsx(markerClass, "animate-pulse bg-chat-accent motion-reduce:animate-none")
|
|
4189
|
-
}) : chat.unread ? /* @__PURE__ */ jsx("span", {
|
|
4646
|
+
}) : chat.unread && !chat.muted ? /* @__PURE__ */ jsx("span", {
|
|
4190
4647
|
role: "img",
|
|
4191
4648
|
"aria-label": labels.unread,
|
|
4192
4649
|
className: clsx(markerClass, "bg-chat-check")
|
|
4193
4650
|
}) : null;
|
|
4194
|
-
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4651
|
+
const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
4652
|
+
/* @__PURE__ */ jsx("span", {
|
|
4653
|
+
className: clsx("min-w-0 flex-1 truncate", chat.unread && !active && !chat.muted && "font-medium", chat.muted && "text-chat-muted"),
|
|
4654
|
+
children: chat.title
|
|
4655
|
+
}),
|
|
4656
|
+
chat.detail != null && /* @__PURE__ */ jsx("span", {
|
|
4657
|
+
className: "shrink-0 text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle tabular-nums",
|
|
4658
|
+
children: chat.detail
|
|
4659
|
+
}),
|
|
4660
|
+
marker
|
|
4661
|
+
] });
|
|
4198
4662
|
const common = {
|
|
4199
4663
|
className: clsx(rowClass, nested && "pl-8", active && "bg-chat-selected", menu && (menuOpen || active ? "pr-8" : "group-hover/item:pr-8 group-focus-within/item:pr-8"), menu && "[@media(hover:none)]:pr-10"),
|
|
4200
4664
|
"aria-current": active ? "page" : void 0,
|
|
@@ -4231,7 +4695,7 @@ function ProjectRow({ project, activeId, row }) {
|
|
|
4231
4695
|
const holdsActive = project.chats.some((c) => c.id === activeId);
|
|
4232
4696
|
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsx(Disclosure$1, {
|
|
4233
4697
|
defaultExpanded: project.defaultExpanded ?? holdsActive,
|
|
4234
|
-
children: ({ isExpanded }) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Heading, {
|
|
4698
|
+
children: ({ isExpanded }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Heading, {
|
|
4235
4699
|
className: "m-0 font-normal",
|
|
4236
4700
|
children: /* @__PURE__ */ jsxs(Button$1, {
|
|
4237
4701
|
slot: "trigger",
|
|
@@ -4290,6 +4754,7 @@ function ChatView({ messages, busy = false, onSend, onStop, approvals, onApprove
|
|
|
4290
4754
|
const labels = useLabels();
|
|
4291
4755
|
const pending = onApprove && approvals?.[0];
|
|
4292
4756
|
const box = (variant) => {
|
|
4757
|
+
if (composer === false) return null;
|
|
4293
4758
|
const element = /* @__PURE__ */ jsx(Composer, {
|
|
4294
4759
|
variant: density === "inline" || short && !empty ? "compact" : variant,
|
|
4295
4760
|
busy,
|
|
@@ -4423,41 +4888,84 @@ function ChatTranscript({ messages, renderPart, widgets, header, notice, footer,
|
|
|
4423
4888
|
});
|
|
4424
4889
|
}
|
|
4425
4890
|
// src/views/app-shell.tsx
|
|
4891
|
+
const DEFAULT_WIDTH = 288;
|
|
4892
|
+
const KEY_STEP = 16;
|
|
4893
|
+
const clampWidth = (px, min, max) => Math.min(max, Math.max(min, Math.round(px)));
|
|
4894
|
+
function readStoredWidth(key) {
|
|
4895
|
+
if (!key) return void 0;
|
|
4896
|
+
try {
|
|
4897
|
+
const n = Number(window.localStorage.getItem(key));
|
|
4898
|
+
return n > 0 ? n : void 0;
|
|
4899
|
+
} catch {
|
|
4900
|
+
return;
|
|
4901
|
+
}
|
|
4902
|
+
}
|
|
4426
4903
|
/**
|
|
4427
4904
|
* A page with a sidebar: pinned and collapsible to an icon rail from tablet width up, a drawer over the
|
|
4428
4905
|
* content on phones. Children get `useSidebar()` and `<SidebarToggle>`.
|
|
4429
4906
|
*/
|
|
4430
|
-
function AppShell({ sidebar, children, defaultCollapsed, className }) {
|
|
4907
|
+
function AppShell({ sidebar, children, defaultCollapsed, collapsed: collapsedProp, onCollapsedChange, scroll = "inner", resizable, width: widthProp, defaultWidth, onWidthChange, persistKey, shortcut = true, className }) {
|
|
4431
4908
|
const drawer = useMediaQuery(DRAWER_QUERY);
|
|
4432
|
-
const
|
|
4909
|
+
const labels = useLabels();
|
|
4910
|
+
const [collapsed, setCollapsed] = useControllable(collapsedProp, () => defaultCollapsed ?? (typeof window !== "undefined" && window.matchMedia("(max-width: 1023.98px)").matches), onCollapsedChange);
|
|
4433
4911
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
4434
|
-
const toggle = useCallback(() => drawer ? setDrawerOpen((o) => !o) : setCollapsed((c) => !c), [drawer]);
|
|
4912
|
+
const toggle = useCallback(() => drawer ? setDrawerOpen((o) => !o) : setCollapsed((c) => !c), [drawer, setCollapsed]);
|
|
4435
4913
|
const dismiss = useCallback(() => setDrawerOpen(false), []);
|
|
4914
|
+
const min = resizable?.min ?? 0;
|
|
4915
|
+
const max = resizable?.max ?? Infinity;
|
|
4916
|
+
const [rawWidth, setWidth] = useControllable(widthProp, () => readStoredWidth(persistKey) ?? defaultWidth ?? DEFAULT_WIDTH, (px) => {
|
|
4917
|
+
if (persistKey) try {
|
|
4918
|
+
window.localStorage.setItem(persistKey, String(px));
|
|
4919
|
+
} catch {}
|
|
4920
|
+
onWidthChange?.(px);
|
|
4921
|
+
});
|
|
4922
|
+
const width = resizable ? clampWidth(rawWidth, min, max) : void 0;
|
|
4923
|
+
const shortcutKeys = shortcut ? `${isMacPlatform() ? "⌘" : labels.ctrlKey} B` : void 0;
|
|
4924
|
+
useEffect(() => {
|
|
4925
|
+
if (!shortcut) return;
|
|
4926
|
+
const mac = isMacPlatform();
|
|
4927
|
+
const onKey = (e) => {
|
|
4928
|
+
if (e.defaultPrevented || e.altKey || e.shiftKey || e.key.toLowerCase() !== "b") return;
|
|
4929
|
+
if (mac ? !e.metaKey || e.ctrlKey : !e.ctrlKey || e.metaKey) return;
|
|
4930
|
+
e.preventDefault();
|
|
4931
|
+
toggle();
|
|
4932
|
+
};
|
|
4933
|
+
window.addEventListener("keydown", onKey);
|
|
4934
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
4935
|
+
}, [shortcut, toggle]);
|
|
4436
4936
|
const pinned = useMemo(() => ({
|
|
4437
4937
|
collapsed,
|
|
4438
4938
|
drawer,
|
|
4439
4939
|
drawerOpen,
|
|
4440
4940
|
toggle,
|
|
4441
|
-
dismiss
|
|
4941
|
+
dismiss,
|
|
4942
|
+
...shortcutKeys && { shortcut: shortcutKeys }
|
|
4442
4943
|
}), [
|
|
4443
4944
|
collapsed,
|
|
4444
4945
|
drawer,
|
|
4445
4946
|
drawerOpen,
|
|
4446
4947
|
toggle,
|
|
4447
|
-
dismiss
|
|
4948
|
+
dismiss,
|
|
4949
|
+
shortcutKeys
|
|
4448
4950
|
]);
|
|
4449
|
-
const labels = useLabels();
|
|
4450
4951
|
const inDrawer = useMemo(() => ({
|
|
4451
4952
|
...pinned,
|
|
4452
4953
|
collapsed: false
|
|
4453
4954
|
}), [pinned]);
|
|
4955
|
+
const windowScroll = scroll === "window";
|
|
4454
4956
|
return /* @__PURE__ */ jsxs(SidebarContext.Provider, {
|
|
4455
4957
|
value: pinned,
|
|
4456
4958
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
4457
|
-
className: clsx("flex
|
|
4458
|
-
children: [!drawer && /* @__PURE__ */
|
|
4459
|
-
|
|
4460
|
-
|
|
4959
|
+
className: clsx("flex bg-chat-bg text-chat-fg", windowScroll ? "min-h-dvh" : "h-dvh overflow-hidden", className),
|
|
4960
|
+
children: [!drawer && /* @__PURE__ */ jsxs("aside", {
|
|
4961
|
+
style: width ? { "--chat-sidebar-width": `${width}px` } : void 0,
|
|
4962
|
+
className: clsx("relative shrink-0 border-r border-chat-divider bg-chat-sidebar transition-[width] duration-200 ease-out motion-reduce:transition-none data-resizing:transition-none", windowScroll ? "sticky top-0 h-dvh self-start overflow-clip" : "overflow-hidden", collapsed ? "w-13" : "w-(--chat-sidebar-width,18rem)"),
|
|
4963
|
+
children: [sidebar, resizable && !collapsed && width && /* @__PURE__ */ jsx(ResizeHandle, {
|
|
4964
|
+
width,
|
|
4965
|
+
min,
|
|
4966
|
+
max,
|
|
4967
|
+
onCommit: setWidth
|
|
4968
|
+
})]
|
|
4461
4969
|
}), /* @__PURE__ */ jsx("main", {
|
|
4462
4970
|
className: "flex min-w-0 flex-1 flex-col",
|
|
4463
4971
|
children
|
|
@@ -4473,15 +4981,58 @@ function AppShell({ sidebar, children, defaultCollapsed, className }) {
|
|
|
4473
4981
|
})]
|
|
4474
4982
|
});
|
|
4475
4983
|
}
|
|
4476
|
-
/**
|
|
4984
|
+
/** The draggable right edge of the sidebar. A drag moves the aside's width variable alone and commits on release. */
|
|
4985
|
+
function ResizeHandle({ width, min, max, onCommit }) {
|
|
4986
|
+
const labels = useLabels();
|
|
4987
|
+
const live = useRef(null);
|
|
4988
|
+
const aside = (el) => el.closest("aside");
|
|
4989
|
+
const move = (el, px) => {
|
|
4990
|
+
live.current = clampWidth(px, min, max);
|
|
4991
|
+
aside(el).style.setProperty("--chat-sidebar-width", `${live.current}px`);
|
|
4992
|
+
};
|
|
4993
|
+
const end = (el) => {
|
|
4994
|
+
delete aside(el).dataset.resizing;
|
|
4995
|
+
if (live.current != null) onCommit(live.current);
|
|
4996
|
+
live.current = null;
|
|
4997
|
+
};
|
|
4998
|
+
return /* @__PURE__ */ jsx("div", {
|
|
4999
|
+
role: "separator",
|
|
5000
|
+
"aria-orientation": "vertical",
|
|
5001
|
+
"aria-label": labels.resizeSidebar,
|
|
5002
|
+
"aria-valuenow": width,
|
|
5003
|
+
"aria-valuemin": min,
|
|
5004
|
+
"aria-valuemax": max,
|
|
5005
|
+
tabIndex: 0,
|
|
5006
|
+
className: "group/handle absolute inset-y-0 right-0 z-10 w-1.5 cursor-col-resize touch-none outline-none",
|
|
5007
|
+
onPointerDown: (e) => {
|
|
5008
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
5009
|
+
aside(e.currentTarget).dataset.resizing = "";
|
|
5010
|
+
live.current = null;
|
|
5011
|
+
},
|
|
5012
|
+
onPointerMove: (e) => {
|
|
5013
|
+
if (e.currentTarget.hasPointerCapture(e.pointerId)) move(e.currentTarget, e.clientX - aside(e.currentTarget).getBoundingClientRect().left);
|
|
5014
|
+
},
|
|
5015
|
+
onPointerUp: (e) => end(e.currentTarget),
|
|
5016
|
+
onPointerCancel: (e) => end(e.currentTarget),
|
|
5017
|
+
onKeyDown: (e) => {
|
|
5018
|
+
const step = e.shiftKey ? 64 : KEY_STEP;
|
|
5019
|
+
const next = e.key === "ArrowLeft" ? width - step : e.key === "ArrowRight" ? width + step : e.key === "Home" ? min : e.key === "End" ? max : null;
|
|
5020
|
+
if (next == null) return;
|
|
5021
|
+
e.preventDefault();
|
|
5022
|
+
onCommit(clampWidth(next, min, max));
|
|
5023
|
+
},
|
|
5024
|
+
children: /* @__PURE__ */ jsx("span", { className: "absolute inset-y-0 right-0 w-px bg-transparent transition-colors group-hover/handle:bg-chat-border-strong group-focus-visible/handle:w-0.5 group-focus-visible/handle:bg-chat-focus in-data-resizing:bg-chat-focus" })
|
|
5025
|
+
});
|
|
5026
|
+
}
|
|
5027
|
+
/** Opens the drawer on phones, collapses / expands the pinned sidebar elsewhere. Its label names the shortcut, e.g. "(⌘ B)". */
|
|
4477
5028
|
function SidebarToggle({ drawerOnly = false, className }) {
|
|
4478
5029
|
const icons = useIcons();
|
|
4479
|
-
const { drawer, drawerOpen, collapsed, toggle } = useSidebar();
|
|
5030
|
+
const { drawer, drawerOpen, collapsed, toggle, shortcut } = useSidebar();
|
|
4480
5031
|
const labels = useLabels();
|
|
4481
5032
|
if (drawerOnly && !drawer) return null;
|
|
4482
5033
|
const label = drawer ? drawerOpen ? labels.closeSidebar : labels.openSidebar : collapsed ? labels.expandSidebar : labels.collapseSidebar;
|
|
4483
5034
|
return /* @__PURE__ */ jsx(IconButton, {
|
|
4484
|
-
label,
|
|
5035
|
+
label: shortcut ? labels.withShortcut(label, shortcut) : label,
|
|
4485
5036
|
onPress: toggle,
|
|
4486
5037
|
className,
|
|
4487
5038
|
children: icons.sidebar
|
|
@@ -4489,7 +5040,7 @@ function SidebarToggle({ drawerOnly = false, className }) {
|
|
|
4489
5040
|
}
|
|
4490
5041
|
// src/views/sidebar.tsx
|
|
4491
5042
|
/** The claude.ai sidebar frame. Collapses to an icon rail inside `AppShell`; a drawer on phones. */
|
|
4492
|
-
function Sidebar({ brand, brandIcon, headerActions, nav, children, footer, className }) {
|
|
5043
|
+
function Sidebar({ brand, brandIcon, headerActions, railActions, nav, children, footer, className }) {
|
|
4493
5044
|
const { collapsed } = useSidebar();
|
|
4494
5045
|
return /* @__PURE__ */ jsxs("div", {
|
|
4495
5046
|
className: clsx("flex h-full flex-col font-chat-sans", collapsed ? "w-13" : "w-full", className),
|
|
@@ -4515,8 +5066,12 @@ function Sidebar({ brand, brandIcon, headerActions, nav, children, footer, class
|
|
|
4515
5066
|
/* @__PURE__ */ jsx(SidebarToggle, {})
|
|
4516
5067
|
]
|
|
4517
5068
|
}),
|
|
4518
|
-
|
|
4519
|
-
className:
|
|
5069
|
+
collapsed && railActions && /* @__PURE__ */ jsx("div", {
|
|
5070
|
+
className: "flex shrink-0 flex-col items-center gap-1 pb-2",
|
|
5071
|
+
children: railActions
|
|
5072
|
+
}),
|
|
5073
|
+
nav && /* @__PURE__ */ jsx(SidebarList, {
|
|
5074
|
+
className: clsx("shrink-0 px-2 pb-2", collapsed && "items-center"),
|
|
4520
5075
|
children: nav
|
|
4521
5076
|
}),
|
|
4522
5077
|
!collapsed && /* @__PURE__ */ jsx("div", {
|
|
@@ -4531,32 +5086,65 @@ function Sidebar({ brand, brandIcon, headerActions, nav, children, footer, class
|
|
|
4531
5086
|
]
|
|
4532
5087
|
});
|
|
4533
5088
|
}
|
|
4534
|
-
/**
|
|
4535
|
-
function
|
|
5089
|
+
/** The `<ul>` for `SidebarItem`s outside `Sidebar`'s `nav` slot (e.g. inside a section): no bullets, no padding. */
|
|
5090
|
+
function SidebarList({ children, className }) {
|
|
5091
|
+
return /* @__PURE__ */ jsx("ul", {
|
|
5092
|
+
className: clsx("m-0 flex list-none flex-col gap-px p-0", className),
|
|
5093
|
+
children
|
|
5094
|
+
});
|
|
5095
|
+
}
|
|
5096
|
+
const iconSize = "[&_svg]:size-(--chat-sidebar-icon-size,1rem) [&_svg]:shrink-0";
|
|
5097
|
+
/**
|
|
5098
|
+
* A nav row: icon and label; just the icon with a tooltip when collapsed. Closes the drawer when picked.
|
|
5099
|
+
* Icons are 1rem, or `--chat-sidebar-icon-size`.
|
|
5100
|
+
*/
|
|
5101
|
+
function SidebarItem({ icon, label, onPress, href, active, shortcut, badge, description, trailing, title, className: extra, ...rest }) {
|
|
4536
5102
|
const { collapsed, dismiss } = useSidebar();
|
|
4537
|
-
const
|
|
5103
|
+
const data = Object.fromEntries(Object.entries(rest).filter(([k]) => k.startsWith("data-")));
|
|
5104
|
+
const hasTrailing = !collapsed && trailing != null;
|
|
5105
|
+
const className = clsx("group flex min-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)]:min-h-10", "data-hovered:bg-chat-hover data-pressed:bg-chat-selected data-focus-visible:ring-2 data-focus-visible:ring-chat-focus", iconSize, collapsed ? "relative w-9 justify-center [@media(pointer:coarse)]:w-10" : "w-full px-2", description && !collapsed && "py-1", hasTrailing && "group-hover/item:pr-9 group-focus-within/item:pr-9 [@media(hover:none)]:pr-10", active && "bg-chat-selected");
|
|
4538
5106
|
const press = () => {
|
|
4539
5107
|
onPress?.();
|
|
4540
5108
|
dismiss();
|
|
4541
5109
|
};
|
|
4542
|
-
const
|
|
5110
|
+
const showIfCut = (e) => {
|
|
5111
|
+
const el = e.currentTarget;
|
|
5112
|
+
el.title = el.scrollWidth > el.clientWidth ? label : "";
|
|
5113
|
+
};
|
|
5114
|
+
const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
4543
5115
|
icon,
|
|
4544
|
-
collapsed ? /* @__PURE__ */
|
|
5116
|
+
collapsed ? /* @__PURE__ */ jsxs("span", {
|
|
4545
5117
|
className: "sr-only",
|
|
4546
|
-
children: label
|
|
4547
|
-
}) : /* @__PURE__ */
|
|
4548
|
-
className: "min-w-0 flex-1
|
|
4549
|
-
children:
|
|
5118
|
+
children: [label, (typeof badge === "string" || typeof badge === "number") && ` (${badge})`]
|
|
5119
|
+
}) : /* @__PURE__ */ jsxs("span", {
|
|
5120
|
+
className: "flex min-w-0 flex-1 flex-col",
|
|
5121
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
5122
|
+
className: "truncate",
|
|
5123
|
+
onPointerEnter: title ? void 0 : showIfCut,
|
|
5124
|
+
children: label
|
|
5125
|
+
}), description && /* @__PURE__ */ jsx("span", {
|
|
5126
|
+
className: "truncate text-(length:--chat-ui-font-size-sm) leading-4 text-chat-muted",
|
|
5127
|
+
children: description
|
|
5128
|
+
})]
|
|
4550
5129
|
}),
|
|
4551
5130
|
!collapsed && shortcut && /* @__PURE__ */ jsx("kbd", {
|
|
4552
5131
|
className: "font-chat-sans text-(length:--chat-ui-font-size-sm) leading-4 text-chat-subtle opacity-0 group-data-hovered:opacity-100",
|
|
4553
5132
|
children: shortcut
|
|
5133
|
+
}),
|
|
5134
|
+
!collapsed && badge != null && /* @__PURE__ */ jsx("span", {
|
|
5135
|
+
className: "shrink-0 text-(length:--chat-ui-font-size-sm) leading-4 text-chat-muted tabular-nums",
|
|
5136
|
+
children: badge
|
|
5137
|
+
}),
|
|
5138
|
+
collapsed && badge != null && /* @__PURE__ */ jsx("span", {
|
|
5139
|
+
"aria-hidden": true,
|
|
5140
|
+
className: "absolute top-1 right-1 size-2 rounded-full bg-chat-accent"
|
|
4554
5141
|
})
|
|
4555
5142
|
] });
|
|
4556
5143
|
const common = {
|
|
4557
5144
|
className,
|
|
4558
5145
|
onPress: press,
|
|
4559
|
-
"aria-current": active ? "page" : void 0
|
|
5146
|
+
"aria-current": active ? "page" : void 0,
|
|
5147
|
+
...title && { title }
|
|
4560
5148
|
};
|
|
4561
5149
|
const control = href ? /* @__PURE__ */ jsx(Link, {
|
|
4562
5150
|
...common,
|
|
@@ -4566,15 +5154,22 @@ function SidebarItem({ icon, label, onPress, href, active, shortcut }) {
|
|
|
4566
5154
|
...common,
|
|
4567
5155
|
children: content
|
|
4568
5156
|
});
|
|
4569
|
-
return /* @__PURE__ */
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
children:
|
|
4573
|
-
|
|
5157
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
5158
|
+
...data,
|
|
5159
|
+
className: clsx("group/item relative list-none", extra),
|
|
5160
|
+
children: [collapsed ? /* @__PURE__ */ jsx(Tooltip, {
|
|
5161
|
+
content: label,
|
|
5162
|
+
placement: "right",
|
|
5163
|
+
children: control
|
|
5164
|
+
}) : control, hasTrailing && /* @__PURE__ */ jsx("div", {
|
|
5165
|
+
className: "absolute top-1/2 right-1 flex -translate-y-1/2 items-center opacity-0 group-hover/item:opacity-100 group-focus-within/item:opacity-100 [@media(hover:none)]:opacity-100",
|
|
5166
|
+
children: trailing
|
|
5167
|
+
})]
|
|
5168
|
+
});
|
|
4574
5169
|
}
|
|
4575
5170
|
const initials = (name) => name.split(/\s+/).slice(0, 2).map((w) => w[0]?.toUpperCase() ?? "").join("");
|
|
4576
5171
|
/** The account row at the bottom: avatar, name, plan, and a menu. */
|
|
4577
|
-
function SidebarUser({ name, detail, avatarUrl, menu }) {
|
|
5172
|
+
function SidebarUser({ name, detail, avatarUrl, menu, popoverProps }) {
|
|
4578
5173
|
const icons = useIcons();
|
|
4579
5174
|
const { collapsed } = useSidebar();
|
|
4580
5175
|
const avatar = avatarUrl ? /* @__PURE__ */ jsx("img", {
|
|
@@ -4589,7 +5184,7 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
|
|
|
4589
5184
|
const button = /* @__PURE__ */ jsxs(Button$1, {
|
|
4590
5185
|
"aria-label": collapsed ? name : void 0,
|
|
4591
5186
|
className: clsx("flex h-12 w-full cursor-pointer items-center gap-2.5 rounded-chat-md text-left outline-none", "data-hovered:bg-chat-hover data-pressed:bg-chat-selected data-focus-visible:ring-2 data-focus-visible:ring-chat-focus", collapsed ? "justify-center" : "px-1.5"),
|
|
4592
|
-
children: [avatar, !collapsed && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
|
|
5187
|
+
children: [avatar, !collapsed && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("span", {
|
|
4593
5188
|
className: "flex min-w-0 flex-1 flex-col",
|
|
4594
5189
|
children: [/* @__PURE__ */ jsx("span", {
|
|
4595
5190
|
className: "truncate text-(length:--chat-ui-font-size) leading-5 text-chat-fg",
|
|
@@ -4606,7 +5201,8 @@ function SidebarUser({ name, detail, avatarUrl, menu }) {
|
|
|
4606
5201
|
if (!menu) return button;
|
|
4607
5202
|
return /* @__PURE__ */ jsxs(MenuTrigger, { children: [button, /* @__PURE__ */ jsx(Popover, {
|
|
4608
5203
|
placement: "top start",
|
|
5204
|
+
...popoverProps,
|
|
4609
5205
|
children: menu
|
|
4610
5206
|
})] });
|
|
4611
5207
|
}
|
|
4612
|
-
export { ActivityGroup, AgentProvider, AgentRegistry, AppShell, ApprovalCard, AssistantMessage, AttachButton, AttachmentCard, AttachmentRow, Badge, Button, Caret, ChatHeader, ChatList, ChatNotice, ChatTheme, ChatTitleMenu, ChatTranscript, ChatView, CodeBlock, Composer, ContextMenu, ContextPanel, CopyButton, DRAWER_QUERY, DictationButton, DictationControls, Disclosure, Drawer, EffortButton, EffortSlider, EmptyState, IconButton, Markdown, MarkdownLinksProvider, Menu, MenuItem, MenuNote, MenuSearch, MenuSection, MenuSeparator, MenuTrigger, MessageList, Modal, ModelPicker, Panel, PastedTextDialog, Popover, SegmentedControl, Sidebar, SidebarItem, SidebarToggle, SidebarUser, SubmenuTrigger, TokenCounter, ToolViewsProvider, Tooltip, UiComponentsProvider, UiMarkupBlock, UiTree, UserMessage, VoiceMeter, WIDGET_MAX_HEIGHT, WidgetFrame, WidgetHostProvider, WidgetPartView, activityHeading, activityLabel, commandTool, defaultIcons, defaultLabels, defaultToolViews, encodeWav, formatDuration, formatRelativeTime, formatTokens, htmlComponent, intersectAllow, memoryTool, messageText, openInNewTab, readTreeInput, readVisualizeInput, sseTransport, uiMarkupToolView, uiTreeToolView, useAgentContext, useAgentRegistry, useAgentSnapshot, useAgentTool, useAttachments, useChat, useChatTool, useCountUp, useDictation, useIcons, useLabels, useMediaQuery, useSidebar, useSmoothText, useToolView, useToolViews, useUiAction, useUiMarkupLanguage, useUiState, useWidgetHost, userMessage, webFetchTool, webSearchTool, widgetCsp, widgetToolView };
|
|
5208
|
+
export { ActivityGroup, AgentProvider, AgentRegistry, AppShell, ApprovalCard, AssistantMessage, AttachButton, AttachmentCard, AttachmentRow, Badge, Button, Caret, ChatHeader, ChatList, ChatListRailButton, ChatNotice, ChatTheme, ChatTitleMenu, ChatTranscript, ChatView, CodeBlock, Composer, ContextMenu, ContextMeter, ContextPanel, CopyButton, CopyField, DRAWER_QUERY, DialogTrigger, DictationButton, DictationControls, Disclosure, Drawer, EffortButton, EffortSlider, EmptyState, IconButton, Markdown, MarkdownLinksProvider, Menu, MenuItem, MenuNote, MenuSearch, MenuSection, MenuSeparator, MenuTrigger, MessageList, Modal, ModelPicker, Panel, PastedTextDialog, Popover, PopoverTrigger, SegmentedControl, Sidebar, SidebarItem, SidebarList, SidebarToggle, SidebarUser, SubmenuTrigger, TokenCounter, ToolViewsProvider, Tooltip, UiComponentsProvider, UiMarkupBlock, UiTree, UserMessage, VoiceMeter, WIDGET_MAX_HEIGHT, WidgetFrame, WidgetHostProvider, WidgetPartView, activityHeading, activityLabel, commandTool, defaultIcons, defaultLabels, defaultToolViews, encodeWav, formatDuration, formatRelativeTime, formatTokens, htmlComponent, intersectAllow, memoryTool, messageText, openInNewTab, readTreeInput, readVisualizeInput, sseTransport, uiMarkupToolView, uiTreeToolView, useAgentContext, useAgentRegistry, useAgentSnapshot, useAgentTool, useAttachments, useChat, useChatTool, useCountUp, useDictation, useIcons, useLabels, useMediaQuery, useSidebar, useSmoothText, useToolView, useToolViews, useUiAction, useUiMarkupLanguage, useUiState, useWidgetHost, userMessage, webFetchTool, webSearchTool, widgetCsp, widgetToolView };
|