@agent-native/toolkit 0.14.0 → 0.14.2

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 (29) hide show
  1. package/dist/canvas-interactions/canvas-interactions.d.ts +3 -3
  2. package/dist/canvas-interactions/canvas-interactions.d.ts.map +1 -1
  3. package/dist/canvas-interactions/canvas-interactions.js +54 -19
  4. package/dist/canvas-interactions/canvas-interactions.js.map +1 -1
  5. package/dist/canvas-interactions/canvas-interactions.spec.js +57 -2
  6. package/dist/canvas-interactions/canvas-interactions.spec.js.map +1 -1
  7. package/dist/chat-history/ChatHistoryList.d.ts.map +1 -1
  8. package/dist/chat-history/ChatHistoryList.js +9 -28
  9. package/dist/chat-history/ChatHistoryList.js.map +1 -1
  10. package/dist/chat-history/ChatHistoryList.spec.js +46 -36
  11. package/dist/chat-history/ChatHistoryList.spec.js.map +1 -1
  12. package/dist/chat-history.css +5 -15
  13. package/dist/collab-ui/PresenceBar.js +1 -1
  14. package/dist/collab-ui/PresenceBar.js.map +1 -1
  15. package/dist/collab-ui/PresenceBar.spec.d.ts +2 -0
  16. package/dist/collab-ui/PresenceBar.spec.d.ts.map +1 -0
  17. package/dist/collab-ui/PresenceBar.spec.js +30 -0
  18. package/dist/collab-ui/PresenceBar.spec.js.map +1 -0
  19. package/dist/composer/VoiceButton.js +1 -1
  20. package/dist/composer/VoiceButton.js.map +1 -1
  21. package/package.json +1 -1
  22. package/src/canvas-interactions/canvas-interactions.spec.ts +79 -2
  23. package/src/canvas-interactions/canvas-interactions.ts +56 -24
  24. package/src/chat-history/ChatHistoryList.spec.tsx +80 -48
  25. package/src/chat-history/ChatHistoryList.tsx +86 -93
  26. package/src/chat-history.css +5 -15
  27. package/src/collab-ui/PresenceBar.spec.tsx +44 -0
  28. package/src/collab-ui/PresenceBar.tsx +1 -1
  29. package/src/composer/VoiceButton.tsx +1 -1
@@ -33,6 +33,27 @@ function typeIntoInput(input: HTMLInputElement, value: string) {
33
33
  input.dispatchEvent(new Event("change", { bubbles: true }));
34
34
  }
35
35
 
36
+ async function openMenu(trigger: HTMLButtonElement) {
37
+ await act(async () => {
38
+ trigger.dispatchEvent(
39
+ new PointerEvent("pointerdown", {
40
+ bubbles: true,
41
+ button: 0,
42
+ pointerType: "mouse",
43
+ }),
44
+ );
45
+ await Promise.resolve();
46
+ });
47
+ }
48
+
49
+ function getMenuItems() {
50
+ return Array.from(
51
+ document.body.querySelectorAll<HTMLElement>(
52
+ ".an-chat-history-row__menu-item",
53
+ ),
54
+ );
55
+ }
56
+
36
57
  describe("ChatHistoryList", () => {
37
58
  let container: HTMLDivElement;
38
59
  let root: Root;
@@ -216,7 +237,7 @@ describe("ChatHistoryList", () => {
216
237
  expect(container.querySelector(".an-chat-history-row__menu")).toBeNull();
217
238
  });
218
239
 
219
- it("supports pin toggling via the row action menu", () => {
240
+ it("supports pin toggling via the row action menu", async () => {
220
241
  const onTogglePin = vi.fn();
221
242
  const items: ChatHistoryItem[] = [
222
243
  item({ id: "thread-1", title: "First chat" }),
@@ -235,13 +256,11 @@ describe("ChatHistoryList", () => {
235
256
  ".an-chat-history-row__menu-trigger",
236
257
  );
237
258
  expect(trigger).not.toBeNull();
238
- act(() => {
239
- trigger!.click();
240
- });
259
+ await openMenu(trigger!);
241
260
 
242
- const pinItem = Array.from(
243
- container.querySelectorAll(".an-chat-history-row__menu-item"),
244
- ).find((el) => el.textContent?.includes("Pin to top"));
261
+ const pinItem = getMenuItems().find((el) =>
262
+ el.textContent?.includes("Pin to top"),
263
+ );
245
264
  expect(pinItem).toBeDefined();
246
265
  act(() => {
247
266
  (pinItem as HTMLButtonElement).click();
@@ -249,7 +268,32 @@ describe("ChatHistoryList", () => {
249
268
  expect(onTogglePin).toHaveBeenCalledWith("thread-1");
250
269
  });
251
270
 
252
- it("uses custom labels for localized row actions", () => {
271
+ it("ports rail row menus outside the scroll container", async () => {
272
+ act(() => {
273
+ root.render(
274
+ <ChatHistoryList
275
+ variant="rail"
276
+ items={[item({ id: "thread-1" })]}
277
+ onSelect={() => {}}
278
+ onDelete={() => {}}
279
+ />,
280
+ );
281
+ });
282
+
283
+ const trigger = container.querySelector<HTMLButtonElement>(
284
+ ".an-chat-history-row__menu-trigger",
285
+ );
286
+ await openMenu(trigger!);
287
+
288
+ const menu = document.body.querySelector<HTMLElement>(
289
+ ".an-chat-history-row__menu-content",
290
+ );
291
+ expect(menu).not.toBeNull();
292
+ expect(menu?.closest(".an-chat-history__list")).toBeNull();
293
+ expect(menu?.getAttribute("role")).toBe("menu");
294
+ });
295
+
296
+ it("uses custom labels for localized row actions", async () => {
253
297
  const onTogglePin = vi.fn();
254
298
  const onDelete = vi.fn();
255
299
  const labels = {
@@ -281,16 +325,14 @@ describe("ChatHistoryList", () => {
281
325
  ".an-chat-history-row__menu-trigger",
282
326
  );
283
327
  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);
328
+ await openMenu(trigger!);
329
+ expect(document.body.textContent).toContain(labels.rename);
330
+ expect(document.body.textContent).toContain(labels.pin);
331
+ expect(document.body.textContent).toContain(labels.delete);
290
332
 
291
- const archiveItem = Array.from(
292
- container.querySelectorAll(".an-chat-history-row__menu-item"),
293
- ).find((element) => element.textContent?.includes(labels.delete));
333
+ const archiveItem = getMenuItems().find((element) =>
334
+ element.textContent?.includes(labels.delete),
335
+ );
294
336
  act(() => {
295
337
  (archiveItem as HTMLButtonElement).click();
296
338
  });
@@ -300,12 +342,10 @@ describe("ChatHistoryList", () => {
300
342
  const renameTrigger = container.querySelector<HTMLButtonElement>(
301
343
  ".an-chat-history-row__menu-trigger",
302
344
  );
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));
345
+ await openMenu(renameTrigger!);
346
+ const renameItem = getMenuItems().find((element) =>
347
+ element.textContent?.includes(labels.rename),
348
+ );
309
349
  act(() => {
310
350
  (renameItem as HTMLButtonElement).click();
311
351
  });
@@ -319,13 +359,11 @@ describe("ChatHistoryList", () => {
319
359
  const pinnedTrigger = container.querySelector<HTMLButtonElement>(
320
360
  ".an-chat-history-row__menu-trigger",
321
361
  );
322
- act(() => {
323
- pinnedTrigger!.click();
324
- });
325
- expect(container.textContent).toContain(labels.unpin);
362
+ await openMenu(pinnedTrigger!);
363
+ expect(document.body.textContent).toContain(labels.unpin);
326
364
  });
327
365
 
328
- it("supports inline rename via the row action menu", () => {
366
+ it("supports inline rename via the row action menu", async () => {
329
367
  const onRename = vi.fn();
330
368
  const items: ChatHistoryItem[] = [
331
369
  item({ id: "thread-1", title: "Old title", titleText: "Old title" }),
@@ -344,12 +382,10 @@ describe("ChatHistoryList", () => {
344
382
  const trigger = container.querySelector<HTMLButtonElement>(
345
383
  ".an-chat-history-row__menu-trigger",
346
384
  );
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"));
385
+ await openMenu(trigger!);
386
+ const renameItem = getMenuItems().find((el) =>
387
+ el.textContent?.includes("Rename"),
388
+ );
353
389
  act(() => {
354
390
  (renameItem as HTMLButtonElement).click();
355
391
  });
@@ -372,7 +408,7 @@ describe("ChatHistoryList", () => {
372
408
  expect(onRename).toHaveBeenCalledWith("thread-1", "New title");
373
409
  });
374
410
 
375
- it("supports item-aware labels and app-specific actions", () => {
411
+ it("supports item-aware labels and app-specific actions", async () => {
376
412
  const onShare = vi.fn();
377
413
  const items: ChatHistoryItem[] = [
378
414
  item({ id: "thread-1", title: "Campaign", titleText: "Campaign" }),
@@ -407,27 +443,23 @@ describe("ChatHistoryList", () => {
407
443
  ".an-chat-history-row__menu-trigger",
408
444
  );
409
445
  expect(trigger?.getAttribute("aria-label")).toBe("Options for Campaign");
410
- act(() => {
411
- trigger!.click();
412
- });
446
+ await openMenu(trigger!);
413
447
 
414
- const shareItem = Array.from(
415
- container.querySelectorAll(".an-chat-history-row__menu-item"),
416
- ).find((element) => element.textContent === "Share");
448
+ const shareItem = getMenuItems().find(
449
+ (element) => element.textContent === "Share",
450
+ );
417
451
  act(() => {
418
452
  (shareItem as HTMLButtonElement).click();
419
453
  });
420
454
  expect(onShare).toHaveBeenCalledWith("thread-1");
421
455
  expect(
422
- container.querySelector(".an-chat-history-row__menu-content"),
456
+ document.body.querySelector(".an-chat-history-row__menu-content"),
423
457
  ).toBeNull();
424
458
 
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");
459
+ await openMenu(trigger!);
460
+ const renameItem = getMenuItems().find(
461
+ (element) => element.textContent === "Rename",
462
+ );
431
463
  act(() => {
432
464
  (renameItem as HTMLButtonElement).click();
433
465
  });
@@ -1,3 +1,4 @@
1
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
1
2
  import {
2
3
  IconDots,
3
4
  IconPencil,
@@ -6,7 +7,7 @@ import {
6
7
  IconSearch,
7
8
  IconTrash,
8
9
  } from "@tabler/icons-react";
9
- import React, { useEffect, useMemo, useRef, useState } from "react";
10
+ import React, { useMemo, useRef, useState } from "react";
10
11
 
11
12
  import { cn } from "../utils.js";
12
13
 
@@ -288,7 +289,6 @@ const ChatHistoryRow = React.memo(function ChatHistoryRow({
288
289
  const [draftTitle, setDraftTitle] = useState("");
289
290
  const [menuOpen, setMenuOpen] = useState(false);
290
291
  const renameInputRef = useRef<HTMLInputElement | null>(null);
291
- const menuRef = useRef<HTMLDivElement | null>(null);
292
292
 
293
293
  const hasMenu = Boolean(
294
294
  onTogglePin ||
@@ -298,24 +298,6 @@ const ChatHistoryRow = React.memo(function ChatHistoryRow({
298
298
  renderAdditionalRowActions,
299
299
  );
300
300
 
301
- useEffect(() => {
302
- if (!menuOpen) return;
303
- function handlePointerDown(event: PointerEvent) {
304
- if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
305
- setMenuOpen(false);
306
- }
307
- }
308
- function handleKeyDown(event: KeyboardEvent) {
309
- if (event.key === "Escape") setMenuOpen(false);
310
- }
311
- document.addEventListener("pointerdown", handlePointerDown);
312
- document.addEventListener("keydown", handleKeyDown);
313
- return () => {
314
- document.removeEventListener("pointerdown", handlePointerDown);
315
- document.removeEventListener("keydown", handleKeyDown);
316
- };
317
- }, [menuOpen]);
318
-
319
301
  function startRename() {
320
302
  setDraftTitle(item.titleText ?? "");
321
303
  setIsRenaming(true);
@@ -409,79 +391,90 @@ const ChatHistoryRow = React.memo(function ChatHistoryRow({
409
391
  )}
410
392
 
411
393
  {!isRenaming && hasMenu && (
412
- <div className="an-chat-history-row__menu" ref={menuRef}>
413
- <button
414
- type="button"
415
- className={cn(
416
- "an-chat-history-row__menu-trigger",
417
- item.pinned && "an-chat-history-row__menu-trigger--pinned",
418
- )}
419
- aria-label={optionsLabel}
420
- aria-haspopup="menu"
421
- aria-expanded={menuOpen}
422
- onClick={() => setMenuOpen((open) => !open)}
423
- >
424
- {item.pinned ? (
425
- <IconPinned size={13} strokeWidth={1.8} />
426
- ) : (
427
- <IconDots size={14} strokeWidth={1.8} />
428
- )}
429
- </button>
430
- {menuOpen && (
431
- <div className="an-chat-history-row__menu-content" role="menu">
432
- {renderRowActions ? (
433
- renderRowActions(item)
434
- ) : (
435
- <>
436
- {onRename && (
437
- <button
438
- type="button"
439
- role="menuitem"
440
- className="an-chat-history-row__menu-item"
441
- onClick={startRename}
442
- >
443
- <IconPencil size={13} strokeWidth={1.8} />
444
- <span>{labels.rename}</span>
445
- </button>
446
- )}
447
- {onTogglePin && (
448
- <button
449
- type="button"
450
- role="menuitem"
451
- className="an-chat-history-row__menu-item"
452
- onClick={() => {
453
- setMenuOpen(false);
454
- onTogglePin(item.id);
455
- }}
456
- >
457
- {item.pinned ? (
458
- <IconPinnedOff size={13} strokeWidth={1.8} />
459
- ) : (
460
- <IconPinned size={13} strokeWidth={1.8} />
461
- )}
462
- <span>{item.pinned ? labels.unpin : labels.pin}</span>
463
- </button>
464
- )}
465
- {renderAdditionalRowActions?.(item, () => setMenuOpen(false))}
466
- {onDelete && (
467
- <button
468
- type="button"
469
- role="menuitem"
470
- className="an-chat-history-row__menu-item an-chat-history-row__menu-item--danger"
471
- onClick={() => {
472
- setMenuOpen(false);
473
- onDelete(item.id);
474
- }}
475
- >
476
- <IconTrash size={13} strokeWidth={1.8} />
477
- <span>{labels.delete}</span>
478
- </button>
479
- )}
480
- </>
481
- )}
482
- </div>
483
- )}
484
- </div>
394
+ <DropdownMenuPrimitive.Root open={menuOpen} onOpenChange={setMenuOpen}>
395
+ <div className="an-chat-history-row__menu">
396
+ <DropdownMenuPrimitive.Trigger asChild>
397
+ <button
398
+ type="button"
399
+ className={cn(
400
+ "an-chat-history-row__menu-trigger",
401
+ item.pinned && "an-chat-history-row__menu-trigger--pinned",
402
+ )}
403
+ aria-label={optionsLabel}
404
+ aria-haspopup="menu"
405
+ aria-expanded={menuOpen}
406
+ >
407
+ {item.pinned ? (
408
+ <IconPinned size={13} strokeWidth={1.8} />
409
+ ) : (
410
+ <IconDots size={14} strokeWidth={1.8} />
411
+ )}
412
+ </button>
413
+ </DropdownMenuPrimitive.Trigger>
414
+ <DropdownMenuPrimitive.Portal>
415
+ <DropdownMenuPrimitive.Content
416
+ className="an-chat-history-row__menu-content"
417
+ align="end"
418
+ collisionPadding={8}
419
+ side="bottom"
420
+ sideOffset={4}
421
+ >
422
+ {renderRowActions ? (
423
+ renderRowActions(item)
424
+ ) : (
425
+ <>
426
+ {onRename && (
427
+ <button
428
+ type="button"
429
+ role="menuitem"
430
+ className="an-chat-history-row__menu-item"
431
+ onClick={startRename}
432
+ >
433
+ <IconPencil size={13} strokeWidth={1.8} />
434
+ <span>{labels.rename}</span>
435
+ </button>
436
+ )}
437
+ {onTogglePin && (
438
+ <button
439
+ type="button"
440
+ role="menuitem"
441
+ className="an-chat-history-row__menu-item"
442
+ onClick={() => {
443
+ setMenuOpen(false);
444
+ onTogglePin(item.id);
445
+ }}
446
+ >
447
+ {item.pinned ? (
448
+ <IconPinnedOff size={13} strokeWidth={1.8} />
449
+ ) : (
450
+ <IconPinned size={13} strokeWidth={1.8} />
451
+ )}
452
+ <span>{item.pinned ? labels.unpin : labels.pin}</span>
453
+ </button>
454
+ )}
455
+ {renderAdditionalRowActions?.(item, () =>
456
+ setMenuOpen(false),
457
+ )}
458
+ {onDelete && (
459
+ <button
460
+ type="button"
461
+ role="menuitem"
462
+ className="an-chat-history-row__menu-item an-chat-history-row__menu-item--danger"
463
+ onClick={() => {
464
+ setMenuOpen(false);
465
+ onDelete(item.id);
466
+ }}
467
+ >
468
+ <IconTrash size={13} strokeWidth={1.8} />
469
+ <span>{labels.delete}</span>
470
+ </button>
471
+ )}
472
+ </>
473
+ )}
474
+ </DropdownMenuPrimitive.Content>
475
+ </DropdownMenuPrimitive.Portal>
476
+ </div>
477
+ </DropdownMenuPrimitive.Root>
485
478
  )}
486
479
  </div>
487
480
  );
@@ -293,22 +293,14 @@
293
293
  transition: opacity 100ms ease;
294
294
  }
295
295
 
296
- .an-chat-history--rail
297
- .an-chat-history-row--has-menu
298
- .an-chat-history-row__timestamp {
299
- display: inline-flex;
300
- min-width: 24px;
301
- justify-content: flex-end;
302
- }
303
-
304
296
  .an-chat-history--rail
305
297
  .an-chat-history-row--active
306
298
  .an-chat-history-row__timestamp {
307
299
  color: hsl(var(--accent-foreground));
308
300
  }
309
301
 
310
- /* The menu overlays this fixed trailing slot so its hover state never changes
311
- the title's available width or moves the rest of the rail. */
302
+ /* Hide trailing metadata while the absolute menu takes its place so hover
303
+ never changes the title's available width or moves the rest of the rail. */
312
304
  .an-chat-history--rail
313
305
  .an-chat-history-row--has-menu:hover
314
306
  .an-chat-history-row__timestamp,
@@ -375,10 +367,11 @@
375
367
  position: absolute;
376
368
  top: 4px;
377
369
  inset-inline-end: 4px;
370
+ z-index: 1;
378
371
  }
379
372
 
380
373
  .an-chat-history--rail .an-chat-history-row__menu {
381
- inset-inline-end: 10px;
374
+ inset-inline-end: 8px;
382
375
  }
383
376
 
384
377
  .an-chat-history-row__menu-trigger {
@@ -428,10 +421,7 @@
428
421
  }
429
422
 
430
423
  .an-chat-history-row__menu-content {
431
- position: absolute;
432
- top: calc(100% + 4px);
433
- inset-inline-end: 0;
434
- z-index: 30;
424
+ z-index: 290;
435
425
  min-width: 160px;
436
426
  padding: 4px;
437
427
  border: 1px solid hsl(var(--border));
@@ -0,0 +1,44 @@
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 { PresenceBar } from "./PresenceBar.js";
8
+
9
+ describe("PresenceBar", () => {
10
+ let container: HTMLDivElement;
11
+ let root: Root;
12
+
13
+ beforeEach(() => {
14
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
15
+ container = document.createElement("div");
16
+ document.body.appendChild(container);
17
+ root = createRoot(container);
18
+ });
19
+
20
+ afterEach(() => {
21
+ act(() => root.unmount());
22
+ container.remove();
23
+ vi.restoreAllMocks();
24
+ vi.unstubAllGlobals();
25
+ });
26
+
27
+ it("gives the integrated AI editing label space after the avatar", () => {
28
+ act(() => {
29
+ root.render(
30
+ <PresenceBar
31
+ activeUsers={[]}
32
+ agentPresent
33
+ agentActive
34
+ showAgentEditingDot={false}
35
+ />,
36
+ );
37
+ });
38
+
39
+ const label = Array.from(container.querySelectorAll("span")).find(
40
+ (element) => element.textContent === "AI editing",
41
+ );
42
+ expect(label?.style.padding).toBe("0px 0px 0px 2px");
43
+ });
44
+ });
@@ -247,7 +247,7 @@ function AgentEditingChip({
247
247
  alignItems: "center",
248
248
  gap: showDot ? 4 : 0,
249
249
  height: integrated ? "auto" : 20,
250
- padding: integrated ? 0 : "0 8px",
250
+ padding: integrated ? "0 0 0 2px" : "0 8px",
251
251
  borderRadius: integrated ? 0 : 9999,
252
252
  backgroundColor: integrated ? "transparent" : `${AGENT_COLOR}20`,
253
253
  color: AGENT_COLOR,
@@ -147,7 +147,7 @@ export function VoiceButton({ voice, isMac, disabled }: VoiceButtonProps) {
147
147
  ? "Stop recording"
148
148
  : transcribing
149
149
  ? "Transcribing…"
150
- : `Dictate (${isMac ? "⌘⇧M" : "Ctrl+Shift+M"})`;
150
+ : `Dictate (${isMac ? "Cmd Shift M" : "Ctrl Shift M"})`;
151
151
 
152
152
  const onClick = () => {
153
153
  if (recording) stop();