cortena-ui 1.6.0 → 1.7.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.
- package/CHANGELOG.md +76 -0
- package/README.md +36 -0
- package/dist/agent-chat/session.js +21 -7
- package/dist/agent-chat/session.js.map +1 -1
- package/dist/agent-chat/store.d.ts +55 -4
- package/dist/agent-chat/store.js +87 -22
- package/dist/agent-chat/store.js.map +1 -1
- package/dist/agent-chat/types.d.ts +11 -0
- package/dist/agent-chat/types.js.map +1 -1
- package/dist/agent-chat.d.ts +3 -3
- package/dist/components/agent-chat-popup.d.ts +2 -2
- package/dist/components/agent-chat-popup.js.map +1 -1
- package/dist/components/agent-chat.d.ts +169 -13
- package/dist/components/agent-chat.js +200 -27
- package/dist/components/agent-chat.js.map +1 -1
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/toast.d.ts +1 -1
- package/package.json +1 -1
- package/src/agent-chat/session.ts +35 -5
- package/src/agent-chat/store.ts +193 -21
- package/src/agent-chat/types.ts +11 -0
- package/src/components/agent-chat-popup.tsx +3 -3
- package/src/components/agent-chat.tsx +576 -149
- package/src/entries/agent-chat.ts +12 -0
|
@@ -1 +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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"}
|
|
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 AgentChatSendInput,\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 | {\n kind: \"send\";\n message: AgentChatMessage | null;\n runId: string;\n sessionKey: string;\n /**\n * A regenerate: no new bubble, but the user turn being asked again is\n * moved onto this run by id, so the transcript still reads Q then A.\n */\n reanchor?: string;\n }\n | { kind: \"message/remove\"; id: 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 | {\n kind: \"history/older\";\n sessionKey: string;\n messages: AgentChatMessage[];\n hasMore: boolean;\n records: number;\n }\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\n ? [...state.messages, action.message]\n : action.reanchor\n ? state.messages.map((m) =>\n m.id === action.reanchor ? { ...m, runId: action.runId } : m,\n )\n : 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 \"message/remove\":\n // The bubble a regenerate replaces. Dropped before the new run starts so\n // the answer takes its place rather than appending a second one.\n return { ...state, messages: state.messages.filter((m) => m.id !== action.id) };\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 // A load for a session that is no longer the one on screen. Two loads\n // race whenever a host switches sessions quickly — resume A, resume B,\n // A's reply lands last — and applying it put A's transcript under B's\n // key. The load that matters is the one for the CURRENT key; every other\n // reply is stale and is dropped whole. `null` is included on purpose: a\n // host that cleared the session while a load was in flight must not be\n // put back into it by the reply.\n if (action.sessionKey !== state.sessionKey) {\n return { ...state, loadingHistory: false };\n }\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 // The same rule as `history/loaded`: a page that was asked for under a\n // key that is no longer the current one — the host switched sessions\n // while it was in flight — must not be prepended to the transcript\n // that is on screen now.\n if (action.sessionKey !== state.sessionKey) {\n return { ...state, loadingHistory: false };\n }\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 * Mint the key for a brand-new session. Defaults to `mintSessionKey(client.agentId)`,\n * which binds the session to this client's agent template. A host whose\n * sessions are bound some other way — cortenaweb lets the user pick an agent\n * per chat, and a chat with no pick is bound server-side to the default —\n * supplies its own.\n */\n mintSessionKey?: () => string;\n /**\n * Read the stored session key back on mount. Default `true`. A host whose\n * URL names the session — `/chat/<key>` — is the source of truth itself and\n * passes `false`: the key is still WRITTEN, so the rule \"one window, one\n * session\" holds, but nothing is restored under the URL's feet and no\n * history is fetched for a session the page is about to leave.\n */\n restoreSession?: boolean;\n /**\n * Fetch `sessions.list` on mount and after each send. Default `true`. A host\n * with its own session list — cortenaweb's sidebar — passes `false` and the\n * hook makes no session calls at all; `sessions` stays empty.\n */\n listSessions?: boolean;\n}\n\n/** What `send` accepts beside the text and the attachments. */\nexport interface AgentChatSendOptions {\n /** The model for this run; the composer's selector. */\n model?: AgentChatSendInput[\"model\"];\n /** Overrides the hook-level `thinking` for this run. */\n thinking?: AgentChatSendInput[\"thinking\"];\n}\n\nexport interface UseAgentChatResult extends AgentChatState {\n send: (text: string, attachments?: unknown[], options?: AgentChatSendOptions) => void;\n sendAction: (action: NonNullable<Parameters<AgentChatClient[\"send\"]>[0][\"a2uiAction\"]>) => void;\n /** A click inside a rendered MCP App, as the next run's input. No user bubble. */\n sendMcpAppAction: (action: NonNullable<AgentChatSendInput[\"mcpAppAction\"]>) => void;\n abort: () => void;\n resolveApproval: (id: string, decision: AgentApprovalDecision) => void;\n newSession: () => void;\n resumeSession: (key: string) => void;\n /**\n * Leave the session without starting another. The transcript empties, the\n * stored key is removed and the next `send` mints a fresh key — which is\n * what a host's \"New chat\" wants when the URL should not change until the\n * first message goes out. `newSession` mints immediately; this does not.\n */\n clearSession: () => void;\n /**\n * Ask again for the message this one answered: the bubble is dropped and the\n * user turn before it is sent again, so the new answer takes its place. Also\n * the Retry on a failed turn. A no-op while a run is live or when nothing\n * precedes the message.\n */\n regenerate: (messageId: string) => void;\n /**\n * Ask for the next page of older history. `true` when a load was started;\n * `false` when there is nothing to do — no session, a load already in\n * flight, or no more history — so a caller that saves scroll position\n * before a prepend knows whether one is coming.\n */\n loadOlder: () => boolean;\n refreshSessions: () => void;\n}\n\nexport function useAgentChat({\n client,\n storageScope,\n ephemeral,\n thinking,\n mintSessionKey: mintKey,\n restoreSession = true,\n listSessions = true,\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 if (!listSessions) {\n return;\n }\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, listSessions]);\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 || !restoreSession ? 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, restoreSession, 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 (\n text: string,\n extra: Partial<Parameters<AgentChatClient[\"send\"]>[0]> = {},\n /** The user message this run asks again, instead of a new bubble. */\n reanchor?: string,\n ) => {\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 =\n current.sessionKey ?? (mintKey ? mintKey() : 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 !reanchor && (text || (!extra.a2uiAction && !extra.mcpAppAction))\n ? {\n id: runId,\n role: \"user\",\n text,\n timestamp: Date.now(),\n runId,\n // On the bubble from the start, so the host's strip draws what\n // was attached before the transcript is persisted.\n ...(extra.attachments?.length ? { attachmentItems: extra.attachments } : {}),\n }\n : null;\n dispatch({ kind: \"send\", message, runId, sessionKey, ...(reanchor ? { reanchor } : {}) });\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, mintKey, refreshSessions, storageScope, thinking],\n );\n\n const send = React.useCallback(\n (text: string, attachments?: unknown[], options?: AgentChatSendOptions) => {\n run(text, {\n ...(attachments?.length ? { attachments } : {}),\n ...(options?.model ? { model: options.model } : {}),\n ...(options?.thinking ? { thinking: options.thinking } : {}),\n });\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 sendMcpAppAction = React.useCallback(\n (mcpAppAction: NonNullable<AgentChatSendInput[\"mcpAppAction\"]>) => {\n run(\"\", { mcpAppAction });\n },\n [run],\n );\n\n const regenerate = React.useCallback(\n (messageId: string) => {\n const { messages, streaming } = stateRef.current;\n if (streaming) {\n return;\n }\n const index = messages.findIndex((m) => m.id === messageId);\n if (index < 1) {\n return;\n }\n let question: AgentChatMessage | null = null;\n for (let i = index - 1; i >= 0; i -= 1) {\n const candidate = messages[i];\n if (candidate?.role === \"user\" && !candidate.isToolMessage) {\n question = candidate;\n break;\n }\n }\n if (question === null) {\n return;\n }\n dispatch({ kind: \"message/remove\", id: messageId });\n // The question is already in the transcript. It is re-sent, not\n // re-drawn: the existing bubble is moved onto the new run so the\n // transcript reads Q then the new A, not Q, Q, A.\n run(question.text, {}, question.id);\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(mintKey ? mintKey() : mintSessionKey(client.agentId));\n }, [client, mintKey, setSession]);\n\n const clearSession = React.useCallback(() => {\n dispatch({ kind: \"session\", key: null });\n if (!ephemeral) {\n writeStoredSessionKey(storageScope, null);\n }\n }, [ephemeral, storageScope]);\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((): boolean => {\n const current = stateRef.current;\n if (!current.sessionKey || current.loadingHistory || !current.hasMoreHistory) {\n return false;\n }\n const sessionKey = current.sessionKey;\n dispatch({ kind: \"history/loading\" });\n void client\n .request<AgentChatHistoryResponse>(\"chat.history\", {\n 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 sessionKey,\n messages: parseHistoryMessages(records),\n hasMore: response?.hasMore ?? false,\n records: records.length,\n });\n })\n .catch(() => dispatch({ kind: \"history/failed\" }));\n return true;\n }, [client]);\n\n return {\n ...state,\n send,\n sendAction,\n sendMcpAppAction,\n abort,\n resolveApproval,\n newSession,\n resumeSession,\n clearSession,\n regenerate,\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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"}
|
|
@@ -25,7 +25,18 @@ export interface AgentChatMessage {
|
|
|
25
25
|
role: "user" | "assistant";
|
|
26
26
|
text: string;
|
|
27
27
|
timestamp: number;
|
|
28
|
+
/** Attachment names or references, as a transcript record carries them. */
|
|
28
29
|
attachments?: string[];
|
|
30
|
+
/**
|
|
31
|
+
* What the user attached to this turn, as the host's composer shaped it.
|
|
32
|
+
* `unknown[]` because the shape is the host's — cortenaweb sends
|
|
33
|
+
* `{ fileName, mimeType, content }` objects — and this package only asks
|
|
34
|
+
* whether there are any. Set on the user message `send` adds, so the bubble
|
|
35
|
+
* can draw the strip before the transcript is persisted. Separate from
|
|
36
|
+
* `attachments` so that field keeps its `string[]` shape for every consumer
|
|
37
|
+
* already reading it.
|
|
38
|
+
*/
|
|
39
|
+
attachmentItems?: unknown[];
|
|
29
40
|
/** The chain of thought behind this turn, collapsed by default. */
|
|
30
41
|
thinkingText?: string;
|
|
31
42
|
isError?: boolean;
|
|
@@ -1 +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"}
|
|
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 /** Attachment names or references, as a transcript record carries them. */\n attachments?: string[];\n /**\n * What the user attached to this turn, as the host's composer shaped it.\n * `unknown[]` because the shape is the host's — cortenaweb sends\n * `{ fileName, mimeType, content }` objects — and this package only asks\n * whether there are any. Set on the user message `send` adds, so the bubble\n * can draw the strip before the transcript is persisted. Separate from\n * `attachments` so that field keeps its `string[]` shape for every consumer\n * already reading it.\n */\n attachmentItems?: unknown[];\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"}
|
package/dist/agent-chat.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { AGUI_CUSTOM_NAMES, AgentChatBridge, AgentChatBridgeOptions, AguiIncomin
|
|
|
5
5
|
import { CreateAguiAgentChatClientOptions, createAguiAgentChatClient } from "./agent-chat/agui-client.js";
|
|
6
6
|
import { STEP_LIMIT, isHiddenMessage, isProvisionalSessionKey, isSessionOfAgent, mintSessionKey, parseHistoryMessages, readStoredSessionKey, readStoredSteps, serialiseSteps, sessionStorageKey, stepsStorageKey, viewStorageKey, wouldShrinkHistory, writeStoredSessionKey, writeStoredSteps } from "./agent-chat/session.js";
|
|
7
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";
|
|
8
|
+
import { AgentChatSendOptions, AgentChatState, AgentToolEntry, UseAgentChatOptions, UseAgentChatResult, readResultMetadata, useAgentChat } from "./agent-chat/store.js";
|
|
9
|
+
import { AgentA2UIBlocks, AgentA2UIBlocksProps, AgentApprovalCard, AgentApprovalCardProps, AgentChat, AgentChatBaseProps, AgentChatClassNames, AgentChatComponents, AgentChatComposerSlotProps, AgentChatEmptySlotProps, AgentChatHostedProps, AgentChatLoadOlderSlotProps, AgentChatMessageSlotProps, AgentChatOwnedProps, AgentChatProps, AgentChatStreamingSlotProps, AgentChatToolsSlotProps, AgentMessageBubble, AgentMessageBubbleProps, AgentSessionDrawer, AgentSessionDrawerProps, AgentSteps, AgentStepsProps, AgentStreamingBubble, AgentStreamingBubbleProps, AgentThinking, AgentThinkingProps, AgentToolCard, AgentToolCardProps, AgentToolRenderContext, readMcpAppUri, shouldStickToBottom } from "./components/agent-chat.js";
|
|
10
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 };
|
|
11
|
+
export { type A2UIChatBlock, AGUI_CUSTOM_NAMES, AgentA2UIBlocks, type AgentA2UIBlocksProps, AgentApprovalCard, type AgentApprovalCardProps, type AgentApprovalDecision, type AgentApprovalEvent, type AgentApprovalRequest, AgentChat, type AgentChatBaseProps, AgentChatBridge, type AgentChatBridgeOptions, type AgentChatClassNames, type AgentChatClient, type AgentChatComponents, type AgentChatComposerSlotProps, type AgentChatEmptySlotProps, type AgentChatEvent, type AgentChatHistoryResponse, type AgentChatHostedProps, type AgentChatLoadOlderSlotProps, type AgentChatMessage, type AgentChatMessageSlotProps, type AgentChatOwnedProps, AgentChatPopup, type AgentChatPopupExtension, type AgentChatPopupProps, type AgentChatPopupState, type AgentChatProps, type AgentChatSendInput, type AgentChatSendOptions, type AgentChatState, type AgentChatStep, type AgentChatStreamingSlotProps, type AgentChatToolsSlotProps, 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 };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { AgentChatClient } from "../agent-chat/types.js";
|
|
3
|
-
import {
|
|
3
|
+
import { AgentChatOwnedProps } from "./agent-chat.js";
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
//#region src/components/agent-chat-popup.d.ts
|
|
6
6
|
export type AgentChatPopupState = "pill" | "panel" | "full";
|
|
@@ -10,7 +10,7 @@ export interface AgentChatPopupExtension {
|
|
|
10
10
|
/** Display name. Also the agent template slug, unless `agentId` overrides it. */
|
|
11
11
|
name: string;
|
|
12
12
|
}
|
|
13
|
-
export interface AgentChatPopupProps extends Omit<
|
|
13
|
+
export interface AgentChatPopupProps extends Omit<AgentChatOwnedProps, "agentName" | "storageScope" | "className" | "headerSlot" | "client" | "chat"> {
|
|
14
14
|
extension: AgentChatPopupExtension;
|
|
15
15
|
/** The transport. Its `agentId` is the template slug the sessions belong to. */
|
|
16
16
|
client: AgentChatClient;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-chat-popup.js","names":[],"sources":["../../src/components/agent-chat-popup.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * AgentChatPopup — the extension's own agent, in the corner of every screen.\n *\n * §17 of how-to-create-a-cortena-extension, audit rule P-25. Mounted in\n * `AppShell`'s `agentSlot`, so it stacks above the theme/help cluster rather\n * than beside it and no extension positions a floating control itself.\n *\n * ```tsx\n * <AppShell\n * extension={{ id: \"tasks\", name: \"Tasks\" }}\n * user={user}\n * agentSlot={\n * <AgentChatPopup\n * extension={{ id: \"tasks\", name: \"Tasks\" }}\n * client={client} // createAguiAgentChatClient({ …, agentId: \"tasks\" })\n * />\n * }\n * />\n * ```\n *\n * Three states, and the transitions between them are fixed: a collapsed pill,\n * a panel, and full screen. **Cmd/Ctrl+J** toggles between the pill and the\n * panel; **Escape** steps full screen -> panel -> pill, except while a run is\n * streaming, where the composer takes Escape to mean \"stop\" — the first Escape\n * during a run is nearly always about the run, not about the window.\n *\n * The chat stays mounted in every state, hidden rather than unmounted while\n * collapsed. That is not an optimisation: a run started before the user\n * collapsed the pop-up keeps streaming into it, and the unread dot on the pill\n * is what says so. Unmounting would throw the reply away halfway through.\n *\n * State is remembered per window, in `sessionStorage`, alongside the session\n * key — a second window of the same extension opens its own chat on its own\n * session rather than inheriting this one's.\n */\n\nimport { Maximize2, Minimize2, X } from \"lucide-react\";\nimport * as React from \"react\";\nimport { BrandMark } from \"@/components/app-shell\";\nimport { Button } from \"@/components/button\";\nimport { AgentChat, type AgentChatProps } from \"@/components/agent-chat\";\nimport { viewStorageKey, windowSessionStorage } from \"@/agent-chat/session\";\nimport type { AgentChatClient } from \"@/agent-chat/types\";\nimport { cn } from \"@/lib/cn\";\n\nexport type AgentChatPopupState = \"pill\" | \"panel\" | \"full\";\n\nexport interface AgentChatPopupExtension {\n /** Registered mark id — the same `x-cortena.icon` the shell draws. */\n id: string;\n /** Display name. Also the agent template slug, unless `agentId` overrides it. */\n name: string;\n}\n\nexport interface AgentChatPopupProps\n extends Omit<\n AgentChatProps,\n \"agentName\" | \"storageScope\" | \"className\" | \"headerSlot\" | \"client\"\n > {\n extension: AgentChatPopupExtension;\n /** The transport. Its `agentId` is the template slug the sessions belong to. */\n client: AgentChatClient;\n /**\n * Names the `sessionStorage` slots. Defaults to the extension id, which is\n * what keeps two extensions on one origin out of each other's sessions.\n */\n storageScope?: string;\n /** Start here instead of collapsed; the stored state still wins on reload. */\n defaultState?: AgentChatPopupState;\n className?: string;\n}\n\n/** Cmd on a Mac, Ctrl elsewhere. Documented in README.md. */\nfunction isToggleShortcut(event: KeyboardEvent): boolean {\n return (event.metaKey || event.ctrlKey) && !event.altKey && event.key.toLowerCase() === \"j\";\n}\n\nfunction AgentChatPopup({\n extension,\n client,\n storageScope,\n defaultState = \"pill\",\n className,\n ...chatProps\n}: AgentChatPopupProps) {\n const scope = storageScope ?? extension.id;\n const [state, setState] = React.useState<AgentChatPopupState>(defaultState);\n const [unread, setUnread] = React.useState(false);\n const stateRef = React.useRef(state);\n stateRef.current = state;\n const pillRef = React.useRef<HTMLButtonElement>(null);\n const panelRef = React.useRef<HTMLDivElement>(null);\n /** Whether the user has opened or closed it, as against it starting open. */\n const moved = React.useRef(false);\n\n // Restore this window's open/collapsed choice. In an effect rather than a\n // lazy initialiser so a server render and the first client render agree.\n React.useEffect(() => {\n try {\n const stored = windowSessionStorage()?.getItem(viewStorageKey(scope));\n if (stored === \"pill\" || stored === \"panel\" || stored === \"full\") {\n setState(stored);\n }\n } catch {\n // A blocked storage just means the pop-up opens collapsed.\n }\n }, [scope]);\n\n const go = React.useCallback(\n (next: AgentChatPopupState) => {\n moved.current = true;\n setState(next);\n if (next !== \"pill\") {\n setUnread(false);\n }\n try {\n windowSessionStorage()?.setItem(viewStorageKey(scope), next);\n } catch {\n // Not remembering the choice is not a reason to refuse the change.\n }\n },\n [scope],\n );\n\n // The unread dot. Subscribed here rather than read out of the chat, because\n // the point is what happened while the surface was not being looked at.\n React.useEffect(() => {\n return client.onChat((event) => {\n if (stateRef.current === \"pill\" && (event.state === \"delta\" || event.state === \"final\")) {\n setUnread(true);\n }\n });\n }, [client]);\n\n React.useEffect(() => {\n const onKeyDown = (event: KeyboardEvent) => {\n if (!isToggleShortcut(event)) {\n return;\n }\n event.preventDefault();\n go(stateRef.current === \"pill\" ? \"panel\" : \"pill\");\n };\n window.addEventListener(\"keydown\", onKeyDown);\n return () => window.removeEventListener(\"keydown\", onKeyDown);\n }, [go]);\n\n /**\n * Escape steps down one level. It is handled on the panel rather than on the\n * window so it only fires when focus is inside the pop-up, and it arrives\n * here only when the composer did not claim it — the composer stops\n * propagation while a run is streaming, where Escape means \"stop the run\".\n */\n const onKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key !== \"Escape\") {\n return;\n }\n event.preventDefault();\n go(stateRef.current === \"full\" ? \"panel\" : \"pill\");\n },\n [go],\n );\n\n const open = state !== \"pill\";\n\n /**\n * Focus follows the pop-up.\n *\n * The panel is `hidden`, not unmounted, so nothing moves focus by itself.\n * Opening left focus on the pill — which is now `hidden`, so the browser\n * resets it to <body> and the next Tab starts at the top of the page rather\n * than in the chat that was just opened. Closing had the mirror problem:\n * focus was inside a subtree that had just become hidden.\n *\n * Only after the user moves it. A pop-up that starts on `panel`, or one\n * restored from `sessionStorage` on load, must not steal focus from the page.\n */\n React.useEffect(() => {\n if (!moved.current) {\n return;\n }\n const frame = requestAnimationFrame(() => {\n if (open) {\n panelRef.current\n ?.querySelector<HTMLElement>('[data-slot=\"agent-chat-input\"]')\n ?.focus({ preventScroll: true });\n } else {\n pillRef.current?.focus({ preventScroll: true });\n }\n });\n return () => cancelAnimationFrame(frame);\n }, [open]);\n\n return (\n <>\n <button\n ref={pillRef}\n type=\"button\"\n data-slot=\"agent-chat-pill\"\n data-state={state}\n data-unread={unread ? \"\" : undefined}\n aria-expanded={open}\n aria-label={`Ask ${extension.name}`}\n hidden={open}\n onClick={() => go(\"panel\")}\n className={cn(\n \"relative inline-flex h-10 items-center gap-2 rounded-[var(--ds-radius-full)] px-3.5\",\n \"border border-[var(--ds-border)] bg-[var(--ds-card)] text-[color:var(--ds-foreground)]\",\n \"shadow-[var(--ds-shadow-md)] outline-none\",\n \"transition-colors duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]\",\n \"hover:bg-[var(--ds-hover)]\",\n \"focus-visible:ring-2 focus-visible:ring-[var(--ds-ring)] focus-visible:ring-offset-2\",\n \"focus-visible:ring-offset-[var(--ds-background)]\",\n className,\n )}\n >\n <span className=\"flex size-5 items-center justify-center text-[color:var(--ds-primary)]\">\n <BrandMark id={extension.id} name={extension.name} size={16} decorative />\n </span>\n <span className=\"text-[length:var(--ds-text-caption-lg)] font-medium\">\n Ask {extension.name}\n </span>\n {unread ? (\n <span\n data-slot=\"agent-chat-pill-unread\"\n aria-label=\"The agent is replying\"\n className=\"size-2 rounded-[var(--ds-radius-full)] bg-[var(--ds-primary)]\"\n />\n ) : null}\n </button>\n\n {/* Hidden, not unmounted: a run started before the pop-up was collapsed\n keeps streaming, and the pill's dot is what reports it. */}\n <div\n ref={panelRef}\n data-slot=\"agent-chat-panel\"\n data-state={state}\n role=\"dialog\"\n aria-label={`${extension.name} agent`}\n hidden={!open}\n onKeyDown={onKeyDown}\n className={cn(\n \"fixed z-50 flex flex-col overflow-hidden border border-[var(--ds-border)]\",\n \"bg-[var(--ds-card)] shadow-[var(--ds-shadow-lg)]\",\n state === \"full\"\n ? \"inset-0 rounded-none\"\n : cn(\n \"right-6 bottom-6 rounded-[var(--ds-radius-lg)]\",\n \"h-[min(40rem,calc(100vh-3rem))] w-[min(26.25rem,calc(100vw-3rem))]\",\n ),\n )}\n >\n <AgentChat\n client={client}\n agentName={extension.name}\n storageScope={scope}\n className=\"min-h-0 flex-1\"\n headerSlot={\n <>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n data-slot=\"agent-chat-expand\"\n aria-label={state === \"full\" ? \"Restore the panel\" : \"Expand to full screen\"}\n onClick={() => go(state === \"full\" ? \"panel\" : \"full\")}\n >\n {state === \"full\" ? <Minimize2 aria-hidden /> : <Maximize2 aria-hidden />}\n </Button>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n data-slot=\"agent-chat-collapse\"\n aria-label=\"Collapse the agent\"\n onClick={() => go(\"pill\")}\n >\n <X aria-hidden />\n </Button>\n </>\n }\n {...chatProps}\n />\n </div>\n </>\n );\n}\n\nexport { AgentChatPopup };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2EA,SAAS,iBAAiB,OAA+B;CACvD,QAAQ,MAAM,WAAW,MAAM,YAAY,CAAC,MAAM,UAAU,MAAM,IAAI,YAAY,MAAM;AAC1F;AAEA,SAAS,eAAe,EACtB,WACA,QACA,cACA,eAAe,QACf,WACA,GAAG,aACmB;CACtB,MAAM,QAAQ,gBAAgB,UAAU;CACxC,MAAM,CAAC,OAAO,YAAY,MAAM,SAA8B,YAAY;CAC1E,MAAM,CAAC,QAAQ,aAAa,MAAM,SAAS,KAAK;CAChD,MAAM,WAAW,MAAM,OAAO,KAAK;CACnC,SAAS,UAAU;CACnB,MAAM,UAAU,MAAM,OAA0B,IAAI;CACpD,MAAM,WAAW,MAAM,OAAuB,IAAI;;CAElD,MAAM,QAAQ,MAAM,OAAO,KAAK;CAIhC,MAAM,gBAAgB;EACpB,IAAI;GACF,MAAM,SAAS,qBAAqB,CAAC,EAAE,QAAQ,eAAe,KAAK,CAAC;GACpE,IAAI,WAAW,UAAU,WAAW,WAAW,WAAW,QACxD,SAAS,MAAM;EAEnB,QAAQ,CAER;CACF,GAAG,CAAC,KAAK,CAAC;CAEV,MAAM,KAAK,MAAM,aACd,SAA8B;EAC7B,MAAM,UAAU;EAChB,SAAS,IAAI;EACb,IAAI,SAAS,QACX,UAAU,KAAK;EAEjB,IAAI;GACF,qBAAqB,CAAC,EAAE,QAAQ,eAAe,KAAK,GAAG,IAAI;EAC7D,QAAQ,CAER;CACF,GACA,CAAC,KAAK,CACR;CAIA,MAAM,gBAAgB;EACpB,OAAO,OAAO,QAAQ,UAAU;GAC9B,IAAI,SAAS,YAAY,WAAW,MAAM,UAAU,WAAW,MAAM,UAAU,UAC7E,UAAU,IAAI;EAElB,CAAC;CACH,GAAG,CAAC,MAAM,CAAC;CAEX,MAAM,gBAAgB;EACpB,MAAM,aAAa,UAAyB;GAC1C,IAAI,CAAC,iBAAiB,KAAK,GACzB;GAEF,MAAM,eAAe;GACrB,GAAG,SAAS,YAAY,SAAS,UAAU,MAAM;EACnD;EACA,OAAO,iBAAiB,WAAW,SAAS;EAC5C,aAAa,OAAO,oBAAoB,WAAW,SAAS;CAC9D,GAAG,CAAC,EAAE,CAAC;;;;;;;CAQP,MAAM,YAAY,MAAM,aACrB,UAA+C;EAC9C,IAAI,MAAM,QAAQ,UAChB;EAEF,MAAM,eAAe;EACrB,GAAG,SAAS,YAAY,SAAS,UAAU,MAAM;CACnD,GACA,CAAC,EAAE,CACL;CAEA,MAAM,OAAO,UAAU;;;;;;;;;;;;;CAcvB,MAAM,gBAAgB;EACpB,IAAI,CAAC,MAAM,SACT;EAEF,MAAM,QAAQ,4BAA4B;GACxC,IAAI,MACF,SAAS,SACL,cAA2B,kCAAgC,CAAC,EAC5D,MAAM,EAAE,eAAe,KAAK,CAAC;QAEjC,QAAQ,SAAS,MAAM,EAAE,eAAe,KAAK,CAAC;EAElD,CAAC;EACD,aAAa,qBAAqB,KAAK;CACzC,GAAG,CAAC,IAAI,CAAC;CAET,OACE,qBAAA,UAAA,EAAA,UAAA,CACE,qBAAC,UAAD;EACE,KAAK;EACL,MAAK;EACL,aAAU;EACV,cAAY;EACZ,eAAa,SAAS,KAAK,KAAA;EAC3B,iBAAe;EACf,cAAY,OAAO,UAAU;EAC7B,QAAQ;EACR,eAAe,GAAG,OAAO;EACzB,WAAW,GACT,uFACA,0FACA,6CACA,kFACA,8BACA,wFACA,oDACA,SACF;EAnBF,UAAA;GAqBE,oBAAC,QAAD;IAAM,WAAU;IACd,UAAA,oBAAC,WAAD;KAAW,IAAI,UAAU;KAAI,MAAM,UAAU;KAAM,MAAM;KAAI,YAAA;IAAY,CAAA;GACrE,CAAA;GACN,qBAAC,QAAD;IAAM,WAAU;IAAhB,UAAA,CAAsE,QAC/D,UAAU,IACX;;GACL,SACC,oBAAC,QAAD;IACE,aAAU;IACV,cAAW;IACX,WAAU;GACX,CAAA,IACC;EACE;CAIR,CAAA,GAAA,oBAAC,OAAD;EACE,KAAK;EACL,aAAU;EACV,cAAY;EACZ,MAAK;EACL,cAAY,GAAG,UAAU,KAAK;EAC9B,QAAQ,CAAC;EACE;EACX,WAAW,GACT,6EACA,oDACA,UAAU,SACN,yBACA,GACE,kDACA,oEACF,CACN;EAEA,UAAA,oBAAC,WAAD;GACU;GACR,WAAW,UAAU;GACrB,cAAc;GACd,WAAU;GACV,YACE,qBAAA,UAAA,EAAA,UAAA,CACE,oBAAC,QAAD;IACE,MAAK;IACL,SAAQ;IACR,aAAU;IACV,cAAY,UAAU,SAAS,sBAAsB;IACrD,eAAe,GAAG,UAAU,SAAS,UAAU,MAAM;IAEpD,UAAA,UAAU,SAAS,oBAAC,WAAD,EAAW,eAAA,KAAa,CAAA,IAAI,oBAAC,WAAD,EAAW,eAAA,KAAa,CAAA;GAClE,CAAA,GACR,oBAAC,QAAD;IACE,MAAK;IACL,SAAQ;IACR,aAAU;IACV,cAAW;IACX,eAAe,GAAG,MAAM;IAExB,UAAA,oBAAC,GAAD,EAAG,eAAA,KAAa,CAAA;GACV,CAAA,CACR,EAAA,CAAA;GAEJ,GAAI;EACL,CAAA;CACE,CAAA,CACL,EAAA,CAAA;AAEN"}
|
|
1
|
+
{"version":3,"file":"agent-chat-popup.js","names":[],"sources":["../../src/components/agent-chat-popup.tsx"],"sourcesContent":["\"use client\";\n\n/**\n * AgentChatPopup — the extension's own agent, in the corner of every screen.\n *\n * §17 of how-to-create-a-cortena-extension, audit rule P-25. Mounted in\n * `AppShell`'s `agentSlot`, so it stacks above the theme/help cluster rather\n * than beside it and no extension positions a floating control itself.\n *\n * ```tsx\n * <AppShell\n * extension={{ id: \"tasks\", name: \"Tasks\" }}\n * user={user}\n * agentSlot={\n * <AgentChatPopup\n * extension={{ id: \"tasks\", name: \"Tasks\" }}\n * client={client} // createAguiAgentChatClient({ …, agentId: \"tasks\" })\n * />\n * }\n * />\n * ```\n *\n * Three states, and the transitions between them are fixed: a collapsed pill,\n * a panel, and full screen. **Cmd/Ctrl+J** toggles between the pill and the\n * panel; **Escape** steps full screen -> panel -> pill, except while a run is\n * streaming, where the composer takes Escape to mean \"stop\" — the first Escape\n * during a run is nearly always about the run, not about the window.\n *\n * The chat stays mounted in every state, hidden rather than unmounted while\n * collapsed. That is not an optimisation: a run started before the user\n * collapsed the pop-up keeps streaming into it, and the unread dot on the pill\n * is what says so. Unmounting would throw the reply away halfway through.\n *\n * State is remembered per window, in `sessionStorage`, alongside the session\n * key — a second window of the same extension opens its own chat on its own\n * session rather than inheriting this one's.\n */\n\nimport { Maximize2, Minimize2, X } from \"lucide-react\";\nimport * as React from \"react\";\nimport { BrandMark } from \"@/components/app-shell\";\nimport { Button } from \"@/components/button\";\nimport { AgentChat, type AgentChatOwnedProps } from \"@/components/agent-chat\";\nimport { viewStorageKey, windowSessionStorage } from \"@/agent-chat/session\";\nimport type { AgentChatClient } from \"@/agent-chat/types\";\nimport { cn } from \"@/lib/cn\";\n\nexport type AgentChatPopupState = \"pill\" | \"panel\" | \"full\";\n\nexport interface AgentChatPopupExtension {\n /** Registered mark id — the same `x-cortena.icon` the shell draws. */\n id: string;\n /** Display name. Also the agent template slug, unless `agentId` overrides it. */\n name: string;\n}\n\nexport interface AgentChatPopupProps\n extends Omit<\n AgentChatOwnedProps,\n \"agentName\" | \"storageScope\" | \"className\" | \"headerSlot\" | \"client\" | \"chat\"\n > {\n extension: AgentChatPopupExtension;\n /** The transport. Its `agentId` is the template slug the sessions belong to. */\n client: AgentChatClient;\n /**\n * Names the `sessionStorage` slots. Defaults to the extension id, which is\n * what keeps two extensions on one origin out of each other's sessions.\n */\n storageScope?: string;\n /** Start here instead of collapsed; the stored state still wins on reload. */\n defaultState?: AgentChatPopupState;\n className?: string;\n}\n\n/** Cmd on a Mac, Ctrl elsewhere. Documented in README.md. */\nfunction isToggleShortcut(event: KeyboardEvent): boolean {\n return (event.metaKey || event.ctrlKey) && !event.altKey && event.key.toLowerCase() === \"j\";\n}\n\nfunction AgentChatPopup({\n extension,\n client,\n storageScope,\n defaultState = \"pill\",\n className,\n ...chatProps\n}: AgentChatPopupProps) {\n const scope = storageScope ?? extension.id;\n const [state, setState] = React.useState<AgentChatPopupState>(defaultState);\n const [unread, setUnread] = React.useState(false);\n const stateRef = React.useRef(state);\n stateRef.current = state;\n const pillRef = React.useRef<HTMLButtonElement>(null);\n const panelRef = React.useRef<HTMLDivElement>(null);\n /** Whether the user has opened or closed it, as against it starting open. */\n const moved = React.useRef(false);\n\n // Restore this window's open/collapsed choice. In an effect rather than a\n // lazy initialiser so a server render and the first client render agree.\n React.useEffect(() => {\n try {\n const stored = windowSessionStorage()?.getItem(viewStorageKey(scope));\n if (stored === \"pill\" || stored === \"panel\" || stored === \"full\") {\n setState(stored);\n }\n } catch {\n // A blocked storage just means the pop-up opens collapsed.\n }\n }, [scope]);\n\n const go = React.useCallback(\n (next: AgentChatPopupState) => {\n moved.current = true;\n setState(next);\n if (next !== \"pill\") {\n setUnread(false);\n }\n try {\n windowSessionStorage()?.setItem(viewStorageKey(scope), next);\n } catch {\n // Not remembering the choice is not a reason to refuse the change.\n }\n },\n [scope],\n );\n\n // The unread dot. Subscribed here rather than read out of the chat, because\n // the point is what happened while the surface was not being looked at.\n React.useEffect(() => {\n return client.onChat((event) => {\n if (stateRef.current === \"pill\" && (event.state === \"delta\" || event.state === \"final\")) {\n setUnread(true);\n }\n });\n }, [client]);\n\n React.useEffect(() => {\n const onKeyDown = (event: KeyboardEvent) => {\n if (!isToggleShortcut(event)) {\n return;\n }\n event.preventDefault();\n go(stateRef.current === \"pill\" ? \"panel\" : \"pill\");\n };\n window.addEventListener(\"keydown\", onKeyDown);\n return () => window.removeEventListener(\"keydown\", onKeyDown);\n }, [go]);\n\n /**\n * Escape steps down one level. It is handled on the panel rather than on the\n * window so it only fires when focus is inside the pop-up, and it arrives\n * here only when the composer did not claim it — the composer stops\n * propagation while a run is streaming, where Escape means \"stop the run\".\n */\n const onKeyDown = React.useCallback(\n (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key !== \"Escape\") {\n return;\n }\n event.preventDefault();\n go(stateRef.current === \"full\" ? \"panel\" : \"pill\");\n },\n [go],\n );\n\n const open = state !== \"pill\";\n\n /**\n * Focus follows the pop-up.\n *\n * The panel is `hidden`, not unmounted, so nothing moves focus by itself.\n * Opening left focus on the pill — which is now `hidden`, so the browser\n * resets it to <body> and the next Tab starts at the top of the page rather\n * than in the chat that was just opened. Closing had the mirror problem:\n * focus was inside a subtree that had just become hidden.\n *\n * Only after the user moves it. A pop-up that starts on `panel`, or one\n * restored from `sessionStorage` on load, must not steal focus from the page.\n */\n React.useEffect(() => {\n if (!moved.current) {\n return;\n }\n const frame = requestAnimationFrame(() => {\n if (open) {\n panelRef.current\n ?.querySelector<HTMLElement>('[data-slot=\"agent-chat-input\"]')\n ?.focus({ preventScroll: true });\n } else {\n pillRef.current?.focus({ preventScroll: true });\n }\n });\n return () => cancelAnimationFrame(frame);\n }, [open]);\n\n return (\n <>\n <button\n ref={pillRef}\n type=\"button\"\n data-slot=\"agent-chat-pill\"\n data-state={state}\n data-unread={unread ? \"\" : undefined}\n aria-expanded={open}\n aria-label={`Ask ${extension.name}`}\n hidden={open}\n onClick={() => go(\"panel\")}\n className={cn(\n \"relative inline-flex h-10 items-center gap-2 rounded-[var(--ds-radius-full)] px-3.5\",\n \"border border-[var(--ds-border)] bg-[var(--ds-card)] text-[color:var(--ds-foreground)]\",\n \"shadow-[var(--ds-shadow-md)] outline-none\",\n \"transition-colors duration-[var(--ds-duration-fast)] ease-[var(--ds-ease-out)]\",\n \"hover:bg-[var(--ds-hover)]\",\n \"focus-visible:ring-2 focus-visible:ring-[var(--ds-ring)] focus-visible:ring-offset-2\",\n \"focus-visible:ring-offset-[var(--ds-background)]\",\n className,\n )}\n >\n <span className=\"flex size-5 items-center justify-center text-[color:var(--ds-primary)]\">\n <BrandMark id={extension.id} name={extension.name} size={16} decorative />\n </span>\n <span className=\"text-[length:var(--ds-text-caption-lg)] font-medium\">\n Ask {extension.name}\n </span>\n {unread ? (\n <span\n data-slot=\"agent-chat-pill-unread\"\n aria-label=\"The agent is replying\"\n className=\"size-2 rounded-[var(--ds-radius-full)] bg-[var(--ds-primary)]\"\n />\n ) : null}\n </button>\n\n {/* Hidden, not unmounted: a run started before the pop-up was collapsed\n keeps streaming, and the pill's dot is what reports it. */}\n <div\n ref={panelRef}\n data-slot=\"agent-chat-panel\"\n data-state={state}\n role=\"dialog\"\n aria-label={`${extension.name} agent`}\n hidden={!open}\n onKeyDown={onKeyDown}\n className={cn(\n \"fixed z-50 flex flex-col overflow-hidden border border-[var(--ds-border)]\",\n \"bg-[var(--ds-card)] shadow-[var(--ds-shadow-lg)]\",\n state === \"full\"\n ? \"inset-0 rounded-none\"\n : cn(\n \"right-6 bottom-6 rounded-[var(--ds-radius-lg)]\",\n \"h-[min(40rem,calc(100vh-3rem))] w-[min(26.25rem,calc(100vw-3rem))]\",\n ),\n )}\n >\n <AgentChat\n client={client}\n agentName={extension.name}\n storageScope={scope}\n className=\"min-h-0 flex-1\"\n headerSlot={\n <>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n data-slot=\"agent-chat-expand\"\n aria-label={state === \"full\" ? \"Restore the panel\" : \"Expand to full screen\"}\n onClick={() => go(state === \"full\" ? \"panel\" : \"full\")}\n >\n {state === \"full\" ? <Minimize2 aria-hidden /> : <Maximize2 aria-hidden />}\n </Button>\n <Button\n size=\"icon-sm\"\n variant=\"ghost\"\n data-slot=\"agent-chat-collapse\"\n aria-label=\"Collapse the agent\"\n onClick={() => go(\"pill\")}\n >\n <X aria-hidden />\n </Button>\n </>\n }\n {...chatProps}\n />\n </div>\n </>\n );\n}\n\nexport { AgentChatPopup };\n"],"mappings":"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"}
|