@assistant-ui/core 0.3.3 → 0.3.4

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 (127) hide show
  1. package/dist/internal.d.ts +4 -2
  2. package/dist/internal.js +4 -2
  3. package/dist/react/primitive-hooks/useSuggestionTrigger.js +29 -25
  4. package/dist/react/primitive-hooks/useSuggestionTrigger.js.map +1 -1
  5. package/dist/react/primitives/message/MessageParts.d.ts +6 -1
  6. package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
  7. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  8. package/dist/react/providers/ReadonlyThreadProvider.d.ts.map +1 -1
  9. package/dist/react/providers/ReadonlyThreadProvider.js +1 -0
  10. package/dist/react/providers/ReadonlyThreadProvider.js.map +1 -1
  11. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +67 -0
  12. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  13. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +76 -15
  14. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  15. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +4 -0
  16. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  17. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +21 -0
  18. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  19. package/dist/react/runtimes/cloud/auiV0.d.ts +2 -0
  20. package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
  21. package/dist/react/runtimes/cloud/auiV0.js +6 -4
  22. package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
  23. package/dist/react/types/MessagePartComponentTypes.d.ts +8 -0
  24. package/dist/react/types/MessagePartComponentTypes.d.ts.map +1 -1
  25. package/dist/runtime/api/bindings.d.ts +5 -0
  26. package/dist/runtime/api/bindings.d.ts.map +1 -1
  27. package/dist/runtime/api/thread-list-runtime.d.ts +14 -1
  28. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  29. package/dist/runtime/api/thread-list-runtime.js +6 -1
  30. package/dist/runtime/api/thread-list-runtime.js.map +1 -1
  31. package/dist/runtime/api/thread-runtime.d.ts +7 -1
  32. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  33. package/dist/runtime/api/thread-runtime.js +11 -3
  34. package/dist/runtime/api/thread-runtime.js.map +1 -1
  35. package/dist/runtime/base/base-composer-runtime-core.d.ts +3 -0
  36. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  37. package/dist/runtime/base/base-composer-runtime-core.js +54 -12
  38. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  39. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +10 -0
  40. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
  41. package/dist/runtime/interfaces/thread-runtime-core.d.ts +14 -0
  42. package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
  43. package/dist/runtimes/external-store/external-store-adapter.d.ts +9 -0
  44. package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
  45. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +1 -0
  46. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  47. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +4 -0
  48. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  49. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +1 -0
  50. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  51. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +6 -0
  52. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  53. package/dist/runtimes/local/local-thread-runtime-core.d.ts +1 -0
  54. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  55. package/dist/runtimes/local/local-thread-runtime-core.js +1 -0
  56. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  57. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +1 -0
  58. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
  59. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +1 -0
  60. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
  61. package/dist/runtimes/remote-thread-list/empty-thread-core.js +1 -0
  62. package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
  63. package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts +1 -0
  64. package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
  65. package/dist/store/runtime-clients/thread-list-item-runtime-client.js +66 -45
  66. package/dist/store/runtime-clients/thread-list-item-runtime-client.js.map +1 -1
  67. package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
  68. package/dist/store/runtime-clients/thread-list-runtime-client.js +91 -75
  69. package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
  70. package/dist/store/scopes/thread-list-item.d.ts +7 -0
  71. package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
  72. package/dist/store/scopes/threads.d.ts +1 -0
  73. package/dist/store/scopes/threads.d.ts.map +1 -1
  74. package/dist/types/message.d.ts +11 -0
  75. package/dist/types/message.d.ts.map +1 -1
  76. package/dist/types/message.js.map +1 -1
  77. package/dist/utils/data-url.d.ts +14 -1
  78. package/dist/utils/data-url.d.ts.map +1 -1
  79. package/dist/utils/data-url.js +23 -1
  80. package/dist/utils/data-url.js.map +1 -1
  81. package/dist/utils/image-media-type.d.ts +13 -0
  82. package/dist/utils/image-media-type.d.ts.map +1 -0
  83. package/dist/utils/image-media-type.js +141 -0
  84. package/dist/utils/image-media-type.js.map +1 -0
  85. package/dist/utils/wire-media.d.ts +35 -0
  86. package/dist/utils/wire-media.d.ts.map +1 -0
  87. package/dist/utils/wire-media.js +55 -0
  88. package/dist/utils/wire-media.js.map +1 -0
  89. package/package.json +3 -3
  90. package/src/internal.ts +12 -1
  91. package/src/react/primitive-hooks/useSuggestionTrigger.test.ts +148 -0
  92. package/src/react/primitive-hooks/useSuggestionTrigger.ts +10 -4
  93. package/src/react/primitives/message/MessageParts.tsx +6 -1
  94. package/src/react/providers/ReadonlyThreadProvider.tsx +1 -0
  95. package/src/react/runtimes/RemoteThreadListHookInstanceManager.running.test.tsx +157 -0
  96. package/src/react/runtimes/RemoteThreadListHookInstanceManager.test.tsx +160 -0
  97. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +135 -32
  98. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +37 -0
  99. package/src/react/runtimes/cloud/auiV0.ts +8 -2
  100. package/src/react/types/MessagePartComponentTypes.ts +8 -0
  101. package/src/runtime/api/bindings.ts +5 -0
  102. package/src/runtime/api/thread-list-runtime.ts +22 -1
  103. package/src/runtime/api/thread-runtime.ts +15 -6
  104. package/src/runtime/base/base-composer-runtime-core.ts +72 -19
  105. package/src/runtime/interfaces/thread-list-runtime-core.ts +11 -0
  106. package/src/runtime/interfaces/thread-runtime-core.ts +15 -0
  107. package/src/runtimes/external-store/external-store-adapter.ts +9 -0
  108. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +9 -0
  109. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +8 -0
  110. package/src/runtimes/local/local-thread-runtime-core.ts +1 -0
  111. package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +1 -0
  112. package/src/runtimes/remote-thread-list/empty-thread-core.ts +1 -0
  113. package/src/store/runtime-clients/thread-list-item-runtime-client.ts +13 -2
  114. package/src/store/runtime-clients/thread-list-runtime-client.ts +13 -1
  115. package/src/store/scopes/thread-list-item.ts +7 -0
  116. package/src/store/scopes/threads.ts +1 -0
  117. package/src/tests/RemoteThreadListThreadListRuntimeCore-reloadMainThread.test.ts +257 -0
  118. package/src/tests/auiV0Encode.test.ts +125 -1
  119. package/src/tests/base-composer-runtime-core-addAttachment.test.ts +296 -0
  120. package/src/tests/event-subscription-listener-errors.test.ts +1 -0
  121. package/src/tests/external-store-thread-list-runtime-core.test.ts +39 -1
  122. package/src/types/message.ts +11 -0
  123. package/src/utils/data-url.ts +24 -0
  124. package/src/utils/image-media-type.test.ts +96 -0
  125. package/src/utils/image-media-type.ts +118 -0
  126. package/src/utils/wire-media.test.ts +96 -0
  127. package/src/utils/wire-media.ts +70 -0
@@ -0,0 +1,141 @@
1
+ //#region src/utils/image-media-type.ts
2
+ /**
3
+ * Leading-byte signatures for the image formats a browser or attachment
4
+ * adapter realistically produces. `null` matches any byte, which WebP needs
5
+ * because its four size bytes sit between the two markers, and AVIF and HEIC
6
+ * need because their leading `ftyp` box size varies by encoder.
7
+ *
8
+ * These are file format constants rather than an evolving API, so the table
9
+ * mirrors the one the AI SDK applies on the far side of several adapters
10
+ * without taking a dependency to reach it.
11
+ */
12
+ const IMAGE_SIGNATURES = [
13
+ {
14
+ mediaType: "image/gif",
15
+ prefix: [
16
+ 71,
17
+ 73,
18
+ 70
19
+ ]
20
+ },
21
+ {
22
+ mediaType: "image/png",
23
+ prefix: [
24
+ 137,
25
+ 80,
26
+ 78,
27
+ 71
28
+ ]
29
+ },
30
+ {
31
+ mediaType: "image/jpeg",
32
+ prefix: [255, 216]
33
+ },
34
+ {
35
+ mediaType: "image/webp",
36
+ prefix: [
37
+ 82,
38
+ 73,
39
+ 70,
40
+ 70,
41
+ null,
42
+ null,
43
+ null,
44
+ null,
45
+ 87,
46
+ 69,
47
+ 66,
48
+ 80
49
+ ]
50
+ },
51
+ {
52
+ mediaType: "image/bmp",
53
+ prefix: [66, 77]
54
+ },
55
+ {
56
+ mediaType: "image/tiff",
57
+ prefix: [
58
+ 73,
59
+ 73,
60
+ 42,
61
+ 0
62
+ ]
63
+ },
64
+ {
65
+ mediaType: "image/tiff",
66
+ prefix: [
67
+ 77,
68
+ 77,
69
+ 0,
70
+ 42
71
+ ]
72
+ },
73
+ {
74
+ mediaType: "image/avif",
75
+ prefix: [
76
+ null,
77
+ null,
78
+ null,
79
+ null,
80
+ 102,
81
+ 116,
82
+ 121,
83
+ 112,
84
+ 97,
85
+ 118,
86
+ 105,
87
+ 102
88
+ ]
89
+ },
90
+ {
91
+ mediaType: "image/heic",
92
+ prefix: [
93
+ null,
94
+ null,
95
+ null,
96
+ null,
97
+ 102,
98
+ 116,
99
+ 121,
100
+ 112,
101
+ 104,
102
+ 101,
103
+ 105,
104
+ 99
105
+ ]
106
+ }
107
+ ];
108
+ /** 16 base64 characters decode to the 12 bytes the longest signature needs. */
109
+ const PREFIX_CHARS = 16;
110
+ const decodePrefix = (base64) => {
111
+ const prefix = base64.slice(0, PREFIX_CHARS);
112
+ const aligned = prefix.slice(0, prefix.length - prefix.length % 4);
113
+ if (aligned.length === 0) return void 0;
114
+ const nodeBuffer = globalThis.Buffer;
115
+ if (nodeBuffer) return new Uint8Array(nodeBuffer.from(aligned, "base64"));
116
+ try {
117
+ const binary = atob(aligned);
118
+ const bytes = new Uint8Array(binary.length);
119
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
120
+ return bytes;
121
+ } catch {
122
+ return;
123
+ }
124
+ };
125
+ /**
126
+ * The image media type a base64 payload's leading bytes identify, or
127
+ * `undefined` when they match no known signature or the input is not base64.
128
+ *
129
+ * An `ImageMessagePart` carries no media type, so an adapter that must declare
130
+ * one on the wire reads it from the bytes rather than assuming a format. Never
131
+ * throws: the input is whatever a caller put on the part.
132
+ */
133
+ function detectImageMediaType(base64) {
134
+ const bytes = decodePrefix(base64);
135
+ if (!bytes) return void 0;
136
+ return IMAGE_SIGNATURES.find(({ prefix }) => bytes.length >= prefix.length && prefix.every((byte, index) => byte === null || bytes[index] === byte))?.mediaType;
137
+ }
138
+ //#endregion
139
+ export { detectImageMediaType };
140
+
141
+ //# sourceMappingURL=image-media-type.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-media-type.js","names":["IMAGE_SIGNATURES","mediaType","prefix","PREFIX_CHARS","decodePrefix","base64","Uint8Array","slice","aligned","length","undefined","nodeBuffer","globalThis","Buffer","from","data","encoding","binary","atob","bytes","i","charCodeAt","detectImageMediaType","find","every","byte","index"],"sources":["../../src/utils/image-media-type.ts"],"sourcesContent":["/**\n * Leading-byte signatures for the image formats a browser or attachment\n * adapter realistically produces. `null` matches any byte, which WebP needs\n * because its four size bytes sit between the two markers, and AVIF and HEIC\n * need because their leading `ftyp` box size varies by encoder.\n *\n * These are file format constants rather than an evolving API, so the table\n * mirrors the one the AI SDK applies on the far side of several adapters\n * without taking a dependency to reach it.\n */\nconst IMAGE_SIGNATURES: readonly {\n readonly mediaType: string;\n readonly prefix: readonly (number | null)[];\n}[] = [\n { mediaType: \"image/gif\", prefix: [0x47, 0x49, 0x46] },\n { mediaType: \"image/png\", prefix: [0x89, 0x50, 0x4e, 0x47] },\n { mediaType: \"image/jpeg\", prefix: [0xff, 0xd8] },\n {\n mediaType: \"image/webp\",\n prefix: [\n 0x52,\n 0x49,\n 0x46,\n 0x46,\n null,\n null,\n null,\n null,\n 0x57,\n 0x45,\n 0x42,\n 0x50,\n ],\n },\n { mediaType: \"image/bmp\", prefix: [0x42, 0x4d] },\n { mediaType: \"image/tiff\", prefix: [0x49, 0x49, 0x2a, 0x00] },\n { mediaType: \"image/tiff\", prefix: [0x4d, 0x4d, 0x00, 0x2a] },\n {\n mediaType: \"image/avif\",\n prefix: [\n null,\n null,\n null,\n null,\n 0x66,\n 0x74,\n 0x79,\n 0x70,\n 0x61,\n 0x76,\n 0x69,\n 0x66,\n ],\n },\n {\n mediaType: \"image/heic\",\n prefix: [\n null,\n null,\n null,\n null,\n 0x66,\n 0x74,\n 0x79,\n 0x70,\n 0x68,\n 0x65,\n 0x69,\n 0x63,\n ],\n },\n];\n\n/** 16 base64 characters decode to the 12 bytes the longest signature needs. */\nconst PREFIX_CHARS = 16;\n\nconst decodePrefix = (base64: string): Uint8Array | undefined => {\n const prefix = base64.slice(0, PREFIX_CHARS);\n const aligned = prefix.slice(0, prefix.length - (prefix.length % 4));\n if (aligned.length === 0) return undefined;\n\n const nodeBuffer = (\n globalThis as {\n Buffer?: {\n from(data: string, encoding: string): Uint8Array;\n };\n }\n ).Buffer;\n if (nodeBuffer) return new Uint8Array(nodeBuffer.from(aligned, \"base64\"));\n\n try {\n const binary = atob(aligned);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n return bytes;\n } catch {\n return undefined;\n }\n};\n\n/**\n * The image media type a base64 payload's leading bytes identify, or\n * `undefined` when they match no known signature or the input is not base64.\n *\n * An `ImageMessagePart` carries no media type, so an adapter that must declare\n * one on the wire reads it from the bytes rather than assuming a format. Never\n * throws: the input is whatever a caller put on the part.\n */\nexport function detectImageMediaType(base64: string): string | undefined {\n const bytes = decodePrefix(base64);\n if (!bytes) return undefined;\n\n return IMAGE_SIGNATURES.find(\n ({ prefix }) =>\n bytes.length >= prefix.length &&\n prefix.every((byte, index) => byte === null || bytes[index] === byte),\n )?.mediaType;\n}\n"],"mappings":";;;;;;;;;;;AAUA,MAAMA,mBAGA;CACJ;EAAEC,WAAW;EAAaC,QAAQ;GAAC;GAAM;GAAM;EAAI;CAAE;CACrD;EAAED,WAAW;EAAaC,QAAQ;GAAC;GAAM;GAAM;GAAM;EAAI;CAAE;CAC3D;EAAED,WAAW;EAAcC,QAAQ,CAAC,KAAM,GAAI;CAAE;CAChD;EACED,WAAW;EACXC,QAAQ;GACN;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EAAI;CAER;CACA;EAAED,WAAW;EAAaC,QAAQ,CAAC,IAAM,EAAI;CAAE;CAC/C;EAAED,WAAW;EAAcC,QAAQ;GAAC;GAAM;GAAM;GAAM;EAAI;CAAE;CAC5D;EAAED,WAAW;EAAcC,QAAQ;GAAC;GAAM;GAAM;GAAM;EAAI;CAAE;CAC5D;EACED,WAAW;EACXC,QAAQ;GACN;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EAAI;CAER;CACA;EACED,WAAW;EACXC,QAAQ;GACN;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EAAI;CAER;AAAC;;AAIH,MAAMC,eAAe;AAErB,MAAMC,gBAAgBC,WAA2C;CAC/D,MAAMH,SAASG,OAAOE,MAAM,GAAGJ,YAAY;CAC3C,MAAMK,UAAUN,OAAOK,MAAM,GAAGL,OAAOO,SAAUP,OAAOO,SAAS,CAAE;CACnE,IAAID,QAAQC,WAAW,GAAG,OAAOC,KAAAA;CAEjC,MAAMC,aACJC,WAKAC;CACF,IAAIF,YAAY,OAAO,IAAIL,WAAWK,WAAWG,KAAKN,SAAS,QAAQ,CAAC;CAExE,IAAI;EACF,MAAMS,SAASC,KAAKV,OAAO;EAC3B,MAAMW,QAAQ,IAAIb,WAAWW,OAAOR,MAAM;EAC1C,KAAK,IAAIW,IAAI,GAAGA,IAAIH,OAAOR,QAAQW,KAAKD,MAAMC,KAAKH,OAAOI,WAAWD,CAAC;EACtE,OAAOD;CACT,QAAQ;EACN;CACF;AACF;;;;;;;;;AAUA,SAAgBG,qBAAqBjB,QAAoC;CACvE,MAAMc,QAAQf,aAAaC,MAAM;CACjC,IAAI,CAACc,OAAO,OAAOT,KAAAA;CAEnB,OAAOV,iBAAiBuB,MACrB,EAAErB,aACDiB,MAAMV,UAAUP,OAAOO,UACvBP,OAAOsB,OAAOC,MAAMC,UAAUD,SAAS,QAAQN,MAAMO,WAAWD,IAAI,CACxE,CAAC,EAAExB;AACL"}
@@ -0,0 +1,35 @@
1
+ //#region src/utils/wire-media.d.ts
2
+ /**
3
+ * The media type to declare for an `ImageMessagePart`, which carries none of
4
+ * its own: an attachment's content type, then the payload's data URL
5
+ * declaration, then its leading bytes, then `image/png`.
6
+ *
7
+ * The floor cannot be a wildcard. `image/*` is rejected outright by the AI SDK
8
+ * for a url source and whenever inline bytes fail to sniff, so it fails exactly
9
+ * where a fallback is needed.
10
+ */
11
+ declare function resolveImageMediaType(image: string, contentType?: string | undefined): string;
12
+ /**
13
+ * The media type to declare for a `FileMessagePart`: its own `mimeType`, then
14
+ * the payload's data URL declaration, then `application/octet-stream`.
15
+ *
16
+ * `mimeType` is a plain string, and an adapter reading `file.type` on a file
17
+ * the OS cannot type yields `""`, so the declared value is not always present.
18
+ */
19
+ declare function resolveFileMediaType(data: string, mimeType?: string | undefined): string;
20
+ /**
21
+ * A payload placed into a wire field that is contractually a url.
22
+ *
23
+ * Two hazards drive this. A consumer may hand the value to an unguarded
24
+ * `new URL()`, so a payload that is not a url has to be wrapped; and a data
25
+ * URL's own media type wins over a separately declared one downstream, so a
26
+ * base64 envelope that disagrees with `mediaType` is rebuilt around the same
27
+ * bytes. Everything else passes through byte for byte: an envelope that agrees,
28
+ * a url of any other scheme, and a percent-encoded data URL, whose declaration
29
+ * cannot be corrected without transcoding the payload and is authoritative for
30
+ * the bytes it carries anyway.
31
+ */
32
+ declare function toMediaWireUrl(payload: string, mediaType: string): string;
33
+ //#endregion
34
+ export { resolveFileMediaType, resolveImageMediaType, toMediaWireUrl };
35
+ //# sourceMappingURL=wire-media.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wire-media.d.ts","names":[],"sources":["../../src/utils/wire-media.ts"],"mappings":";;;;;;;;;;iBAYgB,sBACd,eACA;;;;;;;;iBA2Bc,qBACd,cACA;;;;;;;;;;;;;iBAiBc,eAAe,iBAAiB"}
@@ -0,0 +1,55 @@
1
+ import { dataUrlMediaType, isParsableUrl, parseDataUrl } from "./data-url.js";
2
+ import { detectImageMediaType } from "./image-media-type.js";
3
+ //#region src/utils/wire-media.ts
4
+ /**
5
+ * The media type to declare for an `ImageMessagePart`, which carries none of
6
+ * its own: an attachment's content type, then the payload's data URL
7
+ * declaration, then its leading bytes, then `image/png`.
8
+ *
9
+ * The floor cannot be a wildcard. `image/*` is rejected outright by the AI SDK
10
+ * for a url source and whenever inline bytes fail to sniff, so it fails exactly
11
+ * where a fallback is needed.
12
+ */
13
+ function resolveImageMediaType(image, contentType) {
14
+ if (contentType?.startsWith("image/")) return contentType;
15
+ const declared = dataUrlMediaType(image);
16
+ if (declared?.startsWith("image/")) return declared;
17
+ const payload = parseDataUrl(image)?.data ?? (isParsableUrl(image) ? void 0 : image);
18
+ if (payload !== void 0) {
19
+ const sniffed = detectImageMediaType(payload);
20
+ if (sniffed) return sniffed;
21
+ }
22
+ return "image/png";
23
+ }
24
+ /**
25
+ * The media type to declare for a `FileMessagePart`: its own `mimeType`, then
26
+ * the payload's data URL declaration, then `application/octet-stream`.
27
+ *
28
+ * `mimeType` is a plain string, and an adapter reading `file.type` on a file
29
+ * the OS cannot type yields `""`, so the declared value is not always present.
30
+ */
31
+ function resolveFileMediaType(data, mimeType) {
32
+ return mimeType || dataUrlMediaType(data) || "application/octet-stream";
33
+ }
34
+ /**
35
+ * A payload placed into a wire field that is contractually a url.
36
+ *
37
+ * Two hazards drive this. A consumer may hand the value to an unguarded
38
+ * `new URL()`, so a payload that is not a url has to be wrapped; and a data
39
+ * URL's own media type wins over a separately declared one downstream, so a
40
+ * base64 envelope that disagrees with `mediaType` is rebuilt around the same
41
+ * bytes. Everything else passes through byte for byte: an envelope that agrees,
42
+ * a url of any other scheme, and a percent-encoded data URL, whose declaration
43
+ * cannot be corrected without transcoding the payload and is authoritative for
44
+ * the bytes it carries anyway.
45
+ */
46
+ function toMediaWireUrl(payload, mediaType) {
47
+ const parsed = parseDataUrl(payload);
48
+ if (parsed) return parsed.mimeType === mediaType ? payload : `data:${mediaType};base64,${parsed.data}`;
49
+ if (isParsableUrl(payload)) return payload;
50
+ return `data:${mediaType};base64,${payload}`;
51
+ }
52
+ //#endregion
53
+ export { resolveFileMediaType, resolveImageMediaType, toMediaWireUrl };
54
+
55
+ //# sourceMappingURL=wire-media.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wire-media.js","names":["dataUrlMediaType","isParsableUrl","parseDataUrl","detectImageMediaType","resolveImageMediaType","image","contentType","startsWith","declared","parsed","payload","data","undefined","sniffed","resolveFileMediaType","mimeType","toMediaWireUrl","mediaType"],"sources":["../../src/utils/wire-media.ts"],"sourcesContent":["import { dataUrlMediaType, isParsableUrl, parseDataUrl } from \"./data-url\";\nimport { detectImageMediaType } from \"./image-media-type\";\n\n/**\n * The media type to declare for an `ImageMessagePart`, which carries none of\n * its own: an attachment's content type, then the payload's data URL\n * declaration, then its leading bytes, then `image/png`.\n *\n * The floor cannot be a wildcard. `image/*` is rejected outright by the AI SDK\n * for a url source and whenever inline bytes fail to sniff, so it fails exactly\n * where a fallback is needed.\n */\nexport function resolveImageMediaType(\n image: string,\n contentType?: string | undefined,\n): string {\n if (contentType?.startsWith(\"image/\")) return contentType;\n\n const declared = dataUrlMediaType(image);\n if (declared?.startsWith(\"image/\")) return declared;\n\n // Read through a data URL envelope too, so a generic one such as\n // `application/octet-stream` does not mask the format. A url of any other\n // scheme has no local bytes to read.\n const parsed = parseDataUrl(image);\n const payload = parsed?.data ?? (isParsableUrl(image) ? undefined : image);\n if (payload !== undefined) {\n const sniffed = detectImageMediaType(payload);\n if (sniffed) return sniffed;\n }\n\n return \"image/png\";\n}\n\n/**\n * The media type to declare for a `FileMessagePart`: its own `mimeType`, then\n * the payload's data URL declaration, then `application/octet-stream`.\n *\n * `mimeType` is a plain string, and an adapter reading `file.type` on a file\n * the OS cannot type yields `\"\"`, so the declared value is not always present.\n */\nexport function resolveFileMediaType(\n data: string,\n mimeType?: string | undefined,\n): string {\n return mimeType || dataUrlMediaType(data) || \"application/octet-stream\";\n}\n\n/**\n * A payload placed into a wire field that is contractually a url.\n *\n * Two hazards drive this. A consumer may hand the value to an unguarded\n * `new URL()`, so a payload that is not a url has to be wrapped; and a data\n * URL's own media type wins over a separately declared one downstream, so a\n * base64 envelope that disagrees with `mediaType` is rebuilt around the same\n * bytes. Everything else passes through byte for byte: an envelope that agrees,\n * a url of any other scheme, and a percent-encoded data URL, whose declaration\n * cannot be corrected without transcoding the payload and is authoritative for\n * the bytes it carries anyway.\n */\nexport function toMediaWireUrl(payload: string, mediaType: string): string {\n const parsed = parseDataUrl(payload);\n if (parsed) {\n return parsed.mimeType === mediaType\n ? payload\n : `data:${mediaType};base64,${parsed.data}`;\n }\n if (isParsableUrl(payload)) return payload;\n return `data:${mediaType};base64,${payload}`;\n}\n"],"mappings":";;;;;;;;;;;;AAYA,SAAgBI,sBACdC,OACAC,aACQ;CACR,IAAIA,aAAaC,WAAW,QAAQ,GAAG,OAAOD;CAE9C,MAAME,WAAWR,iBAAiBK,KAAK;CACvC,IAAIG,UAAUD,WAAW,QAAQ,GAAG,OAAOC;CAM3C,MAAME,UADSR,aAAaG,KACZI,CAAM,EAAEE,SAASV,cAAcI,KAAK,IAAIO,KAAAA,IAAYP;CACpE,IAAIK,YAAYE,KAAAA,GAAW;EACzB,MAAMC,UAAUV,qBAAqBO,OAAO;EAC5C,IAAIG,SAAS,OAAOA;CACtB;CAEA,OAAO;AACT;;;;;;;;AASA,SAAgBC,qBACdH,MACAI,UACQ;CACR,OAAOA,YAAYf,iBAAiBW,IAAI,KAAK;AAC/C;;;;;;;;;;;;;AAcA,SAAgBK,eAAeN,SAAiBO,WAA2B;CACzE,MAAMR,SAASP,aAAaQ,OAAO;CACnC,IAAID,QACF,OAAOA,OAAOM,aAAaE,YACvBP,UACA,QAAQO,UAAS,UAAWR,OAAOE;CAEzC,IAAIV,cAAcS,OAAO,GAAG,OAAOA;CACnC,OAAO,QAAQO,UAAS,UAAWP;AACrC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/core",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Framework-agnostic core runtime for assistant-ui",
5
5
  "keywords": [
6
6
  "assistant",
@@ -59,7 +59,7 @@
59
59
  ],
60
60
  "sideEffects": false,
61
61
  "dependencies": {
62
- "assistant-stream": "^0.3.31",
62
+ "assistant-stream": "^0.3.32",
63
63
  "nanoid": "^6.0.0"
64
64
  },
65
65
  "optionalDevDependencies": {
@@ -95,7 +95,7 @@
95
95
  "react-dom": "^19.2.8",
96
96
  "vitest": "^4.1.10",
97
97
  "zustand": "^5.0.14",
98
- "@assistant-ui/store": "0.3.2",
98
+ "@assistant-ui/store": "0.3.3",
99
99
  "@assistant-ui/tap": "0.9.9",
100
100
  "@assistant-ui/vite": "0.0.11",
101
101
  "@assistant-ui/x-buildutils": "0.0.20",
package/src/internal.ts CHANGED
@@ -57,7 +57,18 @@ export { isJSONValue, isRecord } from "./utils/json/is-json";
57
57
 
58
58
  // Data-URL decoder and http(s) matcher, reused by framework adapters so the
59
59
  // outbound part conversion lives in one place.
60
- export { httpUrlPattern, parseDataUrl } from "./utils/data-url";
60
+ export {
61
+ dataUrlMediaType,
62
+ httpUrlPattern,
63
+ isParsableUrl,
64
+ parseDataUrl,
65
+ } from "./utils/data-url";
66
+ export { detectImageMediaType } from "./utils/image-media-type";
67
+ export {
68
+ resolveFileMediaType,
69
+ resolveImageMediaType,
70
+ toMediaWireUrl,
71
+ } from "./utils/wire-media";
61
72
 
62
73
  export * from "./runtime/internal";
63
74
  export * from "./runtimes/internal";
@@ -0,0 +1,148 @@
1
+ /** @vitest-environment jsdom */
2
+ import { cleanup, renderHook } from "@testing-library/react";
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+
5
+ const mocks = vi.hoisted(() => {
6
+ const append = vi.fn();
7
+ const setText = vi.fn();
8
+
9
+ const state = {
10
+ thread: {
11
+ isDisabled: false,
12
+ isRunning: false,
13
+ capabilities: { queue: false },
14
+ },
15
+ };
16
+ const composerState = {
17
+ text: "",
18
+ runConfig: { custom: { model: "gpt-test" } },
19
+ };
20
+
21
+ return {
22
+ append,
23
+ setText,
24
+ state,
25
+ composerState,
26
+ aui: {
27
+ thread: {
28
+ getState: () => state.thread,
29
+ append,
30
+ },
31
+ composer: {
32
+ getState: () => composerState,
33
+ setText,
34
+ },
35
+ },
36
+ };
37
+ });
38
+
39
+ vi.mock("@assistant-ui/store", async (importOriginal) => ({
40
+ ...(await importOriginal<typeof import("@assistant-ui/store")>()),
41
+ useAui: () => mocks.aui,
42
+ useAuiState: ((selector: (state: typeof mocks.state) => unknown) =>
43
+ selector(mocks.state)) as typeof import("@assistant-ui/store").useAuiState,
44
+ }));
45
+
46
+ import { useSuggestionTrigger } from "./useSuggestionTrigger";
47
+
48
+ afterEach(() => {
49
+ cleanup();
50
+ vi.clearAllMocks();
51
+ mocks.state.thread.isDisabled = false;
52
+ mocks.state.thread.isRunning = false;
53
+ mocks.state.thread.capabilities = { queue: false };
54
+ mocks.composerState.text = "";
55
+ });
56
+
57
+ describe("useSuggestionTrigger", () => {
58
+ it("appends the prompt and clears the composer when sending while idle", () => {
59
+ const { result } = renderHook(() =>
60
+ useSuggestionTrigger({ prompt: "Hello", send: true }),
61
+ );
62
+
63
+ result.current.trigger();
64
+
65
+ expect(result.current.disabled).toBe(false);
66
+ expect(mocks.append).toHaveBeenCalledWith({
67
+ content: [{ type: "text", text: "Hello" }],
68
+ runConfig: { custom: { model: "gpt-test" } },
69
+ });
70
+ expect(mocks.setText).toHaveBeenCalledWith("");
71
+ });
72
+
73
+ it("disables and no-ops when sending while running without queue support", () => {
74
+ mocks.state.thread.isRunning = true;
75
+ mocks.composerState.text = "my draft";
76
+ const { result } = renderHook(() =>
77
+ useSuggestionTrigger({ prompt: "Hello", send: true }),
78
+ );
79
+
80
+ result.current.trigger();
81
+
82
+ expect(result.current.disabled).toBe(true);
83
+ expect(mocks.append).not.toHaveBeenCalled();
84
+ expect(mocks.setText).not.toHaveBeenCalled();
85
+ });
86
+
87
+ it("queues without touching the composer while running when the thread supports queueing", () => {
88
+ mocks.state.thread.isRunning = true;
89
+ mocks.state.thread.capabilities = { queue: true };
90
+ mocks.composerState.text = "my draft";
91
+ const { result } = renderHook(() =>
92
+ useSuggestionTrigger({ prompt: "Hello", send: true }),
93
+ );
94
+
95
+ result.current.trigger();
96
+
97
+ expect(result.current.disabled).toBe(false);
98
+ expect(mocks.append).toHaveBeenCalledWith({
99
+ content: [{ type: "text", text: "Hello" }],
100
+ runConfig: { custom: { model: "gpt-test" } },
101
+ });
102
+ expect(mocks.setText).not.toHaveBeenCalled();
103
+ });
104
+
105
+ it("replaces the composer text when send is false, even while running", () => {
106
+ mocks.state.thread.isRunning = true;
107
+ mocks.composerState.text = "my draft";
108
+ const { result } = renderHook(() =>
109
+ useSuggestionTrigger({ prompt: "Hello" }),
110
+ );
111
+
112
+ result.current.trigger();
113
+
114
+ expect(result.current.disabled).toBe(false);
115
+ expect(mocks.append).not.toHaveBeenCalled();
116
+ expect(mocks.setText).toHaveBeenCalledWith("Hello");
117
+ });
118
+
119
+ it("appends to the composer text when clearComposer is false", () => {
120
+ mocks.composerState.text = "my draft";
121
+ const { result } = renderHook(() =>
122
+ useSuggestionTrigger({ prompt: "Hello", clearComposer: false }),
123
+ );
124
+
125
+ result.current.trigger();
126
+
127
+ expect(mocks.setText).toHaveBeenCalledWith("my draft Hello");
128
+ });
129
+
130
+ it("inserts the prompt alone when the composer is empty and clearComposer is false", () => {
131
+ const { result } = renderHook(() =>
132
+ useSuggestionTrigger({ prompt: "Hello", clearComposer: false }),
133
+ );
134
+
135
+ result.current.trigger();
136
+
137
+ expect(mocks.setText).toHaveBeenCalledWith("Hello");
138
+ });
139
+
140
+ it("disables when the thread is disabled", () => {
141
+ mocks.state.thread.isDisabled = true;
142
+ const { result } = renderHook(() =>
143
+ useSuggestionTrigger({ prompt: "Hello" }),
144
+ );
145
+
146
+ expect(result.current.disabled).toBe(true);
147
+ });
148
+ });
@@ -13,18 +13,24 @@ export const useSuggestionTrigger = ({
13
13
  clearComposer = true,
14
14
  }: UseSuggestionTriggerOptions) => {
15
15
  const aui = useAui();
16
- const disabled = useAuiState((s) => s.thread.isDisabled);
17
16
  const resolvedSend = send ?? false;
17
+ const disabled = useAuiState(
18
+ (s) =>
19
+ s.thread.isDisabled ||
20
+ (resolvedSend && s.thread.isRunning && !s.thread.capabilities.queue),
21
+ );
18
22
 
19
23
  const trigger = useCallback(() => {
20
- const isRunning = aui.thread.getState().isRunning;
24
+ if (resolvedSend) {
25
+ const { isRunning, capabilities } = aui.thread.getState();
26
+ if (isRunning && !capabilities.queue) return;
21
27
 
22
- if (resolvedSend && !isRunning) {
23
28
  aui.thread.append({
24
29
  content: [{ type: "text", text: prompt }],
25
30
  runConfig: aui.composer.getState().runConfig,
26
31
  });
27
- if (clearComposer) {
32
+ // A queued send must not clear the draft the user is still composing.
33
+ if (clearComposer && !isRunning) {
28
34
  aui.composer.setText("");
29
35
  }
30
36
  } else {
@@ -214,7 +214,12 @@ export namespace MessagePrimitiveParts {
214
214
  Image?: ImageMessagePartComponent | undefined;
215
215
  /** Component for rendering file content */
216
216
  File?: FileMessagePartComponent | undefined;
217
- /** Component for rendering audio content (experimental) */
217
+ /**
218
+ * Component for rendering audio content.
219
+ *
220
+ * @deprecated Render audio through the `File` slot instead, branching on an
221
+ * `audio/*` mime type.
222
+ */
218
223
  Unstable_Audio?: Unstable_AudioMessagePartComponent | undefined;
219
224
  /** Configuration for data part rendering */
220
225
  data?: DataConfig | undefined;
@@ -26,6 +26,7 @@ const READONLY_THREAD_LIST_ITEM: ThreadListItemState = Object.freeze({
26
26
  remoteId: undefined,
27
27
  externalId: undefined,
28
28
  isMain: true,
29
+ isRunning: false,
29
30
  status: "regular" as const,
30
31
  title: undefined,
31
32
  });
@@ -0,0 +1,157 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { ThreadListRuntimeCore } from "../../runtime/interfaces/thread-list-runtime-core";
3
+ import type { ThreadRuntimeCore } from "../../runtime/interfaces/thread-runtime-core";
4
+ import type { ThreadMessage } from "../../types/message";
5
+ import { RemoteThreadListHookInstanceManager } from "./RemoteThreadListHookInstanceManager";
6
+
7
+ const makeRuntime = (
8
+ initial: Partial<Pick<ThreadRuntimeCore, "isRunning" | "messages">> = {},
9
+ ) => {
10
+ const subscribers = new Set<() => void>();
11
+ const runtime = {
12
+ isRunning: initial.isRunning,
13
+ messages: initial.messages ?? [],
14
+ subscribe: (callback: () => void) => {
15
+ subscribers.add(callback);
16
+ return () => subscribers.delete(callback);
17
+ },
18
+ } as unknown as ThreadRuntimeCore & {
19
+ isRunning: boolean | undefined;
20
+ messages: readonly ThreadMessage[];
21
+ };
22
+
23
+ return {
24
+ runtime,
25
+ subscriberCount: () => subscribers.size,
26
+ setRunning: (isRunning: boolean | undefined) => {
27
+ runtime.isRunning = isRunning;
28
+ for (const callback of subscribers) callback();
29
+ },
30
+ };
31
+ };
32
+
33
+ const makeManager = () =>
34
+ new RemoteThreadListHookInstanceManager(
35
+ () => ({}) as never,
36
+ {} as ThreadListRuntimeCore,
37
+ );
38
+
39
+ // mirrors what the React binder does on every publication
40
+ const publish = (
41
+ manager: RemoteThreadListHookInstanceManager,
42
+ threadId: string,
43
+ runtime: ThreadRuntimeCore,
44
+ ) => {
45
+ const internals = manager as unknown as {
46
+ instances: Map<string, { generation: number }>;
47
+ _publishThreadRuntime: (
48
+ threadId: string,
49
+ runtime: ThreadRuntimeCore,
50
+ generation: number,
51
+ ) => void;
52
+ };
53
+ const generation = internals.instances.get(threadId)!.generation;
54
+ internals._publishThreadRuntime(threadId, runtime, generation);
55
+ };
56
+
57
+ const start = (manager: RemoteThreadListHookInstanceManager, id: string) => {
58
+ manager.startThreadRuntime(id).catch(() => {});
59
+ };
60
+
61
+ describe("RemoteThreadListHookInstanceManager run tracking", () => {
62
+ it("reports a thread with no attached runtime as not running", () => {
63
+ const manager = makeManager();
64
+ start(manager, "thread-1");
65
+
66
+ expect(manager.__internal_isThreadRunning("thread-1")).toBe(false);
67
+ expect(manager.__internal_isThreadRunning("never-started")).toBe(false);
68
+ });
69
+
70
+ it("adopts the run state of the published runtime", () => {
71
+ const manager = makeManager();
72
+ start(manager, "thread-1");
73
+ const { runtime } = makeRuntime({ isRunning: true });
74
+
75
+ publish(manager, "thread-1", runtime);
76
+
77
+ expect(manager.__internal_isThreadRunning("thread-1")).toBe(true);
78
+ });
79
+
80
+ it("falls back to the trailing assistant message when the runtime does not track runs", () => {
81
+ const manager = makeManager();
82
+ start(manager, "thread-1");
83
+ const { runtime } = makeRuntime({
84
+ messages: [
85
+ { role: "assistant", status: { type: "running" } },
86
+ ] as unknown as readonly ThreadMessage[],
87
+ });
88
+
89
+ publish(manager, "thread-1", runtime);
90
+
91
+ expect(manager.__internal_isThreadRunning("thread-1")).toBe(true);
92
+ });
93
+
94
+ it("tracks a thread the user has switched away from", () => {
95
+ const manager = makeManager();
96
+ start(manager, "background");
97
+ start(manager, "main");
98
+ const background = makeRuntime({ isRunning: false });
99
+ publish(manager, "background", background.runtime);
100
+ publish(manager, "main", makeRuntime({ isRunning: false }).runtime);
101
+
102
+ background.setRunning(true);
103
+
104
+ expect(manager.__internal_isThreadRunning("background")).toBe(true);
105
+ expect(manager.__internal_isThreadRunning("main")).toBe(false);
106
+ });
107
+
108
+ it("notifies the thread list only when the thread crosses the running boundary", () => {
109
+ const manager = makeManager();
110
+ start(manager, "thread-1");
111
+ const thread = makeRuntime({ isRunning: false });
112
+ publish(manager, "thread-1", thread.runtime);
113
+
114
+ const onChange = vi.fn();
115
+ manager.__internal_subscribeRunningChanged(onChange);
116
+
117
+ thread.setRunning(false);
118
+ expect(onChange).not.toHaveBeenCalled();
119
+
120
+ thread.setRunning(true);
121
+ expect(onChange).toHaveBeenCalledTimes(1);
122
+
123
+ thread.setRunning(true);
124
+ expect(onChange).toHaveBeenCalledTimes(1);
125
+
126
+ thread.setRunning(false);
127
+ expect(onChange).toHaveBeenCalledTimes(2);
128
+ });
129
+
130
+ it("moves tracking to the runtime a restart publishes", () => {
131
+ const manager = makeManager();
132
+ start(manager, "thread-1");
133
+ const before = makeRuntime({ isRunning: true });
134
+ publish(manager, "thread-1", before.runtime);
135
+
136
+ const after = makeRuntime({ isRunning: false });
137
+ publish(manager, "thread-1", after.runtime);
138
+
139
+ expect(manager.__internal_isThreadRunning("thread-1")).toBe(false);
140
+ expect(before.subscriberCount()).toBe(0);
141
+
142
+ after.setRunning(true);
143
+ expect(manager.__internal_isThreadRunning("thread-1")).toBe(true);
144
+ });
145
+
146
+ it("releases the run subscription when the thread runtime stops", () => {
147
+ const manager = makeManager();
148
+ start(manager, "thread-1");
149
+ const thread = makeRuntime({ isRunning: true });
150
+ publish(manager, "thread-1", thread.runtime);
151
+
152
+ manager.stopThreadRuntime("thread-1");
153
+
154
+ expect(thread.subscriberCount()).toBe(0);
155
+ expect(manager.__internal_isThreadRunning("thread-1")).toBe(false);
156
+ });
157
+ });