@artooi/ag-ui-web-component 0.29.0 → 0.30.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 (38) hide show
  1. package/CHANGELOG.md +199 -1
  2. package/README.md +196 -33
  3. package/dist/ag-ui-web-component.bundle.js +211 -28
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +60 -1
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +25 -1
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/core/agui_client.d.ts +18 -1
  10. package/dist/core/agui_client.d.ts.map +1 -1
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +578 -35
  14. package/dist/index.js.map +4 -4
  15. package/dist/ui/message_actions.d.ts +14 -4
  16. package/dist/ui/message_actions.d.ts.map +1 -1
  17. package/dist/ui/styles.d.ts +1 -1
  18. package/dist/ui/styles.d.ts.map +1 -1
  19. package/dist/ui/subagent_panel.d.ts +92 -0
  20. package/dist/ui/subagent_panel.d.ts.map +1 -0
  21. package/dist/ui/subagent_update.d.ts +19 -0
  22. package/dist/ui/subagent_update.d.ts.map +1 -0
  23. package/dist/ui/tool_call_card.d.ts +73 -1
  24. package/dist/ui/tool_call_card.d.ts.map +1 -1
  25. package/dist/ui/ui_strings.d.ts +10 -0
  26. package/dist/ui/ui_strings.d.ts.map +1 -1
  27. package/package.json +1 -1
  28. package/src/constants.ts +63 -1
  29. package/src/core/ag_ui_chat.ts +180 -26
  30. package/src/core/agui_client.ts +26 -2
  31. package/src/index.ts +4 -0
  32. package/src/ui/message_actions.ts +20 -8
  33. package/src/ui/styles.ts +183 -0
  34. package/src/ui/subagent_panel.ts +213 -0
  35. package/src/ui/subagent_update.ts +80 -0
  36. package/src/ui/tool_call_card.ts +129 -3
  37. package/src/ui/ui_strings.ts +15 -0
  38. package/src/version.ts +1 -1
@@ -42,6 +42,61 @@ function formatPayload(text: string): string {
42
42
  }
43
43
  }
44
44
 
45
+ /**
46
+ * One region of a card's body, handed to a host {@link ToolPayloadFormatter}.
47
+ *
48
+ * A discriminated union rather than three positional parameters because the two
49
+ * halves do not carry the same thing: arguments are the parsed record the call
50
+ * was made with, and a result is the raw string the tool returned, which may not
51
+ * be JSON at all. Flattening both into one `payload` parameter would force every
52
+ * formatter to re-derive which it had before it could read it, and the
53
+ * arguments would arrive re-serialised for no reason.
54
+ *
55
+ * `toolName` is the raw tool name, not the card's `x-summary` label -- a
56
+ * formatter dispatches on identity, and the label is a display string a server
57
+ * may change.
58
+ */
59
+ export type ToolPayload =
60
+ | {
61
+ readonly kind: "arguments";
62
+ readonly toolName: string;
63
+ readonly args: Record<string, unknown>;
64
+ }
65
+ | {
66
+ readonly kind: "result";
67
+ readonly toolName: string;
68
+ readonly status: SettledStatus;
69
+ readonly text: string;
70
+ };
71
+
72
+ /**
73
+ * Renders one region of a tool card's body, for a host that would rather show a
74
+ * table or a summary line than a wall of pretty-printed JSON.
75
+ *
76
+ * Return a `Node` to take the region over, a `string` to replace its text, or
77
+ * `null` to fall through to the built-in pretty-print -- so a formatter that
78
+ * only cares about one tool, or only about results, declines the rest rather
79
+ * than reimplementing them.
80
+ *
81
+ * **Presentation only.** The model reads the tool result from its own copy of
82
+ * the message, which this never touches, so anything said here is said to the
83
+ * person and not to the agent. Translating a value -- an enum constant into a
84
+ * friendly label, an epoch into a date -- belongs on the server, where it also
85
+ * reaches the model's prose; doing it here would make the card and the answer
86
+ * beside it disagree about what happened.
87
+ *
88
+ * A returned string is set as text, never parsed as markup: this is not a
89
+ * second HTML channel into the transcript, and a host that wants elements
90
+ * builds them itself and returns the node.
91
+ */
92
+ export type ToolPayloadFormatter = (payload: ToolPayload) => Node | string | null;
93
+
94
+ /** Optional per-card wiring beyond the name, arguments, label and strings. */
95
+ export interface ToolCallCardOptions {
96
+ /** Host presentation hook for both body regions. See {@link ToolPayloadFormatter}. */
97
+ readonly formatPayload?: ToolPayloadFormatter;
98
+ }
99
+
45
100
  /**
46
101
  * A live tool-call card for the chat transcript.
47
102
  *
@@ -61,6 +116,12 @@ function formatPayload(text: string): string {
61
116
  * `--ag-ui-tool-icon-*` custom properties or the `tool-card-icon` part without
62
117
  * the card reaching into the host stylesheet.
63
118
  *
119
+ * Either region may be drawn by the host instead: `options.formatPayload` is
120
+ * asked about each one and pretty-prints as before whenever it declines. The
121
+ * arguments are offered from the constructor and the result from {@link settle},
122
+ * because that is when each exists -- so a formatter is asked twice per card,
123
+ * potentially long apart.
124
+ *
64
125
  * Pure DOM. The host appends {@link element} into its shadow root; all visible
65
126
  * text comes from {@link UiStrings}.
66
127
  */
@@ -83,6 +144,23 @@ export class ToolCallCard {
83
144
  */
84
145
  readonly approvalSlot: HTMLDivElement;
85
146
 
147
+ /**
148
+ * Where a *nested* run's progress renders — the sub-agent this call delegated
149
+ * to, narrating itself while the card waits.
150
+ *
151
+ * A slot rather than a rendered thing, on the same reasoning as
152
+ * {@link approvalSlot}: the card owns the position and something else owns the
153
+ * content. What makes the position right is that the wire keys a delegation on
154
+ * this card's own `toolCallId`, so the run being narrated is the one this card
155
+ * already stands for.
156
+ *
157
+ * Placed above the Details toggle rather than inside the body, because the
158
+ * body is what the display modes hide — and a progress line that only appears
159
+ * in `full` mode would leave exactly the stall it exists to end. Empty on every
160
+ * card that delegated nothing, and hidden while empty by the shadow CSS.
161
+ */
162
+ readonly subagentSlot: HTMLDivElement;
163
+
86
164
  readonly #status: HTMLSpanElement;
87
165
  readonly #decision: HTMLSpanElement;
88
166
  readonly #toggle: HTMLButtonElement;
@@ -98,6 +176,9 @@ export class ToolCallCard {
98
176
  * still exist when the user is asked to approve, edit or deny the call.
99
177
  */
100
178
  readonly args: Record<string, unknown>;
179
+ /** The raw tool name, kept for the payloads handed to {@link ToolPayloadFormatter}. */
180
+ readonly #name: string;
181
+ readonly #formatPayload: ToolPayloadFormatter | null;
101
182
  #settled = false;
102
183
 
103
184
  constructor(
@@ -105,9 +186,12 @@ export class ToolCallCard {
105
186
  args: Record<string, unknown>,
106
187
  summary?: string,
107
188
  strings: UiStrings = DEFAULT_UI_STRINGS,
189
+ options: ToolCallCardOptions = {},
108
190
  ) {
109
191
  this.#strings = strings;
110
192
  this.args = args;
193
+ this.#name = name;
194
+ this.#formatPayload = options.formatPayload ?? null;
111
195
 
112
196
  this.element = document.createElement("div");
113
197
  this.element.className = "tool-call";
@@ -146,7 +230,11 @@ export class ToolCallCard {
146
230
  head.append(icon, label, this.#status, this.#decision);
147
231
 
148
232
  const argsSection = this.#section("args", strings.argumentsLabel);
149
- argsSection.body.textContent = JSON.stringify(args, null, 2);
233
+ this.#renderPayload(
234
+ argsSection.body,
235
+ { kind: "arguments", toolName: name, args },
236
+ JSON.stringify(args, null, 2),
237
+ );
150
238
  // Drop the region rather than frame an empty object.
151
239
  argsSection.root.hidden = Object.keys(args).length === 0;
152
240
 
@@ -175,7 +263,11 @@ export class ToolCallCard {
175
263
  this.approvalSlot.className = "tool-call-approval";
176
264
  this.approvalSlot.setAttribute("part", "tool-card-approval");
177
265
 
178
- this.element.append(head, this.#toggle, body, this.approvalSlot);
266
+ this.subagentSlot = document.createElement("div");
267
+ this.subagentSlot.className = "tool-call-subagent";
268
+ this.subagentSlot.setAttribute("part", "tool-card-subagent");
269
+
270
+ this.element.append(head, this.subagentSlot, this.#toggle, body, this.approvalSlot);
179
271
  }
180
272
 
181
273
  /**
@@ -224,10 +316,44 @@ export class ToolCallCard {
224
316
  this.element.setAttribute("data-status", status);
225
317
  this.#status.textContent = statusLabels(this.#strings)[status];
226
318
  this.#resultLabel.textContent = resultLabels(this.#strings)[status];
227
- this.#resultBody.textContent = formatPayload(text);
319
+ this.#renderPayload(
320
+ this.#resultBody,
321
+ { kind: "result", toolName: this.#name, status, text },
322
+ formatPayload(text),
323
+ );
228
324
  this.#resultSection.hidden = false;
229
325
  }
230
326
 
327
+ /**
328
+ * Fill one body region, offering it to the host formatter first.
329
+ *
330
+ * `fallback` is computed by the caller rather than here because the two
331
+ * regions build it differently -- arguments are already parsed, a result is a
332
+ * string that may or may not be JSON -- and because a formatter that takes
333
+ * the region over should not have paid for a pretty-print nobody sees. It is
334
+ * cheap either way; the point is that the built-in rendering stays written
335
+ * once, beside the payload it belongs to.
336
+ *
337
+ * The `data-formatted` marker is what the shadow CSS reads to relax the
338
+ * preformatted whitespace on a region a host owns -- a table inherits it as
339
+ * mangled cell spacing, a sentence as line breaks nobody typed. Marked for a
340
+ * returned string too: one rule, and a summary line wants ordinary wrapping
341
+ * as much as a table does.
342
+ */
343
+ #renderPayload(body: HTMLPreElement, payload: ToolPayload, fallback: string): void {
344
+ const rendered = this.#formatPayload === null ? null : this.#formatPayload(payload);
345
+ if (rendered === null) {
346
+ body.textContent = fallback;
347
+ return;
348
+ }
349
+ body.setAttribute("data-formatted", "true");
350
+ if (typeof rendered === "string") {
351
+ body.textContent = rendered;
352
+ return;
353
+ }
354
+ body.replaceChildren(rendered);
355
+ }
356
+
231
357
  /** Build one labelled region of the body: a heading plus a payload block. */
232
358
  #section(
233
359
  kind: string,
@@ -131,6 +131,18 @@ export interface UiStrings {
131
131
  /** Label on the toggle that expands a tool card's body. */
132
132
  details: string;
133
133
 
134
+ // ── Delegated sub-agent ─────────────────────────────────────────────────────
135
+ /**
136
+ * The delegation row's text before the server's own status line lands.
137
+ *
138
+ * A fallback, not a state: every announcement carries a pre-rendered `status`,
139
+ * and this only shows if one arrives unusable. The row is the expander, so it
140
+ * must never be blank.
141
+ */
142
+ subAgentWorking: string;
143
+ /** `aria-label` of the region holding the sub-agent's own tool calls. */
144
+ subAgentSteps: string;
145
+
134
146
  // ── Confirmation card ───────────────────────────────────────────────────────
135
147
  /** `aria-label` of the editable arguments field on an approval card. */
136
148
  approvalEditArgs: string;
@@ -319,6 +331,9 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
319
331
  declinedLabel: "Declined",
320
332
  details: "Details",
321
333
 
334
+ subAgentWorking: "Working…",
335
+ subAgentSteps: "Steps the sub-agent took",
336
+
322
337
  approvalEditArgs: "Edit the arguments before approving",
323
338
  approvalArgsInvalid: "That is not valid JSON, so nothing was sent.",
324
339
  approvalArgsNotAnObject: "Arguments have to be a JSON object.",
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION: string = "0.29.0";
1
+ export const VERSION: string = "0.30.0";