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,353 @@
1
+ "use client";
2
+ import { foldA2UIBlocks, readA2UIBlock } from "./a2ui-block.js";
3
+ //#region src/agent-chat/bridge.ts
4
+ /**
5
+ * AG-UI events -> the three `AgentChatClient` sinks.
6
+ *
7
+ * Ported from `cortena-shared/src/agui/bridge.ts` (Cortena monorepo, DESIGN-37
8
+ * step 1). `cortena-shared` is not published, so the translation lives here;
9
+ * keep the two in step. It knows nothing about HTTP or `@ag-ui/client`, which
10
+ * is why the tests can drive it with a literal array of events.
11
+ *
12
+ * One change against the original, and it is the reason this file is not a
13
+ * copy: **TOOL_CALL_ARGS is handled.** cortenacore's mapper emits
14
+ * `TOOL_CALL_START` -> `TOOL_CALL_ARGS { toolCallId, delta }` -> `TOOL_CALL_END`
15
+ * at tool-start time, with the whole argument object stringified into one
16
+ * `delta`. The original bridge had no case for it, so every tool card in the
17
+ * AG-UI path showed a name and a spinner and never showed what the tool was
18
+ * called with. The deltas are accumulated per call id and parsed when they
19
+ * form valid JSON, which is also correct for a transport that streams the
20
+ * arguments a fragment at a time.
21
+ */
22
+ /** `CUSTOM` event names cortenacore emits that this bridge acts on. */
23
+ const AGUI_CUSTOM_NAMES = {
24
+ CHAT_ABORTED: "cortena.chat.aborted",
25
+ TOOL_PROGRESS: "cortena.tool.progress",
26
+ APPROVAL_REQUEST: "cortena.approval.request",
27
+ APPROVAL_RESOLVED: "cortena.approval.resolved",
28
+ A2UI: "cortena.a2ui"
29
+ };
30
+ const DECISIONS = [
31
+ "allow-once",
32
+ "allow-always",
33
+ "deny"
34
+ ];
35
+ var AgentChatBridge = class {
36
+ runId;
37
+ sessionKey;
38
+ chat;
39
+ tool;
40
+ approval;
41
+ now;
42
+ seq = 0;
43
+ /** Text of the assistant message currently streaming. */
44
+ text = "";
45
+ textMessageId = null;
46
+ reasoning = "";
47
+ /**
48
+ * The reasoning accumulated before each assistant message began, by message
49
+ * id, and the whole of the re-attach defence for the chain of thought.
50
+ *
51
+ * A re-attach re-streams the run from the top, so every REASONING delta
52
+ * arrives a second time. Restoring this snapshot when a message id starts
53
+ * again rewinds the buffer to exactly the point the first pass reached,
54
+ * whichever side of `TEXT_MESSAGE_START` the reasoning arrived on.
55
+ */
56
+ reasoningAtStart = /* @__PURE__ */ new Map();
57
+ /**
58
+ * A2UI blocks seen this run. The chat treats the content array as a
59
+ * snapshot, so every emission carries all of them — the same rule the text
60
+ * follows, and what makes a re-attach land in the same place.
61
+ */
62
+ a2ui = [];
63
+ /** Accumulated `TOOL_CALL_ARGS` deltas, per tool call id. */
64
+ args = /* @__PURE__ */ new Map();
65
+ /**
66
+ * A2UI messages already folded onto each surface, by their serialised form.
67
+ *
68
+ * A re-attach re-streams the run from the beginning, so every
69
+ * `CUSTOM cortena.a2ui` arrives again. Folding concatenates, so without this
70
+ * the surface's message list doubled on the first reconnection and trebled
71
+ * on the second — a table drawn once, then drawn again underneath itself.
72
+ */
73
+ seenA2ui = /* @__PURE__ */ new Map();
74
+ terminal = false;
75
+ pendingInterrupts = [];
76
+ constructor(opts) {
77
+ this.runId = opts.runId;
78
+ this.sessionKey = opts.sessionKey;
79
+ this.chat = opts.chat;
80
+ this.tool = opts.tool;
81
+ this.approval = opts.approval;
82
+ this.now = opts.now ?? (() => Date.now());
83
+ }
84
+ /** True once RUN_FINISHED or RUN_ERROR has been seen; no retry after this. */
85
+ get finished() {
86
+ return this.terminal;
87
+ }
88
+ /** Interrupts carried by the terminal RUN_FINISHED, if any. */
89
+ get interrupts() {
90
+ return this.pendingInterrupts;
91
+ }
92
+ blocks() {
93
+ const content = [];
94
+ if (this.reasoning) content.push({
95
+ type: "thinking",
96
+ thinking: this.reasoning
97
+ });
98
+ if (this.text) content.push({
99
+ type: "text",
100
+ text: this.text
101
+ });
102
+ for (const block of this.a2ui) content.push({
103
+ type: "a2ui",
104
+ a2ui: block
105
+ });
106
+ return content;
107
+ }
108
+ emitDelta() {
109
+ const content = this.blocks();
110
+ if (content.length === 0) return;
111
+ this.chat({
112
+ runId: this.runId,
113
+ sessionKey: this.sessionKey,
114
+ seq: ++this.seq,
115
+ state: "delta",
116
+ message: {
117
+ role: "assistant",
118
+ content,
119
+ timestamp: this.now()
120
+ }
121
+ });
122
+ }
123
+ emitFinal(interrupted = false) {
124
+ const content = this.blocks();
125
+ this.chat({
126
+ runId: this.runId,
127
+ sessionKey: this.sessionKey,
128
+ seq: ++this.seq,
129
+ state: "final",
130
+ ...interrupted ? { interrupted: true } : {},
131
+ ...content.length > 0 ? { message: {
132
+ role: "assistant",
133
+ content,
134
+ timestamp: this.now()
135
+ } } : {}
136
+ });
137
+ }
138
+ /** Surface a transport failure the way a run error is surfaced. */
139
+ fail(message) {
140
+ if (this.terminal) return;
141
+ this.terminal = true;
142
+ this.chat({
143
+ runId: this.runId,
144
+ sessionKey: this.sessionKey,
145
+ seq: ++this.seq,
146
+ state: "error",
147
+ errorMessage: message
148
+ });
149
+ }
150
+ handleEvent(event) {
151
+ switch (event.type) {
152
+ case "RUN_STARTED":
153
+ this.textMessageId = null;
154
+ this.text = "";
155
+ this.reasoning = "";
156
+ break;
157
+ case "TEXT_MESSAGE_START": {
158
+ const id = event.messageId ?? null;
159
+ this.textMessageId = id;
160
+ this.text = "";
161
+ if (id !== null) {
162
+ const before = this.reasoningAtStart.get(id);
163
+ if (before === void 0) this.reasoningAtStart.set(id, this.reasoning);
164
+ else this.reasoning = before;
165
+ }
166
+ break;
167
+ }
168
+ case "TEXT_MESSAGE_CONTENT":
169
+ if (event.messageId && event.messageId !== this.textMessageId) {
170
+ this.textMessageId = event.messageId;
171
+ this.text = "";
172
+ }
173
+ this.text += event.delta ?? "";
174
+ this.emitDelta();
175
+ break;
176
+ case "REASONING_MESSAGE_CONTENT":
177
+ this.reasoning += event.delta ?? "";
178
+ this.emitDelta();
179
+ break;
180
+ case "TOOL_CALL_START":
181
+ this.tool?.({
182
+ event: "tool.start",
183
+ data: {
184
+ ...event.toolCallId === void 0 ? {} : { id: event.toolCallId },
185
+ ...event.toolCallName === void 0 ? {} : { name: event.toolCallName },
186
+ startedAt: this.now()
187
+ }
188
+ });
189
+ break;
190
+ case "TOOL_CALL_ARGS":
191
+ this.accumulateArgs(event);
192
+ break;
193
+ case "TOOL_CALL_END":
194
+ this.flushArgs(event.toolCallId);
195
+ break;
196
+ case "TOOL_CALL_RESULT":
197
+ this.tool?.({
198
+ event: event.metadata?.isError === true ? "tool.error" : "tool.complete",
199
+ data: {
200
+ ...event.toolCallId === void 0 ? {} : { id: event.toolCallId },
201
+ output: event.content,
202
+ ...event.metadata ? { metadata: event.metadata } : {},
203
+ completedAt: this.now()
204
+ }
205
+ });
206
+ break;
207
+ case "CUSTOM":
208
+ this.handleCustom(event);
209
+ break;
210
+ case "RUN_FINISHED": {
211
+ this.terminal = true;
212
+ const interrupted = event.outcome?.type === "interrupt";
213
+ if (interrupted) this.pendingInterrupts.push(...event.outcome?.interrupts ?? []);
214
+ this.emitFinal(interrupted);
215
+ break;
216
+ }
217
+ case "RUN_ERROR":
218
+ this.terminal = true;
219
+ this.chat({
220
+ runId: this.runId,
221
+ sessionKey: this.sessionKey,
222
+ seq: ++this.seq,
223
+ state: "error",
224
+ errorMessage: event.message ?? "An error occurred"
225
+ });
226
+ }
227
+ }
228
+ /**
229
+ * Accumulate one `TOOL_CALL_ARGS` delta and publish the best reading of it.
230
+ *
231
+ * Published on every delta rather than only at `TOOL_CALL_END`, because a
232
+ * run that is interrupted between the two — an approval, an abort, a dropped
233
+ * stream — would otherwise show a tool card with no input at all, which is
234
+ * the state the card exists to avoid.
235
+ */
236
+ accumulateArgs(event) {
237
+ const id = event.toolCallId;
238
+ if (!id) return;
239
+ const next = (this.args.get(id) ?? "") + (event.delta ?? "");
240
+ this.args.set(id, next);
241
+ this.publishArgs(id, next);
242
+ }
243
+ flushArgs(id) {
244
+ if (!id) return;
245
+ const raw = this.args.get(id);
246
+ if (raw === void 0) return;
247
+ this.args.delete(id);
248
+ this.publishArgs(id, raw);
249
+ }
250
+ publishArgs(id, raw) {
251
+ if (!raw) return;
252
+ this.tool?.({
253
+ event: "tool.args",
254
+ data: {
255
+ id,
256
+ input: parseArgs(raw)
257
+ }
258
+ });
259
+ }
260
+ /**
261
+ * The block with any message this surface has already been sent removed;
262
+ * `null` when nothing in it is new.
263
+ */
264
+ freshA2UI(block) {
265
+ if (!block) return null;
266
+ let seen = this.seenA2ui.get(block.surfaceId);
267
+ if (!seen) {
268
+ seen = /* @__PURE__ */ new Set();
269
+ this.seenA2ui.set(block.surfaceId, seen);
270
+ }
271
+ const messages = [];
272
+ for (const message of block.messages) {
273
+ const key = stableKey(message);
274
+ if (seen.has(key)) continue;
275
+ seen.add(key);
276
+ messages.push(message);
277
+ }
278
+ if (messages.length === 0 && !block.error) return null;
279
+ return {
280
+ ...block,
281
+ messages
282
+ };
283
+ }
284
+ handleCustom(event) {
285
+ const value = event.value ?? {};
286
+ const id = typeof value.id === "string" ? value.id : "";
287
+ switch (event.name) {
288
+ case AGUI_CUSTOM_NAMES.TOOL_PROGRESS:
289
+ this.tool?.({
290
+ event: "tool.progress",
291
+ data: {
292
+ ...typeof value.toolCallId === "string" ? { id: value.toolCallId } : {},
293
+ output: value.partialResult
294
+ }
295
+ });
296
+ break;
297
+ case AGUI_CUSTOM_NAMES.APPROVAL_REQUEST: {
298
+ const request = value.request ?? {};
299
+ this.approval?.({
300
+ type: "request",
301
+ request: {
302
+ id,
303
+ toolName: typeof request.command === "string" ? request.command : "exec",
304
+ args: request,
305
+ timestamp: typeof value.createdAtMs === "number" ? value.createdAtMs : this.now(),
306
+ runId: this.runId,
307
+ sessionKey: this.sessionKey
308
+ }
309
+ });
310
+ break;
311
+ }
312
+ case AGUI_CUSTOM_NAMES.A2UI: {
313
+ const block = this.freshA2UI(readA2UIBlock(value));
314
+ if (block) {
315
+ this.a2ui = foldA2UIBlocks([...this.a2ui, block]);
316
+ this.emitDelta();
317
+ }
318
+ break;
319
+ }
320
+ case AGUI_CUSTOM_NAMES.APPROVAL_RESOLVED: this.approval?.({
321
+ type: "resolved",
322
+ id,
323
+ decision: typeof value.decision === "string" && DECISIONS.includes(value.decision) ? value.decision : null
324
+ });
325
+ }
326
+ }
327
+ };
328
+ /**
329
+ * A message's identity for de-duplication.
330
+ *
331
+ * `JSON.stringify` is enough: these are payloads the transport just parsed out
332
+ * of JSON, so key order is the order the server wrote, and the same message
333
+ * arriving twice on a re-attach serialises identically both times.
334
+ */
335
+ function stableKey(message) {
336
+ try {
337
+ return JSON.stringify(message) ?? "undefined";
338
+ } catch {
339
+ return `${Math.random()}`;
340
+ }
341
+ }
342
+ /** Arguments as an object when the accumulation parses, as the raw text otherwise. */
343
+ function parseArgs(raw) {
344
+ try {
345
+ return JSON.parse(raw);
346
+ } catch {
347
+ return raw;
348
+ }
349
+ }
350
+ //#endregion
351
+ export { AGUI_CUSTOM_NAMES, AgentChatBridge };
352
+
353
+ //# sourceMappingURL=bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bridge.js","names":[],"sources":["../../src/agent-chat/bridge.ts"],"sourcesContent":["/**\n * AG-UI events -> the three `AgentChatClient` sinks.\n *\n * Ported from `cortena-shared/src/agui/bridge.ts` (Cortena monorepo, DESIGN-37\n * step 1). `cortena-shared` is not published, so the translation lives here;\n * keep the two in step. It knows nothing about HTTP or `@ag-ui/client`, which\n * is why the tests can drive it with a literal array of events.\n *\n * One change against the original, and it is the reason this file is not a\n * copy: **TOOL_CALL_ARGS is handled.** cortenacore's mapper emits\n * `TOOL_CALL_START` -> `TOOL_CALL_ARGS { toolCallId, delta }` -> `TOOL_CALL_END`\n * at tool-start time, with the whole argument object stringified into one\n * `delta`. The original bridge had no case for it, so every tool card in the\n * AG-UI path showed a name and a spinner and never showed what the tool was\n * called with. The deltas are accumulated per call id and parsed when they\n * form valid JSON, which is also correct for a transport that streams the\n * arguments a fragment at a time.\n */\n\nimport { type A2UIChatBlock, foldA2UIBlocks, readA2UIBlock } from \"./a2ui-block\";\nimport type {\n AgentApprovalEvent,\n AgentChatEvent,\n AgentToolEvent,\n AgentApprovalDecision,\n} from \"./types\";\n\n/** The subset of an AG-UI event this bridge reads. */\nexport interface AguiIncomingEvent {\n type: string;\n messageId?: string;\n delta?: string;\n toolCallId?: string;\n toolCallName?: string;\n content?: string;\n message?: string;\n name?: string;\n value?: unknown;\n metadata?: Record<string, unknown>;\n snapshot?: unknown;\n outcome?: { type?: string; interrupts?: Array<Record<string, unknown>> };\n}\n\n/** `CUSTOM` event names cortenacore emits that this bridge acts on. */\nexport const AGUI_CUSTOM_NAMES = {\n CHAT_ABORTED: \"cortena.chat.aborted\",\n TOOL_PROGRESS: \"cortena.tool.progress\",\n APPROVAL_REQUEST: \"cortena.approval.request\",\n APPROVAL_RESOLVED: \"cortena.approval.resolved\",\n A2UI: \"cortena.a2ui\",\n} as const;\n\nexport interface AgentChatBridgeOptions {\n runId: string;\n sessionKey: string;\n chat: (event: AgentChatEvent) => void;\n tool?: (event: AgentToolEvent) => void;\n approval?: (event: AgentApprovalEvent) => void;\n now?: () => number;\n}\n\ntype ContentBlock = {\n type: \"thinking\" | \"text\" | \"a2ui\";\n thinking?: string;\n text?: string;\n a2ui?: A2UIChatBlock;\n};\n\nconst DECISIONS: readonly string[] = [\"allow-once\", \"allow-always\", \"deny\"];\n\nexport class AgentChatBridge {\n readonly runId: string;\n readonly sessionKey: string;\n\n private readonly chat: (event: AgentChatEvent) => void;\n private readonly tool: ((event: AgentToolEvent) => void) | undefined;\n private readonly approval: ((event: AgentApprovalEvent) => void) | undefined;\n private readonly now: () => number;\n\n private seq = 0;\n /** Text of the assistant message currently streaming. */\n private text = \"\";\n private textMessageId: string | null = null;\n private reasoning = \"\";\n /**\n * The reasoning accumulated before each assistant message began, by message\n * id, and the whole of the re-attach defence for the chain of thought.\n *\n * A re-attach re-streams the run from the top, so every REASONING delta\n * arrives a second time. Restoring this snapshot when a message id starts\n * again rewinds the buffer to exactly the point the first pass reached,\n * whichever side of `TEXT_MESSAGE_START` the reasoning arrived on.\n */\n private readonly reasoningAtStart = new Map<string, string>();\n /**\n * A2UI blocks seen this run. The chat treats the content array as a\n * snapshot, so every emission carries all of them — the same rule the text\n * follows, and what makes a re-attach land in the same place.\n */\n private a2ui: A2UIChatBlock[] = [];\n /** Accumulated `TOOL_CALL_ARGS` deltas, per tool call id. */\n private readonly args = new Map<string, string>();\n /**\n * A2UI messages already folded onto each surface, by their serialised form.\n *\n * A re-attach re-streams the run from the beginning, so every\n * `CUSTOM cortena.a2ui` arrives again. Folding concatenates, so without this\n * the surface's message list doubled on the first reconnection and trebled\n * on the second — a table drawn once, then drawn again underneath itself.\n */\n private readonly seenA2ui = new Map<string, Set<string>>();\n private terminal = false;\n private readonly pendingInterrupts: Array<Record<string, unknown>> = [];\n\n constructor(opts: AgentChatBridgeOptions) {\n this.runId = opts.runId;\n this.sessionKey = opts.sessionKey;\n this.chat = opts.chat;\n this.tool = opts.tool;\n this.approval = opts.approval;\n this.now = opts.now ?? (() => Date.now());\n }\n\n /** True once RUN_FINISHED or RUN_ERROR has been seen; no retry after this. */\n get finished(): boolean {\n return this.terminal;\n }\n\n /** Interrupts carried by the terminal RUN_FINISHED, if any. */\n get interrupts(): ReadonlyArray<Record<string, unknown>> {\n return this.pendingInterrupts;\n }\n\n private blocks(): ContentBlock[] {\n const content: ContentBlock[] = [];\n if (this.reasoning) {\n content.push({ type: \"thinking\", thinking: this.reasoning });\n }\n if (this.text) {\n content.push({ type: \"text\", text: this.text });\n }\n for (const block of this.a2ui) {\n content.push({ type: \"a2ui\", a2ui: block });\n }\n return content;\n }\n\n private emitDelta(): void {\n const content = this.blocks();\n if (content.length === 0) {\n return;\n }\n this.chat({\n runId: this.runId,\n sessionKey: this.sessionKey,\n seq: ++this.seq,\n state: \"delta\",\n message: { role: \"assistant\", content, timestamp: this.now() },\n });\n }\n\n private emitFinal(interrupted = false): void {\n const content = this.blocks();\n this.chat({\n runId: this.runId,\n sessionKey: this.sessionKey,\n seq: ++this.seq,\n state: \"final\",\n // Carried through so the store can tell \"the run succeeded\" from \"the\n // run is waiting for you\". Both arrive as RUN_FINISHED.\n ...(interrupted ? { interrupted: true } : {}),\n ...(content.length > 0\n ? { message: { role: \"assistant\", content, timestamp: this.now() } }\n : {}),\n });\n }\n\n /** Surface a transport failure the way a run error is surfaced. */\n fail(message: string): void {\n if (this.terminal) {\n return;\n }\n this.terminal = true;\n this.chat({\n runId: this.runId,\n sessionKey: this.sessionKey,\n seq: ++this.seq,\n state: \"error\",\n errorMessage: message,\n });\n }\n\n handleEvent(event: AguiIncomingEvent): void {\n switch (event.type) {\n case \"RUN_STARTED\":\n // The run begins here, so the streaming buffers do too. This is the\n // only place the chain of thought is cleared outright.\n this.textMessageId = null;\n this.text = \"\";\n this.reasoning = \"\";\n break;\n case \"TEXT_MESSAGE_START\": {\n // A new assistant message replaces the text buffer, and *only* the\n // text buffer. cortenacore emits the REASONING deltas before the\n // message they explain, so clearing the reasoning here threw away the\n // chain of thought a moment after it arrived — the thinking block\n // never rendered at all. Re-attach doubling is handled instead by\n // rewinding to the snapshot taken when this message id first started.\n const id = event.messageId ?? null;\n this.textMessageId = id;\n this.text = \"\";\n if (id !== null) {\n const before = this.reasoningAtStart.get(id);\n if (before === undefined) {\n this.reasoningAtStart.set(id, this.reasoning);\n } else {\n this.reasoning = before;\n }\n }\n break;\n }\n case \"TEXT_MESSAGE_CONTENT\":\n if (event.messageId && event.messageId !== this.textMessageId) {\n this.textMessageId = event.messageId;\n this.text = \"\";\n }\n this.text += event.delta ?? \"\";\n this.emitDelta();\n break;\n case \"REASONING_MESSAGE_CONTENT\":\n this.reasoning += event.delta ?? \"\";\n this.emitDelta();\n break;\n case \"TOOL_CALL_START\":\n this.tool?.({\n event: \"tool.start\",\n data: {\n ...(event.toolCallId === undefined ? {} : { id: event.toolCallId }),\n ...(event.toolCallName === undefined ? {} : { name: event.toolCallName }),\n startedAt: this.now(),\n },\n });\n break;\n case \"TOOL_CALL_ARGS\":\n this.accumulateArgs(event);\n break;\n case \"TOOL_CALL_END\":\n // Nothing more will arrive for this call's arguments; emit whatever\n // accumulated, even when it never parsed, so the card shows something.\n this.flushArgs(event.toolCallId);\n break;\n case \"TOOL_CALL_RESULT\":\n // The metadata travels WITH the event. It carries `isError`, and the\n // `meta` a Cortena tool attaches to a failure — the status, the code\n // and the message — and it is the only trustworthy account of how the\n // call went. Dropping it here is what forced the store to guess at an\n // envelope inside `content`, and the guess was wrong for every real\n // result cortenacore emits.\n this.tool?.({\n event: event.metadata?.isError === true ? \"tool.error\" : \"tool.complete\",\n data: {\n ...(event.toolCallId === undefined ? {} : { id: event.toolCallId }),\n output: event.content,\n ...(event.metadata ? { metadata: event.metadata } : {}),\n completedAt: this.now(),\n },\n });\n break;\n case \"CUSTOM\":\n this.handleCustom(event);\n break;\n case \"RUN_FINISHED\": {\n this.terminal = true;\n const interrupted = event.outcome?.type === \"interrupt\";\n if (interrupted) {\n this.pendingInterrupts.push(...(event.outcome?.interrupts ?? []));\n }\n this.emitFinal(interrupted);\n break;\n }\n case \"RUN_ERROR\":\n this.terminal = true;\n this.chat({\n runId: this.runId,\n sessionKey: this.sessionKey,\n seq: ++this.seq,\n state: \"error\",\n errorMessage: event.message ?? \"An error occurred\",\n });\n break;\n default:\n break;\n }\n }\n\n /**\n * Accumulate one `TOOL_CALL_ARGS` delta and publish the best reading of it.\n *\n * Published on every delta rather than only at `TOOL_CALL_END`, because a\n * run that is interrupted between the two — an approval, an abort, a dropped\n * stream — would otherwise show a tool card with no input at all, which is\n * the state the card exists to avoid.\n */\n private accumulateArgs(event: AguiIncomingEvent): void {\n const id = event.toolCallId;\n if (!id) {\n return;\n }\n const next = (this.args.get(id) ?? \"\") + (event.delta ?? \"\");\n this.args.set(id, next);\n this.publishArgs(id, next);\n }\n\n private flushArgs(id?: string): void {\n if (!id) {\n return;\n }\n const raw = this.args.get(id);\n if (raw === undefined) {\n return;\n }\n this.args.delete(id);\n this.publishArgs(id, raw);\n }\n\n private publishArgs(id: string, raw: string): void {\n if (!raw) {\n return;\n }\n this.tool?.({ event: \"tool.args\", data: { id, input: parseArgs(raw) } });\n }\n\n /**\n * The block with any message this surface has already been sent removed;\n * `null` when nothing in it is new.\n */\n private freshA2UI(block: A2UIChatBlock | null): A2UIChatBlock | null {\n if (!block) {\n return null;\n }\n let seen = this.seenA2ui.get(block.surfaceId);\n if (!seen) {\n seen = new Set();\n this.seenA2ui.set(block.surfaceId, seen);\n }\n const messages: unknown[] = [];\n for (const message of block.messages) {\n const key = stableKey(message);\n if (seen.has(key)) {\n continue;\n }\n seen.add(key);\n messages.push(message);\n }\n // An error still deserves a card even when its messages are all repeats.\n if (messages.length === 0 && !block.error) {\n return null;\n }\n return { ...block, messages };\n }\n\n private handleCustom(event: AguiIncomingEvent): void {\n const value = (event.value ?? {}) as Record<string, unknown>;\n const id = typeof value.id === \"string\" ? value.id : \"\";\n switch (event.name) {\n case AGUI_CUSTOM_NAMES.TOOL_PROGRESS:\n this.tool?.({\n event: \"tool.progress\",\n data: {\n ...(typeof value.toolCallId === \"string\" ? { id: value.toolCallId } : {}),\n output: value.partialResult,\n },\n });\n break;\n case AGUI_CUSTOM_NAMES.APPROVAL_REQUEST: {\n const request = (value.request ?? {}) as Record<string, unknown>;\n this.approval?.({\n type: \"request\",\n request: {\n id,\n toolName: typeof request.command === \"string\" ? request.command : \"exec\",\n args: request,\n timestamp: typeof value.createdAtMs === \"number\" ? value.createdAtMs : this.now(),\n // Captured here, where the run and the session are facts about the\n // event rather than about whatever is in flight when the user\n // eventually presses a button.\n runId: this.runId,\n sessionKey: this.sessionKey,\n },\n });\n break;\n }\n case AGUI_CUSTOM_NAMES.A2UI: {\n // One event per block; folding here means two pushes to one surface\n // reach the renderer as a single message list, which is how a surface\n // the agent streams in pieces ends up drawn once.\n const block = this.freshA2UI(readA2UIBlock(value));\n if (block) {\n this.a2ui = foldA2UIBlocks([...this.a2ui, block]);\n this.emitDelta();\n }\n break;\n }\n case AGUI_CUSTOM_NAMES.APPROVAL_RESOLVED:\n this.approval?.({\n type: \"resolved\",\n id,\n decision:\n typeof value.decision === \"string\" && DECISIONS.includes(value.decision)\n ? (value.decision as AgentApprovalDecision)\n : null,\n });\n break;\n default:\n // cortena.chat.aborted needs no change here: abort() has already reset\n // the local streaming state before the request went out.\n break;\n }\n }\n}\n\n/**\n * A message's identity for de-duplication.\n *\n * `JSON.stringify` is enough: these are payloads the transport just parsed out\n * of JSON, so key order is the order the server wrote, and the same message\n * arriving twice on a re-attach serialises identically both times.\n */\nfunction stableKey(message: unknown): string {\n try {\n return JSON.stringify(message) ?? \"undefined\";\n } catch {\n // Circular, which a JSON transport cannot produce. Never equal to anything.\n return `${Math.random()}`;\n }\n}\n\n/** Arguments as an object when the accumulation parses, as the raw text otherwise. */\nfunction parseArgs(raw: string): unknown {\n try {\n return JSON.parse(raw);\n } catch {\n return raw;\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ "use client";
2
+ import { AgentChatMessage, AgentChatStep } from "./types.js";
3
+ //#region src/agent-chat/session.d.ts
4
+ /**
5
+ * A new session key, minted client-side.
6
+ *
7
+ * `agent:<agentId>:new-<Date.now()>-<random36>`. cortenacore reads the
8
+ * `agent:<id>:` prefix to bind the session to that agent's template, and the
9
+ * key is the AG-UI `threadId` verbatim, prefix included. Minting it rather than
10
+ * asking for one is what lets the first message go out immediately.
11
+ */
12
+ export declare function mintSessionKey(agentId: string): string;
13
+ /** Was this key minted locally for a chat that had no session yet? */
14
+ export declare function isProvisionalSessionKey(key: string): boolean;
15
+ /** Does this session belong to this agent? `sessions.list` is filtered by it. */
16
+ export declare function isSessionOfAgent(key: string, agentId: string): boolean;
17
+ /** The `sessionStorage` name the current key is kept under, per extension. */
18
+ export declare function sessionStorageKey(scope: string): string;
19
+ /** The `sessionStorage` name the open/collapsed state is kept under. */
20
+ export declare function viewStorageKey(scope: string): string;
21
+ /** The `sessionStorage` name this window's progress steps are kept under. */
22
+ export declare function stepsStorageKey(scope: string): string;
23
+ export declare function readStoredSessionKey(scope: string): string | null;
24
+ export declare function writeStoredSessionKey(scope: string, key: string | null): void;
25
+ /** Most steps kept, in memory and in `sessionStorage`. A turn that long is a runaway loop. */
26
+ export declare const STEP_LIMIT = 50;
27
+ /**
28
+ * The steps of the current turn, and the session they belong to.
29
+ *
30
+ * They go in the per-window store beside the session key, for the same reason
31
+ * the key does: a tab is reloaded mid-run, or a host remounts the surface on a
32
+ * route change, and both threw the whole strip away. (Collapsing the pop-up is
33
+ * NOT one of them — the panel is `hidden`, not unmounted, and the run streams
34
+ * into it either way.) Messages come back from `chat.history`; steps do not
35
+ * exist on the server, so this is the only place they can come back from.
36
+ *
37
+ * Scoped to a session key, and checked on read: resuming a different session
38
+ * must not inherit the last one's steps.
39
+ */
40
+ export declare function readStoredSteps(scope: string, sessionKey: string): AgentChatStep[];
41
+ /** The slot's contents, projected and trimmed to fit the byte cap. */
42
+ export declare function serialiseSteps(sessionKey: string, steps: readonly AgentChatStep[]): string;
43
+ export declare function writeStoredSteps(scope: string, sessionKey: string | null, steps: readonly AgentChatStep[]): void;
44
+ /** Raw transcript records to the messages the list renders. */
45
+ export declare function parseHistoryMessages(raw: readonly unknown[]): AgentChatMessage[];
46
+ /**
47
+ * The history shrink guard.
48
+ *
49
+ * A history load must not shrink the session already on screen. The first
50
+ * message of a new chat loads history while the turn is still in flight and the
51
+ * user's message has not been persisted yet; replacing wholesale threw that
52
+ * message away, and the reply survived, so the symptom was a conversation with
53
+ * the answer but not the question.
54
+ *
55
+ * Only the same session, and only against losing messages: a longer or equal
56
+ * history still replaces, so an edit, a deletion made elsewhere and a compaction
57
+ * all land normally.
58
+ */
59
+ export declare function wouldShrinkHistory(params: {
60
+ currentSessionKey: string | null;
61
+ loadedSessionKey: string;
62
+ currentCount: number;
63
+ loadedCount: number;
64
+ }): boolean;
65
+ /**
66
+ * Should this message be hidden?
67
+ *
68
+ * The same chain cortenaweb's bubble applies, in the same order. It exists
69
+ * because tool plumbing leaks into a transcript in several recognisable shapes,
70
+ * and a chat that renders them reads as broken. Tool detail is not lost: it is
71
+ * in the tool strip and in the chain of thought.
72
+ *
73
+ * A reply that is only UI the agent drew has no text at all, so an A2UI block
74
+ * counts as content — without that, the surface the user asked for is filtered
75
+ * away and nothing appears.
76
+ */
77
+ export declare function isHiddenMessage(message: AgentChatMessage): boolean;
78
+ //#endregion
79
+ //# sourceMappingURL=session.d.ts.map