@artooi/ag-ui-web-component 0.34.0 → 0.35.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 (58) hide show
  1. package/CHANGELOG.md +501 -1
  2. package/README.md +232 -13
  3. package/dist/ag-ui-web-component.bundle.js +614 -96
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +76 -3
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +37 -2
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/dom/animations.d.ts +14 -0
  10. package/dist/dom/animations.d.ts.map +1 -1
  11. package/dist/dom/highlight_overlay.d.ts +47 -0
  12. package/dist/dom/highlight_overlay.d.ts.map +1 -0
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1886 -320
  16. package/dist/index.js.map +4 -4
  17. package/dist/tools/chat_surface_tools.d.ts +96 -0
  18. package/dist/tools/chat_surface_tools.d.ts.map +1 -0
  19. package/dist/tools/page_action_tools.d.ts +2 -0
  20. package/dist/tools/page_action_tools.d.ts.map +1 -1
  21. package/dist/ui/clamp_launcher.d.ts +10 -5
  22. package/dist/ui/clamp_launcher.d.ts.map +1 -1
  23. package/dist/ui/clamp_panel.d.ts +2 -2
  24. package/dist/ui/clamp_panel.d.ts.map +1 -1
  25. package/dist/ui/launcher_drag.d.ts +2 -2
  26. package/dist/ui/launcher_drag.d.ts.map +1 -1
  27. package/dist/ui/launcher_placement.d.ts +14 -1
  28. package/dist/ui/launcher_placement.d.ts.map +1 -1
  29. package/dist/ui/panel_drag.d.ts.map +1 -1
  30. package/dist/ui/place_widget.d.ts +9 -1
  31. package/dist/ui/place_widget.d.ts.map +1 -1
  32. package/dist/ui/run_notice.d.ts +14 -3
  33. package/dist/ui/run_notice.d.ts.map +1 -1
  34. package/dist/ui/styles.d.ts +1 -1
  35. package/dist/ui/styles.d.ts.map +1 -1
  36. package/dist/ui/thread_drawer.d.ts +21 -0
  37. package/dist/ui/thread_drawer.d.ts.map +1 -1
  38. package/dist/ui/ui_strings.d.ts +16 -0
  39. package/dist/ui/ui_strings.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/constants.ts +82 -3
  42. package/src/core/ag_ui_chat.ts +965 -55
  43. package/src/dom/animations.ts +30 -0
  44. package/src/dom/highlight_overlay.ts +256 -0
  45. package/src/index.ts +12 -0
  46. package/src/tools/chat_surface_tools.ts +207 -0
  47. package/src/tools/page_action_tools.ts +2 -0
  48. package/src/ui/clamp_launcher.ts +25 -7
  49. package/src/ui/clamp_panel.ts +10 -4
  50. package/src/ui/launcher_drag.ts +11 -2
  51. package/src/ui/launcher_placement.ts +34 -8
  52. package/src/ui/panel_drag.ts +4 -0
  53. package/src/ui/place_widget.ts +11 -3
  54. package/src/ui/run_notice.ts +32 -3
  55. package/src/ui/styles.ts +563 -45
  56. package/src/ui/thread_drawer.ts +138 -8
  57. package/src/ui/ui_strings.ts +24 -0
  58. package/src/version.ts +1 -1
@@ -2,6 +2,14 @@ import type { ThreadMeta } from "../core/conversation_store.js";
2
2
  import { type RelativeTimeFormatter, relativeTime } from "./relative_time.js";
3
3
  import { DEFAULT_UI_STRINGS, type UiStrings } from "./ui_strings.js";
4
4
 
5
+ /**
6
+ * How many conversations there have to be before the filter appears.
7
+ *
8
+ * A search box above a list you can already read in one glance is a control
9
+ * asking to be used on nothing.
10
+ */
11
+ export const FILTER_FROM = 8;
12
+
5
13
  /** Actions the host ({@link AgUiChat}) wires to the drawer's rows. */
6
14
  export interface ThreadDrawerCallbacks {
7
15
  /** A row was picked — load that thread and make it active. */
@@ -12,6 +20,15 @@ export interface ThreadDrawerCallbacks {
12
20
  readonly onRename: (threadId: string, title: string) => void;
13
21
  /** A row was deleted (after the inline confirm). */
14
22
  readonly onDelete: (threadId: string) => void;
23
+ /**
24
+ * The list opened or closed.
25
+ *
26
+ * There are five ways out of it -- the close control, the backdrop, Escape,
27
+ * picking a row and starting a new chat -- and only two of them go through
28
+ * the host. A docked list moves the transcript over, so the host has to know
29
+ * about all five or the transcript stays shifted around nothing.
30
+ */
31
+ readonly onVisibility?: (open: boolean) => void;
15
32
  }
16
33
 
17
34
  /**
@@ -39,6 +56,24 @@ export class ThreadDrawer {
39
56
  #strings: UiStrings;
40
57
  #threads: readonly ThreadMeta[] = [];
41
58
  #activeId = "";
59
+ /** Dismisses the list without starting a new conversation. */
60
+ #closeButton: HTMLButtonElement;
61
+ /** Narrows the list. Only rendered once there is enough of a list to narrow. */
62
+ #filter: HTMLInputElement;
63
+ /** The current filter text, lowercased once rather than per row. */
64
+ #query = "";
65
+ /**
66
+ * Whether the list covers the conversation, or sits beside it.
67
+ *
68
+ * Everything a dialog implies follows from this and nothing else does: the
69
+ * modal role, the focus trap, and taking focus on open. A list docked beside
70
+ * a full-page conversation is a region of that page -- telling assistive
71
+ * technology to ignore the rest of the document while it is showing would be
72
+ * a lie, and stealing focus into it would interrupt someone mid-sentence to
73
+ * announce a list they can already see.
74
+ */
75
+ #modal = true;
76
+
42
77
  /** The element focused before the drawer opened, restored on close. */
43
78
  #lastFocused: HTMLElement | null = null;
44
79
 
@@ -81,13 +116,41 @@ export class ThreadDrawer {
81
116
  this.close();
82
117
  this.#callbacks.onNew();
83
118
  });
84
- header.append(this.#heading, this.#newButton);
119
+ // An explicit way back. The backdrop closes on click, which is enough
120
+ // wherever a strip of it is showing -- but the embedded placement widens
121
+ // the panel to the full width of the host's box, so there is no backdrop
122
+ // left to hit and the only exits were Escape, picking a row, or starting a
123
+ // new conversation. The first is invisible and the last is destructive of
124
+ // the thing you were looking at.
125
+ this.#closeButton = document.createElement("button");
126
+ this.#closeButton.type = "button";
127
+ this.#closeButton.className = "drawer-close";
128
+ this.#closeButton.setAttribute("part", "drawer-close");
129
+ this.#closeButton.title = strings.closeHistory;
130
+ this.#closeButton.setAttribute("aria-label", strings.closeHistory);
131
+ this.#closeButton.append(document.createTextNode("\u00d7"));
132
+ this.#closeButton.addEventListener("click", () => this.close());
133
+ header.append(this.#heading, this.#newButton, this.#closeButton);
134
+
135
+ // Hidden until the list is long enough to be worth narrowing. A search box
136
+ // above four conversations is a control asking to be used on something
137
+ // already entirely visible.
138
+ this.#filter = document.createElement("input");
139
+ this.#filter.type = "search";
140
+ this.#filter.className = "drawer-filter";
141
+ this.#filter.setAttribute("part", "drawer-filter");
142
+ this.#filter.placeholder = strings.searchConversations;
143
+ this.#filter.setAttribute("aria-label", strings.searchConversations);
144
+ this.#filter.addEventListener("input", () => {
145
+ this.#query = this.#filter.value.trim().toLowerCase();
146
+ this.#renderList();
147
+ });
85
148
 
86
149
  this.#list = document.createElement("div");
87
150
  this.#list.className = "drawer-list";
88
151
  this.#list.setAttribute("part", "drawer-list");
89
152
 
90
- this.#panel.append(header, this.#list);
153
+ this.#panel.append(header, this.#filter, this.#list);
91
154
  this.element.append(backdrop, this.#panel);
92
155
  }
93
156
 
@@ -116,9 +179,28 @@ export class ThreadDrawer {
116
179
  this.#panel.setAttribute("aria-label", strings.chatHistory);
117
180
  this.#heading.textContent = strings.chats;
118
181
  this.#newButton.textContent = strings.newChat;
182
+ this.#closeButton.title = strings.closeHistory;
183
+ this.#closeButton.setAttribute("aria-label", strings.closeHistory);
184
+ this.#filter.placeholder = strings.searchConversations;
185
+ this.#filter.setAttribute("aria-label", strings.searchConversations);
119
186
  this.#renderList();
120
187
  }
121
188
 
189
+ /**
190
+ * Whether the list is a dialog over the conversation or a rail beside it.
191
+ * The host decides, because only the placement knows whether there is room.
192
+ */
193
+ setModal(modal: boolean): void {
194
+ this.#modal = modal;
195
+ if (modal) {
196
+ this.#panel.setAttribute("role", "dialog");
197
+ this.#panel.setAttribute("aria-modal", "true");
198
+ } else {
199
+ this.#panel.setAttribute("role", "region");
200
+ this.#panel.removeAttribute("aria-modal");
201
+ }
202
+ }
203
+
122
204
  isOpen(): boolean {
123
205
  return !this.element.hidden;
124
206
  }
@@ -131,7 +213,10 @@ export class ThreadDrawer {
131
213
  // the panel (its first control) so keyboard users land inside the dialog.
132
214
  this.#lastFocused = this.#activeElement() as HTMLElement | null;
133
215
  this.element.hidden = false;
134
- this.#newButton.focus();
216
+ if (this.#modal) {
217
+ this.#newButton.focus();
218
+ }
219
+ this.#callbacks.onVisibility?.(true);
135
220
  }
136
221
 
137
222
  close(): void {
@@ -139,8 +224,13 @@ export class ThreadDrawer {
139
224
  return;
140
225
  }
141
226
  this.element.hidden = true;
142
- this.#lastFocused?.focus();
227
+ // Only if it was taken. Restoring focus that was never moved would pull it
228
+ // back from wherever the user has since put it.
229
+ if (this.#modal) {
230
+ this.#lastFocused?.focus();
231
+ }
143
232
  this.#lastFocused = null;
233
+ this.#callbacks.onVisibility?.(false);
144
234
  }
145
235
 
146
236
  toggle(): void {
@@ -163,7 +253,11 @@ export class ThreadDrawer {
163
253
  this.close();
164
254
  return;
165
255
  }
166
- if (event.key !== "Tab") {
256
+ // Escape still closes a docked rail -- it is a way out, not a modal
257
+ // convention -- but nothing beyond this point applies to one. Tabbing out
258
+ // of a list that sits beside the conversation should reach the
259
+ // conversation, which is the whole difference.
260
+ if (event.key !== "Tab" || !this.#modal) {
167
261
  return;
168
262
  }
169
263
  const focusables = Array.from(
@@ -190,19 +284,55 @@ export class ThreadDrawer {
190
284
 
191
285
  #renderList(): void {
192
286
  this.#list.replaceChildren();
193
- if (this.#threads.length === 0) {
287
+ const hideFilter = this.#threads.length < FILTER_FROM;
288
+ this.#filter.hidden = hideFilter;
289
+ // Cleared as it goes, or the query outlives the control that set it: a
290
+ // list that drops below the threshold while a query matches nothing shows
291
+ // "no conversations match that" over conversations that are right there,
292
+ // with nothing left on screen to clear. Reopening does not help either --
293
+ // only a new drawer would.
294
+ if (hideFilter && this.#query !== "") {
295
+ this.#filter.value = "";
296
+ this.#query = "";
297
+ }
298
+ const shown = this.#matching();
299
+ if (shown.length === 0) {
194
300
  const empty = document.createElement("div");
195
301
  empty.className = "drawer-empty";
196
302
  empty.setAttribute("part", "drawer-empty");
197
- empty.textContent = this.#strings.noConversations;
303
+ // Two different situations wearing one sentence would be a small lie:
304
+ // "no conversations yet" reads as data loss when what happened is that a
305
+ // filter matched nothing.
306
+ empty.textContent =
307
+ this.#threads.length === 0 ? this.#strings.noConversations : this.#strings.noMatches;
198
308
  this.#list.appendChild(empty);
199
309
  return;
200
310
  }
201
- for (const meta of this.#threads) {
311
+ for (const meta of shown) {
202
312
  this.#list.appendChild(this.#renderRow(meta));
203
313
  }
204
314
  }
205
315
 
316
+ /**
317
+ * The rows the filter leaves.
318
+ *
319
+ * Title and preview both, because the title is often the model's summary of
320
+ * a conversation and the thing being looked for is as likely to be a phrase
321
+ * from inside it. Client-side over what the drawer already holds: the server
322
+ * index is fetched whole, so a query would be a round trip to filter a list
323
+ * already in memory.
324
+ */
325
+ #matching(): readonly ThreadMeta[] {
326
+ if (this.#query === "") {
327
+ return this.#threads;
328
+ }
329
+ return this.#threads.filter(
330
+ (meta) =>
331
+ meta.title.toLowerCase().includes(this.#query) ||
332
+ meta.preview.toLowerCase().includes(this.#query),
333
+ );
334
+ }
335
+
206
336
  #renderRow(meta: ThreadMeta): HTMLDivElement {
207
337
  const row = document.createElement("div");
208
338
  row.className = "drawer-row";
@@ -15,6 +15,22 @@ export interface UiStrings {
15
15
  title: string;
16
16
  /** History button + drawer dialog label. */
17
17
  chatHistory: string;
18
+ /** Dismiss the chat-history list and return to the conversation. */
19
+ closeHistory: string;
20
+ /** Placeholder in the conversation-list filter. */
21
+ searchConversations: string;
22
+ /** Names the row of messages waiting for the run to finish. */
23
+ queued: string;
24
+ /** Drops one waiting message. `{text}` is the message. */
25
+ removeQueued: string;
26
+ /** Shown when a filter matches nothing, unlike having no conversations. */
27
+ noMatches: string;
28
+ /** Notice text when the agent moved its own panel out of the way. */
29
+ chatMoved: string;
30
+ /** Notice text when the agent collapsed its own panel. */
31
+ chatMinimised: string;
32
+ /** The one control a run notice may carry. */
33
+ undo: string;
18
34
  /** New-chat button (header + drawer). */
19
35
  newChat: string;
20
36
  /** Collapse button. */
@@ -285,6 +301,14 @@ export interface UiStrings {
285
301
  export const DEFAULT_UI_STRINGS: UiStrings = {
286
302
  title: "Assistant",
287
303
  chatHistory: "Chat history",
304
+ closeHistory: "Close history",
305
+ searchConversations: "Search conversations",
306
+ queued: "Waiting to send",
307
+ removeQueued: 'Do not send "{text}"',
308
+ noMatches: "No conversations match that.",
309
+ chatMoved: "Moved this panel out of the way",
310
+ chatMinimised: "Minimised this panel",
311
+ undo: "Undo",
288
312
  newChat: "New chat",
289
313
  collapse: "Collapse",
290
314
  expand: "Expand",
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION: string = "0.34.0";
1
+ export const VERSION: string = "0.35.0";