cortena-ui 1.6.0 → 1.8.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 +122 -0
- package/README.md +104 -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/data-table/data-table.js +32 -4
- package/dist/components/data-table/data-table.js.map +1 -1
- package/dist/components/data-table/system-columns.js +22 -3
- package/dist/components/data-table/system-columns.js.map +1 -1
- package/dist/components/data-table/types.d.ts +42 -1
- package/dist/components/data-table/use-data-table.js +51 -11
- package/dist/components/data-table/use-data-table.js.map +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/components/data-table/data-table.tsx +62 -6
- package/src/components/data-table/system-columns.tsx +24 -1
- package/src/components/data-table/types.ts +42 -1
- package/src/components/data-table/use-data-table.ts +88 -10
- package/src/entries/agent-chat.ts +12 -0
package/dist/agent-chat/store.js
CHANGED
|
@@ -86,7 +86,10 @@ function reduce(state, action) {
|
|
|
86
86
|
case "send": return {
|
|
87
87
|
...state,
|
|
88
88
|
sessionKey: action.sessionKey,
|
|
89
|
-
messages: action.message ? [...state.messages, action.message] : state.messages
|
|
89
|
+
messages: action.message ? [...state.messages, action.message] : action.reanchor ? state.messages.map((m) => m.id === action.reanchor ? {
|
|
90
|
+
...m,
|
|
91
|
+
runId: action.runId
|
|
92
|
+
} : m) : state.messages,
|
|
90
93
|
streaming: true,
|
|
91
94
|
streamText: "",
|
|
92
95
|
streamThinkingText: "",
|
|
@@ -99,6 +102,10 @@ function reduce(state, action) {
|
|
|
99
102
|
approvals: [],
|
|
100
103
|
error: null
|
|
101
104
|
};
|
|
105
|
+
case "message/remove": return {
|
|
106
|
+
...state,
|
|
107
|
+
messages: state.messages.filter((m) => m.id !== action.id)
|
|
108
|
+
};
|
|
102
109
|
case "send/failed": return {
|
|
103
110
|
...state,
|
|
104
111
|
streaming: false,
|
|
@@ -126,6 +133,10 @@ function reduce(state, action) {
|
|
|
126
133
|
loadingHistory: true
|
|
127
134
|
};
|
|
128
135
|
case "history/loaded":
|
|
136
|
+
if (action.sessionKey !== state.sessionKey) return {
|
|
137
|
+
...state,
|
|
138
|
+
loadingHistory: false
|
|
139
|
+
};
|
|
129
140
|
if (wouldShrinkHistory({
|
|
130
141
|
currentSessionKey: state.sessionKey,
|
|
131
142
|
loadedSessionKey: action.sessionKey,
|
|
@@ -150,13 +161,18 @@ function reduce(state, action) {
|
|
|
150
161
|
streamThinkingText: "",
|
|
151
162
|
streamA2ui: []
|
|
152
163
|
};
|
|
153
|
-
case "history/older":
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
164
|
+
case "history/older":
|
|
165
|
+
if (action.sessionKey !== state.sessionKey) return {
|
|
166
|
+
...state,
|
|
167
|
+
loadingHistory: false
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
...state,
|
|
171
|
+
messages: [...action.messages, ...state.messages],
|
|
172
|
+
hasMoreHistory: action.hasMore,
|
|
173
|
+
loadingHistory: false,
|
|
174
|
+
historyCount: state.historyCount + action.records
|
|
175
|
+
};
|
|
160
176
|
case "history/failed": return {
|
|
161
177
|
...state,
|
|
162
178
|
loadingHistory: false
|
|
@@ -621,7 +637,7 @@ function endStep(base, event) {
|
|
|
621
637
|
}
|
|
622
638
|
/** How long a burst of step changes is collapsed for before it is written. */
|
|
623
639
|
const STEP_WRITE_DEBOUNCE_MS = 250;
|
|
624
|
-
function useAgentChat({ client, storageScope, ephemeral, thinking }) {
|
|
640
|
+
function useAgentChat({ client, storageScope, ephemeral, thinking, mintSessionKey: mintKey, restoreSession = true, listSessions = true }) {
|
|
625
641
|
const [state, dispatch] = React.useReducer(reduce, EMPTY);
|
|
626
642
|
const stateRef = React.useRef(state);
|
|
627
643
|
stateRef.current = state;
|
|
@@ -651,6 +667,7 @@ function useAgentChat({ client, storageScope, ephemeral, thinking }) {
|
|
|
651
667
|
};
|
|
652
668
|
}, [client]);
|
|
653
669
|
const refreshSessions = React.useCallback(() => {
|
|
670
|
+
if (!listSessions) return;
|
|
654
671
|
dispatch({ kind: "sessions/loading" });
|
|
655
672
|
client.request("sessions.list", { includeDerivedTitles: true }).then((response) => {
|
|
656
673
|
const sessions = (response?.sessions ?? []).filter((s) => isSessionOfAgent(s.key, client.agentId));
|
|
@@ -668,7 +685,7 @@ function useAgentChat({ client, storageScope, ephemeral, thinking }) {
|
|
|
668
685
|
message: errorText(err)
|
|
669
686
|
});
|
|
670
687
|
});
|
|
671
|
-
}, [client]);
|
|
688
|
+
}, [client, listSessions]);
|
|
672
689
|
const loadHistory = React.useCallback((sessionKey) => {
|
|
673
690
|
dispatch({ kind: "history/loading" });
|
|
674
691
|
client.request("chat.history", {
|
|
@@ -689,7 +706,7 @@ function useAgentChat({ client, storageScope, ephemeral, thinking }) {
|
|
|
689
706
|
React.useEffect(() => {
|
|
690
707
|
if (restored.current) return;
|
|
691
708
|
restored.current = true;
|
|
692
|
-
const stored = ephemeral ? null : readStoredSessionKey(storageScope);
|
|
709
|
+
const stored = ephemeral || !restoreSession ? null : readStoredSessionKey(storageScope);
|
|
693
710
|
const key = stored && isSessionOfAgent(stored, client.agentId) ? stored : null;
|
|
694
711
|
if (key) {
|
|
695
712
|
dispatch({
|
|
@@ -709,6 +726,7 @@ function useAgentChat({ client, storageScope, ephemeral, thinking }) {
|
|
|
709
726
|
ephemeral,
|
|
710
727
|
loadHistory,
|
|
711
728
|
refreshSessions,
|
|
729
|
+
restoreSession,
|
|
712
730
|
storageScope
|
|
713
731
|
]);
|
|
714
732
|
/**
|
|
@@ -759,24 +777,26 @@ function useAgentChat({ client, storageScope, ephemeral, thinking }) {
|
|
|
759
777
|
});
|
|
760
778
|
if (!ephemeral) writeStoredSessionKey(storageScope, key);
|
|
761
779
|
}, [ephemeral, storageScope]);
|
|
762
|
-
const run = React.useCallback((text, extra = {}) => {
|
|
780
|
+
const run = React.useCallback((text, extra = {}, reanchor) => {
|
|
763
781
|
const current = stateRef.current;
|
|
764
782
|
if (current.streaming) return;
|
|
765
|
-
const sessionKey = current.sessionKey ?? mintSessionKey(client.agentId);
|
|
783
|
+
const sessionKey = current.sessionKey ?? (mintKey ? mintKey() : mintSessionKey(client.agentId));
|
|
766
784
|
if (!current.sessionKey && !ephemeral) writeStoredSessionKey(storageScope, sessionKey);
|
|
767
785
|
const runId = generateId();
|
|
768
|
-
const message = text || !extra.a2uiAction && !extra.mcpAppAction ? {
|
|
786
|
+
const message = !reanchor && (text || !extra.a2uiAction && !extra.mcpAppAction) ? {
|
|
769
787
|
id: runId,
|
|
770
788
|
role: "user",
|
|
771
789
|
text,
|
|
772
790
|
timestamp: Date.now(),
|
|
773
|
-
runId
|
|
791
|
+
runId,
|
|
792
|
+
...extra.attachments?.length ? { attachmentItems: extra.attachments } : {}
|
|
774
793
|
} : null;
|
|
775
794
|
dispatch({
|
|
776
795
|
kind: "send",
|
|
777
796
|
message,
|
|
778
797
|
runId,
|
|
779
|
-
sessionKey
|
|
798
|
+
sessionKey,
|
|
799
|
+
...reanchor ? { reanchor } : {}
|
|
780
800
|
});
|
|
781
801
|
client.send({
|
|
782
802
|
sessionKey,
|
|
@@ -791,16 +811,44 @@ function useAgentChat({ client, storageScope, ephemeral, thinking }) {
|
|
|
791
811
|
}, [
|
|
792
812
|
client,
|
|
793
813
|
ephemeral,
|
|
814
|
+
mintKey,
|
|
794
815
|
refreshSessions,
|
|
795
816
|
storageScope,
|
|
796
817
|
thinking
|
|
797
818
|
]);
|
|
798
|
-
const send = React.useCallback((text, attachments) => {
|
|
799
|
-
run(text,
|
|
819
|
+
const send = React.useCallback((text, attachments, options) => {
|
|
820
|
+
run(text, {
|
|
821
|
+
...attachments?.length ? { attachments } : {},
|
|
822
|
+
...options?.model ? { model: options.model } : {},
|
|
823
|
+
...options?.thinking ? { thinking: options.thinking } : {}
|
|
824
|
+
});
|
|
800
825
|
}, [run]);
|
|
801
826
|
const sendAction = React.useCallback((a2uiAction) => {
|
|
802
827
|
run("", { a2uiAction });
|
|
803
828
|
}, [run]);
|
|
829
|
+
const sendMcpAppAction = React.useCallback((mcpAppAction) => {
|
|
830
|
+
run("", { mcpAppAction });
|
|
831
|
+
}, [run]);
|
|
832
|
+
const regenerate = React.useCallback((messageId) => {
|
|
833
|
+
const { messages, streaming } = stateRef.current;
|
|
834
|
+
if (streaming) return;
|
|
835
|
+
const index = messages.findIndex((m) => m.id === messageId);
|
|
836
|
+
if (index < 1) return;
|
|
837
|
+
let question = null;
|
|
838
|
+
for (let i = index - 1; i >= 0; i -= 1) {
|
|
839
|
+
const candidate = messages[i];
|
|
840
|
+
if (candidate?.role === "user" && !candidate.isToolMessage) {
|
|
841
|
+
question = candidate;
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
if (question === null) return;
|
|
846
|
+
dispatch({
|
|
847
|
+
kind: "message/remove",
|
|
848
|
+
id: messageId
|
|
849
|
+
});
|
|
850
|
+
run(question.text, {}, question.id);
|
|
851
|
+
}, [run]);
|
|
804
852
|
const abort = React.useCallback(() => {
|
|
805
853
|
const { runId, sessionKey } = stateRef.current;
|
|
806
854
|
dispatch({ kind: "abort" });
|
|
@@ -831,38 +879,55 @@ function useAgentChat({ client, storageScope, ephemeral, thinking }) {
|
|
|
831
879
|
});
|
|
832
880
|
}, [client]);
|
|
833
881
|
const newSession = React.useCallback(() => {
|
|
834
|
-
setSession(mintSessionKey(client.agentId));
|
|
835
|
-
}, [
|
|
882
|
+
setSession(mintKey ? mintKey() : mintSessionKey(client.agentId));
|
|
883
|
+
}, [
|
|
884
|
+
client,
|
|
885
|
+
mintKey,
|
|
886
|
+
setSession
|
|
887
|
+
]);
|
|
888
|
+
const clearSession = React.useCallback(() => {
|
|
889
|
+
dispatch({
|
|
890
|
+
kind: "session",
|
|
891
|
+
key: null
|
|
892
|
+
});
|
|
893
|
+
if (!ephemeral) writeStoredSessionKey(storageScope, null);
|
|
894
|
+
}, [ephemeral, storageScope]);
|
|
836
895
|
const resumeSession = React.useCallback((key) => {
|
|
837
896
|
setSession(key);
|
|
838
897
|
loadHistory(key);
|
|
839
898
|
}, [loadHistory, setSession]);
|
|
840
899
|
const loadOlder = React.useCallback(() => {
|
|
841
900
|
const current = stateRef.current;
|
|
842
|
-
if (!current.sessionKey || current.loadingHistory || !current.hasMoreHistory) return;
|
|
901
|
+
if (!current.sessionKey || current.loadingHistory || !current.hasMoreHistory) return false;
|
|
902
|
+
const sessionKey = current.sessionKey;
|
|
843
903
|
dispatch({ kind: "history/loading" });
|
|
844
904
|
client.request("chat.history", {
|
|
845
|
-
sessionKey
|
|
905
|
+
sessionKey,
|
|
846
906
|
limit: 100,
|
|
847
907
|
offset: current.historyCount
|
|
848
908
|
}).then((response) => {
|
|
849
909
|
const records = response?.messages ?? [];
|
|
850
910
|
dispatch({
|
|
851
911
|
kind: "history/older",
|
|
912
|
+
sessionKey,
|
|
852
913
|
messages: parseHistoryMessages(records),
|
|
853
914
|
hasMore: response?.hasMore ?? false,
|
|
854
915
|
records: records.length
|
|
855
916
|
});
|
|
856
917
|
}).catch(() => dispatch({ kind: "history/failed" }));
|
|
918
|
+
return true;
|
|
857
919
|
}, [client]);
|
|
858
920
|
return {
|
|
859
921
|
...state,
|
|
860
922
|
send,
|
|
861
923
|
sendAction,
|
|
924
|
+
sendMcpAppAction,
|
|
862
925
|
abort,
|
|
863
926
|
resolveApproval,
|
|
864
927
|
newSession,
|
|
865
928
|
resumeSession,
|
|
929
|
+
clearSession,
|
|
930
|
+
regenerate,
|
|
866
931
|
loadOlder,
|
|
867
932
|
refreshSessions
|
|
868
933
|
};
|
|
@@ -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;
|