cortena-ui 1.6.0 → 1.7.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/CHANGELOG.md +76 -0
- package/README.md +36 -0
- package/dist/agent-chat/session.js +21 -7
- package/dist/agent-chat/session.js.map +1 -1
- package/dist/agent-chat/store.d.ts +55 -4
- package/dist/agent-chat/store.js +87 -22
- package/dist/agent-chat/store.js.map +1 -1
- package/dist/agent-chat/types.d.ts +11 -0
- package/dist/agent-chat/types.js.map +1 -1
- package/dist/agent-chat.d.ts +3 -3
- package/dist/components/agent-chat-popup.d.ts +2 -2
- package/dist/components/agent-chat-popup.js.map +1 -1
- package/dist/components/agent-chat.d.ts +169 -13
- package/dist/components/agent-chat.js +200 -27
- package/dist/components/agent-chat.js.map +1 -1
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/toast.d.ts +1 -1
- package/package.json +1 -1
- package/src/agent-chat/session.ts +35 -5
- package/src/agent-chat/store.ts +193 -21
- package/src/agent-chat/types.ts +11 -0
- package/src/components/agent-chat-popup.tsx +3 -3
- package/src/components/agent-chat.tsx +576 -149
- package/src/entries/agent-chat.ts +12 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { A2UIAction } from "../a2ui/message.js";
|
|
3
3
|
import { A2UIChatBlock } from "../agent-chat/a2ui-block.js";
|
|
4
|
-
import { AgentApprovalRequest, AgentChatClient, AgentChatMessage, AgentChatStep, AgentSession } from "../agent-chat/types.js";
|
|
5
|
-
import { AgentToolEntry } from "../agent-chat/store.js";
|
|
4
|
+
import { AgentApprovalDecision, AgentApprovalRequest, AgentChatClient, AgentChatMessage, AgentChatStep, AgentSession } from "../agent-chat/types.js";
|
|
5
|
+
import { AgentToolEntry, UseAgentChatResult } from "../agent-chat/store.js";
|
|
6
6
|
import * as React from "react";
|
|
7
7
|
//#region src/components/agent-chat.d.ts
|
|
8
8
|
/**
|
|
@@ -118,19 +118,118 @@ export interface AgentToolRenderContext {
|
|
|
118
118
|
/** The `ui://` resource this result carries, when it carries one. */
|
|
119
119
|
mcpAppUri: string | null;
|
|
120
120
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
/**
|
|
122
|
+
* What a host's own message bubble receives. `index` and `messages` are over
|
|
123
|
+
* the VISIBLE messages — the filter chain has already run — so
|
|
124
|
+
* `messages[index - 1]` is the turn before this one, which a feedback control
|
|
125
|
+
* or a transcript export wants.
|
|
126
|
+
*/
|
|
127
|
+
export interface AgentChatMessageSlotProps {
|
|
128
|
+
message: AgentChatMessage;
|
|
129
|
+
index: number;
|
|
130
|
+
messages: readonly AgentChatMessage[];
|
|
131
|
+
agentName: string;
|
|
132
|
+
sessionKey: string | null;
|
|
133
|
+
/** True while a run is live; every A2UI surface goes read-only. */
|
|
134
|
+
streaming: boolean;
|
|
135
|
+
onAction: (action: A2UIAction) => void;
|
|
136
|
+
}
|
|
137
|
+
export interface AgentChatStreamingSlotProps {
|
|
138
|
+
agentName: string;
|
|
139
|
+
text: string;
|
|
140
|
+
thinkingText: string;
|
|
141
|
+
a2ui: readonly A2UIChatBlock[];
|
|
142
|
+
/** The turn's tool calls in plain words; the default draws them under the name. */
|
|
143
|
+
steps: readonly AgentChatStep[];
|
|
144
|
+
}
|
|
145
|
+
export interface AgentChatEmptySlotProps {
|
|
146
|
+
agentName: string;
|
|
147
|
+
/** A history load is in flight for this session; a skeleton fits here. */
|
|
148
|
+
loadingHistory: boolean;
|
|
149
|
+
}
|
|
150
|
+
export interface AgentChatLoadOlderSlotProps {
|
|
151
|
+
hasMore: boolean;
|
|
152
|
+
loading: boolean;
|
|
153
|
+
onLoad: () => void;
|
|
154
|
+
}
|
|
155
|
+
export interface AgentChatToolsSlotProps {
|
|
156
|
+
entries: readonly AgentToolEntry[];
|
|
157
|
+
approvals: readonly AgentApprovalRequest[];
|
|
158
|
+
streaming: boolean;
|
|
159
|
+
/**
|
|
160
|
+
* The host's `renderToolCall`, as it was passed: `undefined` for a call the
|
|
161
|
+
* host declines, so the slot can draw its own line for those and still mount
|
|
162
|
+
* an MCP App through the one seam the component defines.
|
|
163
|
+
*/
|
|
164
|
+
renderToolCall: ((context: AgentToolRenderContext) => React.ReactNode | undefined) | undefined;
|
|
165
|
+
onResolveApproval: (id: string, decision: AgentApprovalDecision) => void;
|
|
166
|
+
}
|
|
167
|
+
export interface AgentChatComposerSlotProps {
|
|
125
168
|
agentName: string;
|
|
169
|
+
streaming: boolean;
|
|
170
|
+
send: UseAgentChatResult["send"];
|
|
171
|
+
abort: () => void;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Renders a host may substitute for the component's own, one region each.
|
|
175
|
+
*
|
|
176
|
+
* The surface — its state, the order of its regions, the scroller that stays
|
|
177
|
+
* pinned to the bottom, the sentinel that pages history, the steps and the
|
|
178
|
+
* tool-card seam — is shared; what each region LOOKS like need not be. This is
|
|
179
|
+
* how cortenaweb draws its own bubble (Shiki, KaTeX, feedback controls, a
|
|
180
|
+
* timestamp chip) and its own composer (attachments, agent and model
|
|
181
|
+
* selectors) on the shared surface instead of keeping a second chat
|
|
182
|
+
* (PLATFORM-64).
|
|
183
|
+
*
|
|
184
|
+
* A component given here replaces its region whole, wrapper included; nothing
|
|
185
|
+
* of the default is drawn around it.
|
|
186
|
+
*/
|
|
187
|
+
export interface AgentChatComponents {
|
|
188
|
+
Message: React.ComponentType<AgentChatMessageSlotProps>;
|
|
126
189
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
190
|
+
* The live bubble. It is handed `steps` because the default draws the
|
|
191
|
+
* progress steps INSIDE the streaming bubble; a host's own bubble draws them
|
|
192
|
+
* itself, or not at all — nothing else draws them while a run is live.
|
|
129
193
|
*/
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
|
|
133
|
-
|
|
194
|
+
Streaming: React.ComponentType<AgentChatStreamingSlotProps>;
|
|
195
|
+
/**
|
|
196
|
+
* Replaces the whole transcript region while there is nothing to show. Not
|
|
197
|
+
* while there is an error: the transcript then mounts with the error banner
|
|
198
|
+
* in it, so a failure before the first message is not hidden behind the
|
|
199
|
+
* host's welcome screen.
|
|
200
|
+
*/
|
|
201
|
+
Empty: React.ComponentType<AgentChatEmptySlotProps>;
|
|
202
|
+
/** Drawn at the top of the transcript while there is, or may be, more. */
|
|
203
|
+
LoadOlder: React.ComponentType<AgentChatLoadOlderSlotProps>;
|
|
204
|
+
/** Replaces the tool strip and the approval cards at the foot. */
|
|
205
|
+
Tools: React.ComponentType<AgentChatToolsSlotProps>;
|
|
206
|
+
Composer: React.ComponentType<AgentChatComposerSlotProps>;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Class names for the wrappers a host cannot otherwise reach.
|
|
210
|
+
*
|
|
211
|
+
* `transcript` REPLACES the default spacing (`gap-5 px-4 py-4`) rather than
|
|
212
|
+
* merging with it: a reading column sets its own gutter through a class, and a
|
|
213
|
+
* utility left on the element would fight it. The structural classes — flex,
|
|
214
|
+
* min-width — stay in either case.
|
|
215
|
+
*/
|
|
216
|
+
export interface AgentChatClassNames {
|
|
217
|
+
header?: string;
|
|
218
|
+
scroll?: string;
|
|
219
|
+
transcript?: string;
|
|
220
|
+
/**
|
|
221
|
+
* The history sentinel at the top of the transcript. Out of the flow by
|
|
222
|
+
* default (`absolute inset-x-0 top-0 h-px`), so it adds no space; a host
|
|
223
|
+
* whose column already reserved a row for it passes an in-flow class.
|
|
224
|
+
*/
|
|
225
|
+
sentinel?: string;
|
|
226
|
+
tools?: string;
|
|
227
|
+
composer?: string;
|
|
228
|
+
}
|
|
229
|
+
/** What every `AgentChat` takes, however its state is owned. */
|
|
230
|
+
export interface AgentChatBaseProps extends Omit<React.ComponentProps<"div">, "children" | "onError"> {
|
|
231
|
+
/** The extension name, shown on each assistant turn. */
|
|
232
|
+
agentName: string;
|
|
134
233
|
/** Rendered between the tool strip and the composer — an attachment picker. */
|
|
135
234
|
attachmentsSlot?: React.ReactNode;
|
|
136
235
|
/**
|
|
@@ -141,8 +240,65 @@ export interface AgentChatProps extends Omit<React.ComponentProps<"div">, "child
|
|
|
141
240
|
renderToolCall?: (context: AgentToolRenderContext) => React.ReactNode | undefined;
|
|
142
241
|
/** Header content, e.g. the pop-up's own window controls. */
|
|
143
242
|
headerSlot?: React.ReactNode;
|
|
243
|
+
/** No header at all — for a host with its own session list and New button. */
|
|
244
|
+
hideHeader?: boolean;
|
|
245
|
+
/** The host's own renders for each region. See `AgentChatComponents`. */
|
|
246
|
+
components?: Partial<AgentChatComponents>;
|
|
247
|
+
/** Classes for the wrappers. See `AgentChatClassNames`. */
|
|
248
|
+
classNames?: AgentChatClassNames;
|
|
249
|
+
/**
|
|
250
|
+
* Page older history from the sentinel at the top of the transcript as the
|
|
251
|
+
* reader reaches it, not only from the `LoadOlder` button. Off by default —
|
|
252
|
+
* the pop-up pages on a click. When on, it is armed only once the scroller
|
|
253
|
+
* overflows: a transcript shorter than its viewport has its sentinel
|
|
254
|
+
* permanently in view, and paging on that pulled every page the server had
|
|
255
|
+
* into a chat with three messages in it.
|
|
256
|
+
*/
|
|
257
|
+
autoLoadOlder?: boolean;
|
|
144
258
|
}
|
|
145
|
-
|
|
259
|
+
/**
|
|
260
|
+
* The surface owning its own state: it calls `useAgentChat` on the client it is
|
|
261
|
+
* given. This is what `AgentChatPopup` mounts.
|
|
262
|
+
*/
|
|
263
|
+
export interface AgentChatOwnedProps extends AgentChatBaseProps {
|
|
264
|
+
/** The transport. `createAguiAgentChatClient` is the AG-UI implementation. */
|
|
265
|
+
client: AgentChatClient;
|
|
266
|
+
/**
|
|
267
|
+
* Names the `sessionStorage` slot this window's session key lives in. Scope
|
|
268
|
+
* it to the extension id; two chats with different scopes never share a key.
|
|
269
|
+
*/
|
|
270
|
+
storageScope: string;
|
|
271
|
+
/** Do not read or write `sessionStorage`; for a guide or a test. */
|
|
272
|
+
ephemeral?: boolean;
|
|
273
|
+
thinking?: "low" | "medium" | "high";
|
|
274
|
+
chat?: undefined;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* The surface drawing state the host owns.
|
|
278
|
+
*
|
|
279
|
+
* A host that needs the state OUTSIDE the surface — a sidebar that lists
|
|
280
|
+
* sessions, a shortcut that stops the run, a page that routes on the session
|
|
281
|
+
* key — holds it itself and passes a `UseAgentChatResult` here; the surface
|
|
282
|
+
* then draws that state instead of holding its own. The result may come from
|
|
283
|
+
* `useAgentChat`, or from a store of the host's own projected onto the same
|
|
284
|
+
* shape, which is how cortenaweb drives this surface from the zustand stores
|
|
285
|
+
* the rest of its shell reads (PLATFORM-64). No client, no storage: the host
|
|
286
|
+
* owns both.
|
|
287
|
+
*/
|
|
288
|
+
export interface AgentChatHostedProps extends AgentChatBaseProps {
|
|
289
|
+
chat: UseAgentChatResult;
|
|
290
|
+
client?: undefined;
|
|
291
|
+
storageScope?: undefined;
|
|
292
|
+
ephemeral?: undefined;
|
|
293
|
+
thinking?: undefined;
|
|
294
|
+
}
|
|
295
|
+
export type AgentChatProps = AgentChatOwnedProps | AgentChatHostedProps;
|
|
296
|
+
/**
|
|
297
|
+
* Two ways in, one surface. The hook is called in a component of its own so
|
|
298
|
+
* a host that hands over `chat` never calls it — and so a mount that switches
|
|
299
|
+
* from one mode to the other remounts rather than changing its hook order.
|
|
300
|
+
*/
|
|
301
|
+
declare function AgentChat(props: AgentChatProps): React.JSX.Element;
|
|
146
302
|
export interface AgentSessionDrawerProps {
|
|
147
303
|
sessions: readonly AgentSession[];
|
|
148
304
|
current: string | null;
|
|
@@ -37,6 +37,13 @@ import * as React from "react";
|
|
|
37
37
|
* and it replaces the card, return nothing and the default card is drawn.
|
|
38
38
|
*/
|
|
39
39
|
/**
|
|
40
|
+
* `useLayoutEffect` in a browser, `useEffect` on a server render, where React
|
|
41
|
+
* warns about the former. The scroll restore after a prepend has to run
|
|
42
|
+
* before the paint: an effect after it shows one frame at the old `scrollTop`,
|
|
43
|
+
* with the page that just landed pushing the reader's place off the screen.
|
|
44
|
+
*/
|
|
45
|
+
const useLayoutEffectSafe = typeof window === "undefined" ? React.useEffect : React.useLayoutEffect;
|
|
46
|
+
/**
|
|
40
47
|
* Should the list jump back to the bottom after its content grew?
|
|
41
48
|
*
|
|
42
49
|
* Judged against the height BEFORE the growth. Measuring afterwards asks "is
|
|
@@ -420,18 +427,68 @@ function AgentApprovalCard({ request, onResolve }) {
|
|
|
420
427
|
]
|
|
421
428
|
});
|
|
422
429
|
}
|
|
423
|
-
|
|
430
|
+
/**
|
|
431
|
+
* Two ways in, one surface. The hook is called in a component of its own so
|
|
432
|
+
* a host that hands over `chat` never calls it — and so a mount that switches
|
|
433
|
+
* from one mode to the other remounts rather than changing its hook order.
|
|
434
|
+
*/
|
|
435
|
+
function AgentChat(props) {
|
|
436
|
+
if (props.chat) {
|
|
437
|
+
const { chat, client: _client, storageScope: _scope, ephemeral: _e, thinking: _t, ...rest } = props;
|
|
438
|
+
return /* @__PURE__ */ jsx(AgentChatSurface, {
|
|
439
|
+
...rest,
|
|
440
|
+
chat
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
return /* @__PURE__ */ jsx(AgentChatOwned, { ...props });
|
|
444
|
+
}
|
|
445
|
+
function AgentChatOwned({ client, storageScope, ephemeral, thinking, chat: _chat, ...rest }) {
|
|
424
446
|
const chat = useAgentChat({
|
|
425
447
|
client,
|
|
426
448
|
storageScope,
|
|
427
449
|
...ephemeral === void 0 ? {} : { ephemeral },
|
|
428
450
|
...thinking === void 0 ? {} : { thinking }
|
|
429
451
|
});
|
|
452
|
+
return /* @__PURE__ */ jsx(AgentChatSurface, {
|
|
453
|
+
...rest,
|
|
454
|
+
chat
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
/** The default at the top of the transcript: a button, when there is more. */
|
|
458
|
+
function DefaultLoadOlder({ hasMore, loading, onLoad }) {
|
|
459
|
+
if (!hasMore) return null;
|
|
460
|
+
return /* @__PURE__ */ jsx("div", {
|
|
461
|
+
className: "flex justify-center",
|
|
462
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
463
|
+
size: "sm",
|
|
464
|
+
variant: "ghost",
|
|
465
|
+
"data-slot": "agent-chat-load-older",
|
|
466
|
+
disabled: loading,
|
|
467
|
+
onClick: onLoad,
|
|
468
|
+
children: loading ? "Loading…" : "Load older messages"
|
|
469
|
+
})
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
/** Distance from the bottom, in pixels, within which new content re-pins the view. */
|
|
473
|
+
const NEAR_BOTTOM_PX = 100;
|
|
474
|
+
function AgentChatSurface({ chat, agentName, attachmentsSlot, renderToolCall, headerSlot, hideHeader, components, classNames, autoLoadOlder = false, className, ...props }) {
|
|
430
475
|
const [drawerOpen, setDrawerOpen] = React.useState(false);
|
|
431
476
|
const [value, setValue] = React.useState("");
|
|
432
477
|
const scrollRef = React.useRef(null);
|
|
433
478
|
const contentRef = React.useRef(null);
|
|
479
|
+
const sentinelRef = React.useRef(null);
|
|
434
480
|
const prevHeightRef = React.useRef(0);
|
|
481
|
+
/** Whether the reader was at the bottom at the last scroll; the smooth re-pin reads it. */
|
|
482
|
+
const nearBottomRef = React.useRef(true);
|
|
483
|
+
/** The scroll height saved before a page of older history is prepended; 0 when none is in flight. */
|
|
484
|
+
const prependHeightRef = React.useRef(0);
|
|
485
|
+
/** Set by a prepend restore; the resize observer sits out the one notification the prepend produces. */
|
|
486
|
+
const holdRef = React.useRef(false);
|
|
487
|
+
const prevMessageCountRef = React.useRef(0);
|
|
488
|
+
const visible = chat.messages.filter((message) => !isHiddenMessage(message));
|
|
489
|
+
const Empty = components?.Empty;
|
|
490
|
+
const showEmpty = visible.length === 0 && !chat.streaming && !chat.error;
|
|
491
|
+
const showTranscript = !(showEmpty && Empty);
|
|
435
492
|
/**
|
|
436
493
|
* Re-pin to the bottom whenever the content grows or the viewport shrinks.
|
|
437
494
|
* A message keeps growing after it mounts — markdown settles, the tool strip
|
|
@@ -440,12 +497,21 @@ function AgentChat({ client, agentName, storageScope, ephemeral, thinking, attac
|
|
|
440
497
|
*
|
|
441
498
|
* Jumping rather than smooth-scrolling: this fires many times a second during
|
|
442
499
|
* streaming, and queued smooth animations fight each other and lose.
|
|
500
|
+
*
|
|
501
|
+
* Keyed on `showTranscript`: the scroller is unmounted while a host's Empty
|
|
502
|
+
* holds the region, and the observer has to attach to the one that mounts
|
|
503
|
+
* when the first message arrives.
|
|
443
504
|
*/
|
|
444
505
|
React.useEffect(() => {
|
|
445
506
|
const scrollEl = scrollRef.current;
|
|
446
507
|
const contentEl = contentRef.current;
|
|
447
508
|
if (!scrollEl || !contentEl || typeof ResizeObserver === "undefined") return;
|
|
448
509
|
const observer = new ResizeObserver(() => {
|
|
510
|
+
if (holdRef.current) {
|
|
511
|
+
holdRef.current = false;
|
|
512
|
+
prevHeightRef.current = scrollEl.scrollHeight;
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
449
515
|
const { scrollTop, scrollHeight, clientHeight } = scrollEl;
|
|
450
516
|
const stick = shouldStickToBottom({
|
|
451
517
|
prevScrollHeight: prevHeightRef.current,
|
|
@@ -457,8 +523,84 @@ function AgentChat({ client, agentName, storageScope, ephemeral, thinking, attac
|
|
|
457
523
|
});
|
|
458
524
|
observer.observe(contentEl);
|
|
459
525
|
observer.observe(scrollEl);
|
|
526
|
+
const onScroll = () => {
|
|
527
|
+
nearBottomRef.current = scrollEl.scrollHeight - scrollEl.scrollTop - scrollEl.clientHeight < NEAR_BOTTOM_PX;
|
|
528
|
+
};
|
|
529
|
+
scrollEl.addEventListener("scroll", onScroll, { passive: true });
|
|
530
|
+
return () => {
|
|
531
|
+
observer.disconnect();
|
|
532
|
+
scrollEl.removeEventListener("scroll", onScroll);
|
|
533
|
+
};
|
|
534
|
+
}, [showTranscript]);
|
|
535
|
+
/**
|
|
536
|
+
* Page older history as the reader reaches the top, not only on a click —
|
|
537
|
+
* when the host asks for it with `autoLoadOlder`. The sentinel is the first
|
|
538
|
+
* child of the transcript; when it scrolls into the viewport and there is
|
|
539
|
+
* more, the next page is asked for.
|
|
540
|
+
*/
|
|
541
|
+
const loadOlderChat = chat.loadOlder;
|
|
542
|
+
const loadOlder = React.useCallback(() => {
|
|
543
|
+
const height = scrollRef.current?.scrollHeight ?? 0;
|
|
544
|
+
if (loadOlderChat() === true && height > 0) prependHeightRef.current = height;
|
|
545
|
+
}, [loadOlderChat]);
|
|
546
|
+
React.useEffect(() => {
|
|
547
|
+
const sentinel = sentinelRef.current;
|
|
548
|
+
const scrollEl = scrollRef.current;
|
|
549
|
+
if (!autoLoadOlder || !sentinel || !scrollEl || typeof IntersectionObserver === "undefined") return;
|
|
550
|
+
const observer = new IntersectionObserver((entries) => {
|
|
551
|
+
if (!entries[0]?.isIntersecting) return;
|
|
552
|
+
if (scrollEl.scrollHeight <= scrollEl.clientHeight) return;
|
|
553
|
+
if (chat.hasMoreHistory && !chat.loadingHistory) loadOlder();
|
|
554
|
+
}, {
|
|
555
|
+
root: scrollEl,
|
|
556
|
+
threshold: .1
|
|
557
|
+
});
|
|
558
|
+
observer.observe(sentinel);
|
|
460
559
|
return () => observer.disconnect();
|
|
461
|
-
}, [
|
|
560
|
+
}, [
|
|
561
|
+
autoLoadOlder,
|
|
562
|
+
chat.hasMoreHistory,
|
|
563
|
+
chat.loadingHistory,
|
|
564
|
+
loadOlder,
|
|
565
|
+
showTranscript
|
|
566
|
+
]);
|
|
567
|
+
/**
|
|
568
|
+
* After a prepend, put the reader back where they were; after an append,
|
|
569
|
+
* follow the newest message — smoothly, once per message rather than per
|
|
570
|
+
* resize, which is why this is separate from the observer above and why it
|
|
571
|
+
* is NOT keyed on the stream text: growth during a run is the observer's,
|
|
572
|
+
* and a smooth scroll queued per delta fought it and lost.
|
|
573
|
+
*
|
|
574
|
+
* A layout effect, so the restore lands before the paint; an effect after
|
|
575
|
+
* it drew one frame at the old `scrollTop` with the new page on top.
|
|
576
|
+
*/
|
|
577
|
+
useLayoutEffectSafe(() => {
|
|
578
|
+
const scrollEl = scrollRef.current;
|
|
579
|
+
if (!scrollEl) return;
|
|
580
|
+
const prevCount = prevMessageCountRef.current;
|
|
581
|
+
const count = chat.messages.length;
|
|
582
|
+
prevMessageCountRef.current = count;
|
|
583
|
+
const saved = prependHeightRef.current;
|
|
584
|
+
if (saved > 0 && !chat.loadingHistory) {
|
|
585
|
+
prependHeightRef.current = 0;
|
|
586
|
+
if (count > prevCount && prevCount > 0) {
|
|
587
|
+
const delta = scrollEl.scrollHeight - saved;
|
|
588
|
+
if (delta > 0) {
|
|
589
|
+
scrollEl.scrollTop += delta;
|
|
590
|
+
prevHeightRef.current = scrollEl.scrollHeight;
|
|
591
|
+
holdRef.current = true;
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (count !== prevCount && nearBottomRef.current) {
|
|
597
|
+
if (typeof scrollEl.scrollTo === "function") scrollEl.scrollTo({
|
|
598
|
+
top: scrollEl.scrollHeight,
|
|
599
|
+
behavior: "smooth"
|
|
600
|
+
});
|
|
601
|
+
else scrollEl.scrollTop = scrollEl.scrollHeight;
|
|
602
|
+
}
|
|
603
|
+
}, [chat.messages, chat.loadingHistory]);
|
|
462
604
|
const submit = React.useCallback(() => {
|
|
463
605
|
const trimmed = value.trim();
|
|
464
606
|
if (!trimmed || chat.streaming) return;
|
|
@@ -479,15 +621,19 @@ function AgentChat({ client, agentName, storageScope, ephemeral, thinking, attac
|
|
|
479
621
|
}, [chat, submit]);
|
|
480
622
|
const toolEntries = chat.toolOrder.map((id) => chat.tools[id]).filter((entry) => entry !== void 0);
|
|
481
623
|
const showTools = toolEntries.length > 0 || chat.approvals.length > 0;
|
|
482
|
-
const
|
|
624
|
+
const Message = components?.Message;
|
|
625
|
+
const Streaming = components?.Streaming;
|
|
626
|
+
const LoadOlder = components?.LoadOlder ?? DefaultLoadOlder;
|
|
627
|
+
const Tools = components?.Tools;
|
|
628
|
+
const Composer = components?.Composer;
|
|
483
629
|
return /* @__PURE__ */ jsxs("div", {
|
|
484
630
|
"data-slot": "agent-chat",
|
|
485
631
|
className: cn("flex h-full min-h-0 w-full flex-col bg-[var(--ds-background)]", className),
|
|
486
632
|
...props,
|
|
487
633
|
children: [
|
|
488
|
-
/* @__PURE__ */ jsxs("div", {
|
|
634
|
+
hideHeader ? null : /* @__PURE__ */ jsxs("div", {
|
|
489
635
|
"data-slot": "agent-chat-header",
|
|
490
|
-
className: cn("flex shrink-0 items-center gap-2 border-b border-[var(--ds-border-subtle)] px-3 py-2"),
|
|
636
|
+
className: cn("flex shrink-0 items-center gap-2 border-b border-[var(--ds-border-subtle)] px-3 py-2", classNames?.header),
|
|
491
637
|
children: [
|
|
492
638
|
/* @__PURE__ */ jsx(Button, {
|
|
493
639
|
size: "sm",
|
|
@@ -517,7 +663,7 @@ function AgentChat({ client, agentName, storageScope, ephemeral, thinking, attac
|
|
|
517
663
|
})
|
|
518
664
|
]
|
|
519
665
|
}),
|
|
520
|
-
drawerOpen ? /* @__PURE__ */ jsx(AgentSessionDrawer, {
|
|
666
|
+
drawerOpen && !hideHeader ? /* @__PURE__ */ jsx(AgentSessionDrawer, {
|
|
521
667
|
sessions: chat.sessions,
|
|
522
668
|
current: chat.sessionKey,
|
|
523
669
|
loading: chat.loadingSessions,
|
|
@@ -526,30 +672,29 @@ function AgentChat({ client, agentName, storageScope, ephemeral, thinking, attac
|
|
|
526
672
|
setDrawerOpen(false);
|
|
527
673
|
}
|
|
528
674
|
}) : null,
|
|
529
|
-
/* @__PURE__ */ jsx("div", {
|
|
675
|
+
showTranscript ? /* @__PURE__ */ jsx("div", {
|
|
530
676
|
ref: scrollRef,
|
|
531
677
|
"data-slot": "agent-chat-scroll",
|
|
532
|
-
className: "min-h-0 flex-1 overflow-x-hidden overflow-y-auto",
|
|
678
|
+
className: cn("min-h-0 flex-1 overflow-x-hidden overflow-y-auto [overflow-anchor:none]", classNames?.scroll),
|
|
533
679
|
children: /* @__PURE__ */ jsxs("div", {
|
|
534
680
|
ref: contentRef,
|
|
535
681
|
role: "log",
|
|
536
682
|
"aria-live": "polite",
|
|
537
683
|
"aria-label": `${agentName} chat`,
|
|
538
|
-
className: "flex min-w-0 flex-col gap-5 px-4 py-4",
|
|
684
|
+
className: cn("relative flex min-w-0 flex-col", classNames?.transcript ?? "gap-5 px-4 py-4"),
|
|
539
685
|
children: [
|
|
686
|
+
/* @__PURE__ */ jsx("div", {
|
|
687
|
+
ref: sentinelRef,
|
|
688
|
+
"data-slot": "agent-chat-sentinel",
|
|
689
|
+
className: classNames?.sentinel ?? "absolute inset-x-0 top-0 h-px"
|
|
690
|
+
}),
|
|
540
691
|
chat.error ? /* @__PURE__ */ jsx(ErrorBanner, { message: chat.error }) : null,
|
|
541
|
-
chat.hasMoreHistory ? /* @__PURE__ */ jsx(
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
variant: "ghost",
|
|
546
|
-
"data-slot": "agent-chat-load-older",
|
|
547
|
-
disabled: chat.loadingHistory,
|
|
548
|
-
onClick: chat.loadOlder,
|
|
549
|
-
children: chat.loadingHistory ? "Loading…" : "Load older messages"
|
|
550
|
-
})
|
|
692
|
+
chat.hasMoreHistory || chat.loadingHistory ? /* @__PURE__ */ jsx(LoadOlder, {
|
|
693
|
+
hasMore: chat.hasMoreHistory,
|
|
694
|
+
loading: chat.loadingHistory,
|
|
695
|
+
onLoad: loadOlder
|
|
551
696
|
}) : null,
|
|
552
|
-
|
|
697
|
+
showEmpty ? /* @__PURE__ */ jsxs("p", {
|
|
553
698
|
"data-slot": "agent-chat-empty",
|
|
554
699
|
className: "text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-muted-foreground)]",
|
|
555
700
|
children: [
|
|
@@ -558,13 +703,27 @@ function AgentChat({ client, agentName, storageScope, ephemeral, thinking, attac
|
|
|
558
703
|
" anything about this screen."
|
|
559
704
|
]
|
|
560
705
|
}) : null,
|
|
561
|
-
visible.map((message) => /* @__PURE__ */ jsx(
|
|
706
|
+
visible.map((message, index) => Message ? /* @__PURE__ */ jsx(Message, {
|
|
707
|
+
message,
|
|
708
|
+
index,
|
|
709
|
+
messages: visible,
|
|
710
|
+
agentName,
|
|
711
|
+
sessionKey: chat.sessionKey,
|
|
712
|
+
streaming: chat.streaming,
|
|
713
|
+
onAction: chat.sendAction
|
|
714
|
+
}, message.id) : /* @__PURE__ */ jsx(AgentMessageBubble, {
|
|
562
715
|
message,
|
|
563
716
|
agentName,
|
|
564
717
|
streaming: chat.streaming,
|
|
565
718
|
onAction: chat.sendAction
|
|
566
719
|
}, message.id)),
|
|
567
|
-
chat.streaming ? /* @__PURE__ */ jsx(
|
|
720
|
+
chat.streaming ? Streaming ? /* @__PURE__ */ jsx(Streaming, {
|
|
721
|
+
agentName,
|
|
722
|
+
text: chat.streamText,
|
|
723
|
+
thinkingText: chat.streamThinkingText,
|
|
724
|
+
a2ui: chat.streamA2ui,
|
|
725
|
+
steps: chat.steps
|
|
726
|
+
}) : /* @__PURE__ */ jsx(AgentStreamingBubble, {
|
|
568
727
|
agentName,
|
|
569
728
|
text: chat.streamText,
|
|
570
729
|
thinkingText: chat.streamThinkingText,
|
|
@@ -573,10 +732,19 @@ function AgentChat({ client, agentName, storageScope, ephemeral, thinking, attac
|
|
|
573
732
|
}) : chat.steps.length > 0 ? /* @__PURE__ */ jsx(AgentSteps, { steps: chat.steps }) : null
|
|
574
733
|
]
|
|
575
734
|
})
|
|
576
|
-
}),
|
|
577
|
-
|
|
735
|
+
}) : Empty ? /* @__PURE__ */ jsx(Empty, {
|
|
736
|
+
agentName,
|
|
737
|
+
loadingHistory: chat.loadingHistory
|
|
738
|
+
}) : null,
|
|
739
|
+
Tools ? /* @__PURE__ */ jsx(Tools, {
|
|
740
|
+
entries: toolEntries,
|
|
741
|
+
approvals: chat.approvals,
|
|
742
|
+
streaming: chat.streaming,
|
|
743
|
+
renderToolCall,
|
|
744
|
+
onResolveApproval: chat.resolveApproval
|
|
745
|
+
}) : showTools ? /* @__PURE__ */ jsxs("div", {
|
|
578
746
|
"data-slot": "agent-chat-tools",
|
|
579
|
-
className: "flex shrink-0 flex-col gap-1.5 border-t border-[var(--ds-border-subtle)] px-3 py-2",
|
|
747
|
+
className: cn("flex shrink-0 flex-col gap-1.5 border-t border-[var(--ds-border-subtle)] px-3 py-2", classNames?.tools),
|
|
580
748
|
children: [
|
|
581
749
|
toolEntries.length > 0 ? /* @__PURE__ */ jsx(Badge, {
|
|
582
750
|
variant: "neutral",
|
|
@@ -601,9 +769,14 @@ function AgentChat({ client, agentName, storageScope, ephemeral, thinking, attac
|
|
|
601
769
|
className: "shrink-0 px-3",
|
|
602
770
|
children: attachmentsSlot
|
|
603
771
|
}) : null,
|
|
604
|
-
/* @__PURE__ */
|
|
772
|
+
Composer ? /* @__PURE__ */ jsx(Composer, {
|
|
773
|
+
agentName,
|
|
774
|
+
streaming: chat.streaming,
|
|
775
|
+
send: chat.send,
|
|
776
|
+
abort: chat.abort
|
|
777
|
+
}) : /* @__PURE__ */ jsxs("div", {
|
|
605
778
|
"data-slot": "agent-chat-composer",
|
|
606
|
-
className: "flex shrink-0 items-end gap-2 border-t border-[var(--ds-border-subtle)] p-3",
|
|
779
|
+
className: cn("flex shrink-0 items-end gap-2 border-t border-[var(--ds-border-subtle)] p-3", classNames?.composer),
|
|
607
780
|
children: [/* @__PURE__ */ jsx(Textarea, {
|
|
608
781
|
"data-slot": "agent-chat-input",
|
|
609
782
|
"aria-label": `Message ${agentName}`,
|