@assistant-ui/react-google-adk 0.0.32 → 0.0.33

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 AgentbaseAI Inc.
3
+ Copyright (c) 2026 AgentbaseAI Inc.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1 +1 @@
1
- {"version":3,"file":"contentToParts.d.ts","sourceRoot":"","sources":["../src/contentToParts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAyB,mBAAgB;AAE/E,eAAO,MAAM,cAAc,GACzB,SAAS,UAAU,CAAC,SAAS,CAAC,KAC7B,YAAY,EAiCd,CAAC"}
1
+ {"version":3,"file":"contentToParts.d.ts","sourceRoot":"","sources":["../src/contentToParts.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAyB,mBAAgB;AAE/E,eAAO,MAAM,cAAc,GACzB,SAAS,UAAU,CAAC,SAAS,CAAC,KAC7B,YAAY,EAmCd,CAAC"}
@@ -29,6 +29,7 @@ const contentToParts = (content) => {
29
29
  output: part.output,
30
30
  outcome: part.outcome
31
31
  } };
32
+ case "activity": return { text: part.message };
32
33
  default: return { text: "" };
33
34
  }
34
35
  });
@@ -1 +1 @@
1
- {"version":3,"file":"contentToParts.js","names":[],"sources":["../src/contentToParts.ts"],"sourcesContent":["import type { AdkEventPart, AdkMessage, AdkMessageContentPart } from \"./types\";\n\nexport const contentToParts = (\n content: AdkMessage[\"content\"],\n): AdkEventPart[] => {\n if (typeof content === \"string\") return [{ text: content }];\n return (content as AdkMessageContentPart[]).map((part): AdkEventPart => {\n switch (part.type) {\n case \"text\":\n return { text: part.text };\n case \"reasoning\":\n return { text: part.text, thought: true };\n case \"image\":\n return { inlineData: { mimeType: part.mimeType, data: part.data } };\n case \"image_url\":\n return { fileData: { fileUri: part.url } };\n case \"file\":\n return { inlineData: { mimeType: part.mimeType, data: part.data } };\n case \"file_url\":\n return {\n fileData: {\n fileUri: part.url,\n ...(part.mimeType != null && { mimeType: part.mimeType }),\n },\n };\n case \"code\":\n return {\n executableCode: { code: part.code, language: part.language },\n };\n case \"code_result\":\n return {\n codeExecutionResult: { output: part.output, outcome: part.outcome },\n };\n default:\n return { text: \"\" };\n }\n });\n};\n"],"mappings":";AAEA,MAAa,kBACX,YACmB;CACnB,IAAI,OAAO,YAAY,UAAU,OAAO,CAAC,EAAE,MAAM,QAAQ,CAAC;CAC1D,OAAQ,QAAoC,KAAK,SAAuB;EACtE,QAAQ,KAAK,MAAb;GACE,KAAK,QACH,OAAO,EAAE,MAAM,KAAK,KAAK;GAC3B,KAAK,aACH,OAAO;IAAE,MAAM,KAAK;IAAM,SAAS;GAAK;GAC1C,KAAK,SACH,OAAO,EAAE,YAAY;IAAE,UAAU,KAAK;IAAU,MAAM,KAAK;GAAK,EAAE;GACpE,KAAK,aACH,OAAO,EAAE,UAAU,EAAE,SAAS,KAAK,IAAI,EAAE;GAC3C,KAAK,QACH,OAAO,EAAE,YAAY;IAAE,UAAU,KAAK;IAAU,MAAM,KAAK;GAAK,EAAE;GACpE,KAAK,YACH,OAAO,EACL,UAAU;IACR,SAAS,KAAK;IACd,GAAI,KAAK,YAAY,QAAQ,EAAE,UAAU,KAAK,SAAS;GACzD,EACF;GACF,KAAK,QACH,OAAO,EACL,gBAAgB;IAAE,MAAM,KAAK;IAAM,UAAU,KAAK;GAAS,EAC7D;GACF,KAAK,eACH,OAAO,EACL,qBAAqB;IAAE,QAAQ,KAAK;IAAQ,SAAS,KAAK;GAAQ,EACpE;GACF,SACE,OAAO,EAAE,MAAM,GAAG;EACtB;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"contentToParts.js","names":[],"sources":["../src/contentToParts.ts"],"sourcesContent":["import type { AdkEventPart, AdkMessage, AdkMessageContentPart } from \"./types\";\n\nexport const contentToParts = (\n content: AdkMessage[\"content\"],\n): AdkEventPart[] => {\n if (typeof content === \"string\") return [{ text: content }];\n return (content as AdkMessageContentPart[]).map((part): AdkEventPart => {\n switch (part.type) {\n case \"text\":\n return { text: part.text };\n case \"reasoning\":\n return { text: part.text, thought: true };\n case \"image\":\n return { inlineData: { mimeType: part.mimeType, data: part.data } };\n case \"image_url\":\n return { fileData: { fileUri: part.url } };\n case \"file\":\n return { inlineData: { mimeType: part.mimeType, data: part.data } };\n case \"file_url\":\n return {\n fileData: {\n fileUri: part.url,\n ...(part.mimeType != null && { mimeType: part.mimeType }),\n },\n };\n case \"code\":\n return {\n executableCode: { code: part.code, language: part.language },\n };\n case \"code_result\":\n return {\n codeExecutionResult: { output: part.output, outcome: part.outcome },\n };\n case \"activity\":\n return { text: part.message };\n default:\n return { text: \"\" };\n }\n });\n};\n"],"mappings":";AAEA,MAAa,kBACX,YACmB;CACnB,IAAI,OAAO,YAAY,UAAU,OAAO,CAAC,EAAE,MAAM,QAAQ,CAAC;CAC1D,OAAQ,QAAoC,KAAK,SAAuB;EACtE,QAAQ,KAAK,MAAb;GACE,KAAK,QACH,OAAO,EAAE,MAAM,KAAK,KAAK;GAC3B,KAAK,aACH,OAAO;IAAE,MAAM,KAAK;IAAM,SAAS;GAAK;GAC1C,KAAK,SACH,OAAO,EAAE,YAAY;IAAE,UAAU,KAAK;IAAU,MAAM,KAAK;GAAK,EAAE;GACpE,KAAK,aACH,OAAO,EAAE,UAAU,EAAE,SAAS,KAAK,IAAI,EAAE;GAC3C,KAAK,QACH,OAAO,EAAE,YAAY;IAAE,UAAU,KAAK;IAAU,MAAM,KAAK;GAAK,EAAE;GACpE,KAAK,YACH,OAAO,EACL,UAAU;IACR,SAAS,KAAK;IACd,GAAI,KAAK,YAAY,QAAQ,EAAE,UAAU,KAAK,SAAS;GACzD,EACF;GACF,KAAK,QACH,OAAO,EACL,gBAAgB;IAAE,MAAM,KAAK;IAAM,UAAU,KAAK;GAAS,EAC7D;GACF,KAAK,eACH,OAAO,EACL,qBAAqB;IAAE,QAAQ,KAAK;IAAQ,SAAS,KAAK;GAAQ,EACpE;GACF,KAAK,YACH,OAAO,EAAE,MAAM,KAAK,QAAQ;GAC9B,SACE,OAAO,EAAE,MAAM,GAAG;EACtB;CACF,CAAC;AACH"}
@@ -1 +1 @@
1
- {"version":3,"file":"convertAdkMessages.d.ts","sourceRoot":"","sources":["../src/convertAdkMessages.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAC5E,OAAO,EAEL,KAAK,eAAe,EACrB,6BAA0B;AAC3B,OAAO,KAAK,EAAE,UAAU,EAAyB,mBAAgB;AA4FjE,eAAO,MAAM,yBAAyB,GAElC,WAAW,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,KAC9C,2BAA2B,CAAC,QAAQ,CAAC,UAAU,CAgEjD,CAAC;AAEJ,eAAO,MAAM,iBAAiB,EAAE,2BAA2B,CAAC,QAAQ,CAAC,UAAU,CACnC,CAAC"}
1
+ {"version":3,"file":"convertAdkMessages.d.ts","sourceRoot":"","sources":["../src/convertAdkMessages.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAC5E,OAAO,EAEL,KAAK,eAAe,EACrB,6BAA0B;AAC3B,OAAO,KAAK,EAAE,UAAU,EAAyB,mBAAgB;AAiGjE,eAAO,MAAM,yBAAyB,GAElC,WAAW,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,KAC9C,2BAA2B,CAAC,QAAQ,CAAC,UAAU,CAgEjD,CAAC;AAEJ,eAAO,MAAM,iBAAiB,EAAE,2BAA2B,CAAC,QAAQ,CAAC,UAAU,CACnC,CAAC"}
@@ -62,6 +62,10 @@ const contentToParts = (content, role) => {
62
62
  outcome: part.outcome
63
63
  }
64
64
  };
65
+ case "activity": return {
66
+ type: "text",
67
+ text: typeof part.message === "string" ? part.message : ""
68
+ };
65
69
  default: return null;
66
70
  }
67
71
  }).filter((p) => p !== null);
@@ -1 +1 @@
1
- {"version":3,"file":"convertAdkMessages.js","names":[],"sources":["../src/convertAdkMessages.ts"],"sourcesContent":["\"use client\";\n\nimport type { ToolCallMessagePart } from \"@assistant-ui/core\";\nimport type { useExternalMessageConverter } from \"@assistant-ui/core/react\";\nimport {\n ADK_REQUEST_CONFIRMATION,\n type AdkToolApproval,\n} from \"./adkToolApproval\";\nimport type { AdkMessage, AdkMessageContentPart } from \"./types\";\n\ntype ContentPart =\n | { type: \"text\"; text: string }\n | { type: \"reasoning\"; text: string }\n | { type: \"image\"; image: string }\n | {\n type: \"file\";\n data: string;\n mimeType: string;\n filename?: string;\n sourceType?: \"url\";\n }\n | { type: \"data\"; name: string; data: unknown };\n\nconst contentToParts = (\n content: AdkMessage[\"content\"],\n role: \"user\" | \"assistant\",\n): ContentPart[] => {\n if (typeof content === \"string\")\n return [{ type: \"text\" as const, text: content }];\n if (!Array.isArray(content)) return [];\n\n return content\n .filter(\n (part): part is AdkMessageContentPart =>\n typeof part === \"object\" && part !== null,\n )\n .map((part): ContentPart | null => {\n switch (part.type) {\n case \"text\":\n return {\n type: \"text\",\n text: typeof part.text === \"string\" ? part.text : \"\",\n };\n case \"reasoning\":\n return {\n type: \"reasoning\",\n text: typeof part.text === \"string\" ? part.text : \"\",\n };\n case \"image\":\n return {\n type: \"image\",\n image: `data:${part.mimeType};base64,${part.data}`,\n };\n case \"image_url\":\n return { type: \"image\", image: part.url };\n case \"file\":\n return {\n type: \"file\",\n data: part.data,\n mimeType: part.mimeType,\n ...(part.filename != null && { filename: part.filename }),\n };\n case \"file_url\":\n if (role === \"user\") {\n return {\n type: \"file\",\n data: part.url,\n mimeType: part.mimeType ?? \"application/octet-stream\",\n sourceType: \"url\",\n };\n }\n return {\n type: \"data\",\n name: \"file_url\",\n data: {\n url: part.url,\n ...(part.mimeType != null && { mimeType: part.mimeType }),\n },\n };\n case \"code\":\n return {\n type: \"data\",\n name: \"executable_code\",\n data: { code: part.code, language: part.language },\n };\n case \"code_result\":\n return {\n type: \"data\",\n name: \"code_execution_result\",\n data: { output: part.output, outcome: part.outcome },\n };\n default:\n return null;\n }\n })\n .filter((p): p is NonNullable<typeof p> => p !== null);\n};\n\nconst EMPTY_APPROVALS: ReadonlyMap<string, AdkToolApproval> = new Map();\n\nexport const createAdkMessageConverter =\n (\n approvals: ReadonlyMap<string, AdkToolApproval>,\n ): useExternalMessageConverter.Callback<AdkMessage> =>\n (message) => {\n switch (message.type) {\n case \"human\":\n return {\n role: \"user\",\n id: message.id,\n content: contentToParts(message.content, \"user\"),\n };\n\n case \"ai\": {\n const toolCallParts: ToolCallMessagePart[] =\n message.tool_calls?.map((tc) => {\n const approval = approvals.get(tc.id);\n return {\n type: \"tool-call\",\n toolCallId: tc.id,\n toolName: tc.name,\n args: tc.args,\n argsText: tc.argsText ?? JSON.stringify(tc.args),\n ...(approval && { approval }),\n };\n }) ?? [];\n\n return {\n role: \"assistant\",\n id: message.id,\n content: [\n ...contentToParts(message.content, \"assistant\"),\n ...toolCallParts,\n ],\n ...(message.status && { status: message.status }),\n ...(message.author && {\n metadata: {\n custom: { author: message.author, branch: message.branch },\n },\n }),\n };\n }\n\n case \"tool\": {\n // A confirmation reply ADK could not read leaves its gate undecided.\n // The reply is not the agent's output, so it is dropped rather than\n // shown as the call's result while the gate waits to be answered again.\n // Only a reply to the confirmation itself is dropped: the gated call\n // carries the same approval, and its own result is the agent's real\n // output.\n const approval = approvals.get(message.tool_call_id);\n if (\n message.name === ADK_REQUEST_CONFIRMATION &&\n approval !== undefined &&\n approval.approved === undefined\n )\n return [];\n\n return {\n role: \"tool\",\n toolCallId: message.tool_call_id,\n toolName: message.name,\n result: message.content,\n isError: message.status === \"error\",\n };\n }\n }\n };\n\nexport const convertAdkMessage: useExternalMessageConverter.Callback<AdkMessage> =\n createAdkMessageConverter(EMPTY_APPROVALS);\n"],"mappings":";;;AAuBA,MAAM,kBACJ,SACA,SACkB;CAClB,IAAI,OAAO,YAAY,UACrB,OAAO,CAAC;EAAE,MAAM;EAAiB,MAAM;CAAQ,CAAC;CAClD,IAAI,CAAC,MAAM,QAAQ,OAAO,GAAG,OAAO,CAAC;CAErC,OAAO,QACJ,QACE,SACC,OAAO,SAAS,YAAY,SAAS,IACzC,CAAC,CACA,KAAK,SAA6B;EACjC,QAAQ,KAAK,MAAb;GACE,KAAK,QACH,OAAO;IACL,MAAM;IACN,MAAM,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO;GACpD;GACF,KAAK,aACH,OAAO;IACL,MAAM;IACN,MAAM,OAAO,KAAK,SAAS,WAAW,KAAK,OAAO;GACpD;GACF,KAAK,SACH,OAAO;IACL,MAAM;IACN,OAAO,QAAQ,KAAK,SAAS,UAAU,KAAK;GAC9C;GACF,KAAK,aACH,OAAO;IAAE,MAAM;IAAS,OAAO,KAAK;GAAI;GAC1C,KAAK,QACH,OAAO;IACL,MAAM;IACN,MAAM,KAAK;IACX,UAAU,KAAK;IACf,GAAI,KAAK,YAAY,QAAQ,EAAE,UAAU,KAAK,SAAS;GACzD;GACF,KAAK;IACH,IAAI,SAAS,QACX,OAAO;KACL,MAAM;KACN,MAAM,KAAK;KACX,UAAU,KAAK,YAAY;KAC3B,YAAY;IACd;IAEF,OAAO;KACL,MAAM;KACN,MAAM;KACN,MAAM;MACJ,KAAK,KAAK;MACV,GAAI,KAAK,YAAY,QAAQ,EAAE,UAAU,KAAK,SAAS;KACzD;IACF;GACF,KAAK,QACH,OAAO;IACL,MAAM;IACN,MAAM;IACN,MAAM;KAAE,MAAM,KAAK;KAAM,UAAU,KAAK;IAAS;GACnD;GACF,KAAK,eACH,OAAO;IACL,MAAM;IACN,MAAM;IACN,MAAM;KAAE,QAAQ,KAAK;KAAQ,SAAS,KAAK;IAAQ;GACrD;GACF,SACE,OAAO;EACX;CACF,CAAC,CAAC,CACD,QAAQ,MAAkC,MAAM,IAAI;AACzD;AAEA,MAAM,kCAAwD,IAAI,IAAI;AAEtE,MAAa,6BAET,eAED,YAAY;CACX,QAAQ,QAAQ,MAAhB;EACE,KAAK,SACH,OAAO;GACL,MAAM;GACN,IAAI,QAAQ;GACZ,SAAS,eAAe,QAAQ,SAAS,MAAM;EACjD;EAEF,KAAK,MAAM;GACT,MAAM,gBACJ,QAAQ,YAAY,KAAK,OAAO;IAC9B,MAAM,WAAW,UAAU,IAAI,GAAG,EAAE;IACpC,OAAO;KACL,MAAM;KACN,YAAY,GAAG;KACf,UAAU,GAAG;KACb,MAAM,GAAG;KACT,UAAU,GAAG,YAAY,KAAK,UAAU,GAAG,IAAI;KAC/C,GAAI,YAAY,EAAE,SAAS;IAC7B;GACF,CAAC,KAAK,CAAC;GAET,OAAO;IACL,MAAM;IACN,IAAI,QAAQ;IACZ,SAAS,CACP,GAAG,eAAe,QAAQ,SAAS,WAAW,GAC9C,GAAG,aACL;IACA,GAAI,QAAQ,UAAU,EAAE,QAAQ,QAAQ,OAAO;IAC/C,GAAI,QAAQ,UAAU,EACpB,UAAU,EACR,QAAQ;KAAE,QAAQ,QAAQ;KAAQ,QAAQ,QAAQ;IAAO,EAC3D,EACF;GACF;EACF;EAEA,KAAK,QAAQ;GAOX,MAAM,WAAW,UAAU,IAAI,QAAQ,YAAY;GACnD,IACE,QAAQ,SAAA,8BACR,aAAa,KAAA,KACb,SAAS,aAAa,KAAA,GAEtB,OAAO,CAAC;GAEV,OAAO;IACL,MAAM;IACN,YAAY,QAAQ;IACpB,UAAU,QAAQ;IAClB,QAAQ,QAAQ;IAChB,SAAS,QAAQ,WAAW;GAC9B;EACF;CACF;AACF;AAEF,MAAa,oBACX,0BAA0B,eAAe"}
1
+ {"version":3,"file":"convertAdkMessages.js","names":[],"sources":["../src/convertAdkMessages.ts"],"sourcesContent":["\"use client\";\n\nimport type { ToolCallMessagePart } from \"@assistant-ui/core\";\nimport type { useExternalMessageConverter } from \"@assistant-ui/core/react\";\nimport {\n ADK_REQUEST_CONFIRMATION,\n type AdkToolApproval,\n} from \"./adkToolApproval\";\nimport type { AdkMessage, AdkMessageContentPart } from \"./types\";\n\ntype ContentPart =\n | { type: \"text\"; text: string }\n | { type: \"reasoning\"; text: string }\n | { type: \"image\"; image: string }\n | {\n type: \"file\";\n data: string;\n mimeType: string;\n filename?: string;\n sourceType?: \"url\";\n }\n | { type: \"data\"; name: string; data: unknown };\n\nconst contentToParts = (\n content: AdkMessage[\"content\"],\n role: \"user\" | \"assistant\",\n): ContentPart[] => {\n if (typeof content === \"string\")\n return [{ type: \"text\" as const, text: content }];\n if (!Array.isArray(content)) return [];\n\n return content\n .filter(\n (part): part is AdkMessageContentPart =>\n typeof part === \"object\" && part !== null,\n )\n .map((part): ContentPart | null => {\n switch (part.type) {\n case \"text\":\n return {\n type: \"text\",\n text: typeof part.text === \"string\" ? part.text : \"\",\n };\n case \"reasoning\":\n return {\n type: \"reasoning\",\n text: typeof part.text === \"string\" ? part.text : \"\",\n };\n case \"image\":\n return {\n type: \"image\",\n image: `data:${part.mimeType};base64,${part.data}`,\n };\n case \"image_url\":\n return { type: \"image\", image: part.url };\n case \"file\":\n return {\n type: \"file\",\n data: part.data,\n mimeType: part.mimeType,\n ...(part.filename != null && { filename: part.filename }),\n };\n case \"file_url\":\n if (role === \"user\") {\n return {\n type: \"file\",\n data: part.url,\n mimeType: part.mimeType ?? \"application/octet-stream\",\n sourceType: \"url\",\n };\n }\n return {\n type: \"data\",\n name: \"file_url\",\n data: {\n url: part.url,\n ...(part.mimeType != null && { mimeType: part.mimeType }),\n },\n };\n case \"code\":\n return {\n type: \"data\",\n name: \"executable_code\",\n data: { code: part.code, language: part.language },\n };\n case \"code_result\":\n return {\n type: \"data\",\n name: \"code_execution_result\",\n data: { output: part.output, outcome: part.outcome },\n };\n case \"activity\":\n return {\n type: \"text\",\n text: typeof part.message === \"string\" ? part.message : \"\",\n };\n default:\n return null;\n }\n })\n .filter((p): p is NonNullable<typeof p> => p !== null);\n};\n\nconst EMPTY_APPROVALS: ReadonlyMap<string, AdkToolApproval> = new Map();\n\nexport const createAdkMessageConverter =\n (\n approvals: ReadonlyMap<string, AdkToolApproval>,\n ): useExternalMessageConverter.Callback<AdkMessage> =>\n (message) => {\n switch (message.type) {\n case \"human\":\n return {\n role: \"user\",\n id: message.id,\n content: contentToParts(message.content, \"user\"),\n };\n\n case \"ai\": {\n const toolCallParts: ToolCallMessagePart[] =\n message.tool_calls?.map((tc) => {\n const approval = approvals.get(tc.id);\n return {\n type: \"tool-call\",\n toolCallId: tc.id,\n toolName: tc.name,\n args: tc.args,\n argsText: tc.argsText ?? JSON.stringify(tc.args),\n ...(approval && { approval }),\n };\n }) ?? [];\n\n return {\n role: \"assistant\",\n id: message.id,\n content: [\n ...contentToParts(message.content, \"assistant\"),\n ...toolCallParts,\n ],\n ...(message.status && { status: message.status }),\n ...(message.author && {\n metadata: {\n custom: { author: message.author, branch: message.branch },\n },\n }),\n };\n }\n\n case \"tool\": {\n // A confirmation reply ADK could not read leaves its gate undecided.\n // The reply is not the agent's output, so it is dropped rather than\n // shown as the call's result while the gate waits to be answered again.\n // Only a reply to the confirmation itself is dropped: the gated call\n // carries the same approval, and its own result is the agent's real\n // output.\n const approval = approvals.get(message.tool_call_id);\n if (\n message.name === ADK_REQUEST_CONFIRMATION &&\n approval !== undefined &&\n approval.approved === undefined\n )\n return [];\n\n return {\n role: \"tool\",\n toolCallId: message.tool_call_id,\n toolName: message.name,\n result: message.content,\n isError: message.status === \"error\",\n };\n }\n }\n };\n\nexport const convertAdkMessage: useExternalMessageConverter.Callback<AdkMessage> =\n createAdkMessageConverter(EMPTY_APPROVALS);\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  //#region src/sdkIdentity.ts
2
2
  const ADK_SDK = {
3
3
  name: "@assistant-ui/react-google-adk",
4
- version: "0.0.32"
4
+ version: "0.0.33"
5
5
  };
6
6
  //#endregion
7
7
  export { ADK_SDK };
@@ -1 +1 @@
1
- {"version":3,"file":"useAdkMessages.d.ts","sourceRoot":"","sources":["../src/useAdkMessages.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAEV,UAAU,EACV,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,wBAAwB,EACxB,0BAA0B,EAC3B,mBAAgB;AAEjB,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,aAAa,CAAC,EAAE;QACd,OAAO,CAAC,EAAE,kBAAkB,CAAC;QAC7B,aAAa,CAAC,EAAE,wBAAwB,CAAC;QACzC,eAAe,CAAC,EAAE,0BAA0B,CAAC;KAC9C,CAAC;CACH,CAAC;AAYF,eAAO,MAAM,cAAc,GAAI,4BAG5B,qBAAqB;;;;eAIb,MAAM,GAAG,SAAS;iBAChB,MAAM,GAAG,SAAS;;;;;;;;+BAsFP,UAAU,EAAE,UAAU,oBAAoB;;wBAvDhB,UAAU,EAAE;4BAmCnD,UAAU,EAAE;8BAnBR,iBAAiB;CAwM/B,CAAC"}
1
+ {"version":3,"file":"useAdkMessages.d.ts","sourceRoot":"","sources":["../src/useAdkMessages.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAEV,UAAU,EACV,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,wBAAwB,EACxB,0BAA0B,EAC3B,mBAAgB;AAEjB,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,aAAa,CAAC,EAAE;QACd,OAAO,CAAC,EAAE,kBAAkB,CAAC;QAC7B,aAAa,CAAC,EAAE,wBAAwB,CAAC;QACzC,eAAe,CAAC,EAAE,0BAA0B,CAAC;KAC9C,CAAC;CACH,CAAC;AAYF,eAAO,MAAM,cAAc,GAAI,4BAG5B,qBAAqB;;;;eAIb,MAAM,GAAG,SAAS;iBAChB,MAAM,GAAG,SAAS;;;;;;;;+BAqFP,UAAU,EAAE,UAAU,oBAAoB;;wBAvDhB,UAAU,EAAE;4BAmCnD,UAAU,EAAE;8BAnBR,iBAAiB;CAyM/B,CAAC"}
@@ -4,7 +4,8 @@ import { AdkEventAccumulator } from "./AdkEventAccumulator.js";
4
4
  import { abortableIterable, invokeUserCallback, openAbortableIterable } from "@assistant-ui/core/internal";
5
5
  import { generateId } from "@assistant-ui/core";
6
6
  import { useAui } from "@assistant-ui/store";
7
- import { useCallback, useEffect, useInsertionEffect, useMemo, useRef, useState } from "react";
7
+ import { useReplaySafeEffect } from "@assistant-ui/store/internal";
8
+ import { useCallback, useInsertionEffect, useMemo, useRef, useState } from "react";
8
9
  //#region src/useAdkMessages.ts
9
10
  const invokeAdkRuntimeCallback = (name, callback, ...args) => {
10
11
  invokeUserCallback("react-google-adk", name, callback, ...args);
@@ -19,7 +20,6 @@ const useAdkMessages = ({ stream, eventHandlers }) => {
19
20
  const [authRequests, setAuthRequests] = useState([]);
20
21
  const [escalated, setEscalated] = useState(false);
21
22
  const [messageMetadata, setMessageMetadata] = useState(/* @__PURE__ */ new Map());
22
- const lastTransferToAgentRef = useRef(void 0);
23
23
  const messagesRef = useRef(messages);
24
24
  const longRunningToolIdsRef = useRef(longRunningToolIds);
25
25
  const stateDeltaRef = useRef(stateDelta);
@@ -83,6 +83,7 @@ const useAdkMessages = ({ stream, eventHandlers }) => {
83
83
  setLongRunningToolIds(accumulator.getLongRunningToolIds());
84
84
  setToolConfirmations(accumulator.getToolConfirmations());
85
85
  setAuthRequests(accumulator.getAuthRequests());
86
+ let lastTransferToAgent;
86
87
  abortControllerRef.current?.abort();
87
88
  const abortController = new AbortController();
88
89
  abortControllerRef.current = abortController;
@@ -117,8 +118,8 @@ const useAdkMessages = ({ stream, eventHandlers }) => {
117
118
  if (newMeta.size > 0) setMessageMetadata(new Map([...messageMetadataRef.current, ...newMeta]));
118
119
  }
119
120
  const transfer = accumulator.getLastTransferToAgent();
120
- if (transfer && transfer !== lastTransferToAgentRef.current) {
121
- lastTransferToAgentRef.current = transfer;
121
+ if (transfer && transfer !== lastTransferToAgent) {
122
+ lastTransferToAgent = transfer;
122
123
  invokeAdkRuntimeCallback("onAgentTransfer", onAgentTransfer, transfer);
123
124
  }
124
125
  if (event.customMetadata && onCustomEvent) for (const [key, value] of Object.entries(event.customMetadata)) invokeAdkRuntimeCallback("onCustomEvent", onCustomEvent, key, value);
@@ -141,7 +142,7 @@ const useAdkMessages = ({ stream, eventHandlers }) => {
141
142
  const cancel = useCallback(() => {
142
143
  if (abortControllerRef.current) abortControllerRef.current.abort();
143
144
  }, []);
144
- useEffect(() => cancel, [cancel]);
145
+ useReplaySafeEffect(() => cancel, []);
145
146
  return {
146
147
  messages,
147
148
  stateDelta,
@@ -1 +1 @@
1
- {"version":3,"file":"useAdkMessages.js","names":[],"sources":["../src/useAdkMessages.ts"],"sourcesContent":["import {\n useState,\n useCallback,\n useEffect,\n useInsertionEffect,\n useRef,\n useMemo,\n} from \"react\";\nimport { generateId } from \"@assistant-ui/core\";\nimport { useAui } from \"@assistant-ui/store\";\nimport {\n abortableIterable,\n invokeUserCallback,\n openAbortableIterable,\n} from \"@assistant-ui/core/internal\";\nimport { AdkEventAccumulator } from \"./AdkEventAccumulator\";\nimport { contentToParts } from \"./contentToParts\";\nimport { toAdkFunctionResponse } from \"./toAdkFunctionResponse\";\nimport type {\n AdkEvent,\n AdkMessage,\n AdkMessageMetadata,\n AdkSendMessageConfig,\n AdkStreamCallback,\n AdkToolConfirmation,\n AdkAuthRequest,\n AdkThreadSnapshot,\n OnAdkErrorCallback,\n OnAdkCustomEventCallback,\n OnAdkAgentTransferCallback,\n} from \"./types\";\n\nexport type UseAdkMessagesOptions = {\n stream: AdkStreamCallback;\n eventHandlers?: {\n onError?: OnAdkErrorCallback;\n onCustomEvent?: OnAdkCustomEventCallback;\n onAgentTransfer?: OnAdkAgentTransferCallback;\n };\n};\n\ntype AdkRuntimeCallbackName = \"onError\" | \"onCustomEvent\" | \"onAgentTransfer\";\n\nconst invokeAdkRuntimeCallback = <TArgs extends readonly unknown[]>(\n name: AdkRuntimeCallbackName,\n callback: ((...args: TArgs) => unknown) | undefined,\n ...args: TArgs\n): void => {\n void invokeUserCallback(\"react-google-adk\", name, callback, ...args);\n};\n\nexport const useAdkMessages = ({\n stream,\n eventHandlers,\n}: UseAdkMessagesOptions) => {\n const [messages, _setMessages] = useState<AdkMessage[]>([]);\n const [stateDelta, setStateDelta] = useState<Record<string, unknown>>({});\n const [agentInfo, setAgentInfo] = useState<{\n name?: string | undefined;\n branch?: string | undefined;\n }>({});\n const [longRunningToolIds, _setLongRunningToolIds] = useState<string[]>([]);\n const [artifactDelta, setArtifactDelta] = useState<Record<string, number>>(\n {},\n );\n const [toolConfirmations, setToolConfirmations] = useState<\n AdkToolConfirmation[]\n >([]);\n const [authRequests, setAuthRequests] = useState<AdkAuthRequest[]>([]);\n const [escalated, setEscalated] = useState(false);\n const [messageMetadata, setMessageMetadata] = useState<\n Map<string, AdkMessageMetadata>\n >(new Map());\n const lastTransferToAgentRef = useRef<string | undefined>(undefined);\n // setMessagesImmediate and setLongRunningToolIds are the only writers of their state and publish these refs with it, so neither ref trails a commit.\n const messagesRef = useRef(messages);\n const longRunningToolIdsRef = useRef(longRunningToolIds);\n const stateDeltaRef = useRef(stateDelta);\n useInsertionEffect(() => {\n stateDeltaRef.current = stateDelta;\n }, [stateDelta]);\n const artifactDeltaRef = useRef(artifactDelta);\n useInsertionEffect(() => {\n artifactDeltaRef.current = artifactDelta;\n }, [artifactDelta]);\n const messageMetadataRef = useRef(messageMetadata);\n useInsertionEffect(() => {\n messageMetadataRef.current = messageMetadata;\n }, [messageMetadata]);\n\n const setMessagesImmediate = useCallback((msgs: AdkMessage[]) => {\n messagesRef.current = msgs;\n _setMessages(msgs);\n }, []);\n const setLongRunningToolIds = useCallback((ids: string[]) => {\n longRunningToolIdsRef.current = ids;\n _setLongRunningToolIds(ids);\n }, []);\n\n /**\n * Swap the thread over to a loaded snapshot in one commit. Unlike\n * {@link replaceMessages} this never passes through a cleared state, so a\n * refetch that lands while a confirmation is on screen replaces it rather\n * than blanking it first.\n */\n const applySnapshot = useCallback(\n (snapshot: AdkThreadSnapshot) => {\n setMessagesImmediate(snapshot.messages);\n setLongRunningToolIds(snapshot.longRunningToolIds ?? []);\n setToolConfirmations(snapshot.toolConfirmations ?? []);\n setAuthRequests(snapshot.authRequests ?? []);\n setEscalated(snapshot.escalated ?? false);\n setMessageMetadata(snapshot.messageMetadata ?? new Map());\n setStateDelta(snapshot.stateDelta ?? {});\n setArtifactDelta(snapshot.artifactDelta ?? {});\n setAgentInfo(snapshot.agentInfo ?? {});\n },\n [setLongRunningToolIds, setMessagesImmediate],\n );\n\n // Replace the message list AND reset derived per-turn HITL state.\n // Used by truncation paths (edit, reload) so that stale interrupt\n // markers and per-message metadata from the removed messages don't leak\n // into the next turn.\n const replaceMessages = useCallback(\n (msgs: AdkMessage[]) => {\n setMessagesImmediate(msgs);\n setLongRunningToolIds([]);\n setToolConfirmations([]);\n setAuthRequests([]);\n setEscalated(false);\n setMessageMetadata(new Map());\n },\n [setLongRunningToolIds, setMessagesImmediate],\n );\n\n const abortControllerRef = useRef<AbortController | null>(null);\n\n const { onError, onCustomEvent, onAgentTransfer } = useMemo(\n () => eventHandlers ?? {},\n [eventHandlers],\n );\n\n const aui = useAui();\n const sendMessage = useCallback(\n async (newMessages: AdkMessage[], config: AdkSendMessageConfig) => {\n const newMessagesWithId = newMessages.map((m) =>\n m.id ? m : { ...m, id: generateId() },\n ) as AdkMessage[];\n\n // A staged message is already in the thread under its own id, and the\n // merged event below re-emits the whole batch under the first one. Seeding\n // with the originals would leave every later staged id beside the merged\n // copy of itself.\n const resentIds = new Set(newMessagesWithId.map((m) => m.id));\n // The optimistic event for a tool-only batch carries no author, so the accumulator cannot settle the calls this send answers.\n const answeredToolCallIds = new Set(\n newMessagesWithId.flatMap((m) =>\n m.type === \"tool\" ? [m.tool_call_id] : [],\n ),\n );\n const accumulator = new AdkEventAccumulator(\n messagesRef.current.filter((m) => !resentIds.has(m.id)),\n longRunningToolIdsRef.current.filter(\n (id) => !answeredToolCallIds.has(id),\n ),\n );\n for (const event of messagesToEvents(newMessagesWithId)) {\n accumulator.processEvent(event);\n }\n setMessagesImmediate(accumulator.getMessages());\n setLongRunningToolIds(accumulator.getLongRunningToolIds());\n setToolConfirmations(accumulator.getToolConfirmations());\n setAuthRequests(accumulator.getAuthRequests());\n\n // Google ADK replaces active runs, while React LangGraph queues sends.\n abortControllerRef.current?.abort();\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n\n try {\n const response = await openAbortableIterable(\n stream(newMessagesWithId, {\n ...config,\n abortSignal: abortController.signal,\n initialize: async () => {\n return await aui.threadListItem.initialize();\n },\n }),\n abortController.signal,\n );\n if (!response) return;\n\n for await (const event of abortableIterable(\n response,\n abortController.signal,\n )) {\n if (\n abortController.signal.aborted ||\n abortControllerRef.current !== abortController\n ) {\n break;\n }\n const updatedMessages = accumulator.processEvent(event);\n setMessagesImmediate(updatedMessages);\n setStateDelta({\n ...stateDeltaRef.current,\n ...accumulator.getStateDelta(),\n });\n setAgentInfo(accumulator.getAgentInfo());\n setLongRunningToolIds(accumulator.getLongRunningToolIds());\n setArtifactDelta({\n ...artifactDeltaRef.current,\n ...accumulator.getArtifactDelta(),\n });\n setToolConfirmations(accumulator.getToolConfirmations());\n setAuthRequests(accumulator.getAuthRequests());\n setEscalated(accumulator.isEscalated());\n {\n const newMeta = accumulator.getMessageMetadata();\n if (newMeta.size > 0) {\n setMessageMetadata(\n new Map([...messageMetadataRef.current, ...newMeta]),\n );\n }\n }\n\n const transfer = accumulator.getLastTransferToAgent();\n if (transfer && transfer !== lastTransferToAgentRef.current) {\n lastTransferToAgentRef.current = transfer;\n invokeAdkRuntimeCallback(\n \"onAgentTransfer\",\n onAgentTransfer,\n transfer,\n );\n }\n\n // Fire custom event callback for events with customMetadata\n if (event.customMetadata && onCustomEvent) {\n for (const [key, value] of Object.entries(event.customMetadata)) {\n invokeAdkRuntimeCallback(\n \"onCustomEvent\",\n onCustomEvent,\n key,\n value,\n );\n }\n }\n\n if (event.errorCode || event.errorMessage) {\n invokeAdkRuntimeCallback(\n \"onError\",\n onError,\n event.errorMessage ?? event.errorCode,\n );\n }\n }\n } catch (error) {\n if (\n !abortController.signal.aborted &&\n abortControllerRef.current === abortController &&\n !(error instanceof Error && error.name === \"AbortError\")\n ) {\n throw error;\n }\n } finally {\n if (abortControllerRef.current === abortController) {\n abortControllerRef.current = null;\n }\n }\n },\n [\n aui,\n setMessagesImmediate,\n setLongRunningToolIds,\n stream,\n onError,\n onCustomEvent,\n onAgentTransfer,\n ],\n );\n\n const cancel = useCallback(() => {\n if (abortControllerRef.current) {\n abortControllerRef.current.abort();\n }\n }, []);\n\n useEffect(() => cancel, [cancel]);\n\n return {\n messages,\n stateDelta,\n agentInfo,\n longRunningToolIds,\n artifactDelta,\n toolConfirmations,\n authRequests,\n escalated,\n messageMetadata,\n sendMessage,\n cancel,\n setMessages: setMessagesImmediate,\n replaceMessages,\n applySnapshot,\n };\n};\n\n/**\n * Transport sends every human and tool message of one `send` call as a single\n * ADK `Content`, and ADK parses that event's function responses before running\n * any tool, so the batch runs whole or not at all. The optimistic projection\n * has to sit on the same boundary, so a run of those messages becomes one\n * synthetic event whose parts come from the same per-message conversion.\n *\n * The transport drops `ai` messages from that `Content`, so one interleaved\n * between two replies does not split the batch on the wire and must not split\n * it here either. It still becomes its own event, placed after the merged one,\n * so the optimistic projection keeps the assistant turn.\n *\n * @internal — exported for unit tests.\n */\nexport const messagesToEvents = (messages: AdkMessage[]): AdkEvent[] => {\n // A reload sends no messages at all, and the empty user content the transport\n // puts on the wire for it is not part of the optimistic view: projecting one\n // would put an empty user bubble above every regenerated turn.\n if (messages.length === 0) return [];\n\n const events: AdkEvent[] = [];\n const run: AdkMessage[] = [];\n let runIndex = 0;\n\n for (const msg of messages) {\n if (msg.type === \"ai\") {\n events.push(messageToEvent(msg));\n } else {\n if (run.length === 0) runIndex = events.length;\n run.push(msg);\n }\n }\n\n const parts = run.flatMap((m) => messageToEvent(m).content?.parts ?? []);\n const human = run.find((m) => m.type === \"human\");\n\n // A batch that contributes no part still reaches the wire: the transport\n // sends an empty user `Content`, which a reload replays as an empty human\n // message. Emitting it here keeps the optimistic view equal to that replay.\n if (parts.length === 0) parts.push({ text: \"\" });\n\n const event: AdkEvent = { id: (human ?? run[0])?.id ?? generateId() };\n if (human || run.length === 0) event.author = \"user\";\n event.content = { role: \"user\", parts };\n events.splice(run.length > 0 ? runIndex : events.length, 0, event);\n\n return events;\n};\n\n/** @internal — exported for unit tests. */\nexport const messageToEvent = (msg: AdkMessage): AdkEvent => {\n if (msg.type === \"human\") {\n return {\n id: msg.id ?? generateId(),\n author: \"user\",\n content: { role: \"user\", parts: contentToParts(msg.content) },\n };\n }\n\n if (msg.type === \"tool\") {\n let response: unknown;\n try {\n response = JSON.parse(msg.content);\n } catch {\n response = msg.content;\n }\n return {\n id: msg.id ?? generateId(),\n content: {\n role: \"user\",\n parts: [\n {\n functionResponse: {\n name: msg.name,\n id: msg.tool_call_id,\n response: toAdkFunctionResponse(response, msg.status === \"error\"),\n },\n },\n ],\n },\n };\n }\n\n const result: AdkEvent = { id: msg.id ?? generateId() };\n if (msg.author != null) result.author = msg.author;\n result.content = {\n role: \"model\",\n parts: [\n ...contentToParts(msg.content),\n ...(msg.tool_calls?.map((tc) => ({\n functionCall: { name: tc.name, id: tc.id, args: { ...tc.args } },\n })) ?? []),\n ],\n };\n return result;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"useAdkMessages.js","names":[],"sources":["../src/useAdkMessages.ts"],"sourcesContent":["import {\n useState,\n useCallback,\n useInsertionEffect,\n useRef,\n useMemo,\n} from \"react\";\nimport { generateId } from \"@assistant-ui/core\";\nimport { useReplaySafeEffect } from \"@assistant-ui/store/internal\";\nimport { useAui } from \"@assistant-ui/store\";\nimport {\n abortableIterable,\n invokeUserCallback,\n openAbortableIterable,\n} from \"@assistant-ui/core/internal\";\nimport { AdkEventAccumulator } from \"./AdkEventAccumulator\";\nimport { contentToParts } from \"./contentToParts\";\nimport { toAdkFunctionResponse } from \"./toAdkFunctionResponse\";\nimport type {\n AdkEvent,\n AdkMessage,\n AdkMessageMetadata,\n AdkSendMessageConfig,\n AdkStreamCallback,\n AdkToolConfirmation,\n AdkAuthRequest,\n AdkThreadSnapshot,\n OnAdkErrorCallback,\n OnAdkCustomEventCallback,\n OnAdkAgentTransferCallback,\n} from \"./types\";\n\nexport type UseAdkMessagesOptions = {\n stream: AdkStreamCallback;\n eventHandlers?: {\n onError?: OnAdkErrorCallback;\n onCustomEvent?: OnAdkCustomEventCallback;\n onAgentTransfer?: OnAdkAgentTransferCallback;\n };\n};\n\ntype AdkRuntimeCallbackName = \"onError\" | \"onCustomEvent\" | \"onAgentTransfer\";\n\nconst invokeAdkRuntimeCallback = <TArgs extends readonly unknown[]>(\n name: AdkRuntimeCallbackName,\n callback: ((...args: TArgs) => unknown) | undefined,\n ...args: TArgs\n): void => {\n void invokeUserCallback(\"react-google-adk\", name, callback, ...args);\n};\n\nexport const useAdkMessages = ({\n stream,\n eventHandlers,\n}: UseAdkMessagesOptions) => {\n const [messages, _setMessages] = useState<AdkMessage[]>([]);\n const [stateDelta, setStateDelta] = useState<Record<string, unknown>>({});\n const [agentInfo, setAgentInfo] = useState<{\n name?: string | undefined;\n branch?: string | undefined;\n }>({});\n const [longRunningToolIds, _setLongRunningToolIds] = useState<string[]>([]);\n const [artifactDelta, setArtifactDelta] = useState<Record<string, number>>(\n {},\n );\n const [toolConfirmations, setToolConfirmations] = useState<\n AdkToolConfirmation[]\n >([]);\n const [authRequests, setAuthRequests] = useState<AdkAuthRequest[]>([]);\n const [escalated, setEscalated] = useState(false);\n const [messageMetadata, setMessageMetadata] = useState<\n Map<string, AdkMessageMetadata>\n >(new Map());\n // setMessagesImmediate and setLongRunningToolIds are the only writers of their state and publish these refs with it, so neither ref trails a commit.\n const messagesRef = useRef(messages);\n const longRunningToolIdsRef = useRef(longRunningToolIds);\n const stateDeltaRef = useRef(stateDelta);\n useInsertionEffect(() => {\n stateDeltaRef.current = stateDelta;\n }, [stateDelta]);\n const artifactDeltaRef = useRef(artifactDelta);\n useInsertionEffect(() => {\n artifactDeltaRef.current = artifactDelta;\n }, [artifactDelta]);\n const messageMetadataRef = useRef(messageMetadata);\n useInsertionEffect(() => {\n messageMetadataRef.current = messageMetadata;\n }, [messageMetadata]);\n\n const setMessagesImmediate = useCallback((msgs: AdkMessage[]) => {\n messagesRef.current = msgs;\n _setMessages(msgs);\n }, []);\n const setLongRunningToolIds = useCallback((ids: string[]) => {\n longRunningToolIdsRef.current = ids;\n _setLongRunningToolIds(ids);\n }, []);\n\n /**\n * Swap the thread over to a loaded snapshot in one commit. Unlike\n * {@link replaceMessages} this never passes through a cleared state, so a\n * refetch that lands while a confirmation is on screen replaces it rather\n * than blanking it first.\n */\n const applySnapshot = useCallback(\n (snapshot: AdkThreadSnapshot) => {\n setMessagesImmediate(snapshot.messages);\n setLongRunningToolIds(snapshot.longRunningToolIds ?? []);\n setToolConfirmations(snapshot.toolConfirmations ?? []);\n setAuthRequests(snapshot.authRequests ?? []);\n setEscalated(snapshot.escalated ?? false);\n setMessageMetadata(snapshot.messageMetadata ?? new Map());\n setStateDelta(snapshot.stateDelta ?? {});\n setArtifactDelta(snapshot.artifactDelta ?? {});\n setAgentInfo(snapshot.agentInfo ?? {});\n },\n [setLongRunningToolIds, setMessagesImmediate],\n );\n\n // Replace the message list AND reset derived per-turn HITL state.\n // Used by truncation paths (edit, reload) so that stale interrupt\n // markers and per-message metadata from the removed messages don't leak\n // into the next turn.\n const replaceMessages = useCallback(\n (msgs: AdkMessage[]) => {\n setMessagesImmediate(msgs);\n setLongRunningToolIds([]);\n setToolConfirmations([]);\n setAuthRequests([]);\n setEscalated(false);\n setMessageMetadata(new Map());\n },\n [setLongRunningToolIds, setMessagesImmediate],\n );\n\n const abortControllerRef = useRef<AbortController | null>(null);\n\n const { onError, onCustomEvent, onAgentTransfer } = useMemo(\n () => eventHandlers ?? {},\n [eventHandlers],\n );\n\n const aui = useAui();\n const sendMessage = useCallback(\n async (newMessages: AdkMessage[], config: AdkSendMessageConfig) => {\n const newMessagesWithId = newMessages.map((m) =>\n m.id ? m : { ...m, id: generateId() },\n ) as AdkMessage[];\n\n // A staged message is already in the thread under its own id, and the\n // merged event below re-emits the whole batch under the first one. Seeding\n // with the originals would leave every later staged id beside the merged\n // copy of itself.\n const resentIds = new Set(newMessagesWithId.map((m) => m.id));\n // The optimistic event for a tool-only batch carries no author, so the accumulator cannot settle the calls this send answers.\n const answeredToolCallIds = new Set(\n newMessagesWithId.flatMap((m) =>\n m.type === \"tool\" ? [m.tool_call_id] : [],\n ),\n );\n const accumulator = new AdkEventAccumulator(\n messagesRef.current.filter((m) => !resentIds.has(m.id)),\n longRunningToolIdsRef.current.filter(\n (id) => !answeredToolCallIds.has(id),\n ),\n );\n for (const event of messagesToEvents(newMessagesWithId)) {\n accumulator.processEvent(event);\n }\n setMessagesImmediate(accumulator.getMessages());\n setLongRunningToolIds(accumulator.getLongRunningToolIds());\n setToolConfirmations(accumulator.getToolConfirmations());\n setAuthRequests(accumulator.getAuthRequests());\n let lastTransferToAgent: string | undefined;\n\n // Google ADK replaces active runs, while React LangGraph queues sends.\n abortControllerRef.current?.abort();\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n\n try {\n const response = await openAbortableIterable(\n stream(newMessagesWithId, {\n ...config,\n abortSignal: abortController.signal,\n initialize: async () => {\n return await aui.threadListItem.initialize();\n },\n }),\n abortController.signal,\n );\n if (!response) return;\n\n for await (const event of abortableIterable(\n response,\n abortController.signal,\n )) {\n if (\n abortController.signal.aborted ||\n abortControllerRef.current !== abortController\n ) {\n break;\n }\n const updatedMessages = accumulator.processEvent(event);\n setMessagesImmediate(updatedMessages);\n setStateDelta({\n ...stateDeltaRef.current,\n ...accumulator.getStateDelta(),\n });\n setAgentInfo(accumulator.getAgentInfo());\n setLongRunningToolIds(accumulator.getLongRunningToolIds());\n setArtifactDelta({\n ...artifactDeltaRef.current,\n ...accumulator.getArtifactDelta(),\n });\n setToolConfirmations(accumulator.getToolConfirmations());\n setAuthRequests(accumulator.getAuthRequests());\n setEscalated(accumulator.isEscalated());\n {\n const newMeta = accumulator.getMessageMetadata();\n if (newMeta.size > 0) {\n setMessageMetadata(\n new Map([...messageMetadataRef.current, ...newMeta]),\n );\n }\n }\n\n const transfer = accumulator.getLastTransferToAgent();\n if (transfer && transfer !== lastTransferToAgent) {\n lastTransferToAgent = transfer;\n invokeAdkRuntimeCallback(\n \"onAgentTransfer\",\n onAgentTransfer,\n transfer,\n );\n }\n\n // Fire custom event callback for events with customMetadata\n if (event.customMetadata && onCustomEvent) {\n for (const [key, value] of Object.entries(event.customMetadata)) {\n invokeAdkRuntimeCallback(\n \"onCustomEvent\",\n onCustomEvent,\n key,\n value,\n );\n }\n }\n\n if (event.errorCode || event.errorMessage) {\n invokeAdkRuntimeCallback(\n \"onError\",\n onError,\n event.errorMessage ?? event.errorCode,\n );\n }\n }\n } catch (error) {\n if (\n !abortController.signal.aborted &&\n abortControllerRef.current === abortController &&\n !(error instanceof Error && error.name === \"AbortError\")\n ) {\n throw error;\n }\n } finally {\n if (abortControllerRef.current === abortController) {\n abortControllerRef.current = null;\n }\n }\n },\n [\n aui,\n setMessagesImmediate,\n setLongRunningToolIds,\n stream,\n onError,\n onCustomEvent,\n onAgentTransfer,\n ],\n );\n\n const cancel = useCallback(() => {\n if (abortControllerRef.current) {\n abortControllerRef.current.abort();\n }\n }, []);\n\n useReplaySafeEffect(() => cancel, []);\n\n return {\n messages,\n stateDelta,\n agentInfo,\n longRunningToolIds,\n artifactDelta,\n toolConfirmations,\n authRequests,\n escalated,\n messageMetadata,\n sendMessage,\n cancel,\n setMessages: setMessagesImmediate,\n replaceMessages,\n applySnapshot,\n };\n};\n\n/**\n * Transport sends every human and tool message of one `send` call as a single\n * ADK `Content`, and ADK parses that event's function responses before running\n * any tool, so the batch runs whole or not at all. The optimistic projection\n * has to sit on the same boundary, so a run of those messages becomes one\n * synthetic event whose parts come from the same per-message conversion.\n *\n * The transport drops `ai` messages from that `Content`, so one interleaved\n * between two replies does not split the batch on the wire and must not split\n * it here either. It still becomes its own event, placed after the merged one,\n * so the optimistic projection keeps the assistant turn.\n *\n * @internal — exported for unit tests.\n */\nexport const messagesToEvents = (messages: AdkMessage[]): AdkEvent[] => {\n // A reload sends no messages at all, and the empty user content the transport\n // puts on the wire for it is not part of the optimistic view: projecting one\n // would put an empty user bubble above every regenerated turn.\n if (messages.length === 0) return [];\n\n const events: AdkEvent[] = [];\n const run: AdkMessage[] = [];\n let runIndex = 0;\n\n for (const msg of messages) {\n if (msg.type === \"ai\") {\n events.push(messageToEvent(msg));\n } else {\n if (run.length === 0) runIndex = events.length;\n run.push(msg);\n }\n }\n\n const parts = run.flatMap((m) => messageToEvent(m).content?.parts ?? []);\n const human = run.find((m) => m.type === \"human\");\n\n // A batch that contributes no part still reaches the wire: the transport\n // sends an empty user `Content`, which a reload replays as an empty human\n // message. Emitting it here keeps the optimistic view equal to that replay.\n if (parts.length === 0) parts.push({ text: \"\" });\n\n const event: AdkEvent = { id: (human ?? run[0])?.id ?? generateId() };\n if (human || run.length === 0) event.author = \"user\";\n event.content = { role: \"user\", parts };\n events.splice(run.length > 0 ? runIndex : events.length, 0, event);\n\n return events;\n};\n\n/** @internal — exported for unit tests. */\nexport const messageToEvent = (msg: AdkMessage): AdkEvent => {\n if (msg.type === \"human\") {\n return {\n id: msg.id ?? generateId(),\n author: \"user\",\n content: { role: \"user\", parts: contentToParts(msg.content) },\n };\n }\n\n if (msg.type === \"tool\") {\n let response: unknown;\n try {\n response = JSON.parse(msg.content);\n } catch {\n response = msg.content;\n }\n return {\n id: msg.id ?? generateId(),\n content: {\n role: \"user\",\n parts: [\n {\n functionResponse: {\n name: msg.name,\n id: msg.tool_call_id,\n response: toAdkFunctionResponse(response, msg.status === \"error\"),\n },\n },\n ],\n },\n };\n }\n\n const result: AdkEvent = { id: msg.id ?? generateId() };\n if (msg.author != null) result.author = msg.author;\n result.content = {\n role: \"model\",\n parts: [\n ...contentToParts(msg.content),\n ...(msg.tool_calls?.map((tc) => ({\n functionCall: { name: tc.name, id: tc.id, args: { ...tc.args } },\n })) ?? []),\n ],\n };\n return result;\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react-google-adk",
3
- "version": "0.0.32",
3
+ "version": "0.0.33",
4
4
  "description": "Google ADK adapter for assistant-ui",
5
5
  "keywords": [
6
6
  "google-adk",
@@ -34,10 +34,10 @@
34
34
  ],
35
35
  "sideEffects": false,
36
36
  "dependencies": {
37
- "@assistant-ui/core": "^0.3.21",
38
- "@assistant-ui/store": "^0.3.15",
37
+ "@assistant-ui/core": "^0.3.22",
38
+ "@assistant-ui/store": "^0.3.16",
39
39
  "assistant-cloud": "*",
40
- "assistant-stream": "^0.3.45"
40
+ "assistant-stream": "^0.3.46"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@google/adk": ">=0.5.0",
@@ -54,6 +54,7 @@
54
54
  },
55
55
  "devDependencies": {
56
56
  "@assistant-ui/x-buildutils": "0.0.29",
57
+ "@assistant-ui/x-react18": "0.0.0",
57
58
  "@testing-library/dom": "^10.4.2",
58
59
  "@testing-library/react": "^16.3.3",
59
60
  "@types/react": "^19.3.0",
@@ -78,6 +79,8 @@
78
79
  "build": "aui-build",
79
80
  "test": "vitest run",
80
81
  "typecheck": "tsc --noEmit",
82
+ "test:peer-react18": "vitest run --config vitest.peer-react18.config.ts",
83
+ "test:types:peer-react18": "aui-react18-types",
81
84
  "test:watch": "vitest"
82
85
  }
83
86
  }
@@ -31,6 +31,8 @@ export const contentToParts = (
31
31
  return {
32
32
  codeExecutionResult: { output: part.output, outcome: part.outcome },
33
33
  };
34
+ case "activity":
35
+ return { text: part.message };
34
36
  default:
35
37
  return { text: "" };
36
38
  }
@@ -40,6 +40,19 @@ describe("convertAdkMessage - human messages", () => {
40
40
  });
41
41
  });
42
42
 
43
+ it("preserves activity messages as visible text", () => {
44
+ const msg: AdkMessage = {
45
+ id: "m1",
46
+ type: "human",
47
+ content: [{ type: "activity", message: "Working on it" }],
48
+ };
49
+
50
+ expect(convertAdkMessage(msg, {})).toMatchObject({
51
+ role: "user",
52
+ content: [{ type: "text", text: "Working on it" }],
53
+ });
54
+ });
55
+
43
56
  it("restores a file_url part as a file part stamped with sourceType url", () => {
44
57
  const msg: AdkMessage = {
45
58
  id: "m1",
@@ -89,6 +89,11 @@ const contentToParts = (
89
89
  name: "code_execution_result",
90
90
  data: { output: part.output, outcome: part.outcome },
91
91
  };
92
+ case "activity":
93
+ return {
94
+ type: "text",
95
+ text: typeof part.message === "string" ? part.message : "",
96
+ };
92
97
  default:
93
98
  return null;
94
99
  }
@@ -156,6 +156,12 @@ describe("getPendingCancellations", () => {
156
156
  });
157
157
 
158
158
  describe("getMessageContent", () => {
159
+ it("sends activity messages as text parts", () => {
160
+ expect(
161
+ contentToParts([{ type: "activity", message: "Working on it" }]),
162
+ ).toEqual([{ text: "Working on it" }]);
163
+ });
164
+
159
165
  it("serializes data URL images as inline data", () => {
160
166
  const content = getMessageContent(
161
167
  makeAppendMessage([
@@ -0,0 +1,15 @@
1
+ import { act, waitFor } from "@testing-library/react";
2
+
3
+ // React 18's act holds renders until its callback settles, so a task that waits on a render starts inside act and settles outside it, where React can commit what the task waits on.
4
+ export async function settleOutsideAct<T>(task: () => Promise<T>): Promise<T> {
5
+ let pending!: Promise<T>;
6
+ await act(async () => {
7
+ pending = task();
8
+ pending.catch(() => {});
9
+ });
10
+ await waitFor(() => pending.then(settled, settled));
11
+ await act(async () => {});
12
+ return pending;
13
+ }
14
+
15
+ const settled = () => {};
@@ -0,0 +1,114 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { act, StrictMode, useEffect } from "react";
4
+ import { afterAll, afterEach, expect, it, vi } from "vitest";
5
+
6
+ type Family = { current: unknown };
7
+ type RendererInternals = {
8
+ setRefreshHandler: (resolve: (type: unknown) => Family | undefined) => void;
9
+ scheduleRefresh: (
10
+ root: unknown,
11
+ update: { staleFamilies: Set<Family>; updatedFamilies: Set<Family> },
12
+ ) => void;
13
+ };
14
+
15
+ let renderer: RendererInternals | undefined;
16
+ const fiberRoots = new Set<unknown>();
17
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
18
+ vi.stubGlobal("__REACT_DEVTOOLS_GLOBAL_HOOK__", {
19
+ supportsFiber: true,
20
+ inject: (internals: RendererInternals) => {
21
+ renderer = internals;
22
+ return 1;
23
+ },
24
+ onScheduleFiberRoot: () => {},
25
+ onCommitFiberRoot: (_id: number, root: unknown) => fiberRoots.add(root),
26
+ onCommitFiberUnmount: () => {},
27
+ });
28
+ const { cleanup, render, waitFor } = await import("@testing-library/react");
29
+ const { useAdkMessages } = await import("./useAdkMessages");
30
+
31
+ afterEach(() => {
32
+ cleanup();
33
+ renderer?.setRefreshHandler(() => undefined);
34
+ fiberRoots.clear();
35
+ });
36
+ afterAll(() => vi.unstubAllGlobals());
37
+
38
+ const refresh = async (Before: unknown, After: unknown) => {
39
+ const family: Family = { current: After };
40
+ renderer!.setRefreshHandler((type) =>
41
+ type === Before || type === After ? family : undefined,
42
+ );
43
+ await act(async () => {
44
+ for (const root of fiberRoots) {
45
+ renderer!.scheduleRefresh(root, {
46
+ staleFamilies: new Set(),
47
+ updatedFamilies: new Set([family]),
48
+ });
49
+ }
50
+ });
51
+ await act(async () => {});
52
+ };
53
+
54
+ it("keeps a direct useAdkMessages stream through Fast Refresh and cancels on unmount", async () => {
55
+ let signal: AbortSignal | undefined;
56
+ let send: (() => void) | undefined;
57
+ let rendered: string | undefined;
58
+ const stream = vi.fn((_messages, config: { abortSignal: AbortSignal }) => {
59
+ signal = config.abortSignal;
60
+ return new Promise<never>(() => {});
61
+ });
62
+ const host = (name: string) => () => {
63
+ rendered = name;
64
+ const { sendMessage } = useAdkMessages({ stream: stream as never });
65
+ send = () => {
66
+ void sendMessage([{ id: "user", type: "human", content: "hello" }], {});
67
+ };
68
+ return null;
69
+ };
70
+ const Before = host("before");
71
+ const After = host("after");
72
+ const view = render(<Before />);
73
+
74
+ act(() => send!());
75
+ await waitFor(() => expect(stream).toHaveBeenCalledTimes(1));
76
+ expect(signal!.aborted).toBe(false);
77
+
78
+ await refresh(Before, After);
79
+ expect(rendered).toBe("after");
80
+ expect(signal!.aborted).toBe(false);
81
+ expect(stream).toHaveBeenCalledTimes(1);
82
+
83
+ view.unmount();
84
+ await act(async () => {});
85
+ expect(signal!.aborted).toBe(true);
86
+ });
87
+
88
+ it("keeps a stream started during StrictMode's effect replay", async () => {
89
+ let signal: AbortSignal | undefined;
90
+ let launched = false;
91
+ const stream = vi.fn((_messages, config: { abortSignal: AbortSignal }) => {
92
+ signal = config.abortSignal;
93
+ return new Promise<never>(() => {});
94
+ });
95
+ const Host = () => {
96
+ const { sendMessage } = useAdkMessages({ stream: stream as never });
97
+ useEffect(() => {
98
+ if (launched) return;
99
+ launched = true;
100
+ void sendMessage([{ id: "user", type: "human", content: "hello" }], {});
101
+ }, [sendMessage]);
102
+ return null;
103
+ };
104
+ const view = render(
105
+ <StrictMode>
106
+ <Host />
107
+ </StrictMode>,
108
+ );
109
+ await waitFor(() => expect(stream).toHaveBeenCalledTimes(1));
110
+ expect(signal!.aborted).toBe(false);
111
+ view.unmount();
112
+ await act(async () => {});
113
+ expect(signal!.aborted).toBe(true);
114
+ });
@@ -77,6 +77,40 @@ describe("optimistic tool outcomes", () => {
77
77
  });
78
78
 
79
79
  describe("ADK runtime callbacks", () => {
80
+ it("reports the same agent transfer again in a later run", async () => {
81
+ const onAgentTransfer = vi.fn();
82
+ const stream: AdkStreamCallback = async function* () {
83
+ yield {
84
+ id: "transfer",
85
+ actions: { transferToAgent: "researcher" },
86
+ };
87
+ yield {
88
+ id: "transfer-duplicate",
89
+ actions: { transferToAgent: "researcher" },
90
+ };
91
+ };
92
+ const { result } = renderHook(() =>
93
+ useAdkMessages({ stream, eventHandlers: { onAgentTransfer } }),
94
+ );
95
+
96
+ await act(async () => {
97
+ await result.current.sendMessage(
98
+ [{ id: "user-1", type: "human", content: "first" }],
99
+ {},
100
+ );
101
+ expect(onAgentTransfer).toHaveBeenCalledTimes(1);
102
+
103
+ await result.current.sendMessage(
104
+ [{ id: "user-2", type: "human", content: "second" }],
105
+ {},
106
+ );
107
+ });
108
+
109
+ expect(onAgentTransfer).toHaveBeenCalledTimes(2);
110
+ expect(onAgentTransfer).toHaveBeenNthCalledWith(1, "researcher");
111
+ expect(onAgentTransfer).toHaveBeenNthCalledWith(2, "researcher");
112
+ });
113
+
80
114
  it.each(["onAgentTransfer", "onCustomEvent", "onError"] as const)(
81
115
  "continues streaming when %s throws",
82
116
  async (callbackName) => {
@@ -305,6 +339,7 @@ describe("ADK stream lifecycle", () => {
305
339
  await started;
306
340
 
307
341
  unmount();
342
+ await act(async () => {});
308
343
 
309
344
  expect(runSignal?.aborted).toBe(true);
310
345
  await expect(sendPromise).resolves.toBeUndefined();
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  useState,
3
3
  useCallback,
4
- useEffect,
5
4
  useInsertionEffect,
6
5
  useRef,
7
6
  useMemo,
8
7
  } from "react";
9
8
  import { generateId } from "@assistant-ui/core";
9
+ import { useReplaySafeEffect } from "@assistant-ui/store/internal";
10
10
  import { useAui } from "@assistant-ui/store";
11
11
  import {
12
12
  abortableIterable,
@@ -71,7 +71,6 @@ export const useAdkMessages = ({
71
71
  const [messageMetadata, setMessageMetadata] = useState<
72
72
  Map<string, AdkMessageMetadata>
73
73
  >(new Map());
74
- const lastTransferToAgentRef = useRef<string | undefined>(undefined);
75
74
  // setMessagesImmediate and setLongRunningToolIds are the only writers of their state and publish these refs with it, so neither ref trails a commit.
76
75
  const messagesRef = useRef(messages);
77
76
  const longRunningToolIdsRef = useRef(longRunningToolIds);
@@ -172,6 +171,7 @@ export const useAdkMessages = ({
172
171
  setLongRunningToolIds(accumulator.getLongRunningToolIds());
173
172
  setToolConfirmations(accumulator.getToolConfirmations());
174
173
  setAuthRequests(accumulator.getAuthRequests());
174
+ let lastTransferToAgent: string | undefined;
175
175
 
176
176
  // Google ADK replaces active runs, while React LangGraph queues sends.
177
177
  abortControllerRef.current?.abort();
@@ -226,8 +226,8 @@ export const useAdkMessages = ({
226
226
  }
227
227
 
228
228
  const transfer = accumulator.getLastTransferToAgent();
229
- if (transfer && transfer !== lastTransferToAgentRef.current) {
230
- lastTransferToAgentRef.current = transfer;
229
+ if (transfer && transfer !== lastTransferToAgent) {
230
+ lastTransferToAgent = transfer;
231
231
  invokeAdkRuntimeCallback(
232
232
  "onAgentTransfer",
233
233
  onAgentTransfer,
@@ -286,7 +286,7 @@ export const useAdkMessages = ({
286
286
  }
287
287
  }, []);
288
288
 
289
- useEffect(() => cancel, [cancel]);
289
+ useReplaySafeEffect(() => cancel, []);
290
290
 
291
291
  return {
292
292
  messages,
@@ -11,6 +11,7 @@ import type {
11
11
  import { useAui } from "@assistant-ui/store";
12
12
  import { useAdkRuntime } from "./useAdkRuntime";
13
13
  import type { AdkMessage, AdkThreadSnapshot } from "./types";
14
+ import { settleOutsideAct } from "./tests/settleOutsideAct";
14
15
 
15
16
  const deferred = <T,>() => {
16
17
  let resolve!: (value: T) => void;
@@ -97,9 +98,9 @@ const renderAdk = async (
97
98
  });
98
99
  await waitFor(() => expect(capture.runtime).not.toBeNull());
99
100
 
100
- await act(async () => {
101
- await capture.runtime!.threads.switchToThread("adk-1");
102
- });
101
+ await settleOutsideAct(() =>
102
+ capture.runtime!.threads.switchToThread("adk-1"),
103
+ );
103
104
 
104
105
  return { capture, streamMock, unmount };
105
106
  };
@@ -250,9 +251,9 @@ describe("useAdkRuntime refetch", () => {
250
251
  await act(async () => {
251
252
  render(<Inner />);
252
253
  });
253
- await act(async () => {
254
- await capture.runtime!.threads.switchToThread("adk-1");
255
- });
254
+ await settleOutsideAct(() =>
255
+ capture.runtime!.threads.switchToThread("adk-1"),
256
+ );
256
257
  await waitFor(() => expect(load).toHaveBeenCalledTimes(1));
257
258
 
258
259
  let settled = false;
@@ -10,6 +10,7 @@ import type {
10
10
  } from "@assistant-ui/core";
11
11
  import { useAdkRuntime } from "./useAdkRuntime";
12
12
  import type { AdkEvent } from "./types";
13
+ import { settleOutsideAct } from "./tests/settleOutsideAct";
13
14
 
14
15
  const makeThreadListAdapter = (): RemoteThreadListAdapter => ({
15
16
  list: vi.fn(async () => ({
@@ -97,9 +98,9 @@ describe("useAdkRuntime replacement runs", () => {
97
98
  render(<Inner />);
98
99
  });
99
100
  await waitFor(() => expect(capture.runtime).not.toBeNull());
100
- await act(async () => {
101
- await capture.runtime!.threads.switchToThread("adk-1");
102
- });
101
+ await settleOutsideAct(() =>
102
+ capture.runtime!.threads.switchToThread("adk-1"),
103
+ );
103
104
 
104
105
  act(() => {
105
106
  capture.runtime!.thread.append({
@@ -14,6 +14,7 @@ import type {
14
14
  } from "@assistant-ui/core";
15
15
  import { useAdkRuntime } from "./useAdkRuntime";
16
16
  import type { AdkEvent } from "./types";
17
+ import { settleOutsideAct } from "./tests/settleOutsideAct";
17
18
 
18
19
  const makeThreadListAdapter = (): RemoteThreadListAdapter => ({
19
20
  list: vi.fn(async () => ({
@@ -153,9 +154,9 @@ const renderStreamingAdk = async (first: AdkEvent, second: AdkEvent) => {
153
154
  render(<Inner />);
154
155
  });
155
156
  await waitFor(() => expect(capture.runtime).not.toBeNull());
156
- await act(async () => {
157
- await capture.runtime!.threads.switchToThread("adk-1");
158
- });
157
+ await settleOutsideAct(() =>
158
+ capture.runtime!.threads.switchToThread("adk-1"),
159
+ );
159
160
  await act(async () => {
160
161
  await new Promise((r) => setTimeout(r, 0));
161
162
  });