@artooi/ag-ui-web-component 0.1.0 → 0.2.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 (109) hide show
  1. package/CHANGELOG.md +102 -2
  2. package/README.md +235 -47
  3. package/dist/ag-ui-web-component.bundle.js +451 -46
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +34 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/{ag_ui_chat.d.ts → core/ag_ui_chat.d.ts} +63 -6
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -0
  9. package/dist/core/agui_client.d.ts.map +1 -0
  10. package/dist/core/conversation_store.d.ts.map +1 -0
  11. package/dist/core/create_http_agent.d.ts.map +1 -0
  12. package/dist/core/define_ag_ui_chat.d.ts.map +1 -0
  13. package/dist/dom/animations.d.ts +71 -0
  14. package/dist/dom/animations.d.ts.map +1 -0
  15. package/dist/{dom_driver.d.ts → dom/dom_driver.d.ts} +7 -1
  16. package/dist/dom/dom_driver.d.ts.map +1 -0
  17. package/dist/dom/native_setter.d.ts +5 -0
  18. package/dist/dom/native_setter.d.ts.map +1 -0
  19. package/dist/index.d.ts +19 -16
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +3886 -388
  22. package/dist/index.js.map +4 -4
  23. package/dist/skills/fill_template.d.ts +17 -0
  24. package/dist/skills/fill_template.d.ts.map +1 -0
  25. package/dist/skills/parse_skills.d.ts +8 -0
  26. package/dist/skills/parse_skills.d.ts.map +1 -0
  27. package/dist/skills/skill.d.ts +24 -0
  28. package/dist/skills/skill.d.ts.map +1 -0
  29. package/dist/{client_tool_registry.d.ts → tools/client_tool_registry.d.ts} +6 -1
  30. package/dist/tools/client_tool_registry.d.ts.map +1 -0
  31. package/dist/tools/is_destructive.d.ts.map +1 -0
  32. package/dist/tools/is_navigates.d.ts.map +1 -0
  33. package/dist/tools/page_map.d.ts.map +1 -0
  34. package/dist/tools/route_map.d.ts +40 -0
  35. package/dist/tools/route_map.d.ts.map +1 -0
  36. package/dist/tools/state_hook.d.ts.map +1 -0
  37. package/dist/ui/confirmation_card.d.ts +22 -0
  38. package/dist/ui/confirmation_card.d.ts.map +1 -0
  39. package/dist/ui/render_markdown.d.ts +13 -0
  40. package/dist/ui/render_markdown.d.ts.map +1 -0
  41. package/dist/ui/reveal_words.d.ts +8 -0
  42. package/dist/ui/reveal_words.d.ts.map +1 -0
  43. package/dist/ui/skills_menu.d.ts +35 -0
  44. package/dist/ui/skills_menu.d.ts.map +1 -0
  45. package/dist/ui/styles.d.ts +2 -0
  46. package/dist/ui/styles.d.ts.map +1 -0
  47. package/dist/ui/tool_call_card.d.ts +35 -0
  48. package/dist/ui/tool_call_card.d.ts.map +1 -0
  49. package/package.json +5 -3
  50. package/src/constants.ts +38 -0
  51. package/src/core/ag_ui_chat.ts +786 -0
  52. package/src/{agui_client.ts → core/agui_client.ts} +1 -1
  53. package/src/{define_ag_ui_chat.ts → core/define_ag_ui_chat.ts} +1 -1
  54. package/src/dom/animations.ts +201 -0
  55. package/src/{dom_driver.ts → dom/dom_driver.ts} +35 -2
  56. package/src/dom/native_setter.ts +42 -0
  57. package/src/index.ts +61 -32
  58. package/src/skills/fill_template.ts +32 -0
  59. package/src/skills/parse_skills.ts +26 -0
  60. package/src/skills/skill.ts +23 -0
  61. package/src/{client_tool_registry.ts → tools/client_tool_registry.ts} +6 -4
  62. package/src/{is_destructive.ts → tools/is_destructive.ts} +1 -1
  63. package/src/{is_navigates.ts → tools/is_navigates.ts} +1 -1
  64. package/src/tools/route_map.ts +140 -0
  65. package/src/{state_hook.ts → tools/state_hook.ts} +1 -1
  66. package/src/ui/confirmation_card.ts +71 -0
  67. package/src/ui/render_markdown.ts +68 -0
  68. package/src/ui/reveal_words.ts +41 -0
  69. package/src/ui/skills_menu.ts +171 -0
  70. package/src/ui/styles.ts +631 -0
  71. package/src/{tool_call_card.ts → ui/tool_call_card.ts} +50 -19
  72. package/src/version.ts +1 -1
  73. package/dist/ag_ui_chat.d.ts.map +0 -1
  74. package/dist/agui_client.d.ts.map +0 -1
  75. package/dist/animations.d.ts +0 -33
  76. package/dist/animations.d.ts.map +0 -1
  77. package/dist/client_tool_registry.d.ts.map +0 -1
  78. package/dist/confirmation_modal.d.ts +0 -14
  79. package/dist/confirmation_modal.d.ts.map +0 -1
  80. package/dist/conversation_store.d.ts.map +0 -1
  81. package/dist/create_http_agent.d.ts.map +0 -1
  82. package/dist/define_ag_ui_chat.d.ts.map +0 -1
  83. package/dist/dom_driver.d.ts.map +0 -1
  84. package/dist/is_destructive.d.ts.map +0 -1
  85. package/dist/is_navigates.d.ts.map +0 -1
  86. package/dist/page_map.d.ts.map +0 -1
  87. package/dist/route_map.d.ts +0 -27
  88. package/dist/route_map.d.ts.map +0 -1
  89. package/dist/state_hook.d.ts.map +0 -1
  90. package/dist/styles.d.ts +0 -2
  91. package/dist/styles.d.ts.map +0 -1
  92. package/dist/tool_call_card.d.ts +0 -29
  93. package/dist/tool_call_card.d.ts.map +0 -1
  94. package/src/ag_ui_chat.ts +0 -411
  95. package/src/animations.ts +0 -86
  96. package/src/confirmation_modal.ts +0 -69
  97. package/src/route_map.ts +0 -83
  98. package/src/styles.ts +0 -296
  99. /package/dist/{agui_client.d.ts → core/agui_client.d.ts} +0 -0
  100. /package/dist/{conversation_store.d.ts → core/conversation_store.d.ts} +0 -0
  101. /package/dist/{create_http_agent.d.ts → core/create_http_agent.d.ts} +0 -0
  102. /package/dist/{define_ag_ui_chat.d.ts → core/define_ag_ui_chat.d.ts} +0 -0
  103. /package/dist/{is_destructive.d.ts → tools/is_destructive.d.ts} +0 -0
  104. /package/dist/{is_navigates.d.ts → tools/is_navigates.d.ts} +0 -0
  105. /package/dist/{page_map.d.ts → tools/page_map.d.ts} +0 -0
  106. /package/dist/{state_hook.d.ts → tools/state_hook.d.ts} +0 -0
  107. /package/src/{conversation_store.ts → core/conversation_store.ts} +0 -0
  108. /package/src/{create_http_agent.ts → core/create_http_agent.ts} +0 -0
  109. /package/src/{page_map.ts → tools/page_map.ts} +0 -0
package/src/ag_ui_chat.ts DELETED
@@ -1,411 +0,0 @@
1
- import type { Context, Message, Tool } from "@ag-ui/core";
2
- import {
3
- AgUiClient,
4
- type AgUiClientHandlers,
5
- type AgUiToolCall,
6
- type ToolExecution,
7
- } from "./agui_client.js";
8
- import { type ClientTool, ClientToolRegistry } from "./client_tool_registry.js";
9
- import { requestConfirmation } from "./confirmation_modal.js";
10
- import { MESSAGE_ROLE, SUBMIT_EVENT, TOOL_CALL_STATUS } from "./constants.js";
11
- import {
12
- type ClientConversationStore,
13
- type NavigationCheckpoint,
14
- SessionStorageStore,
15
- } from "./conversation_store.js";
16
- import { type AgentFactory, createHttpAgent } from "./create_http_agent.js";
17
- import { isDestructive } from "./is_destructive.js";
18
- import { isNavigates } from "./is_navigates.js";
19
- import { createPageMapContext, type PageMap } from "./page_map.js";
20
- import { createRouteTools, type RouteMap } from "./route_map.js";
21
- import { createStateHookTools, type StateHook } from "./state_hook.js";
22
- import { STYLES } from "./styles.js";
23
- import { ToolCallCard } from "./tool_call_card.js";
24
-
25
- /** The role a rendered chat message takes. */
26
- export type MessageRole = (typeof MESSAGE_ROLE)[keyof typeof MESSAGE_ROLE];
27
-
28
- /** `detail` shape of the {@link SUBMIT_EVENT} CustomEvent. */
29
- export interface SubmitDetail {
30
- readonly content: string;
31
- }
32
-
33
- /**
34
- * `<ag-ui-chat>` — a framework-free chat sidebar Web Component over AG-UI.
35
- *
36
- * Owns the Shadow DOM shell (header, scrolling message list, input row),
37
- * builds an {@link AgUiClient} on first send (via the overridable
38
- * {@link agentFactory}), and renders streaming assistant text plus tool-call
39
- * activity. Emits a {@link SUBMIT_EVENT} for host visibility.
40
- *
41
- * The per-run frontend tool catalog and context are supplied by
42
- * {@link getTools} / {@link getContext}, which later phases (the tool
43
- * registry, DOM driver) populate.
44
- */
45
- export class AgUiChat extends HTMLElement {
46
- /** Agent factory; override to inject a custom or fake agent (tests). */
47
- agentFactory: AgentFactory = createHttpAgent;
48
-
49
- /** Extra HTTP headers for the AG-UI endpoint (e.g. CSRF). */
50
- headers: Record<string, string> = {};
51
-
52
- /** When true, destructive tools execute without a confirmation modal. */
53
- autoConfirm = false;
54
-
55
- /**
56
- * Per-run frontend tool catalog provider. Defaults to the built-in
57
- * `route.*` tools (when a {@link routeMap} is set) plus the tools registered
58
- * via {@link registerTool} / {@link registerStateHook}; override to supply a
59
- * fully custom catalog.
60
- */
61
- getTools: () => Tool[] = () => [
62
- ...this.#routeTools().map((t) => ({
63
- name: t.name,
64
- description: t.description,
65
- parameters: t.parameters,
66
- })),
67
- ...this.#toolRegistry.tools(),
68
- ];
69
-
70
- /**
71
- * Per-run context provider. Defaults to the compact page map (when a
72
- * {@link getPageMap} provider is set and {@link autoInjectPageMap} is on).
73
- */
74
- getContext: () => Context[] = () => createPageMapContext(this.getPageMap, this.autoInjectPageMap);
75
-
76
- /**
77
- * Navigable routes the agent can jump to via the built-in `route.*` tools.
78
- * A compact summary also rides in each run's context.
79
- */
80
- routeMap: RouteMap = [];
81
-
82
- /**
83
- * Optional client-side router. When set (an SPA), `navigate_to_route` routes
84
- * in-page and the run loop continues; when unset (an MPA like the admin), it
85
- * falls back to `window.location` and the resumable-loop machinery applies.
86
- */
87
- navigate: ((path: string) => void) | null = null;
88
-
89
- /** Provider for the per-run page map; see {@link getContext}. */
90
- getPageMap: (() => PageMap) | null = null;
91
-
92
- /** Whether to auto-inject the page map into context each run. */
93
- autoInjectPageMap = true;
94
-
95
- /**
96
- * Persistence for the conversation + navigation checkpoint. Defaults to
97
- * per-tab `sessionStorage` so the chat survives full page reloads; inject a
98
- * server-backed store for cross-tab/device durability.
99
- */
100
- conversationStore: ClientConversationStore = new SessionStorageStore();
101
-
102
- /**
103
- * Builds the tool result a navigating tool resumes with after the page
104
- * reloads. Defaults to the landed URL; a host (e.g. the admin package) can
105
- * override to include a page snapshot or post-reload validation errors.
106
- */
107
- navigationResult: (checkpoint: NavigationCheckpoint) => unknown = () => ({
108
- navigated: true,
109
- url: window.location.href,
110
- });
111
-
112
- readonly #toolRegistry = new ClientToolRegistry();
113
- /** Tool-call cards awaiting execution, keyed by call id. */
114
- readonly #toolCards = new Map<string, ToolCallCard>();
115
- readonly #root: ShadowRoot;
116
- readonly #chat: HTMLDivElement;
117
- readonly #messages: HTMLDivElement;
118
- readonly #input: HTMLTextAreaElement;
119
- readonly #send: HTMLButtonElement;
120
-
121
- #client: AgUiClient | null = null;
122
- #streamingBubble: HTMLDivElement | null = null;
123
- #threadId = "";
124
- #initialMessages: readonly Message[] = [];
125
-
126
- constructor() {
127
- super();
128
- this.#root = this.attachShadow({ mode: "open" });
129
- this.#chat = document.createElement("div");
130
- this.#messages = document.createElement("div");
131
- this.#input = document.createElement("textarea");
132
- this.#send = document.createElement("button");
133
- }
134
-
135
- /** Declare a frontend tool the agent may call. */
136
- registerTool(tool: ClientTool): void {
137
- this.#toolRegistry.register(tool);
138
- }
139
-
140
- /** Bind a piece of host state to `read_<name>` / `set_<name>` tools. */
141
- registerStateHook(hook: StateHook): void {
142
- for (const tool of createStateHookTools(hook)) {
143
- this.#toolRegistry.register(tool);
144
- }
145
- }
146
-
147
- /** The built-in `route.*` tools, present only when a route map is set. */
148
- #routeTools(): ClientTool[] {
149
- if (this.routeMap.length === 0) {
150
- return [];
151
- }
152
- return createRouteTools(
153
- () => this.routeMap,
154
- () => this.navigate,
155
- );
156
- }
157
-
158
- /** Resolve a tool by name: built-in route tools first, then the registry. */
159
- #resolveTool(name: string): ClientTool | null {
160
- const route = this.#routeTools().find((t) => t.name === name);
161
- if (route !== undefined) {
162
- return route;
163
- }
164
- return this.#toolRegistry.has(name) ? this.#toolRegistry.get(name) : null;
165
- }
166
-
167
- /** The AG-UI endpoint URL, read from the `endpoint` attribute. */
168
- get endpoint(): string {
169
- return this.getAttribute("endpoint") ?? "";
170
- }
171
-
172
- connectedCallback(): void {
173
- this.#render();
174
- this.#threadId = this.conversationStore.threadId();
175
- void this.#rehydrate();
176
- }
177
-
178
- /**
179
- * Restore the conversation from the store on mount, then — if a navigating
180
- * tool reloaded the page mid-run — resume the loop by supplying that tool's
181
- * result from the page we landed on.
182
- */
183
- async #rehydrate(): Promise<void> {
184
- const messages = await this.conversationStore.loadMessages(this.#threadId);
185
- if (messages !== null) {
186
- this.#initialMessages = messages;
187
- for (const message of messages) {
188
- this.#renderHistoricMessage(message);
189
- }
190
- }
191
- const checkpoint = this.conversationStore.loadCheckpoint(this.#threadId);
192
- if (checkpoint !== null) {
193
- await this.#resumeFrom(checkpoint);
194
- }
195
- }
196
-
197
- /** Render a restored message as a chat bubble (text turns only). */
198
- #renderHistoricMessage(message: Message): void {
199
- if (typeof message.content !== "string" || message.content === "") {
200
- return;
201
- }
202
- if (message.role === MESSAGE_ROLE.USER) {
203
- this.appendMessage(MESSAGE_ROLE.USER, message.content);
204
- } else if (message.role === MESSAGE_ROLE.ASSISTANT) {
205
- this.appendMessage(MESSAGE_ROLE.ASSISTANT, message.content);
206
- }
207
- }
208
-
209
- /** Complete the checkpointed navigating tool call and continue the run. */
210
- async #resumeFrom(checkpoint: NavigationCheckpoint): Promise<void> {
211
- this.conversationStore.saveCheckpoint(this.#threadId, null);
212
- const client = this.#ensureClient();
213
- client.addToolResult(checkpoint.toolCallId, JSON.stringify(this.navigationResult(checkpoint)));
214
- await client.resume();
215
- }
216
-
217
- /** Append a message bubble and return it. */
218
- appendMessage(role: MessageRole, content: string): HTMLDivElement {
219
- const bubble = document.createElement("div");
220
- bubble.className = `message message--${role}`;
221
- bubble.textContent = content;
222
- this.#messages.appendChild(bubble);
223
- this.#messages.scrollTop = this.#messages.scrollHeight;
224
- return bubble;
225
- }
226
-
227
- #render(): void {
228
- const style = document.createElement("style");
229
- style.textContent = STYLES;
230
-
231
- this.#chat.className = "chat";
232
-
233
- const header = document.createElement("div");
234
- header.className = "header";
235
- header.textContent = this.getAttribute("title-text") ?? "Assistant";
236
-
237
- this.#messages.className = "messages";
238
-
239
- const inputRow = document.createElement("div");
240
- inputRow.className = "input-row";
241
-
242
- this.#input.className = "input";
243
- this.#input.rows = 2;
244
- this.#input.placeholder = "Ask anything…";
245
- this.#input.addEventListener("keydown", (event) => this.#onKeydown(event));
246
-
247
- this.#send.className = "send";
248
- this.#send.type = "button";
249
- this.#send.textContent = "Send";
250
- this.#send.addEventListener("click", () => {
251
- void this.#submit();
252
- });
253
-
254
- inputRow.append(this.#input, this.#send);
255
- this.#chat.append(header, this.#messages, inputRow);
256
- this.#root.append(style, this.#chat);
257
- }
258
-
259
- #onKeydown(event: KeyboardEvent): void {
260
- if (event.key === "Enter" && !event.shiftKey) {
261
- event.preventDefault();
262
- void this.#submit();
263
- }
264
- }
265
-
266
- async #submit(): Promise<void> {
267
- const content = this.#input.value.trim();
268
- if (content === "") {
269
- return;
270
- }
271
- this.appendMessage(MESSAGE_ROLE.USER, content);
272
- this.#input.value = "";
273
- this.dispatchEvent(
274
- new CustomEvent<SubmitDetail>(SUBMIT_EVENT, {
275
- detail: { content },
276
- bubbles: true,
277
- composed: true,
278
- }),
279
- );
280
- await this.#client_send(content);
281
- }
282
-
283
- async #client_send(content: string): Promise<void> {
284
- if (this.endpoint === "") {
285
- return;
286
- }
287
- await this.#ensureClient().send(content);
288
- }
289
-
290
- #ensureClient(): AgUiClient {
291
- if (this.#client === null) {
292
- const agent = this.agentFactory({
293
- endpoint: this.endpoint,
294
- headers: this.headers,
295
- threadId: this.#threadId,
296
- initialMessages: this.#initialMessages,
297
- });
298
- this.#client = new AgUiClient({
299
- agent,
300
- handlers: this.#handlers(),
301
- getTools: () => this.getTools(),
302
- getContext: () => this.getContext(),
303
- executeTool: (call) => this.#executeTool(call),
304
- onPersist: (messages) => this.conversationStore.saveMessages(this.#threadId, messages),
305
- });
306
- }
307
- return this.#client;
308
- }
309
-
310
- async #executeTool(call: AgUiToolCall): Promise<ToolExecution | null> {
311
- const card = this.#cardFor(call);
312
- this.#toolCards.delete(call.id);
313
- const tool = this.#resolveTool(call.name);
314
- if (tool === null) {
315
- // A server-side tool the server already executed — not ours to re-run.
316
- card.settle(TOOL_CALL_STATUS.DONE, "Executed on the server.");
317
- return null;
318
- }
319
- if (isDestructive(tool.parameters) && !this.autoConfirm) {
320
- const accepted = await requestConfirmation(this.#chat, {
321
- toolName: call.name,
322
- args: call.args,
323
- });
324
- if (!accepted) {
325
- const message = "User declined the action.";
326
- card.settle(TOOL_CALL_STATUS.DECLINED, message);
327
- return { content: message };
328
- }
329
- }
330
- // A navigating tool reloads only without a client-side router; with a
331
- // host `navigate()` (SPA) it routes in-page and the loop just continues.
332
- const navigates = isNavigates(tool.parameters) && this.navigate === null;
333
- if (navigates) {
334
- // Checkpoint before the handler reloads the page; the history (incl.
335
- // this tool call) was already persisted when the run that produced it
336
- // settled. The result is supplied on the next mount via the resume path.
337
- this.conversationStore.saveCheckpoint(this.#threadId, { toolCallId: call.id });
338
- }
339
- try {
340
- const result = await tool.handler(call.args);
341
- if (navigates) {
342
- card.settle(TOOL_CALL_STATUS.DONE, "Navigating…");
343
- return { content: "", halt: true };
344
- }
345
- const content = JSON.stringify(result ?? null);
346
- card.settle(TOOL_CALL_STATUS.DONE, content);
347
- return { content };
348
- } catch (error) {
349
- if (navigates) {
350
- // The navigation never happened; drop the dangling checkpoint.
351
- this.conversationStore.saveCheckpoint(this.#threadId, null);
352
- }
353
- const message = error instanceof Error ? error.message : String(error);
354
- card.settle(TOOL_CALL_STATUS.ERROR, message);
355
- return { content: `Error: ${message}`, error: message };
356
- }
357
- }
358
-
359
- #handlers(): AgUiClientHandlers {
360
- return {
361
- onRunStart: () => {
362
- this.#send.disabled = true;
363
- },
364
- onTextDelta: (buffer) => {
365
- this.#streamInto(buffer);
366
- },
367
- onTextEnd: (buffer) => {
368
- this.#streamInto(buffer);
369
- this.#streamingBubble = null;
370
- },
371
- onToolCall: (call) => {
372
- this.#cardFor(call);
373
- },
374
- onRunEnd: () => {
375
- this.#send.disabled = false;
376
- this.#streamingBubble = null;
377
- },
378
- onError: (message) => {
379
- this.appendMessage(MESSAGE_ROLE.ASSISTANT, `⚠️ ${message}`);
380
- this.#send.disabled = false;
381
- this.#streamingBubble = null;
382
- },
383
- };
384
- }
385
-
386
- #streamInto(buffer: string): void {
387
- if (this.#streamingBubble === null) {
388
- this.#streamingBubble = this.appendMessage(MESSAGE_ROLE.ASSISTANT, "");
389
- }
390
- this.#streamingBubble.textContent = buffer;
391
- this.#messages.scrollTop = this.#messages.scrollHeight;
392
- }
393
-
394
- /**
395
- * The card for ``call``, creating and appending it on first sight.
396
- *
397
- * {@link AgUiClientHandlers.onToolCall} creates the card (pending) during the
398
- * run; {@link #executeTool} later retrieves the same card to settle it.
399
- */
400
- #cardFor(call: AgUiToolCall): ToolCallCard {
401
- const existing = this.#toolCards.get(call.id);
402
- if (existing !== undefined) {
403
- return existing;
404
- }
405
- const card = new ToolCallCard(call.name, call.args);
406
- this.#toolCards.set(call.id, card);
407
- this.#messages.appendChild(card.element);
408
- this.#messages.scrollTop = this.#messages.scrollHeight;
409
- return card;
410
- }
411
- }
package/src/animations.ts DELETED
@@ -1,86 +0,0 @@
1
- /**
2
- * Visible-action animation primitives.
3
- *
4
- * These run against the **host page** DOM (not the element's shadow root) so a
5
- * user watches the agent type, highlight, and click at human-readable speed.
6
- * Each is configurable; pass small/zero durations in tests (or use fake timers).
7
- */
8
-
9
- const ACCENT = "#4f46e5";
10
-
11
- function delay(ms: number): Promise<void> {
12
- return new Promise<void>((resolve) => {
13
- setTimeout(resolve, ms);
14
- });
15
- }
16
-
17
- /** An element whose `value` can be typed into. */
18
- export type TextLikeElement = HTMLInputElement | HTMLTextAreaElement;
19
-
20
- export interface TypeOptions {
21
- /** Milliseconds between characters. Default 35. */
22
- charDelayMs?: number;
23
- }
24
-
25
- /**
26
- * Clear ``el`` and type ``value`` one character at a time, firing ``input``
27
- * events as a real user would, then a final ``change`` event.
28
- */
29
- export async function typeInto(
30
- el: TextLikeElement,
31
- value: string,
32
- options: TypeOptions = {},
33
- ): Promise<void> {
34
- const charDelayMs = options.charDelayMs ?? 35;
35
- el.value = "";
36
- el.dispatchEvent(new Event("input", { bubbles: true }));
37
- for (const char of value) {
38
- el.value += char;
39
- el.dispatchEvent(new Event("input", { bubbles: true }));
40
- if (charDelayMs > 0) {
41
- await delay(charDelayMs);
42
- }
43
- }
44
- el.dispatchEvent(new Event("change", { bubbles: true }));
45
- }
46
-
47
- export interface HighlightClickOptions {
48
- /** Milliseconds to hold the highlight before clicking. Default 280. */
49
- highlightMs?: number;
50
- }
51
-
52
- /** Outline ``el``, pause so the user sees it, then click and restore. */
53
- export async function highlightThenClick(
54
- el: HTMLElement,
55
- options: HighlightClickOptions = {},
56
- ): Promise<void> {
57
- const highlightMs = options.highlightMs ?? 280;
58
- const previousOutline = el.style.outline;
59
- const previousOffset = el.style.outlineOffset;
60
- el.style.outline = `2px solid ${ACCENT}`;
61
- el.style.outlineOffset = "2px";
62
- await delay(highlightMs);
63
- el.style.outline = previousOutline;
64
- el.style.outlineOffset = previousOffset;
65
- el.click();
66
- }
67
-
68
- /** Scroll ``el`` to the vertical centre of the viewport. */
69
- export function scrollIntoCenterView(el: HTMLElement): void {
70
- el.scrollIntoView({ block: "center", inline: "nearest", behavior: "smooth" });
71
- }
72
-
73
- export interface FlashOptions {
74
- /** Milliseconds to hold the focus flash. Default 200. */
75
- flashMs?: number;
76
- }
77
-
78
- /** Focus ``el`` and briefly flash a ring around it. */
79
- export async function focusWithFlash(el: HTMLElement, options: FlashOptions = {}): Promise<void> {
80
- const flashMs = options.flashMs ?? 200;
81
- el.focus();
82
- const previousShadow = el.style.boxShadow;
83
- el.style.boxShadow = `0 0 0 3px rgba(79, 70, 229, 0.4)`;
84
- await delay(flashMs);
85
- el.style.boxShadow = previousShadow;
86
- }
@@ -1,69 +0,0 @@
1
- /** What the confirmation modal displays. */
2
- export interface ConfirmationRequest {
3
- toolName: string;
4
- args: Record<string, unknown>;
5
- }
6
-
7
- /**
8
- * Render a confirmation modal into ``host`` and resolve when the user decides.
9
- *
10
- * Resolves ``true`` if the user confirms, ``false`` if they cancel or dismiss
11
- * via the backdrop. The modal is appended to ``host`` (the chat container
12
- * inside the element's shadow root) and removed once resolved.
13
- */
14
- export function requestConfirmation(
15
- host: Node & ParentNode,
16
- request: ConfirmationRequest,
17
- ): Promise<boolean> {
18
- return new Promise<boolean>((resolve) => {
19
- const overlay = document.createElement("div");
20
- overlay.className = "modal-overlay";
21
-
22
- const dialog = document.createElement("div");
23
- dialog.className = "modal";
24
-
25
- const title = document.createElement("div");
26
- title.className = "modal-title";
27
- title.textContent = "Confirm action";
28
-
29
- const body = document.createElement("div");
30
- body.className = "modal-body";
31
- body.textContent = `Run “${request.toolName}”?`;
32
-
33
- const args = document.createElement("pre");
34
- args.className = "modal-args";
35
- args.textContent = JSON.stringify(request.args, null, 2);
36
-
37
- const actions = document.createElement("div");
38
- actions.className = "modal-actions";
39
-
40
- const cancel = document.createElement("button");
41
- cancel.className = "modal-btn modal-btn--cancel";
42
- cancel.type = "button";
43
- cancel.textContent = "Cancel";
44
-
45
- const confirm = document.createElement("button");
46
- confirm.className = "modal-btn modal-btn--confirm";
47
- confirm.type = "button";
48
- confirm.textContent = "Run";
49
-
50
- const close = (accepted: boolean): void => {
51
- overlay.remove();
52
- resolve(accepted);
53
- };
54
-
55
- cancel.addEventListener("click", () => close(false));
56
- confirm.addEventListener("click", () => close(true));
57
- overlay.addEventListener("click", (event) => {
58
- if (event.target === overlay) {
59
- close(false);
60
- }
61
- });
62
-
63
- actions.append(cancel, confirm);
64
- dialog.append(title, body, args, actions);
65
- overlay.append(dialog);
66
- host.appendChild(overlay);
67
- confirm.focus();
68
- });
69
- }
package/src/route_map.ts DELETED
@@ -1,83 +0,0 @@
1
- import type { ClientTool } from "./client_tool_registry.js";
2
- import { X_NAVIGATES_KEY } from "./constants.js";
3
-
4
- /** A single navigable route the host declares for the agent. */
5
- export interface Route {
6
- /** Stable id the agent uses with `navigate_to_route`. */
7
- readonly id: string;
8
- /** The URL path to navigate to. */
9
- readonly path: string;
10
- /** Human label shown to the agent. */
11
- readonly title?: string;
12
- /** Optional grouping (e.g. an app or section). */
13
- readonly group?: string;
14
- /** Optional longer description of when to use the route. */
15
- readonly description?: string;
16
- }
17
-
18
- /** The host-declared catalog of navigable routes. */
19
- export type RouteMap = readonly Route[];
20
-
21
- /** Append `params` to `path` as a query string (no-op when empty). */
22
- function withQuery(path: string, params: Record<string, unknown> | undefined): string {
23
- if (params === undefined) {
24
- return path;
25
- }
26
- const usp = new URLSearchParams();
27
- for (const [key, value] of Object.entries(params)) {
28
- usp.set(key, String(value));
29
- }
30
- const query = usp.toString();
31
- return query === "" ? path : `${path}?${query}`;
32
- }
33
-
34
- /**
35
- * The built-in `route.*` tools, bound to live getters so a host can set
36
- * `routeMap` / `navigate` before or after mount.
37
- *
38
- * `list_routes` is read-only; `navigate_to_route` is marked `x-navigates` so an
39
- * MPA reload checkpoints + resumes. When the host supplies a `navigate(path)`
40
- * callback (an SPA), the element routes client-side instead and the run loop
41
- * simply continues — see `AgUiChat`'s execute path.
42
- */
43
- export function createRouteTools(
44
- getRouteMap: () => RouteMap,
45
- getNavigate: () => ((path: string) => void) | null,
46
- ): ClientTool[] {
47
- return [
48
- {
49
- name: "list_routes",
50
- description: "List the routes the app can navigate to.",
51
- parameters: { type: "object", properties: {}, required: [] },
52
- handler: () => getRouteMap(),
53
- },
54
- {
55
- name: "navigate_to_route",
56
- description: "Navigate to one of the app's routes by its id.",
57
- parameters: {
58
- type: "object",
59
- properties: {
60
- route_id: { type: "string" },
61
- params: { type: "object" },
62
- },
63
- required: ["route_id"],
64
- [X_NAVIGATES_KEY]: true,
65
- },
66
- handler: (args) => {
67
- const routeId = args["route_id"];
68
- const route = getRouteMap().find((r) => r.id === routeId);
69
- if (route === undefined) {
70
- throw new Error(`unknown route "${String(routeId)}"`);
71
- }
72
- const path = withQuery(route.path, args["params"] as Record<string, unknown> | undefined);
73
- const navigate = getNavigate();
74
- if (navigate !== null) {
75
- navigate(path);
76
- } else {
77
- window.location.assign(path);
78
- }
79
- return { navigated: true, path };
80
- },
81
- },
82
- ];
83
- }