@artooi/ag-ui-web-component 0.45.0 → 0.47.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 (91) hide show
  1. package/CHANGELOG.md +146 -1
  2. package/README.md +191 -25
  3. package/dist/ag-ui-web-component.bundle.js +399 -157
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +14 -1
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +47 -4
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/core/chat_registry.d.ts +23 -0
  10. package/dist/core/chat_registry.d.ts.map +1 -0
  11. package/dist/core/events/run_finished_detail.d.ts +6 -0
  12. package/dist/core/events/run_finished_detail.d.ts.map +1 -1
  13. package/dist/core/events/thread_detail.d.ts +27 -0
  14. package/dist/core/events/thread_detail.d.ts.map +1 -0
  15. package/dist/core/run_handlers.d.ts +6 -1
  16. package/dist/core/run_handlers.d.ts.map +1 -1
  17. package/dist/dom/animations.d.ts +21 -6
  18. package/dist/dom/animations.d.ts.map +1 -1
  19. package/dist/dom/highlight_overlay.d.ts +7 -1
  20. package/dist/dom/highlight_overlay.d.ts.map +1 -1
  21. package/dist/dom/ring_color.d.ts +14 -0
  22. package/dist/dom/ring_color.d.ts.map +1 -0
  23. package/dist/index.d.ts +3 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +857 -86
  26. package/dist/index.js.map +4 -4
  27. package/dist/tools/tool_catalog.d.ts +5 -0
  28. package/dist/tools/tool_catalog.d.ts.map +1 -1
  29. package/dist/tools/tool_dispatch.d.ts.map +1 -1
  30. package/dist/ui/history/conversation_history.d.ts +10 -1
  31. package/dist/ui/history/conversation_history.d.ts.map +1 -1
  32. package/dist/ui/interrupts/confirmation_styles.d.ts.map +1 -1
  33. package/dist/ui/placement/panel_placement.d.ts +6 -0
  34. package/dist/ui/placement/panel_placement.d.ts.map +1 -1
  35. package/dist/ui/placement/small_viewport_styles.d.ts.map +1 -1
  36. package/dist/ui/placement/surface_for.d.ts +12 -1
  37. package/dist/ui/placement/surface_for.d.ts.map +1 -1
  38. package/dist/ui/progress/thoughts_styles.d.ts.map +1 -1
  39. package/dist/ui/progress/tool_call_card.d.ts +8 -0
  40. package/dist/ui/progress/tool_call_card.d.ts.map +1 -1
  41. package/dist/ui/progress/tool_group_styles.d.ts +2 -0
  42. package/dist/ui/progress/tool_group_styles.d.ts.map +1 -0
  43. package/dist/ui/progress/tool_step_group.d.ts +47 -0
  44. package/dist/ui/progress/tool_step_group.d.ts.map +1 -0
  45. package/dist/ui/progress/tool_step_styles.d.ts.map +1 -1
  46. package/dist/ui/shell/token_styles.d.ts.map +1 -1
  47. package/dist/ui/styles.d.ts.map +1 -1
  48. package/dist/ui/themes/ledger_styles.d.ts.map +1 -1
  49. package/dist/ui/themes/soft_styles.d.ts.map +1 -1
  50. package/dist/ui/transcript/groups_tool_steps.d.ts +13 -0
  51. package/dist/ui/transcript/groups_tool_steps.d.ts.map +1 -0
  52. package/dist/ui/transcript/message_styles.d.ts.map +1 -1
  53. package/dist/ui/transcript/transcript.d.ts +16 -1
  54. package/dist/ui/transcript/transcript.d.ts.map +1 -1
  55. package/dist/ui/ui_strings.d.ts +18 -0
  56. package/dist/ui/ui_strings.d.ts.map +1 -1
  57. package/package.json +1 -1
  58. package/src/constants.ts +15 -1
  59. package/src/core/ag_ui_chat.ts +122 -11
  60. package/src/core/chat_registry.ts +53 -0
  61. package/src/core/events/run_finished_detail.ts +6 -0
  62. package/src/core/events/thread_detail.ts +26 -0
  63. package/src/core/run_handlers.ts +9 -4
  64. package/src/dom/animations.ts +30 -31
  65. package/src/dom/highlight_overlay.ts +9 -5
  66. package/src/dom/ring_color.ts +85 -0
  67. package/src/index.ts +3 -0
  68. package/src/tools/tool_catalog.ts +23 -10
  69. package/src/tools/tool_dispatch.ts +26 -11
  70. package/src/ui/history/conversation_history.ts +28 -4
  71. package/src/ui/interrupts/confirmation_styles.ts +0 -2
  72. package/src/ui/placement/embedded_styles.ts +2 -2
  73. package/src/ui/placement/panel_placement.ts +34 -9
  74. package/src/ui/placement/preset_styles.ts +1 -1
  75. package/src/ui/placement/sidebar_styles.ts +1 -1
  76. package/src/ui/placement/small_viewport_styles.ts +5 -0
  77. package/src/ui/placement/surface_for.ts +12 -1
  78. package/src/ui/progress/thoughts_styles.ts +0 -2
  79. package/src/ui/progress/tool_call_card.ts +29 -8
  80. package/src/ui/progress/tool_group_styles.ts +232 -0
  81. package/src/ui/progress/tool_step_group.ts +272 -0
  82. package/src/ui/progress/tool_step_styles.ts +0 -2
  83. package/src/ui/shell/token_styles.ts +4 -2
  84. package/src/ui/styles.ts +5 -0
  85. package/src/ui/themes/ledger_styles.ts +15 -0
  86. package/src/ui/themes/soft_styles.ts +20 -0
  87. package/src/ui/transcript/groups_tool_steps.ts +16 -0
  88. package/src/ui/transcript/message_styles.ts +0 -2
  89. package/src/ui/transcript/transcript.ts +109 -3
  90. package/src/ui/ui_strings.ts +31 -0
  91. package/src/version.ts +1 -1
@@ -10,6 +10,7 @@ import {
10
10
  STATE_EVENT,
11
11
  SUBMIT_EVENT,
12
12
  SUGGESTIONS_ACTIVITY_TYPE,
13
+ THREAD_EVENT,
13
14
  TOGGLE_EVENT,
14
15
  TOOL_DISPLAY,
15
16
  UNREAD_EVENT,
@@ -75,6 +76,7 @@ import type { ActivityRenderer } from "./activity_renderer.js";
75
76
  import { AgUiClient } from "./agui_client.js";
76
77
  // biome-ignore lint/style/useImportType: the emitted declaration file copies this form
77
78
  import { type AttachmentRef } from "./attachment.js";
79
+ import { ChatRegistration } from "./chat_registry.js";
78
80
  import type { ClientSeed } from "./client_seed.js";
79
81
  import {
80
82
  type ClientConversationStore,
@@ -85,6 +87,7 @@ import {
85
87
  import { type AgentFactory, createHttpAgent } from "./create_http_agent.js";
86
88
  import type { StateDetail } from "./events/state_detail.js";
87
89
  import type { SubmitDetail } from "./events/submit_detail.js";
90
+ import type { ThreadDetail } from "./events/thread_detail.js";
88
91
  import type { ToggleDetail } from "./events/toggle_detail.js";
89
92
  import type { UnreadDetail } from "./events/unread_detail.js";
90
93
  import type { MessageRole } from "./message_role.js";
@@ -750,6 +753,11 @@ export class AgUiChat extends HTMLElement {
750
753
  * then a host's {@link sendMessage} waits for it rather than refusing.
751
754
  */
752
755
  #restoreHold: { readonly hold: object; readonly restored: Promise<void> } | null = null;
756
+ /**
757
+ * The thread the latest {@link THREAD_EVENT} reported, which the next one
758
+ * names as the thread it replaced; `null` before the first.
759
+ */
760
+ #shownThread: string | null = null;
753
761
  /** The decision a run is suspended on, which a Stop abandons. */
754
762
  readonly #decision = new PendingDecision();
755
763
  /**
@@ -767,6 +775,7 @@ export class AgUiChat extends HTMLElement {
767
775
  askUser: () => this.askUser,
768
776
  askUserRenderer: () => this.askUserRenderer,
769
777
  decision: this.#decision,
778
+ setAsking: (callId, asking) => this.#transcript.setAsking(callId, asking),
770
779
  ensureGroup: () => this.#transcript.ensureGroup(),
771
780
  strings: () => this.#strings,
772
781
  hidePending: () => this.#transcript.hidePending(),
@@ -780,6 +789,8 @@ export class AgUiChat extends HTMLElement {
780
789
  allowImages: () => this.allowImages,
781
790
  follow: () => this.#transcript.follow(),
782
791
  });
792
+ // This chat's place among the connected ones, which a free highlight ring reads.
793
+ readonly #chatRegistration = new ChatRegistration(this);
783
794
  /**
784
795
  * Which storage keys are this element's: the namespace it claims, the keys
785
796
  * its layout preferences live under, and the built-in store scoped to them.
@@ -820,6 +831,7 @@ export class AgUiChat extends HTMLElement {
820
831
  resolveTool: (name) => this.#tools.resolve(name),
821
832
  toolSummaries: () => this.toolSummaries,
822
833
  serverSummary: (name) => this.#tools.summary(name),
834
+ toolDisplay: () => this.toolDisplay,
823
835
  // A thunk over the live property, not the property itself: the card keeps
824
836
  // this for the life of the call, and the result region is filled when the
825
837
  // tool settles -- which can be long after a host set the hook.
@@ -1041,6 +1053,7 @@ export class AgUiChat extends HTMLElement {
1041
1053
  cancelRun: () => this.#cancelRun(),
1042
1054
  resetState: () => this.#resetState(),
1043
1055
  setRunning: (running) => this.#setRunning(running),
1056
+ threadChanged: (threadId, reason) => this.#dispatchThread(threadId, reason),
1044
1057
  });
1045
1058
  }
1046
1059
 
@@ -1377,22 +1390,28 @@ export class AgUiChat extends HTMLElement {
1377
1390
 
1378
1391
  /**
1379
1392
  * How much detail tool-call cards show, from the `data-tool-display`
1380
- * attribute (`minimal` / `inline` / `compact` / `full`). Defaults to `full`.
1393
+ * attribute (`grouped` / `minimal` / `inline` / `compact` / `full`).
1394
+ * Defaults to `grouped`, for an absent attribute and for a value the
1395
+ * element does not know; `full` is the per-call card default before
1396
+ * `grouped` existed.
1381
1397
  *
1382
- * Applied by the shadow CSS from the attribute itself, so changing it
1383
- * restyles every card already in the transcript rather than only the ones
1384
- * built afterwards.
1398
+ * The four card modes are applied by the shadow CSS from the attribute
1399
+ * itself, so changing between them restyles every card already in the
1400
+ * transcript. Whether calls fold into a step group is decided as each card
1401
+ * is built: switching to `grouped` folds only the calls drawn afterwards,
1402
+ * and switching away flattens the existing groups, again by CSS.
1385
1403
  */
1386
1404
  get toolDisplay(): ToolDisplayMode {
1387
1405
  const attr = this.getAttribute("data-tool-display");
1388
1406
  if (
1389
1407
  attr === TOOL_DISPLAY.INLINE ||
1390
1408
  attr === TOOL_DISPLAY.MINIMAL ||
1391
- attr === TOOL_DISPLAY.COMPACT
1409
+ attr === TOOL_DISPLAY.COMPACT ||
1410
+ attr === TOOL_DISPLAY.FULL
1392
1411
  ) {
1393
1412
  return attr;
1394
1413
  }
1395
- return TOOL_DISPLAY.FULL;
1414
+ return TOOL_DISPLAY.GROUPED;
1396
1415
  }
1397
1416
 
1398
1417
  set toolDisplay(value: ToolDisplayMode) {
@@ -1440,6 +1459,7 @@ export class AgUiChat extends HTMLElement {
1440
1459
  // this instance doesn't share collapsed/theme/thread state with another on
1441
1460
  // the same origin.
1442
1461
  this.#storage.claim();
1462
+ this.#chatRegistration.join();
1443
1463
  // Restore a dragged size before the panel paints, so it does not snap from
1444
1464
  // the placement default to the user's width on the first frame.
1445
1465
  this.#placement.restoreSize();
@@ -1522,7 +1542,10 @@ export class AgUiChat extends HTMLElement {
1522
1542
  // The catalog requests go out a microtask later, so a host configuring
1523
1543
  // through a framework ref still has a chance to be heard — see #startup.
1524
1544
  queueMicrotask(() => this.#startup());
1525
- void this.#history.rehydrate();
1545
+ // A key changed while the element was away handed it over all the same,
1546
+ // and the restore is the arriving principal's: reported as the live change
1547
+ // is. An adoption is not a handover, and restores the same conversation.
1548
+ void this.#history.rehydrate(changed && held !== "" ? "rescope" : "restore");
1526
1549
  if (changed) {
1527
1550
  // The last of what a live change does: a drawer left open across the
1528
1551
  // move still lists the threads of the principal who left, titles and
@@ -1586,7 +1609,7 @@ export class AgUiChat extends HTMLElement {
1586
1609
  await Promise.all([
1587
1610
  this.#tools.fetchCatalog(),
1588
1611
  this.#skills.fetch(),
1589
- this.#history.rehydrate(),
1612
+ this.#history.rehydrate("restore"),
1590
1613
  ]);
1591
1614
  }
1592
1615
 
@@ -1603,6 +1626,7 @@ export class AgUiChat extends HTMLElement {
1603
1626
  window.visualViewport?.removeEventListener("resize", this.#onViewportResize);
1604
1627
  window.visualViewport?.removeEventListener("scroll", this.#onViewportResize);
1605
1628
  this.#storage.release();
1629
+ this.#chatRegistration.leave();
1606
1630
  this.#cancelRun();
1607
1631
  this.#excerpts.detachPageOffer();
1608
1632
  this.#attachments.tray?.dispose();
@@ -1786,6 +1810,28 @@ export class AgUiChat extends HTMLElement {
1786
1810
  return this.#unread;
1787
1811
  }
1788
1812
 
1813
+ /**
1814
+ * The thread on screen; `null` before the element first connects.
1815
+ *
1816
+ * Only reads. `conversationStore.threadId()` mints and stores a thread when
1817
+ * the store points at none, so a host asking which conversation is showing
1818
+ * could start one; this never does. {@link THREAD_EVENT} says when it changes.
1819
+ */
1820
+ get threadId(): string | null {
1821
+ const threadId = this.#history.threadId;
1822
+ return threadId === "" ? null : threadId;
1823
+ }
1824
+
1825
+ /**
1826
+ * Whether an interaction is in flight: the composer's own state, the one that
1827
+ * shows Stop. It spans every tool round, a retry and a checkpoint
1828
+ * continuation as well as a send, which {@link SUBMIT_EVENT} alone does not.
1829
+ * Reflected as `data-running` on the element, for a host's CSS.
1830
+ */
1831
+ get running(): boolean {
1832
+ return this.#running;
1833
+ }
1834
+
1789
1835
  /**
1790
1836
  * Whether to mount collapsed.
1791
1837
  *
@@ -1842,6 +1888,14 @@ export class AgUiChat extends HTMLElement {
1842
1888
  return this.#placement.moveTo(corner, options);
1843
1889
  }
1844
1890
 
1891
+ /**
1892
+ * Drop a size the user dragged, and its stored copy, so the placement's
1893
+ * default and the host's `--ag-ui-width` / `--ag-ui-height` apply again.
1894
+ */
1895
+ resetSize(): void {
1896
+ this.#placement.resetSize();
1897
+ }
1898
+
1845
1899
  /** Flip the collapsed state. Bound to the built-in header toggle. */
1846
1900
  toggleCollapsed(): void {
1847
1901
  this.setCollapsed(!this.collapsed);
@@ -1952,7 +2006,7 @@ export class AgUiChat extends HTMLElement {
1952
2006
  this.#voice.dispose();
1953
2007
  this.#voice.wire();
1954
2008
  this.#history.adoptActiveThread();
1955
- void this.#history.rehydrate();
2009
+ void this.#history.rehydrate("rescope");
1956
2010
  void this.#history.refreshDrawer();
1957
2011
  }
1958
2012
 
@@ -2023,6 +2077,36 @@ export class AgUiChat extends HTMLElement {
2023
2077
  this.#checkpoints.close();
2024
2078
  }
2025
2079
 
2080
+ /**
2081
+ * Put a stored thread on screen, as picking it in the conversation list does,
2082
+ * for a host that lists conversations in its own chrome.
2083
+ *
2084
+ * Resolves once the thread has been replayed, when {@link THREAD_EVENT} has
2085
+ * fired. **A run in flight is cancelled**, as the list and {@link newChat} do;
2086
+ * a host that would rather wait reads {@link running} first. The thread
2087
+ * already on screen is left alone, and so is everything before the element
2088
+ * connects, since until then it has no store to read the thread from.
2089
+ */
2090
+ async switchThread(threadId: string): Promise<void> {
2091
+ if (!this.#connected) {
2092
+ return;
2093
+ }
2094
+ await this.#history.switchThread(threadId);
2095
+ }
2096
+
2097
+ /**
2098
+ * Delete a stored thread, as the conversation list's delete does. Deleting the
2099
+ * thread on screen cancels its run and puts a fresh thread in its place, and
2100
+ * {@link THREAD_EVENT} reports it; deleting another leaves the screen alone.
2101
+ * Does nothing before the element connects.
2102
+ */
2103
+ deleteThread(threadId: string): void {
2104
+ if (!this.#connected) {
2105
+ return;
2106
+ }
2107
+ this.#history.deleteThread(threadId);
2108
+ }
2109
+
2026
2110
  /**
2027
2111
  * Open the checkpoints panel, or close it if it is already open — what the
2028
2112
  * built-in ⭯ button does, because a control that opens a panel is read as the
@@ -2058,6 +2142,7 @@ export class AgUiChat extends HTMLElement {
2058
2142
  this.#history.startThread();
2059
2143
  this.#setRunning(false);
2060
2144
  this.#setUnread(0);
2145
+ this.#dispatchThread(this.#history.threadId, "new");
2061
2146
  // Whichever control started it: the header's, the history list's (which
2062
2147
  // hands focus back to its opener as it closes, before this runs), or a
2063
2148
  // host's own. A keyboard user otherwise had to find their way back to the
@@ -2332,6 +2417,12 @@ export class AgUiChat extends HTMLElement {
2332
2417
  onFilesPicked: () => this.#attachments.onFilesPicked(),
2333
2418
  });
2334
2419
 
2420
+ // Host content against the input, after everything of the turn being
2421
+ // written. A bare slot, whose own box is none, so an empty one takes no
2422
+ // space and the greeting layout carries a filled one with the composer.
2423
+ const aboveComposer = document.createElement("slot");
2424
+ aboveComposer.name = "above-composer";
2425
+
2335
2426
  // A coarse footer slot below the composer.
2336
2427
  const footer = document.createElement("slot");
2337
2428
  footer.name = "footer";
@@ -2343,7 +2434,8 @@ export class AgUiChat extends HTMLElement {
2343
2434
 
2344
2435
  // Composer surfaces sit just above the input: the skills palette (opens on
2345
2436
  // `/`), the chips, the hint saying what the composer still needs, the
2346
- // queued messages and the pending-attachments tray.
2437
+ // queued messages and the pending-attachments tray, then the host's own
2438
+ // content directly over the input.
2347
2439
  this.#chat.replaceChildren(
2348
2440
  header,
2349
2441
  this.#messagesWrap,
@@ -2352,6 +2444,7 @@ export class AgUiChat extends HTMLElement {
2352
2444
  this.#composerHint,
2353
2445
  this.#queuedRow,
2354
2446
  this.#attachSlot,
2447
+ aboveComposer,
2355
2448
  inputRow,
2356
2449
  footer,
2357
2450
  this.#drawer.element,
@@ -2586,6 +2679,22 @@ export class AgUiChat extends HTMLElement {
2586
2679
  this.#history.stopContinuation();
2587
2680
  }
2588
2681
 
2682
+ /**
2683
+ * Say the conversation on screen changed: {@link THREAD_EVENT}.
2684
+ *
2685
+ * The thread it replaced is the one last reported, rather than the one the
2686
+ * element last pointed at: a switch superseded before it replayed never drew
2687
+ * its thread, so naming it as the previous one left a host's own list
2688
+ * marking a thread that was never shown and still marking the one that was.
2689
+ */
2690
+ #dispatchThread(threadId: string, reason: ThreadDetail["reason"]): void {
2691
+ const detail: ThreadDetail = { threadId, previousThreadId: this.#shownThread, reason };
2692
+ this.#shownThread = threadId;
2693
+ this.dispatchEvent(
2694
+ new CustomEvent<ThreadDetail>(THREAD_EVENT, { detail, bubbles: true, composed: true }),
2695
+ );
2696
+ }
2697
+
2589
2698
  /**
2590
2699
  * Swap the composer button between Send (idle) and Stop (running).
2591
2700
  *
@@ -2607,6 +2716,8 @@ export class AgUiChat extends HTMLElement {
2607
2716
  this.#send.title = label;
2608
2717
  this.#send.setAttribute("aria-label", label);
2609
2718
  this.#send.dataset["state"] = running ? "running" : "idle";
2719
+ // For a host's CSS, as `data-empty` is: the same state, on the element.
2720
+ this.toggleAttribute("data-running", running);
2610
2721
  if (settled) {
2611
2722
  this.#flushQueued();
2612
2723
  }
@@ -2998,7 +3109,7 @@ export class AgUiChat extends HTMLElement {
2998
3109
  });
2999
3110
  return new AgUiClient({
3000
3111
  agent,
3001
- handlers: this.#runHandlers.forClient(),
3112
+ handlers: this.#runHandlers.forClient(threadId),
3002
3113
  getTools: () => this.#tools.advertise(),
3003
3114
  getContext: () => this.#dispatch.buildContext(),
3004
3115
  executeTool: (call) => this.#dispatch.execute(call),
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The `<ag-ui-chat>` elements connected to this document, oldest first.
3
+ *
4
+ * The highlight ring is drawn in the host's page, outside every chat, so a
5
+ * token set on a chat never reaches it through the cascade. A free `flash()`
6
+ * still has to answer "which widget is this ring speaking for", and that is a
7
+ * question about the live elements rather than about any one of them.
8
+ *
9
+ * Module-level on purpose, and the second piece of shared mutable state the
10
+ * repository's `CLAUDE.md` records, beside the claimed storage namespaces. See
11
+ * the conditions it states, and how this one meets them, before adding a
12
+ * third.
13
+ */
14
+ const CONNECTED_CHATS = new Set<HTMLElement>();
15
+
16
+ /**
17
+ * One chat's place in the document's list of connected chats.
18
+ *
19
+ * Owned one-to-one by an `<ag-ui-chat>`, which joins on connect and leaves on
20
+ * disconnect, so the list holds exactly the elements that are in a document
21
+ * now. Typed as `HTMLElement` rather than the element's class, because the DOM
22
+ * driver reads it and must not import the element.
23
+ */
24
+ export class ChatRegistration {
25
+ readonly #chat: HTMLElement;
26
+
27
+ constructor(chat: HTMLElement) {
28
+ this.#chat = chat;
29
+ }
30
+
31
+ /**
32
+ * Join, as the most recently connected chat. An element moved in the DOM
33
+ * left on disconnect before it reconnected, so it rejoins at the end and no
34
+ * entry needs removing first.
35
+ */
36
+ join(): void {
37
+ CONNECTED_CHATS.add(this.#chat);
38
+ }
39
+
40
+ /** Leave. The previous chat, if one is still connected, becomes the latest. */
41
+ leave(): void {
42
+ CONNECTED_CHATS.delete(this.#chat);
43
+ }
44
+
45
+ /** The most recently connected chat still in the document, or `null`. */
46
+ static latest(): HTMLElement | null {
47
+ let latest: HTMLElement | null = null;
48
+ for (const chat of CONNECTED_CHATS) {
49
+ latest = chat;
50
+ }
51
+ return latest;
52
+ }
53
+ }
@@ -2,6 +2,12 @@ import type { ToolRun } from "./tool_run.js";
2
2
 
3
3
  /** `detail` shape of the {@link RUN_FINISHED_EVENT} CustomEvent. */
4
4
  export interface RunFinishedDetail {
5
+ /**
6
+ * The thread the run belonged to, fixed when its client was built. Not always
7
+ * the thread on screen: a run stopped by a new chat or a thread switch still
8
+ * reports once its request closes, and by then another thread is.
9
+ */
10
+ readonly threadId: string;
5
11
  /** In settle order. Empty when the interaction called no tools. */
6
12
  readonly tools: readonly ToolRun[];
7
13
  /**
@@ -0,0 +1,26 @@
1
+ /** `detail` shape of the {@link THREAD_EVENT} CustomEvent. */
2
+ export interface ThreadDetail {
3
+ /** The thread now on screen. */
4
+ readonly threadId: string;
5
+ /**
6
+ * The thread on screen before the change: the one the previous event
7
+ * reported, and `null` before the first, as at the restore when the element
8
+ * first connects. Equal to `threadId` when the same thread was restored
9
+ * again (`reload()`, the element connecting again).
10
+ */
11
+ readonly previousThreadId: string | null;
12
+ /**
13
+ * What changed it:
14
+ *
15
+ * - `new`: `newChat()` or the built-in New chat button.
16
+ * - `switch`: `switchThread()` or a row picked in the conversation list.
17
+ * - `delete`: the active thread was deleted, and a fresh one took its place.
18
+ * - `restore`: the stored thread was replayed, on connect or by `reload()`.
19
+ * - `rescope`: a `user-key` change handed the element to another principal,
20
+ * live or while it was out of the document.
21
+ *
22
+ * A closed set today; a host should ignore a reason it does not know, so a
23
+ * later one can be added without breaking it.
24
+ */
25
+ readonly reason: "new" | "switch" | "delete" | "restore" | "rescope";
26
+ }
@@ -176,8 +176,13 @@ export class RunHandlers {
176
176
  * A handler table for one AG-UI client. Every table built for the current
177
177
  * conversation draws into the same transcript and shares its bookkeeping, so
178
178
  * a continuation run reports into the interaction the user is looking at.
179
+ *
180
+ * `threadId` is the thread the client was built for, named on the
181
+ * run-finished detail. Taken here rather than read when the run settles,
182
+ * because a stopped run settles after the reset that stopped it, when the
183
+ * thread on screen is already the next one.
179
184
  */
180
- forClient(): AgUiClientHandlers {
185
+ forClient(threadId: string): AgUiClientHandlers {
181
186
  const ledger = this.#ledger;
182
187
  const attached = (): boolean => ledger === this.#ledger;
183
188
  const { onSettled, ...drawing } = this.#tableFor(ledger);
@@ -190,7 +195,7 @@ export class RunHandlers {
190
195
  // Last, so a listener that refetches sees a transcript that has already
191
196
  // stopped changing -- and outside the check, because what a run did is
192
197
  // true whichever conversation is on screen when it ends.
193
- this.#dispatchRunFinished(ledger);
198
+ this.#dispatchRunFinished(ledger, threadId);
194
199
  },
195
200
  };
196
201
  }
@@ -444,7 +449,7 @@ export class RunHandlers {
444
449
  * server is a fact about the run, and a name can appear on both sides across a
445
450
  * conversation.
446
451
  */
447
- #dispatchRunFinished(ledger: RunLedger): void {
452
+ #dispatchRunFinished(ledger: RunLedger, threadId: string): void {
448
453
  const tools: ToolRun[] = ledger.tools.map(({ id, name }) => ({
449
454
  name,
450
455
  side: ledger.serverSettled.has(id) ? "server" : "client",
@@ -455,7 +460,7 @@ export class RunHandlers {
455
460
  ledger.invalidated.clear();
456
461
  this.#host.element.dispatchEvent(
457
462
  new CustomEvent<RunFinishedDetail>(RUN_FINISHED_EVENT, {
458
- detail: { tools, invalidated },
463
+ detail: { threadId, tools, invalidated },
459
464
  bubbles: true,
460
465
  composed: true,
461
466
  }),
@@ -8,17 +8,7 @@
8
8
 
9
9
  import { showHighlightOverlay } from "./highlight_overlay.js";
10
10
  import { setNativeChecked, setNativeValue } from "./native_setter.js";
11
-
12
- /** Ring colour when the host page has not themed `--ag-ui-accent`. */
13
- const ACCENT = "#4f46e5";
14
-
15
- /**
16
- * Fallback for the box-shadow rings, which read as a soft halo rather than a
17
- * line and so carry their own alpha.
18
- */
19
- const ACCENT_HALO = "rgba(79, 70, 229, 0.4)";
20
-
21
- const ACCENT_PROPERTY = "--ag-ui-accent";
11
+ import { ringColor } from "./ring_color.js";
22
12
 
23
13
  function delay(ms: number): Promise<void> {
24
14
  return new Promise<void>((resolve) => {
@@ -48,19 +38,23 @@ function motionDelay(ms: number): Promise<void> {
48
38
  }
49
39
 
50
40
  /**
51
- * Resolve the accent colour from the target's own computed style.
41
+ * Which `<ag-ui-chat>` a ring speaks for, so it is drawn in that chat's colour.
52
42
  *
53
- * `--ag-ui-accent` inherits, so reading it off the element about to be touched
54
- * picks up whatever the host's cascade produced there, and a themed page is
55
- * flashed in its own colour rather than this package's indigo.
43
+ * The ring is drawn in the host's page, which a token set on the chat never
44
+ * reaches, so the chat is read directly. Unset, it is the most recently
45
+ * connected chat; a page with two widgets names the one acting.
56
46
  */
57
- function accentColor(el: HTMLElement, fallback: string): string {
58
- const themed = window.getComputedStyle(el).getPropertyValue(ACCENT_PROPERTY).trim();
59
- return themed === "" ? fallback : themed;
47
+ export interface RingOwnerOptions {
48
+ /** The chat whose ring colour and accent to use. Default: the latest connected. */
49
+ chat?: HTMLElement;
60
50
  }
61
51
 
62
- function ringShadow(el: HTMLElement): string {
63
- return `0 0 0 3px ${accentColor(el, ACCENT_HALO)}`;
52
+ /**
53
+ * The soft halo the press and toggle rings draw: the ring colour at 40%.
54
+ * Mixed rather than given its own alpha, so it follows whichever colour won.
55
+ */
56
+ function ringShadow(el: HTMLElement, chat: HTMLElement | undefined): string {
57
+ return `0 0 0 3px color-mix(in srgb, ${ringColor(el, chat)} 40%, transparent)`;
64
58
  }
65
59
 
66
60
  /** An element whose `value` can be typed into. */
@@ -93,7 +87,7 @@ export async function typeInto(
93
87
  el.dispatchEvent(new Event("change", { bubbles: true }));
94
88
  }
95
89
 
96
- export interface HighlightClickOptions {
90
+ export interface HighlightClickOptions extends RingOwnerOptions {
97
91
  /** Milliseconds to hold the highlight before clicking. Default 280. */
98
92
  highlightMs?: number;
99
93
  }
@@ -106,7 +100,7 @@ export async function highlightThenClick(
106
100
  const highlightMs = options.highlightMs ?? 280;
107
101
  const previousOutline = el.style.outline;
108
102
  const previousOffset = el.style.outlineOffset;
109
- el.style.outline = `2px solid ${accentColor(el, ACCENT)}`;
103
+ el.style.outline = `2px solid ${ringColor(el, options.chat)}`;
110
104
  el.style.outlineOffset = "2px";
111
105
  await delay(highlightMs);
112
106
  el.style.outline = previousOutline;
@@ -176,10 +170,14 @@ const FLASH_MS = 1200;
176
170
  /** Share of {@link FLASH_MS} spent fading out rather than holding. */
177
171
  const FLASH_FADE_RATIO = 1 / 3;
178
172
 
179
- export interface FlashOptions {
173
+ export interface FlashOptions extends RingOwnerOptions {
180
174
  /** Total milliseconds the ring is on screen, hold plus fade. Default 1200. */
181
175
  flashMs?: number;
182
- /** Ring colour. Defaults to the target's `--ag-ui-accent`, else the package accent. */
176
+ /**
177
+ * Ring colour. Defaults to `--ag-ui-highlight-ring-color` (on the target,
178
+ * then on the chat), then the target's `--ag-ui-accent`, then the chat's
179
+ * accent.
180
+ */
183
181
  color?: string;
184
182
  /**
185
183
  * Dim everything except the target.
@@ -226,6 +224,7 @@ async function flashRing(
226
224
  gradient: options.gradient === true,
227
225
  ...(options.color === undefined ? {} : { color: options.color }),
228
226
  ...(options.ringPadding === undefined ? {} : { padding: options.ringPadding }),
227
+ ...(options.chat === undefined ? {} : { chat: options.chat }),
229
228
  });
230
229
  await delay(flashMs);
231
230
  dismiss();
@@ -234,7 +233,7 @@ async function flashRing(
234
233
  const previousOutline = el.style.outline;
235
234
  const previousOffset = el.style.outlineOffset;
236
235
  const previousTransition = el.style.transition;
237
- const color = options.color ?? accentColor(el, ACCENT);
236
+ const color = options.color ?? ringColor(el, options.chat);
238
237
  // outline, not box-shadow: a shadow paints outside the border box, so any
239
238
  // overflow:hidden ancestor sharing the target's box (a card, a table cell)
240
239
  // clips the whole ring away while the helper still reports success.
@@ -273,7 +272,7 @@ export function focusWithFlash(el: HTMLElement, options: FlashOptions = {}): Pro
273
272
  return flashRing(el, options, true);
274
273
  }
275
274
 
276
- export interface PressOptions {
275
+ export interface PressOptions extends RingOwnerOptions {
277
276
  /** Milliseconds to hold the pressed state before clicking. Default 140. */
278
277
  pressMs?: number;
279
278
  }
@@ -290,7 +289,7 @@ export async function pressThenClick(el: HTMLElement, options: PressOptions = {}
290
289
  const previousShadow = el.style.boxShadow;
291
290
  el.style.transition = "transform 80ms ease";
292
291
  el.style.transform = "scale(0.96)";
293
- el.style.boxShadow = ringShadow(el);
292
+ el.style.boxShadow = ringShadow(el, options.chat);
294
293
  await motionDelay(pressMs);
295
294
  el.style.transform = previousTransform;
296
295
  el.style.transition = previousTransition;
@@ -298,7 +297,7 @@ export async function pressThenClick(el: HTMLElement, options: PressOptions = {}
298
297
  el.click();
299
298
  }
300
299
 
301
- export interface SelectOptions {
300
+ export interface SelectOptions extends RingOwnerOptions {
302
301
  /** Milliseconds to hold the highlight before committing. Default 220. */
303
302
  highlightMs?: number;
304
303
  }
@@ -330,7 +329,7 @@ export async function selectOption(
330
329
  const highlightMs = options.highlightMs ?? 220;
331
330
  const previousOutline = el.style.outline;
332
331
  const previousOffset = el.style.outlineOffset;
333
- el.style.outline = `2px solid ${accentColor(el, ACCENT)}`;
332
+ el.style.outline = `2px solid ${ringColor(el, options.chat)}`;
334
333
  el.style.outlineOffset = "2px";
335
334
  await motionDelay(highlightMs);
336
335
  setNativeValue(el, option.value);
@@ -340,7 +339,7 @@ export async function selectOption(
340
339
  el.style.outlineOffset = previousOffset;
341
340
  }
342
341
 
343
- export interface ToggleOptions {
342
+ export interface ToggleOptions extends RingOwnerOptions {
344
343
  /** Milliseconds to hold the flash. Default 200. */
345
344
  flashMs?: number;
346
345
  }
@@ -356,7 +355,7 @@ export async function toggleControl(
356
355
  ): Promise<void> {
357
356
  const flashMs = options.flashMs ?? 200;
358
357
  const previousShadow = el.style.boxShadow;
359
- el.style.boxShadow = ringShadow(el);
358
+ el.style.boxShadow = ringShadow(el, options.chat);
360
359
  await motionDelay(flashMs);
361
360
  setNativeChecked(el, checked);
362
361
  el.dispatchEvent(new Event("input", { bubbles: true }));
@@ -1,7 +1,5 @@
1
1
  import { HIGHLIGHT_OVERLAY_Z_INDEX } from "../constants.js";
2
-
3
- /** The package accent, used when the host has themed nothing. */
4
- const ACCENT = "#4f46e5";
2
+ import { ringColor } from "./ring_color.js";
5
3
 
6
4
  /** How the overlay is drawn around the element being pointed at. */
7
5
  export interface HighlightOverlayOptions {
@@ -9,8 +7,14 @@ export interface HighlightOverlayOptions {
9
7
  readonly scrim?: boolean;
10
8
  /** Draw the ring as a moving gradient rather than a flat colour. Default false. */
11
9
  readonly gradient?: boolean;
12
- /** Ring colour, and the flat fallback for the gradient. */
10
+ /**
11
+ * Ring colour, and the flat fallback for the gradient. Default:
12
+ * `--ag-ui-highlight-ring-color` on the target, then on the chat, then the
13
+ * target's `--ag-ui-accent`, then the chat's accent.
14
+ */
13
15
  readonly color?: string;
16
+ /** The chat whose ring colour and accent to use. Default: the latest connected. */
17
+ readonly chat?: HTMLElement;
14
18
  /** Gap between the target's box and the ring. Default 4. */
15
19
  readonly padding?: number;
16
20
  /** Corner radius of the cut-out and the ring. Default: the target's own. */
@@ -205,7 +209,7 @@ function safeColor(value: string | undefined): string | null {
205
209
  * the padding box, leaving the frame.
206
210
  */
207
211
  function ringPaint(target: HTMLElement, options: HighlightOverlayOptions): string {
208
- const color = safeColor(options.color) ?? tokenOn(target, "--ag-ui-accent", ACCENT);
212
+ const color = safeColor(options.color) ?? ringColor(target, options.chat);
209
213
  if (options.gradient !== true) {
210
214
  return `border-color: ${color}`;
211
215
  }