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
package/CHANGELOG.md ADDED
@@ -0,0 +1,107 @@
1
+ # cortena-ui
2
+
3
+ Notable changes per release. Versions before 1.6.0 are recorded in the git log
4
+ and in `../../CONSUMING.md`; this file starts where the changelog does.
5
+
6
+ ## 1.6.0
7
+
8
+ ### Added
9
+
10
+ - **`cortena-ui/agent-chat`: one plain progress line per tool call** (SKILLS-9).
11
+ Each AG-UI tool call becomes a sentence under the assistant turn — a spinner
12
+ while it runs, a tick when it lands, a cross and the error envelope's message
13
+ when it fails, and the elapsed time on anything still running after eight
14
+ seconds. The list is labelled `Progress` and is deliberately NOT a live
15
+ region of its own: it is drawn inside the transcript, which is already
16
+ `role="log" aria-live="polite"`.
17
+ - `agent-chat/step-label.ts`: `stepLabel(toolName, args, phase)`,
18
+ `confirmationRequiredLabel(args)` and `humaniseId(id)` — the wording map for
19
+ `engram_search`, `engram_describe` and `broker_invoke`, pure and with no
20
+ dependency on the result.
21
+ - `useAgentChat` gains `steps: AgentChatStep[]`, paired by AG-UI
22
+ `toolCallId`. A `409 confirmation_required` result relabels the step rather
23
+ than failing it; an aborted or errored run ends every step still running.
24
+ - Steps are kept in `sessionStorage` beside the session key, so a collapsed
25
+ pop-up, a reload or a remount mid-run does not lose them —
26
+ `chat.history` cannot bring them back, because the server never sees them.
27
+ - New exports: `AgentSteps`, `AgentStepsProps`, `AgentChatStep`,
28
+ `AgentStepPhase`, `stepLabel`, `confirmationRequiredLabel`, `humaniseId`,
29
+ `genericLabel`, `isGenericLabel`, `truncate`, `readResultMetadata`,
30
+ `AgentToolResultMetadata`, `AgentToolResultMeta`, `readStoredSteps`,
31
+ `writeStoredSteps`, `serialiseSteps`, `stepsStorageKey`, `STEP_LIMIT`.
32
+
33
+ - **The outcome of a tool call is read from `TOOL_CALL_RESULT.metadata`**
34
+ (SKILLS-9 review). `AgentChatBridge` forwards the metadata into
35
+ `AgentToolEvent.data.metadata`, and the store decides from
36
+ `{ toolName, isError, meta?: { status, code, message } }` — an error with
37
+ `meta.code === "confirmation_required"` is the broker asking a question, any
38
+ other error is worded from `meta.message` or the first 200 characters of the
39
+ result text, and everything else is done. `content` is never parsed for an
40
+ outcome: a Cortena tool result is `{ content: [...], details }`, so the
41
+ `ok: false` the client used to look for was never at the top level and every
42
+ failed call was drawn as a success.
43
+
44
+ - **A step always stops spinning, and never claims more than it knows.** A
45
+ plain `RUN_FINISHED` marks every open step done with its label untouched (the
46
+ run succeeded, so the call did); an abort marks them `stopped` — a new,
47
+ neutral status, because the user asked for it and a red cross reads as a
48
+ fault they caused; and a step restored from storage as `running` or `paused`
49
+ comes back `stopped` with no message, because the page went away, not the
50
+ tool call. A `tool.start` for a step that has already ended is a reconnect
51
+ replay and is ignored.
52
+
53
+ - **An interrupt is not a success** (SKILLS-9 review 2). `RUN_FINISHED` with
54
+ `outcome.type === "interrupt"` is cortenacore pausing the run for an approval
55
+ or a frontend tool with the tool call STILL OPEN, and ticking it told the user
56
+ a write had gone through while the card asking their permission for it was on
57
+ the screen. `AgentChatEvent` carries `interrupted`, and such a step becomes
58
+ `paused` — a second neutral status, "Waiting for your approval", no spinner
59
+ and no elapsed counter. The result on the continuation resolves it; a `Deny`
60
+ marks it `stopped`. A stored `status` is validated against the known set on
61
+ the way back in, and anything else is `stopped`.
62
+
63
+ - **A late result cannot resurrect a dropped step** (SKILLS-9 review 2). A
64
+ runaway turn pushes its first calls past `STEP_LIMIT`, and their results
65
+ arrive afterwards; the store keeps the last 100 dropped ids and ignores them
66
+ rather than appending a `Working: tool` line for the oldest call of the turn
67
+ at the bottom of the strip.
68
+
69
+ - **`isError` is believed from either witness** (SKILLS-9 review 2). A
70
+ `tool.error` whose metadata arrived without the field was read as a success.
71
+
72
+ - **The id and the tool name are bounded** (SKILLS-9 review 2) — 80 characters,
73
+ in the stored projection and on `data-tool`. Neither is written by this
74
+ package, and 9 kB of either filled the 64 kB slot on its own.
75
+
76
+ - **Bounded and sanitised labels.** Every interpolated string — an operation id,
77
+ a tool name, a server message — goes through `truncate`, which strips
78
+ `\p{Cf}` and `\p{Cc}` before the length check, so a bidi override cannot
79
+ reverse a line and 8 kB of catalogue id cannot fill the strip.
80
+
81
+ - **Only a projection is persisted.** `id`, `toolName`, `label`, `status`,
82
+ `startedAt`, `endedAt`, `errorMessage` — never `args`, which are the user's
83
+ data. The write is debounced 250 ms and flushed on unmount, the list is capped
84
+ at `STEP_LIMIT` in the reducer, and the slot is capped at 64 kB, oldest first.
85
+
86
+ - **Accessibility.** The elapsed counter is `aria-hidden` (it changed once a
87
+ second inside a live region), a running step is held out of the announcement
88
+ until its arguments have produced a real sentence rather than reading out
89
+ "Working: broker_invoke" and correcting itself, and the label span is keyed on
90
+ the words, so a re-word is a node replacement rather than a mutated text node
91
+ some screen readers never announce.
92
+
93
+ ### Unchanged
94
+
95
+ - No change to AG-UI protocol handling beyond forwarding
96
+ `TOOL_CALL_RESULT.metadata`, which the bridge already parsed and dropped.
97
+ Streamed reasoning keeps the fix that stops a later event wiping it, now with
98
+ the re-attach rewind covered by a test.
99
+ - `agent-chat` remains the only entry that can reach `@ag-ui/client`; every
100
+ other bundle probe is unchanged.
101
+
102
+ ### Bundle
103
+
104
+ - `agent-chat` grows **+6.8 kB** total (2,738.6 → 2,745.4 kB) and **+5.9 kB**
105
+ eager (843.4 → 849.3 kB): the wording map, the step reducer and the strip.
106
+ Not "unmoved" — it is a new surface, and it costs what it costs. Every other
107
+ entry is byte-identical, and `CONSUMING.md`'s table carries the numbers.
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,197 @@ 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
+ ### Progress steps
190
+
191
+ Every AG-UI tool call becomes **one plain sentence** under the assistant turn,
192
+ streamed as it happens: a spinner while it runs, a tick and muted text when it
193
+ lands, a cross and the failure's `message` when it fails, and a neutral
194
+ "Stopped" when the user pressed Stop. A step still running after eight seconds
195
+ appends the time it has taken. The list is an `aria-live="polite"` region, so a
196
+ screen reader hears the run rather than silence — the seconds are `aria-hidden`
197
+ (they change every second) and a step is held out of the announcement until its
198
+ arguments have produced a real sentence.
199
+
200
+ This is not the tool strip at the foot of the panel. The strip is the record —
201
+ tool name, arguments, output, the `ui://` resource `renderToolCall` mounts an
202
+ MCP App from — and reading it means knowing what `broker_invoke` is. The steps
203
+ are for the person waiting.
204
+
205
+ The wording is `stepLabel(toolName, args, phase)` in `agent-chat/step-label.ts`,
206
+ a pure function with no network and no result:
207
+
208
+ | the call | the line |
209
+ | --- | --- |
210
+ | `engram_search {intent}` | Looking for a way to \<intent\>, truncated at 80 characters |
211
+ | `engram_describe {id: "skill.…"}` | Reading skill: \<the call's `title`, or the id's last segment\> |
212
+ | `engram_describe {id}` | Checking how to \<id as words: `tasks.task.list` → "tasks task list"\> |
213
+ | `broker_invoke {operation, confirmed: true}` | Doing: \<operation\>, then Done: \<operation\> |
214
+ | `broker_invoke {operation}` | Fetching \<operation\> |
215
+ | anything else, or arguments not yet arrived | Working: \<tool name\> |
216
+
217
+ A step that finished on that last line is re-worded if its arguments arrive
218
+ late; a step that already has a real sentence keeps it.
219
+
220
+ Two of those need saying out loud. **An unconfirmed invoke is worded as a
221
+ read** — "Fetching …" — because the client cannot know read from write before
222
+ the reply comes back; only the catalogue knows, and the model only sets
223
+ `confirmed` once the user has said yes. And a result whose code is
224
+ `confirmation_required` is **not** drawn as a failure: the step keeps its tick
225
+ and is relabelled "Needs your confirmation: \<operation\>", which is what the
226
+ broker is actually saying.
227
+
228
+ Every string interpolated into a label — an intent, a title, an operation id, a
229
+ tool name, a server's message — is bounded at 80 characters (200 for a failure
230
+ message) and stripped of control and format characters first, so a catalogue
231
+ entry cannot fill the strip and a U+202E cannot reverse it.
232
+
233
+ #### How a step knows it failed
234
+
235
+ **`TOOL_CALL_RESULT.metadata`, and nothing else.** The contract cortenacore
236
+ emits, and the only thing this client reads:
237
+
238
+ ```jsonc
239
+ metadata: {
240
+ toolName: "broker_invoke",
241
+ isError: true,
242
+ meta: { status: 409, code: "confirmation_required", message: "Set confirmed=true." }
243
+ }
244
+ ```
245
+
246
+ `meta` is optional; where it is absent, only `isError` can be trusted. An error
247
+ with `meta.code === "confirmation_required"` is the question above; any other
248
+ error is worded from `meta.message`, or from the first 200 characters of the
249
+ result text when there is none.
250
+
251
+ `content` is **never** parsed for an outcome. It is the result as the model sees
252
+ it — for a Cortena tool, `{ content: [{ type: "text", text }], details }` — and
253
+ looking for an `ok: false` in it is guessing at a shape nobody promised.
254
+
255
+ Steps live in `useAgentChat`'s `steps`, are cleared when the next run starts,
256
+ and are kept in `sessionStorage` beside the session key. That last part is not
257
+ decoration: `chat.history` has no steps in it — the server never sees them — so
258
+ a collapsed pop-up, a reload or a host remount mid-run would otherwise lose the
259
+ whole strip. `AgentSteps` is exported for a host that wants to draw them
260
+ somewhere else.
261
+
262
+ #### What is kept, and where
263
+
264
+ Steps are held in memory with their `args`, and **only a projection is
265
+ persisted**: `id`, `toolName`, `label`, `status`, `startedAt`, `endedAt`,
266
+ `errorMessage`. A call's arguments are the user's data and do not belong in a
267
+ store every script on the origin can read; the label is already derived from
268
+ them. The write is debounced by 250 ms and flushed on unmount, the list is
269
+ capped at 50 steps, and the slot is capped at 64 kB with the oldest dropped
270
+ first. A step restored with `status: "running"` comes back as "Interrupted." —
271
+ the run that owned it is gone, and no result will ever arrive.
272
+
273
+ ### What the extension must proxy
274
+
275
+ The pop-up talks to one origin, `baseUrl`, and every call carries the user's
276
+ JWT. An extension's gateway proxies these to cortenacore; nothing here holds a
277
+ service credential.
278
+
279
+ | method + path | what it is |
280
+ | --- | --- |
281
+ | `POST {base}/agui/run` | the run stream. AG-UI `RunAgentInput` in, `text/event-stream` out. `threadId` is the session key verbatim, prefix included |
282
+ | `POST {base}/agui/abort` | `{ runId, threadId }` |
283
+ | `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 |
284
+ | `GET {base}/api/core/sessions/list` | the session drawer, filtered client-side to `agent:<id>:` |
285
+ | `GET {base}/api/core/chat/history` | `?sessionKey=&limit=100&offset=` |
286
+ | `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 |
287
+ | `GET /api/design-tokens` | only for an MCP App frame: the token stylesheet the sandboxed document themes itself from |
288
+
289
+ The stream ends by **closing the connection** after `RUN_FINISHED` or
290
+ `RUN_ERROR`. There is no `data: [DONE]` sentinel; emitting one makes the AG-UI
291
+ client error the subject, because it parses every `data:` line as JSON.
292
+ `: keepalive` comment frames are fine.
293
+
294
+ **Two server toggles must be on**, in every environment that runs an extension.
295
+ Both default to false, and while either is off its paths answer `404` before
296
+ authentication, which reads as "the agent does not exist":
297
+
298
+ ```
299
+ cortenacore.http.endpoints.agui.enabled = true
300
+ cortenacore.http.endpoints.controlPlane.enabled = true
301
+ ```
302
+
303
+ ### The MCP App frame
304
+
305
+ Rendering an extension's own screen means a sandboxed iframe, a `srcdoc` CSP, a
306
+ JSON-RPC postMessage bridge and a `tools/invoke` proxy holding a credential —
307
+ host concerns, not component ones. `renderToolCall` is the seam: it is handed
308
+ each tool call and the `ui://` resource its result carries, and a node it
309
+ returns replaces the default card.
310
+
311
+ ```tsx
312
+ <AgentChatPopup
313
+ extension={extension}
314
+ client={client}
315
+ renderToolCall={({ entry, mcpAppUri }) =>
316
+ mcpAppUri ? <McpAppFrame uri={mcpAppUri} result={entry.output} /> : undefined
317
+ }
318
+ />
319
+ ```
320
+
89
321
  ## Charts in tests
90
322
 
91
323
  Both chart engines size themselves from the parent box, and under jsdom every
@@ -175,7 +407,7 @@ from `@a2ui/*` is installed, so `vitest.config.ts` has no entry for it.
175
407
  pnpm check # types
176
408
  pnpm lint:design # no literals where a token exists; no dangling var()
177
409
  pnpm build # dist/ via tsdown (ESM + d.ts); also runs on prepack
178
- pnpm test # the browser suite, then the bundle budgets
410
+ pnpm test # changed-only: the suites of this package your diff affects
179
411
  pnpm test:bundle # bundle budgets alone (builds first, then real Vite apps)
180
412
  pnpm bundle:probe # the size table, printed, without asserting anything
181
413
  pnpm guide # three-theme guide on a local Vite server
@@ -205,7 +437,7 @@ guide/ Vite app: ?theme=light|dark|system frames, or all three
205
437
  ## Releasing
206
438
 
207
439
  ```bash
208
- pnpm ui:check && pnpm ui:test # browser suite, then the bundle budgets
440
+ pnpm ui:check && FORCE_FULL=1 pnpm test:all # types, token lint, every suite
209
441
  cd packages/ui && npm version minor && npm publish # prepack builds dist/
210
442
  git push --follow-tags
211
443
  ```
@@ -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