@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
@@ -1,6 +1,6 @@
1
1
  import { type AbstractAgent, type AgentSubscriber, randomUUID } from "@ag-ui/client";
2
2
  import type { Context, Message, Tool } from "@ag-ui/core";
3
- import { MAX_TOOL_ROUNDS } from "./constants.js";
3
+ import { MAX_TOOL_ROUNDS } from "../constants.js";
4
4
 
5
5
  /** A tool call surfaced to the host by {@link AgUiClient}. */
6
6
  export interface AgUiToolCall {
@@ -1,5 +1,5 @@
1
+ import { ELEMENT_TAG } from "../constants.js";
1
2
  import { AgUiChat } from "./ag_ui_chat.js";
2
- import { ELEMENT_TAG } from "./constants.js";
3
3
 
4
4
  /**
5
5
  * Register the `<ag-ui-chat>` Custom Element.
@@ -0,0 +1,201 @@
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
+ import { setNativeChecked, setNativeValue } from "./native_setter.js";
10
+
11
+ const ACCENT = "#4f46e5";
12
+
13
+ function delay(ms: number): Promise<void> {
14
+ return new Promise<void>((resolve) => {
15
+ setTimeout(resolve, ms);
16
+ });
17
+ }
18
+
19
+ /** An element whose `value` can be typed into. */
20
+ export type TextLikeElement = HTMLInputElement | HTMLTextAreaElement;
21
+
22
+ export interface TypeOptions {
23
+ /** Milliseconds between characters. Default 35. */
24
+ charDelayMs?: number;
25
+ }
26
+
27
+ /**
28
+ * Clear ``el`` and type ``value`` one character at a time, firing ``input``
29
+ * events as a real user would, then a final ``change`` event.
30
+ */
31
+ export async function typeInto(
32
+ el: TextLikeElement,
33
+ value: string,
34
+ options: TypeOptions = {},
35
+ ): Promise<void> {
36
+ const charDelayMs = options.charDelayMs ?? 35;
37
+ setNativeValue(el, "");
38
+ el.dispatchEvent(new Event("input", { bubbles: true }));
39
+ for (const char of value) {
40
+ setNativeValue(el, el.value + char);
41
+ el.dispatchEvent(new Event("input", { bubbles: true }));
42
+ if (charDelayMs > 0) {
43
+ await delay(charDelayMs);
44
+ }
45
+ }
46
+ el.dispatchEvent(new Event("change", { bubbles: true }));
47
+ }
48
+
49
+ export interface HighlightClickOptions {
50
+ /** Milliseconds to hold the highlight before clicking. Default 280. */
51
+ highlightMs?: number;
52
+ }
53
+
54
+ /** Outline ``el``, pause so the user sees it, then click and restore. */
55
+ export async function highlightThenClick(
56
+ el: HTMLElement,
57
+ options: HighlightClickOptions = {},
58
+ ): Promise<void> {
59
+ const highlightMs = options.highlightMs ?? 280;
60
+ const previousOutline = el.style.outline;
61
+ const previousOffset = el.style.outlineOffset;
62
+ el.style.outline = `2px solid ${ACCENT}`;
63
+ el.style.outlineOffset = "2px";
64
+ await delay(highlightMs);
65
+ el.style.outline = previousOutline;
66
+ el.style.outlineOffset = previousOffset;
67
+ el.click();
68
+ }
69
+
70
+ /** Scroll ``el`` to the vertical centre of the viewport. */
71
+ export function scrollIntoCenterView(el: HTMLElement): void {
72
+ el.scrollIntoView({ block: "center", inline: "nearest", behavior: "smooth" });
73
+ }
74
+
75
+ export interface FlashOptions {
76
+ /** Milliseconds to hold the focus flash. Default 200. */
77
+ flashMs?: number;
78
+ }
79
+
80
+ /** Focus ``el`` and briefly flash a ring around it. */
81
+ export async function focusWithFlash(el: HTMLElement, options: FlashOptions = {}): Promise<void> {
82
+ const flashMs = options.flashMs ?? 200;
83
+ el.focus();
84
+ const previousShadow = el.style.boxShadow;
85
+ el.style.boxShadow = `0 0 0 3px rgba(79, 70, 229, 0.4)`;
86
+ await delay(flashMs);
87
+ el.style.boxShadow = previousShadow;
88
+ }
89
+
90
+ const RING = "0 0 0 3px rgba(79, 70, 229, 0.4)";
91
+
92
+ /**
93
+ * Whether the user has asked the OS/browser to minimise motion.
94
+ *
95
+ * The richer action animations below check this and skip their hold delays so
96
+ * the agent's edits still happen (events fire, values set) but without the
97
+ * visible pause. The character-typing / highlight primitives above predate this
98
+ * and keep their explicit-duration contract.
99
+ */
100
+ export function prefersReducedMotion(): boolean {
101
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
102
+ }
103
+
104
+ /** Like {@link delay}, but resolves immediately under reduced motion. */
105
+ function motionDelay(ms: number): Promise<void> {
106
+ if (ms <= 0 || prefersReducedMotion()) {
107
+ return Promise.resolve();
108
+ }
109
+ return delay(ms);
110
+ }
111
+
112
+ export interface PressOptions {
113
+ /** Milliseconds to hold the pressed state before clicking. Default 140. */
114
+ pressMs?: number;
115
+ }
116
+
117
+ /**
118
+ * Show a brief "pressed" affordance on a button/control — a slight scale-down
119
+ * plus accent ring — then click it and restore. Reads as an actual press, not
120
+ * just an outline (cf. {@link highlightThenClick}).
121
+ */
122
+ export async function pressThenClick(el: HTMLElement, options: PressOptions = {}): Promise<void> {
123
+ const pressMs = options.pressMs ?? 140;
124
+ const previousTransform = el.style.transform;
125
+ const previousTransition = el.style.transition;
126
+ const previousShadow = el.style.boxShadow;
127
+ el.style.transition = "transform 80ms ease";
128
+ el.style.transform = "scale(0.96)";
129
+ el.style.boxShadow = RING;
130
+ await motionDelay(pressMs);
131
+ el.style.transform = previousTransform;
132
+ el.style.transition = previousTransition;
133
+ el.style.boxShadow = previousShadow;
134
+ el.click();
135
+ }
136
+
137
+ export interface SelectOptions {
138
+ /** Milliseconds to hold the highlight before committing. Default 220. */
139
+ highlightMs?: number;
140
+ }
141
+
142
+ /** Find an option in ``el`` whose value or visible text equals ``value``. */
143
+ function findOption(el: HTMLSelectElement, value: string): HTMLOptionElement | null {
144
+ for (const option of Array.from(el.options)) {
145
+ if (option.value === value || option.text === value) {
146
+ return option;
147
+ }
148
+ }
149
+ return null;
150
+ }
151
+
152
+ /**
153
+ * Outline a `<select>`, pause so the user sees the pick, then set it to the
154
+ * option matching ``value`` (by value or visible text) and fire `input` +
155
+ * `change`. Throws when no option matches.
156
+ */
157
+ export async function selectOption(
158
+ el: HTMLSelectElement,
159
+ value: string,
160
+ options: SelectOptions = {},
161
+ ): Promise<void> {
162
+ const option = findOption(el, value);
163
+ if (option === null) {
164
+ throw new Error(`no <option> matching "${value}"`);
165
+ }
166
+ const highlightMs = options.highlightMs ?? 220;
167
+ const previousOutline = el.style.outline;
168
+ const previousOffset = el.style.outlineOffset;
169
+ el.style.outline = `2px solid ${ACCENT}`;
170
+ el.style.outlineOffset = "2px";
171
+ await motionDelay(highlightMs);
172
+ setNativeValue(el, option.value);
173
+ el.dispatchEvent(new Event("input", { bubbles: true }));
174
+ el.dispatchEvent(new Event("change", { bubbles: true }));
175
+ el.style.outline = previousOutline;
176
+ el.style.outlineOffset = previousOffset;
177
+ }
178
+
179
+ export interface ToggleOptions {
180
+ /** Milliseconds to hold the flash. Default 200. */
181
+ flashMs?: number;
182
+ }
183
+
184
+ /**
185
+ * Flash a ring around a checkbox/radio, set its ``checked`` state, and fire
186
+ * `input` + `change` so frameworks observe the flip.
187
+ */
188
+ export async function toggleControl(
189
+ el: HTMLInputElement,
190
+ checked: boolean,
191
+ options: ToggleOptions = {},
192
+ ): Promise<void> {
193
+ const flashMs = options.flashMs ?? 200;
194
+ const previousShadow = el.style.boxShadow;
195
+ el.style.boxShadow = RING;
196
+ await motionDelay(flashMs);
197
+ setNativeChecked(el, checked);
198
+ el.dispatchEvent(new Event("input", { bubbles: true }));
199
+ el.dispatchEvent(new Event("change", { bubbles: true }));
200
+ el.style.boxShadow = previousShadow;
201
+ }
@@ -2,11 +2,18 @@ import {
2
2
  focusWithFlash,
3
3
  type HighlightClickOptions,
4
4
  highlightThenClick,
5
+ type PressOptions,
6
+ pressThenClick,
7
+ type SelectOptions,
5
8
  scrollIntoCenterView,
9
+ selectOption,
6
10
  type TextLikeElement,
11
+ type ToggleOptions,
7
12
  type TypeOptions,
13
+ toggleControl,
8
14
  typeInto,
9
15
  } from "./animations.js";
16
+ import { setNativeChecked, setNativeValue } from "./native_setter.js";
10
17
 
11
18
  /**
12
19
  * Generic, framework-free DOM-driving primitives.
@@ -42,6 +49,32 @@ export async function clickElement(
42
49
  await highlightThenClick(el, options);
43
50
  }
44
51
 
52
+ /** Scroll to a button/control and click it with a visible "press" animation. */
53
+ export async function pressButton(el: HTMLElement, options: PressOptions = {}): Promise<void> {
54
+ scrollIntoCenterView(el);
55
+ await pressThenClick(el, options);
56
+ }
57
+
58
+ /** Scroll to a `<select>`, highlight it, and pick the matching option (animated). */
59
+ export async function selectControl(
60
+ el: HTMLSelectElement,
61
+ value: string,
62
+ options: SelectOptions = {},
63
+ ): Promise<void> {
64
+ scrollIntoCenterView(el);
65
+ await selectOption(el, value, options);
66
+ }
67
+
68
+ /** Scroll to a checkbox/radio, flash it, and set its checked state (animated). */
69
+ export async function toggleCheckbox(
70
+ el: HTMLInputElement,
71
+ checked: boolean,
72
+ options: ToggleOptions = {},
73
+ ): Promise<void> {
74
+ scrollIntoCenterView(el);
75
+ await toggleControl(el, checked, options);
76
+ }
77
+
45
78
  /**
46
79
  * Set a `<select>` or checkbox value without typing animation, dispatching the
47
80
  * ``input`` and ``change`` events frameworks listen for.
@@ -51,9 +84,9 @@ export function setControlValue(
51
84
  value: string | boolean,
52
85
  ): void {
53
86
  if (el instanceof HTMLInputElement && el.type === "checkbox") {
54
- el.checked = Boolean(value);
87
+ setNativeChecked(el, Boolean(value));
55
88
  } else {
56
- el.value = String(value);
89
+ setNativeValue(el, String(value));
57
90
  }
58
91
  el.dispatchEvent(new Event("input", { bubbles: true }));
59
92
  el.dispatchEvent(new Event("change", { bubbles: true }));
@@ -0,0 +1,42 @@
1
+ // Set a control's `value` / `checked` through the **native prototype setter**,
2
+ // so framework value-tracking sees the change. React (and Vue/Svelte) replace
3
+ // the element's `value`/`checked` setter with a tracked one; assigning
4
+ // `el.value = x` directly hits that override and the subsequent `input` event
5
+ // carries no change, so the field looks filled but the framework's state stays
6
+ // empty. Calling the original prototype setter, then dispatching `input`, makes
7
+ // `fillField` / `typeInto` / `setControlValue` work on framework-controlled
8
+ // inputs out of the box.
9
+
10
+ type ValueSetter = (this: HTMLElement, value: string) => void;
11
+ type CheckedSetter = (this: HTMLElement, checked: boolean) => void;
12
+
13
+ /** The original prototype setter for ``prop`` on ``proto``. */
14
+ function prototypeSetter(proto: object, prop: string): (this: HTMLElement, value: never) => void {
15
+ // Captured once at module load; DOM environments always define these.
16
+ const setter = (Object.getOwnPropertyDescriptor(proto, prop) as PropertyDescriptor).set;
17
+ return setter as unknown as (this: HTMLElement, value: never) => void;
18
+ }
19
+
20
+ const setInputValue = prototypeSetter(HTMLInputElement.prototype, "value") as ValueSetter;
21
+ const setTextareaValue = prototypeSetter(HTMLTextAreaElement.prototype, "value") as ValueSetter;
22
+ const setSelectValue = prototypeSetter(HTMLSelectElement.prototype, "value") as ValueSetter;
23
+ const setInputChecked = prototypeSetter(HTMLInputElement.prototype, "checked") as CheckedSetter;
24
+
25
+ /** Set ``el.value`` via the element's native prototype setter. */
26
+ export function setNativeValue(
27
+ el: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement,
28
+ value: string,
29
+ ): void {
30
+ if (el instanceof HTMLTextAreaElement) {
31
+ setTextareaValue.call(el, value);
32
+ } else if (el instanceof HTMLSelectElement) {
33
+ setSelectValue.call(el, value);
34
+ } else {
35
+ setInputValue.call(el, value);
36
+ }
37
+ }
38
+
39
+ /** Set ``el.checked`` via the native prototype setter. */
40
+ export function setNativeChecked(el: HTMLInputElement, checked: boolean): void {
41
+ setInputChecked.call(el, checked);
42
+ }
package/src/index.ts CHANGED
@@ -1,60 +1,89 @@
1
1
  // Public surface re-exports. Per CLAUDE.md, this is the only re-export point.
2
- export { AgUiChat, type MessageRole, type SubmitDetail } from "./ag_ui_chat.js";
3
- export {
4
- AgUiClient,
5
- type AgUiClientConfig,
6
- type AgUiClientHandlers,
7
- type AgUiRunInputs,
8
- type AgUiToolCall,
9
- type ExecuteTool,
10
- type ToolExecution,
11
- } from "./agui_client.js";
12
- export {
13
- type FlashOptions,
14
- focusWithFlash,
15
- type HighlightClickOptions,
16
- highlightThenClick,
17
- scrollIntoCenterView,
18
- type TextLikeElement,
19
- type TypeOptions,
20
- typeInto,
21
- } from "./animations.js";
22
- export { type ClientTool, ClientToolRegistry } from "./client_tool_registry.js";
23
- export { type ConfirmationRequest, requestConfirmation } from "./confirmation_modal.js";
2
+
24
3
  export {
25
4
  ELEMENT_TAG,
26
5
  MAX_TOOL_ROUNDS,
27
6
  MESSAGE_ROLE,
28
7
  SUBMIT_EVENT,
8
+ TOGGLE_EVENT,
29
9
  TOOL_CALL_STATUS,
10
+ TOOL_DISPLAY,
11
+ X_CONFIRM_KEY,
30
12
  X_DESTRUCTIVE_KEY,
31
13
  X_NAVIGATES_KEY,
14
+ X_SUMMARY_KEY,
32
15
  } from "./constants.js";
16
+ export {
17
+ AgUiChat,
18
+ type MessageRole,
19
+ type SubmitDetail,
20
+ type ToggleDetail,
21
+ } from "./core/ag_ui_chat.js";
22
+ export {
23
+ AgUiClient,
24
+ type AgUiClientConfig,
25
+ type AgUiClientHandlers,
26
+ type AgUiRunInputs,
27
+ type AgUiToolCall,
28
+ type ExecuteTool,
29
+ type ToolExecution,
30
+ } from "./core/agui_client.js";
33
31
  export {
34
32
  type ClientConversationStore,
35
33
  type NavigationCheckpoint,
36
34
  SessionStorageStore,
37
- } from "./conversation_store.js";
35
+ } from "./core/conversation_store.js";
38
36
  export {
39
37
  type AgentFactory,
40
38
  createHttpAgent,
41
39
  type HttpAgentOptions,
42
- } from "./create_http_agent.js";
43
- export { defineAgUiChat } from "./define_ag_ui_chat.js";
40
+ } from "./core/create_http_agent.js";
41
+ export { defineAgUiChat } from "./core/define_ag_ui_chat.js";
42
+ export {
43
+ type FlashOptions,
44
+ focusWithFlash,
45
+ type HighlightClickOptions,
46
+ highlightThenClick,
47
+ type PressOptions,
48
+ prefersReducedMotion,
49
+ pressThenClick,
50
+ type SelectOptions,
51
+ scrollIntoCenterView,
52
+ selectOption,
53
+ type TextLikeElement,
54
+ type ToggleOptions,
55
+ type TypeOptions,
56
+ toggleControl,
57
+ typeInto,
58
+ } from "./dom/animations.js";
44
59
  export {
45
60
  clickElement,
46
61
  type FillFieldOptions,
47
62
  fillField,
63
+ pressButton,
64
+ selectControl,
48
65
  setControlValue,
49
- } from "./dom_driver.js";
50
- export { isDestructive } from "./is_destructive.js";
51
- export { isNavigates } from "./is_navigates.js";
52
- export { createPageMapContext, type PageMap } from "./page_map.js";
53
- export { createRouteTools, type Route, type RouteMap } from "./route_map.js";
54
- export { createStateHookTools, type StateHook } from "./state_hook.js";
66
+ toggleCheckbox,
67
+ } from "./dom/dom_driver.js";
68
+ export { setNativeChecked, setNativeValue } from "./dom/native_setter.js";
69
+ export type { Skill } from "./skills/skill.js";
70
+ export { type ClientTool, ClientToolRegistry } from "./tools/client_tool_registry.js";
71
+ export { isDestructive } from "./tools/is_destructive.js";
72
+ export { isNavigates } from "./tools/is_navigates.js";
73
+ export { createPageMapContext, type PageMap } from "./tools/page_map.js";
74
+ export {
75
+ createRouteTools,
76
+ type Route,
77
+ type RouteMap,
78
+ type RouteWithParams,
79
+ } from "./tools/route_map.js";
80
+ export { createStateHookTools, type StateHook } from "./tools/state_hook.js";
81
+ export { type ConfirmationRequest, requestConfirmation } from "./ui/confirmation_card.js";
82
+ export { renderMarkdown } from "./ui/render_markdown.js";
55
83
  export {
56
84
  type SettledStatus,
57
85
  ToolCallCard,
58
86
  type ToolCallStatus,
59
- } from "./tool_call_card.js";
87
+ type ToolDisplayMode,
88
+ } from "./ui/tool_call_card.js";
60
89
  export { VERSION } from "./version.js";
@@ -0,0 +1,32 @@
1
+ /** Result of {@link fillTemplate}: the filled text plus any unresolved keys. */
2
+ export interface TemplateResult {
3
+ /** The prompt with resolved `{placeholder}`s substituted. */
4
+ readonly text: string;
5
+ /** Placeholder names with no value available (deduped, in first-seen order). */
6
+ readonly missing: readonly string[];
7
+ }
8
+
9
+ const PLACEHOLDER_RE = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
10
+
11
+ /**
12
+ * Substitute `{name}` placeholders in ``template`` from ``values``.
13
+ *
14
+ * A placeholder whose value is missing, ``null``, or an empty string is left
15
+ * verbatim and reported in ``missing`` — the caller blocks send and tells the
16
+ * user what's needed (e.g. "select rows first") rather than sending a
17
+ * half-filled prompt.
18
+ */
19
+ export function fillTemplate(template: string, values: Record<string, unknown>): TemplateResult {
20
+ const missing: string[] = [];
21
+ const text = template.replace(PLACEHOLDER_RE, (token, key: string) => {
22
+ const value = values[key];
23
+ if (value === undefined || value === null || value === "") {
24
+ if (!missing.includes(key)) {
25
+ missing.push(key);
26
+ }
27
+ return token;
28
+ }
29
+ return String(value);
30
+ });
31
+ return { text, missing };
32
+ }
@@ -0,0 +1,26 @@
1
+ import type { Skill } from "./skill.js";
2
+
3
+ /** Whether ``value`` has the required string fields of a {@link Skill}. */
4
+ function isSkill(value: unknown): value is Skill {
5
+ if (typeof value !== "object" || value === null) {
6
+ return false;
7
+ }
8
+ const record = value as Record<string, unknown>;
9
+ return (
10
+ typeof record["name"] === "string" &&
11
+ typeof record["title"] === "string" &&
12
+ typeof record["prompt"] === "string"
13
+ );
14
+ }
15
+
16
+ /**
17
+ * Parse an untrusted value (parsed JSON from `data-skills` or a skills
18
+ * endpoint) into a list of {@link Skill}s, dropping anything malformed. Never
19
+ * throws — a bad payload yields an empty list.
20
+ */
21
+ export function parseSkills(value: unknown): Skill[] {
22
+ if (!Array.isArray(value)) {
23
+ return [];
24
+ }
25
+ return value.filter(isSkill);
26
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A pre-defined prompt the user can launch from the chat — surfaced as a chip
3
+ * and/or in the `/`-command palette. One catalog feeds both surfaces; the host
4
+ * provides skills via the `skills` property / `data-skills` attribute / a
5
+ * fetched endpoint (see {@link AgUiChat}).
6
+ */
7
+ export interface Skill {
8
+ /** Stable id; the `/token` (kebab-case) in the palette. */
9
+ readonly name: string;
10
+ /** Label shown in chips and the palette. */
11
+ readonly title: string;
12
+ /** Secondary line shown in the palette. */
13
+ readonly description?: string;
14
+ /**
15
+ * The prompt inserted (or sent). May contain `{placeholder}`s filled from the
16
+ * host's skill context before send; an unfilled placeholder blocks send.
17
+ */
18
+ readonly prompt: string;
19
+ /** Send immediately on pick instead of pre-filling the input (default false). */
20
+ readonly sendImmediately?: boolean;
21
+ /** Also surface this skill as a chip (default false; the palette shows all). */
22
+ readonly chip?: boolean;
23
+ }
@@ -24,11 +24,13 @@ export interface ClientTool {
24
24
  export class ClientToolRegistry {
25
25
  readonly #tools = new Map<string, ClientTool>();
26
26
 
27
- /** Register a tool. Throws if the name is already taken. */
27
+ /**
28
+ * Register a tool, replacing any existing one with the same name.
29
+ *
30
+ * Idempotent on the name so re-fired host refs / React StrictMode's
31
+ * double-invoke don't throw — re-registering is a replace, not an error.
32
+ */
28
33
  register(tool: ClientTool): void {
29
- if (this.#tools.has(tool.name)) {
30
- throw new Error(`tool "${tool.name}" already registered`);
31
- }
32
34
  this.#tools.set(tool.name, tool);
33
35
  }
34
36
 
@@ -1,4 +1,4 @@
1
- import { X_DESTRUCTIVE_KEY } from "./constants.js";
1
+ import { X_DESTRUCTIVE_KEY } from "../constants.js";
2
2
 
3
3
  /**
4
4
  * Whether a tool's JSON-Schema `parameters` marks it destructive.
@@ -1,4 +1,4 @@
1
- import { X_NAVIGATES_KEY } from "./constants.js";
1
+ import { X_NAVIGATES_KEY } from "../constants.js";
2
2
 
3
3
  /**
4
4
  * Whether a tool's JSON-Schema `parameters` marks it as navigating.