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.
@@ -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
- export interface AgentChatProps extends Omit<React.ComponentProps<"div">, "children" | "onError"> {
122
- /** The transport. `createAguiAgentChatClient` is the AG-UI implementation. */
123
- client: AgentChatClient;
124
- /** The extension name, shown on each assistant turn. */
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
- * Names the `sessionStorage` slot this window's session key lives in. Scope
128
- * it to the extension id; two chats with different scopes never share a key.
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
- storageScope: string;
131
- /** Do not read or write `sessionStorage`; for a guide or a test. */
132
- ephemeral?: boolean;
133
- thinking?: "low" | "medium" | "high";
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
- declare function AgentChat({ client, agentName, storageScope, ephemeral, thinking, attachmentsSlot, renderToolCall, headerSlot, className, ...props }: AgentChatProps): React.JSX.Element;
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
- function AgentChat({ client, agentName, storageScope, ephemeral, thinking, attachmentsSlot, renderToolCall, headerSlot, className, ...props }) {
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 visible = chat.messages.filter((message) => !isHiddenMessage(message));
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("div", {
542
- className: "flex justify-center",
543
- children: /* @__PURE__ */ jsx(Button, {
544
- size: "sm",
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
- visible.length === 0 && !chat.streaming ? /* @__PURE__ */ jsxs("p", {
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(AgentMessageBubble, {
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(AgentStreamingBubble, {
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
- showTools ? /* @__PURE__ */ jsxs("div", {
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__ */ jsxs("div", {
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}`,