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,876 @@
1
+ "use client";
2
+ "use client";
3
+ import { extractA2UIBlocks, foldA2UIBlocks } from "./a2ui-block.js";
4
+ import { confirmationRequiredLabel, isGenericLabel, stepLabel, truncate } from "./step-label.js";
5
+ import { isProvisionalSessionKey, isSessionOfAgent, mintSessionKey, parseHistoryMessages, readStoredSessionKey, readStoredSteps, separateThinking, wouldShrinkHistory, writeStoredSessionKey, writeStoredSteps } from "./session.js";
6
+ import * as React from "react";
7
+ //#region src/agent-chat/store.ts
8
+ /**
9
+ * The chat state, and the hook that drives an `AgentChatClient` from React.
10
+ *
11
+ * The accumulation rules are ported from `cortena-shared/src/stores/use-chat.ts`
12
+ * (Cortena monorepo) so a run leaves this state in the same place it leaves
13
+ * cortenaweb's. The ones that are not obvious, and that a rewrite gets wrong:
14
+ *
15
+ * - A `delta` carries a snapshot, so text and A2UI blocks are **replaced**, not
16
+ * appended. Appending doubles every character.
17
+ * - Thinking is never replaced by something shorter. The upstream emitter can
18
+ * fall back to increments when a snapshot is unavailable, and treating one of
19
+ * those as a snapshot truncates the accumulated chain of thought.
20
+ * - On `final`, a second assistant message in the same turn folds the previous
21
+ * visible text into the chain of thought rather than adding a second bubble,
22
+ * and keeps the A2UI blocks of the message it folded away — the surface is
23
+ * still on screen and still interactive.
24
+ * - A reply that is only a drawn surface has no text, so `text || a2ui.length`
25
+ * is the test for "there is something to show". Testing text alone silently
26
+ * dropped everything the agent drew.
27
+ */
28
+ const EMPTY = {
29
+ sessionKey: null,
30
+ messages: [],
31
+ streaming: false,
32
+ streamText: "",
33
+ streamThinkingText: "",
34
+ streamA2ui: [],
35
+ runId: null,
36
+ hasMoreHistory: false,
37
+ loadingHistory: false,
38
+ toolOrder: [],
39
+ tools: {},
40
+ steps: [],
41
+ retiredSteps: [],
42
+ approvals: [],
43
+ historyCount: 0,
44
+ sessions: [],
45
+ loadingSessions: false,
46
+ error: null
47
+ };
48
+ function generateId() {
49
+ return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
50
+ }
51
+ function extractText(message) {
52
+ return (message?.content ?? []).filter((c) => c.type === "text" && c.text).map((c) => c.text).join("");
53
+ }
54
+ function extractThinking(message) {
55
+ return (message?.content ?? []).filter((c) => c.type === "thinking").map((c) => c.thinking ?? c.text ?? "").filter(Boolean).join("\n\n");
56
+ }
57
+ function reduce(state, action) {
58
+ switch (action.kind) {
59
+ case "chat": return reduceChat(state, action.event);
60
+ case "tool": return reduceTool(state, action.event);
61
+ case "approval/request": return state.approvals.some((r) => r.id === action.request.id) ? state : {
62
+ ...state,
63
+ approvals: [...state.approvals, action.request]
64
+ };
65
+ case "approval/resolved": return {
66
+ ...state,
67
+ approvals: state.approvals.filter((r) => r.id !== action.id)
68
+ };
69
+ case "approval/denied": return {
70
+ ...state,
71
+ approvals: state.approvals.filter((r) => r.id !== action.id),
72
+ steps: endRunningSteps(state.steps, { kind: "denied" })
73
+ };
74
+ case "approval/failed": return state.approvals.some((r) => r.id === action.request.id) ? state : {
75
+ ...state,
76
+ approvals: [...state.approvals, {
77
+ ...action.request,
78
+ error: action.message
79
+ }]
80
+ };
81
+ case "session": return action.key === state.sessionKey ? state : {
82
+ ...EMPTY,
83
+ sessions: state.sessions,
84
+ sessionKey: action.key
85
+ };
86
+ case "send": return {
87
+ ...state,
88
+ sessionKey: action.sessionKey,
89
+ messages: action.message ? [...state.messages, action.message] : state.messages,
90
+ streaming: true,
91
+ streamText: "",
92
+ streamThinkingText: "",
93
+ streamA2ui: [],
94
+ runId: action.runId,
95
+ toolOrder: [],
96
+ tools: {},
97
+ steps: [],
98
+ retiredSteps: [],
99
+ approvals: [],
100
+ error: null
101
+ };
102
+ case "send/failed": return {
103
+ ...state,
104
+ streaming: false,
105
+ runId: null,
106
+ messages: [...state.messages, {
107
+ id: generateId(),
108
+ role: "assistant",
109
+ text: action.message,
110
+ timestamp: Date.now(),
111
+ isError: true
112
+ }]
113
+ };
114
+ case "abort": return {
115
+ ...state,
116
+ streaming: false,
117
+ streamText: "",
118
+ streamThinkingText: "",
119
+ streamA2ui: [],
120
+ runId: null,
121
+ approvals: [],
122
+ steps: endRunningSteps(state.steps, { kind: "stopped" })
123
+ };
124
+ case "history/loading": return {
125
+ ...state,
126
+ loadingHistory: true
127
+ };
128
+ case "history/loaded":
129
+ if (wouldShrinkHistory({
130
+ currentSessionKey: state.sessionKey,
131
+ loadedSessionKey: action.sessionKey,
132
+ currentCount: state.messages.length,
133
+ loadedCount: action.messages.length
134
+ })) return {
135
+ ...state,
136
+ sessionKey: action.sessionKey,
137
+ hasMoreHistory: action.hasMore,
138
+ loadingHistory: false,
139
+ historyCount: action.records
140
+ };
141
+ return {
142
+ ...state,
143
+ sessionKey: action.sessionKey,
144
+ messages: action.messages,
145
+ hasMoreHistory: action.hasMore,
146
+ loadingHistory: false,
147
+ historyCount: action.records,
148
+ streaming: false,
149
+ streamText: "",
150
+ streamThinkingText: "",
151
+ streamA2ui: []
152
+ };
153
+ case "history/older": return {
154
+ ...state,
155
+ messages: [...action.messages, ...state.messages],
156
+ hasMoreHistory: action.hasMore,
157
+ loadingHistory: false,
158
+ historyCount: state.historyCount + action.records
159
+ };
160
+ case "history/failed": return {
161
+ ...state,
162
+ loadingHistory: false
163
+ };
164
+ case "sessions/loading": return {
165
+ ...state,
166
+ loadingSessions: true
167
+ };
168
+ case "sessions/loaded": return {
169
+ ...state,
170
+ sessions: action.sessions,
171
+ loadingSessions: false
172
+ };
173
+ case "steps/restored": return state.steps.length > 0 ? state : {
174
+ ...state,
175
+ steps: action.steps.slice(-50)
176
+ };
177
+ case "error": return {
178
+ ...state,
179
+ error: action.message
180
+ };
181
+ default: return state;
182
+ }
183
+ }
184
+ function reduceChat(state, event) {
185
+ if (event.sessionKey && state.sessionKey && event.sessionKey !== state.sessionKey) {
186
+ if (!isProvisionalSessionKey(state.sessionKey)) return state;
187
+ state = {
188
+ ...state,
189
+ sessionKey: event.sessionKey
190
+ };
191
+ } else if (event.sessionKey && !state.sessionKey) state = {
192
+ ...state,
193
+ sessionKey: event.sessionKey
194
+ };
195
+ switch (event.state) {
196
+ case "delta": {
197
+ const text = extractText(event.message);
198
+ const thinking = extractThinking(event.message);
199
+ const a2ui = extractA2UIBlocks(event.message);
200
+ const next = {
201
+ ...state,
202
+ streaming: true
203
+ };
204
+ if (text) next.streamText = text;
205
+ if (a2ui.length > 0) next.streamA2ui = a2ui;
206
+ const acceptThinking = !!thinking && thinking.length >= state.streamThinkingText.length;
207
+ if (acceptThinking) next.streamThinkingText = thinking;
208
+ return text || acceptThinking || a2ui.length > 0 ? next : state;
209
+ }
210
+ case "final": {
211
+ const finalText = extractText(event.message);
212
+ const finalThinking = extractThinking(event.message);
213
+ const finalA2ui = extractA2UIBlocks(event.message);
214
+ const text = finalText || state.streamText;
215
+ const thinkingFromBlocks = finalThinking || state.streamThinkingText;
216
+ const a2ui = foldA2UIBlocks(finalA2ui.length > 0 ? finalA2ui : state.streamA2ui);
217
+ const cleared = {
218
+ streaming: false,
219
+ streamText: "",
220
+ streamThinkingText: "",
221
+ streamA2ui: [],
222
+ steps: endRunningSteps(state.steps, event.interrupted ? { kind: "paused" } : { kind: "finished" })
223
+ };
224
+ if (!text && a2ui.length === 0) {
225
+ const accumulated = state.streamThinkingText;
226
+ const last = state.messages[state.messages.length - 1];
227
+ if (!accumulated || !foldsInto(last, state.runId)) return {
228
+ ...state,
229
+ ...cleared
230
+ };
231
+ const messages = [...state.messages];
232
+ messages[messages.length - 1] = {
233
+ ...last,
234
+ timestamp: Date.now(),
235
+ thinkingText: [last.thinkingText ?? "", accumulated].filter(Boolean).join("\n\n")
236
+ };
237
+ return {
238
+ ...state,
239
+ ...cleared,
240
+ messages
241
+ };
242
+ }
243
+ const { thinkingText: tagThinking, mainText } = separateThinking(text);
244
+ const thinkingText = thinkingFromBlocks || tagThinking;
245
+ const displayText = thinkingFromBlocks ? text : mainText;
246
+ const messages = [...state.messages];
247
+ const last = messages[messages.length - 1];
248
+ if (foldsInto(last, state.runId)) {
249
+ const updated = [[last.thinkingText ?? "", last.text.trim()].filter(Boolean).join("\n\n"), thinkingText].filter(Boolean).join("\n\n");
250
+ messages[messages.length - 1] = {
251
+ ...last,
252
+ text: displayText,
253
+ timestamp: Date.now(),
254
+ ...updated ? { thinkingText: updated } : { thinkingText: void 0 },
255
+ ...a2ui.length > 0 || last.a2ui?.length ? { a2ui: foldA2UIBlocks([...last.a2ui ?? [], ...a2ui]) } : {}
256
+ };
257
+ } else messages.push({
258
+ id: generateId(),
259
+ role: "assistant",
260
+ text: displayText,
261
+ timestamp: Date.now(),
262
+ ...state.runId ? { runId: state.runId } : {},
263
+ ...thinkingText ? { thinkingText } : {},
264
+ ...a2ui.length > 0 ? { a2ui } : {}
265
+ });
266
+ return {
267
+ ...state,
268
+ ...cleared,
269
+ messages
270
+ };
271
+ }
272
+ case "error": return {
273
+ ...state,
274
+ streaming: false,
275
+ streamText: "",
276
+ streamThinkingText: "",
277
+ streamA2ui: [],
278
+ runId: null,
279
+ steps: endRunningSteps(state.steps, {
280
+ kind: "failed",
281
+ message: truncate(event.errorMessage ?? "The run failed.", ERROR_LIMIT)
282
+ }),
283
+ messages: [...state.messages, {
284
+ id: generateId(),
285
+ role: "assistant",
286
+ text: event.errorMessage ?? "An error occurred",
287
+ timestamp: Date.now(),
288
+ isError: true
289
+ }]
290
+ };
291
+ default: return state;
292
+ }
293
+ }
294
+ /**
295
+ * May a `final` fold into this message?
296
+ *
297
+ * Only when it is an assistant bubble from the SAME run. Two assistant
298
+ * messages in one turn are one answer with an interruption in the middle, and
299
+ * folding them is right. Two assistant messages from different runs are two
300
+ * answers to two questions, and folding them rewrote the earlier answer with
301
+ * the later one — visibly, in a transcript the user had already read.
302
+ *
303
+ * A message with no `runId` is history, which never folds either.
304
+ */
305
+ function foldsInto(last, runId) {
306
+ return last !== void 0 && last.role === "assistant" && !last.isError && runId !== null && last.runId === runId;
307
+ }
308
+ function reduceTool(state, event) {
309
+ const id = event.data.id;
310
+ if (!id) return state;
311
+ const base = state.tools[id] ?? {
312
+ id,
313
+ name: event.data.name ?? "tool",
314
+ status: "running",
315
+ startedAt: event.data.startedAt ?? Date.now()
316
+ };
317
+ let entry;
318
+ switch (event.event) {
319
+ case "tool.start":
320
+ entry = {
321
+ ...base,
322
+ name: event.data.name ?? base.name,
323
+ status: "running"
324
+ };
325
+ break;
326
+ case "tool.args":
327
+ entry = {
328
+ ...base,
329
+ input: event.data.input
330
+ };
331
+ break;
332
+ case "tool.progress":
333
+ entry = {
334
+ ...base,
335
+ output: event.data.output
336
+ };
337
+ break;
338
+ case "tool.complete":
339
+ entry = {
340
+ ...base,
341
+ status: "complete",
342
+ output: event.data.output,
343
+ completedAt: event.data.completedAt ?? Date.now()
344
+ };
345
+ break;
346
+ case "tool.error":
347
+ entry = {
348
+ ...base,
349
+ status: "error",
350
+ output: event.data.output,
351
+ completedAt: event.data.completedAt ?? Date.now()
352
+ };
353
+ break;
354
+ default: return state;
355
+ }
356
+ const stepped = reduceStep(state, event, id);
357
+ return {
358
+ ...state,
359
+ tools: {
360
+ ...state.tools,
361
+ [id]: entry
362
+ },
363
+ toolOrder: state.toolOrder.includes(id) ? state.toolOrder : [...state.toolOrder, id],
364
+ steps: stepped.steps,
365
+ retiredSteps: stepped.retiredSteps
366
+ };
367
+ }
368
+ /**
369
+ * `TOOL_CALL_RESULT.metadata`, read defensively.
370
+ *
371
+ * `null` when nothing usable arrived, which a transport that predates the
372
+ * contract will produce. A step with no metadata at all is treated as a
373
+ * success by `tool.complete` and as a failure by `tool.error`, which is the
374
+ * most either event can honestly claim on its own.
375
+ *
376
+ * Note what is NOT here: any reading of `content`. The result text is the
377
+ * model's copy of the reply, in whatever shape the tool returned, and the
378
+ * client's previous attempt to find `ok: false` in it matched nothing real —
379
+ * a Cortena tool result is `{ content: [...], details }`, so the envelope it
380
+ * was looking for was two levels down when it existed at all.
381
+ */
382
+ function readResultMetadata(raw) {
383
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
384
+ const record = raw;
385
+ const meta = readMeta(record.meta);
386
+ return {
387
+ ...typeof record.toolName === "string" ? { toolName: record.toolName } : {},
388
+ isError: record.isError === true,
389
+ ...meta ? { meta } : {}
390
+ };
391
+ }
392
+ function readMeta(raw) {
393
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
394
+ const record = raw;
395
+ const meta = {
396
+ ...typeof record.status === "number" ? { status: record.status } : {},
397
+ ...typeof record.code === "string" ? { code: record.code } : {},
398
+ ...typeof record.message === "string" ? { message: record.message } : {}
399
+ };
400
+ return Object.keys(meta).length > 0 ? meta : null;
401
+ }
402
+ /**
403
+ * Is this the broker asking for a yes rather than reporting a failure?
404
+ *
405
+ * The broker refuses a catalogued write with `409 confirmation_required` until
406
+ * the call carries `confirmed: true`. It is the safety rail working, and the
407
+ * step says so instead of showing the user a red cross for it.
408
+ *
409
+ * Keyed on the CODE, never on the status. `409` is also an ordinary conflict —
410
+ * a task already claimed, a version that moved under a write — and wording one
411
+ * of those as "Needs your confirmation" invites the user to retry something
412
+ * that will never work.
413
+ */
414
+ function isConfirmationRequired(meta) {
415
+ return meta?.code === "confirmation_required";
416
+ }
417
+ /** Longest failure message put on a step. */
418
+ const ERROR_LIMIT = 200;
419
+ /**
420
+ * The human part of a tool result, for a failure that carried no `meta`.
421
+ *
422
+ * A Cortena tool returns `{ content: [{ type: "text", text }], details }`,
423
+ * JSON-stringified by the mapper, so the text blocks are the message and the
424
+ * rest is machinery. Anything else — a plain string, a shape this does not
425
+ * recognise — is used as it stands. Bounded and sanitised either way.
426
+ */
427
+ function resultText(output) {
428
+ const direct = typeof output === "string" ? output : "";
429
+ let value = output;
430
+ if (direct) try {
431
+ value = JSON.parse(direct);
432
+ } catch {
433
+ return truncate(direct, ERROR_LIMIT);
434
+ }
435
+ if (typeof value === "object" && value !== null) {
436
+ const content = value.content;
437
+ if (Array.isArray(content)) {
438
+ const text = content.map((part) => typeof part === "object" && part !== null && typeof part.text === "string" ? part.text : "").filter(Boolean).join(" ");
439
+ if (text) return truncate(text, ERROR_LIMIT);
440
+ }
441
+ }
442
+ return truncate(direct, ERROR_LIMIT);
443
+ }
444
+ /** What a failed step says: the server's message, or the result text. */
445
+ function errorMessageOf(meta, output) {
446
+ return (meta?.message ? truncate(meta.message, ERROR_LIMIT) : "") || resultText(output) || "The call failed.";
447
+ }
448
+ /** The call's input as an object, or nothing when it never parsed as one. */
449
+ function readArgs(input) {
450
+ return typeof input === "object" && input !== null && !Array.isArray(input) ? input : void 0;
451
+ }
452
+ /** Is this step still open — spinning, or waiting on the user? */
453
+ function isOpenStep(step) {
454
+ return step.status === "running" || step.status === "paused";
455
+ }
456
+ function endRunningSteps(steps, outcome) {
457
+ const closes = (step) => outcome.kind === "paused" ? step.status === "running" : outcome.kind === "denied" ? step.status === "paused" : isOpenStep(step);
458
+ if (!steps.some(closes)) return steps;
459
+ const endedAt = Date.now();
460
+ return steps.map((step) => {
461
+ if (!closes(step)) return step;
462
+ if (outcome.kind === "paused") return {
463
+ ...step,
464
+ status: "paused"
465
+ };
466
+ if (outcome.kind === "failed") return {
467
+ ...step,
468
+ status: "error",
469
+ endedAt,
470
+ errorMessage: outcome.message
471
+ };
472
+ if (outcome.kind === "stopped" || outcome.kind === "denied") return {
473
+ ...step,
474
+ status: "stopped",
475
+ endedAt
476
+ };
477
+ return {
478
+ ...step,
479
+ status: "done",
480
+ endedAt
481
+ };
482
+ });
483
+ }
484
+ /**
485
+ * A step appended, with the list capped at `STEP_LIMIT`.
486
+ *
487
+ * A turn with more calls than this is a loop, not a plan, and an uncapped list
488
+ * grows without bound in memory and re-renders the whole strip on every event
489
+ * of it. The most recent are kept: they are what the user is waiting on, and
490
+ * the ids of the ones dropped are kept so a late result for one is recognised
491
+ * rather than mistaken for a call nobody announced.
492
+ */
493
+ function append(steps, retiredSteps, step) {
494
+ const next = [...steps, step];
495
+ if (next.length <= 50) return {
496
+ steps: next,
497
+ retiredSteps
498
+ };
499
+ const dropped = next.slice(0, next.length - 50).map((s) => s.id);
500
+ return {
501
+ steps: next.slice(-50),
502
+ retiredSteps: [...retiredSteps, ...dropped].slice(-RETIRED_LIMIT)
503
+ };
504
+ }
505
+ /** Most dropped ids remembered. Enough for stragglers, not a transcript. */
506
+ const RETIRED_LIMIT = 100;
507
+ function replaceStep(steps, index, next) {
508
+ const copy = [...steps];
509
+ copy[index] = next;
510
+ return copy;
511
+ }
512
+ /**
513
+ * One tool event, folded into the step list.
514
+ *
515
+ * Start and end are paired by the AG-UI `toolCallId`, not by position: calls
516
+ * overlap, and a run that issues two searches at once ended the wrong one when
517
+ * the list was treated as a stack.
518
+ *
519
+ * The label is recomputed when the arguments land, because they land in a
520
+ * SECOND event. `TOOL_CALL_START` carries the name only, so a label fixed at
521
+ * start time is "Working: engram_search" for the whole of the call — the one
522
+ * line the wording map exists to avoid.
523
+ */
524
+ function reduceStep(state, event, id) {
525
+ const { steps, retiredSteps } = state;
526
+ const keep = {
527
+ steps,
528
+ retiredSteps
529
+ };
530
+ const index = steps.findIndex((step) => step.id === id);
531
+ const existing = index === -1 ? void 0 : steps[index];
532
+ if (!existing && retiredSteps.includes(id)) return keep;
533
+ if (event.event === "tool.start") {
534
+ if (existing && !isOpenStep(existing)) return keep;
535
+ const toolName = event.data.name ?? existing?.toolName ?? "tool";
536
+ const step = {
537
+ id,
538
+ toolName,
539
+ ...existing?.args ? { args: existing.args } : {},
540
+ label: stepLabel(toolName, existing?.args, "running"),
541
+ startedAt: event.data.startedAt ?? existing?.startedAt ?? Date.now(),
542
+ status: "running"
543
+ };
544
+ return existing && index !== -1 ? {
545
+ steps: replaceStep(steps, index, step),
546
+ retiredSteps
547
+ } : append(steps, retiredSteps, step);
548
+ }
549
+ const base = existing ?? {
550
+ id,
551
+ toolName: event.data.name ?? "tool",
552
+ label: stepLabel(event.data.name ?? "tool", void 0, "running"),
553
+ startedAt: event.data.startedAt ?? Date.now(),
554
+ status: "running"
555
+ };
556
+ const at = index === -1 ? steps.length : index;
557
+ const put = (next) => index === -1 ? append(steps, retiredSteps, next) : {
558
+ steps: replaceStep(steps, at, next),
559
+ retiredSteps
560
+ };
561
+ switch (event.event) {
562
+ case "tool.args": {
563
+ const args = readArgs(event.data.input);
564
+ if (!args) return keep;
565
+ if (base.label === CONFIRMATION_FALLBACK) return put({
566
+ ...base,
567
+ args,
568
+ label: confirmationRequiredLabel(args)
569
+ });
570
+ const open = isOpenStep(base);
571
+ const relabel = open || isGenericLabel(base.toolName, base.label);
572
+ return put({
573
+ ...base,
574
+ args,
575
+ label: relabel ? stepLabel(base.toolName, args, open ? "running" : "done") : base.label
576
+ });
577
+ }
578
+ case "tool.progress": return keep;
579
+ case "tool.complete":
580
+ case "tool.error": return put(endStep(base, event));
581
+ default: return keep;
582
+ }
583
+ }
584
+ /** The confirmation label with no operation on it; see `tool.args` above. */
585
+ const CONFIRMATION_FALLBACK = confirmationRequiredLabel(void 0);
586
+ /**
587
+ * A start event's step, ended by its result.
588
+ *
589
+ * The whole outcome decision, in one place, against
590
+ * `TOOL_CALL_RESULT.metadata` and nothing else:
591
+ *
592
+ * - `isError` with `meta.code === "confirmation_required"` — not a failure.
593
+ * The broker is asking a question, and the step is relabelled to ask it.
594
+ * - `isError` otherwise — a failure, worded from `meta.message` when there is
595
+ * one and from the result text when there is not.
596
+ * - neither — done.
597
+ */
598
+ function endStep(base, event) {
599
+ const metadata = readResultMetadata(event.data.metadata);
600
+ const isError = metadata?.isError === true || event.event === "tool.error";
601
+ const meta = metadata?.meta;
602
+ const endedAt = event.data.completedAt ?? Date.now();
603
+ if (isError && isConfirmationRequired(meta)) return {
604
+ ...base,
605
+ status: "done",
606
+ endedAt,
607
+ label: confirmationRequiredLabel(base.args)
608
+ };
609
+ if (isError) return {
610
+ ...base,
611
+ status: "error",
612
+ endedAt,
613
+ errorMessage: errorMessageOf(meta, event.data.output)
614
+ };
615
+ return {
616
+ ...base,
617
+ status: "done",
618
+ endedAt,
619
+ label: stepLabel(base.toolName, base.args, "done")
620
+ };
621
+ }
622
+ /** How long a burst of step changes is collapsed for before it is written. */
623
+ const STEP_WRITE_DEBOUNCE_MS = 250;
624
+ function useAgentChat({ client, storageScope, ephemeral, thinking }) {
625
+ const [state, dispatch] = React.useReducer(reduce, EMPTY);
626
+ const stateRef = React.useRef(state);
627
+ stateRef.current = state;
628
+ React.useEffect(() => {
629
+ const offChat = client.onChat((event) => dispatch({
630
+ kind: "chat",
631
+ event
632
+ }));
633
+ const offTool = client.onTool((event) => dispatch({
634
+ kind: "tool",
635
+ event
636
+ }));
637
+ const offApproval = client.onApproval((event) => {
638
+ if (event.type === "request") dispatch({
639
+ kind: "approval/request",
640
+ request: event.request
641
+ });
642
+ else dispatch({
643
+ kind: "approval/resolved",
644
+ id: event.id
645
+ });
646
+ });
647
+ return () => {
648
+ offChat();
649
+ offTool();
650
+ offApproval();
651
+ };
652
+ }, [client]);
653
+ const refreshSessions = React.useCallback(() => {
654
+ dispatch({ kind: "sessions/loading" });
655
+ client.request("sessions.list", { includeDerivedTitles: true }).then((response) => {
656
+ const sessions = (response?.sessions ?? []).filter((s) => isSessionOfAgent(s.key, client.agentId));
657
+ dispatch({
658
+ kind: "sessions/loaded",
659
+ sessions
660
+ });
661
+ }).catch((err) => {
662
+ dispatch({
663
+ kind: "sessions/loaded",
664
+ sessions: []
665
+ });
666
+ dispatch({
667
+ kind: "error",
668
+ message: errorText(err)
669
+ });
670
+ });
671
+ }, [client]);
672
+ const loadHistory = React.useCallback((sessionKey) => {
673
+ dispatch({ kind: "history/loading" });
674
+ client.request("chat.history", {
675
+ sessionKey,
676
+ limit: 100
677
+ }).then((response) => {
678
+ const records = response?.messages ?? [];
679
+ dispatch({
680
+ kind: "history/loaded",
681
+ sessionKey,
682
+ messages: parseHistoryMessages(records),
683
+ hasMore: response?.hasMore ?? false,
684
+ records: records.length
685
+ });
686
+ }).catch(() => dispatch({ kind: "history/failed" }));
687
+ }, [client]);
688
+ const restored = React.useRef(false);
689
+ React.useEffect(() => {
690
+ if (restored.current) return;
691
+ restored.current = true;
692
+ const stored = ephemeral ? null : readStoredSessionKey(storageScope);
693
+ const key = stored && isSessionOfAgent(stored, client.agentId) ? stored : null;
694
+ if (key) {
695
+ dispatch({
696
+ kind: "session",
697
+ key
698
+ });
699
+ const steps = ephemeral ? [] : readStoredSteps(storageScope, key);
700
+ if (steps.length > 0) dispatch({
701
+ kind: "steps/restored",
702
+ steps
703
+ });
704
+ loadHistory(key);
705
+ }
706
+ refreshSessions();
707
+ }, [
708
+ client,
709
+ ephemeral,
710
+ loadHistory,
711
+ refreshSessions,
712
+ storageScope
713
+ ]);
714
+ /**
715
+ * Keep the per-window store in step with the strip, on a debounce.
716
+ *
717
+ * Skipped while there is no session key, which is also the first commit —
718
+ * writing then would empty the slot the effect above is about to read.
719
+ *
720
+ * Debounced because a busy run changes `steps` several times per call —
721
+ * start, arguments, result — and each write serialises the whole list and
722
+ * hands it to a SYNCHRONOUS storage API on the main thread. A quarter of a
723
+ * second collapses a call's worth of edits into one write and is far below
724
+ * anything a reload could notice.
725
+ */
726
+ const pendingSteps = React.useRef(null);
727
+ React.useEffect(() => {
728
+ if (ephemeral || !state.sessionKey) return;
729
+ const sessionKey = state.sessionKey;
730
+ const steps = state.steps;
731
+ pendingSteps.current = {
732
+ sessionKey,
733
+ steps
734
+ };
735
+ const id = setTimeout(() => {
736
+ pendingSteps.current = null;
737
+ writeStoredSteps(storageScope, sessionKey, steps);
738
+ }, STEP_WRITE_DEBOUNCE_MS);
739
+ return () => clearTimeout(id);
740
+ }, [
741
+ ephemeral,
742
+ state.sessionKey,
743
+ state.steps,
744
+ storageScope
745
+ ]);
746
+ React.useEffect(() => {
747
+ return () => {
748
+ const pending = pendingSteps.current;
749
+ if (!ephemeral && pending) {
750
+ pendingSteps.current = null;
751
+ writeStoredSteps(storageScope, pending.sessionKey, pending.steps);
752
+ }
753
+ };
754
+ }, [ephemeral, storageScope]);
755
+ const setSession = React.useCallback((key) => {
756
+ dispatch({
757
+ kind: "session",
758
+ key
759
+ });
760
+ if (!ephemeral) writeStoredSessionKey(storageScope, key);
761
+ }, [ephemeral, storageScope]);
762
+ const run = React.useCallback((text, extra = {}) => {
763
+ const current = stateRef.current;
764
+ if (current.streaming) return;
765
+ const sessionKey = current.sessionKey ?? mintSessionKey(client.agentId);
766
+ if (!current.sessionKey && !ephemeral) writeStoredSessionKey(storageScope, sessionKey);
767
+ const runId = generateId();
768
+ const message = text || !extra.a2uiAction && !extra.mcpAppAction ? {
769
+ id: runId,
770
+ role: "user",
771
+ text,
772
+ timestamp: Date.now(),
773
+ runId
774
+ } : null;
775
+ dispatch({
776
+ kind: "send",
777
+ message,
778
+ runId,
779
+ sessionKey
780
+ });
781
+ client.send({
782
+ sessionKey,
783
+ message: text,
784
+ runId,
785
+ ...thinking ? { thinking } : {},
786
+ ...extra
787
+ }).then(() => refreshSessions()).catch((err) => dispatch({
788
+ kind: "send/failed",
789
+ message: errorText(err)
790
+ }));
791
+ }, [
792
+ client,
793
+ ephemeral,
794
+ refreshSessions,
795
+ storageScope,
796
+ thinking
797
+ ]);
798
+ const send = React.useCallback((text, attachments) => {
799
+ run(text, attachments?.length ? { attachments } : {});
800
+ }, [run]);
801
+ const sendAction = React.useCallback((a2uiAction) => {
802
+ run("", { a2uiAction });
803
+ }, [run]);
804
+ const abort = React.useCallback(() => {
805
+ const { runId, sessionKey } = stateRef.current;
806
+ dispatch({ kind: "abort" });
807
+ client.abort({
808
+ ...runId ? { runId } : {},
809
+ ...sessionKey ? { sessionKey } : {}
810
+ }).catch(() => {});
811
+ }, [client]);
812
+ const resolveApproval = React.useCallback((id, decision) => {
813
+ const request = stateRef.current.approvals.find((r) => r.id === id);
814
+ if (!request) return;
815
+ dispatch(decision === "deny" ? {
816
+ kind: "approval/denied",
817
+ id
818
+ } : {
819
+ kind: "approval/resolved",
820
+ id
821
+ });
822
+ client.resolveApproval(id, decision, {
823
+ runId: request.runId,
824
+ sessionKey: request.sessionKey
825
+ }).catch((err) => {
826
+ dispatch({
827
+ kind: "approval/failed",
828
+ request,
829
+ message: errorText(err)
830
+ });
831
+ });
832
+ }, [client]);
833
+ const newSession = React.useCallback(() => {
834
+ setSession(mintSessionKey(client.agentId));
835
+ }, [client, setSession]);
836
+ const resumeSession = React.useCallback((key) => {
837
+ setSession(key);
838
+ loadHistory(key);
839
+ }, [loadHistory, setSession]);
840
+ const loadOlder = React.useCallback(() => {
841
+ const current = stateRef.current;
842
+ if (!current.sessionKey || current.loadingHistory || !current.hasMoreHistory) return;
843
+ dispatch({ kind: "history/loading" });
844
+ client.request("chat.history", {
845
+ sessionKey: current.sessionKey,
846
+ limit: 100,
847
+ offset: current.historyCount
848
+ }).then((response) => {
849
+ const records = response?.messages ?? [];
850
+ dispatch({
851
+ kind: "history/older",
852
+ messages: parseHistoryMessages(records),
853
+ hasMore: response?.hasMore ?? false,
854
+ records: records.length
855
+ });
856
+ }).catch(() => dispatch({ kind: "history/failed" }));
857
+ }, [client]);
858
+ return {
859
+ ...state,
860
+ send,
861
+ sendAction,
862
+ abort,
863
+ resolveApproval,
864
+ newSession,
865
+ resumeSession,
866
+ loadOlder,
867
+ refreshSessions
868
+ };
869
+ }
870
+ function errorText(err) {
871
+ return err instanceof Error ? err.message : "The agent could not be reached";
872
+ }
873
+ //#endregion
874
+ export { readResultMetadata, useAgentChat };
875
+
876
+ //# sourceMappingURL=store.js.map