@artooi/ag-ui-web-component 0.18.0 → 0.19.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.
@@ -1,4 +1,4 @@
1
- import { TOOL_CALL_STATUS, TOOL_DISPLAY } from "../constants.js";
1
+ import { TOOL_CALL_STATUS, type TOOL_DISPLAY } from "../constants.js";
2
2
  import { DEFAULT_UI_STRINGS, type UiStrings } from "./ui_strings.js";
3
3
 
4
4
  /** Any state a tool-call card can be in. */
@@ -20,7 +20,7 @@ function statusLabels(strings: UiStrings): Record<ToolCallStatus, string> {
20
20
  };
21
21
  }
22
22
 
23
- /** Toggle-button label for each settled outcome's collapsible body (full mode). */
23
+ /** Section-heading text for each settled outcome's result region. */
24
24
  function resultLabels(strings: UiStrings): Record<SettledStatus, string> {
25
25
  return {
26
26
  [TOOL_CALL_STATUS.DONE]: strings.resultLabel,
@@ -29,19 +29,32 @@ function resultLabels(strings: UiStrings): Record<SettledStatus, string> {
29
29
  };
30
30
  }
31
31
 
32
+ /** Pretty-print a JSON payload; fall back to the raw text if it isn't JSON. */
33
+ function formatPayload(text: string): string {
34
+ try {
35
+ return JSON.stringify(JSON.parse(text), null, 2);
36
+ } catch {
37
+ return text;
38
+ }
39
+ }
40
+
32
41
  /**
33
42
  * A live tool-call card for the chat transcript.
34
43
  *
35
- * Construction renders a status icon, the tool name, and a `running…` status
36
- * pill; in `full` mode it also shows the pretty-printed arguments inline.
37
- * {@link settle} later flips the pill to the outcome and depending on the
38
- * {@link ToolDisplayMode} appends a collapsible body:
44
+ * Construction renders a status icon, the tool name, a `running…` pill, and the
45
+ * card's body: an **arguments** region and a **result** region, each with its own
46
+ * heading and its own `part`. {@link settle} fills in the result and flips the
47
+ * pill. Both payloads are pretty-printed, and the two are never concatenated —
48
+ * the previous compact layout ran `args: {...}` and the result together in one
49
+ * `<pre>`, leaving no way to see where the call ended and the answer began.
39
50
  *
40
- * - `inline` no args; the result behind its own toggle (like `full` but with
41
- * the card chrome stripped to a single light row).
42
- * - `minimal` pill only; nothing to expand.
43
- * - `compact` one "Details" toggle revealing args *and* result together.
44
- * - `full` the result (or error / decline message) behind its own toggle.
51
+ * **The card renders one DOM shape in every display mode, and CSS decides what
52
+ * shows.** That is what makes `data-tool-display` behave like `data-answer-well`
53
+ * flip it on the host and every card already on screen re-reads it. Building a
54
+ * different structure per mode meant only cards created *after* the change
55
+ * picked it up, so the setting appeared not to work until the next conversation.
56
+ * Visibility is selected from the host attribute rather than a value copied onto
57
+ * the card at construction, for the same reason.
45
58
  *
46
59
  * The leading icon carries no text of its own: its glyph/spinner is drawn by
47
60
  * the shadow CSS keyed off the card's `data-status`, so a host themes it via
@@ -57,20 +70,20 @@ export class ToolCallCard {
57
70
  readonly element: HTMLDivElement;
58
71
 
59
72
  readonly #status: HTMLSpanElement;
60
- readonly #mode: ToolDisplayMode;
61
- readonly #args: Record<string, unknown>;
73
+ readonly #decision: HTMLSpanElement;
74
+ readonly #toggle: HTMLButtonElement;
75
+ readonly #resultSection: HTMLDivElement;
76
+ readonly #resultLabel: HTMLSpanElement;
77
+ readonly #resultBody: HTMLPreElement;
62
78
  readonly #strings: UiStrings;
63
79
  #settled = false;
64
80
 
65
81
  constructor(
66
82
  name: string,
67
83
  args: Record<string, unknown>,
68
- mode: ToolDisplayMode = TOOL_DISPLAY.FULL,
69
84
  summary?: string,
70
85
  strings: UiStrings = DEFAULT_UI_STRINGS,
71
86
  ) {
72
- this.#mode = mode;
73
- this.#args = args;
74
87
  this.#strings = strings;
75
88
 
76
89
  this.element = document.createElement("div");
@@ -78,7 +91,7 @@ export class ToolCallCard {
78
91
  this.element.setAttribute("part", "tool-card");
79
92
  this.element.setAttribute("data-tool-name", name);
80
93
  this.element.setAttribute("data-status", TOOL_CALL_STATUS.PENDING);
81
- this.element.setAttribute("data-display", mode);
94
+ this.element.setAttribute("data-expanded", "false");
82
95
 
83
96
  const head = document.createElement("div");
84
97
  head.className = "tool-call-head";
@@ -103,16 +116,56 @@ export class ToolCallCard {
103
116
  this.#status.setAttribute("part", "tool-card-status");
104
117
  this.#status.textContent = statusLabels(strings)[TOOL_CALL_STATUS.PENDING];
105
118
 
106
- head.append(icon, label, this.#status);
107
- this.element.append(head);
119
+ this.#decision = document.createElement("span");
120
+ this.#decision.className = "tool-call-decision";
121
+ this.#decision.setAttribute("part", "tool-card-decision");
122
+ this.#decision.hidden = true;
123
+
124
+ head.append(icon, label, this.#status, this.#decision);
125
+
126
+ const argsSection = this.#section("args", strings.argumentsLabel);
127
+ argsSection.body.textContent = JSON.stringify(args, null, 2);
128
+ // A call with no arguments renders an empty object in a box of its own,
129
+ // which is a frame around nothing. Drop the region instead.
130
+ argsSection.root.hidden = Object.keys(args).length === 0;
131
+
132
+ const resultSection = this.#section("result", strings.resultLabel);
133
+ this.#resultSection = resultSection.root;
134
+ this.#resultLabel = resultSection.label;
135
+ this.#resultBody = resultSection.body;
136
+ // Nothing to show until `settle` supplies it; a pending card would
137
+ // otherwise expand onto an empty region.
138
+ resultSection.root.hidden = true;
139
+
140
+ this.#toggle = document.createElement("button");
141
+ this.#toggle.type = "button";
142
+ this.#toggle.className = "tool-call-toggle";
143
+ this.#toggle.setAttribute("part", "tool-card-toggle");
144
+ this.#toggle.setAttribute("aria-expanded", "false");
145
+ this.#toggle.textContent = strings.details;
146
+ this.#toggle.addEventListener("click", () => this.#setExpanded(!this.#expanded()));
147
+
148
+ const body = document.createElement("div");
149
+ body.className = "tool-call-body";
150
+ body.setAttribute("part", "tool-card-body");
151
+ body.append(argsSection.root, resultSection.root);
152
+
153
+ this.element.append(head, this.#toggle, body);
154
+ }
108
155
 
109
- if (mode === TOOL_DISPLAY.FULL) {
110
- const argsEl = document.createElement("pre");
111
- argsEl.className = "tool-call-args";
112
- argsEl.setAttribute("part", "tool-card-args");
113
- argsEl.textContent = JSON.stringify(args, null, 2);
114
- this.element.append(argsEl);
115
- }
156
+ /**
157
+ * Record that a human approved or declined this call, as a line in the card.
158
+ *
159
+ * Approval used to leave no trace at all: a declined call became a tool
160
+ * result saying so, while an approved one simply ran, making the transcript
161
+ * of a gated call byte-identical to one that was never gated. The prompt is
162
+ * gone once answered, so this is where the decision lives.
163
+ */
164
+ recordDecision(kind: "approved" | "declined"): void {
165
+ this.element.setAttribute("data-decision", kind);
166
+ this.#decision.textContent =
167
+ kind === "approved" ? this.#strings.decisionApproved : this.#strings.decisionDeclined;
168
+ this.#decision.hidden = false;
116
169
  }
117
170
 
118
171
  /** Whether {@link settle} has already run (so a terminal sweep can skip it). */
@@ -121,50 +174,55 @@ export class ToolCallCard {
121
174
  }
122
175
 
123
176
  /**
124
- * Flip the status pill to ``status`` and, unless in `minimal` mode, append a
125
- * collapsed body behind a click-to-expand toggle: the result alone (`full` /
126
- * `inline`), or the args + result together (`compact`).
177
+ * Flip the status pill to `status` and fill in the result region, whose
178
+ * heading names the outcome (result / error / declined).
127
179
  */
128
180
  settle(status: SettledStatus, text: string): void {
129
181
  // Idempotent: a duplicate `TOOL_CALL_RESULT`, or a replayed tool message
130
- // for an already-settled card, must not append a second toggle+body. The
131
- // first settle wins; later calls are ignored.
182
+ // for an already-settled card, must not overwrite the first outcome.
132
183
  if (this.#settled) {
133
184
  return;
134
185
  }
135
186
  this.#settled = true;
136
187
  this.element.setAttribute("data-status", status);
137
188
  this.#status.textContent = statusLabels(this.#strings)[status];
189
+ this.#resultLabel.textContent = resultLabels(this.#strings)[status];
190
+ this.#resultBody.textContent = formatPayload(text);
191
+ this.#resultSection.hidden = false;
192
+ }
138
193
 
139
- if (this.#mode === TOOL_DISPLAY.MINIMAL) {
140
- return;
141
- }
194
+ /** Build one labelled region of the body: a heading plus a payload block. */
195
+ #section(
196
+ kind: string,
197
+ labelText: string,
198
+ ): {
199
+ root: HTMLDivElement;
200
+ label: HTMLSpanElement;
201
+ body: HTMLPreElement;
202
+ } {
203
+ const root = document.createElement("div");
204
+ root.className = `tool-call-section tool-call-section--${kind}`;
205
+ root.setAttribute("part", `tool-card-section tool-card-${kind}-section`);
142
206
 
143
- const toggle = document.createElement("button");
144
- toggle.type = "button";
145
- toggle.className = "tool-call-toggle";
146
- toggle.setAttribute("part", "tool-card-toggle");
147
- toggle.setAttribute("aria-expanded", "false");
148
-
149
- const output = document.createElement("pre");
150
- output.className = "tool-call-result";
151
- output.setAttribute("part", "tool-card-result");
152
- output.hidden = true;
153
-
154
- if (this.#mode === TOOL_DISPLAY.COMPACT) {
155
- toggle.textContent = this.#strings.details;
156
- output.textContent = `args: ${JSON.stringify(this.#args)}\n\n${text}`;
157
- } else {
158
- toggle.textContent = resultLabels(this.#strings)[status];
159
- output.textContent = text;
160
- }
207
+ const label = document.createElement("span");
208
+ label.className = "tool-call-section-label";
209
+ label.setAttribute("part", `tool-card-section-label tool-card-${kind}-label`);
210
+ label.textContent = labelText;
161
211
 
162
- toggle.addEventListener("click", () => {
163
- const expand = output.hidden;
164
- output.hidden = !expand;
165
- toggle.setAttribute("aria-expanded", String(expand));
166
- });
212
+ const body = document.createElement("pre");
213
+ body.className = `tool-call-${kind}`;
214
+ body.setAttribute("part", `tool-card-${kind}`);
215
+
216
+ root.append(label, body);
217
+ return { root, label, body };
218
+ }
219
+
220
+ #expanded(): boolean {
221
+ return this.element.getAttribute("data-expanded") === "true";
222
+ }
167
223
 
168
- this.element.append(toggle, output);
224
+ #setExpanded(expand: boolean): void {
225
+ this.element.setAttribute("data-expanded", String(expand));
226
+ this.#toggle.setAttribute("aria-expanded", String(expand));
169
227
  }
170
228
  }
@@ -89,13 +89,19 @@ export interface UiStrings {
89
89
  toolError: string;
90
90
  /** Status pill on a declined call. */
91
91
  toolDeclined: string;
92
- /** Toggle label revealing a successful result (full mode). */
92
+ /** Note on a tool card whose call a human approved. */
93
+ decisionApproved: string;
94
+ /** Note on a tool card whose call a human declined. */
95
+ decisionDeclined: string;
96
+ /** Heading over a tool card's arguments region. */
97
+ argumentsLabel: string;
98
+ /** Heading over a tool card's result region when the call succeeded. */
93
99
  resultLabel: string;
94
- /** Toggle label revealing an error (full mode). */
100
+ /** Heading over a tool card's result region when the call failed. */
95
101
  errorLabel: string;
96
- /** Toggle label revealing a declined call (full mode). */
102
+ /** Heading over a tool card's result region when the call was declined. */
97
103
  declinedLabel: string;
98
- /** Toggle label revealing args + result together (compact mode). */
104
+ /** Label on the toggle that expands a tool card's body. */
99
105
  details: string;
100
106
 
101
107
  // ── Confirmation card ───────────────────────────────────────────────────────
@@ -237,6 +243,9 @@ export const DEFAULT_UI_STRINGS: UiStrings = {
237
243
  toolDone: "✓ done",
238
244
  toolError: "⚠ error",
239
245
  toolDeclined: "⊘ declined",
246
+ decisionApproved: "approved by you",
247
+ decisionDeclined: "declined by you",
248
+ argumentsLabel: "Arguments",
240
249
  resultLabel: "Result",
241
250
  errorLabel: "Error",
242
251
  declinedLabel: "Declined",
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION: string = "0.18.0";
1
+ export const VERSION: string = "0.19.0";