@assistant-ui/core 0.1.15 → 0.1.17

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 (113) hide show
  1. package/dist/index.d.ts +4 -4
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -1
  5. package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -1
  6. package/dist/react/index.d.ts +2 -1
  7. package/dist/react/index.d.ts.map +1 -1
  8. package/dist/react/index.js +1 -0
  9. package/dist/react/index.js.map +1 -1
  10. package/dist/react/primitives/message/MessageGroupedParts.d.ts +104 -0
  11. package/dist/react/primitives/message/MessageGroupedParts.d.ts.map +1 -0
  12. package/dist/react/primitives/message/MessageGroupedParts.js +74 -0
  13. package/dist/react/primitives/message/MessageGroupedParts.js.map +1 -0
  14. package/dist/react/primitives/message/MessageParts.d.ts +14 -1
  15. package/dist/react/primitives/message/MessageParts.d.ts.map +1 -1
  16. package/dist/react/primitives/message/MessageParts.js +55 -35
  17. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  18. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +3 -3
  19. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  20. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +26 -5
  21. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  22. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +2 -2
  23. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  24. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +6 -5
  25. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  26. package/dist/react/utils/groupParts.d.ts +49 -0
  27. package/dist/react/utils/groupParts.d.ts.map +1 -0
  28. package/dist/react/utils/groupParts.js +97 -0
  29. package/dist/react/utils/groupParts.js.map +1 -0
  30. package/dist/runtime/api/bindings.d.ts +1 -0
  31. package/dist/runtime/api/bindings.d.ts.map +1 -1
  32. package/dist/runtime/api/composer-runtime.d.ts +7 -5
  33. package/dist/runtime/api/composer-runtime.d.ts.map +1 -1
  34. package/dist/runtime/api/composer-runtime.js +1 -1
  35. package/dist/runtime/api/composer-runtime.js.map +1 -1
  36. package/dist/runtime/api/thread-list-item-runtime.d.ts +18 -3
  37. package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
  38. package/dist/runtime/api/thread-list-item-runtime.js +1 -1
  39. package/dist/runtime/api/thread-list-item-runtime.js.map +1 -1
  40. package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
  41. package/dist/runtime/api/thread-list-runtime.js +1 -0
  42. package/dist/runtime/api/thread-list-runtime.js.map +1 -1
  43. package/dist/runtime/api/thread-runtime.d.ts +5 -5
  44. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  45. package/dist/runtime/api/thread-runtime.js +1 -1
  46. package/dist/runtime/api/thread-runtime.js.map +1 -1
  47. package/dist/runtime/base/base-composer-runtime-core.d.ts +4 -3
  48. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  49. package/dist/runtime/base/base-composer-runtime-core.js +54 -26
  50. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  51. package/dist/runtime/base/base-thread-runtime-core.d.ts +3 -3
  52. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  53. package/dist/runtime/base/base-thread-runtime-core.js +11 -11
  54. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  55. package/dist/runtime/interfaces/composer-runtime-core.d.ts +28 -2
  56. package/dist/runtime/interfaces/composer-runtime-core.d.ts.map +1 -1
  57. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +1 -0
  58. package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
  59. package/dist/runtime/interfaces/thread-runtime-core.d.ts +37 -2
  60. package/dist/runtime/interfaces/thread-runtime-core.d.ts.map +1 -1
  61. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +2 -2
  62. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  63. package/dist/runtimes/local/local-thread-runtime-core.js +2 -2
  64. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  65. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +3 -0
  66. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
  67. package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
  68. package/dist/runtimes/remote-thread-list/types.d.ts +13 -1
  69. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  70. package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
  71. package/dist/store/runtime-clients/composer-runtime-client.js +5 -6
  72. package/dist/store/runtime-clients/composer-runtime-client.js.map +1 -1
  73. package/dist/store/scopes/composer.d.ts +11 -1
  74. package/dist/store/scopes/composer.d.ts.map +1 -1
  75. package/dist/store/scopes/thread-list-item.d.ts +11 -0
  76. package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
  77. package/dist/store/scopes/thread.d.ts +17 -0
  78. package/dist/store/scopes/thread.d.ts.map +1 -1
  79. package/dist/subscribable/subscribable.d.ts +4 -4
  80. package/dist/subscribable/subscribable.d.ts.map +1 -1
  81. package/dist/subscribable/subscribable.js +4 -4
  82. package/dist/subscribable/subscribable.js.map +1 -1
  83. package/package.json +21 -9
  84. package/src/index.ts +10 -0
  85. package/src/react/adapters/LocalStorageThreadListAdapter.tsx +2 -0
  86. package/src/react/index.ts +2 -0
  87. package/src/react/primitives/message/MessageGroupedParts.tsx +186 -0
  88. package/src/react/primitives/message/MessageParts.tsx +101 -49
  89. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +40 -7
  90. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +6 -12
  91. package/src/react/utils/groupParts.ts +152 -0
  92. package/src/runtime/api/bindings.ts +1 -0
  93. package/src/runtime/api/composer-runtime.ts +14 -11
  94. package/src/runtime/api/thread-list-item-runtime.ts +28 -6
  95. package/src/runtime/api/thread-list-runtime.ts +1 -0
  96. package/src/runtime/api/thread-runtime.ts +11 -7
  97. package/src/runtime/base/base-composer-runtime-core.ts +99 -35
  98. package/src/runtime/base/base-thread-runtime-core.ts +21 -12
  99. package/src/runtime/interfaces/composer-runtime-core.ts +39 -7
  100. package/src/runtime/interfaces/thread-list-runtime-core.ts +1 -0
  101. package/src/runtime/interfaces/thread-runtime-core.ts +44 -6
  102. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +2 -2
  103. package/src/runtimes/local/local-thread-runtime-core.ts +2 -2
  104. package/src/runtimes/remote-thread-list/remote-thread-state.ts +3 -0
  105. package/src/runtimes/remote-thread-list/types.ts +13 -1
  106. package/src/store/runtime-clients/composer-runtime-client.ts +5 -9
  107. package/src/store/scopes/composer.ts +11 -0
  108. package/src/store/scopes/thread-list-item.ts +11 -0
  109. package/src/store/scopes/thread.ts +17 -0
  110. package/src/subscribable/subscribable.ts +10 -7
  111. package/src/tests/RemoteThreadListThreadListRuntimeCore-custom-metadata.test.ts +123 -0
  112. package/src/tests/base-composer-runtime-core-addAttachment.test.ts +217 -0
  113. package/src/tests/groupParts.test.ts +114 -0
@@ -0,0 +1,217 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { DefaultThreadComposerRuntimeCore } from "../runtime/base/default-thread-composer-runtime-core";
3
+ import type { AttachmentAdapter } from "../adapters/attachment";
4
+ import type { ThreadRuntimeCore } from "../runtime/interfaces/thread-runtime-core";
5
+ import type { PendingAttachment } from "../types/attachment";
6
+
7
+ const makeAdapter = (
8
+ overrides: Partial<AttachmentAdapter> = {},
9
+ ): AttachmentAdapter => ({
10
+ accept: "image/*",
11
+ add: async ({ file }: { file: File }): Promise<PendingAttachment> => ({
12
+ id: "att-1",
13
+ type: "image",
14
+ name: file.name,
15
+ contentType: file.type,
16
+ file,
17
+ status: { type: "running", reason: "uploading", progress: 0 },
18
+ }),
19
+ remove: async () => {},
20
+ send: async (a) => ({ ...a, status: { type: "complete" }, content: [] }),
21
+ ...overrides,
22
+ });
23
+
24
+ const makeComposer = (adapter?: AttachmentAdapter) => {
25
+ const runtime = {
26
+ append: vi.fn(),
27
+ cancelRun: vi.fn(),
28
+ subscribe: vi.fn(() => () => {}),
29
+ capabilities: { cancel: false },
30
+ messages: [],
31
+ adapters: adapter ? { attachments: adapter } : undefined,
32
+ } as unknown as Omit<ThreadRuntimeCore, "composer"> & {
33
+ adapters?: { attachments?: AttachmentAdapter };
34
+ };
35
+ return new DefaultThreadComposerRuntimeCore(runtime);
36
+ };
37
+
38
+ describe("BaseComposerRuntimeCore.addAttachment error events", () => {
39
+ it("emits attachmentAddError when no adapter is configured", async () => {
40
+ const composer = makeComposer();
41
+ const onError = vi.fn();
42
+ const onAdd = vi.fn();
43
+ composer.unstable_on("attachmentAddError", onError);
44
+ composer.unstable_on("attachmentAdd", onAdd);
45
+
46
+ await expect(
47
+ composer.addAttachment(new File(["x"], "f.txt", { type: "text/plain" })),
48
+ ).rejects.toThrow("Attachments are not supported");
49
+
50
+ expect(onError).toHaveBeenCalledTimes(1);
51
+ expect(onError).toHaveBeenCalledWith(
52
+ expect.objectContaining({
53
+ reason: "no-adapter",
54
+ message: "Attachments are not supported",
55
+ error: expect.any(Error),
56
+ }),
57
+ );
58
+ expect(onAdd).not.toHaveBeenCalled();
59
+ });
60
+
61
+ it("emits attachmentAddError when file type is rejected by adapter.accept", async () => {
62
+ const composer = makeComposer(makeAdapter());
63
+ const onError = vi.fn();
64
+ const onAdd = vi.fn();
65
+ composer.unstable_on("attachmentAddError", onError);
66
+ composer.unstable_on("attachmentAdd", onAdd);
67
+
68
+ await expect(
69
+ composer.addAttachment(new File(["x"], "f.txt", { type: "text/plain" })),
70
+ ).rejects.toThrow(/File type text\/plain is not accepted/);
71
+
72
+ expect(onError).toHaveBeenCalledTimes(1);
73
+ expect(onError).toHaveBeenCalledWith(
74
+ expect.objectContaining({
75
+ reason: "not-accepted",
76
+ message: expect.stringContaining(
77
+ "File type text/plain is not accepted",
78
+ ),
79
+ error: expect.any(Error),
80
+ }),
81
+ );
82
+ expect(onAdd).not.toHaveBeenCalled();
83
+ });
84
+
85
+ it("emits attachmentAddError when adapter.add throws", async () => {
86
+ const composer = makeComposer(
87
+ makeAdapter({
88
+ add: async () => {
89
+ throw new Error("upload failed");
90
+ },
91
+ }),
92
+ );
93
+ const onError = vi.fn();
94
+ const onAdd = vi.fn();
95
+ composer.unstable_on("attachmentAddError", onError);
96
+ composer.unstable_on("attachmentAdd", onAdd);
97
+
98
+ await expect(
99
+ composer.addAttachment(new File(["x"], "f.png", { type: "image/png" })),
100
+ ).rejects.toThrow("upload failed");
101
+
102
+ expect(onError).toHaveBeenCalledTimes(1);
103
+ expect(onError).toHaveBeenCalledWith(
104
+ expect.objectContaining({
105
+ reason: "adapter-error",
106
+ message: "upload failed",
107
+ error: expect.any(Error),
108
+ }),
109
+ );
110
+ expect(onAdd).not.toHaveBeenCalled();
111
+ });
112
+
113
+ it("emits attachmentAdd on successful add", async () => {
114
+ const composer = makeComposer(makeAdapter());
115
+ const onError = vi.fn();
116
+ const onAdd = vi.fn();
117
+ composer.unstable_on("attachmentAddError", onError);
118
+ composer.unstable_on("attachmentAdd", onAdd);
119
+
120
+ await composer.addAttachment(
121
+ new File(["x"], "f.png", { type: "image/png" }),
122
+ );
123
+
124
+ expect(onAdd).toHaveBeenCalledTimes(1);
125
+ expect(onError).not.toHaveBeenCalled();
126
+ });
127
+
128
+ it("does not let subscriber errors mask the original throw", async () => {
129
+ const composer = makeComposer(makeAdapter());
130
+ composer.unstable_on("attachmentAddError", () => {
131
+ throw new Error("subscriber boom");
132
+ });
133
+
134
+ await expect(
135
+ composer.addAttachment(new File(["x"], "f.txt", { type: "text/plain" })),
136
+ ).rejects.toThrow(/File type text\/plain is not accepted/);
137
+ });
138
+
139
+ it("emits attachmentAddError when async generator adapter throws mid-iteration", async () => {
140
+ const composer = makeComposer(
141
+ makeAdapter({
142
+ async *add({ file }: { file: File }) {
143
+ yield {
144
+ id: "att-1",
145
+ type: "image" as const,
146
+ name: file.name,
147
+ contentType: file.type,
148
+ file,
149
+ status: {
150
+ type: "running" as const,
151
+ reason: "uploading" as const,
152
+ progress: 0,
153
+ },
154
+ };
155
+ throw new Error("network error");
156
+ },
157
+ }),
158
+ );
159
+ const onError = vi.fn();
160
+ const onAdd = vi.fn();
161
+ composer.unstable_on("attachmentAddError", onError);
162
+ composer.unstable_on("attachmentAdd", onAdd);
163
+
164
+ await expect(
165
+ composer.addAttachment(new File(["x"], "f.png", { type: "image/png" })),
166
+ ).rejects.toThrow("network error");
167
+
168
+ expect(onError).toHaveBeenCalledTimes(1);
169
+ expect(onError).toHaveBeenCalledWith(
170
+ expect.objectContaining({
171
+ reason: "adapter-error",
172
+ message: "network error",
173
+ attachmentId: "att-1",
174
+ error: expect.any(Error),
175
+ }),
176
+ );
177
+ expect(onAdd).not.toHaveBeenCalled();
178
+ expect(composer.attachments).toHaveLength(1);
179
+ const att = composer.attachments[0]!;
180
+ expect(att.status.type).toBe("incomplete");
181
+ if (att.status.type === "incomplete") {
182
+ expect(att.status.reason).toBe("error");
183
+ }
184
+ });
185
+
186
+ it("emits attachmentAddError with attachment id when adapter yields an errored attachment", async () => {
187
+ const composer = makeComposer(
188
+ makeAdapter({
189
+ add: async ({ file }) => ({
190
+ id: "att-2",
191
+ type: "image",
192
+ name: file.name,
193
+ contentType: file.type,
194
+ file,
195
+ status: { type: "incomplete", reason: "error" },
196
+ }),
197
+ }),
198
+ );
199
+ const onError = vi.fn();
200
+ const onAdd = vi.fn();
201
+ composer.unstable_on("attachmentAddError", onError);
202
+ composer.unstable_on("attachmentAdd", onAdd);
203
+
204
+ await composer.addAttachment(
205
+ new File(["x"], "f.png", { type: "image/png" }),
206
+ );
207
+
208
+ expect(onError).toHaveBeenCalledTimes(1);
209
+ expect(onError).toHaveBeenCalledWith(
210
+ expect.objectContaining({
211
+ reason: "adapter-error",
212
+ attachmentId: "att-2",
213
+ }),
214
+ );
215
+ expect(onAdd).not.toHaveBeenCalled();
216
+ });
217
+ });
@@ -0,0 +1,114 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ buildGroupTree,
4
+ normalizeGroupKey,
5
+ type GroupNode,
6
+ } from "../react/utils/groupParts";
7
+
8
+ const asPaths = (keys: readonly (string | readonly string[] | null)[]) =>
9
+ keys.map((k) => normalizeGroupKey(k));
10
+
11
+ // Compact tree dump: "G:key#nodeKey[i,j]{...}" | "P:#nodeKey(i)"
12
+ const dump = (nodes: readonly GroupNode[]): string =>
13
+ nodes
14
+ .map((n) => {
15
+ if (n.type === "part") {
16
+ return `P:#${n.nodeKey}(${n.index})`;
17
+ }
18
+ const inner = dump(n.children);
19
+ return `G:${n.key}#${n.nodeKey}[${n.indices.join(",")}]{${inner}}`;
20
+ })
21
+ .join(",");
22
+
23
+ describe("normalizeGroupKey", () => {
24
+ it("maps null/undefined/[] to []", () => {
25
+ expect(normalizeGroupKey(null)).toEqual([]);
26
+ expect(normalizeGroupKey(undefined)).toEqual([]);
27
+ expect(normalizeGroupKey([])).toEqual([]);
28
+ });
29
+
30
+ it("wraps a string into a single-element array", () => {
31
+ expect(normalizeGroupKey("foo")).toEqual(["foo"]);
32
+ });
33
+
34
+ it("passes arrays through", () => {
35
+ expect(normalizeGroupKey(["a", "b"])).toEqual(["a", "b"]);
36
+ });
37
+ });
38
+
39
+ describe("buildGroupTree", () => {
40
+ it("returns an empty list for no parts", () => {
41
+ expect(buildGroupTree([])).toEqual([]);
42
+ });
43
+
44
+ it("emits one part leaf per ungrouped part (no coalescing)", () => {
45
+ const tree = buildGroupTree(asPaths([null, null, null]));
46
+ expect(dump(tree)).toBe("P:#0(0),P:#1(1),P:#2(2)");
47
+ });
48
+
49
+ it("wraps adjacent same-key parts in one group with one part child each", () => {
50
+ const tree = buildGroupTree(asPaths(["a", "a", "a"]));
51
+ expect(dump(tree)).toBe("G:a#0[0,1,2]{P:#0.0(0),P:#0.1(1),P:#0.2(2)}");
52
+ });
53
+
54
+ it("splits non-adjacent runs of the same key into separate groups", () => {
55
+ const tree = buildGroupTree(asPaths(["a", null, "a"]));
56
+ expect(dump(tree)).toBe("G:a#0[0]{P:#0.0(0)},P:#1(1),G:a#2[2]{P:#2.0(2)}");
57
+ });
58
+
59
+ it("nests groups: parts at depth 1 sit alongside depth-2 subgroups", () => {
60
+ // ["A","B"], ["A","B"], ["A"], ["A"], ["A","C"]:
61
+ // Outer A spans 0..4. Inside A: a B subgroup (0,1), two depth-1 parts
62
+ // (2,3), then a C subgroup (4).
63
+ const tree = buildGroupTree(
64
+ asPaths([["A", "B"], ["A", "B"], ["A"], ["A"], ["A", "C"]]),
65
+ );
66
+ expect(dump(tree)).toBe(
67
+ "G:A#0[0,1,2,3,4]{G:B#0.0[0,1]{P:#0.0.0(0),P:#0.0.1(1)},P:#0.1(2),P:#0.2(3),G:C#0.3[4]{P:#0.3.0(4)}}",
68
+ );
69
+ });
70
+
71
+ it("treats longer prefix changes as group close+open", () => {
72
+ // ["A","B"], ["A","B","C"], ["A","B"]: opens C under B, closes back.
73
+ const tree = buildGroupTree(
74
+ asPaths([
75
+ ["A", "B"],
76
+ ["A", "B", "C"],
77
+ ["A", "B"],
78
+ ]),
79
+ );
80
+ expect(dump(tree)).toBe(
81
+ "G:A#0[0,1,2]{G:B#0.0[0,1,2]{P:#0.0.0(0),G:C#0.0.1[1]{P:#0.0.1.0(1)},P:#0.0.2(2)}}",
82
+ );
83
+ });
84
+
85
+ it("does not coalesce same-keyed groups separated by a divergent sibling", () => {
86
+ const tree = buildGroupTree(
87
+ asPaths([
88
+ ["A", "B"],
89
+ ["A", "C"],
90
+ ["A", "B"],
91
+ ]),
92
+ );
93
+ expect(dump(tree)).toBe(
94
+ "G:A#0[0,1,2]{G:B#0.0[0]{P:#0.0.0(0)},G:C#0.1[1]{P:#0.1.0(1)},G:B#0.2[2]{P:#0.2.0(2)}}",
95
+ );
96
+ });
97
+
98
+ it("accepts strings and arrays interchangeably via normalizeGroupKey", () => {
99
+ const tree = buildGroupTree([
100
+ normalizeGroupKey("A"),
101
+ normalizeGroupKey(["A"]),
102
+ ]);
103
+ expect(dump(tree)).toBe("G:A#0[0,1]{P:#0.0(0),P:#0.1(1)}");
104
+ });
105
+
106
+ it("assigns stable nodeKeys under append (existing keys do not shift)", () => {
107
+ const before = buildGroupTree(asPaths([["A"], null]));
108
+ const after = buildGroupTree(asPaths([["A"], null, ["B"]]));
109
+
110
+ expect(before[0]!.nodeKey).toBe(after[0]!.nodeKey);
111
+ expect(before[1]!.nodeKey).toBe(after[1]!.nodeKey);
112
+ expect(after[2]!.nodeKey).toBe("2");
113
+ });
114
+ });