@agent-native/toolkit 0.6.0 → 0.7.0

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 (173) hide show
  1. package/README.md +17 -1
  2. package/dist/chat-history/ChatHistoryList.d.ts +103 -0
  3. package/dist/chat-history/ChatHistoryList.d.ts.map +1 -0
  4. package/dist/chat-history/ChatHistoryList.js +109 -0
  5. package/dist/chat-history/ChatHistoryList.js.map +1 -0
  6. package/dist/chat-history/ChatHistoryList.spec.d.ts +2 -0
  7. package/dist/chat-history/ChatHistoryList.spec.d.ts.map +1 -0
  8. package/dist/chat-history/ChatHistoryList.spec.js +283 -0
  9. package/dist/chat-history/ChatHistoryList.spec.js.map +1 -0
  10. package/dist/chat-history/index.d.ts +2 -0
  11. package/dist/chat-history/index.d.ts.map +1 -0
  12. package/dist/chat-history/index.js +2 -0
  13. package/dist/chat-history/index.js.map +1 -0
  14. package/dist/chat-history.css +314 -0
  15. package/package.json +17 -1
  16. package/src/app-shell/header-actions.tsx +84 -0
  17. package/src/app-shell/index.ts +1 -0
  18. package/src/chat-history/ChatHistoryList.spec.tsx +453 -0
  19. package/src/chat-history/ChatHistoryList.tsx +469 -0
  20. package/src/chat-history/index.ts +7 -0
  21. package/src/chat-history.css +314 -0
  22. package/src/collab-ui/AgentPresenceChip.tsx +69 -0
  23. package/src/collab-ui/LiveCursorOverlay.tsx +241 -0
  24. package/src/collab-ui/PresenceBar.tsx +346 -0
  25. package/src/collab-ui/RecentEditHighlights.tsx +225 -0
  26. package/src/collab-ui/RemoteSelectionRings.tsx +227 -0
  27. package/src/collab-ui/agent-identity.ts +16 -0
  28. package/src/collab-ui/index.ts +39 -0
  29. package/src/collab-ui/lead-client.ts +32 -0
  30. package/src/collab-ui/types.spec.ts +18 -0
  31. package/src/collab-ui/types.ts +96 -0
  32. package/src/composer/AgentComposerFrame.tsx +59 -0
  33. package/src/composer/ComposerPlusMenu.tsx +983 -0
  34. package/src/composer/MentionPopover.tsx +412 -0
  35. package/src/composer/PastedTextChip.tsx +150 -0
  36. package/src/composer/PromptComposer.spec.ts +73 -0
  37. package/src/composer/PromptComposer.tsx +718 -0
  38. package/src/composer/RealtimeVoiceMode.spec.tsx +771 -0
  39. package/src/composer/RealtimeVoiceMode.tsx +856 -0
  40. package/src/composer/TiptapComposer.spec.ts +390 -0
  41. package/src/composer/TiptapComposer.tsx +2936 -0
  42. package/src/composer/VoiceButton.spec.ts +33 -0
  43. package/src/composer/VoiceButton.tsx +255 -0
  44. package/src/composer/asset-picker-url.spec.ts +56 -0
  45. package/src/composer/asset-picker-url.ts +80 -0
  46. package/src/composer/attachment-accept.spec.ts +22 -0
  47. package/src/composer/attachment-accept.ts +71 -0
  48. package/src/composer/draft-key.spec.ts +19 -0
  49. package/src/composer/draft-key.ts +7 -0
  50. package/src/composer/extensions/FileReference.tsx +55 -0
  51. package/src/composer/extensions/MentionReference.tsx +93 -0
  52. package/src/composer/extensions/SkillReference.tsx +56 -0
  53. package/src/composer/index.ts +79 -0
  54. package/src/composer/pasted-text.spec.ts +162 -0
  55. package/src/composer/pasted-text.ts +160 -0
  56. package/src/composer/prompt-attachments.spec.ts +80 -0
  57. package/src/composer/prompt-attachments.ts +93 -0
  58. package/src/composer/realtime-voice-audio-level.spec.ts +32 -0
  59. package/src/composer/realtime-voice-audio-level.ts +64 -0
  60. package/src/composer/realtime-voice-transcript.spec.ts +95 -0
  61. package/src/composer/realtime-voice-transcript.ts +134 -0
  62. package/src/composer/runtime-adapters.tsx +343 -0
  63. package/src/composer/types.ts +66 -0
  64. package/src/composer/use-file-search.ts +64 -0
  65. package/src/composer/use-mention-search.ts +90 -0
  66. package/src/composer/use-skills.ts +45 -0
  67. package/src/composer/useRealtimeVoiceMode.spec.ts +818 -0
  68. package/src/composer/useRealtimeVoiceMode.tsx +2117 -0
  69. package/src/composer/useVoiceDictation.spec.ts +45 -0
  70. package/src/composer/useVoiceDictation.ts +1191 -0
  71. package/src/context-ui/ContextMeter.tsx +101 -0
  72. package/src/context-ui/ContextSegmentRow.tsx +184 -0
  73. package/src/context-ui/ContextTreemap.tsx +125 -0
  74. package/src/context-ui/ContextXRayPanel.tsx +377 -0
  75. package/src/context-ui/format.ts +42 -0
  76. package/src/context-ui/index.ts +15 -0
  77. package/src/context-ui/types.ts +43 -0
  78. package/src/design-tweaks/index.ts +1 -0
  79. package/src/design-tweaks/visual-style-controls.tsx +1084 -0
  80. package/src/editor/BubbleToolbar.tsx +253 -0
  81. package/src/editor/CodeBlockNode.tsx +254 -0
  82. package/src/editor/DragHandle.spec.ts +588 -0
  83. package/src/editor/DragHandle.ts +1437 -0
  84. package/src/editor/ImageExtension.roundtrip.spec.ts +117 -0
  85. package/src/editor/ImageExtension.ts +306 -0
  86. package/src/editor/RegistryBlockContext.tsx +107 -0
  87. package/src/editor/RegistryBlockNode.spec.ts +279 -0
  88. package/src/editor/RegistryBlockNode.tsx +652 -0
  89. package/src/editor/RichMarkdownEditor.collab.spec.ts +79 -0
  90. package/src/editor/RichMarkdownEditor.roundtrip.spec.ts +226 -0
  91. package/src/editor/RichMarkdownEditor.tsx +81 -0
  92. package/src/editor/RunId.ts +61 -0
  93. package/src/editor/SharedRichEditor.tsx +321 -0
  94. package/src/editor/SlashCommandMenu.tsx +310 -0
  95. package/src/editor/disableHistory.spec.ts +85 -0
  96. package/src/editor/extensions.ts +402 -0
  97. package/src/editor/gfmDoc.spec.ts +230 -0
  98. package/src/editor/gfmDoc.ts +92 -0
  99. package/src/editor/index.ts +93 -0
  100. package/src/editor/registrySlashCommands.ts +131 -0
  101. package/src/editor/structuralReorderUndo.spec.ts +144 -0
  102. package/src/editor/surgical-apply.spec.ts +279 -0
  103. package/src/editor/surgical-apply.ts +192 -0
  104. package/src/editor/useCollabReconcile.concurrent.spec.ts +422 -0
  105. package/src/editor/useCollabReconcile.idempotent.spec.ts +324 -0
  106. package/src/editor/useCollabReconcile.ts +589 -0
  107. package/src/editor.css +510 -0
  108. package/src/hooks/index.ts +2 -0
  109. package/src/hooks/use-mobile.tsx +21 -0
  110. package/src/hooks/use-toast.ts +188 -0
  111. package/src/index.ts +8 -0
  112. package/src/onboarding/index.ts +1 -0
  113. package/src/provider.tsx +47 -0
  114. package/src/sharing/VisibilityBadge.spec.ts +96 -0
  115. package/src/sharing/VisibilityBadge.tsx +37 -0
  116. package/src/sharing/index.ts +4 -0
  117. package/src/styles.css +21 -0
  118. package/src/ui/accordion.tsx +56 -0
  119. package/src/ui/alert-dialog.tsx +141 -0
  120. package/src/ui/alert.tsx +59 -0
  121. package/src/ui/aspect-ratio.tsx +5 -0
  122. package/src/ui/avatar.tsx +48 -0
  123. package/src/ui/badge.tsx +37 -0
  124. package/src/ui/breadcrumb.tsx +115 -0
  125. package/src/ui/button.tsx +78 -0
  126. package/src/ui/calendar.tsx +214 -0
  127. package/src/ui/card.tsx +86 -0
  128. package/src/ui/carousel.tsx +260 -0
  129. package/src/ui/chart.tsx +375 -0
  130. package/src/ui/checkbox.tsx +28 -0
  131. package/src/ui/collapsible.tsx +9 -0
  132. package/src/ui/command.spec.tsx +51 -0
  133. package/src/ui/command.tsx +174 -0
  134. package/src/ui/context-menu.tsx +202 -0
  135. package/src/ui/date-picker.tsx +64 -0
  136. package/src/ui/dialog.spec.tsx +43 -0
  137. package/src/ui/dialog.tsx +146 -0
  138. package/src/ui/drawer.tsx +126 -0
  139. package/src/ui/dropdown-menu.tsx +218 -0
  140. package/src/ui/form.tsx +177 -0
  141. package/src/ui/hover-card.tsx +27 -0
  142. package/src/ui/index.ts +52 -0
  143. package/src/ui/input-otp.tsx +69 -0
  144. package/src/ui/input.tsx +22 -0
  145. package/src/ui/label.tsx +24 -0
  146. package/src/ui/logo.tsx +36 -0
  147. package/src/ui/menubar.tsx +235 -0
  148. package/src/ui/navigation-menu.tsx +128 -0
  149. package/src/ui/pagination.tsx +121 -0
  150. package/src/ui/popover.tsx +44 -0
  151. package/src/ui/progress.tsx +26 -0
  152. package/src/ui/radio-group.tsx +42 -0
  153. package/src/ui/resizable.tsx +43 -0
  154. package/src/ui/scroll-area.tsx +46 -0
  155. package/src/ui/select.tsx +158 -0
  156. package/src/ui/separator.tsx +29 -0
  157. package/src/ui/sheet.tsx +160 -0
  158. package/src/ui/sidebar.tsx +777 -0
  159. package/src/ui/skeleton.tsx +18 -0
  160. package/src/ui/slider.tsx +26 -0
  161. package/src/ui/sonner.tsx +58 -0
  162. package/src/ui/spinner.tsx +17 -0
  163. package/src/ui/switch.tsx +27 -0
  164. package/src/ui/table.tsx +117 -0
  165. package/src/ui/tabs.tsx +53 -0
  166. package/src/ui/textarea.tsx +23 -0
  167. package/src/ui/toast.tsx +127 -0
  168. package/src/ui/toaster.tsx +33 -0
  169. package/src/ui/toggle-group.tsx +59 -0
  170. package/src/ui/toggle.tsx +43 -0
  171. package/src/ui/tooltip.tsx +53 -0
  172. package/src/ui/use-toast.ts +3 -0
  173. package/src/utils.ts +8 -0
@@ -0,0 +1,453 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { act } from "react";
4
+ import { createRoot, type Root } from "react-dom/client";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+
7
+ import {
8
+ ChatHistoryList,
9
+ type ChatHistoryItem,
10
+ type ChatHistorySection,
11
+ } from "./ChatHistoryList.js";
12
+
13
+ function item(
14
+ overrides: Partial<ChatHistoryItem> & { id: string },
15
+ ): ChatHistoryItem {
16
+ return {
17
+ title: overrides.id,
18
+ ...overrides,
19
+ };
20
+ }
21
+
22
+ /** React tracks the DOM input's value via a wrapped setter to decide whether
23
+ * to fire its synthetic change handler, so setting `.value` directly is not
24
+ * observed. Go through the native prototype setter instead, matching the
25
+ * pattern used by CommandMenu.spec.tsx. */
26
+ function typeIntoInput(input: HTMLInputElement, value: string) {
27
+ const setter = Object.getOwnPropertyDescriptor(
28
+ window.HTMLInputElement.prototype,
29
+ "value",
30
+ )?.set;
31
+ setter?.call(input, value);
32
+ input.dispatchEvent(new Event("input", { bubbles: true }));
33
+ input.dispatchEvent(new Event("change", { bubbles: true }));
34
+ }
35
+
36
+ describe("ChatHistoryList", () => {
37
+ let container: HTMLDivElement;
38
+ let root: Root;
39
+
40
+ beforeEach(() => {
41
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
42
+ vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {
43
+ cb(0);
44
+ return 0;
45
+ });
46
+ container = document.createElement("div");
47
+ document.body.appendChild(container);
48
+ root = createRoot(container);
49
+ });
50
+
51
+ afterEach(() => {
52
+ act(() => root.unmount());
53
+ container.remove();
54
+ document.body.innerHTML = "";
55
+ vi.restoreAllMocks();
56
+ vi.unstubAllGlobals();
57
+ });
58
+
59
+ it("renders a flat item list and calls onSelect when a row is clicked", () => {
60
+ const onSelect = vi.fn();
61
+ const items: ChatHistoryItem[] = [
62
+ item({ id: "thread-1", title: "First chat", timestamp: "Yesterday" }),
63
+ item({ id: "thread-2", title: "Second chat", timestamp: "2 min ago" }),
64
+ ];
65
+
66
+ act(() => {
67
+ root.render(<ChatHistoryList items={items} onSelect={onSelect} />);
68
+ });
69
+
70
+ const titles = Array.from(
71
+ container.querySelectorAll(".an-chat-history-row__title"),
72
+ ).map((el) => el.textContent);
73
+ expect(titles).toEqual(["First chat", "Second chat"]);
74
+ expect(container.textContent).toContain("Yesterday");
75
+ expect(container.textContent).toContain("2 min ago");
76
+
77
+ const buttons = container.querySelectorAll(".an-chat-history-row__button");
78
+ act(() => {
79
+ (buttons[1] as HTMLButtonElement).click();
80
+ });
81
+ expect(onSelect).toHaveBeenCalledWith("thread-2");
82
+ });
83
+
84
+ it("highlights the active item", () => {
85
+ const items: ChatHistoryItem[] = [
86
+ item({ id: "thread-1" }),
87
+ item({ id: "thread-2" }),
88
+ ];
89
+
90
+ act(() => {
91
+ root.render(
92
+ <ChatHistoryList
93
+ items={items}
94
+ activeId="thread-2"
95
+ onSelect={() => {}}
96
+ />,
97
+ );
98
+ });
99
+
100
+ const rows = container.querySelectorAll(".an-chat-history-row");
101
+ expect(rows[0].className).not.toContain("an-chat-history-row--active");
102
+ expect(rows[1].className).toContain("an-chat-history-row--active");
103
+ });
104
+
105
+ it("renders grouped sections with labels", () => {
106
+ const sections: ChatHistorySection[] = [
107
+ {
108
+ id: "scoped",
109
+ label: "This deck",
110
+ items: [item({ id: "thread-1", title: "Deck chat" })],
111
+ },
112
+ {
113
+ id: "other",
114
+ label: "All chats",
115
+ items: [item({ id: "thread-2", title: "Other chat" })],
116
+ },
117
+ ];
118
+
119
+ act(() => {
120
+ root.render(<ChatHistoryList sections={sections} onSelect={() => {}} />);
121
+ });
122
+
123
+ const labels = Array.from(
124
+ container.querySelectorAll(".an-chat-history__section-label"),
125
+ ).map((el) => el.textContent);
126
+ expect(labels).toEqual(["This deck", "All chats"]);
127
+ });
128
+
129
+ it("shows the empty state, distinguishing plain-empty from no-search-results", () => {
130
+ act(() => {
131
+ root.render(<ChatHistoryList items={[]} onSelect={() => {}} />);
132
+ });
133
+ expect(container.textContent).toContain("No chats yet");
134
+
135
+ act(() => {
136
+ root.render(
137
+ <ChatHistoryList
138
+ items={[]}
139
+ searchValue="foo"
140
+ onSearchChange={() => {}}
141
+ onSelect={() => {}}
142
+ />,
143
+ );
144
+ });
145
+ expect(container.textContent).toContain("No matching chats");
146
+ });
147
+
148
+ it("shows a loading state instead of the list", () => {
149
+ const items: ChatHistoryItem[] = [item({ id: "thread-1" })];
150
+ act(() => {
151
+ root.render(
152
+ <ChatHistoryList
153
+ items={items}
154
+ loading
155
+ loadingLabel="Searching..."
156
+ onSelect={() => {}}
157
+ />,
158
+ );
159
+ });
160
+ expect(container.textContent).toContain("Searching...");
161
+ expect(container.querySelector(".an-chat-history-row")).toBeNull();
162
+ });
163
+
164
+ it("shows an error state that takes priority over loading and items", () => {
165
+ const items: ChatHistoryItem[] = [item({ id: "thread-1" })];
166
+ act(() => {
167
+ root.render(
168
+ <ChatHistoryList
169
+ items={items}
170
+ loading
171
+ error="Could not load chats"
172
+ onSelect={() => {}}
173
+ />,
174
+ );
175
+ });
176
+ expect(container.textContent).toContain("Could not load chats");
177
+ expect(container.querySelector(".an-chat-history-row")).toBeNull();
178
+ });
179
+
180
+ it("renders a controlled search box only when onSearchChange is supplied", () => {
181
+ const onSearchChange = vi.fn();
182
+ act(() => {
183
+ root.render(
184
+ <ChatHistoryList
185
+ items={[]}
186
+ searchValue="abc"
187
+ onSearchChange={onSearchChange}
188
+ onSelect={() => {}}
189
+ />,
190
+ );
191
+ });
192
+ const input = container.querySelector<HTMLInputElement>(
193
+ ".an-chat-history__search-input",
194
+ );
195
+ expect(input).not.toBeNull();
196
+ expect(input!.value).toBe("abc");
197
+
198
+ act(() => {
199
+ typeIntoInput(input!, "abcd");
200
+ });
201
+ expect(onSearchChange).toHaveBeenCalledWith("abcd");
202
+
203
+ act(() => {
204
+ root.render(<ChatHistoryList items={[]} onSelect={() => {}} />);
205
+ });
206
+ expect(
207
+ container.querySelector(".an-chat-history__search-input"),
208
+ ).toBeNull();
209
+ });
210
+
211
+ it("does not render a row action menu when no menu callbacks are given", () => {
212
+ const items: ChatHistoryItem[] = [item({ id: "thread-1" })];
213
+ act(() => {
214
+ root.render(<ChatHistoryList items={items} onSelect={() => {}} />);
215
+ });
216
+ expect(container.querySelector(".an-chat-history-row__menu")).toBeNull();
217
+ });
218
+
219
+ it("supports pin toggling via the row action menu", () => {
220
+ const onTogglePin = vi.fn();
221
+ const items: ChatHistoryItem[] = [
222
+ item({ id: "thread-1", title: "First chat" }),
223
+ ];
224
+ act(() => {
225
+ root.render(
226
+ <ChatHistoryList
227
+ items={items}
228
+ onSelect={() => {}}
229
+ onTogglePin={onTogglePin}
230
+ />,
231
+ );
232
+ });
233
+
234
+ const trigger = container.querySelector<HTMLButtonElement>(
235
+ ".an-chat-history-row__menu-trigger",
236
+ );
237
+ expect(trigger).not.toBeNull();
238
+ act(() => {
239
+ trigger!.click();
240
+ });
241
+
242
+ const pinItem = Array.from(
243
+ container.querySelectorAll(".an-chat-history-row__menu-item"),
244
+ ).find((el) => el.textContent?.includes("Pin to top"));
245
+ expect(pinItem).toBeDefined();
246
+ act(() => {
247
+ (pinItem as HTMLButtonElement).click();
248
+ });
249
+ expect(onTogglePin).toHaveBeenCalledWith("thread-1");
250
+ });
251
+
252
+ it("uses custom labels for localized row actions", () => {
253
+ const onTogglePin = vi.fn();
254
+ const onDelete = vi.fn();
255
+ const labels = {
256
+ options: "Conversation actions",
257
+ renameInput: "Rename analytics conversation",
258
+ rename: "Rename conversation",
259
+ pin: "Keep at top",
260
+ unpin: "Remove from top",
261
+ delete: "Archive conversation",
262
+ };
263
+ const render = (pinned = false) => {
264
+ act(() => {
265
+ root.render(
266
+ <ChatHistoryList
267
+ key={String(pinned)}
268
+ items={[item({ id: "thread-1", titleText: "Campaign", pinned })]}
269
+ onSelect={() => {}}
270
+ onRename={() => {}}
271
+ onTogglePin={onTogglePin}
272
+ onDelete={onDelete}
273
+ labels={labels}
274
+ />,
275
+ );
276
+ });
277
+ };
278
+
279
+ render();
280
+ const trigger = container.querySelector<HTMLButtonElement>(
281
+ ".an-chat-history-row__menu-trigger",
282
+ );
283
+ expect(trigger?.getAttribute("aria-label")).toBe(labels.options);
284
+ act(() => {
285
+ trigger!.click();
286
+ });
287
+ expect(container.textContent).toContain(labels.rename);
288
+ expect(container.textContent).toContain(labels.pin);
289
+ expect(container.textContent).toContain(labels.delete);
290
+
291
+ const archiveItem = Array.from(
292
+ container.querySelectorAll(".an-chat-history-row__menu-item"),
293
+ ).find((element) => element.textContent?.includes(labels.delete));
294
+ act(() => {
295
+ (archiveItem as HTMLButtonElement).click();
296
+ });
297
+ expect(onDelete).toHaveBeenCalledWith("thread-1");
298
+
299
+ render();
300
+ const renameTrigger = container.querySelector<HTMLButtonElement>(
301
+ ".an-chat-history-row__menu-trigger",
302
+ );
303
+ act(() => {
304
+ renameTrigger!.click();
305
+ });
306
+ const renameItem = Array.from(
307
+ container.querySelectorAll(".an-chat-history-row__menu-item"),
308
+ ).find((element) => element.textContent?.includes(labels.rename));
309
+ act(() => {
310
+ (renameItem as HTMLButtonElement).click();
311
+ });
312
+ expect(
313
+ container
314
+ .querySelector<HTMLInputElement>(".an-chat-history-row__rename-input")
315
+ ?.getAttribute("aria-label"),
316
+ ).toBe(labels.renameInput);
317
+
318
+ render(true);
319
+ const pinnedTrigger = container.querySelector<HTMLButtonElement>(
320
+ ".an-chat-history-row__menu-trigger",
321
+ );
322
+ act(() => {
323
+ pinnedTrigger!.click();
324
+ });
325
+ expect(container.textContent).toContain(labels.unpin);
326
+ });
327
+
328
+ it("supports inline rename via the row action menu", () => {
329
+ const onRename = vi.fn();
330
+ const items: ChatHistoryItem[] = [
331
+ item({ id: "thread-1", title: "Old title", titleText: "Old title" }),
332
+ ];
333
+ act(() => {
334
+ root.render(
335
+ <ChatHistoryList
336
+ items={items}
337
+ onSelect={() => {}}
338
+ onRename={onRename}
339
+ renameMaxLength={160}
340
+ />,
341
+ );
342
+ });
343
+
344
+ const trigger = container.querySelector<HTMLButtonElement>(
345
+ ".an-chat-history-row__menu-trigger",
346
+ );
347
+ act(() => {
348
+ trigger!.click();
349
+ });
350
+ const renameItem = Array.from(
351
+ container.querySelectorAll(".an-chat-history-row__menu-item"),
352
+ ).find((el) => el.textContent?.includes("Rename"));
353
+ act(() => {
354
+ (renameItem as HTMLButtonElement).click();
355
+ });
356
+
357
+ const input = container.querySelector<HTMLInputElement>(
358
+ ".an-chat-history-row__rename-input",
359
+ );
360
+ expect(input).not.toBeNull();
361
+ expect(input!.value).toBe("Old title");
362
+ expect(input!.maxLength).toBe(160);
363
+
364
+ act(() => {
365
+ typeIntoInput(input!, "New title");
366
+ });
367
+ act(() => {
368
+ input!.dispatchEvent(
369
+ new KeyboardEvent("keydown", { key: "Enter", bubbles: true }),
370
+ );
371
+ });
372
+ expect(onRename).toHaveBeenCalledWith("thread-1", "New title");
373
+ });
374
+
375
+ it("supports item-aware labels and app-specific actions", () => {
376
+ const onShare = vi.fn();
377
+ const items: ChatHistoryItem[] = [
378
+ item({ id: "thread-1", title: "Campaign", titleText: "Campaign" }),
379
+ ];
380
+ act(() => {
381
+ root.render(
382
+ <ChatHistoryList
383
+ items={items}
384
+ onSelect={() => {}}
385
+ onRename={() => {}}
386
+ labels={{
387
+ options: (chat) => `Options for ${chat.titleText}`,
388
+ renameInput: (chat) => `Rename ${chat.titleText}`,
389
+ }}
390
+ renderAdditionalRowActions={(chat, closeMenu) => (
391
+ <button
392
+ type="button"
393
+ className="an-chat-history-row__menu-item"
394
+ onClick={() => {
395
+ closeMenu();
396
+ onShare(chat.id);
397
+ }}
398
+ >
399
+ Share
400
+ </button>
401
+ )}
402
+ />,
403
+ );
404
+ });
405
+
406
+ const trigger = container.querySelector<HTMLButtonElement>(
407
+ ".an-chat-history-row__menu-trigger",
408
+ );
409
+ expect(trigger?.getAttribute("aria-label")).toBe("Options for Campaign");
410
+ act(() => {
411
+ trigger!.click();
412
+ });
413
+
414
+ const shareItem = Array.from(
415
+ container.querySelectorAll(".an-chat-history-row__menu-item"),
416
+ ).find((element) => element.textContent === "Share");
417
+ act(() => {
418
+ (shareItem as HTMLButtonElement).click();
419
+ });
420
+ expect(onShare).toHaveBeenCalledWith("thread-1");
421
+ expect(
422
+ container.querySelector(".an-chat-history-row__menu-content"),
423
+ ).toBeNull();
424
+
425
+ act(() => {
426
+ trigger!.click();
427
+ });
428
+ const renameItem = Array.from(
429
+ container.querySelectorAll(".an-chat-history-row__menu-item"),
430
+ ).find((element) => element.textContent === "Rename");
431
+ act(() => {
432
+ (renameItem as HTMLButtonElement).click();
433
+ });
434
+ expect(
435
+ container
436
+ .querySelector<HTMLInputElement>(".an-chat-history-row__rename-input")
437
+ ?.getAttribute("aria-label"),
438
+ ).toBe("Rename Campaign");
439
+ });
440
+
441
+ it("renders the footer inside the scroll region regardless of list state", () => {
442
+ act(() => {
443
+ root.render(
444
+ <ChatHistoryList
445
+ items={[]}
446
+ onSelect={() => {}}
447
+ footer={<div data-testid="footer">Load older chats</div>}
448
+ />,
449
+ );
450
+ });
451
+ expect(container.textContent).toContain("Load older chats");
452
+ });
453
+ });