@artooi/ag-ui-web-component 0.23.0 → 0.24.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.
- package/CHANGELOG.md +149 -36
- package/README.md +140 -9
- package/dist/ag-ui-web-component.bundle.js +113 -122
- package/dist/ag-ui-web-component.bundle.js.map +3 -3
- package/dist/constants.d.ts +71 -85
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +105 -127
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +24 -30
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/core/attachment.d.ts +9 -14
- package/dist/core/attachment.d.ts.map +1 -1
- package/dist/core/conversation_store.d.ts +38 -27
- package/dist/core/conversation_store.d.ts.map +1 -1
- package/dist/core/create_http_agent.d.ts +13 -15
- package/dist/core/create_http_agent.d.ts.map +1 -1
- package/dist/core/remote_conversation_store.d.ts +10 -9
- package/dist/core/remote_conversation_store.d.ts.map +1 -1
- package/dist/core/run_index.d.ts +11 -20
- package/dist/core/run_index.d.ts.map +1 -1
- package/dist/core/transcribe_audio.d.ts +8 -8
- package/dist/core/transcribe_audio.d.ts.map +1 -1
- package/dist/core/upload_attachment.d.ts +15 -18
- package/dist/core/upload_attachment.d.ts.map +1 -1
- package/dist/core/utils.d.ts +4 -6
- package/dist/core/utils.d.ts.map +1 -1
- package/dist/dom/animations.d.ts +22 -30
- package/dist/dom/animations.d.ts.map +1 -1
- package/dist/dom/dom_driver.d.ts +7 -7
- package/dist/dom/native_setter.d.ts +2 -2
- package/dist/dom/native_setter.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +357 -383
- package/dist/index.js.map +2 -2
- package/dist/skills/fill_template.d.ts +4 -5
- package/dist/skills/fill_template.d.ts.map +1 -1
- package/dist/skills/parse_skills.d.ts.map +1 -1
- package/dist/skills/skill.d.ts +7 -8
- package/dist/skills/skill.d.ts.map +1 -1
- package/dist/tools/client_tool_registry.d.ts +2 -2
- package/dist/tools/page_action_tools.d.ts +7 -10
- package/dist/tools/page_action_tools.d.ts.map +1 -1
- package/dist/tools/page_state.d.ts +5 -8
- package/dist/tools/page_state.d.ts.map +1 -1
- package/dist/tools/route_map.d.ts +7 -10
- package/dist/tools/route_map.d.ts.map +1 -1
- package/dist/ui/approval_card.d.ts +15 -20
- package/dist/ui/approval_card.d.ts.map +1 -1
- package/dist/ui/attach_copy_buttons.d.ts +4 -10
- package/dist/ui/attach_copy_buttons.d.ts.map +1 -1
- package/dist/ui/attachment_chips.d.ts +7 -10
- package/dist/ui/attachment_chips.d.ts.map +1 -1
- package/dist/ui/attachment_tray.d.ts +6 -6
- package/dist/ui/checkpoint_menu.d.ts +7 -8
- package/dist/ui/checkpoint_menu.d.ts.map +1 -1
- package/dist/ui/confirmation_card.d.ts +10 -15
- package/dist/ui/confirmation_card.d.ts.map +1 -1
- package/dist/ui/question_card.d.ts +12 -15
- package/dist/ui/question_card.d.ts.map +1 -1
- package/dist/ui/relative_time.d.ts +5 -7
- package/dist/ui/relative_time.d.ts.map +1 -1
- package/dist/ui/render_markdown.d.ts +8 -8
- package/dist/ui/render_markdown.d.ts.map +1 -1
- package/dist/ui/resize_handle.d.ts +21 -34
- package/dist/ui/resize_handle.d.ts.map +1 -1
- package/dist/ui/run_notice.d.ts +5 -7
- package/dist/ui/run_notice.d.ts.map +1 -1
- package/dist/ui/skills_menu.d.ts +4 -5
- package/dist/ui/skills_menu.d.ts.map +1 -1
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/thoughts_block.d.ts +9 -11
- package/dist/ui/thoughts_block.d.ts.map +1 -1
- package/dist/ui/thread_drawer.d.ts +6 -5
- package/dist/ui/thread_drawer.d.ts.map +1 -1
- package/dist/ui/tool_call_card.d.ts +17 -25
- package/dist/ui/tool_call_card.d.ts.map +1 -1
- package/dist/ui/ui_strings.d.ts +6 -12
- package/dist/ui/ui_strings.d.ts.map +1 -1
- package/dist/ui/voice_input.d.ts +10 -11
- package/dist/ui/voice_input.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +74 -87
- package/src/core/ag_ui_chat.ts +313 -269
- package/src/core/agui_client.ts +60 -71
- package/src/core/attachment.ts +9 -14
- package/src/core/conversation_store.ts +55 -33
- package/src/core/create_http_agent.ts +18 -22
- package/src/core/remote_conversation_store.ts +28 -15
- package/src/core/run_index.ts +14 -23
- package/src/core/transcribe_audio.ts +9 -10
- package/src/core/upload_attachment.ts +18 -21
- package/src/core/utils.ts +4 -6
- package/src/dom/animations.ts +33 -43
- package/src/dom/dom_driver.ts +7 -7
- package/src/dom/native_setter.ts +11 -12
- package/src/index.ts +3 -0
- package/src/skills/fill_template.ts +4 -5
- package/src/skills/parse_skills.ts +3 -4
- package/src/skills/skill.ts +7 -8
- package/src/tools/client_tool_registry.ts +2 -2
- package/src/tools/page_action_tools.ts +12 -15
- package/src/tools/page_state.ts +5 -8
- package/src/tools/route_map.ts +15 -19
- package/src/ui/approval_card.ts +15 -20
- package/src/ui/attach_copy_buttons.ts +9 -18
- package/src/ui/attachment_chips.ts +7 -10
- package/src/ui/attachment_tray.ts +6 -6
- package/src/ui/checkpoint_menu.ts +7 -8
- package/src/ui/confirmation_card.ts +10 -15
- package/src/ui/question_card.ts +12 -15
- package/src/ui/relative_time.ts +5 -7
- package/src/ui/render_markdown.ts +25 -51
- package/src/ui/resize_handle.ts +25 -38
- package/src/ui/run_notice.ts +9 -12
- package/src/ui/skills_menu.ts +4 -5
- package/src/ui/styles.ts +92 -101
- package/src/ui/thoughts_block.ts +11 -13
- package/src/ui/thread_drawer.ts +6 -5
- package/src/ui/tool_call_card.ts +22 -32
- package/src/ui/ui_strings.ts +6 -12
- package/src/ui/voice_input.ts +10 -11
- package/src/version.ts +1 -1
package/src/core/ag_ui_chat.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
LOAD_CAPABILITY_TOOL,
|
|
11
11
|
MESSAGE_ROLE,
|
|
12
12
|
READ_PAGE_TOOL,
|
|
13
|
+
RUN_FINISHED_EVENT,
|
|
13
14
|
STATE_EVENT,
|
|
14
15
|
SUBMIT_EVENT,
|
|
15
16
|
TOGGLE_EVENT,
|
|
@@ -105,6 +106,23 @@ export interface StateDetail {
|
|
|
105
106
|
readonly state: Readonly<Record<string, unknown>>;
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
/** One tool that ran during an interaction, as {@link RunFinishedDetail} lists it. */
|
|
110
|
+
export interface ToolRun {
|
|
111
|
+
readonly name: string;
|
|
112
|
+
/**
|
|
113
|
+
* Where it executed. `"server"` is the one a data-rendering host cares about:
|
|
114
|
+
* a `"client"` tool ran in the host's own handler, so the host already knows
|
|
115
|
+
* whatever it did.
|
|
116
|
+
*/
|
|
117
|
+
readonly side: "server" | "client";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** `detail` shape of the {@link RUN_FINISHED_EVENT} CustomEvent. */
|
|
121
|
+
export interface RunFinishedDetail {
|
|
122
|
+
/** In settle order. Empty when the interaction called no tools. */
|
|
123
|
+
readonly tools: readonly ToolRun[];
|
|
124
|
+
}
|
|
125
|
+
|
|
108
126
|
/** `detail` shape of the {@link TOGGLE_EVENT} CustomEvent. */
|
|
109
127
|
export interface ToggleDetail {
|
|
110
128
|
readonly collapsed: boolean;
|
|
@@ -119,14 +137,13 @@ export interface UnreadDetail {
|
|
|
119
137
|
* Attributes read once while connecting, to decide what chrome exists at all.
|
|
120
138
|
*
|
|
121
139
|
* Changing one afterwards is silently ignored: the tray, the mic, the skills
|
|
122
|
-
* menu and the header icon are built
|
|
123
|
-
*
|
|
124
|
-
* `attributeChangedCallback`.
|
|
140
|
+
* menu and the header icon are built during connect and no later read revisits
|
|
141
|
+
* the decision. Observed only so `attributeChangedCallback` can warn.
|
|
125
142
|
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* `data-
|
|
129
|
-
*
|
|
143
|
+
* Excludes the attributes that are re-read per use, where a late change works
|
|
144
|
+
* and a warning would be wrong: `data-runs-url`, `data-page-actions`,
|
|
145
|
+
* `data-text-animation`, `data-tool-display`, `endpoint`, and the CSS-reactive
|
|
146
|
+
* `theme` / `collapsed`.
|
|
130
147
|
*/
|
|
131
148
|
const CONNECT_TIME_ATTRIBUTES = [
|
|
132
149
|
"data-attachments-url",
|
|
@@ -169,43 +186,36 @@ const THEME_KEY = "ag-ui-chat:theme";
|
|
|
169
186
|
* `<ag-ui-chat>` — a framework-free chat sidebar Web Component over AG-UI.
|
|
170
187
|
*
|
|
171
188
|
* Owns the Shadow DOM shell (header, scrolling message list, input row),
|
|
172
|
-
* builds an {@link AgUiClient} on first send
|
|
173
|
-
* {@link agentFactory}
|
|
189
|
+
* builds an {@link AgUiClient} on first send via the overridable
|
|
190
|
+
* {@link agentFactory}, and renders streaming assistant text plus tool-call
|
|
174
191
|
* activity. Emits a {@link SUBMIT_EVENT} for host visibility.
|
|
175
192
|
*
|
|
176
|
-
* The per-run frontend tool catalog and context
|
|
177
|
-
* {@link
|
|
178
|
-
* registry, DOM driver) populate.
|
|
193
|
+
* The per-run frontend tool catalog and context come from {@link getTools} and
|
|
194
|
+
* {@link getContext}.
|
|
179
195
|
*/
|
|
180
196
|
export class AgUiChat extends HTMLElement {
|
|
181
197
|
/** Agent factory; override to inject a custom or fake agent (tests). */
|
|
182
198
|
agentFactory: AgentFactory = createHttpAgent;
|
|
183
199
|
|
|
184
200
|
/**
|
|
185
|
-
* Static extra HTTP headers, sent with
|
|
186
|
-
*
|
|
187
|
-
*
|
|
201
|
+
* Static extra HTTP headers, sent with every request this element makes: the
|
|
202
|
+
* agent run, the thread index and its messages, the tool and skill catalogs,
|
|
203
|
+
* the run index, uploads and transcription.
|
|
188
204
|
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
* re-assignment updates it, so a token captured here is pinned until the host
|
|
193
|
-
* remembers to assign again.
|
|
205
|
+
* For values fixed for the element's lifetime. A rotating credential belongs
|
|
206
|
+
* in {@link getHeaders} instead — only a re-assignment updates this, so a
|
|
207
|
+
* token captured here is pinned until the host assigns again.
|
|
194
208
|
*/
|
|
195
209
|
headers: Record<string, string> = {};
|
|
196
210
|
|
|
197
211
|
/**
|
|
198
|
-
* Live header source,
|
|
199
|
-
* supply rotating credentials
|
|
212
|
+
* Live header source, called afresh immediately before every request — the
|
|
213
|
+
* way to supply rotating credentials, with nothing to re-assign or keep in
|
|
214
|
+
* sync.
|
|
200
215
|
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* Composes with {@link headers} rather than replacing it: the two are merged
|
|
206
|
-
* per key with `getHeaders()` winning, so a static `X-Client` and a rotating
|
|
207
|
-
* `Authorization` can be configured independently and neither silently drops
|
|
208
|
-
* the other.
|
|
216
|
+
* Composes with {@link headers} rather than replacing it: merged per key with
|
|
217
|
+
* `getHeaders()` winning, so a static `X-Client` and a rotating
|
|
218
|
+
* `Authorization` are configured independently and neither drops the other.
|
|
209
219
|
*/
|
|
210
220
|
getHeaders: (() => Record<string, string>) | null = null;
|
|
211
221
|
|
|
@@ -222,40 +232,34 @@ export class AgUiChat extends HTMLElement {
|
|
|
222
232
|
|
|
223
233
|
/**
|
|
224
234
|
* When true, the built-in `ask_user` frontend tool is offered to the agent:
|
|
225
|
-
* calling it renders an inline question card
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
* change the advertised catalog until a host asks for it.
|
|
235
|
+
* calling it renders an inline question card and returns the user's answer.
|
|
236
|
+
* Off by default, like the other built-in tool groups, so the advertised
|
|
237
|
+
* catalog does not change until a host asks for it.
|
|
229
238
|
*/
|
|
230
239
|
askUser = false;
|
|
231
240
|
|
|
232
241
|
/**
|
|
233
|
-
* Optional full replacement for the `ask_user` question UI
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
* resolves with the answer. Unset (default) uses the built-in
|
|
237
|
-
* {@link requestQuestion} card — style that via the `strings` override and the
|
|
238
|
-
* `question*` CSS `::part()`s. Requires {@link askUser} to be enabled.
|
|
242
|
+
* Optional full replacement for the `ask_user` question UI, resolving with
|
|
243
|
+
* the answer; the same seam as {@link approvalRenderer}, styled via `strings`
|
|
244
|
+
* and the `question*` `::part()`s when left unset. Requires {@link askUser}.
|
|
239
245
|
*/
|
|
240
246
|
askUserRenderer: QuestionRenderer | null = null;
|
|
241
247
|
|
|
242
248
|
/**
|
|
243
|
-
* Optional full replacement for the server-side-tool approval UI
|
|
244
|
-
*
|
|
245
|
-
* card
|
|
246
|
-
* `
|
|
247
|
-
*
|
|
248
|
-
* `::part()`s. The gate itself is enabled server-side; this only changes how
|
|
249
|
-
* the decision is collected.
|
|
249
|
+
* Optional full replacement for the server-side-tool approval UI: an approval
|
|
250
|
+
* interrupt invokes this instead of the built-in {@link requestApproval}
|
|
251
|
+
* card, resolving `true` to approve or `false` to deny. Style the built-in
|
|
252
|
+
* card via `strings` and the `approval*` `::part()`s instead. The gate itself
|
|
253
|
+
* is enabled server-side; this only changes how the decision is collected.
|
|
250
254
|
*/
|
|
251
255
|
approvalRenderer: ApprovalRenderer | null = null;
|
|
252
256
|
|
|
253
257
|
/**
|
|
254
|
-
* Optional per-call confirmation predicate. When set
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
* static `x-destructive` flag
|
|
258
|
-
*
|
|
258
|
+
* Optional per-call confirmation predicate. When set it is authoritative,
|
|
259
|
+
* deciding from the tool name and args whether this particular call needs
|
|
260
|
+
* confirmation — so one tool can be instant for some args and confirmed for
|
|
261
|
+
* others, which a static `x-destructive` flag cannot express. When unset the
|
|
262
|
+
* `x-destructive` flag decides. `autoConfirm` short-circuits both.
|
|
259
263
|
*/
|
|
260
264
|
confirmPredicate:
|
|
261
265
|
| ((toolName: string, args: Record<string, unknown>) => boolean | Promise<boolean>)
|
|
@@ -280,11 +284,8 @@ export class AgUiChat extends HTMLElement {
|
|
|
280
284
|
* Per-run context provider. Defaults to the compact page map, when a
|
|
281
285
|
* {@link getPageMap} provider is set and {@link autoInjectPageMap} is on.
|
|
282
286
|
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
* the client's copy only duplicated it, on exactly the turn a file was
|
|
286
|
-
* attached. Attachments still reach the agent; they reach it through the
|
|
287
|
-
* message, which is where they were already.
|
|
287
|
+
* Attachments are deliberately not restated here: the server derives its own
|
|
288
|
+
* manifest from the refs riding the messages.
|
|
288
289
|
*/
|
|
289
290
|
getContext: () => Context[] = () => [
|
|
290
291
|
...createPageMapContext(this.getPageMap, this.autoInjectPageMap),
|
|
@@ -318,22 +319,18 @@ export class AgUiChat extends HTMLElement {
|
|
|
318
319
|
|
|
319
320
|
/**
|
|
320
321
|
* How attached files are uploaded. `null` (default) uses the built-in
|
|
321
|
-
* multipart `POST` to `data-attachments-url
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
* wire (refs are transport-agnostic). When set, the 📎 affordance appears even
|
|
326
|
-
* with no `data-attachments-url`; the handler owns its own endpoint + headers.
|
|
322
|
+
* multipart `POST` to `data-attachments-url`; a custom {@link UploadHandler}
|
|
323
|
+
* swaps the transport without changing the tray, the chips, or the AG-UI
|
|
324
|
+
* wire. When set, the 📎 affordance appears even with no
|
|
325
|
+
* `data-attachments-url`, and the handler owns its own endpoint and headers.
|
|
327
326
|
*/
|
|
328
327
|
uploadHandler: UploadHandler | null = null;
|
|
329
328
|
|
|
330
329
|
/**
|
|
331
330
|
* How recorded voice clips are transcribed. `null` (default) POSTs the clip
|
|
332
|
-
* to `data-transcribe-url
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
* touching the mic button. When set, the 🎤 affordance appears even with no
|
|
336
|
-
* `data-transcribe-url`.
|
|
331
|
+
* to `data-transcribe-url`; a custom {@link TranscribeHandler} swaps the
|
|
332
|
+
* transport without touching the mic button. When set, the 🎤 affordance
|
|
333
|
+
* appears even with no `data-transcribe-url`.
|
|
337
334
|
*/
|
|
338
335
|
transcribeHandler: TranscribeHandler | null = null;
|
|
339
336
|
|
|
@@ -355,12 +352,10 @@ export class AgUiChat extends HTMLElement {
|
|
|
355
352
|
skillContext: () => Record<string, unknown> = () => ({});
|
|
356
353
|
|
|
357
354
|
/**
|
|
358
|
-
* Friendly display labels for tool-call cards, keyed by tool name
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
* only the tool-call name). Client tools should prefer `x-summary` on their
|
|
363
|
-
* schema; this map is the seam for everything else.
|
|
355
|
+
* Friendly display labels for tool-call cards, keyed by tool name. The
|
|
356
|
+
* fallback when a tool has no `x-summary` in its own schema, which chiefly
|
|
357
|
+
* means server-side tools: AG-UI streams only the tool-call name, so their
|
|
358
|
+
* schema never reaches the browser. Client tools should prefer `x-summary`.
|
|
364
359
|
*/
|
|
365
360
|
toolSummaries: Record<string, string> = {};
|
|
366
361
|
|
|
@@ -398,6 +393,12 @@ export class AgUiChat extends HTMLElement {
|
|
|
398
393
|
* the real output with the generic "executed on the server" fallback.
|
|
399
394
|
*/
|
|
400
395
|
readonly #serverSettled = new Set<string>();
|
|
396
|
+
/**
|
|
397
|
+
* Tool calls made during the current interaction, in the order they started,
|
|
398
|
+
* so {@link RUN_FINISHED_EVENT} can report them once the whole thing settles.
|
|
399
|
+
* Spans tool rounds and an approval interrupt; cleared when the event fires.
|
|
400
|
+
*/
|
|
401
|
+
#runTools: { readonly id: string; readonly name: string }[] = [];
|
|
401
402
|
readonly #root: ShadowRoot;
|
|
402
403
|
readonly #chat: HTMLDivElement;
|
|
403
404
|
readonly #messages: HTMLDivElement;
|
|
@@ -540,13 +541,10 @@ export class AgUiChat extends HTMLElement {
|
|
|
540
541
|
* Continue `runId` as a **new** run, seeded server-side from its snapshot.
|
|
541
542
|
*
|
|
542
543
|
* Uses a short-lived agent pointed at the resume / fork endpoint and seeded
|
|
543
|
-
* with
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
* "only the new turn" can't be got wrong by forgetting to clear it. The
|
|
548
|
-
* fresh `run_id` the endpoints also require comes free — a new agent mints
|
|
549
|
-
* one per run.
|
|
544
|
+
* with no history, because those endpoints supply the prior turns from the
|
|
545
|
+
* snapshot and re-sending them would duplicate. A separate agent makes that
|
|
546
|
+
* structural — the main agent keeps its own history — and mints the fresh
|
|
547
|
+
* `run_id` the endpoints also require.
|
|
550
548
|
*
|
|
551
549
|
* Handlers are the element's own, so the continuation streams into the same
|
|
552
550
|
* transcript the user is looking at.
|
|
@@ -627,12 +625,11 @@ export class AgUiChat extends HTMLElement {
|
|
|
627
625
|
return;
|
|
628
626
|
}
|
|
629
627
|
// Everything else here is read once, in connectedCallback, to build chrome
|
|
630
|
-
// that then exists
|
|
631
|
-
//
|
|
632
|
-
//
|
|
633
|
-
//
|
|
634
|
-
//
|
|
635
|
-
// Observed purely so this can be said out loud.
|
|
628
|
+
// that then exists or does not. A later change is silently ignored and the
|
|
629
|
+
// symptom is an affordance that never appears, which reads as a broken
|
|
630
|
+
// component rather than a mis-timed assignment — the common React/Vue shape,
|
|
631
|
+
// where the element mounts on one render pass and attributes are patched in
|
|
632
|
+
// on the next. Observed purely so this can be said out loud.
|
|
636
633
|
if (previous === value || !this.#connected) {
|
|
637
634
|
return;
|
|
638
635
|
}
|
|
@@ -657,10 +654,8 @@ export class AgUiChat extends HTMLElement {
|
|
|
657
654
|
* when the server streams `STATE_SNAPSHOT` / `STATE_DELTA`. Assigning seeds
|
|
658
655
|
* the next run; reading returns whatever the agent last applied.
|
|
659
656
|
*
|
|
660
|
-
* Listen for {@link STATE_EVENT} to react to server-driven changes.
|
|
661
|
-
*
|
|
662
|
-
* Not to be confused with {@link registerPageState}, which exposes host
|
|
663
|
-
* state to the agent as ordinary *tools*.
|
|
657
|
+
* Listen for {@link STATE_EVENT} to react to server-driven changes. Distinct
|
|
658
|
+
* from {@link registerPageState}, which exposes host state as ordinary tools.
|
|
664
659
|
*/
|
|
665
660
|
get sharedState(): Readonly<Record<string, unknown>> {
|
|
666
661
|
return this.#client?.state ?? this.#sharedState;
|
|
@@ -681,11 +676,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
681
676
|
}
|
|
682
677
|
|
|
683
678
|
/**
|
|
684
|
-
* @deprecated Renamed to {@link registerPageState}
|
|
685
|
-
* AG-UI shared-state sync
|
|
686
|
-
*
|
|
687
|
-
* page state. Behaviour is unchanged; this alias will be removed in a future
|
|
688
|
-
* major.
|
|
679
|
+
* @deprecated Renamed to {@link registerPageState} — the old name read as
|
|
680
|
+
* AG-UI shared-state sync. Behaviour is unchanged; the alias will be removed
|
|
681
|
+
* in a future major.
|
|
689
682
|
*/
|
|
690
683
|
registerStateHook(binding: PageState): void {
|
|
691
684
|
this.registerPageState(binding);
|
|
@@ -762,10 +755,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
762
755
|
/**
|
|
763
756
|
* The built-in `ask_user` frontend tool, or `[]` when {@link askUser} is off.
|
|
764
757
|
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
* tool result — no new protocol, reusing the machinery already in place.
|
|
758
|
+
* The agent calls it, the client executes it locally through the normal
|
|
759
|
+
* frontend-tool path by rendering a {@link requestQuestion} card, and the
|
|
760
|
+
* answer flows back as the tool result. No new protocol.
|
|
769
761
|
*/
|
|
770
762
|
#askUserTool(): ClientTool[] {
|
|
771
763
|
if (!this.askUser) {
|
|
@@ -855,19 +847,18 @@ export class AgUiChat extends HTMLElement {
|
|
|
855
847
|
* `credentials` mode (`"omit"` / `"same-origin"` / `"include"`). Mirrored to
|
|
856
848
|
* the `credentials` attribute, so markup embeds can set it without script.
|
|
857
849
|
*
|
|
858
|
-
* `null` (the default) leaves the browser's
|
|
859
|
-
*
|
|
860
|
-
*
|
|
861
|
-
*
|
|
862
|
-
*
|
|
863
|
-
*
|
|
864
|
-
* `
|
|
865
|
-
* (non-wildcard) `Access-Control-Allow-Origin` on the server.
|
|
850
|
+
* `null` (the default) leaves the browser's `same-origin` default in place,
|
|
851
|
+
* which sends no cookies at all to an endpoint on a different origin — and
|
|
852
|
+
* the request goes out anonymously rather than failing, so the symptom is a
|
|
853
|
+
* 401 from a server that looks correctly configured. A cookie-authenticated
|
|
854
|
+
* cross-origin deployment wants `"include"`, plus
|
|
855
|
+
* `Access-Control-Allow-Credentials: true` and a concrete, non-wildcard
|
|
856
|
+
* `Access-Control-Allow-Origin` on the server.
|
|
866
857
|
*
|
|
867
858
|
* Read per request, so a late assignment applies to everything after it.
|
|
868
|
-
* `"omit"` cannot be honoured by the built-in
|
|
869
|
-
*
|
|
870
|
-
*
|
|
859
|
+
* `"omit"` cannot be honoured by the built-in upload transport, an
|
|
860
|
+
* `XMLHttpRequest` with only a two-state cookie switch; every other endpoint
|
|
861
|
+
* honours all three modes.
|
|
871
862
|
*/
|
|
872
863
|
get credentials(): RequestCredentials | null {
|
|
873
864
|
const attr = this.getAttribute("credentials");
|
|
@@ -1004,25 +995,21 @@ export class AgUiChat extends HTMLElement {
|
|
|
1004
995
|
* The catalog requests the element issues on startup: the tool labels
|
|
1005
996
|
* (`data-tools-url`) and the backend skills (`data-skills-url`).
|
|
1006
997
|
*
|
|
1007
|
-
* Deliberately one microtask behind `connectedCallback`. A
|
|
1008
|
-
*
|
|
1009
|
-
*
|
|
1010
|
-
*
|
|
1011
|
-
*
|
|
1012
|
-
*
|
|
1013
|
-
*
|
|
1014
|
-
*
|
|
998
|
+
* Deliberately one microtask behind `connectedCallback`. A framework ref is
|
|
999
|
+
* attached after the node is inserted but within the same commit, so a
|
|
1000
|
+
* request issued from `connectedCallback` itself goes out before `headers`,
|
|
1001
|
+
* {@link getHeaders} or {@link credentials} exist and 401s in a way that
|
|
1002
|
+
* reads as a server fault. A microtask lands after that commit, still before
|
|
1003
|
+
* paint.
|
|
1004
|
+
*
|
|
1005
|
+
* It is not a fix for configuration arriving later than the commit (a passive
|
|
1006
|
+
* effect, an awaited token fetch): configure before insertion, or call
|
|
1007
|
+
* {@link reload}, since a longer timer would hide that race rather than close
|
|
1008
|
+
* it.
|
|
1015
1009
|
*
|
|
1016
|
-
*
|
|
1017
|
-
*
|
|
1018
|
-
*
|
|
1019
|
-
* {@link reload} once configured, because a longer timer would hide that race
|
|
1020
|
-
* rather than close it. And it deliberately excludes the *history* replay,
|
|
1021
|
-
* which stays in `connectedCallback`: the replay renders into the transcript,
|
|
1022
|
-
* so deferring it lets a `sendMessage()` issued in the same task land first
|
|
1023
|
-
* and the replay then duplicate it. The thread history is therefore the one
|
|
1024
|
-
* request that can still go out before a ref is attached — {@link reload}
|
|
1025
|
-
* covers it.
|
|
1010
|
+
* The history replay stays in `connectedCallback` on purpose. It renders into
|
|
1011
|
+
* the transcript, so deferring it would let a `sendMessage()` in the same
|
|
1012
|
+
* task land first and be duplicated by the replay.
|
|
1026
1013
|
*/
|
|
1027
1014
|
#startup(): void {
|
|
1028
1015
|
// An element can be inserted and removed inside one task (a discarded
|
|
@@ -1040,15 +1027,13 @@ export class AgUiChat extends HTMLElement {
|
|
|
1040
1027
|
* the backend skill catalog and the thread's history — with the transport
|
|
1041
1028
|
* configuration as it stands now.
|
|
1042
1029
|
*
|
|
1043
|
-
*
|
|
1044
|
-
*
|
|
1045
|
-
* startup requests
|
|
1046
|
-
* "try again, properly authenticated" without removing and re-inserting the
|
|
1047
|
-
* node.
|
|
1030
|
+
* For a host that can only configure the element after the fact (a token
|
|
1031
|
+
* fetched in a passive effect, an async auth handshake), this re-issues the
|
|
1032
|
+
* startup requests authenticated, without removing and re-inserting the node.
|
|
1048
1033
|
*
|
|
1049
|
-
* A reload, not a merge
|
|
1050
|
-
*
|
|
1051
|
-
*
|
|
1034
|
+
* A reload, not a merge: the in-flight run is cancelled and the transcript is
|
|
1035
|
+
* rebuilt from persisted history, so anything streamed since is dropped. Call
|
|
1036
|
+
* it once, when configuration lands, not between turns.
|
|
1052
1037
|
*/
|
|
1053
1038
|
async reload(): Promise<void> {
|
|
1054
1039
|
this.#cancelRun();
|
|
@@ -1058,12 +1043,10 @@ export class AgUiChat extends HTMLElement {
|
|
|
1058
1043
|
}
|
|
1059
1044
|
|
|
1060
1045
|
/**
|
|
1061
|
-
* Tear down live resources when the element leaves the DOM
|
|
1062
|
-
*
|
|
1063
|
-
*
|
|
1064
|
-
*
|
|
1065
|
-
* removed `<ag-ui-chat>` leaks a streaming request, uploads, and a live
|
|
1066
|
-
* `MediaRecorder`.
|
|
1046
|
+
* Tear down live resources when the element leaves the DOM: cancel the
|
|
1047
|
+
* in-flight run so its stream closes, abort in-flight uploads so they do not
|
|
1048
|
+
* orphan server-side files, and release the mic so the browser's recording
|
|
1049
|
+
* indicator clears. Without this a removed element leaks all three.
|
|
1067
1050
|
*/
|
|
1068
1051
|
disconnectedCallback(): void {
|
|
1069
1052
|
this.#connected = false;
|
|
@@ -1075,14 +1058,10 @@ export class AgUiChat extends HTMLElement {
|
|
|
1075
1058
|
/**
|
|
1076
1059
|
* Read an opt-in flag attribute the way HTML reads a boolean attribute.
|
|
1077
1060
|
*
|
|
1078
|
-
* Present means on
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
* reaches for first — silently *disabled* the feature it names, with nothing
|
|
1083
|
-
* to indicate why the chips never appeared.
|
|
1084
|
-
*
|
|
1085
|
-
* `="false"` still turns it off, so an explicit opt-out keeps working.
|
|
1061
|
+
* Present means on: bare (`data-prompt-chips`), empty (`=""`), or any value
|
|
1062
|
+
* except the literal `"false"`. Comparing against `"true"` instead would make
|
|
1063
|
+
* the bare spelling every native boolean attribute uses silently disable the
|
|
1064
|
+
* feature it names. `="false"` still turns it off.
|
|
1086
1065
|
*/
|
|
1087
1066
|
#flag(name: string): boolean {
|
|
1088
1067
|
const value = this.getAttribute(name);
|
|
@@ -1335,21 +1314,16 @@ export class AgUiChat extends HTMLElement {
|
|
|
1335
1314
|
/**
|
|
1336
1315
|
* Act on a picked skill.
|
|
1337
1316
|
*
|
|
1338
|
-
* A skill
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1341
|
-
*
|
|
1342
|
-
* then never reaches the browser at all: a skill is often where a project's
|
|
1343
|
-
* internal workflow is written down most plainly, and a catalog endpoint is a
|
|
1344
|
-
* plain GET.
|
|
1317
|
+
* A skill with no `prompt` is server-resolved: picking it sends the bare
|
|
1318
|
+
* `/name` token for the agent to expand, so the wording never reaches the
|
|
1319
|
+
* browser. Prefer that shape — a skill often states a project's internal
|
|
1320
|
+
* workflow most plainly, and a catalog endpoint is a plain GET.
|
|
1345
1321
|
*
|
|
1346
|
-
* A skill
|
|
1347
|
-
*
|
|
1348
|
-
* Right for a user-facing convenience, and for placeholders only the page can
|
|
1349
|
-
* supply.
|
|
1322
|
+
* A skill carrying a `prompt` has the client fill its `{placeholder}`s from
|
|
1323
|
+
* the page instead, which is right for placeholders only the page can supply.
|
|
1350
1324
|
*
|
|
1351
|
-
* Either way a pick
|
|
1352
|
-
*
|
|
1325
|
+
* Either way a pick sends; `sendImmediately: false` opts into pre-filling the
|
|
1326
|
+
* composer instead.
|
|
1353
1327
|
*/
|
|
1354
1328
|
#applySkill(skill: Skill): void {
|
|
1355
1329
|
if (skill.prompt === undefined) {
|
|
@@ -1483,18 +1457,13 @@ export class AgUiChat extends HTMLElement {
|
|
|
1483
1457
|
}
|
|
1484
1458
|
|
|
1485
1459
|
/**
|
|
1486
|
-
* Which edges the layout is holding still, by measuring rather than guessing
|
|
1460
|
+
* Which edges the layout is holding still, by measuring rather than guessing:
|
|
1461
|
+
* nudge the size by a pixel, see which edges stayed put, and undo. One forced
|
|
1462
|
+
* reflow per drag.
|
|
1487
1463
|
*
|
|
1488
|
-
*
|
|
1489
|
-
*
|
|
1490
|
-
*
|
|
1491
|
-
* the page's own CSS puts it — flex-start, flex-end, a grid cell. Mapping
|
|
1492
|
-
* placement to a corner got this wrong for any host that right-aligns the
|
|
1493
|
-
* element, and the symptom is bad enough to read as a broken control: the
|
|
1494
|
-
* panel shrinks when dragged outward, travelling by its opposite corner.
|
|
1495
|
-
*
|
|
1496
|
-
* So: nudge the size by a pixel, see which edges stayed put, and undo. One
|
|
1497
|
-
* forced reflow per drag, which is cheap next to being wrong.
|
|
1464
|
+
* `placement` cannot answer this — an embedded panel goes wherever the page's
|
|
1465
|
+
* CSS puts it — and see {@link createResizeHandle} for why guessing produces
|
|
1466
|
+
* a visibly broken control.
|
|
1498
1467
|
*/
|
|
1499
1468
|
#measureAnchor(): ResizeAnchor {
|
|
1500
1469
|
const before = this.getBoundingClientRect();
|
|
@@ -1533,14 +1502,13 @@ export class AgUiChat extends HTMLElement {
|
|
|
1533
1502
|
/**
|
|
1534
1503
|
* Write a dragged size onto the host, on the axes this placement leaves free.
|
|
1535
1504
|
*
|
|
1536
|
-
*
|
|
1537
|
-
*
|
|
1538
|
-
*
|
|
1539
|
-
*
|
|
1540
|
-
* `100vh`. The cascade cannot arbitrate this
|
|
1541
|
-
*
|
|
1542
|
-
*
|
|
1543
|
-
* persisted size is only ever applied to the ones it does not.
|
|
1505
|
+
* Writing the custom property rather than inline `width` / `height` does not
|
|
1506
|
+
* by itself leave placement in charge: an inline custom property still
|
|
1507
|
+
* outranks a `:host([placement=…])` rule setting the same property, so a
|
|
1508
|
+
* height dragged while floating would cap a docked sidebar asking for
|
|
1509
|
+
* `100vh`. The cascade cannot arbitrate this, so the axis check must — a
|
|
1510
|
+
* placement owns the axes it fixes, and a persisted size is applied only to
|
|
1511
|
+
* the ones it leaves free.
|
|
1544
1512
|
*/
|
|
1545
1513
|
#applySize(size: ResizeSize): void {
|
|
1546
1514
|
const axis = this.#resizeAxis();
|
|
@@ -1622,12 +1590,10 @@ export class AgUiChat extends HTMLElement {
|
|
|
1622
1590
|
* Open the thread-history drawer: the imperative route to the control that
|
|
1623
1591
|
* renders as `::part(history-button)`.
|
|
1624
1592
|
*
|
|
1625
|
-
* A host that hides `::part(header)`
|
|
1626
|
-
*
|
|
1627
|
-
*
|
|
1628
|
-
*
|
|
1629
|
-
* {@link openCheckpoints}, {@link newChat}, {@link toggleCollapsed} and
|
|
1630
|
-
* {@link toggleTheme}.
|
|
1593
|
+
* A host that hides `::part(header)` for its own title bar hides the history,
|
|
1594
|
+
* new-chat and collapse buttons with it. Each has a method so that chrome can
|
|
1595
|
+
* be rebuilt: this one, {@link openCheckpoints}, {@link newChat},
|
|
1596
|
+
* {@link toggleCollapsed} and {@link toggleTheme}.
|
|
1631
1597
|
*/
|
|
1632
1598
|
openThreads(): void {
|
|
1633
1599
|
void this.#refreshDrawer();
|
|
@@ -1743,23 +1709,17 @@ export class AgUiChat extends HTMLElement {
|
|
|
1743
1709
|
/**
|
|
1744
1710
|
* Notice a previous run that never produced a response.
|
|
1745
1711
|
*
|
|
1746
|
-
* {@link AgUiClient.send} persists the user's message
|
|
1747
|
-
* run, so a transcript
|
|
1748
|
-
*
|
|
1749
|
-
*
|
|
1750
|
-
* of the transcript already says so, which is why this needs no
|
|
1751
|
-
* {@link ClientConversationStore} method and no `pagehide` listener (neither
|
|
1752
|
-
* of which fires on a crash or a force-quit anyway).
|
|
1712
|
+
* {@link AgUiClient.send} persists the user's message before starting the
|
|
1713
|
+
* run, so a transcript ending on that user message means nothing came back.
|
|
1714
|
+
* The transcript's shape alone detects it, needing no store method and no
|
|
1715
|
+
* `pagehide` listener — neither of which fires on a crash or force-quit.
|
|
1753
1716
|
*
|
|
1754
|
-
*
|
|
1755
|
-
* checkpoint and resumes,
|
|
1756
|
-
* reaching here.
|
|
1717
|
+
* An agent-initiated reload is not this case: a navigating tool leaves a
|
|
1718
|
+
* checkpoint and resumes, so the caller returns early on one.
|
|
1757
1719
|
*
|
|
1758
|
-
* Deliberately a notice
|
|
1759
|
-
*
|
|
1760
|
-
*
|
|
1761
|
-
* executed before the interruption would run a second time. Saying plainly
|
|
1762
|
-
* that the answer was lost is the honest option, and the user can re-ask.
|
|
1720
|
+
* Deliberately a notice, never a resume. AG-UI has no resume-an-aborted-run
|
|
1721
|
+
* primitive, and re-sending the accumulated messages is semantically a new
|
|
1722
|
+
* run, so any server-side tool already executed would run a second time.
|
|
1763
1723
|
*/
|
|
1764
1724
|
/**
|
|
1765
1725
|
* Build a round's context, recording which page it describes.
|
|
@@ -1820,22 +1780,23 @@ export class AgUiChat extends HTMLElement {
|
|
|
1820
1780
|
// wrap words.
|
|
1821
1781
|
this.appendMessage(MESSAGE_ROLE.ASSISTANT, text).classList.add("message--restored");
|
|
1822
1782
|
}
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1783
|
+
// Narrowed rather than trusted, for the same reason `messageAttachments`
|
|
1784
|
+
// narrows the neighbouring field: anything that throws in this loop aborts
|
|
1785
|
+
// the replay at this message, and every later turn silently disappears from
|
|
1786
|
+
// the transcript. See `restoredToolCalls`.
|
|
1787
|
+
for (const call of restoredToolCalls(message.toolCalls)) {
|
|
1788
|
+
const restored = {
|
|
1789
|
+
id: call.id,
|
|
1790
|
+
name: call.function.name,
|
|
1791
|
+
args: this.#parseArgs(call.function.arguments),
|
|
1792
|
+
};
|
|
1793
|
+
// Restored history goes through the same interception as the live
|
|
1794
|
+
// stream — otherwise a reload resurrects the raw `load_capability`
|
|
1795
|
+
// card the live path deliberately replaced.
|
|
1796
|
+
if (this.#noticeIfSkillLoad(restored)) {
|
|
1797
|
+
continue;
|
|
1838
1798
|
}
|
|
1799
|
+
this.#cardFor(restored);
|
|
1839
1800
|
}
|
|
1840
1801
|
return;
|
|
1841
1802
|
}
|
|
@@ -1847,8 +1808,14 @@ export class AgUiChat extends HTMLElement {
|
|
|
1847
1808
|
}
|
|
1848
1809
|
}
|
|
1849
1810
|
|
|
1850
|
-
/** Parse a tool call's JSON `arguments`
|
|
1851
|
-
#parseArgs(raw:
|
|
1811
|
+
/** Parse a tool call's JSON `arguments` from history into an object. */
|
|
1812
|
+
#parseArgs(raw: unknown): Record<string, unknown> {
|
|
1813
|
+
if (typeof raw !== "string") {
|
|
1814
|
+
// A restored call whose `arguments` are missing or not a string still has
|
|
1815
|
+
// a name worth showing, so this renders an empty-args card rather than
|
|
1816
|
+
// dropping the card.
|
|
1817
|
+
return {};
|
|
1818
|
+
}
|
|
1852
1819
|
try {
|
|
1853
1820
|
const parsed: unknown = JSON.parse(raw);
|
|
1854
1821
|
if (typeof parsed === "object" && parsed !== null) {
|
|
@@ -1881,13 +1848,12 @@ export class AgUiChat extends HTMLElement {
|
|
|
1881
1848
|
/**
|
|
1882
1849
|
* Append a message bubble and return it.
|
|
1883
1850
|
*
|
|
1884
|
-
* Assistant content
|
|
1885
|
-
*
|
|
1886
|
-
* rendering user-authored markup).
|
|
1851
|
+
* Assistant content renders as sanitised markdown/HTML; user content stays
|
|
1852
|
+
* literal text, which also avoids rendering user-authored markup.
|
|
1887
1853
|
*
|
|
1888
|
-
* Assistant bubbles land in the current answer group, opening one if
|
|
1889
|
-
*
|
|
1890
|
-
*
|
|
1854
|
+
* Assistant bubbles land in the current answer group, opening one if needed;
|
|
1855
|
+
* a user bubble closes the prior group and sits directly in the list, the
|
|
1856
|
+
* well wrapping only the assistant turn.
|
|
1891
1857
|
*/
|
|
1892
1858
|
appendMessage(role: MessageRole, content: string): HTMLDivElement {
|
|
1893
1859
|
const bubble = document.createElement("div");
|
|
@@ -2153,12 +2119,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
2153
2119
|
* Build a header control button: a named slot a host can project markup into,
|
|
2154
2120
|
* with the built-in glyph as the slot's fallback.
|
|
2155
2121
|
*
|
|
2156
|
-
* The
|
|
2157
|
-
*
|
|
2158
|
-
*
|
|
2159
|
-
* supply a brand `<img>` or `<svg>`. This is the same slot-with-fallback
|
|
2160
|
-
* idiom the header icon already uses, so existing embeds render exactly as
|
|
2161
|
-
* before.
|
|
2122
|
+
* The slot is what lets a host replace the mark with its own `<img>` or
|
|
2123
|
+
* `<svg>` rather than only restyle it through the `part`; the same
|
|
2124
|
+
* slot-with-fallback idiom the header icon uses.
|
|
2162
2125
|
*/
|
|
2163
2126
|
#headerButton(modifier: string, label: string, glyph: string): HTMLButtonElement {
|
|
2164
2127
|
const button = document.createElement("button");
|
|
@@ -2400,19 +2363,16 @@ export class AgUiChat extends HTMLElement {
|
|
|
2400
2363
|
* Send a message as if the user had typed it — renders the user bubble,
|
|
2401
2364
|
* dispatches {@link SUBMIT_EVENT}, and starts the run.
|
|
2402
2365
|
*
|
|
2403
|
-
* The programmatic half of the composer, for a host driving its own input
|
|
2404
|
-
*
|
|
2405
|
-
*
|
|
2406
|
-
* here; Send itself now reads the composer, clears it, and calls this.
|
|
2366
|
+
* The programmatic half of the composer, for a host driving its own input.
|
|
2367
|
+
* Everything the built-in Send does happens here; Send reads the composer,
|
|
2368
|
+
* clears it, and calls this.
|
|
2407
2369
|
*
|
|
2408
|
-
* `attachments` are durable {@link AttachmentRef}s —
|
|
2409
|
-
*
|
|
2410
|
-
* {@link ATTACHMENT_EVENT} reports. Pass them to attach files to the message.
|
|
2370
|
+
* `attachments` are durable {@link AttachmentRef}s — what {@link attachFile}
|
|
2371
|
+
* resolves to and what {@link ATTACHMENT_EVENT} reports.
|
|
2411
2372
|
*
|
|
2412
|
-
* No-ops while a run is in flight
|
|
2413
|
-
*
|
|
2414
|
-
*
|
|
2415
|
-
* composer stays in charge of its own state.
|
|
2373
|
+
* No-ops on an empty message, and while a run is in flight, since a second
|
|
2374
|
+
* concurrent run would orphan the first. Unlike the built-in Send it does not
|
|
2375
|
+
* consult the tray: what you pass is what is sent.
|
|
2416
2376
|
*/
|
|
2417
2377
|
async sendMessage(content: string, attachments: readonly AttachmentRef[] = []): Promise<void> {
|
|
2418
2378
|
if (this.#running || (content === "" && attachments.length === 0)) {
|
|
@@ -2437,11 +2397,11 @@ export class AgUiChat extends HTMLElement {
|
|
|
2437
2397
|
* picker and drag-and-drop do — validation, progress chip, and all.
|
|
2438
2398
|
*
|
|
2439
2399
|
* Returns `false` when uploads are not configured (no `data-attachments-url`
|
|
2440
|
-
* and no {@link uploadHandler})
|
|
2441
|
-
*
|
|
2400
|
+
* and no {@link uploadHandler}) — the only signal a host gets, since the tray
|
|
2401
|
+
* does not exist to report anything then.
|
|
2442
2402
|
*
|
|
2443
2403
|
* Uploading is asynchronous: watch {@link ATTACHMENT_EVENT} for the resulting
|
|
2444
|
-
* {@link AttachmentRef}
|
|
2404
|
+
* {@link AttachmentRef} and pass it to {@link sendMessage} once `pending`
|
|
2445
2405
|
* reaches zero.
|
|
2446
2406
|
*/
|
|
2447
2407
|
attachFile(file: File): boolean {
|
|
@@ -2546,17 +2506,15 @@ export class AgUiChat extends HTMLElement {
|
|
|
2546
2506
|
return null;
|
|
2547
2507
|
}
|
|
2548
2508
|
// The page moved under this round. Acting now would target whatever
|
|
2549
|
-
//
|
|
2550
|
-
//
|
|
2551
|
-
//
|
|
2552
|
-
// only way the agent acts on the wrong page without either side noticing.
|
|
2509
|
+
// matches on the new page, and the case worth preventing is a same-named
|
|
2510
|
+
// control matching silently — the only way the agent acts on the wrong page
|
|
2511
|
+
// without either side noticing.
|
|
2553
2512
|
//
|
|
2554
|
-
//
|
|
2555
|
-
// approve an action
|
|
2556
|
-
//
|
|
2557
|
-
//
|
|
2558
|
-
//
|
|
2559
|
-
// the host's tools are not page-scoped in the first place.
|
|
2513
|
+
// Must precede the confirmation prompt, so the user is never asked to
|
|
2514
|
+
// approve an action about to be refused. Navigating tools are exempt, since
|
|
2515
|
+
// moving the page is their job, as is read_page, the documented recovery.
|
|
2516
|
+
// Gated on a page-map provider: without one there is no read_page to
|
|
2517
|
+
// recommend and the host's tools are not page-scoped anyway.
|
|
2560
2518
|
if (
|
|
2561
2519
|
this.getPageMap !== null &&
|
|
2562
2520
|
call.name !== READ_PAGE_TOOL &&
|
|
@@ -2632,12 +2590,11 @@ export class AgUiChat extends HTMLElement {
|
|
|
2632
2590
|
* Render an approval card per server-side-tool interrupt and collect the
|
|
2633
2591
|
* user's decisions (approve → run it, deny → decline it).
|
|
2634
2592
|
*
|
|
2635
|
-
* The run is suspended on these cards
|
|
2593
|
+
* The run is suspended on these cards. A Stop while any is open aborts the
|
|
2636
2594
|
* shared {@link #confirmAbort} controller, resolving every still-open card as
|
|
2637
|
-
* denied
|
|
2638
|
-
*
|
|
2639
|
-
*
|
|
2640
|
-
* result will ever arrive for it.
|
|
2595
|
+
* denied. An approved tool runs on the follow-up resume run and streams its
|
|
2596
|
+
* result into the same pending card; a denied one settles here, since no
|
|
2597
|
+
* result will ever arrive.
|
|
2641
2598
|
*/
|
|
2642
2599
|
async #resolveInterrupts(
|
|
2643
2600
|
interrupts: readonly Interrupt[],
|
|
@@ -2648,8 +2605,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
2648
2605
|
this.#hidePending();
|
|
2649
2606
|
for (const interrupt of interrupts) {
|
|
2650
2607
|
const request: ApprovalRequest = {};
|
|
2651
|
-
|
|
2652
|
-
|
|
2608
|
+
const phrase = confirmPhrase(interrupt) ?? interrupt.message;
|
|
2609
|
+
if (phrase !== undefined) {
|
|
2610
|
+
request.message = phrase;
|
|
2653
2611
|
}
|
|
2654
2612
|
const card =
|
|
2655
2613
|
interrupt.toolCallId !== undefined ? this.#toolCards.get(interrupt.toolCallId) : undefined;
|
|
@@ -2738,6 +2696,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
2738
2696
|
if (this.#noticeIfSkillLoad(call)) {
|
|
2739
2697
|
return;
|
|
2740
2698
|
}
|
|
2699
|
+
// Recorded after the skill-load return: a capability load is the agent
|
|
2700
|
+
// arranging itself, not work a host's data could have moved under.
|
|
2701
|
+
this.#runTools.push({ id: call.id, name: call.name });
|
|
2741
2702
|
this.#cardFor(call);
|
|
2742
2703
|
},
|
|
2743
2704
|
onActivity: (activityType, content) => {
|
|
@@ -2802,10 +2763,35 @@ export class AgUiChat extends HTMLElement {
|
|
|
2802
2763
|
}
|
|
2803
2764
|
this.#currentGroup = null;
|
|
2804
2765
|
this.#thoughts = null;
|
|
2766
|
+
this.#dispatchRunFinished();
|
|
2805
2767
|
},
|
|
2806
2768
|
};
|
|
2807
2769
|
}
|
|
2808
2770
|
|
|
2771
|
+
/**
|
|
2772
|
+
* Tell the host the interaction is over and what ran in it.
|
|
2773
|
+
*
|
|
2774
|
+
* Last thing in `onSettled`, so a listener that refetches sees a transcript
|
|
2775
|
+
* that has already stopped changing. `side` is read from the streamed-result
|
|
2776
|
+
* bookkeeping rather than from the tool list: whether a call executed on the
|
|
2777
|
+
* server is a fact about the run, and a name can appear on both sides across a
|
|
2778
|
+
* conversation.
|
|
2779
|
+
*/
|
|
2780
|
+
#dispatchRunFinished(): void {
|
|
2781
|
+
const tools: ToolRun[] = this.#runTools.map(({ id, name }) => ({
|
|
2782
|
+
name,
|
|
2783
|
+
side: this.#serverSettled.has(id) ? "server" : "client",
|
|
2784
|
+
}));
|
|
2785
|
+
this.#runTools = [];
|
|
2786
|
+
this.dispatchEvent(
|
|
2787
|
+
new CustomEvent<RunFinishedDetail>(RUN_FINISHED_EVENT, {
|
|
2788
|
+
detail: { tools },
|
|
2789
|
+
bubbles: true,
|
|
2790
|
+
composed: true,
|
|
2791
|
+
}),
|
|
2792
|
+
);
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2809
2795
|
/** A muted "⏹ Stopped" line in the transcript (distinct from the ⚠️ error bubble). */
|
|
2810
2796
|
#appendStoppedNote(): void {
|
|
2811
2797
|
const note = document.createElement("div");
|
|
@@ -2933,15 +2919,73 @@ export class AgUiChat extends HTMLElement {
|
|
|
2933
2919
|
}
|
|
2934
2920
|
}
|
|
2935
2921
|
|
|
2922
|
+
/**
|
|
2923
|
+
* A server-authored question for a gated call, read off the interrupt's metadata.
|
|
2924
|
+
*
|
|
2925
|
+
* The question an AG-UI interrupt carries by default is the call itself, spelled
|
|
2926
|
+
* out: `Approve create_event({"title": "Design sync", …})?`. Accurate, and not
|
|
2927
|
+
* something to put in front of a person. A client-side confirmation has
|
|
2928
|
+
* `x-confirm` on the tool's schema for exactly this, so the same key is read here
|
|
2929
|
+
* — whichever end gates a call, the phrase comes from one place, and a server
|
|
2930
|
+
* that supplies none keeps the generated text.
|
|
2931
|
+
*
|
|
2932
|
+
* Narrowed rather than trusted: `metadata` is `Record<string, any>` on the wire,
|
|
2933
|
+
* so anything at all can arrive under that key, and a non-string would render as
|
|
2934
|
+
* "[object Object]" in the one place a person is being asked to allow a write.
|
|
2935
|
+
*/
|
|
2936
|
+
function confirmPhrase(interrupt: Interrupt): string | undefined {
|
|
2937
|
+
const phrase = interrupt.metadata?.[X_CONFIRM_KEY];
|
|
2938
|
+
return typeof phrase === "string" && phrase.trim() !== "" ? phrase : undefined;
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
/** One tool call as a restored assistant message carries it. */
|
|
2942
|
+
interface RestoredToolCall {
|
|
2943
|
+
readonly id: string;
|
|
2944
|
+
readonly function: { readonly name: string; readonly arguments?: unknown };
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
/**
|
|
2948
|
+
* The tool calls a restored assistant turn carries, with anything shapeless dropped.
|
|
2949
|
+
*
|
|
2950
|
+
* Narrowing here rather than trusting the declared type, for three reasons that
|
|
2951
|
+
* point the same way.
|
|
2952
|
+
*
|
|
2953
|
+
* **`null` is a value this field really takes.** `@ag-ui/core` types `toolCalls`
|
|
2954
|
+
* as optional (`z.ZodOptional`), so TypeScript offers only `undefined` — but the
|
|
2955
|
+
* protocol's Python models declare `tool_calls: list[ToolCall] | None`, and a
|
|
2956
|
+
* server dumping them without `exclude_none` sends `null`. The two SDKs disagree
|
|
2957
|
+
* about the wire, and a client cannot afford to take either one's word for it.
|
|
2958
|
+
*
|
|
2959
|
+
* **A throw here costs the rest of the transcript.** This runs inside the replay
|
|
2960
|
+
* of stored history, one message at a time; an exception aborts the whole replay,
|
|
2961
|
+
* so a single bad entry silently truncates the conversation from that point on —
|
|
2962
|
+
* with no error state and nothing on screen to explain the gap.
|
|
2963
|
+
*
|
|
2964
|
+
* **Storage is untrusted anyway** — hand-edited, truncated, written by an older
|
|
2965
|
+
* version, or supplied by a host's own store. `messageAttachments` already takes
|
|
2966
|
+
* exactly this stance for the neighbouring field on the same message.
|
|
2967
|
+
*/
|
|
2968
|
+
function restoredToolCalls(value: unknown): readonly RestoredToolCall[] {
|
|
2969
|
+
return Array.isArray(value) ? value.filter(isRestoredToolCall) : [];
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
/** Whether an unknown history entry has enough shape to render a tool card. */
|
|
2973
|
+
function isRestoredToolCall(value: unknown): value is RestoredToolCall {
|
|
2974
|
+
if (typeof value !== "object" || value === null) {
|
|
2975
|
+
return false;
|
|
2976
|
+
}
|
|
2977
|
+
const call = value as { id?: unknown; function?: { name?: unknown } };
|
|
2978
|
+
return typeof call.id === "string" && typeof call.function?.name === "string";
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2936
2981
|
/**
|
|
2937
2982
|
* The skill name a `load_capability` call activated, or `null` when the call is
|
|
2938
2983
|
* something else.
|
|
2939
2984
|
*
|
|
2940
|
-
* Every deferred capability loads through this one tool, so the id is
|
|
2941
|
-
*
|
|
2942
|
-
*
|
|
2943
|
-
*
|
|
2944
|
-
* what the model selected, which is what the notice reports.
|
|
2985
|
+
* Every deferred capability loads through this one tool, so the id is a skill
|
|
2986
|
+
* name only when the project wired agent skills; another project's capability
|
|
2987
|
+
* id surfaces here too. Acceptable for a muted notice, and better than a
|
|
2988
|
+
* parallel signal — the id is exactly what the model selected.
|
|
2945
2989
|
*/
|
|
2946
2990
|
function skillNameFrom(call: AgUiToolCall): string | null {
|
|
2947
2991
|
if (call.name !== LOAD_CAPABILITY_TOOL) {
|