@assistant-ui/react 0.15.26 → 0.15.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/mcp-apps/app-frame.js +2 -2
  4. package/dist/mcp-apps/app-frame.js.map +1 -1
  5. package/dist/mcp-apps/bridge.d.ts.map +1 -1
  6. package/dist/mcp-apps/bridge.js +28 -3
  7. package/dist/mcp-apps/bridge.js.map +1 -1
  8. package/dist/mcp-apps/index.d.ts +1 -1
  9. package/dist/mcp-apps/index.d.ts.map +1 -1
  10. package/dist/mcp-apps/types.d.ts +18 -1
  11. package/dist/mcp-apps/types.d.ts.map +1 -1
  12. package/dist/mcp-apps/types.js.map +1 -1
  13. package/dist/primitives/composer/ComposerAttachmentDropzone.d.ts.map +1 -1
  14. package/dist/primitives/composer/ComposerAttachmentDropzone.js +14 -12
  15. package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
  16. package/dist/unstable/useComposerInput.d.ts +2 -0
  17. package/dist/unstable/useComposerInput.d.ts.map +1 -1
  18. package/dist/unstable/useComposerInput.js +2 -0
  19. package/dist/unstable/useComposerInput.js.map +1 -1
  20. package/package.json +4 -4
  21. package/src/index.ts +1 -0
  22. package/src/mcp-apps/McpAppRenderer.test.tsx +7 -2
  23. package/src/mcp-apps/app-frame.test.tsx +64 -6
  24. package/src/mcp-apps/app-frame.tsx +2 -2
  25. package/src/mcp-apps/bridge.test.ts +154 -0
  26. package/src/mcp-apps/bridge.ts +51 -9
  27. package/src/mcp-apps/index.ts +1 -0
  28. package/src/mcp-apps/types.ts +15 -1
  29. package/src/primitives/composer/ComposerAttachmentDropzone.test.tsx +32 -1
  30. package/src/primitives/composer/ComposerAttachmentDropzone.tsx +14 -12
  31. package/src/primitives/composer/ComposerDisabledThread.test.tsx +121 -0
  32. package/src/unstable/useComposerInput.ts +2 -0
@@ -1 +1 @@
1
- {"version":3,"file":"ComposerAttachmentDropzone.js","names":["forwardRef","useCallback","useEffect","useState","ReactElement","isValidElement","composeEventHandlers","Slot","React","useAui","renderSlot","ComposerPrimitiveAttachmentDropzone","Element","HTMLDivElement","Props","HTMLAttributes","asChild","render","disabled","children","rest","ref","isDragging","setIsDragging","aui","handleDragEnterCapture","e","DragEvent","dataTransfer","types","includes","preventDefault","thread","getState","capabilities","attachments","dropEffect","handleDragOverCapture","handleDragLeaveCapture","next","relatedTarget","Node","currentTarget","contains","handleDrop","files","Array","from","length","Promise","all","map","file","composer","addAttachment","mergedProps","onDragEnterCapture","onDragOverCapture","onDragLeaveCapture","onDropCapture","Comp","Root","displayName"],"sources":["../../../src/primitives/composer/ComposerAttachmentDropzone.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n forwardRef,\n useCallback,\n useEffect,\n useState,\n type ReactElement,\n isValidElement,\n} from \"react\";\n\nimport { composeEventHandlers } from \"radix-ui/internal\";\nimport { Slot } from \"radix-ui\";\nimport type React from \"react\";\nimport { useAui } from \"@assistant-ui/store\";\nimport { renderSlot } from \"../../utils/Primitive\";\n\nexport namespace ComposerPrimitiveAttachmentDropzone {\n export type Element = HTMLDivElement;\n export type Props = React.HTMLAttributes<HTMLDivElement> & {\n asChild?: boolean | undefined;\n render?: ReactElement | undefined;\n disabled?: boolean | undefined;\n };\n}\n\nexport const ComposerPrimitiveAttachmentDropzone = forwardRef<\n HTMLDivElement,\n ComposerPrimitiveAttachmentDropzone.Props\n>(({ disabled, asChild = false, render, children, ...rest }, ref) => {\n const [isDragging, setIsDragging] = useState(false);\n const aui = useAui();\n\n useEffect(() => {\n if (!disabled) return;\n // Disabled handlers cannot clear the latch, so reset it when the enabled period ends.\n // eslint-disable-next-line react-hooks/set-state-in-effect\n setIsDragging(false);\n }, [disabled]);\n\n // An unprevented file drop navigates the tab to the file, so file drags are\n // claimed via preventDefault even when the runtime does not support attachments.\n const handleDragEnterCapture = useCallback(\n (e: React.DragEvent) => {\n if (!e.dataTransfer.types.includes(\"Files\")) return;\n e.preventDefault();\n if (disabled || !aui.thread.getState().capabilities.attachments) {\n e.dataTransfer.dropEffect = \"none\";\n return;\n }\n setIsDragging(true);\n },\n [disabled, aui],\n );\n\n const handleDragOverCapture = useCallback(\n (e: React.DragEvent) => {\n if (!e.dataTransfer.types.includes(\"Files\")) return;\n e.preventDefault();\n if (disabled || !aui.thread.getState().capabilities.attachments) {\n e.dataTransfer.dropEffect = \"none\";\n return;\n }\n if (!isDragging) setIsDragging(true);\n },\n [disabled, isDragging, aui],\n );\n\n const handleDragLeaveCapture = useCallback(\n (e: React.DragEvent) => {\n if (disabled) return;\n e.preventDefault();\n const next = e.relatedTarget as Node | null;\n if (next && e.currentTarget.contains(next)) {\n return;\n }\n setIsDragging(false);\n },\n [disabled],\n );\n\n const handleDrop = useCallback(\n async (e: React.DragEvent) => {\n setIsDragging(false);\n if (!e.dataTransfer.types.includes(\"Files\")) return;\n e.preventDefault();\n if (disabled) return;\n const files = Array.from(e.dataTransfer.files);\n if (!aui.thread.getState().capabilities.attachments || files.length === 0)\n return;\n await Promise.all(\n files.map(async (file) => {\n try {\n await aui.composer.addAttachment(file);\n } catch {\n // The composer runtime emits composer.attachmentAddError before rejecting;\n // the readonly and empty-thread stubs only throw.\n }\n }),\n );\n },\n [disabled, aui],\n );\n\n const mergedProps = {\n ...(isDragging && !disabled ? { \"data-dragging\": \"true\" } : null),\n ...rest,\n onDragEnterCapture: composeEventHandlers(\n rest.onDragEnterCapture,\n handleDragEnterCapture,\n ),\n onDragOverCapture: composeEventHandlers(\n rest.onDragOverCapture,\n handleDragOverCapture,\n ),\n onDragLeaveCapture: composeEventHandlers(\n rest.onDragLeaveCapture,\n handleDragLeaveCapture,\n ),\n onDropCapture: composeEventHandlers(rest.onDropCapture, handleDrop),\n ref,\n };\n\n if (render && isValidElement(render)) {\n return renderSlot(render, children, mergedProps);\n }\n\n const Comp = asChild ? Slot.Root : \"div\";\n return <Comp {...mergedProps}>{children}</Comp>;\n});\n\nComposerPrimitiveAttachmentDropzone.displayName =\n \"ComposerPrimitive.AttachmentDropzone\";\n"],"mappings":";;;;;;;;AA0BA,MAAaW,sCAAsCX,YAGhD,EAAEkB,UAAUF,UAAU,OAAOC,QAAQE,UAAU,GAAGC,QAAQC,QAAQ;CACnE,MAAM,CAACC,YAAYC,iBAAiBpB,SAAS,KAAK;CAClD,MAAMqB,MAAMf,OAAO;CAEnBP,gBAAgB;EACd,IAAI,CAACgB,UAAU;EAGfK,cAAc,KAAK;CACrB,GAAG,CAACL,QAAQ,CAAC;CAIb,MAAMO,yBAAyBxB,aAC5ByB,MAAuB;EACtB,IAAI,CAACA,EAAEE,aAAaC,MAAMC,SAAS,OAAO,GAAG;EAC7CJ,EAAEK,eAAe;EACjB,IAAIb,YAAY,CAACM,IAAIQ,OAAOC,SAAS,CAAC,CAACC,aAAaC,aAAa;GAC/DT,EAAEE,aAAaQ,aAAa;GAC5B;EACF;EACAb,cAAc,IAAI;CACpB,GACA,CAACL,UAAUM,GAAG,CAChB;CAEA,MAAMa,wBAAwBpC,aAC3ByB,QAAuB;EACtB,IAAI,CAACA,IAAEE,aAAaC,MAAMC,SAAS,OAAO,GAAG;EAC7CJ,IAAEK,eAAe;EACjB,IAAIb,YAAY,CAACM,IAAIQ,OAAOC,SAAS,CAAC,CAACC,aAAaC,aAAa;GAC/DT,IAAEE,aAAaQ,aAAa;GAC5B;EACF;EACA,IAAI,CAACd,YAAYC,cAAc,IAAI;CACrC,GACA;EAACL;EAAUI;EAAYE;CAAG,CAC5B;CAEA,MAAMc,yBAAyBrC,aAC5ByB,QAAuB;EACtB,IAAIR,UAAU;EACdQ,IAAEK,eAAe;EACjB,MAAMQ,OAAOb,IAAEc;EACf,IAAID,QAAQb,IAAEgB,cAAcC,SAASJ,IAAI,GACvC;EAEFhB,cAAc,KAAK;CACrB,GACA,CAACL,QAAQ,CACX;CAEA,MAAM0B,aAAa3C,YACjB,OAAOyB,QAAuB;EAC5BH,cAAc,KAAK;EACnB,IAAI,CAACG,IAAEE,aAAaC,MAAMC,SAAS,OAAO,GAAG;EAC7CJ,IAAEK,eAAe;EACjB,IAAIb,UAAU;EACd,MAAM2B,QAAQC,MAAMC,KAAKrB,IAAEE,aAAaiB,KAAK;EAC7C,IAAI,CAACrB,IAAIQ,OAAOC,SAAS,CAAC,CAACC,aAAaC,eAAeU,MAAMG,WAAW,GACtE;EACF,MAAMC,QAAQC,IACZL,MAAMM,IAAI,OAAOC,SAAS;GACxB,IAAI;IACF,MAAM5B,IAAI6B,SAASC,cAAcF,IAAI;GACvC,QAAQ,CAEN;EAEJ,CAAC,CACH;CACF,GACA,CAAClC,UAAUM,GAAG,CAChB;CAEA,MAAM+B,cAAc;EAClB,GAAIjC,cAAc,CAACJ,WAAW,EAAE,iBAAiB,OAAO,IAAI;EAC5D,GAAGE;EACHoC,oBAAoBlD,qBAClBc,KAAKoC,oBACL/B,sBACF;EACAgC,mBAAmBnD,qBACjBc,KAAKqC,mBACLpB,qBACF;EACAqB,oBAAoBpD,qBAClBc,KAAKsC,oBACLpB,sBACF;EACAqB,eAAerD,qBAAqBc,KAAKuC,eAAef,UAAU;EAClEvB;CACF;CAEA,IAAIJ,UAAUZ,eAAeY,MAAM,GACjC,OAAOP,WAAWO,QAAQE,UAAUoC,WAAW;CAGjD,MAAMK,OAAO5C,UAAUT,KAAKsD,OAAO;CACnC,OAAO,oBAAC,MAAD;EAAM,GAAIN;EAAcpC;CAAe,CAAA;AAChD,CAAC;AAEDR,oCAAoCmD,cAClC"}
1
+ {"version":3,"file":"ComposerAttachmentDropzone.js","names":["forwardRef","useCallback","useEffect","useState","ReactElement","isValidElement","composeEventHandlers","Slot","React","useAui","useAuiState","renderSlot","ComposerPrimitiveAttachmentDropzone","Element","HTMLDivElement","Props","HTMLAttributes","asChild","render","disabled","children","rest","ref","isDragging","setIsDragging","aui","disabledByThread","s","thread","isDisabled","handleDragEnterCapture","e","DragEvent","dataTransfer","types","includes","preventDefault","getState","capabilities","attachments","dropEffect","handleDragOverCapture","handleDragLeaveCapture","next","relatedTarget","Node","currentTarget","contains","handleDrop","files","Array","from","length","Promise","all","map","file","composer","addAttachment","mergedProps","onDragEnterCapture","onDragOverCapture","onDragLeaveCapture","onDropCapture","Comp","Root","displayName"],"sources":["../../../src/primitives/composer/ComposerAttachmentDropzone.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n forwardRef,\n useCallback,\n useEffect,\n useState,\n type ReactElement,\n isValidElement,\n} from \"react\";\n\nimport { composeEventHandlers } from \"radix-ui/internal\";\nimport { Slot } from \"radix-ui\";\nimport type React from \"react\";\nimport { useAui, useAuiState } from \"@assistant-ui/store\";\nimport { renderSlot } from \"../../utils/Primitive\";\n\nexport namespace ComposerPrimitiveAttachmentDropzone {\n export type Element = HTMLDivElement;\n export type Props = React.HTMLAttributes<HTMLDivElement> & {\n asChild?: boolean | undefined;\n render?: ReactElement | undefined;\n disabled?: boolean | undefined;\n };\n}\n\nexport const ComposerPrimitiveAttachmentDropzone = forwardRef<\n HTMLDivElement,\n ComposerPrimitiveAttachmentDropzone.Props\n>(({ disabled, asChild = false, render, children, ...rest }, ref) => {\n const [isDragging, setIsDragging] = useState(false);\n const aui = useAui();\n const disabledByThread = useAuiState((s) => s.thread.isDisabled);\n const isDisabled = disabled || disabledByThread;\n\n useEffect(() => {\n if (!isDisabled) return;\n // Disabled handlers cannot clear the latch, so reset it when the enabled period ends.\n // eslint-disable-next-line react-hooks/set-state-in-effect\n setIsDragging(false);\n }, [isDisabled]);\n\n // An unprevented file drop navigates the tab to the file, so file drags are\n // claimed via preventDefault even when the runtime does not support attachments.\n const handleDragEnterCapture = useCallback(\n (e: React.DragEvent) => {\n if (!e.dataTransfer.types.includes(\"Files\")) return;\n e.preventDefault();\n if (isDisabled || !aui.thread.getState().capabilities.attachments) {\n e.dataTransfer.dropEffect = \"none\";\n return;\n }\n setIsDragging(true);\n },\n [isDisabled, aui],\n );\n\n const handleDragOverCapture = useCallback(\n (e: React.DragEvent) => {\n if (!e.dataTransfer.types.includes(\"Files\")) return;\n e.preventDefault();\n if (isDisabled || !aui.thread.getState().capabilities.attachments) {\n e.dataTransfer.dropEffect = \"none\";\n return;\n }\n if (!isDragging) setIsDragging(true);\n },\n [isDisabled, isDragging, aui],\n );\n\n const handleDragLeaveCapture = useCallback(\n (e: React.DragEvent) => {\n if (isDisabled) return;\n e.preventDefault();\n const next = e.relatedTarget as Node | null;\n if (next && e.currentTarget.contains(next)) {\n return;\n }\n setIsDragging(false);\n },\n [isDisabled],\n );\n\n const handleDrop = useCallback(\n async (e: React.DragEvent) => {\n setIsDragging(false);\n if (!e.dataTransfer.types.includes(\"Files\")) return;\n e.preventDefault();\n if (isDisabled) return;\n const files = Array.from(e.dataTransfer.files);\n if (!aui.thread.getState().capabilities.attachments || files.length === 0)\n return;\n await Promise.all(\n files.map(async (file) => {\n try {\n await aui.composer.addAttachment(file);\n } catch {\n // The composer runtime emits composer.attachmentAddError before rejecting;\n // the readonly and empty-thread stubs only throw.\n }\n }),\n );\n },\n [isDisabled, aui],\n );\n\n const mergedProps = {\n ...(isDragging && !isDisabled ? { \"data-dragging\": \"true\" } : null),\n ...rest,\n onDragEnterCapture: composeEventHandlers(\n rest.onDragEnterCapture,\n handleDragEnterCapture,\n ),\n onDragOverCapture: composeEventHandlers(\n rest.onDragOverCapture,\n handleDragOverCapture,\n ),\n onDragLeaveCapture: composeEventHandlers(\n rest.onDragLeaveCapture,\n handleDragLeaveCapture,\n ),\n onDropCapture: composeEventHandlers(rest.onDropCapture, handleDrop),\n ref,\n };\n\n if (render && isValidElement(render)) {\n return renderSlot(render, children, mergedProps);\n }\n\n const Comp = asChild ? Slot.Root : \"div\";\n return <Comp {...mergedProps}>{children}</Comp>;\n});\n\nComposerPrimitiveAttachmentDropzone.displayName =\n \"ComposerPrimitive.AttachmentDropzone\";\n"],"mappings":";;;;;;;;AA0BA,MAAaY,sCAAsCZ,YAGhD,EAAEmB,UAAUF,UAAU,OAAOC,QAAQE,UAAU,GAAGC,QAAQC,QAAQ;CACnE,MAAM,CAACC,YAAYC,iBAAiBrB,SAAS,KAAK;CAClD,MAAMsB,MAAMhB,OAAO;CACnB,MAAMiB,mBAAmBhB,aAAaiB,MAAMA,EAAEC,OAAOC,UAAU;CAC/D,MAAMA,aAAaV,YAAYO;CAE/BxB,gBAAgB;EACd,IAAI,CAAC2B,YAAY;EAGjBL,cAAc,KAAK;CACrB,GAAG,CAACK,UAAU,CAAC;CAIf,MAAMC,yBAAyB7B,aAC5B8B,MAAuB;EACtB,IAAI,CAACA,EAAEE,aAAaC,MAAMC,SAAS,OAAO,GAAG;EAC7CJ,EAAEK,eAAe;EACjB,IAAIP,cAAc,CAACJ,IAAIG,OAAOS,SAAS,CAAC,CAACC,aAAaC,aAAa;GACjER,EAAEE,aAAaO,aAAa;GAC5B;EACF;EACAhB,cAAc,IAAI;CACpB,GACA,CAACK,YAAYJ,GAAG,CAClB;CAEA,MAAMgB,wBAAwBxC,aAC3B8B,QAAuB;EACtB,IAAI,CAACA,IAAEE,aAAaC,MAAMC,SAAS,OAAO,GAAG;EAC7CJ,IAAEK,eAAe;EACjB,IAAIP,cAAc,CAACJ,IAAIG,OAAOS,SAAS,CAAC,CAACC,aAAaC,aAAa;GACjER,IAAEE,aAAaO,aAAa;GAC5B;EACF;EACA,IAAI,CAACjB,YAAYC,cAAc,IAAI;CACrC,GACA;EAACK;EAAYN;EAAYE;CAAG,CAC9B;CAEA,MAAMiB,yBAAyBzC,aAC5B8B,QAAuB;EACtB,IAAIF,YAAY;EAChBE,IAAEK,eAAe;EACjB,MAAMO,OAAOZ,IAAEa;EACf,IAAID,QAAQZ,IAAEe,cAAcC,SAASJ,IAAI,GACvC;EAEFnB,cAAc,KAAK;CACrB,GACA,CAACK,UAAU,CACb;CAEA,MAAMmB,aAAa/C,YACjB,OAAO8B,QAAuB;EAC5BP,cAAc,KAAK;EACnB,IAAI,CAACO,IAAEE,aAAaC,MAAMC,SAAS,OAAO,GAAG;EAC7CJ,IAAEK,eAAe;EACjB,IAAIP,YAAY;EAChB,MAAMoB,QAAQC,MAAMC,KAAKpB,IAAEE,aAAagB,KAAK;EAC7C,IAAI,CAACxB,IAAIG,OAAOS,SAAS,CAAC,CAACC,aAAaC,eAAeU,MAAMG,WAAW,GACtE;EACF,MAAMC,QAAQC,IACZL,MAAMM,IAAI,OAAOC,SAAS;GACxB,IAAI;IACF,MAAM/B,IAAIgC,SAASC,cAAcF,IAAI;GACvC,QAAQ,CAEN;EAEJ,CAAC,CACH;CACF,GACA,CAAC3B,YAAYJ,GAAG,CAClB;CAEA,MAAMkC,cAAc;EAClB,GAAIpC,cAAc,CAACM,aAAa,EAAE,iBAAiB,OAAO,IAAI;EAC9D,GAAGR;EACHuC,oBAAoBtD,qBAClBe,KAAKuC,oBACL9B,sBACF;EACA+B,mBAAmBvD,qBACjBe,KAAKwC,mBACLpB,qBACF;EACAqB,oBAAoBxD,qBAClBe,KAAKyC,oBACLpB,sBACF;EACAqB,eAAezD,qBAAqBe,KAAK0C,eAAef,UAAU;EAClE1B;CACF;CAEA,IAAIJ,UAAUb,eAAea,MAAM,GACjC,OAAOP,WAAWO,QAAQE,UAAUuC,WAAW;CAGjD,MAAMK,OAAO/C,UAAUV,KAAK0D,OAAO;CACnC,OAAO,oBAAC,MAAD;EAAM,GAAIN;EAAcvC;CAAe,CAAA;AAChD,CAAC;AAEDR,oCAAoCsD,cAClC"}
@@ -54,6 +54,8 @@ export type Unstable_ComposerInput = {
54
54
  * disabled={isDisabled}
55
55
  * onChange={(e) => setText(e.target.value)}
56
56
  * onKeyDown={(e) => {
57
+ * if (e.nativeEvent.isComposing || e.keyCode === 229) return;
58
+ *
57
59
  * if (e.key === "Enter" && !e.shiftKey && canSend) {
58
60
  * e.preventDefault();
59
61
  * send();
@@ -1 +1 @@
1
- {"version":3,"file":"useComposerInput.d.ts","sourceRoot":"","sources":["../../src/unstable/useComposerInput.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EACL,KAAK,uBAAuB,EAI7B,wDAAqD;AAEtD,+GAA+G;AAC/G,MAAM,MAAM,gCAAgC,GAAG;IAC7C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAChC,CAAC;AAEF,+GAA+G;AAC/G,MAAM,MAAM,sBAAsB,GAAG;IACnC,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;;OAGG;IACH,IAAI,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC1C;;;OAGG;IACH,UAAU,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,CAAC,EAAE,gCAAgC,GACzC,sBAAsB,CA0BxB;AAED,+GAA+G;AAC/G,MAAM,MAAM,gCAAgC,GAAG,uBAAuB,CAAC;AAEvE;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mCAAmC,IAAI,gCAAgC,CAEtF"}
1
+ {"version":3,"file":"useComposerInput.d.ts","sourceRoot":"","sources":["../../src/unstable/useComposerInput.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EACL,KAAK,uBAAuB,EAI7B,wDAAqD;AAEtD,+GAA+G;AAC/G,MAAM,MAAM,gCAAgC,GAAG;IAC7C;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAChC,CAAC;AAEF,+GAA+G;AAC/G,MAAM,MAAM,sBAAsB,GAAG;IACnC,uEAAuE;IACvE,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B;;;OAGG;IACH,IAAI,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC1C;;;OAGG;IACH,UAAU,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,CAAC,EAAE,gCAAgC,GACzC,sBAAsB,CA0BxB;AAED,+GAA+G;AAC/G,MAAM,MAAM,gCAAgC,GAAG,uBAAuB,CAAC;AAEvE;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mCAAmC,IAAI,gCAAgC,CAEtF"}
@@ -23,6 +23,8 @@ import { flushTapSync } from "@assistant-ui/tap";
23
23
  * disabled={isDisabled}
24
24
  * onChange={(e) => setText(e.target.value)}
25
25
  * onKeyDown={(e) => {
26
+ * if (e.nativeEvent.isComposing || e.keyCode === 229) return;
27
+ *
26
28
  * if (e.key === "Enter" && !e.shiftKey && canSend) {
27
29
  * e.preventDefault();
28
30
  * send();
@@ -1 +1 @@
1
- {"version":3,"file":"useComposerInput.js","names":["useCallback","useAui","flushTapSync","useComposerSend","ComposerSendOptions","TriggerPopoverAriaProps","useComposerInputDisabled","useComposerInputValue","useTriggerPopoverAriaProps","Unstable_UseComposerInputOptions","disabled","Unstable_ComposerInput","value","setText","text","send","options","isDisabled","canSend","unstable_useComposerInput","aui","composer","getState","isEditing","rawSend","sendDisabled","sendOptions","Unstable_TriggerPopoverAriaProps","unstable_useTriggerPopoverAriaProps"],"sources":["../../src/unstable/useComposerInput.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback } from \"react\";\nimport { useAui } from \"@assistant-ui/store\";\nimport { flushTapSync } from \"@assistant-ui/tap\";\nimport { useComposerSend } from \"@assistant-ui/core/react\";\nimport type { ComposerSendOptions } from \"@assistant-ui/core/store\";\nimport {\n type TriggerPopoverAriaProps,\n useComposerInputDisabled,\n useComposerInputValue,\n useTriggerPopoverAriaProps,\n} from \"../primitives/composer/useComposerInputState\";\n\n/** @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release. */\nexport type Unstable_UseComposerInputOptions = {\n /**\n * Disables the input in addition to the composer's own disabled sources\n * (thread disabled, active dictation). When disabled, `isDisabled` is `true`\n * and `canSend` is `false`.\n */\n disabled?: boolean | undefined;\n};\n\n/** @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release. */\nexport type Unstable_ComposerInput = {\n /** Current composer text, or `\"\"` when the composer is not editing. */\n value: string;\n /**\n * Writes `text` into the composer, mirroring `ComposerPrimitive.Input`:\n * a no-op unless the composer is editing; the controlled value stays in\n * sync within the same tick.\n */\n setText(text: string): void;\n /**\n * Sends the current message when `canSend` is `true`; otherwise a no-op.\n * Accepts the same options as the composer send action.\n */\n send(options?: ComposerSendOptions): void;\n /**\n * Whether the input is disabled, combining the `disabled` option with the\n * composer's own disabled sources (thread disabled, active dictation).\n */\n isDisabled: boolean;\n /**\n * Whether a send is currently available. Matches `ComposerPrimitive.Send`\n * gating (non-empty editing composer, not running without queue support) and\n * is additionally `false` while `isDisabled`.\n */\n canSend: boolean;\n};\n\n/**\n * Headless bridge to the composer's text value and send action, for building a\n * custom composer input without `ComposerPrimitive.Input`. It is a thin bridge,\n * not a second input: it does not own keyboard behavior, autosize, IME or\n * contentEditable sync, paste/drop attachments, focus management, or rich-text\n * state. Spread `unstable_useTriggerPopoverAriaProps()` onto your element for\n * trigger-popover combobox semantics.\n *\n * @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release.\n *\n * @example\n * ```tsx\n * const { value, setText, send, isDisabled, canSend } = unstable_useComposerInput();\n * <textarea\n * value={value}\n * disabled={isDisabled}\n * onChange={(e) => setText(e.target.value)}\n * onKeyDown={(e) => {\n * if (e.key === \"Enter\" && !e.shiftKey && canSend) {\n * e.preventDefault();\n * send();\n * }\n * }}\n * />\n * ```\n */\nexport function unstable_useComposerInput(\n options?: Unstable_UseComposerInputOptions,\n): Unstable_ComposerInput {\n const aui = useAui();\n const value = useComposerInputValue();\n const isDisabled = useComposerInputDisabled(options?.disabled);\n\n const setText = useCallback(\n (text: string) => {\n if (!aui.composer.getState().isEditing) return;\n flushTapSync(() => {\n aui.composer.setText(text);\n });\n },\n [aui],\n );\n\n const { send: rawSend, disabled: sendDisabled } = useComposerSend();\n const canSend = !sendDisabled && !isDisabled;\n const send = useCallback(\n (sendOptions?: ComposerSendOptions) => {\n if (!canSend) return;\n rawSend(sendOptions);\n },\n [canSend, rawSend],\n );\n\n return { value, setText, send, isDisabled, canSend };\n}\n\n/** @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release. */\nexport type Unstable_TriggerPopoverAriaProps = TriggerPopoverAriaProps;\n\n/**\n * ARIA combobox attributes for the focused element (typically the composer\n * input) describing the open trigger popover, per the WAI-ARIA editable\n * combobox pattern. Returns an empty object outside a `TriggerPopoverRoot` or\n * when no popover is open. Spread these last so they take precedence over any\n * matching ARIA props you set yourself, mirroring `ComposerPrimitive.Input`.\n *\n * @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release.\n *\n * @example\n * ```tsx\n * const aria = unstable_useTriggerPopoverAriaProps();\n * <textarea {...aria} />\n * ```\n */\nexport function unstable_useTriggerPopoverAriaProps(): Unstable_TriggerPopoverAriaProps {\n return useTriggerPopoverAriaProps();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8EA,SAAgBmB,0BACdH,SACwB;CACxB,MAAMI,MAAMnB,OAAO;CACnB,MAAMW,QAAQL,sBAAsB;CACpC,MAAMU,aAAaX,yBAAyBU,SAASN,QAAQ;CAE7D,MAAMG,UAAUb,aACbc,SAAiB;EAChB,IAAI,CAACM,IAAIC,SAASC,SAAS,CAAC,CAACC,WAAW;EACxCrB,mBAAmB;GACjBkB,IAAIC,SAASR,QAAQC,IAAI;EAC3B,CAAC;CACH,GACA,CAACM,GAAG,CACN;CAEA,MAAM,EAAEL,MAAMS,SAASd,UAAUe,iBAAiBtB,gBAAgB;CAClE,MAAMe,UAAU,CAACO,gBAAgB,CAACR;CASlC,OAAO;EAAEL;EAAOC;EAASE,MARZf,aACV0B,gBAAsC;GACrC,IAAI,CAACR,SAAS;GACdM,QAAQE,WAAW;EACrB,GACA,CAACR,SAASM,OAAO,CAGMT;EAAME;EAAYC;CAAQ;AACrD;;;;;;;;;;;;;;;;AAoBA,SAAgBU,sCAAwE;CACtF,OAAOpB,2BAA2B;AACpC"}
1
+ {"version":3,"file":"useComposerInput.js","names":["useCallback","useAui","flushTapSync","useComposerSend","ComposerSendOptions","TriggerPopoverAriaProps","useComposerInputDisabled","useComposerInputValue","useTriggerPopoverAriaProps","Unstable_UseComposerInputOptions","disabled","Unstable_ComposerInput","value","setText","text","send","options","isDisabled","canSend","unstable_useComposerInput","aui","composer","getState","isEditing","rawSend","sendDisabled","sendOptions","Unstable_TriggerPopoverAriaProps","unstable_useTriggerPopoverAriaProps"],"sources":["../../src/unstable/useComposerInput.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback } from \"react\";\nimport { useAui } from \"@assistant-ui/store\";\nimport { flushTapSync } from \"@assistant-ui/tap\";\nimport { useComposerSend } from \"@assistant-ui/core/react\";\nimport type { ComposerSendOptions } from \"@assistant-ui/core/store\";\nimport {\n type TriggerPopoverAriaProps,\n useComposerInputDisabled,\n useComposerInputValue,\n useTriggerPopoverAriaProps,\n} from \"../primitives/composer/useComposerInputState\";\n\n/** @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release. */\nexport type Unstable_UseComposerInputOptions = {\n /**\n * Disables the input in addition to the composer's own disabled sources\n * (thread disabled, active dictation). When disabled, `isDisabled` is `true`\n * and `canSend` is `false`.\n */\n disabled?: boolean | undefined;\n};\n\n/** @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release. */\nexport type Unstable_ComposerInput = {\n /** Current composer text, or `\"\"` when the composer is not editing. */\n value: string;\n /**\n * Writes `text` into the composer, mirroring `ComposerPrimitive.Input`:\n * a no-op unless the composer is editing; the controlled value stays in\n * sync within the same tick.\n */\n setText(text: string): void;\n /**\n * Sends the current message when `canSend` is `true`; otherwise a no-op.\n * Accepts the same options as the composer send action.\n */\n send(options?: ComposerSendOptions): void;\n /**\n * Whether the input is disabled, combining the `disabled` option with the\n * composer's own disabled sources (thread disabled, active dictation).\n */\n isDisabled: boolean;\n /**\n * Whether a send is currently available. Matches `ComposerPrimitive.Send`\n * gating (non-empty editing composer, not running without queue support) and\n * is additionally `false` while `isDisabled`.\n */\n canSend: boolean;\n};\n\n/**\n * Headless bridge to the composer's text value and send action, for building a\n * custom composer input without `ComposerPrimitive.Input`. It is a thin bridge,\n * not a second input: it does not own keyboard behavior, autosize, IME or\n * contentEditable sync, paste/drop attachments, focus management, or rich-text\n * state. Spread `unstable_useTriggerPopoverAriaProps()` onto your element for\n * trigger-popover combobox semantics.\n *\n * @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release.\n *\n * @example\n * ```tsx\n * const { value, setText, send, isDisabled, canSend } = unstable_useComposerInput();\n * <textarea\n * value={value}\n * disabled={isDisabled}\n * onChange={(e) => setText(e.target.value)}\n * onKeyDown={(e) => {\n * if (e.nativeEvent.isComposing || e.keyCode === 229) return;\n *\n * if (e.key === \"Enter\" && !e.shiftKey && canSend) {\n * e.preventDefault();\n * send();\n * }\n * }}\n * />\n * ```\n */\nexport function unstable_useComposerInput(\n options?: Unstable_UseComposerInputOptions,\n): Unstable_ComposerInput {\n const aui = useAui();\n const value = useComposerInputValue();\n const isDisabled = useComposerInputDisabled(options?.disabled);\n\n const setText = useCallback(\n (text: string) => {\n if (!aui.composer.getState().isEditing) return;\n flushTapSync(() => {\n aui.composer.setText(text);\n });\n },\n [aui],\n );\n\n const { send: rawSend, disabled: sendDisabled } = useComposerSend();\n const canSend = !sendDisabled && !isDisabled;\n const send = useCallback(\n (sendOptions?: ComposerSendOptions) => {\n if (!canSend) return;\n rawSend(sendOptions);\n },\n [canSend, rawSend],\n );\n\n return { value, setText, send, isDisabled, canSend };\n}\n\n/** @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release. */\nexport type Unstable_TriggerPopoverAriaProps = TriggerPopoverAriaProps;\n\n/**\n * ARIA combobox attributes for the focused element (typically the composer\n * input) describing the open trigger popover, per the WAI-ARIA editable\n * combobox pattern. Returns an empty object outside a `TriggerPopoverRoot` or\n * when no popover is open. Spread these last so they take precedence over any\n * matching ARIA props you set yourself, mirroring `ComposerPrimitive.Input`.\n *\n * @deprecated Experimental since 2026-06-23. Not scheduled for removal; the API may change in any release.\n *\n * @example\n * ```tsx\n * const aria = unstable_useTriggerPopoverAriaProps();\n * <textarea {...aria} />\n * ```\n */\nexport function unstable_useTriggerPopoverAriaProps(): Unstable_TriggerPopoverAriaProps {\n return useTriggerPopoverAriaProps();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgFA,SAAgBmB,0BACdH,SACwB;CACxB,MAAMI,MAAMnB,OAAO;CACnB,MAAMW,QAAQL,sBAAsB;CACpC,MAAMU,aAAaX,yBAAyBU,SAASN,QAAQ;CAE7D,MAAMG,UAAUb,aACbc,SAAiB;EAChB,IAAI,CAACM,IAAIC,SAASC,SAAS,CAAC,CAACC,WAAW;EACxCrB,mBAAmB;GACjBkB,IAAIC,SAASR,QAAQC,IAAI;EAC3B,CAAC;CACH,GACA,CAACM,GAAG,CACN;CAEA,MAAM,EAAEL,MAAMS,SAASd,UAAUe,iBAAiBtB,gBAAgB;CAClE,MAAMe,UAAU,CAACO,gBAAgB,CAACR;CASlC,OAAO;EAAEL;EAAOC;EAASE,MARZf,aACV0B,gBAAsC;GACrC,IAAI,CAACR,SAAS;GACdM,QAAQE,WAAW;EACrB,GACA,CAACR,SAASM,OAAO,CAGMT;EAAME;EAAYC;CAAQ;AACrD;;;;;;;;;;;;;;;;AAoBA,SAAgBU,sCAAwE;CACtF,OAAOpB,2BAA2B;AACpC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react",
3
- "version": "0.15.26",
3
+ "version": "0.15.27",
4
4
  "description": "Open-source TypeScript/React library for building production-grade AI chat experiences",
5
5
  "keywords": [
6
6
  "ai",
@@ -55,10 +55,10 @@
55
55
  ],
56
56
  "sideEffects": false,
57
57
  "dependencies": {
58
- "@assistant-ui/core": "^0.3.25",
59
- "@assistant-ui/store": "^0.3.18",
58
+ "@assistant-ui/core": "^0.3.26",
59
+ "@assistant-ui/store": "^0.3.19",
60
60
  "@assistant-ui/tap": "^0.9.22",
61
- "assistant-cloud": "^0.2.6",
61
+ "assistant-cloud": "^0.2.7",
62
62
  "assistant-stream": "^0.3.49",
63
63
  "radix-ui": "^1.7.0",
64
64
  "react-textarea-autosize": "^8.5.9",
package/src/index.ts CHANGED
@@ -586,6 +586,7 @@ export type {
586
586
  McpAppsHost,
587
587
  McpAppsRemoteHostOptions,
588
588
  McpAppToolCallParams,
589
+ McpAppInitializeParams,
589
590
  McpAppBridgeHandlers,
590
591
  ToolCallMessagePartMcpMetadata,
591
592
  } from "./mcp-apps";
@@ -11,6 +11,7 @@ import type {
11
11
  McpAppBridgeHandlers,
12
12
  McpAppFrameProps,
13
13
  McpAppHostContext,
14
+ McpAppInitializeParams,
14
15
  McpAppsHost,
15
16
  McpAppsRemoteHostOptions,
16
17
  } from "./types";
@@ -229,8 +230,12 @@ describe("McpAppRenderer", () => {
229
230
  await waitFor(() => expect(framePropsCalls().length).toBeGreaterThan(0));
230
231
  const handlers = framePropsCalls().at(-1)?.handlers;
231
232
  expect(handlers?.requestDisplayMode).toBeDefined();
232
- handlers?.onInitialized?.();
233
+ const params: McpAppInitializeParams = {
234
+ appCapabilities: { availableDisplayModes: ["inline", "fullscreen"] },
235
+ };
236
+ handlers?.onInitialized?.(params);
233
237
  expect(onInitialized).toHaveBeenCalledOnce();
238
+ expect(onInitialized).toHaveBeenCalledWith(params);
234
239
  });
235
240
 
236
241
  it("leaves mounted parts alone when renderer options are unchanged", async () => {
@@ -759,7 +764,7 @@ describe("McpAppRenderer", () => {
759
764
  await handlers.updateModelContext?.({ text: "context" });
760
765
  await handlers.openLink?.({ url: "https://example.com" });
761
766
  await handlers.sendMessage?.({ text: "hello" });
762
- handlers.onInitialized?.();
767
+ handlers.onInitialized?.({});
763
768
  await handlers.callTool?.({ name: "search" });
764
769
  await handlers.readResource?.({ uri: "ui://resource" });
765
770
  await handlers.listResources?.();
@@ -8,7 +8,11 @@ import type {
8
8
  } from "../sandbox-host/SandboxHost";
9
9
  import type { CreateMcpAppBridgeOptions, McpAppBridge } from "./bridge";
10
10
  import type * as BridgeModule from "./bridge";
11
- import { MCP_APP_MIME_TYPE, type McpAppHostContext } from "./types";
11
+ import {
12
+ MCP_APP_MIME_TYPE,
13
+ type McpAppHostContext,
14
+ type McpAppInitializeParams,
15
+ } from "./types";
12
16
 
13
17
  const { sandboxHostMock, createMcpAppBridgeMock } = vi.hoisted(() => ({
14
18
  sandboxHostMock: vi.fn(),
@@ -272,6 +276,60 @@ describe("McpAppFrame", () => {
272
276
  }
273
277
  });
274
278
 
279
+ it("forwards initialization metadata to the live handler", () => {
280
+ let createBridge: SandboxHostProps["createBridge"] | null = null;
281
+ sandboxHostMock.mockImplementation((props: SandboxHostProps) => {
282
+ createBridge ??= props.createBridge;
283
+ return null;
284
+ });
285
+ const bridge: McpAppBridge = {
286
+ onMessage: vi.fn(),
287
+ dispose: vi.fn(),
288
+ notifyToolInput: vi.fn(),
289
+ notifyToolResult: vi.fn(),
290
+ notifyHostContextChanged: vi.fn(),
291
+ };
292
+ createMcpAppBridgeMock.mockReturnValue(bridge);
293
+ const onInitialized = vi.fn();
294
+ const view = (handler: typeof onInitialized) => (
295
+ <McpAppFrame
296
+ app={{ resourceUri: "ui://example/widget" }}
297
+ resource={{
298
+ uri: "ui://example/widget",
299
+ mimeType: MCP_APP_MIME_TYPE,
300
+ html: "",
301
+ }}
302
+ handlers={{ onInitialized: handler }}
303
+ />
304
+ );
305
+ const rendered = render(view(onInitialized));
306
+
307
+ const sandboxBridge = createBridge!(
308
+ {
309
+ iframe: document.createElement("iframe"),
310
+ origin: "https://widget.example",
311
+ sendMessage: vi.fn(),
312
+ },
313
+ { setHeight: vi.fn() },
314
+ );
315
+ const options = createMcpAppBridgeMock.mock
316
+ .calls[0]![0] as CreateMcpAppBridgeOptions;
317
+ const replacementOnInitialized = vi.fn();
318
+ const params: McpAppInitializeParams = {
319
+ protocolVersion: "2026-01-26",
320
+ appInfo: { name: "example-app", version: "1.2.3" },
321
+ appCapabilities: { availableDisplayModes: ["inline", "pip"] },
322
+ };
323
+
324
+ rendered.rerender(view(replacementOnInitialized));
325
+ options.handlers?.onInitialized?.(params);
326
+
327
+ expect(onInitialized).not.toHaveBeenCalled();
328
+ expect(replacementOnInitialized).toHaveBeenCalledOnce();
329
+ expect(replacementOnInitialized).toHaveBeenCalledWith(params);
330
+ sandboxBridge.dispose();
331
+ });
332
+
275
333
  it("disposes the underlying bridge with the sandbox bridge", () => {
276
334
  let createBridge: SandboxHostProps["createBridge"] | null = null;
277
335
  sandboxHostMock.mockImplementation((props: SandboxHostProps) => {
@@ -350,7 +408,7 @@ describe("McpAppFrame", () => {
350
408
  );
351
409
  const options = createMcpAppBridgeMock.mock
352
410
  .calls[0]![0] as CreateMcpAppBridgeOptions;
353
- options.handlers?.onInitialized?.();
411
+ options.handlers?.onInitialized?.({});
354
412
 
355
413
  rendered.rerender(
356
414
  view({ displayMode: "inline", availableDisplayModes: ["inline", "pip"] }),
@@ -429,7 +487,7 @@ describe("McpAppFrame", () => {
429
487
 
430
488
  const options = createMcpAppBridgeMock.mock
431
489
  .calls[0]![0] as CreateMcpAppBridgeOptions;
432
- options.handlers?.onInitialized?.();
490
+ options.handlers?.onInitialized?.({});
433
491
 
434
492
  expect(bridge.notifyHostContextChanged).not.toHaveBeenCalled();
435
493
 
@@ -474,7 +532,7 @@ describe("McpAppFrame", () => {
474
532
  );
475
533
  const options = createMcpAppBridgeMock.mock
476
534
  .calls[0]![0] as CreateMcpAppBridgeOptions;
477
- options.handlers?.onInitialized?.();
535
+ options.handlers?.onInitialized?.({});
478
536
 
479
537
  rendered.rerender(view());
480
538
  expect(bridge.notifyHostContextChanged).not.toHaveBeenCalled();
@@ -519,7 +577,7 @@ describe("McpAppFrame", () => {
519
577
  );
520
578
  const options = createMcpAppBridgeMock.mock
521
579
  .calls[0]![0] as CreateMcpAppBridgeOptions;
522
- options.handlers?.onInitialized?.();
580
+ options.handlers?.onInitialized?.({});
523
581
 
524
582
  rendered.rerender(view(new Date(2)));
525
583
  expect(bridge.notifyHostContextChanged).toHaveBeenCalledTimes(1);
@@ -565,7 +623,7 @@ describe("McpAppFrame", () => {
565
623
  );
566
624
  const options = createMcpAppBridgeMock.mock
567
625
  .calls[0]![0] as CreateMcpAppBridgeOptions;
568
- options.handlers?.onInitialized?.();
626
+ options.handlers?.onInitialized?.({});
569
627
 
570
628
  rendered.rerender(view({}));
571
629
  expect(bridge.notifyHostContextChanged).not.toHaveBeenCalled();
@@ -143,9 +143,9 @@ function buildLiveHandlers(
143
143
  lifecycle.onSizeChange(p);
144
144
  live()?.onSizeChange?.(p);
145
145
  };
146
- out.onInitialized = () => {
146
+ out.onInitialized = (params) => {
147
147
  lifecycle.onInitialized();
148
- live()?.onInitialized?.();
148
+ live()?.onInitialized?.(params);
149
149
  };
150
150
  out.onRequestTeardown = (p) => live()?.onRequestTeardown?.(p);
151
151
  out.onLog = (p) => live()?.onLog?.(p);
@@ -197,6 +197,160 @@ describe("createMcpAppBridge", () => {
197
197
  bridge.dispose();
198
198
  });
199
199
 
200
+ it("passes view initialization metadata to onInitialized", () => {
201
+ const { frame } = makeFrame();
202
+ const onInitialized = vi.fn();
203
+ const bridge = createMcpAppBridge({ frame, handlers: { onInitialized } });
204
+ const params = {
205
+ protocolVersion: "2026-01-26",
206
+ appInfo: { name: "example-app", version: "1.2.3" },
207
+ appCapabilities: {
208
+ availableDisplayModes: ["inline", "fullscreen"],
209
+ tools: { listChanged: true },
210
+ },
211
+ };
212
+
213
+ deliver(bridge, {
214
+ jsonrpc: "2.0",
215
+ id: 1,
216
+ method: "ui/initialize",
217
+ params,
218
+ });
219
+ expect(onInitialized).not.toHaveBeenCalled();
220
+ deliver(bridge, {
221
+ jsonrpc: "2.0",
222
+ method: "notifications/initialized",
223
+ });
224
+
225
+ expect(onInitialized).toHaveBeenCalledOnce();
226
+ expect(onInitialized).toHaveBeenCalledWith(params);
227
+ bridge.dispose();
228
+ });
229
+
230
+ it("omits malformed initialization fields and keeps the protocol fallback", async () => {
231
+ const { frame, captured } = makeFrame();
232
+ const onInitialized = vi.fn();
233
+ const bridge = createMcpAppBridge({ frame, handlers: { onInitialized } });
234
+
235
+ deliver(bridge, {
236
+ jsonrpc: "2.0",
237
+ id: 1,
238
+ method: "ui/initialize",
239
+ params: {
240
+ protocolVersion: 1,
241
+ appInfo: { name: "malformed" },
242
+ appCapabilities: { availableDisplayModes: "fullscreen" },
243
+ },
244
+ });
245
+ deliver(bridge, {
246
+ jsonrpc: "2.0",
247
+ method: "notifications/initialized",
248
+ });
249
+ await flush();
250
+
251
+ expect(onInitialized).toHaveBeenCalledOnce();
252
+ expect(onInitialized).toHaveBeenCalledWith({
253
+ appCapabilities: {},
254
+ });
255
+ expect((captured[0] as McpAppJsonRpcResponse).result).toMatchObject({
256
+ protocolVersion: MCP_APP_PROTOCOL_VERSION,
257
+ });
258
+ bridge.dispose();
259
+ });
260
+
261
+ it("keeps only the display modes the host recognizes", () => {
262
+ const { frame } = makeFrame();
263
+ const onInitialized = vi.fn();
264
+ const bridge = createMcpAppBridge({ frame, handlers: { onInitialized } });
265
+
266
+ deliver(bridge, {
267
+ jsonrpc: "2.0",
268
+ id: 1,
269
+ method: "ui/initialize",
270
+ params: {
271
+ appCapabilities: { availableDisplayModes: ["inline", "sidebar"] },
272
+ },
273
+ });
274
+ deliver(bridge, {
275
+ jsonrpc: "2.0",
276
+ method: "notifications/initialized",
277
+ });
278
+
279
+ expect(onInitialized).toHaveBeenCalledWith({
280
+ appCapabilities: { availableDisplayModes: ["inline"] },
281
+ });
282
+ bridge.dispose();
283
+ });
284
+
285
+ it("reads a sparse display mode list without walking its length", () => {
286
+ const { frame } = makeFrame();
287
+ const onInitialized = vi.fn();
288
+ const bridge = createMcpAppBridge({ frame, handlers: { onInitialized } });
289
+ const availableDisplayModes = ["inline"];
290
+ availableDisplayModes.length = 2 ** 32 - 1;
291
+
292
+ deliver(bridge, {
293
+ jsonrpc: "2.0",
294
+ id: 1,
295
+ method: "ui/initialize",
296
+ params: { appCapabilities: { availableDisplayModes } },
297
+ });
298
+ deliver(bridge, {
299
+ jsonrpc: "2.0",
300
+ method: "notifications/initialized",
301
+ });
302
+
303
+ expect(onInitialized).toHaveBeenCalledWith({
304
+ appCapabilities: { availableDisplayModes: ["inline"] },
305
+ });
306
+ bridge.dispose();
307
+ });
308
+
309
+ it("initializes when the display mode list carries its own methods", async () => {
310
+ const { frame, captured } = makeFrame();
311
+ const onInitialized = vi.fn();
312
+ const bridge = createMcpAppBridge({ frame, handlers: { onInitialized } });
313
+
314
+ deliver(bridge, {
315
+ jsonrpc: "2.0",
316
+ id: 1,
317
+ method: "ui/initialize",
318
+ params: {
319
+ appCapabilities: {
320
+ availableDisplayModes: Object.assign(["inline", "pip"], {
321
+ filter: 0,
322
+ constructor: 0,
323
+ }),
324
+ },
325
+ },
326
+ });
327
+ deliver(bridge, {
328
+ jsonrpc: "2.0",
329
+ method: "notifications/initialized",
330
+ });
331
+ await flush();
332
+
333
+ expect((captured[0] as McpAppJsonRpcResponse).error).toBeUndefined();
334
+ expect(onInitialized).toHaveBeenCalledWith({
335
+ appCapabilities: { availableDisplayModes: ["inline", "pip"] },
336
+ });
337
+ bridge.dispose();
338
+ });
339
+
340
+ it("calls onInitialized with empty params when ui/initialize never arrived", () => {
341
+ const { frame } = makeFrame();
342
+ const onInitialized = vi.fn();
343
+ const bridge = createMcpAppBridge({ frame, handlers: { onInitialized } });
344
+
345
+ deliver(bridge, {
346
+ jsonrpc: "2.0",
347
+ method: "notifications/initialized",
348
+ });
349
+
350
+ expect(onInitialized).toHaveBeenCalledWith({});
351
+ bridge.dispose();
352
+ });
353
+
200
354
  it("routes tools/call to handler", async () => {
201
355
  const { frame, captured } = makeFrame();
202
356
  const callTool = vi.fn().mockResolvedValue({ ok: true });
@@ -6,6 +6,7 @@ import {
6
6
  type McpAppDisplayMode,
7
7
  type McpAppHostContext,
8
8
  type McpAppHostInfo,
9
+ type McpAppInitializeParams,
9
10
  type McpAppJsonRpcMessage,
10
11
  type McpAppJsonRpcNotification,
11
12
  type McpAppJsonRpcRequest,
@@ -19,6 +20,50 @@ const VALID_DISPLAY_MODES = [
19
20
  "pip",
20
21
  ] as const satisfies readonly McpAppDisplayMode[];
21
22
 
23
+ function isMcpAppDisplayMode(value: unknown): value is McpAppDisplayMode {
24
+ return VALID_DISPLAY_MODES.some((mode) => mode === value);
25
+ }
26
+
27
+ function isAppInfo(
28
+ value: unknown,
29
+ ): value is NonNullable<McpAppInitializeParams["appInfo"]> {
30
+ return (
31
+ isRecord(value) &&
32
+ typeof value.name === "string" &&
33
+ typeof value.version === "string"
34
+ );
35
+ }
36
+
37
+ function getAppCapabilities(
38
+ value: unknown,
39
+ ): McpAppInitializeParams["appCapabilities"] {
40
+ if (!isRecord(value)) return undefined;
41
+ const { availableDisplayModes, ...rest } = value;
42
+ return {
43
+ ...rest,
44
+ // Object.values reads only the entries present, so a sparse list claiming a huge length stays cheap, and the list's own methods are never called.
45
+ ...(Array.isArray(availableDisplayModes)
46
+ ? {
47
+ availableDisplayModes: Object.values(availableDisplayModes).filter(
48
+ isMcpAppDisplayMode,
49
+ ),
50
+ }
51
+ : {}),
52
+ };
53
+ }
54
+
55
+ function getInitializeParams(params: unknown): McpAppInitializeParams {
56
+ if (!isRecord(params)) return {};
57
+ const appCapabilities = getAppCapabilities(params.appCapabilities);
58
+ return {
59
+ ...(isAppInfo(params.appInfo) ? { appInfo: params.appInfo } : {}),
60
+ ...(appCapabilities ? { appCapabilities } : {}),
61
+ ...(typeof params.protocolVersion === "string"
62
+ ? { protocolVersion: params.protocolVersion }
63
+ : {}),
64
+ };
65
+ }
66
+
22
67
  export type McpAppBridgeFrame = SandboxHostFrame;
23
68
 
24
69
  export type CreateMcpAppBridgeOptions = {
@@ -91,6 +136,7 @@ export function createMcpAppBridge(
91
136
  hostContext = {},
92
137
  } = opts;
93
138
  let disposed = false;
139
+ let initializeParams: McpAppInitializeParams = {};
94
140
 
95
141
  const post = (msg: McpAppJsonRpcMessage) => {
96
142
  if (disposed) return;
@@ -142,10 +188,9 @@ export function createMcpAppBridge(
142
188
 
143
189
  switch (normalizeMethod(req.method)) {
144
190
  case "ui/initialize": {
191
+ initializeParams = getInitializeParams(params);
145
192
  const requestedProtocolVersion =
146
- isRecord(params) && typeof params.protocolVersion === "string"
147
- ? params.protocolVersion
148
- : MCP_APP_PROTOCOL_VERSION;
193
+ initializeParams.protocolVersion ?? MCP_APP_PROTOCOL_VERSION;
149
194
  respond(req.id, {
150
195
  result: {
151
196
  protocolVersion: requestedProtocolVersion,
@@ -360,10 +405,7 @@ export function createMcpAppBridge(
360
405
  return;
361
406
  }
362
407
  const modeParams = (params ?? {}) as { mode?: unknown };
363
- if (
364
- typeof modeParams.mode !== "string" ||
365
- !VALID_DISPLAY_MODES.includes(modeParams.mode as McpAppDisplayMode)
366
- ) {
408
+ if (!isMcpAppDisplayMode(modeParams.mode)) {
367
409
  errorResponse(
368
410
  req.id,
369
411
  JSONRPC_ERROR.invalidParams,
@@ -373,7 +415,7 @@ export function createMcpAppBridge(
373
415
  }
374
416
  respond(req.id, {
375
417
  result: await handlers.requestDisplayMode({
376
- mode: modeParams.mode as McpAppDisplayMode,
418
+ mode: modeParams.mode,
377
419
  }),
378
420
  });
379
421
  return;
@@ -398,7 +440,7 @@ export function createMcpAppBridge(
398
440
  try {
399
441
  switch (normalizeMethod(note.method)) {
400
442
  case "notifications/initialized": {
401
- handlers.onInitialized?.();
443
+ handlers.onInitialized?.(initializeParams);
402
444
  return;
403
445
  }
404
446
  case "notifications/size_changed": {
@@ -17,6 +17,7 @@ export type {
17
17
  McpAppsHost,
18
18
  McpAppsRemoteHostOptions,
19
19
  McpAppToolCallParams,
20
+ McpAppInitializeParams,
20
21
  McpAppBridgeHandlers,
21
22
  ToolCallMessagePartMcpMetadata,
22
23
  } from "./types";
@@ -87,6 +87,20 @@ export type McpAppToolCallParams = {
87
87
  arguments?: Record<string, unknown>;
88
88
  };
89
89
 
90
+ /**
91
+ * The params of the app's `ui/initialize` request, as `onInitialized`
92
+ * receives them. A field that fails validation is omitted, and
93
+ * `availableDisplayModes` keeps only the modes the bridge recognizes.
94
+ */
95
+ export type McpAppInitializeParams = {
96
+ appInfo?: { name: string; version: string; [k: string]: unknown };
97
+ appCapabilities?: {
98
+ availableDisplayModes?: McpAppDisplayMode[];
99
+ [k: string]: unknown;
100
+ };
101
+ protocolVersion?: string;
102
+ };
103
+
90
104
  export type McpAppBridgeHandlers = {
91
105
  allowedTools?: readonly string[];
92
106
  callTool?: (params: McpAppToolCallParams) => Promise<unknown> | unknown;
@@ -99,7 +113,7 @@ export type McpAppBridgeHandlers = {
99
113
  mode: McpAppDisplayMode;
100
114
  }) => Promise<{ mode: McpAppDisplayMode }> | { mode: McpAppDisplayMode };
101
115
  onSizeChange?: (params: { width?: number; height?: number }) => void;
102
- onInitialized?: () => void;
116
+ onInitialized?: (params: McpAppInitializeParams) => void;
103
117
  onRequestTeardown?: (params: unknown) => void;
104
118
  onLog?: (params: unknown) => void;
105
119
  onError?: (error: Error) => void;
@@ -6,9 +6,10 @@ import { createRoot, type Root } from "react-dom/client";
6
6
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
7
  import { ComposerPrimitiveAttachmentDropzone } from "./ComposerAttachmentDropzone";
8
8
 
9
- const { addAttachment, threadCapabilities } = vi.hoisted(() => ({
9
+ const { addAttachment, threadCapabilities, threadState } = vi.hoisted(() => ({
10
10
  addAttachment: vi.fn<(file: File) => Promise<void>>(),
11
11
  threadCapabilities: { attachments: true },
12
+ threadState: { isDisabled: false },
12
13
  }));
13
14
 
14
15
  (globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
@@ -25,6 +26,9 @@ vi.mock("@assistant-ui/store", async (importOriginal) => {
25
26
  getState: () => ({ capabilities: threadCapabilities }),
26
27
  },
27
28
  }),
29
+ useAuiState: (
30
+ selector: (state: { thread: typeof threadState }) => unknown,
31
+ ) => selector({ thread: threadState }),
28
32
  };
29
33
  });
30
34
 
@@ -53,6 +57,7 @@ describe("ComposerPrimitiveAttachmentDropzone", () => {
53
57
  beforeEach(async () => {
54
58
  addAttachment.mockReset();
55
59
  threadCapabilities.attachments = true;
60
+ threadState.isDisabled = false;
56
61
  container = document.createElement("div");
57
62
  document.body.appendChild(container);
58
63
  root = createRoot(container);
@@ -250,6 +255,32 @@ describe("ComposerPrimitiveAttachmentDropzone", () => {
250
255
  expect(dropzone.hasAttribute("data-dragging")).toBe(false);
251
256
  });
252
257
 
258
+ it("blocks file drops when the thread is disabled", async () => {
259
+ threadState.isDisabled = true;
260
+ await act(async () => {
261
+ root.render(
262
+ <ComposerPrimitiveAttachmentDropzone data-testid="dropzone">
263
+ <div>dropzone</div>
264
+ </ComposerPrimitiveAttachmentDropzone>,
265
+ );
266
+ });
267
+ const dropzone = container.querySelector("[data-testid='dropzone']")!;
268
+ const over = createDragEvent("dragover", ["Files"]);
269
+ const drop = createDropEvent([
270
+ new File(["file"], "photo.png", { type: "image/png" }),
271
+ ]);
272
+
273
+ await act(async () => {
274
+ dropzone.dispatchEvent(over);
275
+ dropzone.dispatchEvent(drop);
276
+ });
277
+
278
+ expect(over.defaultPrevented).toBe(true);
279
+ expect(drop.defaultPrevented).toBe(true);
280
+ expect(addAttachment).not.toHaveBeenCalled();
281
+ expect(dropzone.hasAttribute("data-dragging")).toBe(false);
282
+ });
283
+
253
284
  it("ignores non-file drags", async () => {
254
285
  const dropzone = container.querySelector("[data-testid='dropzone']");
255
286
  expect(dropzone).not.toBeNull();