cortena-ui 1.4.2 → 1.6.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.
Files changed (138) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/LICENSE +7 -0
  3. package/README.md +235 -3
  4. package/dist/a2ui/views.js +2 -2
  5. package/dist/agent-chat/a2ui-block.d.ts +60 -0
  6. package/dist/agent-chat/a2ui-block.js +69 -0
  7. package/dist/agent-chat/a2ui-block.js.map +1 -0
  8. package/dist/agent-chat/agui-client.d.ts +40 -0
  9. package/dist/agent-chat/agui-client.js +251 -0
  10. package/dist/agent-chat/agui-client.js.map +1 -0
  11. package/dist/agent-chat/bridge.d.ts +109 -0
  12. package/dist/agent-chat/bridge.js +353 -0
  13. package/dist/agent-chat/bridge.js.map +1 -0
  14. package/dist/agent-chat/session.d.ts +79 -0
  15. package/dist/agent-chat/session.js +391 -0
  16. package/dist/agent-chat/session.js.map +1 -0
  17. package/dist/agent-chat/step-label.d.ts +99 -0
  18. package/dist/agent-chat/step-label.js +116 -0
  19. package/dist/agent-chat/step-label.js.map +1 -0
  20. package/dist/agent-chat/store.d.ts +102 -0
  21. package/dist/agent-chat/store.js +876 -0
  22. package/dist/agent-chat/store.js.map +1 -0
  23. package/dist/agent-chat/types.d.ts +277 -0
  24. package/dist/agent-chat/types.js +17 -0
  25. package/dist/agent-chat/types.js.map +1 -0
  26. package/dist/agent-chat.d.ts +11 -0
  27. package/dist/agent-chat.js +11 -0
  28. package/dist/components/admin-permissions/admin-permissions.d.ts +66 -0
  29. package/dist/components/admin-permissions/admin-permissions.js +101 -0
  30. package/dist/components/admin-permissions/admin-permissions.js.map +1 -0
  31. package/dist/components/admin-permissions/context.d.ts +70 -0
  32. package/dist/components/admin-permissions/context.js +258 -0
  33. package/dist/components/admin-permissions/context.js.map +1 -0
  34. package/dist/components/admin-permissions/index.d.ts +10 -0
  35. package/dist/components/admin-permissions/licence.d.ts +15 -0
  36. package/dist/components/admin-permissions/licence.js +78 -0
  37. package/dist/components/admin-permissions/licence.js.map +1 -0
  38. package/dist/components/admin-permissions/matrix.d.ts +20 -0
  39. package/dist/components/admin-permissions/matrix.js +191 -0
  40. package/dist/components/admin-permissions/matrix.js.map +1 -0
  41. package/dist/components/admin-permissions/members.d.ts +18 -0
  42. package/dist/components/admin-permissions/members.js +185 -0
  43. package/dist/components/admin-permissions/members.js.map +1 -0
  44. package/dist/components/admin-permissions/role-assignment.d.ts +35 -0
  45. package/dist/components/admin-permissions/role-assignment.js +174 -0
  46. package/dist/components/admin-permissions/role-assignment.js.map +1 -0
  47. package/dist/components/admin-permissions/roles.d.ts +25 -0
  48. package/dist/components/admin-permissions/roles.js +168 -0
  49. package/dist/components/admin-permissions/roles.js.map +1 -0
  50. package/dist/components/admin-permissions/types.d.ts +152 -0
  51. package/dist/components/admin-permissions/types.js +63 -0
  52. package/dist/components/admin-permissions/types.js.map +1 -0
  53. package/dist/components/agent-chat-popup.d.ts +29 -0
  54. package/dist/components/agent-chat-popup.js +188 -0
  55. package/dist/components/agent-chat-popup.js.map +1 -0
  56. package/dist/components/agent-chat.d.ts +163 -0
  57. package/dist/components/agent-chat.js +673 -0
  58. package/dist/components/agent-chat.js.map +1 -0
  59. package/dist/components/app-shell.d.ts +126 -0
  60. package/dist/components/app-shell.js +297 -0
  61. package/dist/components/app-shell.js.map +1 -0
  62. package/dist/components/badge.d.ts +1 -1
  63. package/dist/components/button-link.js +1 -1
  64. package/dist/components/button.d.ts +1 -1
  65. package/dist/components/checkbox.d.ts +1 -1
  66. package/dist/components/combobox.d.ts +1 -1
  67. package/dist/components/combobox.js +1 -1
  68. package/dist/components/consent-screen.d.ts +65 -0
  69. package/dist/components/consent-screen.js +123 -0
  70. package/dist/components/consent-screen.js.map +1 -0
  71. package/dist/components/data-table/data-table.d.ts +15 -1
  72. package/dist/components/data-table/data-table.js +18 -4
  73. package/dist/components/data-table/data-table.js.map +1 -1
  74. package/dist/components/data-table/index.d.ts +4 -4
  75. package/dist/components/data-table/parts.d.ts +27 -3
  76. package/dist/components/data-table/parts.js +175 -55
  77. package/dist/components/data-table/parts.js.map +1 -1
  78. package/dist/components/data-table/types.d.ts +61 -0
  79. package/dist/components/data-table/use-data-table.js +91 -6
  80. package/dist/components/data-table/use-data-table.js.map +1 -1
  81. package/dist/components/data-table/use-server-source.js +119 -28
  82. package/dist/components/data-table/use-server-source.js.map +1 -1
  83. package/dist/components/help-panel.d.ts +131 -0
  84. package/dist/components/help-panel.js +545 -0
  85. package/dist/components/help-panel.js.map +1 -0
  86. package/dist/components/login-screen.d.ts +127 -0
  87. package/dist/components/login-screen.js +339 -0
  88. package/dist/components/login-screen.js.map +1 -0
  89. package/dist/components/session-guard.d.ts +268 -0
  90. package/dist/components/session-guard.js +632 -0
  91. package/dist/components/session-guard.js.map +1 -0
  92. package/dist/components/toast.d.ts +1 -1
  93. package/dist/core.d.ts +5 -1
  94. package/dist/core.js +11 -7
  95. package/dist/data-table.d.ts +13 -4
  96. package/dist/data-table.js +10 -2
  97. package/dist/hooks/use-cortena-theme.js +49 -3
  98. package/dist/hooks/use-cortena-theme.js.map +1 -1
  99. package/dist/index.d.ts +17 -4
  100. package/dist/index.js +21 -8
  101. package/dist/markdown.d.ts +2 -1
  102. package/dist/markdown.js +2 -1
  103. package/package.json +18 -5
  104. package/src/agent-chat/a2ui-block.ts +118 -0
  105. package/src/agent-chat/agui-client.ts +405 -0
  106. package/src/agent-chat/bridge.ts +445 -0
  107. package/src/agent-chat/session.ts +549 -0
  108. package/src/agent-chat/step-label.ts +177 -0
  109. package/src/agent-chat/store.ts +1234 -0
  110. package/src/agent-chat/types.ts +308 -0
  111. package/src/components/admin-permissions/admin-permissions.tsx +130 -0
  112. package/src/components/admin-permissions/context.tsx +376 -0
  113. package/src/components/admin-permissions/index.tsx +32 -0
  114. package/src/components/admin-permissions/licence.tsx +84 -0
  115. package/src/components/admin-permissions/matrix.tsx +257 -0
  116. package/src/components/admin-permissions/members.tsx +204 -0
  117. package/src/components/admin-permissions/role-assignment.tsx +239 -0
  118. package/src/components/admin-permissions/roles.tsx +169 -0
  119. package/src/components/admin-permissions/types.ts +231 -0
  120. package/src/components/agent-chat-popup.tsx +289 -0
  121. package/src/components/agent-chat.tsx +1006 -0
  122. package/src/components/app-shell.tsx +502 -0
  123. package/src/components/consent-screen.tsx +239 -0
  124. package/src/components/data-table/data-table.tsx +36 -0
  125. package/src/components/data-table/index.tsx +6 -1
  126. package/src/components/data-table/parts.tsx +223 -47
  127. package/src/components/data-table/types.ts +68 -0
  128. package/src/components/data-table/use-data-table.ts +152 -4
  129. package/src/components/data-table/use-server-source.ts +150 -12
  130. package/src/components/help-panel.tsx +765 -0
  131. package/src/components/login-screen.tsx +479 -0
  132. package/src/components/session-guard.tsx +1071 -0
  133. package/src/entries/agent-chat.ts +137 -0
  134. package/src/entries/core.ts +8 -0
  135. package/src/entries/data-table.ts +41 -0
  136. package/src/entries/markdown.ts +25 -0
  137. package/src/hooks/use-cortena-theme.ts +63 -4
  138. package/src/index.ts +6 -0
@@ -0,0 +1,1006 @@
1
+ "use client";
2
+
3
+ /**
4
+ * AgentChat — the chat surface an extension's own agent runs in.
5
+ *
6
+ * §17 of how-to-create-a-cortena-extension, audit rule P-25. Moved here from
7
+ * `cortenaweb/components/chat/**` so there is one chat and not two: a cut-down
8
+ * widget diverges on approvals, A2UI and MCP Apps within one release, and every
9
+ * fix then has to be made twice. cortenaweb adopts this component as a
10
+ * follow-up; until then the two are kept in step by hand.
11
+ *
12
+ * Everything comes from the `AgentChatClient` prop — nothing here knows about
13
+ * HTTP, AG-UI or a store — which is what lets `test/agent-chat.test.tsx` drive
14
+ * the whole surface from a fake that replays scripted event sequences.
15
+ *
16
+ * Not in this package, on purpose: the MCP App frame. Rendering an extension's
17
+ * own screen means a sandboxed iframe, a `srcdoc` CSP, a JSON-RPC postMessage
18
+ * bridge and a `tools/invoke` proxy holding a credential — a host concern, not
19
+ * a component one. `renderToolCall` is the seam: return a node for a tool call
20
+ * and it replaces the card, return nothing and the default card is drawn.
21
+ */
22
+
23
+ import {
24
+ Check,
25
+ ChevronDown,
26
+ ChevronRight,
27
+ MessageSquarePlus,
28
+ Send,
29
+ ShieldCheck,
30
+ ShieldX,
31
+ Sparkles,
32
+ Square,
33
+ X,
34
+ } from "lucide-react";
35
+ import * as React from "react";
36
+ import { A2UIRenderer } from "@/a2ui/renderer";
37
+ import { A2UIErrorCard } from "@/a2ui/views";
38
+ import type { A2UIAction, A2UIMessage } from "@/a2ui/message";
39
+ import { Badge } from "@/components/badge";
40
+ import { Button } from "@/components/button";
41
+ import { ErrorBanner } from "@/components/error-banner";
42
+ import { Markdown } from "@/components/markdown";
43
+ import { Spinner } from "@/components/spinner";
44
+ import { Textarea } from "@/components/textarea";
45
+ import { isHiddenMessage, STORED_TEXT_LIMIT } from "@/agent-chat/session";
46
+ import { isGenericLabel, truncate } from "@/agent-chat/step-label";
47
+ import { type AgentToolEntry, useAgentChat } from "@/agent-chat/store";
48
+ import type {
49
+ AgentApprovalRequest,
50
+ AgentChatClient,
51
+ AgentChatMessage,
52
+ AgentChatStep,
53
+ AgentSession,
54
+ } from "@/agent-chat/types";
55
+ import { sessionTitle } from "@/agent-chat/types";
56
+ import type { A2UIChatBlock } from "@/agent-chat/a2ui-block";
57
+ import { cn } from "@/lib/cn";
58
+
59
+ /* ── stick to bottom ─────────────────────────────────────────────────────── */
60
+
61
+ /**
62
+ * Should the list jump back to the bottom after its content grew?
63
+ *
64
+ * Judged against the height BEFORE the growth. Measuring afterwards asks "is
65
+ * the newest content already visible", which is false by construction for any
66
+ * append taller than the threshold — a paragraph, a code block, an image — so
67
+ * the re-pin skipped exactly when it was needed.
68
+ */
69
+ export function shouldStickToBottom(params: {
70
+ prevScrollHeight: number;
71
+ scrollTop: number;
72
+ clientHeight: number;
73
+ threshold?: number;
74
+ }): boolean {
75
+ const { prevScrollHeight, scrollTop, clientHeight, threshold = 150 } = params;
76
+ if (prevScrollHeight <= 0) {
77
+ return true;
78
+ }
79
+ return prevScrollHeight - scrollTop - clientHeight < threshold;
80
+ }
81
+
82
+ /* ── thinking ────────────────────────────────────────────────────────────── */
83
+
84
+ export interface AgentThinkingProps {
85
+ text: string;
86
+ /** Open by default while the turn is live, collapsed once it is finished. */
87
+ defaultOpen?: boolean;
88
+ streaming?: boolean;
89
+ }
90
+
91
+ function AgentThinking({ text, defaultOpen, streaming }: AgentThinkingProps) {
92
+ const [open, setOpen] = React.useState(defaultOpen ?? false);
93
+ if (!text) {
94
+ return null;
95
+ }
96
+ return (
97
+ <div data-slot="agent-chat-thinking" className="mt-1">
98
+ <button
99
+ type="button"
100
+ aria-expanded={open}
101
+ onClick={() => setOpen((v) => !v)}
102
+ className={cn(
103
+ "inline-flex items-center gap-1 rounded-[var(--ds-radius-sm)] px-1 py-0.5",
104
+ "text-[length:var(--ds-text-caption)] text-[color:var(--ds-muted-foreground)]",
105
+ "transition-colors duration-[var(--ds-duration-fast)] hover:text-[color:var(--ds-foreground)]",
106
+ )}
107
+ >
108
+ {open ? <ChevronDown className="size-3" aria-hidden /> : <ChevronRight className="size-3" aria-hidden />}
109
+ {streaming ? "Thinking…" : "Chain of thought"}
110
+ </button>
111
+ {open ? (
112
+ <div
113
+ data-slot="agent-chat-thinking-body"
114
+ className={cn(
115
+ "mt-1 max-h-64 overflow-auto rounded-[var(--ds-radius-sm)] border border-[var(--ds-border-subtle)]",
116
+ "bg-[var(--ds-muted)] px-3 py-2 whitespace-pre-wrap",
117
+ "text-[length:var(--ds-text-caption)] text-[color:var(--ds-muted-foreground)]",
118
+ )}
119
+ >
120
+ {text}
121
+ </div>
122
+ ) : null}
123
+ </div>
124
+ );
125
+ }
126
+
127
+ /* ── progress steps ──────────────────────────────────────────────────────── */
128
+
129
+ /**
130
+ * How long a step runs before it starts saying how long it has been running.
131
+ *
132
+ * Under this, a timer is noise: every call takes a second or two and a number
133
+ * ticking beside each one is a fidget spinner. Over it, the user has begun to
134
+ * wonder whether anything is happening at all, and the number is the answer.
135
+ */
136
+ const ELAPSED_AFTER_MS = 8_000;
137
+
138
+ /**
139
+ * `Date.now()`, re-read once a second while `active`.
140
+ *
141
+ * The interval only exists while something is running, so a finished chat sits
142
+ * idle rather than re-rendering the whole transcript once a second for ever.
143
+ */
144
+ function useTickingNow(active: boolean): number {
145
+ const [now, setNow] = React.useState(() => Date.now());
146
+ React.useEffect(() => {
147
+ if (!active) {
148
+ return;
149
+ }
150
+ setNow(Date.now());
151
+ const id = setInterval(() => setNow(Date.now()), 1000);
152
+ return () => clearInterval(id);
153
+ }, [active]);
154
+ return now;
155
+ }
156
+
157
+ export interface AgentStepsProps {
158
+ steps: readonly AgentChatStep[];
159
+ }
160
+
161
+ /**
162
+ * What the agent is doing, one plain sentence per tool call (SKILLS-9).
163
+ *
164
+ * This is the surface a person watches while they wait, and it is deliberately
165
+ * not the tool strip: the strip is the record — names, arguments, output, the
166
+ * MCP App seam — and reading it requires knowing what `broker_invoke` is. The
167
+ * strip stays at the foot for anyone who wants the detail.
168
+ *
169
+ * NOT a live region of its own. The strip is drawn inside the transcript, and
170
+ * the transcript is already `role="log" aria-live="polite"` — two nested live
171
+ * regions is one announcement per change from the inner one and another from
172
+ * the outer, which is how a screen reader ends up saying every step twice.
173
+ * `aria-label` stays: it names the group for anyone who navigates to it.
174
+ */
175
+ function AgentSteps({ steps }: AgentStepsProps) {
176
+ const running = steps.some((step) => step.status === "running");
177
+ const now = useTickingNow(running);
178
+ if (steps.length === 0) {
179
+ return null;
180
+ }
181
+ return (
182
+ <div
183
+ data-slot="agent-chat-steps"
184
+ aria-label="Progress"
185
+ className="mt-1 flex flex-col gap-0.5"
186
+ >
187
+ {steps.map((step) => {
188
+ const elapsed = step.status === "running" ? now - step.startedAt : 0;
189
+ const seconds = elapsed >= ELAPSED_AFTER_MS ? Math.floor(elapsed / 1000) : 0;
190
+ // Held out of the announcement while the line is still the fallback.
191
+ // The arguments arrive one event after the start, so every call passes
192
+ // through "Working: broker_invoke" for a few hundred milliseconds, and
193
+ // a live region reads it out — then reads the real sentence over the
194
+ // top of it. The row is still drawn; it joins the announcement the
195
+ // moment it has something to say, or when it ends, whichever is first.
196
+ const held = step.status === "running" && isGenericLabel(step.toolName, step.label);
197
+ return (
198
+ <p
199
+ key={step.id}
200
+ data-slot="agent-chat-step"
201
+ data-status={step.status}
202
+ // The tool name is the model's, not ours, and an attribute is a
203
+ // poor place for 9 kB of it.
204
+ data-tool={truncate(step.toolName, STORED_TEXT_LIMIT)}
205
+ {...(held ? { "aria-hidden": true } : {})}
206
+ className={cn(
207
+ "flex items-start gap-2 text-[length:var(--ds-text-caption-lg)]",
208
+ step.status === "error"
209
+ ? "text-[color:var(--ds-destructive)]"
210
+ : "text-[color:var(--ds-muted-foreground)]",
211
+ )}
212
+ >
213
+ <span
214
+ aria-hidden
215
+ className="mt-0.5 inline-flex size-3.5 shrink-0 items-center justify-center"
216
+ >
217
+ {step.status === "running" ? (
218
+ <Spinner size={12} aria-hidden />
219
+ ) : step.status === "error" ? (
220
+ <X className="size-3" />
221
+ ) : step.status === "stopped" ? (
222
+ <Square className="size-2.5" />
223
+ ) : step.status === "paused" ? (
224
+ // A plain dot, drawn in the muted colour: neither a spinner
225
+ // (nothing is happening) nor a tick (nothing has happened).
226
+ <span className="size-1.5 rounded-full bg-[color:var(--ds-muted-foreground)]" />
227
+ ) : (
228
+ <Check className="size-3 text-[color:var(--ds-success)]" />
229
+ )}
230
+ </span>
231
+ <span className="min-w-0">
232
+ {/* Keyed on the words. A step is re-worded in place when its
233
+ arguments land, and a text node mutated under a live region
234
+ is a change some screen readers do not announce at all; a new
235
+ key makes it a node replacement, which they all do. */}
236
+ <span key={step.label} data-slot="agent-chat-step-label">
237
+ {step.label}
238
+ </span>
239
+ {/* The seconds change once a second, and a live region reads the
240
+ whole line again each time they do. The count is for the eye:
241
+ a screen reader hearing "Fetching tasks task list" once, and
242
+ again when it lands, is told everything it needs. */}
243
+ {seconds > 0 ? (
244
+ <span aria-hidden data-slot="agent-chat-step-elapsed">{` · ${seconds}s`}</span>
245
+ ) : null}
246
+ {step.status === "stopped" ? (
247
+ <span data-slot="agent-chat-step-note">{" — Stopped"}</span>
248
+ ) : null}
249
+ {step.status === "paused" ? (
250
+ <span data-slot="agent-chat-step-note">{" — Waiting for your approval"}</span>
251
+ ) : null}
252
+ {step.status === "error" && step.errorMessage ? (
253
+ <span data-slot="agent-chat-step-error">{` — ${step.errorMessage}`}</span>
254
+ ) : null}
255
+ </span>
256
+ </p>
257
+ );
258
+ })}
259
+ </div>
260
+ );
261
+ }
262
+
263
+ /* ── A2UI blocks ─────────────────────────────────────────────────────────── */
264
+
265
+ export interface AgentA2UIBlocksProps {
266
+ blocks: readonly A2UIChatBlock[] | undefined;
267
+ /** Set on the streaming bubble: actions are disabled while the turn is live. */
268
+ streaming?: boolean;
269
+ onAction?: (action: A2UIAction) => void;
270
+ }
271
+
272
+ /**
273
+ * Every block of one message, in arrival order.
274
+ *
275
+ * A block carrying a transport `error` — an unclosed fence, a body that did not
276
+ * parse — gets an error card rather than being dropped. A block that is merely
277
+ * malformed inside is the renderer's error to draw, and it draws the parts that
278
+ * were fine.
279
+ */
280
+ function AgentA2UIBlocks({ blocks, streaming, onAction }: AgentA2UIBlocksProps) {
281
+ if (!blocks || blocks.length === 0) {
282
+ return null;
283
+ }
284
+ return (
285
+ <div className="mt-2 flex flex-col gap-3" data-slot="agent-chat-a2ui">
286
+ {blocks.map((block, index) => (
287
+ <div key={`${block.surfaceId}:${index}`}>
288
+ {block.error ? (
289
+ <A2UIErrorCard title="The agent's UI did not arrive complete" detail={block.error} />
290
+ ) : null}
291
+ {block.messages.length > 0 ? (
292
+ <A2UIRenderer
293
+ // The payload stays `unknown[]`: it is the agent's, and only the
294
+ // renderer validates it. Every node goes through the catalogue's
295
+ // schema before it is drawn.
296
+ message={block.messages as A2UIMessage[]}
297
+ // A surface still streaming must not fire actions at the agent
298
+ // from a half-built form, so it is read-only until final.
299
+ {...(streaming || !onAction ? {} : { onAction })}
300
+ />
301
+ ) : null}
302
+ </div>
303
+ ))}
304
+ </div>
305
+ );
306
+ }
307
+
308
+ /* ── bubbles ─────────────────────────────────────────────────────────────── */
309
+
310
+ function AssistantAvatar() {
311
+ return (
312
+ <span
313
+ data-slot="agent-chat-avatar"
314
+ aria-hidden
315
+ className={cn(
316
+ "grid size-7 shrink-0 place-items-center rounded-[var(--ds-radius-sm)]",
317
+ "border border-[var(--ds-ring)] bg-[var(--ds-primary-soft)] text-[color:var(--ds-primary)]",
318
+ )}
319
+ >
320
+ <Sparkles className="size-3.5" aria-hidden />
321
+ </span>
322
+ );
323
+ }
324
+
325
+ export interface AgentMessageBubbleProps {
326
+ message: AgentChatMessage;
327
+ agentName: string;
328
+ /**
329
+ * True while a run is live. Every A2UI surface in the transcript goes
330
+ * read-only, not just the one being drawn: an earlier surface is still a
331
+ * form wired to `sendAction`, and pressing it mid-run starts a second
332
+ * concurrent turn on the same session.
333
+ */
334
+ streaming?: boolean;
335
+ onAction?: (action: A2UIAction) => void;
336
+ }
337
+
338
+ function AgentMessageBubble({ message, agentName, streaming, onAction }: AgentMessageBubbleProps) {
339
+ if (isHiddenMessage(message)) {
340
+ return null;
341
+ }
342
+ if (message.isError) {
343
+ return (
344
+ <div data-slot="agent-chat-message" data-role="error">
345
+ <ErrorBanner message={message.text} />
346
+ </div>
347
+ );
348
+ }
349
+ if (message.role === "user") {
350
+ return (
351
+ <div data-slot="agent-chat-message" data-role="user" className="flex w-full justify-end">
352
+ <div
353
+ className={cn(
354
+ "max-w-[85%] rounded-[var(--ds-radius-lg)] border border-[var(--ds-border-subtle)]",
355
+ "bg-[var(--ds-popover)] px-3.5 py-2 text-[color:var(--ds-foreground)]",
356
+ )}
357
+ >
358
+ <Markdown compact>{message.text}</Markdown>
359
+ </div>
360
+ </div>
361
+ );
362
+ }
363
+ return (
364
+ <div data-slot="agent-chat-message" data-role="assistant" className="flex w-full gap-3">
365
+ <AssistantAvatar />
366
+ <div className="min-w-0 flex-1">
367
+ <span
368
+ className={cn(
369
+ "text-[length:var(--ds-text-caption)] font-semibold",
370
+ "text-[color:var(--ds-foreground)]",
371
+ )}
372
+ >
373
+ {agentName}
374
+ </span>
375
+ <AgentThinking text={message.thinkingText ?? ""} />
376
+ {message.text ? (
377
+ <div className="mt-1">
378
+ <Markdown compact>{message.text}</Markdown>
379
+ </div>
380
+ ) : null}
381
+ <AgentA2UIBlocks
382
+ blocks={message.a2ui}
383
+ {...(streaming ? { streaming } : {})}
384
+ {...(onAction ? { onAction } : {})}
385
+ />
386
+ </div>
387
+ </div>
388
+ );
389
+ }
390
+
391
+ export interface AgentStreamingBubbleProps {
392
+ agentName: string;
393
+ text: string;
394
+ thinkingText: string;
395
+ a2ui: readonly A2UIChatBlock[];
396
+ /** The turn's tool calls in plain words, drawn under the agent's name. */
397
+ steps?: readonly AgentChatStep[];
398
+ }
399
+
400
+ /**
401
+ * The assistant bubble while a turn is live.
402
+ *
403
+ * The filter chain is deliberately not applied: streaming text is
404
+ * agent-authored prose in progress, and the filters are about finished
405
+ * messages. Markdown is not processed either — partially arrived source
406
+ * (`**bo`) looks worse than its raw form — so the streaming view is plain text
407
+ * under `whitespace-pre-wrap` and `Markdown` takes over on `final`.
408
+ */
409
+ function AgentStreamingBubble({
410
+ agentName,
411
+ text,
412
+ thinkingText,
413
+ a2ui,
414
+ steps,
415
+ }: AgentStreamingBubbleProps) {
416
+ return (
417
+ <div data-slot="agent-chat-streaming" className="flex w-full gap-3">
418
+ <AssistantAvatar />
419
+ <div className="min-w-0 flex-1">
420
+ <span
421
+ className={cn(
422
+ "text-[length:var(--ds-text-caption)] font-semibold",
423
+ "text-[color:var(--ds-foreground)]",
424
+ )}
425
+ >
426
+ {agentName}
427
+ </span>
428
+ <AgentSteps steps={steps ?? []} />
429
+ {thinkingText ? <AgentThinking text={thinkingText} defaultOpen streaming /> : null}
430
+ {text ? (
431
+ <div
432
+ data-slot="agent-chat-streaming-text"
433
+ className="mt-1 break-words whitespace-pre-wrap text-[color:var(--ds-foreground)]"
434
+ >
435
+ {text}
436
+ </div>
437
+ ) : null}
438
+ <AgentA2UIBlocks blocks={a2ui} streaming />
439
+ </div>
440
+ </div>
441
+ );
442
+ }
443
+
444
+ /* ── tool cards ──────────────────────────────────────────────────────────── */
445
+
446
+ /** A `ui://` resource in a tool result is an MCP App: the extension's own screen. */
447
+ export function readMcpAppUri(output: unknown): string | null {
448
+ const text =
449
+ typeof output === "string" ? output : output === undefined ? "" : JSON.stringify(output);
450
+ return text.match(/"uri"\s*:\s*"(ui:\/\/[^"]+)"/)?.[1] ?? null;
451
+ }
452
+
453
+ function summarise(input: unknown): string {
454
+ if (input === undefined || input === null) {
455
+ return "";
456
+ }
457
+ if (typeof input === "string") {
458
+ return input;
459
+ }
460
+ const record = input as Record<string, unknown>;
461
+ for (const key of ["command", "cmd", "query", "path", "url", "name"]) {
462
+ const value = record[key];
463
+ if (typeof value === "string" && value.trim()) {
464
+ return value;
465
+ }
466
+ }
467
+ const json = JSON.stringify(input);
468
+ return json.length > 120 ? `${json.slice(0, 117)}…` : json;
469
+ }
470
+
471
+ export interface AgentToolCardProps {
472
+ entry: AgentToolEntry;
473
+ }
474
+
475
+ /**
476
+ * The default card. It shows the tool's name, its input and its output — the
477
+ * input being the part that was missing for the whole of the AG-UI path,
478
+ * because the bridge had no `TOOL_CALL_ARGS` case.
479
+ */
480
+ function AgentToolCard({ entry }: AgentToolCardProps) {
481
+ const [open, setOpen] = React.useState(false);
482
+ const summary = summarise(entry.input);
483
+ return (
484
+ <div
485
+ data-slot="agent-tool-card"
486
+ data-tool={entry.name}
487
+ data-status={entry.status}
488
+ className={cn(
489
+ "overflow-hidden rounded-[var(--ds-radius-sm)] border border-[var(--ds-border-subtle)]",
490
+ "bg-[var(--ds-muted)]",
491
+ )}
492
+ >
493
+ <button
494
+ type="button"
495
+ aria-expanded={open}
496
+ onClick={() => setOpen((v) => !v)}
497
+ className={cn(
498
+ "flex w-full items-center gap-2.5 px-3 py-2 text-left",
499
+ "transition-colors duration-[var(--ds-duration-fast)] hover:bg-[var(--ds-hover)]",
500
+ )}
501
+ >
502
+ <span className="inline-flex size-3.5 shrink-0 items-center justify-center">
503
+ {entry.status === "running" ? (
504
+ <Spinner size={14} />
505
+ ) : (
506
+ <span
507
+ aria-hidden
508
+ className={cn(
509
+ "size-2 rounded-[var(--ds-radius-full)]",
510
+ entry.status === "error" ? "bg-[var(--ds-destructive)]" : "bg-[var(--ds-success)]",
511
+ )}
512
+ />
513
+ )}
514
+ </span>
515
+ <span className="font-mono text-[length:var(--ds-text-caption)] text-[color:var(--ds-primary)]">
516
+ {entry.name}
517
+ </span>
518
+ {summary ? (
519
+ <span
520
+ data-slot="agent-tool-card-input"
521
+ className="min-w-0 truncate text-[length:var(--ds-text-caption)] text-[color:var(--ds-muted-foreground)]"
522
+ >
523
+ {summary}
524
+ </span>
525
+ ) : null}
526
+ <span className="ml-auto shrink-0 text-[color:var(--ds-text-tertiary)]">
527
+ {open ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />}
528
+ </span>
529
+ </button>
530
+ {open ? (
531
+ <div className="border-t border-[var(--ds-border-subtle)] bg-[var(--ds-background)] px-3 py-2">
532
+ {entry.input !== undefined ? (
533
+ <pre
534
+ data-slot="agent-tool-card-args"
535
+ className="max-h-40 overflow-auto font-mono text-[length:var(--ds-text-caption-sm)] whitespace-pre-wrap text-[color:var(--ds-muted-foreground)]"
536
+ >
537
+ {typeof entry.input === "string" ? entry.input : JSON.stringify(entry.input, null, 2)}
538
+ </pre>
539
+ ) : null}
540
+ {entry.output !== undefined && entry.output !== null ? (
541
+ <pre className="mt-2 max-h-40 overflow-auto font-mono text-[length:var(--ds-text-caption-sm)] whitespace-pre-wrap text-[color:var(--ds-muted-foreground)]">
542
+ {typeof entry.output === "string"
543
+ ? entry.output
544
+ : JSON.stringify(entry.output, null, 2)}
545
+ </pre>
546
+ ) : null}
547
+ </div>
548
+ ) : null}
549
+ </div>
550
+ );
551
+ }
552
+
553
+ export interface AgentApprovalCardProps {
554
+ request: AgentApprovalRequest;
555
+ onResolve: (id: string, decision: "allow-once" | "allow-always" | "deny") => void;
556
+ }
557
+
558
+ /**
559
+ * `exec` as a render-and-wait card: the run is paused on this decision.
560
+ *
561
+ * The three answers are the three cortenacore accepts. "Always allow" is not a
562
+ * nicety — without it the same command is asked about on every turn, and a user
563
+ * who has to answer the same question repeatedly stops reading the question.
564
+ */
565
+ function AgentApprovalCard({ request, onResolve }: AgentApprovalCardProps) {
566
+ const command =
567
+ typeof request.args.command === "string" ? request.args.command : request.toolName;
568
+ return (
569
+ <div
570
+ data-slot="agent-approval-card"
571
+ role="group"
572
+ aria-label="Command approval required"
573
+ className={cn(
574
+ "flex flex-col gap-3 rounded-[var(--ds-radius-md)] border border-[var(--ds-border)]",
575
+ "bg-[var(--ds-card)] p-3",
576
+ )}
577
+ >
578
+ <div className="flex items-center gap-2">
579
+ <ShieldCheck className="size-4 text-[color:var(--ds-primary)]" aria-hidden />
580
+ <span className="text-[length:var(--ds-text-body-sm)] font-medium text-[color:var(--ds-foreground)]">
581
+ Approval required
582
+ </span>
583
+ </div>
584
+ <pre className="max-h-32 overflow-auto rounded-[var(--ds-radius-sm)] bg-[var(--ds-muted)] p-2 font-mono text-[length:var(--ds-text-caption)] whitespace-pre-wrap text-[color:var(--ds-foreground)]">
585
+ {command}
586
+ </pre>
587
+ {/* A decision the server refused. The card came back rather than
588
+ vanishing, so the answer can be given again. */}
589
+ {request.error ? (
590
+ <p
591
+ data-slot="agent-approval-error"
592
+ role="alert"
593
+ className="text-[length:var(--ds-text-caption)] text-[color:var(--ds-destructive)]"
594
+ >
595
+ {request.error}
596
+ </p>
597
+ ) : null}
598
+ <div className="flex flex-wrap items-center gap-2">
599
+ <Button size="sm" onClick={() => onResolve(request.id, "allow-once")}>
600
+ <ShieldCheck aria-hidden />
601
+ Approve
602
+ </Button>
603
+ <Button size="sm" variant="outline" onClick={() => onResolve(request.id, "allow-always")}>
604
+ Always allow
605
+ </Button>
606
+ <Button size="sm" variant="destructive" onClick={() => onResolve(request.id, "deny")}>
607
+ <ShieldX aria-hidden />
608
+ Deny
609
+ </Button>
610
+ </div>
611
+ </div>
612
+ );
613
+ }
614
+
615
+ /* ── the surface ─────────────────────────────────────────────────────────── */
616
+
617
+ export interface AgentToolRenderContext {
618
+ entry: AgentToolEntry;
619
+ /** The `ui://` resource this result carries, when it carries one. */
620
+ mcpAppUri: string | null;
621
+ }
622
+
623
+ export interface AgentChatProps extends Omit<React.ComponentProps<"div">, "children" | "onError"> {
624
+ /** The transport. `createAguiAgentChatClient` is the AG-UI implementation. */
625
+ client: AgentChatClient;
626
+ /** The extension name, shown on each assistant turn. */
627
+ agentName: string;
628
+ /**
629
+ * Names the `sessionStorage` slot this window's session key lives in. Scope
630
+ * it to the extension id; two chats with different scopes never share a key.
631
+ */
632
+ storageScope: string;
633
+ /** Do not read or write `sessionStorage`; for a guide or a test. */
634
+ ephemeral?: boolean;
635
+ thinking?: "low" | "medium" | "high";
636
+ /** Rendered between the tool strip and the composer — an attachment picker. */
637
+ attachmentsSlot?: React.ReactNode;
638
+ /**
639
+ * Host wiring for a tool call. Return a node to replace the default card —
640
+ * this is the seam an MCP App frame mounts through, since the sandboxed
641
+ * iframe, its CSP and the `tools/invoke` proxy are the host's to own.
642
+ */
643
+ renderToolCall?: (context: AgentToolRenderContext) => React.ReactNode | undefined;
644
+ /** Header content, e.g. the pop-up's own window controls. */
645
+ headerSlot?: React.ReactNode;
646
+ }
647
+
648
+ function AgentChat({
649
+ client,
650
+ agentName,
651
+ storageScope,
652
+ ephemeral,
653
+ thinking,
654
+ attachmentsSlot,
655
+ renderToolCall,
656
+ headerSlot,
657
+ className,
658
+ ...props
659
+ }: AgentChatProps) {
660
+ const chat = useAgentChat({
661
+ client,
662
+ storageScope,
663
+ ...(ephemeral === undefined ? {} : { ephemeral }),
664
+ ...(thinking === undefined ? {} : { thinking }),
665
+ });
666
+ const [drawerOpen, setDrawerOpen] = React.useState(false);
667
+ const [value, setValue] = React.useState("");
668
+ const scrollRef = React.useRef<HTMLDivElement>(null);
669
+ const contentRef = React.useRef<HTMLDivElement>(null);
670
+ const prevHeightRef = React.useRef(0);
671
+
672
+ /**
673
+ * Re-pin to the bottom whenever the content grows or the viewport shrinks.
674
+ * A message keeps growing after it mounts — markdown settles, the tool strip
675
+ * appears between the list and the composer — and every one of those leaves
676
+ * the newest content under the composer until the reader scrolls by hand.
677
+ *
678
+ * Jumping rather than smooth-scrolling: this fires many times a second during
679
+ * streaming, and queued smooth animations fight each other and lose.
680
+ */
681
+ React.useEffect(() => {
682
+ const scrollEl = scrollRef.current;
683
+ const contentEl = contentRef.current;
684
+ if (!scrollEl || !contentEl || typeof ResizeObserver === "undefined") {
685
+ return;
686
+ }
687
+ const observer = new ResizeObserver(() => {
688
+ const { scrollTop, scrollHeight, clientHeight } = scrollEl;
689
+ const stick = shouldStickToBottom({
690
+ prevScrollHeight: prevHeightRef.current,
691
+ scrollTop,
692
+ clientHeight,
693
+ });
694
+ prevHeightRef.current = scrollHeight;
695
+ if (stick) {
696
+ scrollEl.scrollTop = scrollEl.scrollHeight;
697
+ }
698
+ });
699
+ observer.observe(contentEl);
700
+ observer.observe(scrollEl);
701
+ return () => observer.disconnect();
702
+ }, []);
703
+
704
+ const submit = React.useCallback(() => {
705
+ const trimmed = value.trim();
706
+ // Enter reaches here while a run is live, where the button is a Stop
707
+ // control and cannot be pressed. One run at a time, by both routes.
708
+ if (!trimmed || chat.streaming) {
709
+ return;
710
+ }
711
+ chat.send(trimmed);
712
+ setValue("");
713
+ }, [chat, value]);
714
+
715
+ const onKeyDown = React.useCallback(
716
+ (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
717
+ if (event.key === "Escape" && chat.streaming) {
718
+ // Escape stops the run. It does not close the pop-up while a run is in
719
+ // flight: the first Escape is nearly always "stop", not "go away".
720
+ event.preventDefault();
721
+ event.stopPropagation();
722
+ chat.abort();
723
+ return;
724
+ }
725
+ if (event.key === "Enter" && !event.shiftKey) {
726
+ event.preventDefault();
727
+ submit();
728
+ }
729
+ },
730
+ [chat, submit],
731
+ );
732
+
733
+ const toolEntries = chat.toolOrder
734
+ .map((id) => chat.tools[id])
735
+ .filter((entry): entry is AgentToolEntry => entry !== undefined);
736
+ // The strip outlives the run rather than vanishing with it. The steps of a
737
+ // finished turn are the record of what the agent did on the user's behalf,
738
+ // and an MCP App drawn by one of them is a surface the user is meant to work
739
+ // in — one that disappeared the instant the agent stopped talking would be no
740
+ // use to anybody. The next run clears it.
741
+ const showTools = toolEntries.length > 0 || chat.approvals.length > 0;
742
+ const visible = chat.messages.filter((message) => !isHiddenMessage(message));
743
+
744
+ return (
745
+ <div
746
+ data-slot="agent-chat"
747
+ className={cn("flex h-full min-h-0 w-full flex-col bg-[var(--ds-background)]", className)}
748
+ {...props}
749
+ >
750
+ <div
751
+ data-slot="agent-chat-header"
752
+ className={cn(
753
+ "flex shrink-0 items-center gap-2 border-b border-[var(--ds-border-subtle)] px-3 py-2",
754
+ )}
755
+ >
756
+ <Button
757
+ size="sm"
758
+ variant="ghost"
759
+ data-slot="agent-chat-sessions-trigger"
760
+ aria-expanded={drawerOpen}
761
+ onClick={() => {
762
+ setDrawerOpen((open) => !open);
763
+ chat.refreshSessions();
764
+ }}
765
+ >
766
+ {chat.sessions.length === 1 ? "1 session" : `${chat.sessions.length} sessions`}
767
+ </Button>
768
+ <Button
769
+ size="sm"
770
+ variant="ghost"
771
+ data-slot="agent-chat-new-session"
772
+ aria-label="New session"
773
+ onClick={() => {
774
+ chat.newSession();
775
+ setDrawerOpen(false);
776
+ }}
777
+ >
778
+ <MessageSquarePlus aria-hidden />
779
+ New
780
+ </Button>
781
+ <div className="ml-auto flex items-center gap-1">{headerSlot}</div>
782
+ </div>
783
+
784
+ {drawerOpen ? (
785
+ <AgentSessionDrawer
786
+ sessions={chat.sessions}
787
+ current={chat.sessionKey}
788
+ loading={chat.loadingSessions}
789
+ onResume={(key) => {
790
+ chat.resumeSession(key);
791
+ setDrawerOpen(false);
792
+ }}
793
+ />
794
+ ) : null}
795
+
796
+ <div
797
+ ref={scrollRef}
798
+ data-slot="agent-chat-scroll"
799
+ className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto"
800
+ >
801
+ <div
802
+ ref={contentRef}
803
+ role="log"
804
+ aria-live="polite"
805
+ aria-label={`${agentName} chat`}
806
+ className="flex min-w-0 flex-col gap-5 px-4 py-4"
807
+ >
808
+ {chat.error ? (
809
+ <ErrorBanner message={chat.error} />
810
+ ) : null}
811
+
812
+ {chat.hasMoreHistory ? (
813
+ <div className="flex justify-center">
814
+ <Button
815
+ size="sm"
816
+ variant="ghost"
817
+ data-slot="agent-chat-load-older"
818
+ disabled={chat.loadingHistory}
819
+ onClick={chat.loadOlder}
820
+ >
821
+ {chat.loadingHistory ? "Loading…" : "Load older messages"}
822
+ </Button>
823
+ </div>
824
+ ) : null}
825
+
826
+ {visible.length === 0 && !chat.streaming ? (
827
+ <p
828
+ data-slot="agent-chat-empty"
829
+ className="text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-muted-foreground)]"
830
+ >
831
+ Ask {agentName} anything about this screen.
832
+ </p>
833
+ ) : null}
834
+
835
+ {visible.map((message) => (
836
+ <AgentMessageBubble
837
+ key={message.id}
838
+ message={message}
839
+ agentName={agentName}
840
+ streaming={chat.streaming}
841
+ onAction={chat.sendAction}
842
+ />
843
+ ))}
844
+
845
+ {chat.streaming ? (
846
+ <AgentStreamingBubble
847
+ agentName={agentName}
848
+ text={chat.streamText}
849
+ thinkingText={chat.streamThinkingText}
850
+ a2ui={chat.streamA2ui}
851
+ steps={chat.steps}
852
+ />
853
+ ) : chat.steps.length > 0 ? (
854
+ // The turn is over but its steps are the record of what was done to
855
+ // answer it, and they stay until the next run replaces them — the
856
+ // same rule the tool strip below follows.
857
+ <AgentSteps steps={chat.steps} />
858
+ ) : null}
859
+ </div>
860
+ </div>
861
+
862
+ {showTools ? (
863
+ <div
864
+ data-slot="agent-chat-tools"
865
+ className="flex shrink-0 flex-col gap-1.5 border-t border-[var(--ds-border-subtle)] px-3 py-2"
866
+ >
867
+ {toolEntries.length > 0 ? (
868
+ <Badge variant="neutral" className="w-fit">
869
+ {toolEntries.length === 1 ? "1 step" : `${toolEntries.length} steps`}
870
+ </Badge>
871
+ ) : null}
872
+ {toolEntries.map((entry) => {
873
+ const custom = renderToolCall?.({
874
+ entry,
875
+ mcpAppUri: readMcpAppUri(entry.output),
876
+ });
877
+ return (
878
+ <React.Fragment key={entry.id}>
879
+ {custom ?? <AgentToolCard entry={entry} />}
880
+ </React.Fragment>
881
+ );
882
+ })}
883
+ {chat.approvals.map((request) => (
884
+ <AgentApprovalCard
885
+ key={request.id}
886
+ request={request}
887
+ onResolve={chat.resolveApproval}
888
+ />
889
+ ))}
890
+ </div>
891
+ ) : null}
892
+
893
+ {attachmentsSlot ? (
894
+ <div data-slot="agent-chat-attachments" className="shrink-0 px-3">
895
+ {attachmentsSlot}
896
+ </div>
897
+ ) : null}
898
+
899
+ <div
900
+ data-slot="agent-chat-composer"
901
+ className="flex shrink-0 items-end gap-2 border-t border-[var(--ds-border-subtle)] p-3"
902
+ >
903
+ <Textarea
904
+ data-slot="agent-chat-input"
905
+ aria-label={`Message ${agentName}`}
906
+ placeholder={`Message ${agentName}…`}
907
+ rows={1}
908
+ className="max-h-40 min-h-9 resize-none"
909
+ value={value}
910
+ onChange={(event) => setValue(event.target.value)}
911
+ onKeyDown={onKeyDown}
912
+ />
913
+ {chat.streaming ? (
914
+ <Button
915
+ size="icon"
916
+ variant="outline"
917
+ data-slot="agent-chat-stop"
918
+ aria-label="Stop the run"
919
+ onClick={chat.abort}
920
+ >
921
+ <Square aria-hidden />
922
+ </Button>
923
+ ) : (
924
+ <Button
925
+ size="icon"
926
+ data-slot="agent-chat-send"
927
+ aria-label="Send"
928
+ disabled={!value.trim()}
929
+ onClick={submit}
930
+ >
931
+ <Send aria-hidden />
932
+ </Button>
933
+ )}
934
+ </div>
935
+ </div>
936
+ );
937
+ }
938
+
939
+ /* ── the session drawer ──────────────────────────────────────────────────── */
940
+
941
+ export interface AgentSessionDrawerProps {
942
+ sessions: readonly AgentSession[];
943
+ current: string | null;
944
+ loading?: boolean;
945
+ onResume: (key: string) => void;
946
+ }
947
+
948
+ /**
949
+ * Every session of this agent, resumable from any window.
950
+ *
951
+ * The list is not this window's history: a session started in another window is
952
+ * here and can be resumed, which is the difference between "sessions are per
953
+ * window" (which key a window starts on) and "sessions are private to a window"
954
+ * (which they are not).
955
+ */
956
+ function AgentSessionDrawer({ sessions, current, loading, onResume }: AgentSessionDrawerProps) {
957
+ return (
958
+ <div
959
+ data-slot="agent-chat-sessions"
960
+ className={cn(
961
+ "max-h-56 shrink-0 overflow-y-auto border-b border-[var(--ds-border-subtle)]",
962
+ "bg-[var(--ds-card)] px-2 py-1.5",
963
+ )}
964
+ >
965
+ {loading && sessions.length === 0 ? (
966
+ <p className="px-2 py-1 text-[length:var(--ds-text-caption)] text-[color:var(--ds-muted-foreground)]">
967
+ Loading sessions…
968
+ </p>
969
+ ) : null}
970
+ {!loading && sessions.length === 0 ? (
971
+ <p className="px-2 py-1 text-[length:var(--ds-text-caption)] text-[color:var(--ds-muted-foreground)]">
972
+ No earlier sessions.
973
+ </p>
974
+ ) : null}
975
+ {sessions.map((session) => (
976
+ <button
977
+ key={session.key}
978
+ type="button"
979
+ data-slot="agent-chat-session"
980
+ data-current={session.key === current ? "" : undefined}
981
+ onClick={() => onResume(session.key)}
982
+ className={cn(
983
+ "flex w-full items-center gap-2 rounded-[var(--ds-radius-sm)] px-2 py-1.5 text-left",
984
+ "text-[length:var(--ds-text-caption-lg)] text-[color:var(--ds-foreground)]",
985
+ "transition-colors duration-[var(--ds-duration-fast)] hover:bg-[var(--ds-hover)]",
986
+ session.key === current && "bg-[var(--ds-primary-soft)]",
987
+ )}
988
+ >
989
+ <span className="min-w-0 truncate">{sessionTitle(session)}</span>
990
+ </button>
991
+ ))}
992
+ </div>
993
+ );
994
+ }
995
+
996
+ export {
997
+ AgentA2UIBlocks,
998
+ AgentApprovalCard,
999
+ AgentChat,
1000
+ AgentMessageBubble,
1001
+ AgentSessionDrawer,
1002
+ AgentSteps,
1003
+ AgentStreamingBubble,
1004
+ AgentThinking,
1005
+ AgentToolCard,
1006
+ };