@assistant-ui/react 0.12.26 → 0.12.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.
- package/dist/primitives/message/MessageIf.js +1 -1
- package/dist/primitives/message/MessageIf.js.map +1 -1
- package/package.json +5 -5
- package/src/primitives/message/MessageIf.ts +1 -1
- package/src/tests/BaseComposerRuntimeCore.test.ts +143 -0
- package/src/tests/MessageParts.loading.test.tsx +104 -0
- package/src/tests/RemoteThreadListRuntime.deferredProvider.test.tsx +79 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageIf.js","sourceRoot":"","sources":["../../../src/primitives/message/MessageIf.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAGb,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAkBlD,MAAM,YAAY,GAAG,CAAC,KAAwB,EAAE,EAAE;IAChD,OAAO,WAAW,CAAC,CAAC,CAAC,EAAE,EAAE;QACvB,MAAM,EACJ,IAAI,EACJ,WAAW,EACX,KAAK,EACL,WAAW,EACX,MAAM,EACN,MAAM,EACN,QAAQ,EACR,UAAU,GACX,GAAG,CAAC,CAAC,OAAO,CAAC;QAEd,IAAI,KAAK,CAAC,WAAW,KAAK,IAAI,IAAI,WAAW,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QAEhE,IAAI,KAAK,CAAC,IAAI,IAAI,IAAI,KAAK,MAAM;YAAE,OAAO,KAAK,CAAC;QAChD,IAAI,KAAK,CAAC,SAAS,IAAI,IAAI,KAAK,WAAW;YAAE,OAAO,KAAK,CAAC;QAC1D,IAAI,KAAK,CAAC,MAAM,IAAI,IAAI,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAEpD,IAAI,KAAK,CAAC,WAAW,KAAK,IAAI,IAAI,CAAC,UAAU,IAAI,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QACvE,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM;YAAE,OAAO,KAAK,CAAC;QAEpE,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO,KAAK,CAAC;QACrD,IAAI,KAAK,CAAC,MAAM,KAAK,KAAK,IAAI,QAAQ;YAAE,OAAO,KAAK,CAAC;QAErD,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI,IAAI,MAAM,IAAI,IAAI;YAAE,OAAO,KAAK,CAAC;QAC5D,IAAI,KAAK,CAAC,QAAQ,KAAK,KAAK,IAAI,MAAM,IAAI,IAAI;YAAE,OAAO,KAAK,CAAC;QAE7D,IACE,KAAK,CAAC,cAAc,KAAK,IAAI;YAC7B,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC;YAEzC,OAAO,KAAK,CAAC;QACf,IACE,KAAK,CAAC,cAAc,KAAK,KAAK;YAC9B,IAAI,KAAK,MAAM;YACf,
|
|
1
|
+
{"version":3,"file":"MessageIf.js","sourceRoot":"","sources":["../../../src/primitives/message/MessageIf.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAGb,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAkBlD,MAAM,YAAY,GAAG,CAAC,KAAwB,EAAE,EAAE;IAChD,OAAO,WAAW,CAAC,CAAC,CAAC,EAAE,EAAE;QACvB,MAAM,EACJ,IAAI,EACJ,WAAW,EACX,KAAK,EACL,WAAW,EACX,MAAM,EACN,MAAM,EACN,QAAQ,EACR,UAAU,GACX,GAAG,CAAC,CAAC,OAAO,CAAC;QAEd,IAAI,KAAK,CAAC,WAAW,KAAK,IAAI,IAAI,WAAW,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QAEhE,IAAI,KAAK,CAAC,IAAI,IAAI,IAAI,KAAK,MAAM;YAAE,OAAO,KAAK,CAAC;QAChD,IAAI,KAAK,CAAC,SAAS,IAAI,IAAI,KAAK,WAAW;YAAE,OAAO,KAAK,CAAC;QAC1D,IAAI,KAAK,CAAC,MAAM,IAAI,IAAI,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAEpD,IAAI,KAAK,CAAC,WAAW,KAAK,IAAI,IAAI,CAAC,UAAU,IAAI,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QACvE,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM;YAAE,OAAO,KAAK,CAAC;QAEpE,IAAI,KAAK,CAAC,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ;YAAE,OAAO,KAAK,CAAC;QACrD,IAAI,KAAK,CAAC,MAAM,KAAK,KAAK,IAAI,QAAQ;YAAE,OAAO,KAAK,CAAC;QAErD,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI,IAAI,MAAM,IAAI,IAAI;YAAE,OAAO,KAAK,CAAC;QAC5D,IAAI,KAAK,CAAC,QAAQ,KAAK,KAAK,IAAI,MAAM,IAAI,IAAI;YAAE,OAAO,KAAK,CAAC;QAE7D,IACE,KAAK,CAAC,cAAc,KAAK,IAAI;YAC7B,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC;YAEzC,OAAO,KAAK,CAAC;QACf,IACE,KAAK,CAAC,cAAc,KAAK,KAAK;YAC9B,IAAI,KAAK,MAAM;YACf,WAAW,EAAE,MAAM;YAEnB,OAAO,KAAK,CAAC;QAEf,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QAClE,IAAI,KAAK,CAAC,UAAU,KAAK,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QAEjE,IACE,KAAK,CAAC,iBAAiB,KAAK,SAAS;YACrC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,EAAE,IAAI,IAAI,IAAI,CAAC;gBAClD,KAAK,CAAC,iBAAiB;YAEzB,OAAO,KAAK,CAAC;QAEf,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC,CAAC;AAMF;;GAEG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAiC,CAAC,EAC/D,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,EAAE;IACH,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IACnC,OAAO,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;AAClC,CAAC,CAAC;AAEF,kBAAkB,CAAC,WAAW,GAAG,qBAAqB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.27",
|
|
4
4
|
"description": "TypeScript/React library for AI Chat",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"radix-ui",
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
],
|
|
49
49
|
"sideEffects": false,
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@assistant-ui/core": "^0.1.
|
|
52
|
-
"@assistant-ui/store": "^0.2.
|
|
53
|
-
"@assistant-ui/tap": "^0.5.
|
|
51
|
+
"@assistant-ui/core": "^0.1.16",
|
|
52
|
+
"@assistant-ui/store": "^0.2.9",
|
|
53
|
+
"@assistant-ui/tap": "^0.5.10",
|
|
54
54
|
"@radix-ui/primitive": "^1.1.3",
|
|
55
55
|
"@radix-ui/react-compose-refs": "^1.1.2",
|
|
56
56
|
"@radix-ui/react-context": "^1.1.3",
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"@types/node": "^25.6.0",
|
|
86
86
|
"@types/react": "^19.2.14",
|
|
87
87
|
"@types/react-dom": "^19.2.3",
|
|
88
|
-
"jsdom": "^29.0
|
|
88
|
+
"jsdom": "^29.1.0",
|
|
89
89
|
"react": "^19.2.5",
|
|
90
90
|
"react-dom": "^19.2.5",
|
|
91
91
|
"vitest": "^4.1.5",
|
|
@@ -426,6 +426,149 @@ describe("BaseComposerRuntimeCore", () => {
|
|
|
426
426
|
// Pending was sent through adapter
|
|
427
427
|
expect(adapter.send).toHaveBeenCalledTimes(1);
|
|
428
428
|
});
|
|
429
|
+
|
|
430
|
+
describe("adapter.accept enforcement", () => {
|
|
431
|
+
const makeImageAdapter = (): AttachmentAdapter => ({
|
|
432
|
+
accept: "image/*",
|
|
433
|
+
add: vi.fn(),
|
|
434
|
+
remove: vi.fn(),
|
|
435
|
+
send: vi.fn(),
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
it("rejects external attachment with contentType not matching adapter.accept", async () => {
|
|
439
|
+
composer.setAttachmentAdapter(makeImageAdapter());
|
|
440
|
+
const onError = vi.fn();
|
|
441
|
+
const onAdd = vi.fn();
|
|
442
|
+
composer.unstable_on("attachmentAddError", onError);
|
|
443
|
+
composer.unstable_on("attachmentAdd", onAdd);
|
|
444
|
+
|
|
445
|
+
await expect(
|
|
446
|
+
composer.addAttachment(
|
|
447
|
+
makeCreateAttachment({
|
|
448
|
+
name: "doc.pdf",
|
|
449
|
+
contentType: "application/pdf",
|
|
450
|
+
}),
|
|
451
|
+
),
|
|
452
|
+
).rejects.toThrow(/File type application\/pdf is not accepted/);
|
|
453
|
+
|
|
454
|
+
expect(composer.attachments).toHaveLength(0);
|
|
455
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
456
|
+
expect(onAdd).not.toHaveBeenCalled();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it("accepts external attachment with contentType matching adapter.accept", async () => {
|
|
460
|
+
composer.setAttachmentAdapter(makeImageAdapter());
|
|
461
|
+
const onError = vi.fn();
|
|
462
|
+
|
|
463
|
+
composer.unstable_on("attachmentAddError", onError);
|
|
464
|
+
await composer.addAttachment(
|
|
465
|
+
makeCreateAttachment({
|
|
466
|
+
name: "photo.png",
|
|
467
|
+
contentType: "image/png",
|
|
468
|
+
}),
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
expect(composer.attachments).toHaveLength(1);
|
|
472
|
+
expect(onError).not.toHaveBeenCalled();
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it("accepts external attachment when only filename extension matches adapter.accept", async () => {
|
|
476
|
+
composer.setAttachmentAdapter({
|
|
477
|
+
accept: ".pdf",
|
|
478
|
+
add: vi.fn(),
|
|
479
|
+
remove: vi.fn(),
|
|
480
|
+
send: vi.fn(),
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
await composer.addAttachment(
|
|
484
|
+
makeCreateAttachment({ name: "report.pdf" }),
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
expect(composer.attachments).toHaveLength(1);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("rejects external attachment when contentType missing and extension does not match", async () => {
|
|
491
|
+
composer.setAttachmentAdapter(makeImageAdapter());
|
|
492
|
+
const onError = vi.fn();
|
|
493
|
+
composer.unstable_on("attachmentAddError", onError);
|
|
494
|
+
|
|
495
|
+
await expect(
|
|
496
|
+
composer.addAttachment(makeCreateAttachment({ name: "notes.txt" })),
|
|
497
|
+
).rejects.toThrow(/is not accepted/);
|
|
498
|
+
|
|
499
|
+
expect(onError).toHaveBeenCalledTimes(1);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it("adds external attachment without check when no adapter is configured", async () => {
|
|
503
|
+
// No adapter — preserves the original "no adapter required" guarantee
|
|
504
|
+
await composer.addAttachment(
|
|
505
|
+
makeCreateAttachment({
|
|
506
|
+
name: "anything.xyz",
|
|
507
|
+
contentType: "application/x-anything",
|
|
508
|
+
}),
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
expect(composer.attachments).toHaveLength(1);
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it("does not let subscriber errors mask the original throw", async () => {
|
|
515
|
+
composer.setAttachmentAdapter(makeImageAdapter());
|
|
516
|
+
composer.unstable_on("attachmentAddError", () => {
|
|
517
|
+
throw new Error("subscriber boom");
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
await expect(
|
|
521
|
+
composer.addAttachment(
|
|
522
|
+
makeCreateAttachment({
|
|
523
|
+
name: "doc.pdf",
|
|
524
|
+
contentType: "application/pdf",
|
|
525
|
+
}),
|
|
526
|
+
),
|
|
527
|
+
).rejects.toThrow(/is not accepted/);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("does not fire attachmentAddError when an attachmentAdd subscriber throws", async () => {
|
|
531
|
+
composer.setAttachmentAdapter(makeImageAdapter());
|
|
532
|
+
const onError = vi.fn();
|
|
533
|
+
composer.unstable_on("attachmentAdd", () => {
|
|
534
|
+
throw new Error("add subscriber boom");
|
|
535
|
+
});
|
|
536
|
+
composer.unstable_on("attachmentAddError", onError);
|
|
537
|
+
|
|
538
|
+
await expect(
|
|
539
|
+
composer.addAttachment(
|
|
540
|
+
makeCreateAttachment({
|
|
541
|
+
name: "photo.png",
|
|
542
|
+
contentType: "image/png",
|
|
543
|
+
}),
|
|
544
|
+
),
|
|
545
|
+
).rejects.toThrow("add subscriber boom");
|
|
546
|
+
|
|
547
|
+
expect(composer.attachments).toHaveLength(1);
|
|
548
|
+
expect(onError).not.toHaveBeenCalled();
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("does not fire attachmentAddError when a state subscriber throws on add", async () => {
|
|
552
|
+
composer.setAttachmentAdapter(makeImageAdapter());
|
|
553
|
+
const onError = vi.fn();
|
|
554
|
+
composer.subscribe(() => {
|
|
555
|
+
throw new Error("state subscriber boom");
|
|
556
|
+
});
|
|
557
|
+
composer.unstable_on("attachmentAddError", onError);
|
|
558
|
+
|
|
559
|
+
await expect(
|
|
560
|
+
composer.addAttachment(
|
|
561
|
+
makeCreateAttachment({
|
|
562
|
+
name: "photo.png",
|
|
563
|
+
contentType: "image/png",
|
|
564
|
+
}),
|
|
565
|
+
),
|
|
566
|
+
).rejects.toThrow("state subscriber boom");
|
|
567
|
+
|
|
568
|
+
expect(composer.attachments).toHaveLength(1);
|
|
569
|
+
expect(onError).not.toHaveBeenCalled();
|
|
570
|
+
});
|
|
571
|
+
});
|
|
429
572
|
});
|
|
430
573
|
|
|
431
574
|
describe("send options", () => {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import type { FC, PropsWithChildren } from "react";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { AssistantRuntimeProvider } from "../context";
|
|
7
|
+
import * as MessagePartPrimitive from "../primitives/messagePart";
|
|
8
|
+
import * as MessagePrimitive from "../primitives/message";
|
|
9
|
+
import * as ThreadPrimitive from "../primitives/thread";
|
|
10
|
+
import { useLocalRuntime } from "../legacy-runtime/runtime-cores/local/useLocalRuntime";
|
|
11
|
+
import type { ChatModelAdapter, ThreadMessageLike } from "../index";
|
|
12
|
+
|
|
13
|
+
const noOpAdapter: ChatModelAdapter = {
|
|
14
|
+
async *run() {},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const initialMessages: ThreadMessageLike[] = [
|
|
18
|
+
{
|
|
19
|
+
role: "assistant",
|
|
20
|
+
content: [],
|
|
21
|
+
status: { type: "running" },
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const completeInitialMessages: ThreadMessageLike[] = [
|
|
26
|
+
{
|
|
27
|
+
role: "assistant",
|
|
28
|
+
content: [],
|
|
29
|
+
status: { type: "complete", reason: "stop" },
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const RunningText: FC = () => {
|
|
34
|
+
return (
|
|
35
|
+
<p>
|
|
36
|
+
<MessagePartPrimitive.Text />
|
|
37
|
+
<MessagePartPrimitive.InProgress>
|
|
38
|
+
<span data-testid="loading-dot">dot</span>
|
|
39
|
+
</MessagePartPrimitive.InProgress>
|
|
40
|
+
</p>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const ComponentsMessage: FC = () => {
|
|
45
|
+
return <MessagePrimitive.Parts components={{ Text: RunningText }} />;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const ChildrenMessage: FC = () => {
|
|
49
|
+
return (
|
|
50
|
+
<MessagePrimitive.Parts>
|
|
51
|
+
{({ part }) => {
|
|
52
|
+
if (part.type === "text") return <RunningText />;
|
|
53
|
+
return null;
|
|
54
|
+
}}
|
|
55
|
+
</MessagePrimitive.Parts>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const RuntimeProvider: FC<
|
|
60
|
+
PropsWithChildren<{ messages?: ThreadMessageLike[] }>
|
|
61
|
+
> = ({ children, messages = initialMessages }) => {
|
|
62
|
+
const runtime = useLocalRuntime(noOpAdapter, {
|
|
63
|
+
initialMessages: messages,
|
|
64
|
+
});
|
|
65
|
+
return (
|
|
66
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
67
|
+
{children}
|
|
68
|
+
</AssistantRuntimeProvider>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const renderThread = (MessageComponent: FC, messages?: ThreadMessageLike[]) => {
|
|
73
|
+
render(
|
|
74
|
+
<RuntimeProvider messages={messages}>
|
|
75
|
+
<ThreadPrimitive.Messages components={{ Message: MessageComponent }} />
|
|
76
|
+
</RuntimeProvider>,
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
describe("MessagePrimitive.Parts loading state", () => {
|
|
81
|
+
it("renders the loading indicator for the components API when assistant parts are empty", async () => {
|
|
82
|
+
renderThread(ComponentsMessage);
|
|
83
|
+
|
|
84
|
+
await waitFor(() => {
|
|
85
|
+
expect(screen.getByTestId("loading-dot")).toBeTruthy();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("renders the loading indicator for the children API when assistant parts are empty", async () => {
|
|
90
|
+
renderThread(ChildrenMessage);
|
|
91
|
+
|
|
92
|
+
await waitFor(() => {
|
|
93
|
+
expect(screen.getByTestId("loading-dot")).toBeTruthy();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("does not render the loading indicator when assistant parts are empty but the message is complete", async () => {
|
|
98
|
+
renderThread(ChildrenMessage, completeInitialMessages);
|
|
99
|
+
|
|
100
|
+
await waitFor(() => {
|
|
101
|
+
expect(screen.queryByTestId("loading-dot")).toBeNull();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { render } from "@testing-library/react";
|
|
4
|
+
import { type FC, type PropsWithChildren, useState } from "react";
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { useRemoteThreadListRuntime } from "@assistant-ui/core/react";
|
|
7
|
+
import { makeAdapter } from "@assistant-ui/core/tests/remote-thread-list-test-helpers";
|
|
8
|
+
import type { AssistantRuntime } from "@assistant-ui/core";
|
|
9
|
+
import { AssistantRuntimeProvider } from "../context";
|
|
10
|
+
import { useLocalRuntime } from "../legacy-runtime/runtime-cores/local/useLocalRuntime";
|
|
11
|
+
import { useAssistantRuntime } from "../legacy-runtime/hooks/AssistantContext";
|
|
12
|
+
import type { ChatModelAdapter, RemoteThreadListAdapter } from "../index";
|
|
13
|
+
|
|
14
|
+
const noOpAdapter: ChatModelAdapter = {
|
|
15
|
+
async *run() {},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const useTestRuntimeHook = () => useLocalRuntime(noOpAdapter);
|
|
19
|
+
|
|
20
|
+
const RuntimeProvider: FC<
|
|
21
|
+
PropsWithChildren<{ adapter: RemoteThreadListAdapter }>
|
|
22
|
+
> = ({ children, adapter }) => {
|
|
23
|
+
const runtime = useRemoteThreadListRuntime({
|
|
24
|
+
runtimeHook: useTestRuntimeHook,
|
|
25
|
+
adapter,
|
|
26
|
+
});
|
|
27
|
+
return (
|
|
28
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
29
|
+
{children}
|
|
30
|
+
</AssistantRuntimeProvider>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const RuntimeCapture: FC<{
|
|
35
|
+
runtimeRef: { current: AssistantRuntime | null };
|
|
36
|
+
}> = ({ runtimeRef }) => {
|
|
37
|
+
const runtime = useAssistantRuntime();
|
|
38
|
+
runtimeRef.current = runtime;
|
|
39
|
+
return null;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// regression for #3678: deferred unstable_Provider must not strand the runtime binder.
|
|
43
|
+
describe("useRemoteThreadListRuntime with a deferred unstable_Provider", () => {
|
|
44
|
+
it("composer.setText does not throw EMPTY_THREAD_ERROR when unstable_Provider defers children", () => {
|
|
45
|
+
const Provider: FC<PropsWithChildren> = ({ children }) => {
|
|
46
|
+
const [ready] = useState(false);
|
|
47
|
+
if (!ready) return null;
|
|
48
|
+
return <>{children}</>;
|
|
49
|
+
};
|
|
50
|
+
const adapter = makeAdapter({ unstable_Provider: Provider });
|
|
51
|
+
|
|
52
|
+
const runtimeRef: { current: AssistantRuntime | null } = { current: null };
|
|
53
|
+
render(
|
|
54
|
+
<RuntimeProvider adapter={adapter}>
|
|
55
|
+
<RuntimeCapture runtimeRef={runtimeRef} />
|
|
56
|
+
</RuntimeProvider>,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const runtime = runtimeRef.current;
|
|
60
|
+
expect(runtime).toBeTruthy();
|
|
61
|
+
expect(() => runtime!.thread.composer.setText("hello")).not.toThrow();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("composer.setText still works when unstable_Provider renders children unconditionally", () => {
|
|
65
|
+
const Provider: FC<PropsWithChildren> = ({ children }) => <>{children}</>;
|
|
66
|
+
const adapter = makeAdapter({ unstable_Provider: Provider });
|
|
67
|
+
|
|
68
|
+
const runtimeRef: { current: AssistantRuntime | null } = { current: null };
|
|
69
|
+
render(
|
|
70
|
+
<RuntimeProvider adapter={adapter}>
|
|
71
|
+
<RuntimeCapture runtimeRef={runtimeRef} />
|
|
72
|
+
</RuntimeProvider>,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const runtime = runtimeRef.current;
|
|
76
|
+
expect(runtime).toBeTruthy();
|
|
77
|
+
expect(() => runtime!.thread.composer.setText("hello")).not.toThrow();
|
|
78
|
+
});
|
|
79
|
+
});
|