@artooi/ag-ui-web-component 0.46.0 → 0.47.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 (54) hide show
  1. package/CHANGELOG.md +87 -1
  2. package/README.md +130 -19
  3. package/dist/ag-ui-web-component.bundle.js +99 -94
  4. package/dist/ag-ui-web-component.bundle.js.map +4 -4
  5. package/dist/constants.d.ts +10 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/core/ag_ui_chat.d.ts +38 -0
  8. package/dist/core/ag_ui_chat.d.ts.map +1 -1
  9. package/dist/core/chat_registry.d.ts +23 -0
  10. package/dist/core/chat_registry.d.ts.map +1 -0
  11. package/dist/core/events/run_finished_detail.d.ts +6 -0
  12. package/dist/core/events/run_finished_detail.d.ts.map +1 -1
  13. package/dist/core/events/thread_detail.d.ts +27 -0
  14. package/dist/core/events/thread_detail.d.ts.map +1 -0
  15. package/dist/core/run_handlers.d.ts +6 -1
  16. package/dist/core/run_handlers.d.ts.map +1 -1
  17. package/dist/dom/animations.d.ts +21 -6
  18. package/dist/dom/animations.d.ts.map +1 -1
  19. package/dist/dom/highlight_overlay.d.ts +7 -1
  20. package/dist/dom/highlight_overlay.d.ts.map +1 -1
  21. package/dist/dom/ring_color.d.ts +14 -0
  22. package/dist/dom/ring_color.d.ts.map +1 -0
  23. package/dist/index.d.ts +3 -2
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +249 -54
  26. package/dist/index.js.map +4 -4
  27. package/dist/ui/history/conversation_history.d.ts +10 -1
  28. package/dist/ui/history/conversation_history.d.ts.map +1 -1
  29. package/dist/ui/placement/panel_placement.d.ts +6 -0
  30. package/dist/ui/placement/panel_placement.d.ts.map +1 -1
  31. package/dist/ui/placement/small_viewport_styles.d.ts.map +1 -1
  32. package/dist/ui/progress/tool_call_card.d.ts.map +1 -1
  33. package/dist/ui/progress/tool_step_group.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/constants.ts +11 -0
  36. package/src/core/ag_ui_chat.ts +108 -5
  37. package/src/core/chat_registry.ts +53 -0
  38. package/src/core/events/run_finished_detail.ts +6 -0
  39. package/src/core/events/thread_detail.ts +26 -0
  40. package/src/core/run_handlers.ts +9 -4
  41. package/src/dom/animations.ts +30 -31
  42. package/src/dom/highlight_overlay.ts +9 -5
  43. package/src/dom/ring_color.ts +85 -0
  44. package/src/index.ts +3 -0
  45. package/src/ui/history/conversation_history.ts +28 -4
  46. package/src/ui/placement/embedded_styles.ts +2 -2
  47. package/src/ui/placement/panel_placement.ts +34 -9
  48. package/src/ui/placement/preset_styles.ts +1 -1
  49. package/src/ui/placement/sidebar_styles.ts +1 -1
  50. package/src/ui/placement/small_viewport_styles.ts +5 -0
  51. package/src/ui/progress/tool_call_card.ts +18 -8
  52. package/src/ui/progress/tool_step_group.ts +3 -0
  53. package/src/ui/shell/token_styles.ts +2 -2
  54. package/src/version.ts +1 -1
@@ -8,17 +8,7 @@
8
8
 
9
9
  import { showHighlightOverlay } from "./highlight_overlay.js";
10
10
  import { setNativeChecked, setNativeValue } from "./native_setter.js";
11
-
12
- /** Ring colour when the host page has not themed `--ag-ui-accent`. */
13
- const ACCENT = "#4f46e5";
14
-
15
- /**
16
- * Fallback for the box-shadow rings, which read as a soft halo rather than a
17
- * line and so carry their own alpha.
18
- */
19
- const ACCENT_HALO = "rgba(79, 70, 229, 0.4)";
20
-
21
- const ACCENT_PROPERTY = "--ag-ui-accent";
11
+ import { ringColor } from "./ring_color.js";
22
12
 
23
13
  function delay(ms: number): Promise<void> {
24
14
  return new Promise<void>((resolve) => {
@@ -48,19 +38,23 @@ function motionDelay(ms: number): Promise<void> {
48
38
  }
49
39
 
50
40
  /**
51
- * Resolve the accent colour from the target's own computed style.
41
+ * Which `<ag-ui-chat>` a ring speaks for, so it is drawn in that chat's colour.
52
42
  *
53
- * `--ag-ui-accent` inherits, so reading it off the element about to be touched
54
- * picks up whatever the host's cascade produced there, and a themed page is
55
- * flashed in its own colour rather than this package's indigo.
43
+ * The ring is drawn in the host's page, which a token set on the chat never
44
+ * reaches, so the chat is read directly. Unset, it is the most recently
45
+ * connected chat; a page with two widgets names the one acting.
56
46
  */
57
- function accentColor(el: HTMLElement, fallback: string): string {
58
- const themed = window.getComputedStyle(el).getPropertyValue(ACCENT_PROPERTY).trim();
59
- return themed === "" ? fallback : themed;
47
+ export interface RingOwnerOptions {
48
+ /** The chat whose ring colour and accent to use. Default: the latest connected. */
49
+ chat?: HTMLElement;
60
50
  }
61
51
 
62
- function ringShadow(el: HTMLElement): string {
63
- return `0 0 0 3px ${accentColor(el, ACCENT_HALO)}`;
52
+ /**
53
+ * The soft halo the press and toggle rings draw: the ring colour at 40%.
54
+ * Mixed rather than given its own alpha, so it follows whichever colour won.
55
+ */
56
+ function ringShadow(el: HTMLElement, chat: HTMLElement | undefined): string {
57
+ return `0 0 0 3px color-mix(in srgb, ${ringColor(el, chat)} 40%, transparent)`;
64
58
  }
65
59
 
66
60
  /** An element whose `value` can be typed into. */
@@ -93,7 +87,7 @@ export async function typeInto(
93
87
  el.dispatchEvent(new Event("change", { bubbles: true }));
94
88
  }
95
89
 
96
- export interface HighlightClickOptions {
90
+ export interface HighlightClickOptions extends RingOwnerOptions {
97
91
  /** Milliseconds to hold the highlight before clicking. Default 280. */
98
92
  highlightMs?: number;
99
93
  }
@@ -106,7 +100,7 @@ export async function highlightThenClick(
106
100
  const highlightMs = options.highlightMs ?? 280;
107
101
  const previousOutline = el.style.outline;
108
102
  const previousOffset = el.style.outlineOffset;
109
- el.style.outline = `2px solid ${accentColor(el, ACCENT)}`;
103
+ el.style.outline = `2px solid ${ringColor(el, options.chat)}`;
110
104
  el.style.outlineOffset = "2px";
111
105
  await delay(highlightMs);
112
106
  el.style.outline = previousOutline;
@@ -176,10 +170,14 @@ const FLASH_MS = 1200;
176
170
  /** Share of {@link FLASH_MS} spent fading out rather than holding. */
177
171
  const FLASH_FADE_RATIO = 1 / 3;
178
172
 
179
- export interface FlashOptions {
173
+ export interface FlashOptions extends RingOwnerOptions {
180
174
  /** Total milliseconds the ring is on screen, hold plus fade. Default 1200. */
181
175
  flashMs?: number;
182
- /** Ring colour. Defaults to the target's `--ag-ui-accent`, else the package accent. */
176
+ /**
177
+ * Ring colour. Defaults to `--ag-ui-highlight-ring-color` (on the target,
178
+ * then on the chat), then the target's `--ag-ui-accent`, then the chat's
179
+ * accent.
180
+ */
183
181
  color?: string;
184
182
  /**
185
183
  * Dim everything except the target.
@@ -226,6 +224,7 @@ async function flashRing(
226
224
  gradient: options.gradient === true,
227
225
  ...(options.color === undefined ? {} : { color: options.color }),
228
226
  ...(options.ringPadding === undefined ? {} : { padding: options.ringPadding }),
227
+ ...(options.chat === undefined ? {} : { chat: options.chat }),
229
228
  });
230
229
  await delay(flashMs);
231
230
  dismiss();
@@ -234,7 +233,7 @@ async function flashRing(
234
233
  const previousOutline = el.style.outline;
235
234
  const previousOffset = el.style.outlineOffset;
236
235
  const previousTransition = el.style.transition;
237
- const color = options.color ?? accentColor(el, ACCENT);
236
+ const color = options.color ?? ringColor(el, options.chat);
238
237
  // outline, not box-shadow: a shadow paints outside the border box, so any
239
238
  // overflow:hidden ancestor sharing the target's box (a card, a table cell)
240
239
  // clips the whole ring away while the helper still reports success.
@@ -273,7 +272,7 @@ export function focusWithFlash(el: HTMLElement, options: FlashOptions = {}): Pro
273
272
  return flashRing(el, options, true);
274
273
  }
275
274
 
276
- export interface PressOptions {
275
+ export interface PressOptions extends RingOwnerOptions {
277
276
  /** Milliseconds to hold the pressed state before clicking. Default 140. */
278
277
  pressMs?: number;
279
278
  }
@@ -290,7 +289,7 @@ export async function pressThenClick(el: HTMLElement, options: PressOptions = {}
290
289
  const previousShadow = el.style.boxShadow;
291
290
  el.style.transition = "transform 80ms ease";
292
291
  el.style.transform = "scale(0.96)";
293
- el.style.boxShadow = ringShadow(el);
292
+ el.style.boxShadow = ringShadow(el, options.chat);
294
293
  await motionDelay(pressMs);
295
294
  el.style.transform = previousTransform;
296
295
  el.style.transition = previousTransition;
@@ -298,7 +297,7 @@ export async function pressThenClick(el: HTMLElement, options: PressOptions = {}
298
297
  el.click();
299
298
  }
300
299
 
301
- export interface SelectOptions {
300
+ export interface SelectOptions extends RingOwnerOptions {
302
301
  /** Milliseconds to hold the highlight before committing. Default 220. */
303
302
  highlightMs?: number;
304
303
  }
@@ -330,7 +329,7 @@ export async function selectOption(
330
329
  const highlightMs = options.highlightMs ?? 220;
331
330
  const previousOutline = el.style.outline;
332
331
  const previousOffset = el.style.outlineOffset;
333
- el.style.outline = `2px solid ${accentColor(el, ACCENT)}`;
332
+ el.style.outline = `2px solid ${ringColor(el, options.chat)}`;
334
333
  el.style.outlineOffset = "2px";
335
334
  await motionDelay(highlightMs);
336
335
  setNativeValue(el, option.value);
@@ -340,7 +339,7 @@ export async function selectOption(
340
339
  el.style.outlineOffset = previousOffset;
341
340
  }
342
341
 
343
- export interface ToggleOptions {
342
+ export interface ToggleOptions extends RingOwnerOptions {
344
343
  /** Milliseconds to hold the flash. Default 200. */
345
344
  flashMs?: number;
346
345
  }
@@ -356,7 +355,7 @@ export async function toggleControl(
356
355
  ): Promise<void> {
357
356
  const flashMs = options.flashMs ?? 200;
358
357
  const previousShadow = el.style.boxShadow;
359
- el.style.boxShadow = ringShadow(el);
358
+ el.style.boxShadow = ringShadow(el, options.chat);
360
359
  await motionDelay(flashMs);
361
360
  setNativeChecked(el, checked);
362
361
  el.dispatchEvent(new Event("input", { bubbles: true }));
@@ -1,7 +1,5 @@
1
1
  import { HIGHLIGHT_OVERLAY_Z_INDEX } from "../constants.js";
2
-
3
- /** The package accent, used when the host has themed nothing. */
4
- const ACCENT = "#4f46e5";
2
+ import { ringColor } from "./ring_color.js";
5
3
 
6
4
  /** How the overlay is drawn around the element being pointed at. */
7
5
  export interface HighlightOverlayOptions {
@@ -9,8 +7,14 @@ export interface HighlightOverlayOptions {
9
7
  readonly scrim?: boolean;
10
8
  /** Draw the ring as a moving gradient rather than a flat colour. Default false. */
11
9
  readonly gradient?: boolean;
12
- /** Ring colour, and the flat fallback for the gradient. */
10
+ /**
11
+ * Ring colour, and the flat fallback for the gradient. Default:
12
+ * `--ag-ui-highlight-ring-color` on the target, then on the chat, then the
13
+ * target's `--ag-ui-accent`, then the chat's accent.
14
+ */
13
15
  readonly color?: string;
16
+ /** The chat whose ring colour and accent to use. Default: the latest connected. */
17
+ readonly chat?: HTMLElement;
14
18
  /** Gap between the target's box and the ring. Default 4. */
15
19
  readonly padding?: number;
16
20
  /** Corner radius of the cut-out and the ring. Default: the target's own. */
@@ -205,7 +209,7 @@ function safeColor(value: string | undefined): string | null {
205
209
  * the padding box, leaving the frame.
206
210
  */
207
211
  function ringPaint(target: HTMLElement, options: HighlightOverlayOptions): string {
208
- const color = safeColor(options.color) ?? tokenOn(target, "--ag-ui-accent", ACCENT);
212
+ const color = safeColor(options.color) ?? ringColor(target, options.chat);
209
213
  if (options.gradient !== true) {
210
214
  return `border-color: ${color}`;
211
215
  }
@@ -0,0 +1,85 @@
1
+ import { ChatRegistration } from "../core/chat_registry.js";
2
+
3
+ /** The public ring token, read from the target and then from the chat. */
4
+ const RING_TOKEN = "--ag-ui-highlight-ring-color";
5
+
6
+ /** The accent a host themes its page with, read from the target. */
7
+ const ACCENT_TOKEN = "--ag-ui-accent";
8
+
9
+ /**
10
+ * The accent the chat actually resolved, its theme's default included. Read
11
+ * through the private alias because the public token is unset on a page that
12
+ * themed nothing, which is exactly the page this exists for.
13
+ */
14
+ const CHAT_ACCENT = "--_accent";
15
+
16
+ /** The package indigo, for a page with no chat and no theming at all. */
17
+ const PACKAGE_ACCENT = "#4f46e5";
18
+
19
+ /**
20
+ * The colour a highlight ring is drawn in when the caller passed none.
21
+ *
22
+ * In order: `--ag-ui-highlight-ring-color` on the target, the same token on
23
+ * the chat, the target's `--ag-ui-accent`, the chat's resolved accent, the
24
+ * package indigo. The target's accent stays above the chat's own default so a
25
+ * host that themes `--ag-ui-accent` on its page keeps the rings it has; the
26
+ * ring token sits above both so a host can keep the ring apart from the accent.
27
+ *
28
+ * `chat` defaults to the most recently connected `<ag-ui-chat>`, the common
29
+ * page with one widget. A page with two passes the one the ring speaks for.
30
+ */
31
+ export function ringColor(target: HTMLElement, chat?: HTMLElement): string {
32
+ const onTarget = readToken(target, RING_TOKEN);
33
+ if (onTarget !== "") {
34
+ return onTarget;
35
+ }
36
+ const owner = chat ?? ChatRegistration.latest();
37
+ const onChat = owner === null ? "" : resolveOn(owner, RING_TOKEN);
38
+ if (onChat !== "") {
39
+ return onChat;
40
+ }
41
+ const accent = readToken(target, ACCENT_TOKEN);
42
+ if (accent !== "") {
43
+ return accent;
44
+ }
45
+ const chatAccent = owner === null ? "" : resolveOn(owner, CHAT_ACCENT);
46
+ return chatAccent === "" ? PACKAGE_ACCENT : chatAccent;
47
+ }
48
+
49
+ function readToken(el: HTMLElement, name: string): string {
50
+ return window.getComputedStyle(el).getPropertyValue(name).trim();
51
+ }
52
+
53
+ /**
54
+ * The custom property `name` on `chat`, as the colour it computes to there, or
55
+ * `""` when it is unset or not a colour.
56
+ *
57
+ * Resolved inside the chat rather than handed to the target, because the
58
+ * chat's colours are `light-dark()` pairs and a custom property keeps them
59
+ * unresolved: written onto an element in the page, the pair would pick its
60
+ * half by the page's colour scheme, so a dark widget on a light page would ring
61
+ * in its light-mode accent. A probe inside the shadow tree inherits the chat's
62
+ * scheme, and its computed `color` is the resolved value.
63
+ *
64
+ * An element with no open shadow root (a host passing something other than a
65
+ * chat) is read as it is, unresolved; that is the best it offers.
66
+ */
67
+ function resolveOn(chat: HTMLElement, name: string): string {
68
+ const value = readToken(chat, name);
69
+ const root = chat.shadowRoot;
70
+ if (value === "" || root === null) {
71
+ return value;
72
+ }
73
+ const probe = document.createElement("span");
74
+ probe.hidden = true;
75
+ probe.style.color = value;
76
+ // An unparseable value leaves the declaration empty, and the probe would
77
+ // then report the colour it inherits instead of saying it has none.
78
+ if (probe.style.color === "") {
79
+ return "";
80
+ }
81
+ root.appendChild(probe);
82
+ const resolved = window.getComputedStyle(probe).color;
83
+ probe.remove();
84
+ return resolved;
85
+ }
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ export {
17
17
  STATE_EVENT,
18
18
  SUBMIT_EVENT,
19
19
  SUGGESTIONS_ACTIVITY_TYPE,
20
+ THREAD_EVENT,
20
21
  TOGGLE_EVENT,
21
22
  TOOL_CALL_STATUS,
22
23
  TOOL_DISPLAY,
@@ -63,6 +64,7 @@ export type { InvalidateDetail } from "./core/events/invalidate_detail.js";
63
64
  export type { RunFinishedDetail } from "./core/events/run_finished_detail.js";
64
65
  export type { StateDetail } from "./core/events/state_detail.js";
65
66
  export type { SubmitDetail } from "./core/events/submit_detail.js";
67
+ export type { ThreadDetail } from "./core/events/thread_detail.js";
66
68
  export type { ToggleDetail } from "./core/events/toggle_detail.js";
67
69
  export type { ToolRun } from "./core/events/tool_run.js";
68
70
  export type { UnreadDetail } from "./core/events/unread_detail.js";
@@ -89,6 +91,7 @@ export {
89
91
  type PressOptions,
90
92
  prefersReducedMotion,
91
93
  pressThenClick,
94
+ type RingOwnerOptions,
92
95
  type ScrollOptions,
93
96
  type SelectOptions,
94
97
  scrollIntoCenterView,
@@ -9,6 +9,7 @@ import type {
9
9
  ClientConversationStore,
10
10
  NavigationCheckpoint,
11
11
  } from "../../core/conversation_store.js";
12
+ import type { ThreadDetail } from "../../core/events/thread_detail.js";
12
13
  import type { MessageRole } from "../../core/message_role.js";
13
14
  import { RunIndex } from "../../core/run_index.js";
14
15
  import { toolStatusFromOutcome } from "../../core/tool_outcome.js";
@@ -119,6 +120,11 @@ export interface ConversationHistoryHost {
119
120
  readonly resetState: () => void;
120
121
  /** Swap the composer between Send and Stop, which the composer owns. */
121
122
  readonly setRunning: (running: boolean) => void;
123
+ /**
124
+ * Say the conversation on screen changed, once its transcript is replaced.
125
+ * The element fills in the thread it replaced, which it last reported.
126
+ */
127
+ readonly threadChanged: (threadId: string, reason: ThreadDetail["reason"]) => void;
122
128
  }
123
129
 
124
130
  /**
@@ -335,7 +341,7 @@ export class ConversationHistory {
335
341
  this.#host.conversationStore().setActiveThread(threadId);
336
342
  this.#threadId = threadId;
337
343
  this.#host.setRunning(false);
338
- await this.rehydrate();
344
+ await this.rehydrate("switch");
339
345
  }
340
346
 
341
347
  /** Rename a thread, then reload the list that shows its title. */
@@ -355,6 +361,8 @@ export class ConversationHistory {
355
361
  this.#host.resetState();
356
362
  this.adoptActiveThread();
357
363
  this.#host.setRunning(false);
364
+ // No replay to wait for: the thread that took its place is a fresh one.
365
+ this.#host.threadChanged(this.#threadId, "delete");
358
366
  }
359
367
  void this.refreshDrawer();
360
368
  }
@@ -628,8 +636,11 @@ export class ConversationHistory {
628
636
  * for the run it would have resumed -- unless what replaced it is a restore
629
637
  * of the same conversation, still current, which answers for that run
630
638
  * instead; see `#forgetStopped`.
639
+ *
640
+ * `reason` is what the thread event this restore ends with reports as why
641
+ * the conversation changed. A restore that is stopped reports nothing.
631
642
  */
632
- async rehydrate(): Promise<void> {
643
+ async rehydrate(reason: ThreadDetail["reason"]): Promise<void> {
633
644
  // Claimed before awaiting, and checked by `#restoring` wherever the restore
634
645
  // has waited or run host code. With a slow remote store, picking thread B
635
646
  // then C interleaved both replays into one transcript, and New chat drew
@@ -727,11 +738,24 @@ export class ConversationHistory {
727
738
  }
728
739
  }
729
740
  }
741
+ if (checkpoint === null) {
742
+ this.#noticeIfRunUnfinished(messages);
743
+ }
744
+ // Said once the transcript is the restored one, and only by a restore
745
+ // still current: one that was stopped returned above, so two quick
746
+ // switches report the second alone. Before a resume, because the resume
747
+ // is a run, and waiting for it reported the change a whole answer late.
748
+ this.#host.threadChanged(threadId, reason);
730
749
  if (checkpoint !== null) {
750
+ // A listener is host code, and can stop the restore as a replayed
751
+ // renderer can. Resuming then answered the call into the conversation
752
+ // that replaced it, and left the checkpoint for a later visit to resume.
753
+ if (!this.#restoring(generation)) {
754
+ this.#forgetStopped(store, threadId, checkpoint);
755
+ return;
756
+ }
731
757
  await this.#resumeFrom(checkpoint, generation);
732
- return;
733
758
  }
734
- this.#noticeIfRunUnfinished(messages);
735
759
  } finally {
736
760
  // The resumed run starting has let go of the hold already, as a send's
737
761
  // does, and a request that then fails still started one: the client
@@ -4,8 +4,8 @@ export const EMBEDDED_STYLES: string = `
4
4
  the widget lives in the host's own layout (fixes overlay/z-index clashes). */
5
5
  :host([placement="embedded"]) {
6
6
  --_position: var(--ag-ui-position, static);
7
- --_width: var(--ag-ui-width, 100%);
8
- --_height: var(--ag-ui-height, 100%);
7
+ --_width: var(--_drag-width, var(--ag-ui-width, 100%));
8
+ --_height: var(--_drag-height, var(--ag-ui-height, 100%));
9
9
  --_max-width: var(--ag-ui-max-width, 100%);
10
10
  --_max-height: var(--ag-ui-max-height, 100%);
11
11
  --_shadow: var(--ag-ui-shadow, none);
@@ -27,6 +27,17 @@ import { surfaceFor } from "./surface_for.js";
27
27
  /** Per-tab persistence key for a dragged panel size. */
28
28
  const SIZE_KEY = "ag-ui-chat:size";
29
29
 
30
+ /**
31
+ * The private properties a dragged size lives in. They are not the public
32
+ * `--ag-ui-width` / `--ag-ui-height` tokens, which belong to the host: an
33
+ * inline write there outranked every host rule and was read back by the
34
+ * small-viewport block, so a size dragged in a wide window leaked into the
35
+ * phone shape. The placement rules read these before the host token; the
36
+ * small-viewport block does not read them at all.
37
+ */
38
+ const DRAG_WIDTH = "--_drag-width";
39
+ const DRAG_HEIGHT = "--_drag-height";
40
+
30
41
  /** Per-tab persistence key for a dragged launcher position. */
31
42
  const LAUNCHER_KEY = "ag-ui-chat:launcher";
32
43
 
@@ -189,6 +200,20 @@ export class PanelPlacement {
189
200
  this.#applySize(this.#readSize());
190
201
  }
191
202
 
203
+ /**
204
+ * Drop the dragged size and its stored copy, so the placement's own size and
205
+ * the host's tokens apply again. For a host entering a sized state of its
206
+ * own, which a user's drag would otherwise outrank.
207
+ */
208
+ resetSize(): void {
209
+ this.#host.element.style.removeProperty(DRAG_WIDTH);
210
+ this.#host.element.style.removeProperty(DRAG_HEIGHT);
211
+ this.#host.clearPreference(SIZE_KEY);
212
+ // The anchor was measured against the dragged box; the resting one may
213
+ // pin different edges.
214
+ this.syncResizeAnchor();
215
+ }
216
+
192
217
  /**
193
218
  * A zero-sized box carrying the host's viewport insets as padding, so they
194
219
  * can be read back as used pixel lengths. See the `.viewport-probe` rule for
@@ -414,8 +439,8 @@ export class PanelPlacement {
414
439
  */
415
440
  #measureAnchor(): ResizeAnchor {
416
441
  const before = this.#host.element.getBoundingClientRect();
417
- const width = this.#host.element.style.getPropertyValue("--ag-ui-width");
418
- const height = this.#host.element.style.getPropertyValue("--ag-ui-height");
442
+ const width = this.#host.element.style.getPropertyValue(DRAG_WIDTH);
443
+ const height = this.#host.element.style.getPropertyValue(DRAG_HEIGHT);
419
444
  // Shrink first. Growing is the obvious probe and cannot answer the question
420
445
  // at a size already resting against max-width or max-height: the box does
421
446
  // not change, no edge moves, and every clamped axis then reads as pinned on
@@ -431,8 +456,8 @@ export class PanelPlacement {
431
456
  const grown = shrunk.x === null || shrunk.y === null ? this.#probeAnchor(before, 1) : shrunk;
432
457
  // Restore exactly what was there, including "nothing" — leaving a probe
433
458
  // value behind would pin a panel that had been sizing itself.
434
- this.#restoreProperty("--ag-ui-width", width);
435
- this.#restoreProperty("--ag-ui-height", height);
459
+ this.#restoreProperty(DRAG_WIDTH, width);
460
+ this.#restoreProperty(DRAG_HEIGHT, height);
436
461
  return {
437
462
  // Neither direction moved it: the axis cannot be resized at all, so the
438
463
  // floating default is the best answer available and is the one the
@@ -497,7 +522,7 @@ export class PanelPlacement {
497
522
  /**
498
523
  * Write a dragged size onto the host, on the axes this placement leaves free.
499
524
  *
500
- * Writing the custom property rather than inline `width` / `height` does not
525
+ * Writing a custom property rather than inline `width` / `height` does not
501
526
  * by itself leave placement in charge: an inline custom property still
502
527
  * outranks a `:host([placement=…])` rule setting the same property, so a
503
528
  * height dragged while floating would cap a docked sidebar asking for
@@ -518,10 +543,10 @@ export class PanelPlacement {
518
543
  // symmetry nobody has asked for. The limit that matters, staying inside
519
544
  // what the host left free, is enforced above.
520
545
  if (size.width !== undefined) {
521
- this.#host.element.style.setProperty("--ag-ui-width", `${size.width}px`);
546
+ this.#host.element.style.setProperty(DRAG_WIDTH, `${size.width}px`);
522
547
  }
523
548
  if (size.height !== undefined && axis === "both") {
524
- this.#host.element.style.setProperty("--ag-ui-height", `${size.height}px`);
549
+ this.#host.element.style.setProperty(DRAG_HEIGHT, `${size.height}px`);
525
550
  }
526
551
  }
527
552
 
@@ -535,10 +560,10 @@ export class PanelPlacement {
535
560
  releaseOwnedAxes(): void {
536
561
  const axis = this.#resizeAxis();
537
562
  if (axis !== "both") {
538
- this.#host.element.style.removeProperty("--ag-ui-height");
563
+ this.#host.element.style.removeProperty(DRAG_HEIGHT);
539
564
  }
540
565
  if (axis === "none") {
541
- this.#host.element.style.removeProperty("--ag-ui-width");
566
+ this.#host.element.style.removeProperty(DRAG_WIDTH);
542
567
  }
543
568
  }
544
569
 
@@ -12,7 +12,7 @@ export const PRESET_STYLES: string = `
12
12
 
13
13
  :host([placement="side"]) {
14
14
  --_inset: var(--ag-ui-inset, calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
15
- --_width: var(--ag-ui-width, 420px);
15
+ --_width: var(--_drag-width, var(--ag-ui-width, 420px));
16
16
  --_height: var(--ag-ui-height, var(--_viewport-height));
17
17
  --_max-height: var(--ag-ui-max-height, var(--_viewport-height));
18
18
  --_panel-radius: var(--ag-ui-panel-radius, 0);
@@ -7,7 +7,7 @@ export const SIDEBAR_STYLES: string = `
7
7
  host-managed push instead. */
8
8
  :host([placement="sidebar"]) {
9
9
  --_inset: var(--ag-ui-inset, calc(var(--_viewport-inset-top) + var(--_keyboard-inset-top)) var(--_viewport-inset-right) var(--_viewport-inset-bottom) auto);
10
- --_width: var(--ag-ui-width, 420px);
10
+ --_width: var(--_drag-width, var(--ag-ui-width, 420px));
11
11
  --_height: var(--ag-ui-height, var(--_viewport-height));
12
12
  --_max-height: var(--ag-ui-max-height, var(--_viewport-height));
13
13
  --_panel-radius: var(--ag-ui-panel-radius, 0);
@@ -17,6 +17,11 @@ export const SMALL_VIEWPORT_STYLES: string = `
17
17
  fine-pointered and small, and conflating the two gets both wrong. What the
18
18
  pointer decides is which controls make sense, further down.
19
19
 
20
+ The size the user dragged is deliberately not read here. The desktop
21
+ placements read it before the host token, and this block restates the sizes
22
+ without it, so a persisted or freshly dragged size cannot change the shape
23
+ of a phone, and a window narrowed after a drag goes full-bleed.
24
+
20
25
  Two placements are left alone. "page" is already this shape. "embedded" sits
21
26
  in a box the host sized and placed, and taking that over would break the app
22
27
  shell it was embedded into -- the host is the only party that knows whether
@@ -197,9 +197,7 @@ export class ToolCallCard {
197
197
 
198
198
  this.element = document.createElement("div");
199
199
  this.element.className = "tool-call";
200
- this.element.setAttribute("part", "tool-card");
201
200
  this.element.setAttribute("data-tool-name", name);
202
- this.element.setAttribute("data-status", TOOL_CALL_STATUS.PENDING);
203
201
  this.element.setAttribute("data-expanded", "false");
204
202
 
205
203
  const head = document.createElement("div");
@@ -221,8 +219,7 @@ export class ToolCallCard {
221
219
 
222
220
  this.#status = document.createElement("span");
223
221
  this.#status.className = "tool-call-status";
224
- this.#status.setAttribute("part", "tool-card-status");
225
- this.#status.textContent = statusLabels(strings)[TOOL_CALL_STATUS.PENDING];
222
+ this.#setStatus(TOOL_CALL_STATUS.PENDING);
226
223
 
227
224
  this.#decision = document.createElement("span");
228
225
  this.#decision.className = "tool-call-decision";
@@ -283,8 +280,7 @@ export class ToolCallCard {
283
280
  if (this.#settled) {
284
281
  return;
285
282
  }
286
- this.element.setAttribute("data-status", status);
287
- this.#status.textContent = statusLabels(this.#strings)[status];
283
+ this.#setStatus(status);
288
284
  }
289
285
 
290
286
  /**
@@ -326,8 +322,7 @@ export class ToolCallCard {
326
322
  return;
327
323
  }
328
324
  this.#settled = true;
329
- this.element.setAttribute("data-status", status);
330
- this.#status.textContent = statusLabels(this.#strings)[status];
325
+ this.#setStatus(status);
331
326
  this.#resultLabel.textContent = resultLabels(this.#strings)[status];
332
327
  this.#renderPayload(
333
328
  this.#resultBody,
@@ -337,6 +332,21 @@ export class ToolCallCard {
337
332
  this.#resultSection.hidden = false;
338
333
  }
339
334
 
335
+ /**
336
+ * Write the status everywhere it is read from, in one place so they cannot
337
+ * disagree: `data-status` for the shadow CSS, the pill's text, and the part
338
+ * lists. `::part()` takes no attribute selector, so the card and its pill also
339
+ * name the status as a part (`tool-card-error`, `tool-card-status-error`), the
340
+ * modifier convention a run notice uses, and a host styles failures alone.
341
+ * Setting the whole list replaces the previous modifier.
342
+ */
343
+ #setStatus(status: ToolCallStatus): void {
344
+ this.element.setAttribute("data-status", status);
345
+ this.element.setAttribute("part", `tool-card tool-card-${status}`);
346
+ this.#status.setAttribute("part", `tool-card-status tool-card-status-${status}`);
347
+ this.#status.textContent = statusLabels(this.#strings)[status];
348
+ }
349
+
340
350
  /**
341
351
  * Fill one body region, offering it to the host formatter first.
342
352
  *
@@ -225,6 +225,9 @@ export class ToolStepGroup {
225
225
  state = "error";
226
226
  }
227
227
  this.element.setAttribute("data-state", state);
228
+ // The state is also a part, because `::part()` takes no attribute selector;
229
+ // the whole list is rewritten so the old modifier goes.
230
+ this.element.setAttribute("part", `tool-group tool-group-${state}`);
228
231
  this.element.setAttribute("data-count", String(total));
229
232
 
230
233
  const only = total === 1 ? steps[0] : undefined;
@@ -300,8 +300,8 @@ export const TOKEN_STYLES: string = `
300
300
  grid/flex layout) to embed it in the page flow instead of floating. */
301
301
  --_position: var(--ag-ui-position, fixed);
302
302
  --_z-index: var(--ag-ui-z-index, 2147483000);
303
- --_width: var(--ag-ui-width, 380px);
304
- --_height: var(--ag-ui-height, 560px);
303
+ --_width: var(--_drag-width, var(--ag-ui-width, 380px));
304
+ --_height: var(--_drag-height, var(--ag-ui-height, 560px));
305
305
  /* The gutter a resting floating panel keeps between itself and the edge of
306
306
  the box the host left free. One number, because the inset below spends it
307
307
  and the cap beneath has to know it was spent. */
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION: string = "0.46.0";
1
+ export const VERSION: string = "0.47.0";