cortena-ui 1.4.2 → 1.5.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 (133) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +149 -1
  3. package/dist/a2ui/views.js +2 -2
  4. package/dist/agent-chat/a2ui-block.d.ts +60 -0
  5. package/dist/agent-chat/a2ui-block.js +69 -0
  6. package/dist/agent-chat/a2ui-block.js.map +1 -0
  7. package/dist/agent-chat/agui-client.d.ts +40 -0
  8. package/dist/agent-chat/agui-client.js +251 -0
  9. package/dist/agent-chat/agui-client.js.map +1 -0
  10. package/dist/agent-chat/bridge.d.ts +109 -0
  11. package/dist/agent-chat/bridge.js +349 -0
  12. package/dist/agent-chat/bridge.js.map +1 -0
  13. package/dist/agent-chat/session.d.ts +58 -0
  14. package/dist/agent-chat/session.js +249 -0
  15. package/dist/agent-chat/session.js.map +1 -0
  16. package/dist/agent-chat/store.d.ts +67 -0
  17. package/dist/agent-chat/store.js +548 -0
  18. package/dist/agent-chat/store.js.map +1 -0
  19. package/dist/agent-chat/types.d.ts +187 -0
  20. package/dist/agent-chat/types.js +17 -0
  21. package/dist/agent-chat/types.js.map +1 -0
  22. package/dist/agent-chat.d.ts +10 -0
  23. package/dist/agent-chat.js +10 -0
  24. package/dist/components/admin-permissions/admin-permissions.d.ts +66 -0
  25. package/dist/components/admin-permissions/admin-permissions.js +101 -0
  26. package/dist/components/admin-permissions/admin-permissions.js.map +1 -0
  27. package/dist/components/admin-permissions/context.d.ts +70 -0
  28. package/dist/components/admin-permissions/context.js +258 -0
  29. package/dist/components/admin-permissions/context.js.map +1 -0
  30. package/dist/components/admin-permissions/index.d.ts +10 -0
  31. package/dist/components/admin-permissions/licence.d.ts +15 -0
  32. package/dist/components/admin-permissions/licence.js +78 -0
  33. package/dist/components/admin-permissions/licence.js.map +1 -0
  34. package/dist/components/admin-permissions/matrix.d.ts +20 -0
  35. package/dist/components/admin-permissions/matrix.js +191 -0
  36. package/dist/components/admin-permissions/matrix.js.map +1 -0
  37. package/dist/components/admin-permissions/members.d.ts +18 -0
  38. package/dist/components/admin-permissions/members.js +185 -0
  39. package/dist/components/admin-permissions/members.js.map +1 -0
  40. package/dist/components/admin-permissions/role-assignment.d.ts +35 -0
  41. package/dist/components/admin-permissions/role-assignment.js +174 -0
  42. package/dist/components/admin-permissions/role-assignment.js.map +1 -0
  43. package/dist/components/admin-permissions/roles.d.ts +25 -0
  44. package/dist/components/admin-permissions/roles.js +168 -0
  45. package/dist/components/admin-permissions/roles.js.map +1 -0
  46. package/dist/components/admin-permissions/types.d.ts +152 -0
  47. package/dist/components/admin-permissions/types.js +63 -0
  48. package/dist/components/admin-permissions/types.js.map +1 -0
  49. package/dist/components/agent-chat-popup.d.ts +29 -0
  50. package/dist/components/agent-chat-popup.js +188 -0
  51. package/dist/components/agent-chat-popup.js.map +1 -0
  52. package/dist/components/agent-chat.d.ts +143 -0
  53. package/dist/components/agent-chat.js +578 -0
  54. package/dist/components/agent-chat.js.map +1 -0
  55. package/dist/components/app-shell.d.ts +126 -0
  56. package/dist/components/app-shell.js +297 -0
  57. package/dist/components/app-shell.js.map +1 -0
  58. package/dist/components/badge.d.ts +1 -1
  59. package/dist/components/button-link.js +1 -1
  60. package/dist/components/button.d.ts +2 -2
  61. package/dist/components/checkbox.d.ts +1 -1
  62. package/dist/components/combobox.d.ts +1 -1
  63. package/dist/components/combobox.js +1 -1
  64. package/dist/components/consent-screen.d.ts +65 -0
  65. package/dist/components/consent-screen.js +123 -0
  66. package/dist/components/consent-screen.js.map +1 -0
  67. package/dist/components/data-table/data-table.d.ts +15 -1
  68. package/dist/components/data-table/data-table.js +18 -4
  69. package/dist/components/data-table/data-table.js.map +1 -1
  70. package/dist/components/data-table/index.d.ts +4 -4
  71. package/dist/components/data-table/parts.d.ts +27 -3
  72. package/dist/components/data-table/parts.js +175 -55
  73. package/dist/components/data-table/parts.js.map +1 -1
  74. package/dist/components/data-table/types.d.ts +61 -0
  75. package/dist/components/data-table/use-data-table.js +91 -6
  76. package/dist/components/data-table/use-data-table.js.map +1 -1
  77. package/dist/components/data-table/use-server-source.js +119 -28
  78. package/dist/components/data-table/use-server-source.js.map +1 -1
  79. package/dist/components/help-panel.d.ts +131 -0
  80. package/dist/components/help-panel.js +545 -0
  81. package/dist/components/help-panel.js.map +1 -0
  82. package/dist/components/login-screen.d.ts +127 -0
  83. package/dist/components/login-screen.js +339 -0
  84. package/dist/components/login-screen.js.map +1 -0
  85. package/dist/components/session-guard.d.ts +268 -0
  86. package/dist/components/session-guard.js +632 -0
  87. package/dist/components/session-guard.js.map +1 -0
  88. package/dist/components/toast.d.ts +1 -1
  89. package/dist/core.d.ts +5 -1
  90. package/dist/core.js +11 -7
  91. package/dist/data-table.d.ts +13 -4
  92. package/dist/data-table.js +10 -2
  93. package/dist/hooks/use-cortena-theme.js +49 -3
  94. package/dist/hooks/use-cortena-theme.js.map +1 -1
  95. package/dist/index.d.ts +17 -4
  96. package/dist/index.js +21 -8
  97. package/dist/markdown.d.ts +2 -1
  98. package/dist/markdown.js +2 -1
  99. package/package.json +16 -4
  100. package/src/agent-chat/a2ui-block.ts +118 -0
  101. package/src/agent-chat/agui-client.ts +405 -0
  102. package/src/agent-chat/bridge.ts +433 -0
  103. package/src/agent-chat/session.ts +392 -0
  104. package/src/agent-chat/store.ts +738 -0
  105. package/src/agent-chat/types.ts +213 -0
  106. package/src/components/admin-permissions/admin-permissions.tsx +130 -0
  107. package/src/components/admin-permissions/context.tsx +376 -0
  108. package/src/components/admin-permissions/index.tsx +32 -0
  109. package/src/components/admin-permissions/licence.tsx +84 -0
  110. package/src/components/admin-permissions/matrix.tsx +257 -0
  111. package/src/components/admin-permissions/members.tsx +204 -0
  112. package/src/components/admin-permissions/role-assignment.tsx +239 -0
  113. package/src/components/admin-permissions/roles.tsx +169 -0
  114. package/src/components/admin-permissions/types.ts +231 -0
  115. package/src/components/agent-chat-popup.tsx +289 -0
  116. package/src/components/agent-chat.tsx +843 -0
  117. package/src/components/app-shell.tsx +502 -0
  118. package/src/components/consent-screen.tsx +239 -0
  119. package/src/components/data-table/data-table.tsx +36 -0
  120. package/src/components/data-table/index.tsx +6 -1
  121. package/src/components/data-table/parts.tsx +223 -47
  122. package/src/components/data-table/types.ts +68 -0
  123. package/src/components/data-table/use-data-table.ts +152 -4
  124. package/src/components/data-table/use-server-source.ts +150 -12
  125. package/src/components/help-panel.tsx +765 -0
  126. package/src/components/login-screen.tsx +479 -0
  127. package/src/components/session-guard.tsx +1071 -0
  128. package/src/entries/agent-chat.ts +113 -0
  129. package/src/entries/core.ts +8 -0
  130. package/src/entries/data-table.ts +41 -0
  131. package/src/entries/markdown.ts +25 -0
  132. package/src/hooks/use-cortena-theme.ts +63 -4
  133. package/src/index.ts +6 -0
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2026 Ascendence AI Technology Pvt Ltd. All rights reserved.
2
+
3
+ This package is published for use with the Cortena platform by licensed
4
+ Cortena customers and partners only. No permission is granted to copy,
5
+ modify, redistribute or use it outside a Cortena deployment.
6
+
7
+ Provided as is, without warranty.
package/README.md CHANGED
@@ -28,18 +28,59 @@ consumer's bundler getting tree-shaking right:
28
28
 
29
29
  | entry | what it holds | what it costs |
30
30
  | --- | --- | --- |
31
- | `cortena-ui/core` | primitives, overlays, light form controls, `cn`, `useCortenaTheme` | nothing heavy |
31
+ | `cortena-ui/core` | primitives, overlays, light form controls, `AppShell`, `cn`, `useCortenaTheme` | nothing heavy |
32
32
  | `cortena-ui/form` | `Form`, `useForm`, `zodResolver`, date pickers, `Dropzone` | react-hook-form, zod, react-day-picker, react-dropzone |
33
33
  | `cortena-ui/data-table` | `DataTable` and its parts | TanStack Table and Virtual; exceljs on the first export |
34
34
  | `cortena-ui/chart` | `Chart`, themed Recharts primitives | an engine on the first chart drawn, not on import |
35
35
  | `cortena-ui/markdown` | `Markdown` | react-markdown, remark-gfm, rehype-sanitize |
36
36
  | `cortena-ui/a2ui` | the A2UI catalogue and renderer | most of the package, by design |
37
37
  | `cortena-ui/sortable-list` | `SortableList`, `SortableHandle`, `arrayMove` | dnd-kit |
38
+ | `cortena-ui/agent-chat` | `AgentChatPopup`, `AgentChat`, `createAguiAgentChatClient` | the A2UI catalogue, plus `@ag-ui/client` (rxjs, zod 3, uuid, protobuf) |
38
39
 
39
40
  Each component has exactly one home, so the barrel re-exports every entry
40
41
  without an ambiguous name. **Adding a component means adding it to its area
41
42
  entry in `src/entries/`, not to `src/index.ts`.**
42
43
 
44
+ `AgentChat` has its own entry for the same kind of reason: `@ag-ui/client` is
45
+ the run transport, and an extension that draws a button should not download a
46
+ chat stream it never opens. It is also **not re-exported from the root barrel**,
47
+ so `import { Button } from "cortena-ui"` cannot reach it however the consumer's
48
+ bundler behaves. `pnpm bundle:probe` measures it, and
49
+ `test/bundle-probe.test.mjs` fails if `@ag-ui/*` turns up in any other probe.
50
+
51
+ `@ag-ui/client` is an **optional peer**, not a dependency: an extension that
52
+ uses this entry installs it itself, at a version its own package.json names.
53
+
54
+ ```jsonc
55
+ "peerDependencies": { "@ag-ui/client": "^0.0.59" },
56
+ "peerDependenciesMeta": { "@ag-ui/client": { "optional": true } }
57
+ ```
58
+
59
+ As a dependency it was pinned to one exact build for the whole fleet, and an
60
+ app that also talks AG-UI directly — which is every app with a second agent
61
+ surface — resolved two copies. Two copies of an rxjs-based client is two event
62
+ pipelines and two zod registries, and the symptom is a subscription that never
63
+ fires. Optional, because only `createAguiAgentChatClient` imports it: `AgentChat`,
64
+ `AgentChatPopup` and `useAgentChat` are written against the `AgentChatClient`
65
+ interface, so an extension on another transport installs nothing.
66
+
67
+ And it is loaded **lazily**, inside `createAguiAgentChatClient`, by dynamic
68
+ import. A static import meant the optional peer was not optional at all: with
69
+ it uninstalled the whole `cortena-ui/agent-chat` module failed to evaluate, so
70
+ an extension on another transport could not draw `AgentChatPopup` either — a
71
+ package it had never heard of decided whether its chat rendered, and said so in
72
+ a resolution error. The entry now loads without the peer, and only a run needs
73
+ it; when it is absent the chat shows
74
+
75
+ > cortena-ui/agent-chat: the AG-UI transport needs the optional peer
76
+ > "@ag-ui/client". Install it (pnpm add @ag-ui/client), or pass your own
77
+ > AgentChatClient to AgentChat.
78
+
79
+ The specifier stays a literal, so a bundler still splits the transport into a
80
+ chunk of its own and `test/bundle-probe.test.mjs` still measures that the
81
+ agent-chat entry carries it. `test/agent-chat-peer.test.mjs` runs the built
82
+ entry in a child process with the peer made unresolvable.
83
+
43
84
  `SortableList` has its own entry for a specific reason rather than a stylistic
44
85
  one: `@dnd-kit/core` and `@dnd-kit/sortable` declare no `"sideEffects"` in their
45
86
  package.json, so a bundler must assume importing them does something observable
@@ -86,6 +127,113 @@ regression; `test/chart-bundle.test.tsx` says which line caused it.
86
127
  check in light, dark and system-dark; add an entry with every variant and
87
128
  size when adding a component.
88
129
 
130
+ ## Shell chrome
131
+
132
+ `AppShell` is the chrome every extension wears (§9 of
133
+ how-to-create-a-cortena-extension, audit rule P-10): the registered brand mark
134
+ and the extension name top left, the avatar menu — the only settings entry
135
+ point — top right, and one fixed bottom-right cluster holding the theme toggle
136
+ then the help button. `BrandMark`, `BottomRightCluster`, `ThemeToggle`,
137
+ `HelpButton` and `documentTitle` are exported alongside it.
138
+
139
+ The mark comes from `cortena-design/marks` by `extension.id`, which is the same
140
+ file the Apps tile, the MCP app card, the agent pop-up pill and the favicon
141
+ draw. Adding a mark, and generating the three favicon files from it with
142
+ `cortena-design-favicon`, are both in `../../CONSUMING.md`, "Shell chrome and
143
+ favicon".
144
+
145
+ `helpPanel` is a slot until `HelpPanel` lands (EXTBP-24); `help.source` is the
146
+ functional document the panel will read.
147
+
148
+ ## The agent pop-up
149
+
150
+ `AgentChatPopup` is the extension's own Cortena Agent, in the corner of every
151
+ screen (§17 of how-to-create-a-cortena-extension, audit rule P-25). It mounts in
152
+ `AppShell`'s `agentSlot`, so it stacks above the theme/help cluster rather than
153
+ beside it.
154
+
155
+ ```tsx
156
+ import { AppShell } from "cortena-ui";
157
+ import { AgentChatPopup, createAguiAgentChatClient } from "cortena-ui/agent-chat";
158
+
159
+ const extension = { id: "tasks", name: "Tasks" };
160
+
161
+ const client = createAguiAgentChatClient({
162
+ baseUrl: "/api/agent", // this extension's own gateway, not cortenacore
163
+ getToken: () => jwt, // the signed-in USER's JWT, never a service token
164
+ agentId: "tasks", // = the AgentTemplate slug = the extension id
165
+ });
166
+
167
+ <AppShell
168
+ extension={extension}
169
+ user={user}
170
+ agentSlot={<AgentChatPopup extension={extension} client={client} />}
171
+ />;
172
+ ```
173
+
174
+ A collapsed pill opens to a panel (420 x 640) and the panel header expands it to
175
+ full screen. **Cmd/Ctrl+J** toggles the pill and the panel; **Escape** steps full
176
+ screen -> panel -> pill, except while a run is streaming, where the composer
177
+ takes Escape to mean "stop the run". The chat is hidden rather than unmounted
178
+ while collapsed, so a run that started before the user collapsed it keeps
179
+ streaming and the pill shows an unread dot.
180
+
181
+ **Sessions are per window.** The current session key lives in `sessionStorage`
182
+ under a name scoped to the extension id, not in `localStorage`: opening the
183
+ extension in a second window starts a second session rather than adopting the
184
+ first window's. Every session stays listed and is resumable from any window, and
185
+ two windows on one session may both send — cortenacore serialises the runs. A
186
+ new key is minted client-side as `agent:<id>:new-<Date.now()>-<random36>` and
187
+ used immediately, so an in-flight first message is never lost.
188
+
189
+ ### What the extension must proxy
190
+
191
+ The pop-up talks to one origin, `baseUrl`, and every call carries the user's
192
+ JWT. An extension's gateway proxies these to cortenacore; nothing here holds a
193
+ service credential.
194
+
195
+ | method + path | what it is |
196
+ | --- | --- |
197
+ | `POST {base}/agui/run` | the run stream. AG-UI `RunAgentInput` in, `text/event-stream` out. `threadId` is the session key verbatim, prefix included |
198
+ | `POST {base}/agui/abort` | `{ runId, threadId }` |
199
+ | `POST {base}/agui/approval` | `{ id, decision, threadId }` — `allow-once`, `allow-always`, `deny`. A separate endpoint on purpose: a decision sent as state on a second `/agui/run` opens a second concurrent stream |
200
+ | `GET {base}/api/core/sessions/list` | the session drawer, filtered client-side to `agent:<id>:` |
201
+ | `GET {base}/api/core/chat/history` | `?sessionKey=&limit=100&offset=` |
202
+ | `POST {base}/tools/invoke` | only for an MCP App frame the host wires in — the app's own `tools/call`, scoped to its `mcp__<server>__` prefix |
203
+ | `GET /api/design-tokens` | only for an MCP App frame: the token stylesheet the sandboxed document themes itself from |
204
+
205
+ The stream ends by **closing the connection** after `RUN_FINISHED` or
206
+ `RUN_ERROR`. There is no `data: [DONE]` sentinel; emitting one makes the AG-UI
207
+ client error the subject, because it parses every `data:` line as JSON.
208
+ `: keepalive` comment frames are fine.
209
+
210
+ **Two server toggles must be on**, in every environment that runs an extension.
211
+ Both default to false, and while either is off its paths answer `404` before
212
+ authentication, which reads as "the agent does not exist":
213
+
214
+ ```
215
+ cortenacore.http.endpoints.agui.enabled = true
216
+ cortenacore.http.endpoints.controlPlane.enabled = true
217
+ ```
218
+
219
+ ### The MCP App frame
220
+
221
+ Rendering an extension's own screen means a sandboxed iframe, a `srcdoc` CSP, a
222
+ JSON-RPC postMessage bridge and a `tools/invoke` proxy holding a credential —
223
+ host concerns, not component ones. `renderToolCall` is the seam: it is handed
224
+ each tool call and the `ui://` resource its result carries, and a node it
225
+ returns replaces the default card.
226
+
227
+ ```tsx
228
+ <AgentChatPopup
229
+ extension={extension}
230
+ client={client}
231
+ renderToolCall={({ entry, mcpAppUri }) =>
232
+ mcpAppUri ? <McpAppFrame uri={mcpAppUri} result={entry.output} /> : undefined
233
+ }
234
+ />
235
+ ```
236
+
89
237
  ## Charts in tests
90
238
 
91
239
  Both chart engines size themselves from the parent box, and under jsdom every
@@ -2,12 +2,12 @@
2
2
  "use client";
3
3
  import { cn } from "../lib/cn.js";
4
4
  import { Alert, AlertDescription, AlertIcon, AlertTitle } from "../components/alert.js";
5
- import { Badge } from "../components/badge.js";
6
5
  import { Button } from "../components/button.js";
6
+ import { Input } from "../components/input.js";
7
+ import { Badge } from "../components/badge.js";
7
8
  import { ButtonLink } from "../components/button-link.js";
8
9
  import { Card, CardContent } from "../components/card.js";
9
10
  import { Checkbox } from "../components/checkbox.js";
10
- import { Input } from "../components/input.js";
11
11
  import { EmptyState } from "../components/empty-state.js";
12
12
  import { Field, FieldDescription, FieldError, FieldLabel } from "../components/field.js";
13
13
  import { Progress, ProgressLabel, ProgressValue } from "../components/progress.js";
@@ -0,0 +1,60 @@
1
+ "use client";
2
+ //#region src/agent-chat/a2ui-block.d.ts
3
+ /**
4
+ * A2UI blocks as the chat carries them.
5
+ *
6
+ * Ported from `cortena-shared/src/a2ui/block.ts` (Cortena monorepo, DESIGN-35).
7
+ * `cortena-shared` is not published, so the parts this package needs are copied
8
+ * rather than imported; keep the two in step when either changes.
9
+ *
10
+ * An agent draws UI by writing a ```a2ui fenced block, cortenacore strips it out
11
+ * of the prose, and it reaches the client on a channel of its own — a
12
+ * `CUSTOM cortena.a2ui` event on the AG-UI transport. Both the streaming bubble
13
+ * and the finished message carry the result here, so the bubble has exactly one
14
+ * thing to render and no markdown to parse.
15
+ *
16
+ * Nothing in this module knows about React: the `messages` array is handed to
17
+ * `<A2UIRenderer message={...} />` verbatim, and folding a surface, validating a
18
+ * component and drawing it are the renderer's job.
19
+ */
20
+ /** One block, as the transport delivers it. */
21
+ export interface A2UIChatBlock {
22
+ /**
23
+ * Accumulation key: the surface id the payload names, or a key private to the
24
+ * block when it names none. Two blocks with the same key are two pushes to one
25
+ * surface and fold together.
26
+ */
27
+ surfaceId: string;
28
+ /** A2UI messages, in arrival order. */
29
+ messages: unknown[];
30
+ /** Where the block came from: assistant text, or a `canvas` tool push. */
31
+ source?: "text" | "canvas";
32
+ /** Assistant message the block belonged to, when the transport knows it. */
33
+ messageId?: string;
34
+ /** Set when the fence never closed or its body did not parse. */
35
+ error?: string;
36
+ /** The raw fence body; only present with `error`. */
37
+ raw?: string;
38
+ }
39
+ /** Read one block out of an unknown wire value; `null` when it is not one. */
40
+ export declare function readA2UIBlock(value: unknown): A2UIChatBlock | null;
41
+ /**
42
+ * Pull the `a2ui` content parts out of a chat event message.
43
+ *
44
+ * A client that does not know the type ignores it, which is what lets the same
45
+ * message shape carry blocks to a reader that cannot draw them.
46
+ */
47
+ export declare function extractA2UIBlocks(message?: {
48
+ content?: Array<Record<string, unknown>>;
49
+ } | null): A2UIChatBlock[];
50
+ /**
51
+ * Fold blocks that share a surface id into one, concatenating their messages.
52
+ *
53
+ * An agent that streams a surface writes the shell, then the rows, then a
54
+ * correction — three fences naming one surface. The renderer folds a message
55
+ * list onto a surface, so handing it the concatenation renders the finished
56
+ * surface; handing it three separate blocks would draw the shell three times.
57
+ */
58
+ export declare function foldA2UIBlocks(blocks: readonly A2UIChatBlock[]): A2UIChatBlock[];
59
+ //#endregion
60
+ //# sourceMappingURL=a2ui-block.d.ts.map
@@ -0,0 +1,69 @@
1
+ "use client";
2
+ //#region src/agent-chat/a2ui-block.ts
3
+ function isRecord(value) {
4
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5
+ }
6
+ /** Read one block out of an unknown wire value; `null` when it is not one. */
7
+ function readA2UIBlock(value) {
8
+ if (!isRecord(value)) return null;
9
+ const messages = Array.isArray(value.messages) ? value.messages : [];
10
+ const surfaceId = typeof value.surfaceId === "string" && value.surfaceId ? value.surfaceId : "@default";
11
+ const error = typeof value.error === "string" && value.error ? value.error : void 0;
12
+ if (messages.length === 0 && !error) return null;
13
+ return {
14
+ surfaceId,
15
+ messages,
16
+ ...value.source === "canvas" || value.source === "text" ? { source: value.source } : {},
17
+ ...typeof value.messageId === "string" && value.messageId ? { messageId: value.messageId } : {},
18
+ ...error ? { error } : {},
19
+ ...typeof value.raw === "string" ? { raw: value.raw } : {}
20
+ };
21
+ }
22
+ /**
23
+ * Pull the `a2ui` content parts out of a chat event message.
24
+ *
25
+ * A client that does not know the type ignores it, which is what lets the same
26
+ * message shape carry blocks to a reader that cannot draw them.
27
+ */
28
+ function extractA2UIBlocks(message) {
29
+ const blocks = [];
30
+ for (const part of message?.content ?? []) {
31
+ if (!isRecord(part) || part.type !== "a2ui") continue;
32
+ const block = readA2UIBlock(part.a2ui ?? part);
33
+ if (block) blocks.push(block);
34
+ }
35
+ return foldA2UIBlocks(blocks);
36
+ }
37
+ /**
38
+ * Fold blocks that share a surface id into one, concatenating their messages.
39
+ *
40
+ * An agent that streams a surface writes the shell, then the rows, then a
41
+ * correction — three fences naming one surface. The renderer folds a message
42
+ * list onto a surface, so handing it the concatenation renders the finished
43
+ * surface; handing it three separate blocks would draw the shell three times.
44
+ */
45
+ function foldA2UIBlocks(blocks) {
46
+ const byId = /* @__PURE__ */ new Map();
47
+ const order = [];
48
+ for (const block of blocks) {
49
+ const existing = byId.get(block.surfaceId);
50
+ if (!existing) {
51
+ byId.set(block.surfaceId, {
52
+ ...block,
53
+ messages: [...block.messages]
54
+ });
55
+ order.push(block.surfaceId);
56
+ continue;
57
+ }
58
+ existing.messages.push(...block.messages);
59
+ if (block.error && !existing.error) {
60
+ existing.error = block.error;
61
+ if (block.raw !== void 0) existing.raw = block.raw;
62
+ }
63
+ }
64
+ return order.map((id) => byId.get(id)).filter(Boolean);
65
+ }
66
+ //#endregion
67
+ export { extractA2UIBlocks, foldA2UIBlocks, readA2UIBlock };
68
+
69
+ //# sourceMappingURL=a2ui-block.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"a2ui-block.js","names":[],"sources":["../../src/agent-chat/a2ui-block.ts"],"sourcesContent":["/**\n * A2UI blocks as the chat carries them.\n *\n * Ported from `cortena-shared/src/a2ui/block.ts` (Cortena monorepo, DESIGN-35).\n * `cortena-shared` is not published, so the parts this package needs are copied\n * rather than imported; keep the two in step when either changes.\n *\n * An agent draws UI by writing a ```a2ui fenced block, cortenacore strips it out\n * of the prose, and it reaches the client on a channel of its own — a\n * `CUSTOM cortena.a2ui` event on the AG-UI transport. Both the streaming bubble\n * and the finished message carry the result here, so the bubble has exactly one\n * thing to render and no markdown to parse.\n *\n * Nothing in this module knows about React: the `messages` array is handed to\n * `<A2UIRenderer message={...} />` verbatim, and folding a surface, validating a\n * component and drawing it are the renderer's job.\n */\n\n/** One block, as the transport delivers it. */\nexport interface A2UIChatBlock {\n /**\n * Accumulation key: the surface id the payload names, or a key private to the\n * block when it names none. Two blocks with the same key are two pushes to one\n * surface and fold together.\n */\n surfaceId: string;\n /** A2UI messages, in arrival order. */\n messages: unknown[];\n /** Where the block came from: assistant text, or a `canvas` tool push. */\n source?: \"text\" | \"canvas\";\n /** Assistant message the block belonged to, when the transport knows it. */\n messageId?: string;\n /** Set when the fence never closed or its body did not parse. */\n error?: string;\n /** The raw fence body; only present with `error`. */\n raw?: string;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Read one block out of an unknown wire value; `null` when it is not one. */\nexport function readA2UIBlock(value: unknown): A2UIChatBlock | null {\n if (!isRecord(value)) {\n return null;\n }\n const messages = Array.isArray(value.messages) ? value.messages : [];\n const surfaceId =\n typeof value.surfaceId === \"string\" && value.surfaceId ? value.surfaceId : \"@default\";\n const error = typeof value.error === \"string\" && value.error ? value.error : undefined;\n if (messages.length === 0 && !error) {\n return null;\n }\n return {\n surfaceId,\n messages,\n ...(value.source === \"canvas\" || value.source === \"text\" ? { source: value.source } : {}),\n ...(typeof value.messageId === \"string\" && value.messageId\n ? { messageId: value.messageId }\n : {}),\n ...(error ? { error } : {}),\n ...(typeof value.raw === \"string\" ? { raw: value.raw } : {}),\n };\n}\n\n/**\n * Pull the `a2ui` content parts out of a chat event message.\n *\n * A client that does not know the type ignores it, which is what lets the same\n * message shape carry blocks to a reader that cannot draw them.\n */\nexport function extractA2UIBlocks(\n message?: { content?: Array<Record<string, unknown>> } | null,\n): A2UIChatBlock[] {\n const blocks: A2UIChatBlock[] = [];\n for (const part of message?.content ?? []) {\n if (!isRecord(part) || part.type !== \"a2ui\") {\n continue;\n }\n const block = readA2UIBlock(part.a2ui ?? part);\n if (block) {\n blocks.push(block);\n }\n }\n return foldA2UIBlocks(blocks);\n}\n\n/**\n * Fold blocks that share a surface id into one, concatenating their messages.\n *\n * An agent that streams a surface writes the shell, then the rows, then a\n * correction — three fences naming one surface. The renderer folds a message\n * list onto a surface, so handing it the concatenation renders the finished\n * surface; handing it three separate blocks would draw the shell three times.\n */\nexport function foldA2UIBlocks(blocks: readonly A2UIChatBlock[]): A2UIChatBlock[] {\n const byId = new Map<string, A2UIChatBlock>();\n const order: string[] = [];\n for (const block of blocks) {\n const existing = byId.get(block.surfaceId);\n if (!existing) {\n byId.set(block.surfaceId, { ...block, messages: [...block.messages] });\n order.push(block.surfaceId);\n continue;\n }\n existing.messages.push(...block.messages);\n // The first failure is the one worth showing: later ones are usually the\n // same malformed payload arriving again.\n if (block.error && !existing.error) {\n existing.error = block.error;\n if (block.raw !== undefined) {\n existing.raw = block.raw;\n }\n }\n }\n return order.map((id) => byId.get(id)!).filter(Boolean);\n}\n"],"mappings":";;AAsCA,SAAS,SAAS,OAAkD;CAClE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;;AAGA,SAAgB,cAAc,OAAsC;CAClE,IAAI,CAAC,SAAS,KAAK,GACjB,OAAO;CAET,MAAM,WAAW,MAAM,QAAQ,MAAM,QAAQ,IAAI,MAAM,WAAW,CAAC;CACnE,MAAM,YACJ,OAAO,MAAM,cAAc,YAAY,MAAM,YAAY,MAAM,YAAY;CAC7E,MAAM,QAAQ,OAAO,MAAM,UAAU,YAAY,MAAM,QAAQ,MAAM,QAAQ,KAAA;CAC7E,IAAI,SAAS,WAAW,KAAK,CAAC,OAC5B,OAAO;CAET,OAAO;EACL;EACA;EACA,GAAI,MAAM,WAAW,YAAY,MAAM,WAAW,SAAS,EAAE,QAAQ,MAAM,OAAO,IAAI,CAAC;EACvF,GAAI,OAAO,MAAM,cAAc,YAAY,MAAM,YAC7C,EAAE,WAAW,MAAM,UAAU,IAC7B,CAAC;EACL,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;EACzB,GAAI,OAAO,MAAM,QAAQ,WAAW,EAAE,KAAK,MAAM,IAAI,IAAI,CAAC;CAC5D;AACF;;;;;;;AAQA,SAAgB,kBACd,SACiB;CACjB,MAAM,SAA0B,CAAC;CACjC,KAAK,MAAM,QAAQ,SAAS,WAAW,CAAC,GAAG;EACzC,IAAI,CAAC,SAAS,IAAI,KAAK,KAAK,SAAS,QACnC;EAEF,MAAM,QAAQ,cAAc,KAAK,QAAQ,IAAI;EAC7C,IAAI,OACF,OAAO,KAAK,KAAK;CAErB;CACA,OAAO,eAAe,MAAM;AAC9B;;;;;;;;;AAUA,SAAgB,eAAe,QAAmD;CAChF,MAAM,uBAAO,IAAI,IAA2B;CAC5C,MAAM,QAAkB,CAAC;CACzB,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,WAAW,KAAK,IAAI,MAAM,SAAS;EACzC,IAAI,CAAC,UAAU;GACb,KAAK,IAAI,MAAM,WAAW;IAAE,GAAG;IAAO,UAAU,CAAC,GAAG,MAAM,QAAQ;GAAE,CAAC;GACrE,MAAM,KAAK,MAAM,SAAS;GAC1B;EACF;EACA,SAAS,SAAS,KAAK,GAAG,MAAM,QAAQ;EAGxC,IAAI,MAAM,SAAS,CAAC,SAAS,OAAO;GAClC,SAAS,QAAQ,MAAM;GACvB,IAAI,MAAM,QAAQ,KAAA,GAChB,SAAS,MAAM,MAAM;EAEzB;CACF;CACA,OAAO,MAAM,KAAK,OAAO,KAAK,IAAI,EAAE,CAAE,CAAC,CAAC,OAAO,OAAO;AACxD"}
@@ -0,0 +1,40 @@
1
+ "use client";
2
+ import { AgentChatClient } from "./types.js";
3
+ //#region src/agent-chat/agui-client.d.ts
4
+ export interface CreateAguiAgentChatClientOptions {
5
+ /**
6
+ * Origin the AG-UI and control-plane paths hang off, no trailing slash.
7
+ * For an extension this is its own gateway, which proxies to cortenacore.
8
+ */
9
+ baseUrl: string;
10
+ /** The signed-in USER's JWT. Read on every request, never captured once. */
11
+ getToken: () => string;
12
+ /** The agent template slug — the extension id. Sessions are `agent:<id>:…`. */
13
+ agentId: string;
14
+ fetchImpl?: typeof fetch;
15
+ /** Frontend tools advertised on every run; AG-UI requires a description. */
16
+ tools?: Array<{
17
+ name: string;
18
+ description: string;
19
+ parameters?: unknown;
20
+ }>;
21
+ /** Backoff before re-attaching a dropped stream. */
22
+ reconnectDelays?: readonly number[];
23
+ maxReconnectAttempts?: number;
24
+ sleep?: (ms: number) => Promise<void>;
25
+ now?: () => number;
26
+ }
27
+ /**
28
+ * An `AgentChatClient` that streams over AG-UI and reads sessions and history
29
+ * over the REST control plane.
30
+ *
31
+ * ```tsx
32
+ * const client = React.useMemo(
33
+ * () => createAguiAgentChatClient({ baseUrl: "/api/agent", getToken: () => jwt, agentId: "tasks" }),
34
+ * [jwt],
35
+ * );
36
+ * ```
37
+ */
38
+ export declare function createAguiAgentChatClient(options: CreateAguiAgentChatClientOptions): AgentChatClient;
39
+ //#endregion
40
+ //# sourceMappingURL=agui-client.d.ts.map
@@ -0,0 +1,251 @@
1
+ "use client";
2
+ import { AgentChatBridge } from "./bridge.js";
3
+ //#region src/agent-chat/agui-client.ts
4
+ const DEFAULT_RECONNECT_DELAYS = [
5
+ 500,
6
+ 1e3,
7
+ 2e3,
8
+ 5e3,
9
+ 1e4
10
+ ];
11
+ const DEFAULT_MAX_RECONNECT_ATTEMPTS = 5;
12
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
13
+ /**
14
+ * What a relative `baseUrl` resolves against.
15
+ *
16
+ * The browser's own origin, and a placeholder under Node — where this module
17
+ * is exercised by tests with a `fetchImpl` stub and there is no origin to
18
+ * speak of. The placeholder never reaches the network: the stub sees the
19
+ * resolved string, and `/api/core/...` is what it asserts on.
20
+ */
21
+ function requestBase() {
22
+ return typeof location === "undefined" ? "http://cortena.invalid" : location.origin;
23
+ }
24
+ const MISSING_AGUI_PEER = "cortena-ui/agent-chat: the AG-UI transport needs the optional peer \"@ag-ui/client\". Install it (pnpm add @ag-ui/client), or pass your own AgentChatClient to AgentChat.";
25
+ /**
26
+ * `@ag-ui/client`, on demand.
27
+ *
28
+ * The specifier is a literal so a bundler still sees the dependency and puts
29
+ * it in a chunk of its own — `test/bundle-probe.test.mjs` measures that the
30
+ * agent-chat entry carries it. What changes is *when* it is evaluated, and
31
+ * what happens when it is not there at all.
32
+ */
33
+ async function loadHttpAgent() {
34
+ try {
35
+ return (await import("@ag-ui/client")).HttpAgent;
36
+ } catch (cause) {
37
+ throw new Error(MISSING_AGUI_PEER, { cause });
38
+ }
39
+ }
40
+ var AguiAgentChatClient = class {
41
+ agentId;
42
+ opts;
43
+ delays;
44
+ maxAttempts;
45
+ sleep;
46
+ chatSinks = /* @__PURE__ */ new Set();
47
+ toolSinks = /* @__PURE__ */ new Set();
48
+ approvalSinks = /* @__PURE__ */ new Set();
49
+ active = null;
50
+ /** The optional peer, requested when the client is created and awaited per run. */
51
+ httpAgent;
52
+ constructor(opts) {
53
+ this.opts = opts;
54
+ this.agentId = opts.agentId;
55
+ this.delays = opts.reconnectDelays ?? DEFAULT_RECONNECT_DELAYS;
56
+ this.maxAttempts = opts.maxReconnectAttempts ?? DEFAULT_MAX_RECONNECT_ATTEMPTS;
57
+ this.sleep = opts.sleep ?? defaultSleep;
58
+ this.httpAgent = loadHttpAgent();
59
+ this.httpAgent.catch(() => {});
60
+ }
61
+ onChat(sink) {
62
+ this.chatSinks.add(sink);
63
+ return () => this.chatSinks.delete(sink);
64
+ }
65
+ onTool(sink) {
66
+ this.toolSinks.add(sink);
67
+ return () => this.toolSinks.delete(sink);
68
+ }
69
+ onApproval(sink) {
70
+ this.approvalSinks.add(sink);
71
+ return () => this.approvalSinks.delete(sink);
72
+ }
73
+ get runUrl() {
74
+ return `${this.opts.baseUrl}/agui/run`;
75
+ }
76
+ get abortUrl() {
77
+ return `${this.opts.baseUrl}/agui/abort`;
78
+ }
79
+ get approvalUrl() {
80
+ return `${this.opts.baseUrl}/agui/approval`;
81
+ }
82
+ headers() {
83
+ return {
84
+ authorization: `Bearer ${this.opts.getToken()}`,
85
+ "content-type": "application/json"
86
+ };
87
+ }
88
+ get fetch() {
89
+ return this.opts.fetchImpl ?? globalThis.fetch;
90
+ }
91
+ /** `sessions.list` and `chat.history` over the REST control plane. */
92
+ async request(method, params) {
93
+ const path = `${this.opts.baseUrl}/api/core/${method.split(".").join("/")}`;
94
+ const readOnly = method === "sessions.list" || method === "chat.history";
95
+ const url = new URL(path, requestBase());
96
+ if (readOnly) {
97
+ for (const [key, value] of Object.entries(params)) if (value !== void 0 && value !== null) url.searchParams.set(key, String(value));
98
+ }
99
+ const response = await this.fetch(url.toString(), {
100
+ method: readOnly ? "GET" : "POST",
101
+ headers: this.headers(),
102
+ ...readOnly ? {} : { body: JSON.stringify(params) }
103
+ });
104
+ if (!response.ok) throw new Error(await readError(response, method));
105
+ return await response.json();
106
+ }
107
+ async send(input) {
108
+ this.startRun(input);
109
+ }
110
+ async startRun(input) {
111
+ const bridge = new AgentChatBridge({
112
+ runId: input.runId,
113
+ sessionKey: input.sessionKey,
114
+ chat: (event) => this.emit(this.chatSinks, event),
115
+ tool: (event) => this.emit(this.toolSinks, event),
116
+ approval: (event) => this.emit(this.approvalSinks, event),
117
+ ...this.opts.now ? { now: this.opts.now } : {}
118
+ });
119
+ const forwarded = {
120
+ ...input.thinking ? { thinking: input.thinking } : {},
121
+ ...input.attachments?.length ? { attachments: input.attachments } : {},
122
+ ...input.model ? { model: input.model } : {},
123
+ ...input.a2uiAction ? { a2uiAction: input.a2uiAction } : {},
124
+ ...input.mcpAppAction ? { mcpAppAction: input.mcpAppAction } : {}
125
+ };
126
+ let Agent;
127
+ try {
128
+ Agent = await this.httpAgent;
129
+ } catch (err) {
130
+ bridge.fail(err instanceof Error ? err.message : MISSING_AGUI_PEER);
131
+ return;
132
+ }
133
+ let attempt = 0;
134
+ let resume = false;
135
+ for (;;) {
136
+ const runId = resume ? `${input.runId}:resume-${attempt}` : input.runId;
137
+ const agent = new Agent({
138
+ url: this.runUrl,
139
+ headers: this.headers(),
140
+ threadId: input.sessionKey,
141
+ initialMessages: resume ? [] : [{
142
+ id: input.runId,
143
+ role: "user",
144
+ content: input.message
145
+ }],
146
+ ...this.opts.fetchImpl ? { fetch: this.opts.fetchImpl } : {}
147
+ });
148
+ this.active = {
149
+ runId: input.runId,
150
+ sessionKey: input.sessionKey,
151
+ bridge,
152
+ agent,
153
+ aborted: this.active?.aborted === true && this.active.runId === input.runId
154
+ };
155
+ try {
156
+ await agent.runAgent({
157
+ runId,
158
+ ...this.opts.tools?.length ? { tools: this.opts.tools } : {},
159
+ forwardedProps: {
160
+ ...forwarded,
161
+ ...resume ? { resumeRunId: input.runId } : {}
162
+ }
163
+ }, { onEvent: ({ event }) => bridge.handleEvent(event) });
164
+ return;
165
+ } catch (err) {
166
+ if (bridge.finished || this.active?.aborted) return;
167
+ attempt += 1;
168
+ if (attempt > this.maxAttempts) {
169
+ bridge.fail(err instanceof Error ? err.message : "The agent stream failed");
170
+ return;
171
+ }
172
+ await this.sleep(this.delays[Math.min(attempt - 1, this.delays.length - 1)] ?? 1e3);
173
+ if (bridge.finished || this.active?.aborted) return;
174
+ resume = true;
175
+ }
176
+ }
177
+ }
178
+ async abort(params) {
179
+ const active = this.active;
180
+ if (active) {
181
+ active.aborted = true;
182
+ try {
183
+ active.agent.abortRun();
184
+ } catch {}
185
+ }
186
+ const runId = params?.runId ?? active?.runId;
187
+ const threadId = params?.sessionKey ?? active?.sessionKey;
188
+ if (!runId || !threadId) return;
189
+ try {
190
+ await this.fetch(this.abortUrl, {
191
+ method: "POST",
192
+ headers: this.headers(),
193
+ body: JSON.stringify({
194
+ runId,
195
+ threadId
196
+ })
197
+ });
198
+ } catch {}
199
+ }
200
+ /**
201
+ * Answer one approval, on the thread and run it arrived on.
202
+ *
203
+ * `this.active` is deliberately not consulted. An approval pauses a run, and
204
+ * a user is free to open another session from the drawer or start another
205
+ * turn while it waits — at which point `active.sessionKey` is a different
206
+ * conversation and "allow" would be sent against it. The request carried its
207
+ * own `{ runId, sessionKey }`; those are what go on the wire.
208
+ */
209
+ async resolveApproval(id, decision, context) {
210
+ const response = await this.fetch(this.approvalUrl, {
211
+ method: "POST",
212
+ headers: this.headers(),
213
+ body: JSON.stringify({
214
+ id,
215
+ decision,
216
+ threadId: context.sessionKey,
217
+ runId: context.runId
218
+ })
219
+ });
220
+ if (!response.ok) throw new Error(await readError(response, "approval"));
221
+ }
222
+ emit(sinks, event) {
223
+ for (const sink of [...sinks]) sink(event);
224
+ }
225
+ };
226
+ /**
227
+ * An `AgentChatClient` that streams over AG-UI and reads sessions and history
228
+ * over the REST control plane.
229
+ *
230
+ * ```tsx
231
+ * const client = React.useMemo(
232
+ * () => createAguiAgentChatClient({ baseUrl: "/api/agent", getToken: () => jwt, agentId: "tasks" }),
233
+ * [jwt],
234
+ * );
235
+ * ```
236
+ */
237
+ function createAguiAgentChatClient(options) {
238
+ return new AguiAgentChatClient(options);
239
+ }
240
+ async function readError(response, method) {
241
+ try {
242
+ const body = await response.json();
243
+ if (body?.error?.message) return body.error.message;
244
+ } catch {}
245
+ if (response.status === 404) return `${method} is unavailable (404). Check that the AG-UI and control-plane endpoints are enabled.`;
246
+ return `${method} failed (${response.status})`;
247
+ }
248
+ //#endregion
249
+ export { MISSING_AGUI_PEER, createAguiAgentChatClient };
250
+
251
+ //# sourceMappingURL=agui-client.js.map