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 @@
1
+ {"version":3,"file":"store.js","names":[],"sources":["../../src/agent-chat/store.ts"],"sourcesContent":["\"use client\";\n\n/**\n * The chat state, and the hook that drives an `AgentChatClient` from React.\n *\n * The accumulation rules are ported from `cortena-shared/src/stores/use-chat.ts`\n * (Cortena monorepo) so a run leaves this state in the same place it leaves\n * cortenaweb's. The ones that are not obvious, and that a rewrite gets wrong:\n *\n * - A `delta` carries a snapshot, so text and A2UI blocks are **replaced**, not\n * appended. Appending doubles every character.\n * - Thinking is never replaced by something shorter. The upstream emitter can\n * fall back to increments when a snapshot is unavailable, and treating one of\n * those as a snapshot truncates the accumulated chain of thought.\n * - On `final`, a second assistant message in the same turn folds the previous\n * visible text into the chain of thought rather than adding a second bubble,\n * and keeps the A2UI blocks of the message it folded away — the surface is\n * still on screen and still interactive.\n * - A reply that is only a drawn surface has no text, so `text || a2ui.length`\n * is the test for \"there is something to show\". Testing text alone silently\n * dropped everything the agent drew.\n */\n\nimport * as React from \"react\";\nimport { type A2UIChatBlock, extractA2UIBlocks, foldA2UIBlocks } from \"./a2ui-block\";\nimport {\n isProvisionalSessionKey,\n isSessionOfAgent,\n mintSessionKey,\n parseHistoryMessages,\n readStoredSessionKey,\n readStoredSteps,\n separateThinking,\n STEP_LIMIT,\n wouldShrinkHistory,\n writeStoredSessionKey,\n writeStoredSteps,\n} from \"./session\";\nimport { confirmationRequiredLabel, isGenericLabel, stepLabel, truncate } from \"./step-label\";\nimport type {\n AgentApprovalDecision,\n AgentApprovalRequest,\n AgentChatClient,\n AgentChatEvent,\n AgentChatHistoryResponse,\n AgentChatMessage,\n AgentChatStep,\n AgentSession,\n AgentSessionsListResponse,\n AgentToolEvent,\n AgentToolResultMeta,\n AgentToolResultMetadata,\n} from \"./types\";\n\nexport interface AgentToolEntry {\n id: string;\n name: string;\n status: \"running\" | \"complete\" | \"error\";\n input?: unknown;\n output?: unknown;\n startedAt: number;\n completedAt?: number;\n}\n\nexport interface AgentChatState {\n sessionKey: string | null;\n messages: AgentChatMessage[];\n streaming: boolean;\n streamText: string;\n streamThinkingText: string;\n streamA2ui: A2UIChatBlock[];\n runId: string | null;\n hasMoreHistory: boolean;\n loadingHistory: boolean;\n toolOrder: string[];\n tools: Record<string, AgentToolEntry>;\n /**\n * The turn's tool calls in plain words, in arrival order (SKILLS-9).\n *\n * The same calls as `tools`, worded rather than dumped, and rendered under\n * the assistant turn instead of in the strip at the foot. Cleared when the\n * next run starts, so the list is always \"what is happening now\" or \"what\n * just happened\", never a transcript.\n */\n steps: AgentChatStep[];\n /**\n * Ids of steps the `STEP_LIMIT` cap has already dropped.\n *\n * A runaway turn pushes the first calls off the front of the list, and their\n * results arrive afterwards. Without this the store saw an id it did not\n * know, took it for a call nobody announced, and appended a fresh\n * \"Working: tool\" line at the BOTTOM of the strip — the oldest call in the\n * turn, drawn as the newest, with no words on it. Bounded, because it exists\n * to recognise a handful of stragglers, not to keep a transcript.\n */\n retiredSteps: string[];\n approvals: AgentApprovalRequest[];\n /**\n * Transcript RECORDS loaded, which is not `messages.length`.\n *\n * `parseHistoryMessages` folds a turn — an assistant preamble, the tool call,\n * the tool result, the answer — into one visible message, so the two numbers\n * diverge on the very first agent turn. `offset` is an offset into the\n * server's records, and paging with the folded count asked for records the\n * screen already had and skipped the ones it did not: \"load older\" silently\n * lost messages the further back a user read.\n */\n historyCount: number;\n sessions: AgentSession[];\n loadingSessions: boolean;\n /** A failure the user has to be told about, e.g. no template for the slug. */\n error: string | null;\n}\n\nconst EMPTY: AgentChatState = {\n sessionKey: null,\n messages: [],\n streaming: false,\n streamText: \"\",\n streamThinkingText: \"\",\n streamA2ui: [],\n runId: null,\n hasMoreHistory: false,\n loadingHistory: false,\n toolOrder: [],\n tools: {},\n steps: [],\n retiredSteps: [],\n approvals: [],\n historyCount: 0,\n sessions: [],\n loadingSessions: false,\n error: null,\n};\n\ntype Action =\n | { kind: \"chat\"; event: AgentChatEvent }\n | { kind: \"tool\"; event: AgentToolEvent }\n | { kind: \"approval/request\"; request: AgentApprovalRequest }\n | { kind: \"approval/resolved\"; id: string }\n | { kind: \"approval/denied\"; id: string }\n | { kind: \"approval/failed\"; request: AgentApprovalRequest; message: string }\n | { kind: \"session\"; key: string | null }\n | { kind: \"send\"; message: AgentChatMessage | null; runId: string; sessionKey: string }\n | { kind: \"send/failed\"; message: string }\n | { kind: \"abort\" }\n | { kind: \"history/loading\" }\n | {\n kind: \"history/loaded\";\n sessionKey: string;\n messages: AgentChatMessage[];\n hasMore: boolean;\n records: number;\n }\n | { kind: \"history/older\"; messages: AgentChatMessage[]; hasMore: boolean; records: number }\n | { kind: \"history/failed\" }\n | { kind: \"sessions/loading\" }\n | { kind: \"sessions/loaded\"; sessions: AgentSession[] }\n | { kind: \"steps/restored\"; steps: AgentChatStep[] }\n | { kind: \"error\"; message: string | null };\n\nfunction generateId(): string {\n return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;\n}\n\nfunction extractText(message?: AgentChatEvent[\"message\"]): string {\n return (message?.content ?? [])\n .filter((c) => c.type === \"text\" && c.text)\n .map((c) => c.text!)\n .join(\"\");\n}\n\nfunction extractThinking(message?: AgentChatEvent[\"message\"]): string {\n const blocks = (message?.content ?? [])\n .filter((c) => c.type === \"thinking\")\n .map((c) => c.thinking ?? c.text ?? \"\")\n .filter(Boolean);\n return blocks.join(\"\\n\\n\");\n}\n\nfunction reduce(state: AgentChatState, action: Action): AgentChatState {\n switch (action.kind) {\n case \"chat\":\n return reduceChat(state, action.event);\n case \"tool\":\n return reduceTool(state, action.event);\n case \"approval/request\":\n return state.approvals.some((r) => r.id === action.request.id)\n ? state\n : { ...state, approvals: [...state.approvals, action.request] };\n case \"approval/resolved\":\n return { ...state, approvals: state.approvals.filter((r) => r.id !== action.id) };\n case \"approval/denied\":\n // The user said no. The call the run paused on is never going to happen,\n // so it stops waiting — `stopped`, not `error`, for the same reason the\n // Stop button is: nothing failed. Steps still RUNNING are untouched; a\n // deny answers one question, it does not end the rest of the turn.\n return {\n ...state,\n approvals: state.approvals.filter((r) => r.id !== action.id),\n steps: endRunningSteps(state.steps, { kind: \"denied\" }),\n };\n case \"approval/failed\":\n // The decision did not land, so the card comes back — with the reason on\n // it. Removing it optimistically and then swallowing the failure left\n // the user certain they had answered and the run paused for ever.\n return state.approvals.some((r) => r.id === action.request.id)\n ? state\n : {\n ...state,\n approvals: [...state.approvals, { ...action.request, error: action.message }],\n };\n case \"session\":\n return action.key === state.sessionKey\n ? state\n : { ...EMPTY, sessions: state.sessions, sessionKey: action.key };\n case \"send\":\n return {\n ...state,\n sessionKey: action.sessionKey,\n messages: action.message ? [...state.messages, action.message] : state.messages,\n streaming: true,\n streamText: \"\",\n streamThinkingText: \"\",\n streamA2ui: [],\n runId: action.runId,\n toolOrder: [],\n tools: {},\n // The steps of the previous turn describe work that is over. Leaving\n // them under the next answer reads as though the agent were still\n // doing them.\n steps: [],\n retiredSteps: [],\n // A new run is a new set of pending decisions. An approval card left\n // over from the previous run points at a run that is over, and\n // answering it sends a decision nothing is waiting for.\n approvals: [],\n error: null,\n };\n case \"send/failed\":\n return {\n ...state,\n streaming: false,\n runId: null,\n messages: [\n ...state.messages,\n {\n id: generateId(),\n role: \"assistant\",\n text: action.message,\n timestamp: Date.now(),\n isError: true,\n },\n ],\n };\n case \"abort\":\n // Reset immediately so the composer flips Stop -> Send without waiting on\n // a server event an aborted run does not reliably send.\n return {\n ...state,\n streaming: false,\n streamText: \"\",\n streamThinkingText: \"\",\n streamA2ui: [],\n runId: null,\n // The run they belonged to is gone; the decisions go with it.\n approvals: [],\n // A stopped run sends no end event for the call in flight, so a step\n // left running spins for ever under an answer that will never come.\n // Stopped, not failed: the user asked for it.\n steps: endRunningSteps(state.steps, { kind: \"stopped\" }),\n };\n case \"history/loading\":\n return { ...state, loadingHistory: true };\n case \"history/loaded\":\n if (\n wouldShrinkHistory({\n currentSessionKey: state.sessionKey,\n loadedSessionKey: action.sessionKey,\n currentCount: state.messages.length,\n loadedCount: action.messages.length,\n })\n ) {\n return {\n ...state,\n sessionKey: action.sessionKey,\n hasMoreHistory: action.hasMore,\n loadingHistory: false,\n historyCount: action.records,\n };\n }\n return {\n ...state,\n sessionKey: action.sessionKey,\n messages: action.messages,\n hasMoreHistory: action.hasMore,\n loadingHistory: false,\n historyCount: action.records,\n // Avoids an orphan streaming bubble beside reloaded history.\n streaming: false,\n streamText: \"\",\n streamThinkingText: \"\",\n streamA2ui: [],\n };\n case \"history/older\":\n return {\n ...state,\n messages: [...action.messages, ...state.messages],\n hasMoreHistory: action.hasMore,\n loadingHistory: false,\n historyCount: state.historyCount + action.records,\n };\n case \"history/failed\":\n return { ...state, loadingHistory: false };\n case \"sessions/loading\":\n return { ...state, loadingSessions: true };\n case \"sessions/loaded\":\n return { ...state, sessions: action.sessions, loadingSessions: false };\n case \"steps/restored\":\n // Only ever into an empty strip. A restore that arrives after the user\n // has already started a new run would otherwise resurrect the last one.\n return state.steps.length > 0 ? state : { ...state, steps: action.steps.slice(-STEP_LIMIT) };\n case \"error\":\n return { ...state, error: action.message };\n default:\n return state;\n }\n}\n\nfunction reduceChat(state: AgentChatState, event: AgentChatEvent): AgentChatState {\n // Events from another session are dropped, or one window's run leaks into\n // another window's transcript. A provisional key is the exception: the server\n // echoes it back today, but if it ever canonicalised the key instead, dropping\n // the event would silently kill the whole first turn.\n if (event.sessionKey && state.sessionKey && event.sessionKey !== state.sessionKey) {\n if (!isProvisionalSessionKey(state.sessionKey)) {\n return state;\n }\n state = { ...state, sessionKey: event.sessionKey };\n } else if (event.sessionKey && !state.sessionKey) {\n state = { ...state, sessionKey: event.sessionKey };\n }\n\n switch (event.state) {\n case \"delta\": {\n const text = extractText(event.message);\n const thinking = extractThinking(event.message);\n const a2ui = extractA2UIBlocks(event.message);\n const next: AgentChatState = { ...state, streaming: true };\n if (text) {\n next.streamText = text;\n }\n if (a2ui.length > 0) {\n next.streamA2ui = a2ui;\n }\n // Never replace accumulated thinking with something shorter.\n const acceptThinking = !!thinking && thinking.length >= state.streamThinkingText.length;\n if (acceptThinking) {\n next.streamThinkingText = thinking;\n }\n return text || acceptThinking || a2ui.length > 0 ? next : state;\n }\n case \"final\": {\n const finalText = extractText(event.message);\n const finalThinking = extractThinking(event.message);\n const finalA2ui = extractA2UIBlocks(event.message);\n const text = finalText || state.streamText;\n const thinkingFromBlocks = finalThinking || state.streamThinkingText;\n const a2ui = foldA2UIBlocks(finalA2ui.length > 0 ? finalA2ui : state.streamA2ui);\n // The run id deliberately SURVIVES a final. A run can end more than\n // once — an interrupt for an approval, then the continuation — and\n // clearing it here meant the second final saw `runId: null`, could not\n // fold, and pushed a second bubble: one answer to one question, drawn\n // as two. `send` sets the next run's id, and `abort`, `error` and\n // `send/failed` each clear it, so nothing outlives the turn it belongs\n // to.\n const cleared = {\n streaming: false,\n streamText: \"\",\n streamThinkingText: \"\",\n streamA2ui: [] as A2UIChatBlock[],\n // The run is over, so nothing is still running. A result event can go\n // missing — a reconnect drops the tail — and the spinner then outlived\n // the answer it was drawn beside. The label is untouched: a plain\n // RUN_FINISHED means the run succeeded, so the step did too.\n //\n // An INTERRUPT is not that. cortenacore ends the run to ask for an\n // approval or to hand a frontend tool to the client, and the tool call\n // is still open: the continuation is a second run that ends it. Ticking\n // it here claimed a write had gone through while the card asking\n // permission for it was still on screen.\n steps: endRunningSteps(\n state.steps,\n event.interrupted ? { kind: \"paused\" } : { kind: \"finished\" },\n ),\n };\n\n if (!text && a2ui.length === 0) {\n // No text and nothing drawn: fold whatever thinking accumulated into\n // the previous turn rather than dropping it.\n const accumulated = state.streamThinkingText;\n const last = state.messages[state.messages.length - 1];\n if (!accumulated || !foldsInto(last, state.runId)) {\n return { ...state, ...cleared };\n }\n const messages = [...state.messages];\n messages[messages.length - 1] = {\n ...last,\n timestamp: Date.now(),\n thinkingText: [last.thinkingText ?? \"\", accumulated].filter(Boolean).join(\"\\n\\n\"),\n };\n return { ...state, ...cleared, messages };\n }\n\n const { thinkingText: tagThinking, mainText } = separateThinking(text);\n const thinkingText = thinkingFromBlocks || tagThinking;\n const displayText = thinkingFromBlocks ? text : mainText;\n const messages = [...state.messages];\n const last = messages[messages.length - 1];\n\n if (foldsInto(last, state.runId)) {\n const combined = [last.thinkingText ?? \"\", last.text.trim()].filter(Boolean).join(\"\\n\\n\");\n const updated = [combined, thinkingText].filter(Boolean).join(\"\\n\\n\");\n messages[messages.length - 1] = {\n ...last,\n text: displayText,\n timestamp: Date.now(),\n ...(updated ? { thinkingText: updated } : { thinkingText: undefined }),\n ...(a2ui.length > 0 || last.a2ui?.length\n ? { a2ui: foldA2UIBlocks([...(last.a2ui ?? []), ...a2ui]) }\n : {}),\n };\n } else {\n messages.push({\n id: generateId(),\n role: \"assistant\",\n text: displayText,\n timestamp: Date.now(),\n // The run this bubble belongs to, so the NEXT run's `final` cannot\n // fold its answer into it.\n ...(state.runId ? { runId: state.runId } : {}),\n ...(thinkingText ? { thinkingText } : {}),\n ...(a2ui.length > 0 ? { a2ui } : {}),\n });\n }\n return { ...state, ...cleared, messages };\n }\n case \"error\":\n return {\n ...state,\n streaming: false,\n streamText: \"\",\n streamThinkingText: \"\",\n streamA2ui: [],\n runId: null,\n // A RUN_ERROR is the end of every call still in flight, whether or not\n // the transport got round to sending a result for it.\n steps: endRunningSteps(state.steps, {\n kind: \"failed\",\n message: truncate(event.errorMessage ?? \"The run failed.\", ERROR_LIMIT),\n }),\n messages: [\n ...state.messages,\n {\n id: generateId(),\n role: \"assistant\",\n text: event.errorMessage ?? \"An error occurred\",\n timestamp: Date.now(),\n isError: true,\n },\n ],\n };\n default:\n return state;\n }\n}\n\n/**\n * May a `final` fold into this message?\n *\n * Only when it is an assistant bubble from the SAME run. Two assistant\n * messages in one turn are one answer with an interruption in the middle, and\n * folding them is right. Two assistant messages from different runs are two\n * answers to two questions, and folding them rewrote the earlier answer with\n * the later one — visibly, in a transcript the user had already read.\n *\n * A message with no `runId` is history, which never folds either.\n */\nfunction foldsInto(\n last: AgentChatMessage | undefined,\n runId: string | null,\n): last is AgentChatMessage {\n return (\n last !== undefined &&\n last.role === \"assistant\" &&\n !last.isError &&\n runId !== null &&\n last.runId === runId\n );\n}\n\nfunction reduceTool(state: AgentChatState, event: AgentToolEvent): AgentChatState {\n const id = event.data.id;\n if (!id) {\n return state;\n }\n const existing = state.tools[id];\n const base: AgentToolEntry = existing ?? {\n id,\n name: event.data.name ?? \"tool\",\n status: \"running\",\n startedAt: event.data.startedAt ?? Date.now(),\n };\n let entry: AgentToolEntry;\n switch (event.event) {\n case \"tool.start\":\n entry = { ...base, name: event.data.name ?? base.name, status: \"running\" };\n break;\n case \"tool.args\":\n entry = { ...base, input: event.data.input };\n break;\n case \"tool.progress\":\n entry = { ...base, output: event.data.output };\n break;\n case \"tool.complete\":\n entry = {\n ...base,\n status: \"complete\",\n output: event.data.output,\n completedAt: event.data.completedAt ?? Date.now(),\n };\n break;\n case \"tool.error\":\n entry = {\n ...base,\n status: \"error\",\n output: event.data.output,\n completedAt: event.data.completedAt ?? Date.now(),\n };\n break;\n default:\n return state;\n }\n const stepped = reduceStep(state, event, id);\n return {\n ...state,\n tools: { ...state.tools, [id]: entry },\n toolOrder: state.toolOrder.includes(id) ? state.toolOrder : [...state.toolOrder, id],\n steps: stepped.steps,\n retiredSteps: stepped.retiredSteps,\n };\n}\n\n/* ── steps ───────────────────────────────────────────────────────────────── */\n\n/** The step list and the retired ids, which only ever move together. */\ninterface StepUpdate {\n steps: AgentChatStep[];\n retiredSteps: string[];\n}\n\n/**\n * `TOOL_CALL_RESULT.metadata`, read defensively.\n *\n * `null` when nothing usable arrived, which a transport that predates the\n * contract will produce. A step with no metadata at all is treated as a\n * success by `tool.complete` and as a failure by `tool.error`, which is the\n * most either event can honestly claim on its own.\n *\n * Note what is NOT here: any reading of `content`. The result text is the\n * model's copy of the reply, in whatever shape the tool returned, and the\n * client's previous attempt to find `ok: false` in it matched nothing real —\n * a Cortena tool result is `{ content: [...], details }`, so the envelope it\n * was looking for was two levels down when it existed at all.\n */\nexport function readResultMetadata(raw: unknown): AgentToolResultMetadata | null {\n if (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n return null;\n }\n const record = raw as Record<string, unknown>;\n const meta = readMeta(record.meta);\n return {\n ...(typeof record.toolName === \"string\" ? { toolName: record.toolName } : {}),\n isError: record.isError === true,\n ...(meta ? { meta } : {}),\n };\n}\n\nfunction readMeta(raw: unknown): AgentToolResultMeta | null {\n if (typeof raw !== \"object\" || raw === null || Array.isArray(raw)) {\n return null;\n }\n const record = raw as Record<string, unknown>;\n const meta: AgentToolResultMeta = {\n ...(typeof record.status === \"number\" ? { status: record.status } : {}),\n ...(typeof record.code === \"string\" ? { code: record.code } : {}),\n ...(typeof record.message === \"string\" ? { message: record.message } : {}),\n };\n return Object.keys(meta).length > 0 ? meta : null;\n}\n\n/**\n * Is this the broker asking for a yes rather than reporting a failure?\n *\n * The broker refuses a catalogued write with `409 confirmation_required` until\n * the call carries `confirmed: true`. It is the safety rail working, and the\n * step says so instead of showing the user a red cross for it.\n *\n * Keyed on the CODE, never on the status. `409` is also an ordinary conflict —\n * a task already claimed, a version that moved under a write — and wording one\n * of those as \"Needs your confirmation\" invites the user to retry something\n * that will never work.\n */\nfunction isConfirmationRequired(meta: AgentToolResultMeta | undefined): boolean {\n return meta?.code === \"confirmation_required\";\n}\n\n/** Longest failure message put on a step. */\nconst ERROR_LIMIT = 200;\n\n/**\n * The human part of a tool result, for a failure that carried no `meta`.\n *\n * A Cortena tool returns `{ content: [{ type: \"text\", text }], details }`,\n * JSON-stringified by the mapper, so the text blocks are the message and the\n * rest is machinery. Anything else — a plain string, a shape this does not\n * recognise — is used as it stands. Bounded and sanitised either way.\n */\nfunction resultText(output: unknown): string {\n const direct = typeof output === \"string\" ? output : \"\";\n let value: unknown = output;\n if (direct) {\n try {\n value = JSON.parse(direct);\n } catch {\n return truncate(direct, ERROR_LIMIT);\n }\n }\n if (typeof value === \"object\" && value !== null) {\n const content = (value as { content?: unknown }).content;\n if (Array.isArray(content)) {\n const text = content\n .map((part) =>\n typeof part === \"object\" &&\n part !== null &&\n typeof (part as { text?: unknown }).text === \"string\"\n ? (part as { text: string }).text\n : \"\",\n )\n .filter(Boolean)\n .join(\" \");\n if (text) {\n return truncate(text, ERROR_LIMIT);\n }\n }\n }\n return truncate(direct, ERROR_LIMIT);\n}\n\n/** What a failed step says: the server's message, or the result text. */\nfunction errorMessageOf(meta: AgentToolResultMeta | undefined, output: unknown): string {\n const fromMeta = meta?.message ? truncate(meta.message, ERROR_LIMIT) : \"\";\n return fromMeta || resultText(output) || \"The call failed.\";\n}\n\n/** The call's input as an object, or nothing when it never parsed as one. */\nfunction readArgs(input: unknown): Record<string, unknown> | undefined {\n return typeof input === \"object\" && input !== null && !Array.isArray(input)\n ? (input as Record<string, unknown>)\n : undefined;\n}\n\n/**\n * Close every step still running, however the run ended.\n *\n * A step's spinner is a promise that something is happening, and the transport\n * does not keep it: a run that errors, is stopped, or simply finishes with a\n * result event lost on the way leaves a line spinning for ever under an answer\n * that has already arrived. Three outcomes, because they are three different\n * facts about the user's work:\n *\n * - `failed` — the run itself broke. Red, with the reason.\n * - `stopped` — the user pressed Stop. Neutral: nothing went wrong.\n * - `finished` — a plain RUN_FINISHED. The run SUCCEEDED, so the calls it made\n * succeeded too; the words they finished with are still true, and only the\n * spinner changes.\n * - `paused` — RUN_FINISHED with `outcome.type === \"interrupt\"`. The call is\n * still open and the run will resume, so the step neither ticks nor spins:\n * it waits, visibly, and the result closes it later.\n * - `denied` — the user refused the approval the run paused on. Only PAUSED\n * steps end, and they end `stopped`.\n *\n * `running` and `paused` are both OPEN, which is what the first three outcomes\n * close: an abort or a run error while a step waits for an approval must not\n * leave it waiting for ever.\n */\ntype StepOutcome =\n | { kind: \"failed\"; message: string }\n | { kind: \"stopped\" }\n | { kind: \"finished\" }\n | { kind: \"paused\" }\n | { kind: \"denied\" };\n\n/** Is this step still open — spinning, or waiting on the user? */\nfunction isOpenStep(step: AgentChatStep): boolean {\n return step.status === \"running\" || step.status === \"paused\";\n}\n\nfunction endRunningSteps(steps: AgentChatStep[], outcome: StepOutcome): AgentChatStep[] {\n const closes = (step: AgentChatStep) =>\n outcome.kind === \"paused\"\n ? step.status === \"running\"\n : outcome.kind === \"denied\"\n ? step.status === \"paused\"\n : isOpenStep(step);\n if (!steps.some(closes)) {\n return steps;\n }\n const endedAt = Date.now();\n return steps.map((step) => {\n if (!closes(step)) {\n return step;\n }\n if (outcome.kind === \"paused\") {\n // No `endedAt`: nothing has ended. It is the absence of `running` that\n // stops the strip's ticking counter.\n return { ...step, status: \"paused\" as const };\n }\n if (outcome.kind === \"failed\") {\n return { ...step, status: \"error\" as const, endedAt, errorMessage: outcome.message };\n }\n if (outcome.kind === \"stopped\" || outcome.kind === \"denied\") {\n return { ...step, status: \"stopped\" as const, endedAt };\n }\n return { ...step, status: \"done\" as const, endedAt };\n });\n}\n\n/**\n * A step appended, with the list capped at `STEP_LIMIT`.\n *\n * A turn with more calls than this is a loop, not a plan, and an uncapped list\n * grows without bound in memory and re-renders the whole strip on every event\n * of it. The most recent are kept: they are what the user is waiting on, and\n * the ids of the ones dropped are kept so a late result for one is recognised\n * rather than mistaken for a call nobody announced.\n */\nfunction append(\n steps: AgentChatStep[],\n retiredSteps: string[],\n step: AgentChatStep,\n): StepUpdate {\n const next = [...steps, step];\n if (next.length <= STEP_LIMIT) {\n return { steps: next, retiredSteps };\n }\n const dropped = next.slice(0, next.length - STEP_LIMIT).map((s) => s.id);\n return {\n steps: next.slice(-STEP_LIMIT),\n retiredSteps: [...retiredSteps, ...dropped].slice(-RETIRED_LIMIT),\n };\n}\n\n/** Most dropped ids remembered. Enough for stragglers, not a transcript. */\nconst RETIRED_LIMIT = 100;\n\nfunction replaceStep(\n steps: AgentChatStep[],\n index: number,\n next: AgentChatStep,\n): AgentChatStep[] {\n const copy = [...steps];\n copy[index] = next;\n return copy;\n}\n\n/**\n * One tool event, folded into the step list.\n *\n * Start and end are paired by the AG-UI `toolCallId`, not by position: calls\n * overlap, and a run that issues two searches at once ended the wrong one when\n * the list was treated as a stack.\n *\n * The label is recomputed when the arguments land, because they land in a\n * SECOND event. `TOOL_CALL_START` carries the name only, so a label fixed at\n * start time is \"Working: engram_search\" for the whole of the call — the one\n * line the wording map exists to avoid.\n */\nfunction reduceStep(\n state: AgentChatState,\n event: AgentToolEvent,\n id: string,\n): StepUpdate {\n const { steps, retiredSteps } = state;\n const keep: StepUpdate = { steps, retiredSteps };\n const index = steps.findIndex((step) => step.id === id);\n const existing = index === -1 ? undefined : steps[index];\n\n // A call the cap already dropped. Its result is arriving after fifty other\n // calls pushed it off the front, and there is nothing left to update: the\n // only alternatives are a stale line back at the bottom of the strip or\n // silence, and silence about the fiftieth-oldest call of a runaway turn is\n // the right answer.\n if (!existing && retiredSteps.includes(id)) {\n return keep;\n }\n\n if (event.event === \"tool.start\") {\n // A start for a call that has already ENDED is a replay, not a restart.\n // A reconnect re-streams the run from the top, so every call of it starts\n // again; taking that at face value put a finished run's steps back into\n // spinners, with no second result coming to take them out again.\n if (existing && !isOpenStep(existing)) {\n return keep;\n }\n const toolName = event.data.name ?? existing?.toolName ?? \"tool\";\n const step: AgentChatStep = {\n id,\n toolName,\n ...(existing?.args ? { args: existing.args } : {}),\n label: stepLabel(toolName, existing?.args, \"running\"),\n startedAt: event.data.startedAt ?? existing?.startedAt ?? Date.now(),\n status: \"running\",\n };\n return existing && index !== -1\n ? { steps: replaceStep(steps, index, step), retiredSteps }\n : append(steps, retiredSteps, step);\n }\n\n // Anything else about a call nobody announced. The strip still shows it —\n // a result with no start is a call that happened, and silence about it is\n // worse than a line with a generic label.\n const base: AgentChatStep = existing ?? {\n id,\n toolName: event.data.name ?? \"tool\",\n label: stepLabel(event.data.name ?? \"tool\", undefined, \"running\"),\n startedAt: event.data.startedAt ?? Date.now(),\n status: \"running\",\n };\n const at = index === -1 ? steps.length : index;\n const put = (next: AgentChatStep): StepUpdate =>\n index === -1\n ? append(steps, retiredSteps, next)\n : { steps: replaceStep(steps, at, next), retiredSteps };\n\n switch (event.event) {\n case \"tool.args\": {\n const args = readArgs(event.data.input);\n if (!args) {\n return keep;\n }\n // The broker asked for a confirmation before the arguments landed, so\n // the question it asked is \"that action\". It has a name now, and the\n // card the user is reading is the one place the name matters.\n if (base.label === CONFIRMATION_FALLBACK) {\n return put({ ...base, args, label: confirmationRequiredLabel(args) });\n }\n // A finished step keeps the words it finished with — unless those words\n // are the fallback. Arguments can land after the result on a short call,\n // and \"Working: broker_invoke\", ticked, is the one line the wording map\n // exists to avoid; a real sentence is better late than never. A label\n // the arguments already produced is never rewritten.\n const open = isOpenStep(base);\n const relabel = open || isGenericLabel(base.toolName, base.label);\n return put({\n ...base,\n args,\n label: relabel ? stepLabel(base.toolName, args, open ? \"running\" : \"done\") : base.label,\n });\n }\n case \"tool.progress\":\n return keep;\n case \"tool.complete\":\n case \"tool.error\":\n return put(endStep(base, event));\n default:\n return keep;\n }\n}\n\n/** The confirmation label with no operation on it; see `tool.args` above. */\nconst CONFIRMATION_FALLBACK = confirmationRequiredLabel(undefined);\n\n/**\n * A start event's step, ended by its result.\n *\n * The whole outcome decision, in one place, against\n * `TOOL_CALL_RESULT.metadata` and nothing else:\n *\n * - `isError` with `meta.code === \"confirmation_required\"` — not a failure.\n * The broker is asking a question, and the step is relabelled to ask it.\n * - `isError` otherwise — a failure, worded from `meta.message` when there is\n * one and from the result text when there is not.\n * - neither — done.\n */\nfunction endStep(base: AgentChatStep, event: AgentToolEvent): AgentChatStep {\n const metadata: AgentToolResultMetadata | null = readResultMetadata(event.data.metadata);\n // Either witness is enough. `tool.error` is what the bridge emits when the\n // metadata says `isError`, but it is also what a transport that predates the\n // contract emits with no metadata at all — and a metadata object that\n // arrived without an `isError` field then made `isError` false and drew a\n // failed call as a success.\n const isError = metadata?.isError === true || event.event === \"tool.error\";\n const meta: AgentToolResultMeta | undefined = metadata?.meta;\n const endedAt = event.data.completedAt ?? Date.now();\n\n if (isError && isConfirmationRequired(meta)) {\n return { ...base, status: \"done\", endedAt, label: confirmationRequiredLabel(base.args) };\n }\n if (isError) {\n return { ...base, status: \"error\", endedAt, errorMessage: errorMessageOf(meta, event.data.output) };\n }\n return { ...base, status: \"done\", endedAt, label: stepLabel(base.toolName, base.args, \"done\") };\n}\n\n/* ── the hook ────────────────────────────────────────────────────────────── */\n\n/** How long a burst of step changes is collapsed for before it is written. */\nconst STEP_WRITE_DEBOUNCE_MS = 250;\n\nexport interface UseAgentChatOptions {\n client: AgentChatClient;\n /**\n * Names the `sessionStorage` slot the current session key lives in. Two\n * pop-ups with different scopes never share a key, which is what keeps one\n * extension's chat out of another's.\n */\n storageScope: string;\n /** Skip reading and writing `sessionStorage`; for a transient mount. */\n ephemeral?: boolean;\n thinking?: \"low\" | \"medium\" | \"high\";\n}\n\nexport interface UseAgentChatResult extends AgentChatState {\n send: (text: string, attachments?: unknown[]) => void;\n sendAction: (action: NonNullable<Parameters<AgentChatClient[\"send\"]>[0][\"a2uiAction\"]>) => void;\n abort: () => void;\n resolveApproval: (id: string, decision: AgentApprovalDecision) => void;\n newSession: () => void;\n resumeSession: (key: string) => void;\n loadOlder: () => void;\n refreshSessions: () => void;\n}\n\nexport function useAgentChat({\n client,\n storageScope,\n ephemeral,\n thinking,\n}: UseAgentChatOptions): UseAgentChatResult {\n const [state, dispatch] = React.useReducer(reduce, EMPTY);\n const stateRef = React.useRef(state);\n stateRef.current = state;\n\n // Subscribe once per client. A collapsed pop-up keeps this mounted — the\n // panel is hidden, not unmounted — so a run started before the collapse\n // keeps streaming into the state the pill's unread dot is read from.\n React.useEffect(() => {\n const offChat = client.onChat((event) => dispatch({ kind: \"chat\", event }));\n const offTool = client.onTool((event) => dispatch({ kind: \"tool\", event }));\n const offApproval = client.onApproval((event) => {\n if (event.type === \"request\") {\n dispatch({ kind: \"approval/request\", request: event.request });\n } else {\n dispatch({ kind: \"approval/resolved\", id: event.id });\n }\n });\n return () => {\n offChat();\n offTool();\n offApproval();\n };\n }, [client]);\n\n const refreshSessions = React.useCallback(() => {\n dispatch({ kind: \"sessions/loading\" });\n void client\n .request<AgentSessionsListResponse>(\"sessions.list\", { includeDerivedTitles: true })\n .then((response) => {\n // Filtered to this agent: a shared control plane lists every session on\n // the pod, and an extension must never show another agent's chats.\n const sessions = (response?.sessions ?? []).filter((s) =>\n isSessionOfAgent(s.key, client.agentId),\n );\n dispatch({ kind: \"sessions/loaded\", sessions });\n })\n .catch((err: unknown) => {\n dispatch({ kind: \"sessions/loaded\", sessions: [] });\n dispatch({ kind: \"error\", message: errorText(err) });\n });\n }, [client]);\n\n const loadHistory = React.useCallback(\n (sessionKey: string) => {\n dispatch({ kind: \"history/loading\" });\n void client\n .request<AgentChatHistoryResponse>(\"chat.history\", { sessionKey, limit: 100 })\n .then((response) => {\n const records = response?.messages ?? [];\n dispatch({\n kind: \"history/loaded\",\n sessionKey,\n messages: parseHistoryMessages(records),\n hasMore: response?.hasMore ?? false,\n records: records.length,\n });\n })\n .catch(() => dispatch({ kind: \"history/failed\" }));\n },\n [client],\n );\n\n // Restore this window's session, or mint one. `sessionStorage`, not\n // `localStorage`: a second window must not adopt the first window's session.\n const restored = React.useRef(false);\n React.useEffect(() => {\n if (restored.current) {\n return;\n }\n restored.current = true;\n const stored = ephemeral ? null : readStoredSessionKey(storageScope);\n const key = stored && isSessionOfAgent(stored, client.agentId) ? stored : null;\n if (key) {\n dispatch({ kind: \"session\", key });\n // Before the history load, and after the `session` reset it would\n // otherwise be cleared by. `chat.history` has no steps in it — the\n // server never sees them — so this slot is the only way back.\n const steps = ephemeral ? [] : readStoredSteps(storageScope, key);\n if (steps.length > 0) {\n dispatch({ kind: \"steps/restored\", steps });\n }\n loadHistory(key);\n }\n refreshSessions();\n }, [client, ephemeral, loadHistory, refreshSessions, storageScope]);\n\n /**\n * Keep the per-window store in step with the strip, on a debounce.\n *\n * Skipped while there is no session key, which is also the first commit —\n * writing then would empty the slot the effect above is about to read.\n *\n * Debounced because a busy run changes `steps` several times per call —\n * start, arguments, result — and each write serialises the whole list and\n * hands it to a SYNCHRONOUS storage API on the main thread. A quarter of a\n * second collapses a call's worth of edits into one write and is far below\n * anything a reload could notice.\n */\n const pendingSteps = React.useRef<{ sessionKey: string; steps: AgentChatStep[] } | null>(null);\n React.useEffect(() => {\n if (ephemeral || !state.sessionKey) {\n return;\n }\n const sessionKey = state.sessionKey;\n const steps = state.steps;\n pendingSteps.current = { sessionKey, steps };\n const id = setTimeout(() => {\n pendingSteps.current = null;\n writeStoredSteps(storageScope, sessionKey, steps);\n }, STEP_WRITE_DEBOUNCE_MS);\n return () => clearTimeout(id);\n }, [ephemeral, state.sessionKey, state.steps, storageScope]);\n\n // Flushed on the way out. The unmount is the case the slot exists for — a\n // pop-up collapsing, a host remounting the surface mid-run — and a debounce\n // that dropped its last write on unmount would lose exactly the strip it was\n // meant to preserve.\n React.useEffect(() => {\n return () => {\n const pending = pendingSteps.current;\n if (!ephemeral && pending) {\n pendingSteps.current = null;\n writeStoredSteps(storageScope, pending.sessionKey, pending.steps);\n }\n };\n }, [ephemeral, storageScope]);\n\n const setSession = React.useCallback(\n (key: string) => {\n dispatch({ kind: \"session\", key });\n if (!ephemeral) {\n writeStoredSessionKey(storageScope, key);\n }\n },\n [ephemeral, storageScope],\n );\n\n const run = React.useCallback(\n (text: string, extra: Partial<Parameters<AgentChatClient[\"send\"]>[0]> = {}) => {\n const current = stateRef.current;\n // One run at a time. A second `send` on a streaming session opens a\n // second stream into one store: two sets of deltas overwrite each\n // other's snapshot, `final` folds one run's answer into the other's\n // bubble, and `abort` can only reach whichever the client thinks is\n // active. The composer disables Send while a run is live; this is the\n // same rule for Enter, for a2ui actions and for a caller of the hook.\n if (current.streaming) {\n return;\n }\n const sessionKey = current.sessionKey ?? mintSessionKey(client.agentId);\n if (!current.sessionKey && !ephemeral) {\n writeStoredSessionKey(storageScope, sessionKey);\n }\n const runId = generateId();\n // An action with no typed text is a click, not a sentence: a user bubble\n // for it would put `{\"name\":\"createTask\"}` in the transcript as though\n // the user had said it.\n const message: AgentChatMessage | null =\n text || (!extra.a2uiAction && !extra.mcpAppAction)\n ? { id: runId, role: \"user\", text, timestamp: Date.now(), runId }\n : null;\n dispatch({ kind: \"send\", message, runId, sessionKey });\n void client\n .send({\n sessionKey,\n message: text,\n runId,\n ...(thinking ? { thinking } : {}),\n ...extra,\n })\n .then(() => refreshSessions())\n .catch((err: unknown) => dispatch({ kind: \"send/failed\", message: errorText(err) }));\n },\n [client, ephemeral, refreshSessions, storageScope, thinking],\n );\n\n const send = React.useCallback(\n (text: string, attachments?: unknown[]) => {\n run(text, attachments?.length ? { attachments } : {});\n },\n [run],\n );\n\n const sendAction = React.useCallback(\n (a2uiAction: NonNullable<Parameters<AgentChatClient[\"send\"]>[0][\"a2uiAction\"]>) => {\n run(\"\", { a2uiAction });\n },\n [run],\n );\n\n const abort = React.useCallback(() => {\n const { runId, sessionKey } = stateRef.current;\n dispatch({ kind: \"abort\" });\n void client\n .abort({\n ...(runId ? { runId } : {}),\n ...(sessionKey ? { sessionKey } : {}),\n })\n .catch(() => {\n // Best-effort: local state is already reset, and dropping the stream\n // aborts the run server-side anyway.\n });\n }, [client]);\n\n const resolveApproval = React.useCallback(\n (id: string, decision: AgentApprovalDecision) => {\n const request = stateRef.current.approvals.find((r) => r.id === id);\n if (!request) {\n return;\n }\n // A deny also ends the step the run paused on: the call is not going to\n // happen, and a line left waiting for an approval the user has already\n // refused is the state this whole path exists to avoid.\n dispatch(\n decision === \"deny\" ? { kind: \"approval/denied\", id } : { kind: \"approval/resolved\", id },\n );\n // Bound to the run and the session the REQUEST arrived on, never to\n // whatever is in flight now. The two come apart the moment a user opens\n // another session while a command waits for an answer.\n void client\n .resolveApproval(id, decision, {\n runId: request.runId,\n sessionKey: request.sessionKey,\n })\n .catch((err: unknown) => {\n dispatch({ kind: \"approval/failed\", request, message: errorText(err) });\n });\n },\n [client],\n );\n\n const newSession = React.useCallback(() => {\n setSession(mintSessionKey(client.agentId));\n }, [client, setSession]);\n\n const resumeSession = React.useCallback(\n (key: string) => {\n setSession(key);\n loadHistory(key);\n },\n [loadHistory, setSession],\n );\n\n const loadOlder = React.useCallback(() => {\n const current = stateRef.current;\n if (!current.sessionKey || current.loadingHistory || !current.hasMoreHistory) {\n return;\n }\n dispatch({ kind: \"history/loading\" });\n void client\n .request<AgentChatHistoryResponse>(\"chat.history\", {\n sessionKey: current.sessionKey,\n limit: 100,\n // Records, not bubbles. See `historyCount`.\n offset: current.historyCount,\n })\n .then((response) => {\n const records = response?.messages ?? [];\n dispatch({\n kind: \"history/older\",\n messages: parseHistoryMessages(records),\n hasMore: response?.hasMore ?? false,\n records: records.length,\n });\n })\n .catch(() => dispatch({ kind: \"history/failed\" }));\n }, [client]);\n\n return {\n ...state,\n send,\n sendAction,\n abort,\n resolveApproval,\n newSession,\n resumeSession,\n loadOlder,\n refreshSessions,\n };\n}\n\nfunction errorText(err: unknown): string {\n return err instanceof Error ? err.message : \"The agent could not be reached\";\n}\n"],"mappings":"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"}
@@ -0,0 +1,277 @@
1
+ "use client";
2
+ import { A2UIChatBlock } from "./a2ui-block.js";
3
+ //#region src/agent-chat/types.d.ts
4
+ /**
5
+ * A row of `sessions.list`.
6
+ *
7
+ * Titles fall back in a fixed order (§17.2 of the extension how-to):
8
+ * `label`, `autoTitle`, `derivedTitle`, `displayName`, `lastMessagePreview`,
9
+ * then "New conversation". `sessionTitle` below is that order, in one place.
10
+ */
11
+ export interface AgentSession {
12
+ /** The session key, verbatim — this is also the AG-UI `threadId`. */
13
+ key: string;
14
+ label?: string | null;
15
+ autoTitle?: string | null;
16
+ derivedTitle?: string | null;
17
+ displayName?: string | null;
18
+ lastMessagePreview?: string | null;
19
+ updatedAt?: number | null;
20
+ }
21
+ /** The title to show for a session row. */
22
+ export declare function sessionTitle(session: AgentSession): string;
23
+ export interface AgentChatMessage {
24
+ id: string;
25
+ role: "user" | "assistant";
26
+ text: string;
27
+ timestamp: number;
28
+ attachments?: string[];
29
+ /** The chain of thought behind this turn, collapsed by default. */
30
+ thinkingText?: string;
31
+ isError?: boolean;
32
+ /** True when the wire record was a tool call or a tool result; hidden. */
33
+ isToolMessage?: boolean;
34
+ /**
35
+ * The run that produced this message. Absent on anything loaded from
36
+ * history. A `final` folds into the previous bubble only when the two share
37
+ * this value, so one run's answer cannot be rewritten by the next one's.
38
+ */
39
+ runId?: string;
40
+ /** UI the agent drew, carried outside the prose. */
41
+ a2ui?: A2UIChatBlock[];
42
+ }
43
+ /**
44
+ * One tool call of the turn, in plain words (SKILLS-9).
45
+ *
46
+ * A step and an `AgentToolEntry` are the same call seen from two distances.
47
+ * The entry is the record — name, arguments, output, the `ui://` resource an
48
+ * MCP App is mounted from — and it is what `renderToolCall` is handed. The step
49
+ * is the sentence: what the agent is doing, now, for someone who is waiting and
50
+ * is not going to open a JSON blob to find out.
51
+ *
52
+ * `label` is stored rather than derived at render time because it is derived
53
+ * from the arguments, and the arguments arrive AFTER the start event. Keeping
54
+ * the computed sentence on the step means the strip is a pure function of state
55
+ * and the recompute happens once, where the arguments land.
56
+ */
57
+ export interface AgentChatStep {
58
+ /** The AG-UI `toolCallId`; the same id the tool strip's entry carries. */
59
+ id: string;
60
+ toolName: string;
61
+ /**
62
+ * The call's input, when it parsed as an object. Absent until it arrives.
63
+ *
64
+ * In memory only. It is NOT persisted — see `storedStep` in `session.ts` —
65
+ * because a tool call's arguments are the user's data: a search intent, a
66
+ * customer id, the body of a record being written.
67
+ */
68
+ args?: Record<string, unknown>;
69
+ /** The plain sentence. See `step-label.ts` for the map that produces it. */
70
+ label: string;
71
+ startedAt: number;
72
+ /**
73
+ * `stopped` is the user's Stop button, and is deliberately not `error`:
74
+ * nothing failed, the answer was simply no longer wanted, and a red cross
75
+ * against a call the user cancelled reads as a fault they caused.
76
+ *
77
+ * `paused` is the run ending on an interrupt with the call still OPEN — the
78
+ * producer is waiting for an approval or for a frontend tool to run — and it
79
+ * is deliberately not `done`: nothing has happened yet, and a tick against a
80
+ * write the user has not agreed to is a lie about their data. It stops the
81
+ * spinner and the elapsed counter, resolves to `done` or `error` when the
82
+ * run resumes and the result arrives, and becomes `stopped` on a deny.
83
+ */
84
+ status: "running" | "done" | "error" | "stopped" | "paused";
85
+ endedAt?: number;
86
+ /** The failure to show, on a step that failed. Bounded and sanitised. */
87
+ errorMessage?: string;
88
+ }
89
+ /** The result detail cortenacore puts on `TOOL_CALL_RESULT.metadata.meta`. */
90
+ export interface AgentToolResultMeta {
91
+ status?: number;
92
+ code?: string;
93
+ message?: string;
94
+ }
95
+ /**
96
+ * `TOOL_CALL_RESULT.metadata`, normalised.
97
+ *
98
+ * This is the ONLY thing that says whether a tool call failed. `content` is
99
+ * the result as the model sees it — for a Cortena tool, a JSON string of
100
+ * `{ content: [{ type: "text", text }], details }` — and reading an envelope
101
+ * out of it means guessing at a shape the producer never promised. The client
102
+ * did guess, for `ok: false` at the top level, and the guess was simply wrong:
103
+ * the top level is the tool-result wrapper, so every failed call was drawn as
104
+ * a success.
105
+ *
106
+ * `meta` is optional, and where it is absent only `isError` can be trusted.
107
+ */
108
+ export interface AgentToolResultMetadata {
109
+ toolName?: string;
110
+ isError: boolean;
111
+ meta?: AgentToolResultMeta;
112
+ }
113
+ /**
114
+ * One chat event. The same `{ state: "delta" | "final" | "error" }` shape
115
+ * cortenacore broadcasts, so a client for a different transport has a
116
+ * documented target to hit.
117
+ *
118
+ * `delta` carries a SNAPSHOT, not an increment: every delta repeats all the
119
+ * text, all the thinking and every A2UI block seen so far. Two transports that
120
+ * disagree about this leave the store in different states for the same run.
121
+ */
122
+ export interface AgentChatEvent {
123
+ runId: string;
124
+ sessionKey: string;
125
+ seq: number;
126
+ state: "delta" | "final" | "error";
127
+ /**
128
+ * Set on a `final` that came from a `RUN_FINISHED` carrying
129
+ * `outcome.type === "interrupt"`.
130
+ *
131
+ * The run has stopped, but it has not FINISHED: cortenacore pauses it for an
132
+ * approval or for a frontend tool and leaves the tool call open, and the
133
+ * continuation is a second run that ends the same call. Without this the
134
+ * store read every interrupt as a success and ticked a write nobody had
135
+ * approved yet.
136
+ */
137
+ interrupted?: boolean;
138
+ message?: {
139
+ role?: string;
140
+ content?: Array<{
141
+ type: string;
142
+ text?: string;
143
+ thinking?: string;
144
+ a2ui?: unknown;
145
+ }>;
146
+ timestamp?: number;
147
+ };
148
+ errorMessage?: string;
149
+ }
150
+ /** One step of the tool strip. */
151
+ export interface AgentToolEvent {
152
+ event: "tool.start" | "tool.args" | "tool.progress" | "tool.complete" | "tool.error";
153
+ data: {
154
+ id?: string;
155
+ name?: string;
156
+ /** `tool.args`: the call's input, parsed when it was valid JSON. */
157
+ input?: unknown;
158
+ output?: unknown;
159
+ /**
160
+ * `TOOL_CALL_RESULT.metadata`, forwarded verbatim on an end event.
161
+ *
162
+ * Untyped on purpose: it crosses a transport, so it is whatever arrived.
163
+ * `readResultMetadata` in `store.ts` is what turns it into an
164
+ * `AgentToolResultMetadata`, and it is the one place that decides whether
165
+ * a call failed.
166
+ */
167
+ metadata?: Record<string, unknown>;
168
+ startedAt?: number;
169
+ completedAt?: number;
170
+ };
171
+ }
172
+ /** Decisions cortenacore accepts on `POST /agui/approval`. */
173
+ export type AgentApprovalDecision = "allow-once" | "allow-always" | "deny";
174
+ export interface AgentApprovalRequest {
175
+ id: string;
176
+ /** The command, or the tool, the approval is about. */
177
+ toolName: string;
178
+ args: Record<string, unknown>;
179
+ timestamp: number;
180
+ /**
181
+ * The run and the session this request arrived on, captured when it arrived.
182
+ *
183
+ * A decision is bound to them rather than resolved against whatever run
184
+ * happens to be in flight when the user presses a button. The two come
185
+ * apart routinely: an approval pauses a run, the user opens another session
186
+ * in the drawer or starts a second turn, and the client's idea of "the
187
+ * current thread" has moved on. Answering "allow" against the wrong thread
188
+ * is a command approved in a conversation nobody was asked about.
189
+ */
190
+ runId: string;
191
+ sessionKey: string;
192
+ /** Set when a decision was made and the server would not take it. */
193
+ error?: string;
194
+ }
195
+ /** Where an approval came from, and therefore where the decision must go. */
196
+ export interface AgentApprovalContext {
197
+ runId: string;
198
+ sessionKey: string;
199
+ }
200
+ export type AgentApprovalEvent = {
201
+ type: "request";
202
+ request: AgentApprovalRequest;
203
+ } |
204
+ /** `decision: null` is an expiry. It is treated as a denial. */
205
+ {
206
+ type: "resolved";
207
+ id: string;
208
+ decision: AgentApprovalDecision | null;
209
+ };
210
+ export interface AgentChatSendInput {
211
+ /** The session key to run in. `threadId` is this value verbatim. */
212
+ sessionKey: string;
213
+ /** What the user typed. Empty for an action sent by rendered UI. */
214
+ message: string;
215
+ /** The run id; also the idempotency key. */
216
+ runId: string;
217
+ attachments?: unknown[];
218
+ thinking?: "low" | "medium" | "high";
219
+ model?: {
220
+ provider: string;
221
+ model: string;
222
+ };
223
+ /** A click inside an A2UI surface. Never a sentence the user appears to type. */
224
+ a2uiAction?: {
225
+ surfaceId: string;
226
+ componentId: string;
227
+ name: string;
228
+ payload: Record<string, unknown>;
229
+ };
230
+ /** The same, from inside a rendered MCP App. */
231
+ mcpAppAction?: Record<string, unknown>;
232
+ }
233
+ /**
234
+ * Everything `AgentChat` needs from a server.
235
+ *
236
+ * `request` is deliberately a method name plus params rather than a URL: the
237
+ * two calls the chat makes — `sessions.list` and `chat.history` — are control
238
+ * plane methods, and a host that proxies them through its own gateway chooses
239
+ * its own paths.
240
+ */
241
+ export interface AgentChatClient {
242
+ /** The agent this client talks to. Session keys are `agent:<agentId>:…`. */
243
+ readonly agentId: string;
244
+ /** Start a run. Resolves when the run has been accepted, not when it ends. */
245
+ send(input: AgentChatSendInput): Promise<void>;
246
+ /** Stop the run in flight. Best-effort; local state resets either way. */
247
+ abort(params?: {
248
+ runId?: string;
249
+ sessionKey?: string;
250
+ }): Promise<void>;
251
+ /**
252
+ * Answer an approval. `context` is the `{ runId, sessionKey }` the request
253
+ * carried, not the run in flight — see `AgentApprovalRequest`. Rejects when
254
+ * the server refuses the decision, so the caller can put the card back.
255
+ */
256
+ resolveApproval(id: string, decision: AgentApprovalDecision, context: AgentApprovalContext): Promise<void>;
257
+ /** `sessions.list` and `chat.history`. */
258
+ request<T = unknown>(method: string, params: Record<string, unknown>): Promise<T>;
259
+ /** Chat deltas, finals and errors. Returns an unsubscribe. */
260
+ onChat(sink: (event: AgentChatEvent) => void): () => void;
261
+ /** Tool starts, arguments, progress and results. Returns an unsubscribe. */
262
+ onTool(sink: (event: AgentToolEvent) => void): () => void;
263
+ /** Approval requests and resolutions. Returns an unsubscribe. */
264
+ onApproval(sink: (event: AgentApprovalEvent) => void): () => void;
265
+ }
266
+ /** What `sessions.list` answers. */
267
+ export interface AgentSessionsListResponse {
268
+ sessions?: AgentSession[];
269
+ }
270
+ /** What `chat.history` answers. `messages` are raw transcript records. */
271
+ export interface AgentChatHistoryResponse {
272
+ messages?: unknown[];
273
+ hasMore?: boolean;
274
+ total?: number;
275
+ }
276
+ //#endregion
277
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,17 @@
1
+ "use client";
2
+ //#region src/agent-chat/types.ts
3
+ /** The title to show for a session row. */
4
+ function sessionTitle(session) {
5
+ for (const candidate of [
6
+ session.label,
7
+ session.autoTitle,
8
+ session.derivedTitle,
9
+ session.displayName,
10
+ session.lastMessagePreview
11
+ ]) if (typeof candidate === "string" && candidate.trim()) return candidate.trim();
12
+ return "New conversation";
13
+ }
14
+ //#endregion
15
+ export { sessionTitle };
16
+
17
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../src/agent-chat/types.ts"],"sourcesContent":["/**\n * The transport contract `AgentChat` is written against.\n *\n * Everything the chat surface needs from a server is behind `AgentChatClient`,\n * and nothing in the interface names AG-UI, HTTP or React. That is what makes\n * the surface testable — `test/agent-chat.test.tsx` drives it from a fake that\n * replays scripted event sequences — and it is what keeps `@ag-ui/client` out\n * of every module except `./agui-client.ts`.\n *\n * The three sinks are subscriptions rather than constructor callbacks so one\n * client can outlive several mounts of the component, which is what a pop-up\n * that collapses and reopens mid-run does.\n */\n\nimport type { A2UIChatBlock } from \"./a2ui-block\";\n\n/* ── what a session is ───────────────────────────────────────────────────── */\n\n/**\n * A row of `sessions.list`.\n *\n * Titles fall back in a fixed order (§17.2 of the extension how-to):\n * `label`, `autoTitle`, `derivedTitle`, `displayName`, `lastMessagePreview`,\n * then \"New conversation\". `sessionTitle` below is that order, in one place.\n */\nexport interface AgentSession {\n /** The session key, verbatim — this is also the AG-UI `threadId`. */\n key: string;\n label?: string | null;\n autoTitle?: string | null;\n derivedTitle?: string | null;\n displayName?: string | null;\n lastMessagePreview?: string | null;\n updatedAt?: number | null;\n}\n\n/** The title to show for a session row. */\nexport function sessionTitle(session: AgentSession): string {\n for (const candidate of [\n session.label,\n session.autoTitle,\n session.derivedTitle,\n session.displayName,\n session.lastMessagePreview,\n ]) {\n if (typeof candidate === \"string\" && candidate.trim()) {\n return candidate.trim();\n }\n }\n return \"New conversation\";\n}\n\n/* ── what a message is ───────────────────────────────────────────────────── */\n\nexport interface AgentChatMessage {\n id: string;\n role: \"user\" | \"assistant\";\n text: string;\n timestamp: number;\n attachments?: string[];\n /** The chain of thought behind this turn, collapsed by default. */\n thinkingText?: string;\n isError?: boolean;\n /** True when the wire record was a tool call or a tool result; hidden. */\n isToolMessage?: boolean;\n /**\n * The run that produced this message. Absent on anything loaded from\n * history. A `final` folds into the previous bubble only when the two share\n * this value, so one run's answer cannot be rewritten by the next one's.\n */\n runId?: string;\n /** UI the agent drew, carried outside the prose. */\n a2ui?: A2UIChatBlock[];\n}\n\n/* ── what a step is ──────────────────────────────────────────────────────── */\n\n/**\n * One tool call of the turn, in plain words (SKILLS-9).\n *\n * A step and an `AgentToolEntry` are the same call seen from two distances.\n * The entry is the record — name, arguments, output, the `ui://` resource an\n * MCP App is mounted from — and it is what `renderToolCall` is handed. The step\n * is the sentence: what the agent is doing, now, for someone who is waiting and\n * is not going to open a JSON blob to find out.\n *\n * `label` is stored rather than derived at render time because it is derived\n * from the arguments, and the arguments arrive AFTER the start event. Keeping\n * the computed sentence on the step means the strip is a pure function of state\n * and the recompute happens once, where the arguments land.\n */\nexport interface AgentChatStep {\n /** The AG-UI `toolCallId`; the same id the tool strip's entry carries. */\n id: string;\n toolName: string;\n /**\n * The call's input, when it parsed as an object. Absent until it arrives.\n *\n * In memory only. It is NOT persisted — see `storedStep` in `session.ts` —\n * because a tool call's arguments are the user's data: a search intent, a\n * customer id, the body of a record being written.\n */\n args?: Record<string, unknown>;\n /** The plain sentence. See `step-label.ts` for the map that produces it. */\n label: string;\n startedAt: number;\n /**\n * `stopped` is the user's Stop button, and is deliberately not `error`:\n * nothing failed, the answer was simply no longer wanted, and a red cross\n * against a call the user cancelled reads as a fault they caused.\n *\n * `paused` is the run ending on an interrupt with the call still OPEN — the\n * producer is waiting for an approval or for a frontend tool to run — and it\n * is deliberately not `done`: nothing has happened yet, and a tick against a\n * write the user has not agreed to is a lie about their data. It stops the\n * spinner and the elapsed counter, resolves to `done` or `error` when the\n * run resumes and the result arrives, and becomes `stopped` on a deny.\n */\n status: \"running\" | \"done\" | \"error\" | \"stopped\" | \"paused\";\n endedAt?: number;\n /** The failure to show, on a step that failed. Bounded and sanitised. */\n errorMessage?: string;\n}\n\n/** The result detail cortenacore puts on `TOOL_CALL_RESULT.metadata.meta`. */\nexport interface AgentToolResultMeta {\n status?: number;\n code?: string;\n message?: string;\n}\n\n/**\n * `TOOL_CALL_RESULT.metadata`, normalised.\n *\n * This is the ONLY thing that says whether a tool call failed. `content` is\n * the result as the model sees it — for a Cortena tool, a JSON string of\n * `{ content: [{ type: \"text\", text }], details }` — and reading an envelope\n * out of it means guessing at a shape the producer never promised. The client\n * did guess, for `ok: false` at the top level, and the guess was simply wrong:\n * the top level is the tool-result wrapper, so every failed call was drawn as\n * a success.\n *\n * `meta` is optional, and where it is absent only `isError` can be trusted.\n */\nexport interface AgentToolResultMetadata {\n toolName?: string;\n isError: boolean;\n meta?: AgentToolResultMeta;\n}\n\n/* ── the three sinks ─────────────────────────────────────────────────────── */\n\n/**\n * One chat event. The same `{ state: \"delta\" | \"final\" | \"error\" }` shape\n * cortenacore broadcasts, so a client for a different transport has a\n * documented target to hit.\n *\n * `delta` carries a SNAPSHOT, not an increment: every delta repeats all the\n * text, all the thinking and every A2UI block seen so far. Two transports that\n * disagree about this leave the store in different states for the same run.\n */\nexport interface AgentChatEvent {\n runId: string;\n sessionKey: string;\n seq: number;\n state: \"delta\" | \"final\" | \"error\";\n /**\n * Set on a `final` that came from a `RUN_FINISHED` carrying\n * `outcome.type === \"interrupt\"`.\n *\n * The run has stopped, but it has not FINISHED: cortenacore pauses it for an\n * approval or for a frontend tool and leaves the tool call open, and the\n * continuation is a second run that ends the same call. Without this the\n * store read every interrupt as a success and ticked a write nobody had\n * approved yet.\n */\n interrupted?: boolean;\n message?: {\n role?: string;\n content?: Array<{ type: string; text?: string; thinking?: string; a2ui?: unknown }>;\n timestamp?: number;\n };\n errorMessage?: string;\n}\n\n/** One step of the tool strip. */\nexport interface AgentToolEvent {\n event: \"tool.start\" | \"tool.args\" | \"tool.progress\" | \"tool.complete\" | \"tool.error\";\n data: {\n id?: string;\n name?: string;\n /** `tool.args`: the call's input, parsed when it was valid JSON. */\n input?: unknown;\n output?: unknown;\n /**\n * `TOOL_CALL_RESULT.metadata`, forwarded verbatim on an end event.\n *\n * Untyped on purpose: it crosses a transport, so it is whatever arrived.\n * `readResultMetadata` in `store.ts` is what turns it into an\n * `AgentToolResultMetadata`, and it is the one place that decides whether\n * a call failed.\n */\n metadata?: Record<string, unknown>;\n startedAt?: number;\n completedAt?: number;\n };\n}\n\n/** Decisions cortenacore accepts on `POST /agui/approval`. */\nexport type AgentApprovalDecision = \"allow-once\" | \"allow-always\" | \"deny\";\n\nexport interface AgentApprovalRequest {\n id: string;\n /** The command, or the tool, the approval is about. */\n toolName: string;\n args: Record<string, unknown>;\n timestamp: number;\n /**\n * The run and the session this request arrived on, captured when it arrived.\n *\n * A decision is bound to them rather than resolved against whatever run\n * happens to be in flight when the user presses a button. The two come\n * apart routinely: an approval pauses a run, the user opens another session\n * in the drawer or starts a second turn, and the client's idea of \"the\n * current thread\" has moved on. Answering \"allow\" against the wrong thread\n * is a command approved in a conversation nobody was asked about.\n */\n runId: string;\n sessionKey: string;\n /** Set when a decision was made and the server would not take it. */\n error?: string;\n}\n\n/** Where an approval came from, and therefore where the decision must go. */\nexport interface AgentApprovalContext {\n runId: string;\n sessionKey: string;\n}\n\nexport type AgentApprovalEvent =\n | { type: \"request\"; request: AgentApprovalRequest }\n /** `decision: null` is an expiry. It is treated as a denial. */\n | { type: \"resolved\"; id: string; decision: AgentApprovalDecision | null };\n\n/* ── the client ──────────────────────────────────────────────────────────── */\n\nexport interface AgentChatSendInput {\n /** The session key to run in. `threadId` is this value verbatim. */\n sessionKey: string;\n /** What the user typed. Empty for an action sent by rendered UI. */\n message: string;\n /** The run id; also the idempotency key. */\n runId: string;\n attachments?: unknown[];\n thinking?: \"low\" | \"medium\" | \"high\";\n model?: { provider: string; model: string };\n /** A click inside an A2UI surface. Never a sentence the user appears to type. */\n a2uiAction?: { surfaceId: string; componentId: string; name: string; payload: Record<string, unknown> };\n /** The same, from inside a rendered MCP App. */\n mcpAppAction?: Record<string, unknown>;\n}\n\n/**\n * Everything `AgentChat` needs from a server.\n *\n * `request` is deliberately a method name plus params rather than a URL: the\n * two calls the chat makes — `sessions.list` and `chat.history` — are control\n * plane methods, and a host that proxies them through its own gateway chooses\n * its own paths.\n */\nexport interface AgentChatClient {\n /** The agent this client talks to. Session keys are `agent:<agentId>:…`. */\n readonly agentId: string;\n /** Start a run. Resolves when the run has been accepted, not when it ends. */\n send(input: AgentChatSendInput): Promise<void>;\n /** Stop the run in flight. Best-effort; local state resets either way. */\n abort(params?: { runId?: string; sessionKey?: string }): Promise<void>;\n /**\n * Answer an approval. `context` is the `{ runId, sessionKey }` the request\n * carried, not the run in flight — see `AgentApprovalRequest`. Rejects when\n * the server refuses the decision, so the caller can put the card back.\n */\n resolveApproval(\n id: string,\n decision: AgentApprovalDecision,\n context: AgentApprovalContext,\n ): Promise<void>;\n /** `sessions.list` and `chat.history`. */\n request<T = unknown>(method: string, params: Record<string, unknown>): Promise<T>;\n /** Chat deltas, finals and errors. Returns an unsubscribe. */\n onChat(sink: (event: AgentChatEvent) => void): () => void;\n /** Tool starts, arguments, progress and results. Returns an unsubscribe. */\n onTool(sink: (event: AgentToolEvent) => void): () => void;\n /** Approval requests and resolutions. Returns an unsubscribe. */\n onApproval(sink: (event: AgentApprovalEvent) => void): () => void;\n}\n\n/** What `sessions.list` answers. */\nexport interface AgentSessionsListResponse {\n sessions?: AgentSession[];\n}\n\n/** What `chat.history` answers. `messages` are raw transcript records. */\nexport interface AgentChatHistoryResponse {\n messages?: unknown[];\n hasMore?: boolean;\n total?: number;\n}\n"],"mappings":";;;AAqCA,SAAgB,aAAa,SAA+B;CAC1D,KAAK,MAAM,aAAa;EACtB,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;EACR,QAAQ;CACV,GACE,IAAI,OAAO,cAAc,YAAY,UAAU,KAAK,GAClD,OAAO,UAAU,KAAK;CAG1B,OAAO;AACT"}
@@ -0,0 +1,11 @@
1
+ "use client";
2
+ import { A2UIChatBlock, extractA2UIBlocks, foldA2UIBlocks, readA2UIBlock } from "./agent-chat/a2ui-block.js";
3
+ import { AgentApprovalDecision, AgentApprovalEvent, AgentApprovalRequest, AgentChatClient, AgentChatEvent, AgentChatHistoryResponse, AgentChatMessage, AgentChatSendInput, AgentChatStep, AgentSession, AgentSessionsListResponse, AgentToolEvent, AgentToolResultMeta, AgentToolResultMetadata, sessionTitle } from "./agent-chat/types.js";
4
+ import { AGUI_CUSTOM_NAMES, AgentChatBridge, AgentChatBridgeOptions, AguiIncomingEvent } from "./agent-chat/bridge.js";
5
+ import { CreateAguiAgentChatClientOptions, createAguiAgentChatClient } from "./agent-chat/agui-client.js";
6
+ import { STEP_LIMIT, isHiddenMessage, isProvisionalSessionKey, isSessionOfAgent, mintSessionKey, parseHistoryMessages, readStoredSessionKey, readStoredSteps, serialiseSteps, sessionStorageKey, stepsStorageKey, viewStorageKey, wouldShrinkHistory, writeStoredSessionKey, writeStoredSteps } from "./agent-chat/session.js";
7
+ import { AgentStepPhase, confirmationRequiredLabel, genericLabel, humaniseId, isGenericLabel, stepLabel, truncate } from "./agent-chat/step-label.js";
8
+ import { AgentChatState, AgentToolEntry, UseAgentChatOptions, UseAgentChatResult, readResultMetadata, useAgentChat } from "./agent-chat/store.js";
9
+ import { AgentA2UIBlocks, AgentA2UIBlocksProps, AgentApprovalCard, AgentApprovalCardProps, AgentChat, AgentChatProps, AgentMessageBubble, AgentMessageBubbleProps, AgentSessionDrawer, AgentSessionDrawerProps, AgentSteps, AgentStepsProps, AgentStreamingBubble, AgentStreamingBubbleProps, AgentThinking, AgentThinkingProps, AgentToolCard, AgentToolCardProps, AgentToolRenderContext, readMcpAppUri, shouldStickToBottom } from "./components/agent-chat.js";
10
+ import { AgentChatPopup, AgentChatPopupExtension, AgentChatPopupProps, AgentChatPopupState } from "./components/agent-chat-popup.js";
11
+ export { type A2UIChatBlock, AGUI_CUSTOM_NAMES, AgentA2UIBlocks, type AgentA2UIBlocksProps, AgentApprovalCard, type AgentApprovalCardProps, type AgentApprovalDecision, type AgentApprovalEvent, type AgentApprovalRequest, AgentChat, AgentChatBridge, type AgentChatBridgeOptions, type AgentChatClient, type AgentChatEvent, type AgentChatHistoryResponse, type AgentChatMessage, AgentChatPopup, type AgentChatPopupExtension, type AgentChatPopupProps, type AgentChatPopupState, type AgentChatProps, type AgentChatSendInput, type AgentChatState, type AgentChatStep, AgentMessageBubble, type AgentMessageBubbleProps, type AgentSession, AgentSessionDrawer, type AgentSessionDrawerProps, type AgentSessionsListResponse, type AgentStepPhase, AgentSteps, type AgentStepsProps, AgentStreamingBubble, type AgentStreamingBubbleProps, AgentThinking, type AgentThinkingProps, AgentToolCard, type AgentToolCardProps, type AgentToolEntry, type AgentToolEvent, type AgentToolRenderContext, type AgentToolResultMeta, type AgentToolResultMetadata, type AguiIncomingEvent, type CreateAguiAgentChatClientOptions, STEP_LIMIT, type UseAgentChatOptions, type UseAgentChatResult, confirmationRequiredLabel, createAguiAgentChatClient, extractA2UIBlocks, foldA2UIBlocks, genericLabel, humaniseId, isGenericLabel, isHiddenMessage, isProvisionalSessionKey, isSessionOfAgent, mintSessionKey, parseHistoryMessages, readA2UIBlock, readMcpAppUri, readResultMetadata, readStoredSessionKey, readStoredSteps, serialiseSteps, sessionStorageKey, sessionTitle, shouldStickToBottom, stepLabel, stepsStorageKey, truncate, useAgentChat, viewStorageKey, wouldShrinkHistory, writeStoredSessionKey, writeStoredSteps };
@@ -0,0 +1,11 @@
1
+ "use client";
2
+ import { extractA2UIBlocks, foldA2UIBlocks, readA2UIBlock } from "./agent-chat/a2ui-block.js";
3
+ import { AGUI_CUSTOM_NAMES, AgentChatBridge } from "./agent-chat/bridge.js";
4
+ import { createAguiAgentChatClient } from "./agent-chat/agui-client.js";
5
+ import { confirmationRequiredLabel, genericLabel, humaniseId, isGenericLabel, stepLabel, truncate } from "./agent-chat/step-label.js";
6
+ import { STEP_LIMIT, isHiddenMessage, isProvisionalSessionKey, isSessionOfAgent, mintSessionKey, parseHistoryMessages, readStoredSessionKey, readStoredSteps, serialiseSteps, sessionStorageKey, stepsStorageKey, viewStorageKey, wouldShrinkHistory, writeStoredSessionKey, writeStoredSteps } from "./agent-chat/session.js";
7
+ import { readResultMetadata, useAgentChat } from "./agent-chat/store.js";
8
+ import { sessionTitle } from "./agent-chat/types.js";
9
+ import { AgentA2UIBlocks, AgentApprovalCard, AgentChat, AgentMessageBubble, AgentSessionDrawer, AgentSteps, AgentStreamingBubble, AgentThinking, AgentToolCard, readMcpAppUri, shouldStickToBottom } from "./components/agent-chat.js";
10
+ import { AgentChatPopup } from "./components/agent-chat-popup.js";
11
+ export { AGUI_CUSTOM_NAMES, AgentA2UIBlocks, AgentApprovalCard, AgentChat, AgentChatBridge, AgentChatPopup, AgentMessageBubble, AgentSessionDrawer, AgentSteps, AgentStreamingBubble, AgentThinking, AgentToolCard, STEP_LIMIT, confirmationRequiredLabel, createAguiAgentChatClient, extractA2UIBlocks, foldA2UIBlocks, genericLabel, humaniseId, isGenericLabel, isHiddenMessage, isProvisionalSessionKey, isSessionOfAgent, mintSessionKey, parseHistoryMessages, readA2UIBlock, readMcpAppUri, readResultMetadata, readStoredSessionKey, readStoredSteps, serialiseSteps, sessionStorageKey, sessionTitle, shouldStickToBottom, stepLabel, stepsStorageKey, truncate, useAgentChat, viewStorageKey, wouldShrinkHistory, writeStoredSessionKey, writeStoredSteps };
@@ -0,0 +1,66 @@
1
+ "use client";
2
+ import { AdminPermissionsApi, AdminRoute } from "./types.js";
3
+ import * as React from "react";
4
+ //#region src/components/admin-permissions/admin-permissions.d.ts
5
+ export interface AdminPermissionsProps extends Omit<React.ComponentProps<"div">, "children" | "title"> {
6
+ /** Everything the screen reads and writes. The extension's, entirely. */
7
+ api: AdminPermissionsApi;
8
+ /**
9
+ * The screen to show. Pass it and the host's router owns the URL; omit it
10
+ * and the composition keeps the current screen in its own state.
11
+ */
12
+ route?: AdminRoute;
13
+ /** The screen to open with when `route` is not passed. @default "members" */
14
+ defaultRoute?: AdminRoute;
15
+ /**
16
+ * Fired when the administrator picks another screen. Always fired, whether
17
+ * or not `route` is controlled, so a host can push the URL without also
18
+ * having to hold the state.
19
+ */
20
+ onNavigate?: (route: AdminRoute) => void;
21
+ /** `false` hides the title block, for a host that renders its own. */
22
+ header?: boolean;
23
+ title?: React.ReactNode;
24
+ description?: React.ReactNode;
25
+ }
26
+ /**
27
+ * AdminPermissions — the consistent admin screen for every Cortena extension
28
+ * (§13, DESIGN-D11 as amended by DESIGN-D22; audit rules P-14, P-42, P-43).
29
+ *
30
+ * Four screens behind `admin/*`: `/admin/members`, `/admin/roles`,
31
+ * `/admin/licence`, `/admin/permissions`. It renders whatever roles and
32
+ * actions the extension declares and **defines none of its own** — there is
33
+ * not one role name, action name or grant in this folder. That is exactly what
34
+ * lets a verification workflow and a work tracker have completely different
35
+ * role models and still look like one product.
36
+ *
37
+ * ```tsx
38
+ * <Route path="admin/*" element={
39
+ * <AdminPermissions
40
+ * api={adminApi}
41
+ * route={screenFromPath(location.pathname)}
42
+ * onNavigate={(next) => navigate(`/admin/${next}`)}
43
+ * />
44
+ * } />
45
+ * ```
46
+ *
47
+ * **Router-agnostic, by a `route` prop plus `onNavigate`.** The component
48
+ * imports no router and never touches `history` or `location`. Those four
49
+ * paths are real URLs — deep-linkable, bookmarkable, and answerable to the
50
+ * back button — and only the host's router can own a URL; a component that
51
+ * pushed history itself would fight whichever router the extension chose.
52
+ * `onNavigate` reports the intent, the host turns it into a navigation, and
53
+ * `route` comes back as the answer.
54
+ *
55
+ * An internal fallback exists for the case with no router in it — the guide,
56
+ * a test, a small app: omit `route` and the composition keeps the screen in
57
+ * its own state. `onNavigate` still fires, so a host can start uncontrolled
58
+ * and add a router later without changing anything else.
59
+ *
60
+ * The navigation is a real tablist rather than a row of links, because the
61
+ * choice is between panels of one screen; the host is free to hide it with
62
+ * `header={false}` and drive `route` from its own chrome.
63
+ */
64
+ export declare function AdminPermissions({ api, route, defaultRoute, onNavigate, header, title, description, className, ...props }: AdminPermissionsProps): React.JSX.Element;
65
+ //#endregion
66
+ //# sourceMappingURL=admin-permissions.d.ts.map