@assistant-ui/core 0.3.11 → 0.3.13

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 (132) hide show
  1. package/dist/index.d.ts +6 -5
  2. package/dist/index.js +2 -2
  3. package/dist/index.js.map +1 -1
  4. package/dist/internal.d.ts +6 -5
  5. package/dist/internal.js +5 -4
  6. package/dist/react/client/interactable-model-context.d.ts.map +1 -1
  7. package/dist/react/client/interactable-model-context.js +5 -1
  8. package/dist/react/client/interactable-model-context.js.map +1 -1
  9. package/dist/runtime/api/message-runtime.d.ts +2 -3
  10. package/dist/runtime/api/message-runtime.d.ts.map +1 -1
  11. package/dist/runtime/api/message-runtime.js +2 -16
  12. package/dist/runtime/api/message-runtime.js.map +1 -1
  13. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  14. package/dist/runtime/api/thread-runtime.js +5 -1
  15. package/dist/runtime/api/thread-runtime.js.map +1 -1
  16. package/dist/runtime/base/base-composer-runtime-core.d.ts +25 -0
  17. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  18. package/dist/runtime/base/base-composer-runtime-core.js +64 -11
  19. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  20. package/dist/runtime/base/base-thread-runtime-core.d.ts +14 -0
  21. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  22. package/dist/runtime/base/base-thread-runtime-core.js +31 -0
  23. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  24. package/dist/runtime/base/default-edit-composer-runtime-core.d.ts.map +1 -1
  25. package/dist/runtime/base/default-edit-composer-runtime-core.js +1 -6
  26. package/dist/runtime/base/default-edit-composer-runtime-core.js.map +1 -1
  27. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  28. package/dist/runtime/base/default-thread-composer-runtime-core.js +1 -4
  29. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  30. package/dist/runtime/internal.d.ts +4 -4
  31. package/dist/runtime/internal.js +4 -4
  32. package/dist/runtime/queue/external-thread-queue-adapter.d.ts +15 -0
  33. package/dist/runtime/queue/external-thread-queue-adapter.d.ts.map +1 -1
  34. package/dist/runtime/queue/message-queue.js +23 -10
  35. package/dist/runtime/queue/message-queue.js.map +1 -1
  36. package/dist/runtimes/external-store/external-store-adapter.d.ts +7 -0
  37. package/dist/runtimes/external-store/external-store-adapter.d.ts.map +1 -1
  38. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +3 -1
  39. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  40. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +49 -13
  41. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  42. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  43. package/dist/runtimes/local/local-thread-runtime-core.js +2 -1
  44. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  45. package/dist/store/clients/external-thread.d.ts +61 -0
  46. package/dist/store/clients/external-thread.d.ts.map +1 -0
  47. package/dist/store/clients/external-thread.js +1445 -0
  48. package/dist/store/clients/external-thread.js.map +1 -0
  49. package/dist/store/clients/runtime-adapter.d.ts.map +1 -1
  50. package/dist/store/clients/runtime-adapter.js +5 -2
  51. package/dist/store/clients/runtime-adapter.js.map +1 -1
  52. package/dist/store/clients/single-thread-list.d.ts +9 -0
  53. package/dist/store/clients/single-thread-list.d.ts.map +1 -0
  54. package/dist/store/clients/single-thread-list.js +175 -0
  55. package/dist/store/clients/single-thread-list.js.map +1 -0
  56. package/dist/store/clients/suggestions.d.ts +3 -1
  57. package/dist/store/clients/suggestions.d.ts.map +1 -1
  58. package/dist/store/clients/suggestions.js +65 -37
  59. package/dist/store/clients/suggestions.js.map +1 -1
  60. package/dist/store/index.d.ts +5 -3
  61. package/dist/store/index.js +4 -2
  62. package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
  63. package/dist/store/runtime-clients/thread-list-item-runtime-client.js +64 -50
  64. package/dist/store/runtime-clients/thread-list-item-runtime-client.js.map +1 -1
  65. package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
  66. package/dist/store/runtime-clients/thread-runtime-client.js +98 -82
  67. package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
  68. package/dist/store/scope-registration.d.ts +2 -2
  69. package/dist/store/scopes/suggestions.d.ts +6 -1
  70. package/dist/store/scopes/suggestions.d.ts.map +1 -1
  71. package/dist/store/scopes/thread.d.ts +17 -0
  72. package/dist/store/scopes/thread.d.ts.map +1 -1
  73. package/dist/types/augmentations.d.ts +33 -0
  74. package/dist/types/augmentations.d.ts.map +1 -0
  75. package/dist/types/augmentations.js +0 -0
  76. package/dist/types/error.d.ts +15 -1
  77. package/dist/types/error.d.ts.map +1 -1
  78. package/dist/types/error.js +18 -1
  79. package/dist/types/error.js.map +1 -1
  80. package/dist/types/message.d.ts +2 -0
  81. package/dist/types/message.d.ts.map +1 -1
  82. package/dist/types/message.js.map +1 -1
  83. package/dist/utils/normalizePartStatus.d.ts +3 -2
  84. package/dist/utils/normalizePartStatus.d.ts.map +1 -1
  85. package/dist/utils/normalizePartStatus.js +15 -1
  86. package/dist/utils/normalizePartStatus.js.map +1 -1
  87. package/package.json +5 -5
  88. package/src/index.ts +7 -0
  89. package/src/internal.ts +1 -1
  90. package/src/react/client/interactable-model-context.ts +6 -1
  91. package/src/react/runtimes/useExternalStoreRuntime.suggestions.test.tsx +114 -0
  92. package/src/runtime/api/message-runtime.test.ts +1 -1
  93. package/src/runtime/api/message-runtime.ts +2 -35
  94. package/src/runtime/api/thread-runtime.ts +8 -1
  95. package/src/runtime/base/base-composer-runtime-core.ts +100 -8
  96. package/src/runtime/base/base-thread-runtime-core.ts +36 -0
  97. package/src/runtime/base/default-edit-composer-runtime-core.ts +1 -15
  98. package/src/runtime/base/default-thread-composer-runtime-core.ts +1 -11
  99. package/src/runtime/queue/external-thread-queue-adapter.ts +17 -0
  100. package/src/runtime/queue/message-queue.ts +26 -9
  101. package/src/runtimes/external-store/external-store-adapter.ts +7 -0
  102. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +100 -26
  103. package/src/runtimes/local/local-thread-runtime-core.ts +4 -1
  104. package/src/store/clients/external-thread.ts +1255 -0
  105. package/src/store/clients/runtime-adapter.ts +5 -2
  106. package/src/store/clients/single-thread-list.ts +116 -0
  107. package/src/store/clients/suggestions.ts +39 -13
  108. package/src/store/index.ts +6 -0
  109. package/src/store/runtime-clients/thread-list-item-runtime-client.ts +13 -28
  110. package/src/store/runtime-clients/thread-runtime-client.ts +5 -0
  111. package/src/store/scopes/suggestions.ts +6 -0
  112. package/src/store/scopes/thread.ts +17 -0
  113. package/src/tests/append-interactable-snapshots.test.ts +350 -0
  114. package/src/tests/base-composer-runtime-core-send.test.ts +126 -0
  115. package/src/tests/base-composer-runtime-core.test.ts +108 -1
  116. package/src/tests/default-edit-composer-runtime-core.test.ts +0 -68
  117. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +300 -0
  118. package/src/tests/external-thread-attachments.test.tsx +584 -0
  119. package/src/tests/external-thread-branches.test.tsx +157 -0
  120. package/src/tests/external-thread-feedback.test.tsx +204 -0
  121. package/src/tests/external-thread-parity.test.tsx +548 -0
  122. package/src/tests/external-thread-refetch.test.tsx +117 -0
  123. package/src/tests/external-thread-speech.test.tsx +328 -0
  124. package/src/tests/external-thread-suggestions.test.tsx +59 -0
  125. package/src/tests/message-queue.test.ts +26 -0
  126. package/src/tests/thread-message-like.test.ts +39 -0
  127. package/src/tests/thread-switch-events.test.tsx +136 -0
  128. package/src/types/augmentations.ts +33 -0
  129. package/src/types/error.ts +24 -0
  130. package/src/types/message.ts +2 -0
  131. package/src/utils/normalizePartStatus.ts +27 -0
  132. package/src/tests/default-thread-composer-runtime-core.test.ts +0 -82
@@ -0,0 +1,157 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { render } from "@testing-library/react";
4
+ import type { FC } from "react";
5
+ import { describe, it, expect, vi } from "vitest";
6
+ import { useAui, AuiProvider } from "@assistant-ui/store";
7
+ import type { ExternalThreadBranchAdapter, ThreadMessage } from "../index";
8
+ import {
9
+ ExternalThread,
10
+ type ExternalThreadProps,
11
+ } from "../store/clients/external-thread";
12
+
13
+ const message = (id: string, role: "user" | "assistant"): ThreadMessage =>
14
+ ({
15
+ id,
16
+ role,
17
+ content: [{ type: "text", text: `text of ${id}` }],
18
+ createdAt: new Date(1718000000000),
19
+ ...(role === "assistant"
20
+ ? { status: { type: "complete", reason: "stop" } }
21
+ : { attachments: [] }),
22
+ metadata: { custom: {} },
23
+ }) as ThreadMessage;
24
+
25
+ const renderThread = (props: ExternalThreadProps) => {
26
+ const captured: { aui?: ReturnType<typeof useAui> } = {};
27
+ const Capture: FC = () => {
28
+ captured.aui = useAui();
29
+ return null;
30
+ };
31
+ const App: FC<{ threadProps: ExternalThreadProps }> = ({ threadProps }) => {
32
+ const aui = useAui({ thread: ExternalThread(threadProps) });
33
+ return (
34
+ <AuiProvider value={aui}>
35
+ <Capture />
36
+ </AuiProvider>
37
+ );
38
+ };
39
+ const utils = render(<App threadProps={props} />);
40
+ return {
41
+ aui: () => captured.aui!,
42
+ rerender: (next: ExternalThreadProps) =>
43
+ utils.rerender(<App threadProps={next} />),
44
+ };
45
+ };
46
+
47
+ const baseProps = (
48
+ branches?: ExternalThreadBranchAdapter,
49
+ ): ExternalThreadProps => ({
50
+ messages: [message("u1", "user"), message("a2", "assistant")],
51
+ isRunning: false,
52
+ ...(branches ? { branches } : {}),
53
+ });
54
+
55
+ const adapterFor = (
56
+ ids: readonly string[],
57
+ switchToBranch = vi.fn(),
58
+ ): ExternalThreadBranchAdapter => ({
59
+ getBranches: (messageId) => (ids.includes(messageId) ? ids : []),
60
+ switchToBranch,
61
+ });
62
+
63
+ describe("ExternalThread branches", () => {
64
+ it("defaults to single-branch state without an adapter", () => {
65
+ const { aui } = renderThread(baseProps());
66
+ const state = aui().thread.getState();
67
+ expect(state.messages[1]!.branchNumber).toBe(1);
68
+ expect(state.messages[1]!.branchCount).toBe(1);
69
+ expect(state.capabilities.switchToBranch).toBe(false);
70
+ expect(() =>
71
+ aui().thread.message({ index: 1 }).switchToBranch({ position: "next" }),
72
+ ).not.toThrow();
73
+ });
74
+
75
+ it("derives branchNumber and branchCount from the adapter", () => {
76
+ const { aui } = renderThread(baseProps(adapterFor(["a1", "a2", "a3"])));
77
+ const state = aui().thread.getState();
78
+ expect(state.messages[1]!.branchNumber).toBe(2);
79
+ expect(state.messages[1]!.branchCount).toBe(3);
80
+ expect(state.capabilities.switchToBranch).toBe(true);
81
+ });
82
+
83
+ it("resolves previous and next to sibling ids and no-ops at the edges", () => {
84
+ const switchToBranch = vi.fn();
85
+ const { aui } = renderThread(
86
+ baseProps(adapterFor(["a1", "a2", "a3"], switchToBranch)),
87
+ );
88
+ const msg = () => aui().thread.message({ index: 1 });
89
+
90
+ msg().switchToBranch({ position: "previous" });
91
+ expect(switchToBranch).toHaveBeenLastCalledWith("a1");
92
+ msg().switchToBranch({ position: "next" });
93
+ expect(switchToBranch).toHaveBeenLastCalledWith("a3");
94
+
95
+ switchToBranch.mockClear();
96
+ const edge = renderThread({
97
+ messages: [message("u1", "user"), message("a1", "assistant")],
98
+ isRunning: false,
99
+ branches: adapterFor(["a1", "a2"], switchToBranch),
100
+ });
101
+ edge.aui().thread.message({ index: 1 }).switchToBranch({
102
+ position: "previous",
103
+ });
104
+ expect(switchToBranch).not.toHaveBeenCalled();
105
+ });
106
+
107
+ it("forwards an explicit branchId unvalidated but ignores self-switches", () => {
108
+ const switchToBranch = vi.fn();
109
+ const { aui } = renderThread(
110
+ baseProps(adapterFor(["a1", "a2"], switchToBranch)),
111
+ );
112
+ const msg = () => aui().thread.message({ index: 1 });
113
+
114
+ msg().switchToBranch({ branchId: "not-in-the-list" });
115
+ expect(switchToBranch).toHaveBeenLastCalledWith("not-in-the-list");
116
+
117
+ switchToBranch.mockClear();
118
+ msg().switchToBranch({ branchId: "a2" });
119
+ expect(switchToBranch).not.toHaveBeenCalled();
120
+ });
121
+
122
+ it("falls back to single-branch state when getBranches omits the own id", () => {
123
+ const switchToBranch = vi.fn();
124
+ const { aui } = renderThread(
125
+ baseProps({
126
+ getBranches: () => ["b1", "b2"],
127
+ switchToBranch,
128
+ }),
129
+ );
130
+ const state = aui().thread.getState();
131
+ expect(state.messages[1]!.branchNumber).toBe(1);
132
+ expect(state.messages[1]!.branchCount).toBe(1);
133
+
134
+ aui().thread.message({ index: 1 }).switchToBranch({ position: "next" });
135
+ expect(switchToBranch).not.toHaveBeenCalled();
136
+ });
137
+
138
+ it("keeps message state identity across adapter recreation with equal values", () => {
139
+ const messages = [message("u1", "user"), message("a2", "assistant")];
140
+ const { aui, rerender } = renderThread({
141
+ messages,
142
+ isRunning: false,
143
+ branches: adapterFor(["a1", "a2", "a3"]),
144
+ });
145
+ const before = aui().thread.getState().messages[1];
146
+
147
+ rerender({
148
+ messages,
149
+ isRunning: false,
150
+ branches: adapterFor(["a1", "a2", "a3"]),
151
+ });
152
+ const after = aui().thread.getState().messages[1];
153
+
154
+ expect(after!.branchNumber).toBe(2);
155
+ expect(before!.parts[0]).toBe(after!.parts[0]);
156
+ });
157
+ });
@@ -0,0 +1,204 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { act, render, waitFor } from "@testing-library/react";
4
+ import type { FC } from "react";
5
+ import { describe, expect, it, vi } from "vitest";
6
+ import { AuiProvider, useAui } from "@assistant-ui/store";
7
+ import type { FeedbackAdapter } from "../index";
8
+ import type {
9
+ ExternalThreadMessage,
10
+ ExternalThreadProps,
11
+ } from "../store/clients/external-thread";
12
+ import { ExternalThread } from "../store/clients/external-thread";
13
+
14
+ const MESSAGES = [
15
+ {
16
+ id: "u1",
17
+ role: "user",
18
+ content: [{ type: "text", text: "hi" }],
19
+ createdAt: new Date(0),
20
+ attachments: [],
21
+ metadata: { custom: {} },
22
+ },
23
+ {
24
+ id: "a1",
25
+ role: "assistant",
26
+ content: [{ type: "text", text: "hello there" }],
27
+ createdAt: new Date(0),
28
+ metadata: { custom: {} },
29
+ },
30
+ ] as unknown as readonly ExternalThreadMessage[];
31
+
32
+ const createFakeAdapter = () => {
33
+ const submit = vi.fn();
34
+ const adapter: FeedbackAdapter = { submit };
35
+ return { adapter, submit };
36
+ };
37
+
38
+ const renderThreadWithProps = (props: Partial<ExternalThreadProps>) => {
39
+ const captured: { aui?: ReturnType<typeof useAui> } = {};
40
+ const Capture: FC = () => {
41
+ captured.aui = useAui();
42
+ return null;
43
+ };
44
+ const App: FC<{ props: Partial<ExternalThreadProps> }> = ({ props }) => {
45
+ const aui = useAui({
46
+ thread: ExternalThread({
47
+ messages: MESSAGES,
48
+ isRunning: false,
49
+ ...props,
50
+ }),
51
+ });
52
+ return (
53
+ <AuiProvider value={aui}>
54
+ <Capture />
55
+ </AuiProvider>
56
+ );
57
+ };
58
+
59
+ const view = render(<App props={props} />);
60
+ return {
61
+ aui: () => captured.aui!,
62
+ rerender: (nextProps: Partial<ExternalThreadProps>) =>
63
+ view.rerender(<App props={nextProps} />),
64
+ };
65
+ };
66
+
67
+ describe("ExternalThread feedback", () => {
68
+ it("reports the feedback capability based on adapter presence", () => {
69
+ const { aui: withoutAdapter } = renderThreadWithProps({});
70
+ expect(withoutAdapter().thread.getState().capabilities.feedback).toBe(
71
+ false,
72
+ );
73
+
74
+ const { adapter } = createFakeAdapter();
75
+ const { aui: withAdapter } = renderThreadWithProps({
76
+ feedbackAdapter: adapter,
77
+ });
78
+ expect(withAdapter().thread.getState().capabilities.feedback).toBe(true);
79
+ });
80
+
81
+ it("throws on submitFeedback when no adapter is configured", () => {
82
+ const { aui } = renderThreadWithProps({});
83
+ expect(() =>
84
+ aui().thread.message({ id: "a1" }).submitFeedback({ type: "positive" }),
85
+ ).toThrow("Feedback adapter not configured");
86
+ });
87
+
88
+ it("submits feedback to the adapter and marks the assistant message", async () => {
89
+ const { adapter, submit } = createFakeAdapter();
90
+ const { aui } = renderThreadWithProps({ feedbackAdapter: adapter });
91
+
92
+ await act(async () => {
93
+ aui().thread.message({ id: "a1" }).submitFeedback({ type: "positive" });
94
+ });
95
+
96
+ expect(submit).toHaveBeenCalledTimes(1);
97
+ expect(submit).toHaveBeenCalledWith({
98
+ message: MESSAGES[1],
99
+ type: "positive",
100
+ });
101
+ await waitFor(() => {
102
+ expect(
103
+ aui().thread.message({ id: "a1" }).getState().metadata
104
+ .submittedFeedback,
105
+ ).toEqual({ type: "positive" });
106
+ });
107
+
108
+ await act(async () => {
109
+ aui().thread.message({ id: "a1" }).submitFeedback({ type: "negative" });
110
+ });
111
+
112
+ expect(submit).toHaveBeenLastCalledWith({
113
+ message: MESSAGES[1],
114
+ type: "negative",
115
+ });
116
+ await waitFor(() => {
117
+ expect(
118
+ aui().thread.message({ id: "a1" }).getState().metadata
119
+ .submittedFeedback,
120
+ ).toEqual({ type: "negative" });
121
+ });
122
+ });
123
+
124
+ it("prefers owner-supplied submittedFeedback over the local overlay", async () => {
125
+ const { adapter } = createFakeAdapter();
126
+ const { aui, rerender } = renderThreadWithProps({
127
+ feedbackAdapter: adapter,
128
+ });
129
+
130
+ await act(async () => {
131
+ aui().thread.message({ id: "a1" }).submitFeedback({ type: "positive" });
132
+ });
133
+ await waitFor(() => {
134
+ expect(
135
+ aui().thread.message({ id: "a1" }).getState().metadata
136
+ .submittedFeedback,
137
+ ).toEqual({ type: "positive" });
138
+ });
139
+
140
+ const ownerMessages = [
141
+ MESSAGES[0]!,
142
+ {
143
+ ...MESSAGES[1]!,
144
+ metadata: { custom: {}, submittedFeedback: { type: "negative" } },
145
+ },
146
+ ] as unknown as readonly ExternalThreadMessage[];
147
+ await act(async () => {
148
+ rerender({ feedbackAdapter: adapter, messages: ownerMessages });
149
+ });
150
+
151
+ expect(
152
+ aui().thread.message({ id: "a1" }).getState().metadata.submittedFeedback,
153
+ ).toEqual({ type: "negative" });
154
+ });
155
+
156
+ it("re-rates an owner-marked message locally, then honors an owner clear", async () => {
157
+ const { adapter } = createFakeAdapter();
158
+ const ratedMessages = [
159
+ MESSAGES[0]!,
160
+ {
161
+ ...MESSAGES[1]!,
162
+ metadata: { custom: {}, submittedFeedback: { type: "positive" } },
163
+ },
164
+ ] as unknown as readonly ExternalThreadMessage[];
165
+ const { aui, rerender } = renderThreadWithProps({
166
+ feedbackAdapter: adapter,
167
+ messages: ratedMessages,
168
+ });
169
+
170
+ await act(async () => {
171
+ aui().thread.message({ id: "a1" }).submitFeedback({ type: "negative" });
172
+ });
173
+ await waitFor(() => {
174
+ expect(
175
+ aui().thread.message({ id: "a1" }).getState().metadata
176
+ .submittedFeedback,
177
+ ).toEqual({ type: "negative" });
178
+ });
179
+
180
+ await act(async () => {
181
+ rerender({ feedbackAdapter: adapter, messages: MESSAGES });
182
+ });
183
+ expect(
184
+ aui().thread.message({ id: "a1" }).getState().metadata.submittedFeedback,
185
+ ).toBeUndefined();
186
+ });
187
+
188
+ it("submits user message feedback without marking the message", async () => {
189
+ const { adapter, submit } = createFakeAdapter();
190
+ const { aui } = renderThreadWithProps({ feedbackAdapter: adapter });
191
+
192
+ await act(async () => {
193
+ aui().thread.message({ id: "u1" }).submitFeedback({ type: "negative" });
194
+ });
195
+
196
+ expect(submit).toHaveBeenCalledWith({
197
+ message: MESSAGES[0],
198
+ type: "negative",
199
+ });
200
+ expect(
201
+ aui().thread.message({ id: "u1" }).getState().metadata.submittedFeedback,
202
+ ).toBeUndefined();
203
+ });
204
+ });