@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.
- package/dist/internal.d.ts +4 -2
- package/dist/internal.js +4 -2
- package/dist/react/primitive-hooks/useSuggestionTrigger.js +29 -25
- package/dist/react/primitive-hooks/useSuggestionTrigger.js.map +1 -1
- package/dist/react/primitives/message/MessageParts.d.ts +6 -1
- package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
- package/dist/react/primitives/message/MessageParts.js.map +1 -1
- package/dist/react/providers/ReadonlyThreadProvider.d.ts.map +1 -1
- package/dist/react/providers/ReadonlyThreadProvider.js +1 -0
- package/dist/react/providers/ReadonlyThreadProvider.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +67 -0
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +76 -15
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +4 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +21 -0
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.d.ts +2 -0
- package/dist/react/runtimes/cloud/auiV0.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/auiV0.js +6 -4
- package/dist/react/runtimes/cloud/auiV0.js.map +1 -1
- package/dist/react/types/MessagePartComponentTypes.d.ts +8 -0
- package/dist/react/types/MessagePartComponentTypes.d.ts.map +1 -1
- package/dist/runtime/api/bindings.d.ts +5 -0
- package/dist/runtime/api/bindings.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-runtime.d.ts +14 -1
- package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-runtime.js +6 -1
- package/dist/runtime/api/thread-list-runtime.js.map +1 -1
- package/dist/runtime/api/thread-runtime.d.ts +7 -1
- package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-runtime.js +11 -3
- package/dist/runtime/api/thread-runtime.js.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts +3 -0
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js +54 -12
- package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
- package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +10 -0
- package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtime/interfaces/thread-runtime-core.d.ts +14 -0
- package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-adapter.d.ts +9 -0
- package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +1 -0
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +4 -0
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +1 -0
- package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js +6 -0
- package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.d.ts +1 -0
- package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/local/local-thread-runtime-core.js +1 -0
- package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +1 -0
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +1 -0
- package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
- package/dist/runtimes/remote-thread-list/empty-thread-core.js +1 -0
- package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts +1 -0
- package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js +66 -45
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-runtime-client.js +91 -75
- package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
- package/dist/store/scopes/thread-list-item.d.ts +7 -0
- package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
- package/dist/store/scopes/threads.d.ts +1 -0
- package/dist/store/scopes/threads.d.ts.map +1 -1
- package/dist/types/message.d.ts +11 -0
- package/dist/types/message.d.ts.map +1 -1
- package/dist/types/message.js.map +1 -1
- package/dist/utils/data-url.d.ts +14 -1
- package/dist/utils/data-url.d.ts.map +1 -1
- package/dist/utils/data-url.js +23 -1
- package/dist/utils/data-url.js.map +1 -1
- package/dist/utils/image-media-type.d.ts +13 -0
- package/dist/utils/image-media-type.d.ts.map +1 -0
- package/dist/utils/image-media-type.js +141 -0
- package/dist/utils/image-media-type.js.map +1 -0
- package/dist/utils/wire-media.d.ts +35 -0
- package/dist/utils/wire-media.d.ts.map +1 -0
- package/dist/utils/wire-media.js +55 -0
- package/dist/utils/wire-media.js.map +1 -0
- package/package.json +3 -3
- package/src/internal.ts +12 -1
- package/src/react/primitive-hooks/useSuggestionTrigger.test.ts +148 -0
- package/src/react/primitive-hooks/useSuggestionTrigger.ts +10 -4
- package/src/react/primitives/message/MessageParts.tsx +6 -1
- package/src/react/providers/ReadonlyThreadProvider.tsx +1 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.running.test.tsx +157 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.test.tsx +160 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +135 -32
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +37 -0
- package/src/react/runtimes/cloud/auiV0.ts +8 -2
- package/src/react/types/MessagePartComponentTypes.ts +8 -0
- package/src/runtime/api/bindings.ts +5 -0
- package/src/runtime/api/thread-list-runtime.ts +22 -1
- package/src/runtime/api/thread-runtime.ts +15 -6
- package/src/runtime/base/base-composer-runtime-core.ts +72 -19
- package/src/runtime/interfaces/thread-list-runtime-core.ts +11 -0
- package/src/runtime/interfaces/thread-runtime-core.ts +15 -0
- package/src/runtimes/external-store/external-store-adapter.ts +9 -0
- package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +9 -0
- package/src/runtimes/external-store/external-store-thread-runtime-core.ts +8 -0
- package/src/runtimes/local/local-thread-runtime-core.ts +1 -0
- package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +1 -0
- package/src/runtimes/remote-thread-list/empty-thread-core.ts +1 -0
- package/src/store/runtime-clients/thread-list-item-runtime-client.ts +13 -2
- package/src/store/runtime-clients/thread-list-runtime-client.ts +13 -1
- package/src/store/scopes/thread-list-item.ts +7 -0
- package/src/store/scopes/threads.ts +1 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-reloadMainThread.test.ts +257 -0
- package/src/tests/auiV0Encode.test.ts +125 -1
- package/src/tests/base-composer-runtime-core-addAttachment.test.ts +296 -0
- package/src/tests/event-subscription-listener-errors.test.ts +1 -0
- package/src/tests/external-store-thread-list-runtime-core.test.ts +39 -1
- package/src/types/message.ts +11 -0
- package/src/utils/data-url.ts +24 -0
- package/src/utils/image-media-type.test.ts +96 -0
- package/src/utils/image-media-type.ts +118 -0
- package/src/utils/wire-media.test.ts +96 -0
- 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
|
+
"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.
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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;
|
|
@@ -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
|
+
});
|