@artooi/ag-ui-web-component 0.22.0 → 0.23.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.
@@ -6,7 +6,18 @@ import type { AttachmentRef } from "../core/attachment.js";
6
6
  * composer tray); a restored bubble re-renders these with no animation.
7
7
  */
8
8
  export declare function renderAttachmentChips(refs: readonly AttachmentRef[]): HTMLDivElement;
9
- /** A coarse type icon for a chip — image, document, or generic file. */
9
+ /**
10
+ * A coarse type mark for a chip — image, PDF, text document, or generic file —
11
+ * as the inline SVG markup of one of the built-in glyphs.
12
+ *
13
+ * Returns markup rather than a character because chips sit beside the
14
+ * composer's SVG send, attach and mic buttons: emoji render at a different
15
+ * optical weight, vary by platform, and take neither the glyph size nor
16
+ * `currentColor`, so the two never matched.
17
+ *
18
+ * The MIME string only selects among author-written constants and is never
19
+ * interpolated into one, so the result is safe to assign as markup.
20
+ */
10
21
  export declare function iconFor(mime: string): string;
11
22
  /** A compact human-readable byte size (e.g. `1.2 MB`). */
12
23
  export declare function formatBytes(bytes: number): string;
@@ -1 +1 @@
1
- {"version":3,"file":"attachment_chips.d.ts","sourceRoot":"","sources":["../../src/ui/attachment_chips.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,SAAS,aAAa,EAAE,GAAG,cAAc,CAQpF;AA4BD,wEAAwE;AACxE,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAW5C;AAED,0DAA0D;AAC1D,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAajD"}
1
+ {"version":3,"file":"attachment_chips.d.ts","sourceRoot":"","sources":["../../src/ui/attachment_chips.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,SAAS,aAAa,EAAE,GAAG,cAAc,CAQpF;AA4BD;;;;;;;;;;;GAWG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAW5C;AAED,0DAA0D;AAC1D,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAajD"}
@@ -1 +1 @@
1
- {"version":3,"file":"attachment_tray.d.ts","sourceRoot":"","sources":["../../src/ui/attachment_tray.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAElE,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAKrE,yDAAyD;AACzD,MAAM,WAAW,oBAAoB;IACnC,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,oFAAoF;IACpF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAC/B,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC;CAC9B;AAcD;;;;;;;;;GASG;AACH,qBAAa,cAAc;;IACzB,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IAMjC,YAAY,MAAM,EAAE,oBAAoB,EAOvC;IAED,gFAAgF;IAChF,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAsBpB;IAED,8DAA8D;IAC9D,SAAS,IAAI,SAAS,aAAa,EAAE,CAQpC;IAED,gFAAgF;IAChF,UAAU,IAAI,OAAO,CAEpB;IAED,+EAA+E;IAC/E,YAAY,IAAI,MAAM,CAErB;IAED,uCAAuC;IACvC,OAAO,IAAI,OAAO,CAEjB;IAED,2EAA2E;IAC3E,UAAU,IAAI,IAAI,CAGjB;IAED,2EAA2E;IAC3E,KAAK,IAAI,IAAI,CAMZ;IAED;;;;OAIG;IACH,OAAO,IAAI,IAAI,CAId;CAoIF"}
1
+ {"version":3,"file":"attachment_tray.d.ts","sourceRoot":"","sources":["../../src/ui/attachment_tray.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAElE,OAAO,EAAsB,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAKrE,yDAAyD;AACzD,MAAM,WAAW,oBAAoB;IACnC,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,oFAAoF;IACpF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IAC/B,6EAA6E;IAC7E,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC;CAC9B;AAcD;;;;;;;;;GASG;AACH,qBAAa,cAAc;;IACzB,qEAAqE;IACrE,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IAMjC,YAAY,MAAM,EAAE,oBAAoB,EAOvC;IAED,gFAAgF;IAChF,GAAG,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAsBpB;IAED,8DAA8D;IAC9D,SAAS,IAAI,SAAS,aAAa,EAAE,CAQpC;IAED,gFAAgF;IAChF,UAAU,IAAI,OAAO,CAEpB;IAED,+EAA+E;IAC/E,YAAY,IAAI,MAAM,CAErB;IAED,uCAAuC;IACvC,OAAO,IAAI,OAAO,CAEjB;IAED,2EAA2E;IAC3E,UAAU,IAAI,IAAI,CAGjB;IAED,2EAA2E;IAC3E,KAAK,IAAI,IAAI,CAMZ;IAED;;;;OAIG;IACH,OAAO,IAAI,IAAI,CAId;CAqIF"}
@@ -1,2 +1,2 @@
1
- export declare const STYLES = "\n/* \u2500\u2500 Token defaults \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Every public --ag-ui-* token is read here into a private --_* alias, and\n only the alias is used by the rules below.\n\n The indirection is what makes ancestor theming work. Declaring the public\n name on :host would set it *on the host element*, and a value on an element\n always beats one inherited from an ancestor \u2014 so a page that put the tokens\n on a wrapper would see no effect at all, which is the one thing the API is\n documented to support. Reading the public name with the default as a var()\n fallback leaves it undeclared on the element, so an ancestor's value is\n inherited normally while a value aimed at the element still wins over it.\n\n Two invariants hold this together:\n 1. No rule outside this file's :host blocks may reference a public name\n directly. A public name read outside these blocks resolves to nothing and\n silently drops its declaration rather than erroring, so the miss shows up\n as a missing colour, not a failure.\n 2. Every --_* alias in use is declared here. Aliases inherit like any custom\n property, so an undeclared one would pick up a same-named property from\n the host page; declaring it on :host shields the shadow tree from that. */\n:host {\n /* Colors */\n --_bg: var(--ag-ui-bg, #ffffff);\n --_fg: var(--ag-ui-fg, #1a1a2e);\n --_accent: var(--ag-ui-accent, #4f46e5);\n --_user-bg: var(--ag-ui-user-bg, #4f46e5);\n --_user-fg: var(--ag-ui-user-fg, #ffffff);\n --_assistant-bg: var(--ag-ui-assistant-bg, #f1f1f6);\n --_hover: var(--ag-ui-hover, #e7e7ee);\n --_input-bg: var(--ag-ui-input-bg, var(--_bg));\n --_tool-bg: var(--ag-ui-tool-bg, var(--_assistant-bg));\n --_tool-fg: var(--ag-ui-tool-fg, var(--_accent));\n --_header-bg: var(--ag-ui-header-bg, var(--_accent));\n --_header-fg: var(--ag-ui-header-fg, #ffffff);\n --_border: var(--ag-ui-border, #e2e2ec);\n --_radius: var(--ag-ui-radius, 12px);\n\n /* Body text and raised chrome. Both are referenced by the code-block copy\n button; neither had a default before, so the declarations reading them\n were dropped and the button fell back to the inherited colour over a\n transparent box. The defaults below restate exactly that, so this is a\n rename with no repaint \u2014 see the note on .code-copy. */\n --_text: var(--ag-ui-text, var(--_fg));\n --_surface: var(--ag-ui-surface, transparent);\n\n /* Status accents for tool-call cards. */\n --_success: var(--ag-ui-success, #15803d);\n --_danger: var(--ag-ui-danger, #b91c1c);\n --_muted: var(--ag-ui-muted, #6b7280);\n\n /* Tool-call status icon glyphs (override to re-theme) + spinner speed.\n The pending state is the animated ring; the settled states use these. */\n --_tool-icon-done: var(--ag-ui-tool-icon-done, \"\u2713\");\n --_tool-icon-error: var(--ag-ui-tool-icon-error, \"\u2715\");\n --_tool-icon-declined: var(--ag-ui-tool-icon-declined, \"\u2298\");\n --_tool-spin-duration: var(--ag-ui-tool-spin-duration, 0.7s);\n\n /* Answer well (opt-in via data-answer-well) \u2014 boxes a whole assistant turn. */\n --_well-bg: var(--ag-ui-well-bg, transparent);\n --_well-border: var(--ag-ui-well-border, var(--_border));\n\n /* Surface \u2014 set --ag-ui-shadow: none for a flush, embedded panel. */\n --_shadow: var(--ag-ui-shadow, 0 12px 32px rgba(20, 20, 50, 0.18));\n --_font: var(--ag-ui-font, inherit);\n --_font-size: var(--ag-ui-font-size, 14px);\n --_code-font: var(--ag-ui-code-font, ui-monospace, \"SF Mono\", Menlo, monospace);\n\n /* Header / launcher icon box. */\n --_icon-size: var(--ag-ui-icon-size, 22px);\n --_icon-radius: var(--ag-ui-icon-radius, 4px);\n\n /* Composer \u2014 one bordered surface holding the field and its tool row. */\n --_composer-radius: var(--ag-ui-composer-radius, 14px);\n --_composer-max-height: var(--ag-ui-composer-max-height, 40vh);\n --_tool-btn-size: var(--ag-ui-tool-btn-size, 30px);\n --_send-size: var(--ag-ui-send-size, 30px);\n --_glyph-size: var(--ag-ui-glyph-size, 18px);\n --_glyph-stroke: var(--ag-ui-glyph-stroke, 1.75);\n\n /* Unread badge on the launcher. */\n --_badge-bg: var(--ag-ui-badge-bg, var(--_danger));\n --_badge-fg: var(--ag-ui-badge-fg, #ffffff);\n --_badge-size: var(--ag-ui-badge-size, 18px);\n --_badge-font-size: var(--ag-ui-badge-font-size, 11px);\n\n /* The floating launcher a collapsed widget shrinks to. */\n --_launcher-size: var(--ag-ui-launcher-size, 56px);\n --_launcher-bg: var(--ag-ui-launcher-bg, var(--_header-bg));\n --_launcher-fg: var(--ag-ui-launcher-fg, var(--_header-fg));\n --_launcher-radius: var(--ag-ui-launcher-radius, 50%);\n --_launcher-icon-size: var(--ag-ui-launcher-icon-size, 26px);\n --_launcher-inset: var(--ag-ui-launcher-inset, auto 0 0 auto);\n\n /* Motion. One duration and two curves drive every collapse, expand and\n slide-over, so the whole widget accelerates and settles as one thing.\n The default curve is a long-tailed ease-out (a fast start that decelerates\n into place); the pop curve overshoots slightly, for something arriving. */\n --_motion: var(--ag-ui-motion, 0.28s);\n --_ease: var(--ag-ui-ease, cubic-bezier(0.32, 0.72, 0, 1));\n --_ease-pop: var(--ag-ui-ease-pop, cubic-bezier(0.34, 1.36, 0.64, 1));\n\n /* Spacing \u2014 the density preset overrides these. */\n --_space: var(--ag-ui-space, 10px);\n --_pad: var(--ag-ui-pad, 16px);\n --_msg-pad: var(--ag-ui-msg-pad, 8px 12px);\n --_msg-radius: var(--ag-ui-msg-radius, 14px);\n\n /* Layout \u2014 override from outside to dock the widget anywhere.\n Set --ag-ui-position: static (and place this element in your own\n grid/flex layout) to embed it in the page flow instead of floating. */\n --_position: var(--ag-ui-position, fixed);\n --_z-index: var(--ag-ui-z-index, 2147483000);\n --_width: var(--ag-ui-width, 380px);\n --_height: var(--ag-ui-height, 560px);\n --_inset: var(--ag-ui-inset, auto 24px 24px auto);\n --_max-width: var(--ag-ui-max-width, calc(100vw - 48px));\n --_max-height: var(--ag-ui-max-height, calc(100vh - 48px));\n /* Reading-column width for placement=\"page\" (full-bleed, centred content). */\n --_content-max-width: var(--ag-ui-content-max-width, 820px);\n /* Slim rail the sidebar placement collapses to. Only that placement reads\n it, but it is declared here so invariant 2 holds for every alias. */\n --_rail-width: var(--ag-ui-rail-width, 52px);\n\n position: var(--_position);\n inset: var(--_inset);\n z-index: var(--_z-index);\n width: var(--_width);\n max-width: var(--_max-width);\n height: var(--_height);\n max-height: var(--_max-height);\n display: flex;\n font-family: var(--_font);\n font-size: var(--_font-size);\n color: var(--_fg);\n}\n\n/* \u2500\u2500 Themes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Themes only re-set the colour variables; layout/spacing are unaffected.\n theme=\"auto\" follows the OS via prefers-color-scheme. */\n:host([theme=\"dark\"]) {\n --_bg: var(--ag-ui-bg, #15151f);\n --_fg: var(--ag-ui-fg, #e8e8f2);\n --_assistant-bg: var(--ag-ui-assistant-bg, #25253a);\n --_hover: var(--ag-ui-hover, #303049);\n --_header-bg: var(--ag-ui-header-bg, #1f1f30);\n --_header-fg: var(--ag-ui-header-fg, #e8e8f2);\n --_border: var(--ag-ui-border, #33334a);\n --_muted: var(--ag-ui-muted, #9aa0b4);\n --_shadow: var(--ag-ui-shadow, 0 12px 32px rgba(0, 0, 0, 0.5));\n}\n\n@media (prefers-color-scheme: dark) {\n :host([theme=\"auto\"]) {\n --_bg: var(--ag-ui-bg, #15151f);\n --_fg: var(--ag-ui-fg, #e8e8f2);\n --_assistant-bg: var(--ag-ui-assistant-bg, #25253a);\n --_hover: var(--ag-ui-hover, #303049);\n --_header-bg: var(--ag-ui-header-bg, #1f1f30);\n --_header-fg: var(--ag-ui-header-fg, #e8e8f2);\n --_border: var(--ag-ui-border, #33334a);\n --_muted: var(--ag-ui-muted, #9aa0b4);\n --_shadow: var(--ag-ui-shadow, 0 12px 32px rgba(0, 0, 0, 0.5));\n }\n}\n\n/* A terminal-flavoured \"code\" theme: dark, monospace, green accent. */\n:host([theme=\"code\"]) {\n --_bg: var(--ag-ui-bg, #0d1117);\n --_fg: var(--ag-ui-fg, #c9d1d9);\n --_accent: var(--ag-ui-accent, #3fb950);\n --_user-bg: var(--ag-ui-user-bg, #238636);\n --_assistant-bg: var(--ag-ui-assistant-bg, #161b22);\n --_hover: var(--ag-ui-hover, #21262d);\n --_header-bg: var(--ag-ui-header-bg, #010409);\n --_header-fg: var(--ag-ui-header-fg, #c9d1d9);\n --_border: var(--ag-ui-border, #30363d);\n --_muted: var(--ag-ui-muted, #8b949e);\n --_font: var(--ag-ui-font, var(--_code-font));\n --_shadow: var(--ag-ui-shadow, 0 12px 32px rgba(0, 0, 0, 0.6));\n}\n\n/* \u2500\u2500 Density \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n:host([density=\"compact\"]) {\n --_font-size: var(--ag-ui-font-size, 13px);\n --_space: var(--ag-ui-space, 6px);\n --_pad: var(--ag-ui-pad, 10px);\n --_msg-pad: var(--ag-ui-msg-pad, 5px 9px);\n --_msg-radius: var(--ag-ui-msg-radius, 10px);\n}\n\n/* \u2500\u2500 Placement presets \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n:host([placement=\"bottom-left\"]) {\n --_inset: var(--ag-ui-inset, auto auto 24px 24px);\n}\n\n:host([placement=\"side\"]) {\n --_inset: var(--ag-ui-inset, 0 0 0 auto);\n --_width: var(--ag-ui-width, 420px);\n --_height: var(--ag-ui-height, 100vh);\n --_max-height: var(--ag-ui-max-height, 100vh);\n --_radius: var(--ag-ui-radius, 0);\n}\n\n:host([placement=\"full\"]) {\n --_inset: var(--ag-ui-inset, 0);\n --_width: var(--ag-ui-width, 100vw);\n --_height: var(--ag-ui-height, 100vh);\n --_max-width: var(--ag-ui-max-width, 100vw);\n --_max-height: var(--ag-ui-max-height, 100vh);\n --_radius: var(--ag-ui-radius, 0);\n}\n\n/* Page: full-bleed background with a centred reading column. Unlike\n \"full\" (edge-to-edge, left-aligned messages) the content sits in a column\n capped at --ag-ui-content-max-width. The column is produced by symmetric auto\n padding on the scroll area + composer (no per-row wrapper), so user pills\n still right-align and the assistant well spans the column. */\n:host([placement=\"page\"]) {\n --_inset: var(--ag-ui-inset, 0);\n --_width: var(--ag-ui-width, 100vw);\n --_height: var(--ag-ui-height, 100vh);\n --_max-width: var(--ag-ui-max-width, 100vw);\n --_max-height: var(--ag-ui-max-height, 100vh);\n --_radius: var(--ag-ui-radius, 0);\n}\n\n:host([placement=\"page\"]) .messages {\n padding-inline: max(var(--_pad), calc((100% - var(--_content-max-width)) / 2));\n}\n\n:host([placement=\"page\"]) .input-row {\n padding-inline: max(12px, calc((100% - var(--_content-max-width)) / 2));\n}\n\n/* The rows between the message list and the composer (skill chips, the\n /-command palette, the missing-placeholder hint, the upload tray) line up\n with the column too \u2014 chips are padding-based, the palette/hint/tray are\n margin-based, so each gets its own inline axis nudged by the same gutter. */\n:host([placement=\"page\"]) .skill-chips,\n:host([placement=\"page\"]) .attachment-tray {\n padding-inline: max(12px, calc((100% - var(--_content-max-width)) / 2));\n}\n\n:host([placement=\"page\"]) .skill-palette,\n:host([placement=\"page\"]) .skill-hint {\n margin-inline: max(12px, calc((100% - var(--_content-max-width)) / 2));\n}\n\n/* In the reading column the assistant well uses the full width; the user\n message stays a right-aligned pill (its default align-self + max-width). */\n:host([placement=\"page\"]) .message--assistant {\n max-width: 100%;\n}\n\n/* Sidebar: a full-height docked panel that slides open/closed and\n collapses to a slim icon rail (not the floating launcher). Docked right by\n default; data-side=\"left\" docks it left. Overlay by default \u2014 set\n --ag-ui-position: static (and place this element in your own layout) for a\n host-managed push instead. */\n:host([placement=\"sidebar\"]) {\n --_inset: var(--ag-ui-inset, 0 0 0 auto);\n --_width: var(--ag-ui-width, 420px);\n --_height: var(--ag-ui-height, 100vh);\n --_max-height: var(--ag-ui-max-height, 100vh);\n --_radius: var(--ag-ui-radius, 0);\n transition: width var(--_motion) var(--_ease);\n}\n\n:host([placement=\"sidebar\"][data-side=\"left\"]) {\n --_inset: var(--ag-ui-inset, 0 auto 0 0);\n}\n\n/* The docked panel is pinned to the edge it docks against rather than filling\n the host as a flex child. Collapsing shrinks the host to the rail width, and\n a flex child would be squashed to 52px on the way out instead of sliding out\n at full width. */\n:host([placement=\"sidebar\"]) .chat {\n position: absolute;\n inset: 0 0 0 auto;\n width: var(--_width);\n transform-origin: right center;\n}\n\n:host([placement=\"sidebar\"][data-side=\"left\"]) .chat {\n inset: 0 auto 0 0;\n transform-origin: left center;\n}\n\n/* Collapsed sidebar: shrink the host to the rail width and slide the panel out\n through the edge it docks against. Higher specificity than the generic\n collapse rules, so it wins regardless of source order. */\n:host([placement=\"sidebar\"][collapsed]) {\n width: var(--_rail-width);\n height: 100vh;\n max-height: 100vh;\n bottom: 0;\n pointer-events: auto;\n}\n\n:host([placement=\"sidebar\"][collapsed]) .chat {\n transform: translateX(100%);\n}\n\n:host([placement=\"sidebar\"][data-side=\"left\"][collapsed]) .chat {\n transform: translateX(-100%);\n}\n\n@media (prefers-reduced-motion: reduce) {\n :host([placement=\"sidebar\"]),\n :host([placement=\"sidebar\"]) .chat {\n transition: none;\n }\n}\n\n/* \u2500\u2500 Launcher \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n What a collapsed widget shrinks to: a round floating button by default, the\n sidebar's slim edge rail under that placement. A sibling of the panel, so it\n survives the panel being hidden, and the only part of a collapsed widget\n that takes pointer events. */\n.launcher {\n display: flex;\n position: absolute;\n inset: var(--_launcher-inset);\n align-items: center;\n justify-content: center;\n width: var(--_launcher-size);\n height: var(--_launcher-size);\n padding: 0;\n border: none;\n border-radius: var(--_launcher-radius);\n background: var(--_launcher-bg);\n color: var(--_launcher-fg);\n box-shadow: var(--_shadow);\n font: inherit;\n cursor: pointer;\n pointer-events: auto;\n opacity: 0;\n transform: scale(0.4);\n visibility: hidden;\n transition:\n opacity var(--_motion) var(--_ease),\n transform var(--_motion) var(--_ease-pop),\n visibility var(--_motion) var(--_ease);\n}\n\n/* The launcher grows out of the corner the panel shrank into.\n It is laid out at rest rather than display:none, which is what lets it\n animate in *and* out: an element that was not rendered has no before-change\n style to transition from, and one whose display flips to none cannot\n transition at all. visibility keeps it unpaintable, untabbable and\n unclickable in between -- the expanded panel's own controls sit under it and\n must stay reachable. */\n:host([collapsed]) .launcher {\n opacity: 1;\n transform: none;\n visibility: visible;\n}\n\n:host([collapsed]) .launcher:hover {\n transform: scale(1.06);\n}\n\n:host([collapsed]) .launcher:active {\n transform: scale(0.94);\n}\n\n.launcher .icon-holder {\n width: var(--_launcher-icon-size);\n height: var(--_launcher-icon-size);\n}\n\n/* The unread badge rides the launcher's top-right corner. The ring in the\n widget's own background is what separates it from the launcher underneath,\n whatever colour the host themes either one. */\n.launcher-badge {\n position: absolute;\n top: -2px;\n right: -2px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n min-width: var(--_badge-size);\n height: var(--_badge-size);\n padding: 0 5px;\n border-radius: 999px;\n background: var(--_badge-bg);\n color: var(--_badge-fg);\n font-size: var(--_badge-font-size);\n font-weight: 600;\n line-height: 1;\n box-shadow: 0 0 0 2px var(--_bg);\n}\n\n.launcher-badge[hidden] {\n display: none;\n}\n\n/* The sidebar collapses to an edge rail instead: full height, square, flush\n against the dock. It slides in with the panel rather than popping. */\n:host([placement=\"sidebar\"][collapsed]) .launcher {\n inset: 0;\n width: auto;\n height: auto;\n align-items: flex-start;\n padding-top: 16px;\n border: 1px solid var(--_border);\n border-radius: 0;\n background: var(--_header-bg);\n color: var(--_header-fg);\n box-shadow: none;\n transform: none;\n}\n\n/* Every transition in this file is timed by --_motion, so collapsing it to a\n frame is the whole reduced-motion story: states still change (and display\n still flips at the end of its discrete transition), nothing travels. */\n@media (prefers-reduced-motion: reduce) {\n :host {\n --_motion: var(--ag-ui-motion, 0.001s);\n }\n}\n\n/* Embedded: drop the floating chrome and the high z-index stacking context so\n the widget lives in the host's own layout (fixes overlay/z-index clashes). */\n:host([placement=\"embedded\"]) {\n --_position: var(--ag-ui-position, static);\n --_width: var(--ag-ui-width, 100%);\n --_height: var(--ag-ui-height, 100%);\n --_max-width: var(--ag-ui-max-width, 100%);\n --_max-height: var(--ag-ui-max-height, 100%);\n --_shadow: var(--ag-ui-shadow, none);\n --_z-index: var(--ag-ui-z-index, auto);\n}\n\n.chat {\n position: relative;\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n background: var(--_bg);\n border: 1px solid var(--_border);\n border-radius: var(--_radius);\n box-shadow: var(--_shadow);\n overflow: hidden;\n}\n\n.header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 10px 14px;\n border-bottom: 1px solid var(--_border);\n background: var(--_header-bg);\n color: var(--_header-fg);\n}\n\n.header-title {\n flex: 1;\n min-width: 0;\n font-weight: 600;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Header / launcher icon holder: a slot, with a data-icon-url <img>\n fallback, sized via --ag-ui-icon-size. */\n.icon-holder {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: var(--_icon-size);\n height: var(--_icon-size);\n line-height: 1;\n}\n\n.icon-img {\n width: 100%;\n height: 100%;\n object-fit: contain;\n border-radius: var(--_icon-radius);\n}\n\n.header-controls {\n display: flex;\n gap: 2px;\n flex: none;\n}\n\n.header-btn {\n border: none;\n background: transparent;\n color: inherit;\n font: inherit;\n line-height: 1;\n padding: 4px 7px;\n border-radius: 6px;\n cursor: pointer;\n opacity: 0.85;\n}\n\n.header-btn:hover {\n opacity: 1;\n background: rgba(255, 255, 255, 0.18);\n}\n\n/* \u2500\u2500 Collapse \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Collapsing shrinks the widget to the round floating launcher: the panel\n scales down toward the launcher's corner and fades out, the launcher pops in\n from that same point. Both halves are transform and opacity only, so the\n morph runs on the compositor and never reflows the host page.\n\n The host box keeps its expanded size. Animating it would animate layout, and\n a dragged --ag-ui-width would then fight the launcher's own size. Nothing\n paints there once the panel is gone, so the box only has to stop swallowing\n clicks: pointer events go to none, and the launcher takes them back.\n\n Two placements collapse to something else and restore what they need:\n \"sidebar\" slides to its rail (below), while \"embedded\" and \"page\" keep the\n original header bar \u2014 embedded is laid out by the host page, where a\n floating circle would escape the layout, and page is a full-screen route\n with no corner to float in. */\n:host([collapsed]) {\n pointer-events: none;\n}\n\n:host([collapsed]) .chat {\n opacity: 0;\n transform: scale(0.94);\n visibility: hidden;\n}\n\n/* visibility is what keeps the panel out of the tab order and the a11y tree at\n rest without display:none killing the transition. It interpolates so that any\n progress below 1 still counts as visible: the panel stays on screen for the\n whole collapse and flips hidden exactly at the end, and on expand it is\n visible from the first frame. */\n.chat {\n transform-origin: bottom right;\n transition:\n opacity var(--_motion) var(--_ease),\n transform var(--_motion) var(--_ease),\n visibility var(--_motion) var(--_ease);\n}\n\n:host([placement=\"bottom-left\"]) .chat {\n transform-origin: bottom left;\n}\n\n/* The two in-flow placements keep the original collapse: hide the body, let\n the host shrink to the header bar. */\n:host([collapsed]:is([placement=\"embedded\"], [placement=\"page\"])) {\n height: auto;\n max-height: none;\n pointer-events: auto;\n}\n\n:host([collapsed]:is([placement=\"embedded\"], [placement=\"page\"])) .chat {\n opacity: 1;\n transform: none;\n visibility: visible;\n}\n\n/* These two keep the header bar, so the launcher must stay out of the way \u2014\n an embedded host is position: static, which would otherwise leave an\n absolutely-positioned circle to escape the layout entirely and land against\n whatever the page happens to position. */\n:host([collapsed]:is([placement=\"embedded\"], [placement=\"page\"])) .launcher {\n visibility: hidden;\n opacity: 0;\n}\n\n:host([collapsed]:is([placement=\"embedded\"], [placement=\"page\"])) .messages,\n:host([collapsed]:is([placement=\"embedded\"], [placement=\"page\"])) .input-row,\n:host([collapsed]:is([placement=\"embedded\"], [placement=\"page\"])) .skill-chips,\n:host([collapsed]:is([placement=\"embedded\"], [placement=\"page\"])) .skill-palette,\n:host([collapsed]:is([placement=\"embedded\"], [placement=\"page\"])) .skill-hint {\n display: none;\n}\n\n.messages {\n flex: 1;\n overflow-y: auto;\n padding: var(--_pad);\n display: flex;\n flex-direction: column;\n gap: var(--_space);\n}\n\n/* Empty-state region (slot): centred while it's the only thing in the\n list, hidden as soon as a message, card, or pending indicator renders. */\n.empty {\n margin: auto;\n text-align: center;\n color: var(--_muted);\n}\n\n.empty[hidden] {\n display: none;\n}\n\n/* \u2500\u2500 Answer group / well \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n One .answer per assistant turn wraps the streamed text, its tool cards,\n and the pending indicator so a whole answer reads (and can be boxed) as one\n unit. A flex column on the message-list gap, stretched to the list width so\n its children keep their own left/right alignment. data-answer-well opts into\n the bordered \"well\"; without it the layout is today's flat stack. */\n.answer {\n display: flex;\n flex-direction: column;\n gap: var(--_space);\n align-self: stretch;\n min-width: 0;\n}\n\n:host([data-answer-well]) .answer {\n padding: var(--_pad);\n background: var(--_well-bg);\n border: 1px solid var(--_well-border);\n border-radius: var(--_radius);\n}\n\n.message {\n max-width: 80%;\n padding: var(--_msg-pad);\n border-radius: var(--_msg-radius);\n line-height: 1.4;\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n/* \u2500\u2500 Incoming-text animations (data-text-animation) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n/* .message--restored (rehydrated history) is excluded: entrance animations are\n for freshly-arriving messages, not the whole transcript replaying on reload.\n Word mode is excluded implicitly \u2014 restored bubbles aren't wrapped. */\n:host([data-text-animation=\"fade\"]) .message--assistant:not(.message--restored) {\n animation: ag-ui-msg-fade 0.25s ease both;\n}\n\n@keyframes ag-ui-msg-fade {\n from { opacity: 0; transform: translateY(4px); }\n to { opacity: 1; transform: none; }\n}\n\n.message--assistant .word {\n animation: ag-ui-word-in 0.3s ease both;\n animation-delay: calc(var(--ag-ui-word-index, 0) * 35ms);\n}\n\n@keyframes ag-ui-word-in {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n :host([data-text-animation=\"fade\"]) .message--assistant,\n .message--assistant .word {\n animation: none;\n }\n}\n\n.message--user {\n align-self: flex-end;\n background: var(--_user-bg);\n color: var(--_user-fg);\n border-bottom-right-radius: 4px;\n}\n\n.message--assistant {\n align-self: flex-start;\n background: var(--_assistant-bg);\n border-bottom-left-radius: 4px;\n /* Assistant bubbles hold rendered markdown/HTML, so collapse the source\n whitespace the renderer leaves between block tags. */\n white-space: normal;\n}\n\n/* Rendered-markdown elements inside an assistant bubble. */\n.message--assistant > :first-child {\n margin-top: 0;\n}\n\n.message--assistant > :last-child {\n margin-bottom: 0;\n}\n\n.message--assistant p,\n.message--assistant ul,\n.message--assistant ol,\n.message--assistant blockquote,\n.message--assistant pre {\n margin: 0.5em 0;\n}\n\n.message--assistant ul,\n.message--assistant ol {\n padding-left: 1.3em;\n}\n\n.message--assistant a {\n color: var(--_accent);\n text-decoration: underline;\n}\n\n.message--assistant code {\n font-family: ui-monospace, \"SF Mono\", Menlo, monospace;\n font-size: 0.92em;\n background: rgba(127, 127, 127, 0.16);\n padding: 1px 4px;\n border-radius: 4px;\n}\n\n.message--assistant pre {\n padding: 8px 10px;\n overflow: auto;\n background: var(--_bg);\n border: 1px solid var(--_border);\n border-radius: 6px;\n}\n\n.message--assistant pre code {\n background: none;\n padding: 0;\n}\n\n/* The copy button sits inside the block, so it scrolls with wide code rather\n than floating over the bubble. Positioning is on the pre; the button only\n appears once one has been attached. */\n.message--assistant pre.has-copy {\n position: relative;\n}\n\n/* This button is the only reader of --ag-ui-surface and --ag-ui-text, and\n neither had a default until the alias layer gave them one. Both references\n used to resolve to nothing, dropping the declaration: the background fell\n back to the initial transparent, and the hover/copied colour to the\n inherited body colour. The defaults chosen upstream (transparent, and the\n body foreground) reproduce that, so the rename repaints nothing. A raised\n surface behind the button is a separate design question. */\n.code-copy {\n position: absolute;\n top: 4px;\n right: 4px;\n padding: 2px 8px;\n font: inherit;\n font-size: 0.75em;\n line-height: 1.6;\n color: var(--_muted);\n background: var(--_surface);\n border: 1px solid var(--_border);\n border-radius: 4px;\n cursor: pointer;\n opacity: 0;\n transition: opacity 0.12s ease;\n}\n\n/* Revealed on hover or keyboard focus. Focus matters as much as hover here:\n hidden-until-hover is invisible to a keyboard user otherwise. */\n.message--assistant pre.has-copy:hover .code-copy,\n.code-copy:focus-visible {\n opacity: 1;\n}\n\n.code-copy:hover {\n color: var(--_text);\n background: var(--_hover);\n}\n\n.code-copy[data-state=\"copied\"] {\n opacity: 1;\n color: var(--_text);\n}\n\n.code-copy[data-state=\"failed\"] {\n opacity: 1;\n color: var(--_danger, var(--_text));\n}\n\n.message--assistant blockquote {\n padding-left: 10px;\n border-left: 3px solid var(--_border);\n color: var(--_muted);\n}\n\n/* Markdown tables. table/thead/tbody/tr/th/td are all in\n the sanitizer's ALLOWED_TAGS, so an agent emitting one renders it \u2014 and until\n now rendered it entirely unstyled, overflowing the bubble. A wide table\n scrolls inside its own box rather than stretching the message: the bubble is\n width-constrained, so without this the columns either crush or push the\n layout sideways. */\n.message--assistant table {\n display: block;\n width: fit-content;\n max-width: 100%;\n overflow-x: auto;\n border-collapse: collapse;\n font-size: 0.95em;\n}\n\n.message--assistant th,\n.message--assistant td {\n padding: 6px 10px;\n border: 1px solid var(--_border);\n text-align: left;\n vertical-align: top;\n}\n\n.message--assistant th {\n background: var(--_hover);\n font-weight: 600;\n}\n\n.pending {\n align-self: flex-start;\n display: inline-flex;\n gap: 4px;\n align-items: center;\n padding: 12px 14px;\n background: var(--_assistant-bg);\n border-radius: 14px;\n border-bottom-left-radius: 4px;\n}\n\n.pending-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--_muted);\n animation: ag-ui-pending 1.2s infinite ease-in-out both;\n}\n\n.pending-dot:nth-child(2) {\n animation-delay: 0.16s;\n}\n\n.pending-dot:nth-child(3) {\n animation-delay: 0.32s;\n}\n\n@keyframes ag-ui-pending {\n 0%, 80%, 100% { opacity: 0.3; transform: translateY(0); }\n 40% { opacity: 1; transform: translateY(-3px); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .pending-dot {\n animation: none;\n opacity: 0.6;\n }\n}\n\n/* \u2500\u2500 Thoughts region \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n A muted, collapsible chain-of-thought at the top of the answer group: open\n while the model reasons, folded once the answer text starts. */\n.thoughts {\n align-self: stretch;\n display: flex;\n flex-direction: column;\n gap: 4px;\n font-size: 12px;\n color: var(--_muted);\n}\n\n.thoughts-toggle {\n align-self: flex-start;\n border: none;\n padding: 0;\n background: none;\n font: inherit;\n font-size: 12px;\n font-weight: 600;\n color: var(--_muted);\n cursor: pointer;\n}\n\n.thoughts-toggle::before {\n content: \"\u25BE \";\n}\n\n.thoughts-toggle[aria-expanded=\"false\"]::before {\n content: \"\u25B8 \";\n}\n\n/* A gentle pulse on the label while reasoning is still streaming. */\n.thoughts[data-streaming] .thoughts-label {\n animation: ag-ui-thoughts-pulse 1.4s ease-in-out infinite;\n}\n\n@keyframes ag-ui-thoughts-pulse {\n 0%, 100% { opacity: 0.55; }\n 50% { opacity: 1; }\n}\n\n.thoughts-body {\n margin: 0;\n padding: 4px 0 4px 10px;\n border-left: 2px solid var(--_border);\n max-height: 220px;\n overflow: auto;\n white-space: pre-wrap;\n word-break: break-word;\n font-family: inherit;\n}\n\n.thoughts-body[hidden] {\n display: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .thoughts[data-streaming] .thoughts-label {\n animation: none;\n }\n}\n\n.tool-call {\n align-self: flex-start;\n max-width: 80%;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n gap: 6px;\n font-size: 12px;\n font-family: ui-monospace, \"SF Mono\", Menlo, monospace;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--_tool-bg);\n border: 1px solid var(--_border);\n color: var(--_tool-fg);\n}\n\n.tool-call-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.tool-call-name {\n flex: 1;\n min-width: 0;\n font-weight: 600;\n word-break: break-word;\n}\n\n/* Leading status icon. Empty in the DOM \u2014 the glyph/spinner is drawn\n here from the card's data-status, so it stays themeable. */\n.tool-call-icon {\n flex: none;\n box-sizing: border-box;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 14px;\n height: 14px;\n font-size: 12px;\n line-height: 1;\n}\n\n/* Pending: a real spinning ring. Speed is tunable; reduced motion stops it. */\n.tool-call[data-status=\"pending\"] .tool-call-icon {\n border: 2px solid var(--_muted);\n border-top-color: transparent;\n border-radius: 50%;\n animation: ag-ui-tool-spin var(--_tool-spin-duration) linear infinite;\n}\n\n@keyframes ag-ui-tool-spin {\n to { transform: rotate(360deg); }\n}\n\n/* Settled: a themeable glyph coloured by outcome. */\n.tool-call[data-status=\"done\"] .tool-call-icon::before {\n content: var(--_tool-icon-done);\n color: var(--_success);\n}\n\n.tool-call[data-status=\"error\"] .tool-call-icon::before {\n content: var(--_tool-icon-error);\n color: var(--_danger);\n}\n\n.tool-call[data-status=\"declined\"] .tool-call-icon::before {\n content: var(--_tool-icon-declined);\n color: var(--_muted);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .tool-call[data-status=\"pending\"] .tool-call-icon {\n animation: none;\n }\n}\n\n/* Inline display mode: the lightest card. Drop the box chrome so the status row\n reads as one line of the answer; the result toggle still expands below it. */\n:host([data-tool-display=\"inline\"]) .tool-call {\n max-width: 100%;\n background: transparent;\n border: none;\n padding: 2px 0;\n gap: 2px;\n}\n\n.tool-call-status {\n flex: none;\n padding: 1px 8px;\n border-radius: 999px;\n font-size: 11px;\n font-weight: 600;\n background: rgba(127, 127, 127, 0.16);\n color: var(--_muted);\n}\n\n.tool-call[data-status=\"done\"] .tool-call-status {\n color: var(--_success);\n}\n\n.tool-call[data-status=\"error\"] .tool-call-status {\n color: var(--_danger);\n}\n\n.tool-call[data-status=\"declined\"] .tool-call-status {\n color: var(--_muted);\n}\n\n.tool-call-body {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.tool-call-section {\n display: flex;\n flex-direction: column;\n gap: 3px;\n}\n\n/* The heading that tells the two payloads apart. Without it the arguments and\n the result were one run of text and a reader had to guess the boundary. */\n.tool-call-section-label {\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n color: var(--_muted);\n}\n\n/* The record of a human decision on a gated call. An approved call used to\n look exactly like one that was never gated. */\n.skill-item-token {\n font-family: ui-monospace, \"SF Mono\", Menlo, monospace;\n font-size: 0.92em;\n color: var(--_accent);\n margin-right: 6px;\n}\n\n.tool-call-decision {\n flex: none;\n font-size: 11px;\n font-style: italic;\n color: var(--_muted);\n}\n\n.tool-call-args,\n.tool-call-result {\n margin: 0;\n padding: 6px 8px;\n max-height: 160px;\n overflow: auto;\n background: var(--_bg);\n border: 1px solid var(--_border);\n border-radius: 6px;\n white-space: pre-wrap;\n word-break: break-word;\n color: var(--_fg);\n}\n\n/* Display modes are pure visibility over one DOM shape, selected from the host\n attribute rather than a value stamped on the card when it was built. That is\n what lets a host flip data-tool-display and have every card already on screen\n re-read it, the way data-answer-well behaves. Baking the structure per mode\n meant the setting only reached cards created afterwards.\n\n Default (no attribute) is the full mode: arguments always visible, result\n behind the toggle. */\n.tool-call[data-expanded=\"false\"] .tool-call-section--result {\n display: none;\n}\n\n/* Compact: one toggle over both regions, so a settled card is a single line\n until asked. */\n:host([data-tool-display=\"compact\"]) .tool-call[data-expanded=\"false\"] .tool-call-section {\n display: none;\n}\n\n/* Inline: the result only; the call's arguments are noise at this density. */\n:host([data-tool-display=\"inline\"]) .tool-call .tool-call-section--args {\n display: none;\n}\n\n/* Minimal: the status row and nothing else, so there is no toggle to press. */\n:host([data-tool-display=\"minimal\"]) .tool-call .tool-call-toggle,\n:host([data-tool-display=\"minimal\"]) .tool-call .tool-call-body {\n display: none;\n}\n\n/* A pending card has no result yet, and in the modes where the arguments are\n hidden too there is nothing behind the toggle. Hide the control rather than\n offer one that expands onto nothing. */\n.tool-call[data-status=\"pending\"] .tool-call-toggle,\n:host([data-tool-display=\"inline\"]) .tool-call[data-status=\"pending\"] .tool-call-toggle {\n display: none;\n}\n\n.tool-call-toggle {\n align-self: flex-start;\n border: none;\n padding: 0;\n background: none;\n font: inherit;\n font-weight: 600;\n color: var(--_accent);\n cursor: pointer;\n}\n\n.tool-call-toggle::before {\n content: \"\u25B8 \";\n}\n\n.tool-call-toggle[aria-expanded=\"true\"]::before {\n content: \"\u25BE \";\n}\n\n/* Resize handle: a corner grip on a floating panel, an edge grip on a docked\n one. Absent entirely where the placement is full-bleed, since there is\n nothing to drag. */\n.resize-handle {\n position: absolute;\n z-index: 2;\n background: transparent;\n touch-action: none;\n}\n\n.resize-handle:focus-visible {\n outline: 2px solid var(--_accent);\n outline-offset: -2px;\n}\n\n/* The grip sits on the corner opposite the panel's anchor, because a resize\n measures from whichever edge is not moving.\n\n Default is floating: pinned bottom-right, so the grip is top-left. */\n.resize-handle {\n top: 0;\n left: 0;\n width: 14px;\n height: 14px;\n cursor: nwse-resize;\n}\n\n/* Placement is only the opening guess, and these two rules are all it is good\n for. Embedded sits in normal flow, pinned top-left, so it grows bottom-right;\n bottom-left is pinned there, so its free corner is top-right. */\n:host([placement=\"embedded\"]) .resize-handle {\n top: auto;\n left: auto;\n right: 0;\n bottom: 0;\n cursor: nwse-resize;\n}\n\n:host([placement=\"bottom-left\"]) .resize-handle {\n left: auto;\n right: 0;\n cursor: nesw-resize;\n}\n\n/* Then the measurement corrects it. Which edges are held still belongs to the\n host's layout rather than to placement -- a floating panel a host right-aligns\n is anchored bottom-left, not bottom-right -- so the element measures the edges\n that stay put and stamps them here as \"<y>-<x>\". Equal specificity to the\n rules above, so source order is what lets the measured value win.\n\n These must not be written as [data-resize-anchor~=\"left\"]: the stamped value\n is one hyphenated token, and ~= matches whitespace-separated words, so such a\n selector cannot ever match. It did not, which left the grip drawn at the\n corner that moves while the cursor pointed along the right diagonal.\n\n Each rule also sets both sides of its axis rather than only the side it\n moves. One that flipped a single way could not undo a placement guess that\n had flipped the other, which put the grip on the anchored corner for an\n embedded panel its host right-aligns. */\n:host([data-resize-anchor$=\"-left\"]) .resize-handle {\n left: auto;\n right: 0;\n}\n\n:host([data-resize-anchor$=\"-right\"]) .resize-handle {\n left: 0;\n right: auto;\n}\n\n:host([data-resize-anchor^=\"top-\"]) .resize-handle {\n top: auto;\n bottom: 0;\n}\n\n:host([data-resize-anchor^=\"bottom-\"]) .resize-handle {\n top: 0;\n bottom: auto;\n}\n\n/* One axis flipped means the drag runs along the other diagonal. */\n:host([data-resize-anchor=\"top-left\"]) .resize-handle,\n:host([data-resize-anchor=\"bottom-right\"]) .resize-handle {\n cursor: nwse-resize;\n}\n\n:host([data-resize-anchor=\"top-right\"]) .resize-handle,\n:host([data-resize-anchor=\"bottom-left\"]) .resize-handle {\n cursor: nesw-resize;\n}\n\n/* Docked: the placement owns the height, so only the inner edge is the user's --\n an edge, not a corner, so this outranks the measured anchor by coming after. */\n:host([placement=\"sidebar\"]) .resize-handle,\n:host([placement=\"side\"]) .resize-handle {\n top: 0;\n bottom: auto;\n width: 8px;\n height: 100%;\n cursor: ew-resize;\n}\n\n/* Docked to the left, so the inner edge is the right-hand one. */\n:host([placement=\"sidebar\"][data-side=\"left\"]) .resize-handle {\n left: auto;\n right: 0;\n}\n\n/* Full-bleed: nothing to drag. */\n:host([placement=\"full\"]) .resize-handle,\n:host([placement=\"page\"]) .resize-handle {\n display: none;\n}\n\n.resize-handle[data-dragging] {\n background: var(--_accent);\n opacity: 0.35;\n}\n\n/* \u2500\u2500 Composer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n One surface owns the border, the background and the focus ring; the field and\n its tool row sit inside it. The row used to be four siblings stretched to the\n textarea's height, which gave a paperclip the same visual weight as the field\n and turned Send into a full-height slab. */\n.input-row {\n display: flex;\n padding: 12px;\n border-top: 1px solid var(--_border);\n}\n\n.composer {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding: 6px 6px 6px 10px;\n background: var(--_input-bg);\n border: 1px solid var(--_border);\n border-radius: var(--_composer-radius);\n transition: border-color var(--_motion) var(--_ease);\n}\n\n.composer:focus-within {\n border-color: var(--_accent);\n}\n\n/* The field grows with its content (sized by #autoGrow) up to the ceiling,\n then scrolls. border-box keeps that measurement stable: with content-box the\n padding would be added to every scrollHeight read and the field would creep\n taller on each keystroke. */\n.input {\n box-sizing: border-box;\n resize: none;\n max-height: var(--_composer-max-height);\n overflow-y: auto;\n padding: 6px 4px 2px;\n background: transparent;\n border: none;\n font: inherit;\n color: inherit;\n outline: none;\n}\n\n.composer-tools {\n display: flex;\n align-items: center;\n gap: 2px;\n}\n\n/* Icon buttons: quiet at rest, so the field is what the eye lands on. */\n.attach-btn,\n.voice-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--_tool-btn-size);\n height: var(--_tool-btn-size);\n padding: 0;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: var(--_muted);\n font: inherit;\n line-height: 1;\n cursor: pointer;\n transition:\n background-color var(--_motion) var(--_ease),\n color var(--_motion) var(--_ease);\n}\n\n.attach-btn:hover,\n.voice-btn:hover {\n background: var(--_hover);\n color: var(--_fg);\n}\n\n.attach-btn:disabled,\n.voice-btn:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n/* Send closes the row on the right: a circle, the only filled control in the\n composer, so \"the thing that acts\" reads at a glance. */\n.send {\n margin-left: auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: var(--_send-size);\n height: var(--_send-size);\n padding: 0;\n border: none;\n border-radius: 50%;\n background: var(--_accent);\n color: #ffffff;\n font: inherit;\n line-height: 1;\n cursor: pointer;\n transition:\n background-color var(--_motion) var(--_ease),\n transform var(--_motion) var(--_ease-pop);\n}\n\n.send:hover {\n transform: scale(1.08);\n}\n\n.send:active {\n transform: scale(0.92);\n}\n\n.send:disabled {\n opacity: 0.5;\n cursor: default;\n transform: none;\n}\n\n/* The composer button doubles as the Stop control while a run is in flight \u2014\n same circle, different glyph, so nothing moves when a run starts. */\n.send[data-state=\"running\"] {\n background: var(--_muted);\n}\n\n.send[data-state=\"idle\"] .send-stop,\n.send[data-state=\"running\"] .send-send {\n display: none;\n}\n\n/* Glyphs \u2014 one class, painted from the button's own colour so every state\n (hover, recording, running) carries the icon with it. */\n.glyph {\n width: var(--_glyph-size);\n height: var(--_glyph-size);\n fill: none;\n stroke: currentColor;\n stroke-width: var(--_glyph-stroke);\n stroke-linecap: round;\n stroke-linejoin: round;\n}\n\n.glyph--solid {\n fill: currentColor;\n stroke: none;\n}\n\n/* In an icon holder (header brand, launcher) the glyph takes the holder's size\n rather than the composer's. */\n.icon-holder .glyph {\n width: 100%;\n height: 100%;\n}\n\n/* \u2500\u2500 File attachments \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n/* The picker button sits in the composer's tool row; hidden until\n data-attachments-url. */\n.attach-input {\n display: none;\n}\n\n/* The mic button's mount point; filled only once #wireVoice mounts the\n control. */\n.voice-slot {\n display: contents;\n}\n\n/* Recording: a red tint + a gentle pulse so it's clearly \"live\". */\n.voice-btn[data-state=\"recording\"] {\n background: var(--_danger);\n color: #ffffff;\n animation: ag-ui-voice-pulse 1.2s ease-in-out infinite;\n}\n\n.voice-btn[data-state=\"recording\"]:hover {\n background: var(--_danger);\n color: #ffffff;\n}\n\n@keyframes ag-ui-voice-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.6; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .voice-btn[data-state=\"recording\"] {\n animation: none;\n }\n}\n\n/* Pending-attachments tray, above the input row; collapses (hidden) when empty. */\n.attachment-slot {\n display: contents;\n}\n\n.attachment-tray {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n padding: 8px 12px 0;\n}\n\n.attachment-chips {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-top: 6px;\n}\n\n.run-notice {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n align-self: flex-start;\n max-width: 100%;\n margin: 2px 0;\n padding: 3px 10px;\n border: 1px dashed var(--_border);\n border-radius: 999px;\n background: transparent;\n color: var(--_muted);\n font-size: 0.8em;\n line-height: 1.4;\n}\n\n.run-notice-icon {\n flex: none;\n opacity: 0.75;\n}\n\n.run-notice-text {\n min-width: 0;\n overflow-wrap: anywhere;\n}\n\n.attachment-chip {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n max-width: 100%;\n padding: 4px 8px;\n border: 1px solid var(--_border);\n border-radius: 999px;\n background: var(--_assistant-bg);\n font-size: 0.85em;\n position: relative;\n}\n\n.attachment-chip--error {\n border-color: var(--_danger);\n color: var(--_danger);\n}\n\n.attachment-chip-name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n max-width: 14ch;\n}\n\n.attachment-chip-size {\n color: var(--_muted);\n white-space: nowrap;\n}\n\n.attachment-chip--error .attachment-chip-size {\n color: var(--_danger);\n}\n\n/* The progress bar fills as the file uploads. */\n.attachment-chip-bar {\n flex-basis: 100%;\n height: 3px;\n border-radius: 2px;\n background: var(--_border);\n overflow: hidden;\n}\n\n.attachment-chip-bar-fill {\n height: 100%;\n background: var(--_accent);\n transition: width 0.15s ease;\n}\n\n.attachment-chip-remove,\n.attachment-chip-retry {\n border: none;\n background: none;\n color: inherit;\n cursor: pointer;\n padding: 0;\n line-height: 1;\n opacity: 0.7;\n}\n\n.attachment-chip-remove:hover,\n.attachment-chip-retry:hover {\n opacity: 1;\n}\n\n/* A subtle outline while a file is dragged over the shell. */\n.chat--dragover {\n outline: 2px dashed var(--_accent);\n outline-offset: -4px;\n}\n\n/* Muted \"\u23F9 Stopped\" line after a cancelled run \u2014 a note, not an error bubble. */\n.stopped-note {\n align-self: flex-start;\n color: var(--_muted);\n font-size: 12px;\n padding: 2px 4px;\n}\n\n/* Inline confirmation card \u2014 lives in the transcript, no focus-stealing overlay. */\n.confirm {\n align-self: stretch;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 12px;\n background: var(--_bg);\n border: 1px solid var(--_accent);\n border-radius: 10px;\n}\n\n.confirm[data-resolved] {\n opacity: 0.7;\n border-color: var(--_border);\n}\n\n.confirm-body {\n font-weight: 600;\n}\n\n.confirm-args {\n margin: 0;\n padding: 8px 10px;\n max-height: 140px;\n overflow: auto;\n font-size: 12px;\n font-family: ui-monospace, \"SF Mono\", Menlo, monospace;\n background: var(--_assistant-bg);\n border-radius: 8px;\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n.confirm-actions {\n display: flex;\n gap: 8px;\n justify-content: flex-end;\n}\n\n.confirm-btn {\n border: 1px solid var(--_border);\n border-radius: 8px;\n padding: 8px 14px;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n background: var(--_bg);\n color: var(--_fg);\n}\n\n.confirm-btn:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n.confirm-btn--confirm {\n border-color: var(--_accent);\n background: var(--_accent);\n color: #ffffff;\n}\n\n/* Approval card \u2014 the server-side-tool gate (approve/deny an interrupt). */\n.approval {\n align-self: stretch;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 12px;\n background: var(--_bg);\n border: 1px solid var(--_accent);\n border-radius: 10px;\n}\n\n.approval[data-resolved] {\n opacity: 0.7;\n border-color: var(--_border);\n}\n\n.approval-body {\n font-weight: 600;\n}\n\n.approval-actions {\n display: flex;\n gap: 8px;\n justify-content: flex-end;\n}\n\n.approval-btn {\n border: 1px solid var(--_border);\n border-radius: 8px;\n padding: 8px 14px;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n background: var(--_bg);\n color: var(--_fg);\n}\n\n.approval-btn:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n.approval-btn--approve {\n border-color: var(--_accent);\n background: var(--_accent);\n color: #ffffff;\n}\n\n/* Question card \u2014 the built-in ask_user prompt (radios and/or free text). */\n.question {\n align-self: stretch;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 12px;\n background: var(--_bg);\n border: 1px solid var(--_accent);\n border-radius: 10px;\n}\n\n.question[data-resolved] {\n opacity: 0.7;\n border-color: var(--_border);\n}\n\n.question-body {\n font-weight: 600;\n}\n\n.question-options {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.question-choice {\n display: flex;\n align-items: center;\n gap: 8px;\n cursor: pointer;\n}\n\n.question-input {\n box-sizing: border-box;\n width: 100%;\n padding: 8px 10px;\n font: inherit;\n color: var(--_fg);\n background: var(--_bg);\n border: 1px solid var(--_border);\n border-radius: 8px;\n}\n\n.question-input:disabled {\n opacity: 0.6;\n}\n\n.question-actions {\n display: flex;\n justify-content: flex-end;\n}\n\n.question-btn {\n border: 1px solid var(--_accent);\n border-radius: 8px;\n padding: 8px 14px;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n background: var(--_accent);\n color: #ffffff;\n}\n\n.question-btn:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n/* Skills \u2014 chips row + the /-command palette, above the input. */\n.skill-chips {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n padding: 10px 12px;\n}\n\n.skill-chip {\n border: 1px solid var(--_border);\n border-radius: 999px;\n padding: 4px 12px;\n font: inherit;\n font-size: 0.9em;\n cursor: pointer;\n background: var(--_assistant-bg);\n color: var(--_fg);\n}\n\n.skill-chip:hover {\n border-color: var(--_accent);\n}\n\n.skill-palette {\n margin: 8px 12px 0;\n display: flex;\n flex-direction: column;\n max-height: 220px;\n overflow: auto;\n background: var(--_bg);\n border: 1px solid var(--_border);\n border-radius: 8px;\n box-shadow: 0 8px 24px rgba(20, 20, 50, 0.16);\n}\n\n.skill-item {\n display: flex;\n flex-direction: column;\n gap: 2px;\n align-items: flex-start;\n padding: 8px 12px;\n border: none;\n background: none;\n font: inherit;\n text-align: left;\n cursor: pointer;\n color: var(--_fg);\n}\n\n.skill-item[aria-selected=\"true\"] {\n background: var(--_assistant-bg);\n}\n\n.skill-item-title {\n font-weight: 600;\n}\n\n.skill-item-desc {\n font-size: 0.85em;\n color: var(--_muted);\n}\n\n/* The hint sits directly above the composer's top border, so a zero bottom\n margin left the text touching the divider. */\n.skill-hint {\n margin: 8px 12px;\n font-size: 0.85em;\n line-height: 1.4;\n color: var(--_danger);\n}\n\n/* Chat-history drawer \u2014 a slide-over within the chat panel.\n The hidden attribute stays the single source of truth for open/closed (no\n JS-driven animation state): display is transitioned discretely, so on close\n the drawer stays displayed for the whole exit and is removed only at the end,\n which is what lets the backdrop fade and the panel slide out. */\n.drawer {\n position: absolute;\n inset: 0;\n z-index: 5;\n display: flex;\n transition: visibility var(--_motion) var(--_ease);\n}\n\n/* Closed. The overlay keeps its box (display, not none) so the backdrop and\n panel inside it stay rendered and can transition both ways; visibility is\n what takes the whole subtree out of the tab order, the a11y tree and hit\n testing at rest, and it holds off until the slide has finished. */\n.drawer[hidden] {\n display: flex;\n visibility: hidden;\n pointer-events: none;\n}\n\n.drawer[hidden] .drawer-backdrop {\n opacity: 0;\n}\n\n.drawer[hidden] .drawer-panel {\n transform: translateX(-100%);\n}\n\n.checkpoints {\n position: absolute;\n inset-block-start: 3rem;\n inset-inline: 0.75rem;\n z-index: 6;\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n padding: 0.5rem;\n border: 1px solid var(--_border);\n border-radius: 0.5rem;\n background: var(--_assistant-bg);\n box-shadow: 0 6px 24px rgb(0 0 0 / 12%);\n max-height: 60%;\n overflow-y: auto;\n transform-origin: top center;\n transition:\n opacity var(--_motion) var(--_ease),\n transform var(--_motion) var(--_ease),\n visibility var(--_motion) var(--_ease);\n}\n\n/* Same idiom as the drawer: laid out at rest, hidden by visibility, so the\n popover can animate open and closed. */\n.checkpoints[hidden] {\n display: flex;\n visibility: hidden;\n pointer-events: none;\n opacity: 0;\n transform: scale(0.96) translateY(-6px);\n}\n\n.checkpoints-title {\n font-size: 0.75rem;\n font-weight: 600;\n opacity: 0.7;\n}\n\n.checkpoints-empty {\n padding: 0.5rem 0.25rem;\n font-size: 0.8125rem;\n opacity: 0.7;\n}\n\n.checkpoint-row {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.25rem;\n border-radius: 0.375rem;\n}\n\n.checkpoint-row:hover {\n background: var(--_hover);\n}\n\n.checkpoint-label {\n flex: 1;\n font-size: 0.8125rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.checkpoint-branch {\n font-size: 0.6875rem;\n padding: 0 0.375rem;\n border-radius: 999px;\n background: var(--_hover);\n opacity: 0.8;\n}\n\n.checkpoint-action {\n font: inherit;\n font-size: 0.75rem;\n cursor: pointer;\n padding: 0.125rem 0.5rem;\n border: 1px solid var(--_border);\n border-radius: 0.375rem;\n background: transparent;\n color: inherit;\n}\n\n.checkpoint-action:hover {\n background: var(--_hover);\n}\n\n.drawer-backdrop {\n position: absolute;\n inset: 0;\n background: rgba(20, 20, 50, 0.32);\n opacity: 1;\n transition: opacity var(--_motion) var(--_ease);\n}\n\n.drawer-panel {\n position: relative;\n display: flex;\n flex-direction: column;\n width: min(300px, 85%);\n height: 100%;\n background: var(--_bg);\n border-right: 1px solid var(--_border);\n box-shadow: var(--_shadow);\n overflow: hidden;\n transform: none;\n transition: transform var(--_motion) var(--_ease);\n}\n\n.drawer-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--_space);\n padding: var(--_pad);\n border-bottom: 1px solid var(--_border);\n}\n\n.drawer-title {\n font-weight: 600;\n}\n\n.drawer-new {\n border: 1px solid var(--_border);\n border-radius: var(--_radius);\n background: var(--_bg);\n color: var(--_accent);\n padding: 4px 10px;\n font: inherit;\n font-size: 0.85em;\n cursor: pointer;\n}\n\n.drawer-list {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n}\n\n.drawer-empty {\n padding: var(--_pad);\n font-size: 0.9em;\n color: var(--_muted);\n}\n\n.drawer-row {\n display: flex;\n align-items: stretch;\n border-bottom: 1px solid var(--_border);\n}\n\n.drawer-row--active {\n background: var(--_assistant-bg);\n}\n\n.drawer-row-select {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 8px 12px;\n border: none;\n background: none;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.drawer-row-title {\n font-weight: 600;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n.drawer-row-time {\n font-size: 0.72em;\n color: var(--_muted);\n}\n\n.drawer-row-preview {\n font-size: 0.8em;\n color: var(--_muted);\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n.drawer-row-actions {\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 0 6px;\n}\n\n.drawer-row-rename,\n.drawer-row-delete {\n border: none;\n background: none;\n color: var(--_muted);\n font-size: 0.9em;\n padding: 4px;\n cursor: pointer;\n}\n\n.drawer-rename-input {\n flex: 1;\n min-width: 0;\n margin: 6px 10px;\n padding: 4px 8px;\n border: 1px solid var(--_accent);\n border-radius: 6px;\n background: var(--_input-bg);\n color: var(--_fg);\n font: inherit;\n}\n\n.drawer-confirm {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n font-size: 0.85em;\n}\n\n.drawer-confirm-label {\n color: var(--_danger);\n}\n\n.drawer-confirm-yes {\n border: none;\n border-radius: 6px;\n background: var(--_danger);\n color: #ffffff;\n padding: 3px 10px;\n font: inherit;\n cursor: pointer;\n}\n\n.drawer-confirm-no {\n border: 1px solid var(--_border);\n border-radius: 6px;\n background: none;\n color: inherit;\n padding: 3px 10px;\n font: inherit;\n cursor: pointer;\n}\n\n/* Embedded placement: an inline, flush side panel rather than a dimmed,\n floating slide-over. */\n:host([placement=\"embedded\"]) .drawer-backdrop {\n background: none;\n}\n\n:host([placement=\"embedded\"]) .drawer-panel {\n width: 100%;\n border-right: none;\n box-shadow: none;\n}\n";
1
+ export declare const STYLES = "\n/* \u2500\u2500 Token defaults \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Every public --ag-ui-* token is read here into a private --_* alias, and\n only the alias is used by the rules below.\n\n The indirection is what makes ancestor theming work. Declaring the public\n name on :host would set it *on the host element*, and a value on an element\n always beats one inherited from an ancestor \u2014 so a page that put the tokens\n on a wrapper would see no effect at all, which is the one thing the API is\n documented to support. Reading the public name with the default as a var()\n fallback leaves it undeclared on the element, so an ancestor's value is\n inherited normally while a value aimed at the element still wins over it.\n\n Two invariants hold this together:\n 1. No rule outside this file's :host blocks may reference a public name\n directly. A public name read outside these blocks resolves to nothing and\n silently drops its declaration rather than erroring, so the miss shows up\n as a missing colour, not a failure.\n 2. Every --_* alias in use is declared here. Aliases inherit like any custom\n property, so an undeclared one would pick up a same-named property from\n the host page; declaring it on :host shields the shadow tree from that. */\n:host {\n /* Colors */\n --_bg: var(--ag-ui-bg, #ffffff);\n --_fg: var(--ag-ui-fg, #1a1a2e);\n --_accent: var(--ag-ui-accent, #4f46e5);\n --_user-bg: var(--ag-ui-user-bg, #4f46e5);\n --_user-fg: var(--ag-ui-user-fg, #ffffff);\n --_assistant-bg: var(--ag-ui-assistant-bg, #f1f1f6);\n --_hover: var(--ag-ui-hover, #e7e7ee);\n --_input-bg: var(--ag-ui-input-bg, var(--_bg));\n --_tool-bg: var(--ag-ui-tool-bg, var(--_assistant-bg));\n --_tool-fg: var(--ag-ui-tool-fg, var(--_accent));\n --_header-bg: var(--ag-ui-header-bg, var(--_accent));\n --_header-fg: var(--ag-ui-header-fg, #ffffff);\n --_border: var(--ag-ui-border, #e2e2ec);\n --_radius: var(--ag-ui-radius, 12px);\n\n /* Body text and raised chrome. Both are referenced by the code-block copy\n button; neither had a default before, so the declarations reading them\n were dropped and the button fell back to the inherited colour over a\n transparent box. The defaults below restate exactly that, so this is a\n rename with no repaint \u2014 see the note on .code-copy. */\n --_text: var(--ag-ui-text, var(--_fg));\n --_surface: var(--ag-ui-surface, transparent);\n\n /* Status accents for tool-call cards. */\n --_success: var(--ag-ui-success, #15803d);\n --_danger: var(--ag-ui-danger, #b91c1c);\n --_muted: var(--ag-ui-muted, #6b7280);\n\n /* Tool-call status icon glyphs (override to re-theme) + spinner speed.\n The pending state is the animated ring; the settled states use these. */\n --_tool-icon-done: var(--ag-ui-tool-icon-done, \"\u2713\");\n --_tool-icon-error: var(--ag-ui-tool-icon-error, \"\u2715\");\n --_tool-icon-declined: var(--ag-ui-tool-icon-declined, \"\u2298\");\n --_tool-spin-duration: var(--ag-ui-tool-spin-duration, 0.7s);\n\n /* Answer well (opt-in via data-answer-well) \u2014 boxes a whole assistant turn. */\n --_well-bg: var(--ag-ui-well-bg, transparent);\n --_well-border: var(--ag-ui-well-border, var(--_border));\n\n /* Surface \u2014 set --ag-ui-shadow: none for a flush, embedded panel. */\n --_shadow: var(--ag-ui-shadow, 0 12px 32px rgba(20, 20, 50, 0.18));\n --_font: var(--ag-ui-font, inherit);\n --_font-size: var(--ag-ui-font-size, 14px);\n --_code-font: var(--ag-ui-code-font, ui-monospace, \"SF Mono\", Menlo, monospace);\n\n /* Header / launcher icon box. */\n --_icon-size: var(--ag-ui-icon-size, 22px);\n --_icon-radius: var(--ag-ui-icon-radius, 4px);\n\n /* Composer \u2014 one bordered surface holding the field and its tool row. */\n --_composer-radius: var(--ag-ui-composer-radius, 14px);\n --_composer-max-height: var(--ag-ui-composer-max-height, 40vh);\n --_tool-btn-size: var(--ag-ui-tool-btn-size, 30px);\n --_send-size: var(--ag-ui-send-size, 30px);\n --_glyph-size: var(--ag-ui-glyph-size, 18px);\n --_glyph-stroke: var(--ag-ui-glyph-stroke, 1.75);\n\n /* Unread badge on the launcher. */\n --_badge-bg: var(--ag-ui-badge-bg, var(--_danger));\n --_badge-fg: var(--ag-ui-badge-fg, #ffffff);\n --_badge-size: var(--ag-ui-badge-size, 18px);\n --_badge-font-size: var(--ag-ui-badge-font-size, 11px);\n\n /* The floating launcher a collapsed widget shrinks to. */\n --_launcher-size: var(--ag-ui-launcher-size, 56px);\n --_launcher-bg: var(--ag-ui-launcher-bg, var(--_header-bg));\n --_launcher-fg: var(--ag-ui-launcher-fg, var(--_header-fg));\n --_launcher-radius: var(--ag-ui-launcher-radius, 50%);\n --_launcher-icon-size: var(--ag-ui-launcher-icon-size, 26px);\n --_launcher-inset: var(--ag-ui-launcher-inset, auto 0 0 auto);\n\n /* Motion. One duration and two curves drive every collapse, expand and\n slide-over, so the whole widget accelerates and settles as one thing.\n The default curve is a long-tailed ease-out (a fast start that decelerates\n into place); the pop curve overshoots slightly, for something arriving. */\n --_motion: var(--ag-ui-motion, 0.28s);\n --_ease: var(--ag-ui-ease, cubic-bezier(0.32, 0.72, 0, 1));\n --_ease-pop: var(--ag-ui-ease-pop, cubic-bezier(0.34, 1.36, 0.64, 1));\n\n /* Spacing \u2014 the density preset overrides these. */\n --_space: var(--ag-ui-space, 10px);\n --_pad: var(--ag-ui-pad, 16px);\n --_msg-pad: var(--ag-ui-msg-pad, 8px 12px);\n --_msg-radius: var(--ag-ui-msg-radius, 14px);\n\n /* Layout \u2014 override from outside to dock the widget anywhere.\n Set --ag-ui-position: static (and place this element in your own\n grid/flex layout) to embed it in the page flow instead of floating. */\n --_position: var(--ag-ui-position, fixed);\n --_z-index: var(--ag-ui-z-index, 2147483000);\n --_width: var(--ag-ui-width, 380px);\n --_height: var(--ag-ui-height, 560px);\n --_inset: var(--ag-ui-inset, auto 24px 24px auto);\n --_max-width: var(--ag-ui-max-width, calc(100vw - 48px));\n --_max-height: var(--ag-ui-max-height, calc(100vh - 48px));\n /* Reading-column width for placement=\"page\" (full-bleed, centred content). */\n --_content-max-width: var(--ag-ui-content-max-width, 820px);\n /* Slim rail the sidebar placement collapses to. Only that placement reads\n it, but it is declared here so invariant 2 holds for every alias. */\n --_rail-width: var(--ag-ui-rail-width, 52px);\n\n position: var(--_position);\n inset: var(--_inset);\n z-index: var(--_z-index);\n width: var(--_width);\n max-width: var(--_max-width);\n height: var(--_height);\n max-height: var(--_max-height);\n display: flex;\n font-family: var(--_font);\n font-size: var(--_font-size);\n color: var(--_fg);\n}\n\n/* \u2500\u2500 Themes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Themes only re-set the colour variables; layout/spacing are unaffected.\n theme=\"auto\" follows the OS via prefers-color-scheme. */\n:host([theme=\"dark\"]) {\n --_bg: var(--ag-ui-bg, #15151f);\n --_fg: var(--ag-ui-fg, #e8e8f2);\n --_assistant-bg: var(--ag-ui-assistant-bg, #25253a);\n --_hover: var(--ag-ui-hover, #303049);\n --_header-bg: var(--ag-ui-header-bg, #1f1f30);\n --_header-fg: var(--ag-ui-header-fg, #e8e8f2);\n --_border: var(--ag-ui-border, #33334a);\n --_muted: var(--ag-ui-muted, #9aa0b4);\n --_shadow: var(--ag-ui-shadow, 0 12px 32px rgba(0, 0, 0, 0.5));\n}\n\n@media (prefers-color-scheme: dark) {\n :host([theme=\"auto\"]) {\n --_bg: var(--ag-ui-bg, #15151f);\n --_fg: var(--ag-ui-fg, #e8e8f2);\n --_assistant-bg: var(--ag-ui-assistant-bg, #25253a);\n --_hover: var(--ag-ui-hover, #303049);\n --_header-bg: var(--ag-ui-header-bg, #1f1f30);\n --_header-fg: var(--ag-ui-header-fg, #e8e8f2);\n --_border: var(--ag-ui-border, #33334a);\n --_muted: var(--ag-ui-muted, #9aa0b4);\n --_shadow: var(--ag-ui-shadow, 0 12px 32px rgba(0, 0, 0, 0.5));\n }\n}\n\n/* A terminal-flavoured \"code\" theme: dark, monospace, green accent. */\n:host([theme=\"code\"]) {\n --_bg: var(--ag-ui-bg, #0d1117);\n --_fg: var(--ag-ui-fg, #c9d1d9);\n --_accent: var(--ag-ui-accent, #3fb950);\n --_user-bg: var(--ag-ui-user-bg, #238636);\n --_assistant-bg: var(--ag-ui-assistant-bg, #161b22);\n --_hover: var(--ag-ui-hover, #21262d);\n --_header-bg: var(--ag-ui-header-bg, #010409);\n --_header-fg: var(--ag-ui-header-fg, #c9d1d9);\n --_border: var(--ag-ui-border, #30363d);\n --_muted: var(--ag-ui-muted, #8b949e);\n --_font: var(--ag-ui-font, var(--_code-font));\n --_shadow: var(--ag-ui-shadow, 0 12px 32px rgba(0, 0, 0, 0.6));\n}\n\n/* \u2500\u2500 Density \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n:host([density=\"compact\"]) {\n --_font-size: var(--ag-ui-font-size, 13px);\n --_space: var(--ag-ui-space, 6px);\n --_pad: var(--ag-ui-pad, 10px);\n --_msg-pad: var(--ag-ui-msg-pad, 5px 9px);\n --_msg-radius: var(--ag-ui-msg-radius, 10px);\n}\n\n/* \u2500\u2500 Placement presets \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n:host([placement=\"bottom-left\"]) {\n --_inset: var(--ag-ui-inset, auto auto 24px 24px);\n}\n\n:host([placement=\"side\"]) {\n --_inset: var(--ag-ui-inset, 0 0 0 auto);\n --_width: var(--ag-ui-width, 420px);\n --_height: var(--ag-ui-height, 100vh);\n --_max-height: var(--ag-ui-max-height, 100vh);\n --_radius: var(--ag-ui-radius, 0);\n}\n\n:host([placement=\"full\"]) {\n --_inset: var(--ag-ui-inset, 0);\n --_width: var(--ag-ui-width, 100vw);\n --_height: var(--ag-ui-height, 100vh);\n --_max-width: var(--ag-ui-max-width, 100vw);\n --_max-height: var(--ag-ui-max-height, 100vh);\n --_radius: var(--ag-ui-radius, 0);\n}\n\n/* Page: full-bleed background with a centred reading column. Unlike\n \"full\" (edge-to-edge, left-aligned messages) the content sits in a column\n capped at --ag-ui-content-max-width. The column is produced by symmetric auto\n padding on the scroll area + composer (no per-row wrapper), so user pills\n still right-align and the assistant well spans the column. */\n:host([placement=\"page\"]) {\n --_inset: var(--ag-ui-inset, 0);\n --_width: var(--ag-ui-width, 100vw);\n --_height: var(--ag-ui-height, 100vh);\n --_max-width: var(--ag-ui-max-width, 100vw);\n --_max-height: var(--ag-ui-max-height, 100vh);\n --_radius: var(--ag-ui-radius, 0);\n}\n\n:host([placement=\"page\"]) .messages {\n padding-inline: max(var(--_pad), calc((100% - var(--_content-max-width)) / 2));\n}\n\n:host([placement=\"page\"]) .input-row {\n padding-inline: max(12px, calc((100% - var(--_content-max-width)) / 2));\n}\n\n/* The rows between the message list and the composer (skill chips, the\n /-command palette, the missing-placeholder hint, the upload tray) line up\n with the column too \u2014 chips are padding-based, the palette/hint/tray are\n margin-based, so each gets its own inline axis nudged by the same gutter. */\n:host([placement=\"page\"]) .skill-chips,\n:host([placement=\"page\"]) .attachment-tray {\n padding-inline: max(12px, calc((100% - var(--_content-max-width)) / 2));\n}\n\n:host([placement=\"page\"]) .skill-palette,\n:host([placement=\"page\"]) .skill-hint {\n margin-inline: max(12px, calc((100% - var(--_content-max-width)) / 2));\n}\n\n/* In the reading column the assistant well uses the full width; the user\n message stays a right-aligned pill (its default align-self + max-width). */\n:host([placement=\"page\"]) .message--assistant {\n max-width: 100%;\n}\n\n/* Sidebar: a full-height docked panel that slides open/closed and\n collapses to a slim icon rail (not the floating launcher). Docked right by\n default; data-side=\"left\" docks it left. Overlay by default \u2014 set\n --ag-ui-position: static (and place this element in your own layout) for a\n host-managed push instead. */\n:host([placement=\"sidebar\"]) {\n --_inset: var(--ag-ui-inset, 0 0 0 auto);\n --_width: var(--ag-ui-width, 420px);\n --_height: var(--ag-ui-height, 100vh);\n --_max-height: var(--ag-ui-max-height, 100vh);\n --_radius: var(--ag-ui-radius, 0);\n transition: width var(--_motion) var(--_ease);\n}\n\n:host([placement=\"sidebar\"][data-side=\"left\"]) {\n --_inset: var(--ag-ui-inset, 0 auto 0 0);\n}\n\n/* The docked panel is pinned to the edge it docks against rather than filling\n the host as a flex child. Collapsing shrinks the host to the rail width, and\n a flex child would be squashed to 52px on the way out instead of sliding out\n at full width. */\n:host([placement=\"sidebar\"]) .chat {\n position: absolute;\n inset: 0 0 0 auto;\n width: var(--_width);\n transform-origin: right center;\n}\n\n:host([placement=\"sidebar\"][data-side=\"left\"]) .chat {\n inset: 0 auto 0 0;\n transform-origin: left center;\n}\n\n/* Collapsed sidebar: shrink the host to the rail width and slide the panel out\n through the edge it docks against. Higher specificity than the generic\n collapse rules, so it wins regardless of source order. */\n:host([placement=\"sidebar\"][collapsed]) {\n width: var(--_rail-width);\n height: 100vh;\n max-height: 100vh;\n bottom: 0;\n pointer-events: auto;\n}\n\n:host([placement=\"sidebar\"][collapsed]) .chat {\n transform: translateX(100%);\n}\n\n:host([placement=\"sidebar\"][data-side=\"left\"][collapsed]) .chat {\n transform: translateX(-100%);\n}\n\n@media (prefers-reduced-motion: reduce) {\n :host([placement=\"sidebar\"]),\n :host([placement=\"sidebar\"]) .chat {\n transition: none;\n }\n}\n\n/* \u2500\u2500 Launcher \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n What a collapsed widget shrinks to: a round floating button by default, the\n sidebar's slim edge rail under that placement. A sibling of the panel, so it\n survives the panel being hidden, and the only part of a collapsed widget\n that takes pointer events. */\n.launcher {\n display: flex;\n position: absolute;\n inset: var(--_launcher-inset);\n align-items: center;\n justify-content: center;\n width: var(--_launcher-size);\n height: var(--_launcher-size);\n padding: 0;\n border: none;\n border-radius: var(--_launcher-radius);\n background: var(--_launcher-bg);\n color: var(--_launcher-fg);\n box-shadow: var(--_shadow);\n font: inherit;\n cursor: pointer;\n pointer-events: auto;\n opacity: 0;\n transform: scale(0.4);\n visibility: hidden;\n transition:\n opacity var(--_motion) var(--_ease),\n transform var(--_motion) var(--_ease-pop),\n visibility var(--_motion) var(--_ease);\n}\n\n/* The launcher grows out of the corner the panel shrank into.\n It is laid out at rest rather than display:none, which is what lets it\n animate in *and* out: an element that was not rendered has no before-change\n style to transition from, and one whose display flips to none cannot\n transition at all. visibility keeps it unpaintable, untabbable and\n unclickable in between -- the expanded panel's own controls sit under it and\n must stay reachable. */\n:host([collapsed]) .launcher {\n opacity: 1;\n transform: none;\n visibility: visible;\n}\n\n:host([collapsed]) .launcher:hover {\n transform: scale(1.06);\n}\n\n:host([collapsed]) .launcher:active {\n transform: scale(0.94);\n}\n\n.launcher .icon-holder {\n width: var(--_launcher-icon-size);\n height: var(--_launcher-icon-size);\n}\n\n/* The unread badge rides the launcher's top-right corner. The ring in the\n widget's own background is what separates it from the launcher underneath,\n whatever colour the host themes either one. */\n.launcher-badge {\n position: absolute;\n top: -2px;\n right: -2px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n min-width: var(--_badge-size);\n height: var(--_badge-size);\n padding: 0 5px;\n border-radius: 999px;\n background: var(--_badge-bg);\n color: var(--_badge-fg);\n font-size: var(--_badge-font-size);\n font-weight: 600;\n line-height: 1;\n box-shadow: 0 0 0 2px var(--_bg);\n}\n\n.launcher-badge[hidden] {\n display: none;\n}\n\n/* The sidebar collapses to an edge rail instead: full height, square, flush\n against the dock. It slides in with the panel rather than popping. */\n:host([placement=\"sidebar\"][collapsed]) .launcher {\n inset: 0;\n width: auto;\n height: auto;\n align-items: flex-start;\n padding-top: 16px;\n border: 1px solid var(--_border);\n border-radius: 0;\n background: var(--_header-bg);\n color: var(--_header-fg);\n box-shadow: none;\n transform: none;\n}\n\n/* Every transition in this file is timed by --_motion, so collapsing it to a\n frame is the whole reduced-motion story: states still change (and display\n still flips at the end of its discrete transition), nothing travels. */\n@media (prefers-reduced-motion: reduce) {\n :host {\n --_motion: var(--ag-ui-motion, 0.001s);\n }\n}\n\n/* Embedded: drop the floating chrome and the high z-index stacking context so\n the widget lives in the host's own layout (fixes overlay/z-index clashes). */\n:host([placement=\"embedded\"]) {\n --_position: var(--ag-ui-position, static);\n --_width: var(--ag-ui-width, 100%);\n --_height: var(--ag-ui-height, 100%);\n --_max-width: var(--ag-ui-max-width, 100%);\n --_max-height: var(--ag-ui-max-height, 100%);\n --_shadow: var(--ag-ui-shadow, none);\n --_z-index: var(--ag-ui-z-index, auto);\n}\n\n.chat {\n position: relative;\n display: flex;\n flex-direction: column;\n flex: 1;\n min-height: 0;\n background: var(--_bg);\n border: 1px solid var(--_border);\n border-radius: var(--_radius);\n box-shadow: var(--_shadow);\n overflow: hidden;\n}\n\n.header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 10px 14px;\n border-bottom: 1px solid var(--_border);\n background: var(--_header-bg);\n color: var(--_header-fg);\n}\n\n.header-title {\n flex: 1;\n min-width: 0;\n font-weight: 600;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* Header / launcher icon holder: a slot, with a data-icon-url <img>\n fallback, sized via --ag-ui-icon-size. */\n.icon-holder {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: var(--_icon-size);\n height: var(--_icon-size);\n line-height: 1;\n}\n\n.icon-img {\n width: 100%;\n height: 100%;\n object-fit: contain;\n border-radius: var(--_icon-radius);\n}\n\n.header-controls {\n display: flex;\n gap: 2px;\n flex: none;\n}\n\n.header-btn {\n border: none;\n background: transparent;\n color: inherit;\n font: inherit;\n line-height: 1;\n padding: 4px 7px;\n border-radius: 6px;\n cursor: pointer;\n opacity: 0.85;\n}\n\n.header-btn:hover {\n opacity: 1;\n background: rgba(255, 255, 255, 0.18);\n}\n\n/* \u2500\u2500 Collapse \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n Collapsing shrinks the widget to the round floating launcher: the panel\n scales down toward the launcher's corner and fades out, the launcher pops in\n from that same point. Both halves are transform and opacity only, so the\n morph runs on the compositor and never reflows the host page.\n\n The host box keeps its expanded size. Animating it would animate layout, and\n a dragged --ag-ui-width would then fight the launcher's own size. Nothing\n paints there once the panel is gone, so the box only has to stop swallowing\n clicks: pointer events go to none, and the launcher takes them back.\n\n Two placements collapse to something else and restore what they need:\n \"sidebar\" slides to its rail (below), while \"embedded\" and \"page\" keep the\n original header bar \u2014 embedded is laid out by the host page, where a\n floating circle would escape the layout, and page is a full-screen route\n with no corner to float in. */\n:host([collapsed]) {\n pointer-events: none;\n}\n\n:host([collapsed]) .chat {\n opacity: 0;\n transform: scale(0.94);\n visibility: hidden;\n}\n\n/* visibility is what keeps the panel out of the tab order and the a11y tree at\n rest without display:none killing the transition. It interpolates so that any\n progress below 1 still counts as visible: the panel stays on screen for the\n whole collapse and flips hidden exactly at the end, and on expand it is\n visible from the first frame. */\n.chat {\n transform-origin: bottom right;\n transition:\n opacity var(--_motion) var(--_ease),\n transform var(--_motion) var(--_ease),\n visibility var(--_motion) var(--_ease);\n}\n\n:host([placement=\"bottom-left\"]) .chat {\n transform-origin: bottom left;\n}\n\n/* The two in-flow placements keep the original collapse: hide the body, let\n the host shrink to the header bar. */\n:host([collapsed]:is([placement=\"embedded\"], [placement=\"page\"])) {\n height: auto;\n max-height: none;\n pointer-events: auto;\n}\n\n:host([collapsed]:is([placement=\"embedded\"], [placement=\"page\"])) .chat {\n opacity: 1;\n transform: none;\n visibility: visible;\n}\n\n/* These two keep the header bar, so the launcher must stay out of the way \u2014\n an embedded host is position: static, which would otherwise leave an\n absolutely-positioned circle to escape the layout entirely and land against\n whatever the page happens to position. */\n:host([collapsed]:is([placement=\"embedded\"], [placement=\"page\"])) .launcher {\n visibility: hidden;\n opacity: 0;\n}\n\n:host([collapsed]:is([placement=\"embedded\"], [placement=\"page\"])) .messages,\n:host([collapsed]:is([placement=\"embedded\"], [placement=\"page\"])) .input-row,\n:host([collapsed]:is([placement=\"embedded\"], [placement=\"page\"])) .skill-chips,\n:host([collapsed]:is([placement=\"embedded\"], [placement=\"page\"])) .skill-palette,\n:host([collapsed]:is([placement=\"embedded\"], [placement=\"page\"])) .skill-hint {\n display: none;\n}\n\n.messages {\n flex: 1;\n overflow-y: auto;\n padding: var(--_pad);\n display: flex;\n flex-direction: column;\n gap: var(--_space);\n}\n\n/* Empty-state region (slot): centred while it's the only thing in the\n list, hidden as soon as a message, card, or pending indicator renders. */\n.empty {\n margin: auto;\n text-align: center;\n color: var(--_muted);\n}\n\n.empty[hidden] {\n display: none;\n}\n\n/* \u2500\u2500 Answer group / well \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n One .answer per assistant turn wraps the streamed text, its tool cards,\n and the pending indicator so a whole answer reads (and can be boxed) as one\n unit. A flex column on the message-list gap, stretched to the list width so\n its children keep their own left/right alignment. data-answer-well opts into\n the bordered \"well\"; without it the layout is today's flat stack. */\n.answer {\n display: flex;\n flex-direction: column;\n gap: var(--_space);\n align-self: stretch;\n min-width: 0;\n}\n\n:host([data-answer-well]) .answer {\n padding: var(--_pad);\n background: var(--_well-bg);\n border: 1px solid var(--_well-border);\n border-radius: var(--_radius);\n}\n\n.message {\n max-width: 80%;\n padding: var(--_msg-pad);\n border-radius: var(--_msg-radius);\n line-height: 1.4;\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n/* \u2500\u2500 Incoming-text animations (data-text-animation) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n/* .message--restored (rehydrated history) is excluded: entrance animations are\n for freshly-arriving messages, not the whole transcript replaying on reload.\n Word mode is excluded implicitly \u2014 restored bubbles aren't wrapped. */\n:host([data-text-animation=\"fade\"]) .message--assistant:not(.message--restored) {\n animation: ag-ui-msg-fade 0.25s ease both;\n}\n\n@keyframes ag-ui-msg-fade {\n from { opacity: 0; transform: translateY(4px); }\n to { opacity: 1; transform: none; }\n}\n\n.message--assistant .word {\n animation: ag-ui-word-in 0.3s ease both;\n animation-delay: calc(var(--ag-ui-word-index, 0) * 35ms);\n}\n\n@keyframes ag-ui-word-in {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n :host([data-text-animation=\"fade\"]) .message--assistant,\n .message--assistant .word {\n animation: none;\n }\n}\n\n.message--user {\n align-self: flex-end;\n background: var(--_user-bg);\n color: var(--_user-fg);\n border-bottom-right-radius: 4px;\n}\n\n.message--assistant {\n align-self: flex-start;\n background: var(--_assistant-bg);\n border-bottom-left-radius: 4px;\n /* Assistant bubbles hold rendered markdown/HTML, so collapse the source\n whitespace the renderer leaves between block tags. */\n white-space: normal;\n}\n\n/* Rendered-markdown elements inside an assistant bubble. */\n.message--assistant > :first-child {\n margin-top: 0;\n}\n\n.message--assistant > :last-child {\n margin-bottom: 0;\n}\n\n.message--assistant p,\n.message--assistant ul,\n.message--assistant ol,\n.message--assistant blockquote,\n.message--assistant pre {\n margin: 0.5em 0;\n}\n\n.message--assistant ul,\n.message--assistant ol {\n padding-left: 1.3em;\n}\n\n.message--assistant a {\n color: var(--_accent);\n text-decoration: underline;\n}\n\n.message--assistant code {\n font-family: ui-monospace, \"SF Mono\", Menlo, monospace;\n font-size: 0.92em;\n background: rgba(127, 127, 127, 0.16);\n padding: 1px 4px;\n border-radius: 4px;\n}\n\n.message--assistant pre {\n padding: 8px 10px;\n overflow: auto;\n background: var(--_bg);\n border: 1px solid var(--_border);\n border-radius: 6px;\n}\n\n.message--assistant pre code {\n background: none;\n padding: 0;\n}\n\n/* The copy button sits inside the block, so it scrolls with wide code rather\n than floating over the bubble. Positioning is on the pre; the button only\n appears once one has been attached. */\n.message--assistant pre.has-copy {\n position: relative;\n}\n\n/* This button is the only reader of --ag-ui-surface and --ag-ui-text, and\n neither had a default until the alias layer gave them one. Both references\n used to resolve to nothing, dropping the declaration: the background fell\n back to the initial transparent, and the hover/copied colour to the\n inherited body colour. The defaults chosen upstream (transparent, and the\n body foreground) reproduce that, so the rename repaints nothing. A raised\n surface behind the button is a separate design question. */\n.code-copy {\n position: absolute;\n top: 4px;\n right: 4px;\n padding: 2px 8px;\n font: inherit;\n font-size: 0.75em;\n line-height: 1.6;\n color: var(--_muted);\n background: var(--_surface);\n border: 1px solid var(--_border);\n border-radius: 4px;\n cursor: pointer;\n opacity: 0;\n transition: opacity 0.12s ease;\n}\n\n/* Revealed on hover or keyboard focus. Focus matters as much as hover here:\n hidden-until-hover is invisible to a keyboard user otherwise. */\n.message--assistant pre.has-copy:hover .code-copy,\n.code-copy:focus-visible {\n opacity: 1;\n}\n\n.code-copy:hover {\n color: var(--_text);\n background: var(--_hover);\n}\n\n.code-copy[data-state=\"copied\"] {\n opacity: 1;\n color: var(--_text);\n}\n\n.code-copy[data-state=\"failed\"] {\n opacity: 1;\n color: var(--_danger, var(--_text));\n}\n\n.message--assistant blockquote {\n padding-left: 10px;\n border-left: 3px solid var(--_border);\n color: var(--_muted);\n}\n\n/* Markdown tables. table/thead/tbody/tr/th/td are all in\n the sanitizer's ALLOWED_TAGS, so an agent emitting one renders it \u2014 and until\n now rendered it entirely unstyled, overflowing the bubble. A wide table\n scrolls inside its own box rather than stretching the message: the bubble is\n width-constrained, so without this the columns either crush or push the\n layout sideways. */\n.message--assistant table {\n display: block;\n width: fit-content;\n max-width: 100%;\n overflow-x: auto;\n border-collapse: collapse;\n font-size: 0.95em;\n}\n\n.message--assistant th,\n.message--assistant td {\n padding: 6px 10px;\n border: 1px solid var(--_border);\n text-align: left;\n vertical-align: top;\n}\n\n.message--assistant th {\n background: var(--_hover);\n font-weight: 600;\n}\n\n.pending {\n align-self: flex-start;\n display: inline-flex;\n gap: 4px;\n align-items: center;\n padding: 12px 14px;\n background: var(--_assistant-bg);\n border-radius: 14px;\n border-bottom-left-radius: 4px;\n}\n\n.pending-dot {\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: var(--_muted);\n animation: ag-ui-pending 1.2s infinite ease-in-out both;\n}\n\n.pending-dot:nth-child(2) {\n animation-delay: 0.16s;\n}\n\n.pending-dot:nth-child(3) {\n animation-delay: 0.32s;\n}\n\n@keyframes ag-ui-pending {\n 0%, 80%, 100% { opacity: 0.3; transform: translateY(0); }\n 40% { opacity: 1; transform: translateY(-3px); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .pending-dot {\n animation: none;\n opacity: 0.6;\n }\n}\n\n/* \u2500\u2500 Thoughts region \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n A muted, collapsible chain-of-thought at the top of the answer group: open\n while the model reasons, folded once the answer text starts. */\n.thoughts {\n align-self: stretch;\n display: flex;\n flex-direction: column;\n gap: 4px;\n font-size: 12px;\n color: var(--_muted);\n}\n\n.thoughts-toggle {\n align-self: flex-start;\n border: none;\n padding: 0;\n background: none;\n font: inherit;\n font-size: 12px;\n font-weight: 600;\n color: var(--_muted);\n cursor: pointer;\n}\n\n.thoughts-toggle::before {\n content: \"\u25BE \";\n}\n\n.thoughts-toggle[aria-expanded=\"false\"]::before {\n content: \"\u25B8 \";\n}\n\n/* A gentle pulse on the label while reasoning is still streaming. */\n.thoughts[data-streaming] .thoughts-label {\n animation: ag-ui-thoughts-pulse 1.4s ease-in-out infinite;\n}\n\n@keyframes ag-ui-thoughts-pulse {\n 0%, 100% { opacity: 0.55; }\n 50% { opacity: 1; }\n}\n\n.thoughts-body {\n margin: 0;\n padding: 4px 0 4px 10px;\n border-left: 2px solid var(--_border);\n max-height: 220px;\n overflow: auto;\n white-space: pre-wrap;\n word-break: break-word;\n font-family: inherit;\n}\n\n.thoughts-body[hidden] {\n display: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .thoughts[data-streaming] .thoughts-label {\n animation: none;\n }\n}\n\n.tool-call {\n align-self: flex-start;\n max-width: 80%;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n gap: 6px;\n font-size: 12px;\n font-family: ui-monospace, \"SF Mono\", Menlo, monospace;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--_tool-bg);\n border: 1px solid var(--_border);\n color: var(--_tool-fg);\n}\n\n.tool-call-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.tool-call-name {\n flex: 1;\n min-width: 0;\n font-weight: 600;\n word-break: break-word;\n}\n\n/* Leading status icon. Empty in the DOM \u2014 the glyph/spinner is drawn\n here from the card's data-status, so it stays themeable. */\n.tool-call-icon {\n flex: none;\n box-sizing: border-box;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 14px;\n height: 14px;\n font-size: 12px;\n line-height: 1;\n}\n\n/* Pending: a real spinning ring. Speed is tunable; reduced motion stops it. */\n.tool-call[data-status=\"pending\"] .tool-call-icon {\n border: 2px solid var(--_muted);\n border-top-color: transparent;\n border-radius: 50%;\n animation: ag-ui-tool-spin var(--_tool-spin-duration) linear infinite;\n}\n\n@keyframes ag-ui-tool-spin {\n to { transform: rotate(360deg); }\n}\n\n/* Settled: a themeable glyph coloured by outcome. */\n.tool-call[data-status=\"done\"] .tool-call-icon::before {\n content: var(--_tool-icon-done);\n color: var(--_success);\n}\n\n.tool-call[data-status=\"error\"] .tool-call-icon::before {\n content: var(--_tool-icon-error);\n color: var(--_danger);\n}\n\n.tool-call[data-status=\"declined\"] .tool-call-icon::before {\n content: var(--_tool-icon-declined);\n color: var(--_muted);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .tool-call[data-status=\"pending\"] .tool-call-icon {\n animation: none;\n }\n}\n\n/* Inline display mode: the lightest card. Drop the box chrome so the status row\n reads as one line of the answer; the result toggle still expands below it. */\n:host([data-tool-display=\"inline\"]) .tool-call {\n max-width: 100%;\n background: transparent;\n border: none;\n padding: 2px 0;\n gap: 2px;\n}\n\n.tool-call-status {\n flex: none;\n padding: 1px 8px;\n border-radius: 999px;\n font-size: 11px;\n font-weight: 600;\n background: rgba(127, 127, 127, 0.16);\n color: var(--_muted);\n}\n\n.tool-call[data-status=\"done\"] .tool-call-status {\n color: var(--_success);\n}\n\n.tool-call[data-status=\"error\"] .tool-call-status {\n color: var(--_danger);\n}\n\n.tool-call[data-status=\"declined\"] .tool-call-status {\n color: var(--_muted);\n}\n\n.tool-call-body {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.tool-call-section {\n display: flex;\n flex-direction: column;\n gap: 3px;\n}\n\n/* The heading that tells the two payloads apart. Without it the arguments and\n the result were one run of text and a reader had to guess the boundary. */\n.tool-call-section-label {\n font-size: 10px;\n font-weight: 700;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n color: var(--_muted);\n}\n\n/* The record of a human decision on a gated call. An approved call used to\n look exactly like one that was never gated. */\n.skill-item-token {\n font-family: ui-monospace, \"SF Mono\", Menlo, monospace;\n font-size: 0.92em;\n color: var(--_accent);\n margin-right: 6px;\n}\n\n.tool-call-decision {\n flex: none;\n font-size: 11px;\n font-style: italic;\n color: var(--_muted);\n}\n\n.tool-call-args,\n.tool-call-result {\n margin: 0;\n padding: 6px 8px;\n max-height: 160px;\n overflow: auto;\n background: var(--_bg);\n border: 1px solid var(--_border);\n border-radius: 6px;\n white-space: pre-wrap;\n word-break: break-word;\n color: var(--_fg);\n}\n\n/* Display modes are pure visibility over one DOM shape, selected from the host\n attribute rather than a value stamped on the card when it was built. That is\n what lets a host flip data-tool-display and have every card already on screen\n re-read it, the way data-answer-well behaves. Baking the structure per mode\n meant the setting only reached cards created afterwards.\n\n Default (no attribute) is the full mode: arguments always visible, result\n behind the toggle. */\n.tool-call[data-expanded=\"false\"] .tool-call-section--result {\n display: none;\n}\n\n/* Compact: one toggle over both regions, so a settled card is a single line\n until asked. */\n:host([data-tool-display=\"compact\"]) .tool-call[data-expanded=\"false\"] .tool-call-section {\n display: none;\n}\n\n/* Inline: the result only; the call's arguments are noise at this density. */\n:host([data-tool-display=\"inline\"]) .tool-call .tool-call-section--args {\n display: none;\n}\n\n/* Minimal: the status row and nothing else, so there is no toggle to press. */\n:host([data-tool-display=\"minimal\"]) .tool-call .tool-call-toggle,\n:host([data-tool-display=\"minimal\"]) .tool-call .tool-call-body {\n display: none;\n}\n\n/* A pending card has no result yet, and in the modes where the arguments are\n hidden too there is nothing behind the toggle. Hide the control rather than\n offer one that expands onto nothing. */\n.tool-call[data-status=\"pending\"] .tool-call-toggle,\n:host([data-tool-display=\"inline\"]) .tool-call[data-status=\"pending\"] .tool-call-toggle {\n display: none;\n}\n\n.tool-call-toggle {\n align-self: flex-start;\n border: none;\n padding: 0;\n background: none;\n font: inherit;\n font-weight: 600;\n color: var(--_accent);\n cursor: pointer;\n}\n\n.tool-call-toggle::before {\n content: \"\u25B8 \";\n}\n\n.tool-call-toggle[aria-expanded=\"true\"]::before {\n content: \"\u25BE \";\n}\n\n/* Resize handle: a corner grip on a floating panel, an edge grip on a docked\n one. Absent entirely where the placement is full-bleed, since there is\n nothing to drag. */\n.resize-handle {\n position: absolute;\n z-index: 2;\n background: transparent;\n touch-action: none;\n}\n\n.resize-handle:focus-visible {\n outline: 2px solid var(--_accent);\n outline-offset: -2px;\n}\n\n/* The grip sits on the corner opposite the panel's anchor, because a resize\n measures from whichever edge is not moving.\n\n Default is floating: pinned bottom-right, so the grip is top-left. */\n.resize-handle {\n top: 0;\n left: 0;\n width: 14px;\n height: 14px;\n cursor: nwse-resize;\n}\n\n/* Placement is only the opening guess, and these two rules are all it is good\n for. Embedded sits in normal flow, pinned top-left, so it grows bottom-right;\n bottom-left is pinned there, so its free corner is top-right. */\n:host([placement=\"embedded\"]) .resize-handle {\n top: auto;\n left: auto;\n right: 0;\n bottom: 0;\n cursor: nwse-resize;\n}\n\n:host([placement=\"bottom-left\"]) .resize-handle {\n left: auto;\n right: 0;\n cursor: nesw-resize;\n}\n\n/* Then the measurement corrects it. Which edges are held still belongs to the\n host's layout rather than to placement -- a floating panel a host right-aligns\n is anchored bottom-left, not bottom-right -- so the element measures the edges\n that stay put and stamps them here as \"<y>-<x>\". Equal specificity to the\n rules above, so source order is what lets the measured value win.\n\n These must not be written as [data-resize-anchor~=\"left\"]: the stamped value\n is one hyphenated token, and ~= matches whitespace-separated words, so such a\n selector cannot ever match. It did not, which left the grip drawn at the\n corner that moves while the cursor pointed along the right diagonal.\n\n Each rule also sets both sides of its axis rather than only the side it\n moves. One that flipped a single way could not undo a placement guess that\n had flipped the other, which put the grip on the anchored corner for an\n embedded panel its host right-aligns. */\n:host([data-resize-anchor$=\"-left\"]) .resize-handle {\n left: auto;\n right: 0;\n}\n\n:host([data-resize-anchor$=\"-right\"]) .resize-handle {\n left: 0;\n right: auto;\n}\n\n:host([data-resize-anchor^=\"top-\"]) .resize-handle {\n top: auto;\n bottom: 0;\n}\n\n:host([data-resize-anchor^=\"bottom-\"]) .resize-handle {\n top: 0;\n bottom: auto;\n}\n\n/* One axis flipped means the drag runs along the other diagonal. */\n:host([data-resize-anchor=\"top-left\"]) .resize-handle,\n:host([data-resize-anchor=\"bottom-right\"]) .resize-handle {\n cursor: nwse-resize;\n}\n\n:host([data-resize-anchor=\"top-right\"]) .resize-handle,\n:host([data-resize-anchor=\"bottom-left\"]) .resize-handle {\n cursor: nesw-resize;\n}\n\n/* Docked: the placement owns the height, so only the inner edge is the user's --\n an edge, not a corner, so this outranks the measured anchor by coming after. */\n:host([placement=\"sidebar\"]) .resize-handle,\n:host([placement=\"side\"]) .resize-handle {\n top: 0;\n bottom: auto;\n width: 8px;\n height: 100%;\n cursor: ew-resize;\n}\n\n/* Docked to the left, so the inner edge is the right-hand one. */\n:host([placement=\"sidebar\"][data-side=\"left\"]) .resize-handle {\n left: auto;\n right: 0;\n}\n\n/* Full-bleed: nothing to drag. */\n:host([placement=\"full\"]) .resize-handle,\n:host([placement=\"page\"]) .resize-handle {\n display: none;\n}\n\n.resize-handle[data-dragging] {\n background: var(--_accent);\n opacity: 0.35;\n}\n\n/* \u2500\u2500 Composer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n One surface owns the border, the background and the focus ring; the field and\n its tool row sit inside it. The row used to be four siblings stretched to the\n textarea's height, which gave a paperclip the same visual weight as the field\n and turned Send into a full-height slab. */\n.input-row {\n display: flex;\n padding: 12px;\n border-top: 1px solid var(--_border);\n}\n\n.composer {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding: 6px 6px 6px 10px;\n background: var(--_input-bg);\n border: 1px solid var(--_border);\n border-radius: var(--_composer-radius);\n transition: border-color var(--_motion) var(--_ease);\n}\n\n.composer:focus-within {\n border-color: var(--_accent);\n}\n\n/* The field grows with its content (sized by #autoGrow) up to the ceiling,\n then scrolls. border-box keeps that measurement stable: with content-box the\n padding would be added to every scrollHeight read and the field would creep\n taller on each keystroke. */\n.input {\n box-sizing: border-box;\n resize: none;\n max-height: var(--_composer-max-height);\n overflow-y: auto;\n padding: 6px 4px 2px;\n background: transparent;\n border: none;\n font: inherit;\n color: inherit;\n outline: none;\n}\n\n.composer-tools {\n display: flex;\n align-items: center;\n gap: 2px;\n}\n\n/* Icon buttons: quiet at rest, so the field is what the eye lands on. */\n.attach-btn,\n.voice-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--_tool-btn-size);\n height: var(--_tool-btn-size);\n padding: 0;\n border: none;\n border-radius: 8px;\n background: transparent;\n color: var(--_muted);\n font: inherit;\n line-height: 1;\n cursor: pointer;\n transition:\n background-color var(--_motion) var(--_ease),\n color var(--_motion) var(--_ease);\n}\n\n.attach-btn:hover,\n.voice-btn:hover {\n background: var(--_hover);\n color: var(--_fg);\n}\n\n.attach-btn:disabled,\n.voice-btn:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n/* Send closes the row on the right: a circle, the only filled control in the\n composer, so \"the thing that acts\" reads at a glance. */\n.send {\n margin-left: auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: var(--_send-size);\n height: var(--_send-size);\n padding: 0;\n border: none;\n border-radius: 50%;\n background: var(--_accent);\n color: #ffffff;\n font: inherit;\n line-height: 1;\n cursor: pointer;\n transition:\n background-color var(--_motion) var(--_ease),\n transform var(--_motion) var(--_ease-pop);\n}\n\n.send:hover {\n transform: scale(1.08);\n}\n\n.send:active {\n transform: scale(0.92);\n}\n\n.send:disabled {\n opacity: 0.5;\n cursor: default;\n transform: none;\n}\n\n/* The composer button doubles as the Stop control while a run is in flight \u2014\n same circle, different glyph, so nothing moves when a run starts. */\n.send[data-state=\"running\"] {\n background: var(--_muted);\n}\n\n.send[data-state=\"idle\"] .send-stop,\n.send[data-state=\"running\"] .send-send {\n display: none;\n}\n\n/* Glyphs \u2014 one class, painted from the button's own colour so every state\n (hover, recording, running) carries the icon with it. */\n.glyph {\n width: var(--_glyph-size);\n height: var(--_glyph-size);\n fill: none;\n stroke: currentColor;\n stroke-width: var(--_glyph-stroke);\n stroke-linecap: round;\n stroke-linejoin: round;\n}\n\n.glyph--solid {\n fill: currentColor;\n stroke: none;\n}\n\n/* In an icon holder (header brand, launcher) the glyph takes the holder's size\n rather than the composer's. */\n.icon-holder .glyph {\n width: 100%;\n height: 100%;\n}\n\n/* \u2500\u2500 File attachments \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n/* The picker button sits in the composer's tool row; hidden until\n data-attachments-url. */\n.attach-input {\n display: none;\n}\n\n/* The mic button's mount point; filled only once #wireVoice mounts the\n control. */\n.voice-slot {\n display: contents;\n}\n\n/* Recording: a red tint + a gentle pulse so it's clearly \"live\". */\n.voice-btn[data-state=\"recording\"] {\n background: var(--_danger);\n color: #ffffff;\n animation: ag-ui-voice-pulse 1.2s ease-in-out infinite;\n}\n\n.voice-btn[data-state=\"recording\"]:hover {\n background: var(--_danger);\n color: #ffffff;\n}\n\n@keyframes ag-ui-voice-pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.6; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .voice-btn[data-state=\"recording\"] {\n animation: none;\n }\n}\n\n/* Pending-attachments tray, above the input row; collapses (hidden) when empty. */\n.attachment-slot {\n display: contents;\n}\n\n/* The padding is the only separation the tray gets: its slot is display:\n contents, so the tray is a direct child of the .chat column, and that column\n sets no gap. The bottom value keeps a chip off the composer's top edge.\n The inline value here is only the default: the page placement overrides\n padding-inline further down to compute its reading-column gutter, and wins\n on specificity whichever way this declaration is written. Read the two\n together before changing either. */\n.attachment-tray {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n padding: 8px 12px;\n}\n\n/* The tray sets the hidden property while empty, and an author display beats\n the UA stylesheet's rule for it, so without this the empty tray keeps\n laying out and its padding is permanent dead space above the composer. */\n.attachment-tray[hidden] {\n display: none;\n}\n\n.attachment-chips {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-top: 6px;\n}\n\n.run-notice {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n align-self: flex-start;\n max-width: 100%;\n margin: 2px 0;\n padding: 3px 10px;\n border: 1px dashed var(--_border);\n border-radius: 999px;\n background: transparent;\n color: var(--_muted);\n font-size: 0.8em;\n line-height: 1.4;\n}\n\n.run-notice-icon {\n flex: none;\n opacity: 0.75;\n}\n\n.run-notice-text {\n min-width: 0;\n overflow-wrap: anywhere;\n}\n\n/* A chip carries its own text colour because it carries its own background.\n The same chip renders in two places with opposite inherited colours: in the\n composer tray it inherits the panel's, but on a sent user bubble it inherits\n the user foreground, which is white on the stock light theme. Against the\n assistant surface underneath that was a 1.13:1 filename \u2014 invisible, and only\n the size stayed legible because it sets its own muted colour. Pair the colour\n with the background it belongs to, and both placements read the same.\n Overridden below for an errored chip, which must keep its red. */\n.attachment-chip {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n /* Without this the 100% below caps the content box, so the chip still\n overflows its container by its own padding and border. Invisible while a\n character cap kept names short; routine once the container is what bounds\n the name. */\n box-sizing: border-box;\n max-width: 100%;\n padding: 4px 8px;\n border: 1px solid var(--_border);\n border-radius: 999px;\n background: var(--_assistant-bg);\n color: var(--_text);\n font-size: 0.85em;\n position: relative;\n}\n\n.attachment-chip--error {\n border-color: var(--_danger);\n color: var(--_danger);\n}\n\n/* The type mark. Painted from currentColor, so it carries the chip's state\n with it and turns red along with an errored one; muted by opacity rather\n than a colour, which is what keeps that true. Sized from the chip's own text\n rather than --ag-ui-glyph-size, the way an icon holder's glyph takes the\n holder's size: the composer's 18px buttons would make a chip button-height. */\n.attachment-chip-icon {\n display: inline-flex;\n flex: none;\n opacity: 0.75;\n}\n\n.attachment-chip-icon .glyph {\n width: 1.25em;\n height: 1.25em;\n}\n\n/* No character cap: the chip is already max-width 100%, so its container is\n what bounds the name, and a fixed cap only truncated names the chip had room\n for. min-width: 0 is what lets the flex item shrink past its content, so the\n ellipsis appears at the container edge instead of the chip overflowing. */\n.attachment-chip-name {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n min-width: 0;\n}\n\n.attachment-chip-size {\n color: var(--_muted);\n white-space: nowrap;\n}\n\n.attachment-chip--error .attachment-chip-size {\n color: var(--_danger);\n}\n\n/* The progress bar fills as the file uploads. */\n.attachment-chip-bar {\n flex-basis: 100%;\n height: 3px;\n border-radius: 2px;\n background: var(--_border);\n overflow: hidden;\n}\n\n.attachment-chip-bar-fill {\n height: 100%;\n background: var(--_accent);\n transition: width 0.15s ease;\n}\n\n.attachment-chip-remove,\n.attachment-chip-retry {\n border: none;\n background: none;\n color: inherit;\n cursor: pointer;\n padding: 0;\n line-height: 1;\n opacity: 0.7;\n}\n\n.attachment-chip-remove:hover,\n.attachment-chip-retry:hover {\n opacity: 1;\n}\n\n/* A subtle outline while a file is dragged over the shell. */\n.chat--dragover {\n outline: 2px dashed var(--_accent);\n outline-offset: -4px;\n}\n\n/* Muted \"\u23F9 Stopped\" line after a cancelled run \u2014 a note, not an error bubble. */\n.stopped-note {\n align-self: flex-start;\n color: var(--_muted);\n font-size: 12px;\n padding: 2px 4px;\n}\n\n/* Inline confirmation card \u2014 lives in the transcript, no focus-stealing overlay. */\n.confirm {\n align-self: stretch;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 12px;\n background: var(--_bg);\n border: 1px solid var(--_accent);\n border-radius: 10px;\n}\n\n.confirm[data-resolved] {\n opacity: 0.7;\n border-color: var(--_border);\n}\n\n.confirm-body {\n font-weight: 600;\n}\n\n.confirm-args {\n margin: 0;\n padding: 8px 10px;\n max-height: 140px;\n overflow: auto;\n font-size: 12px;\n font-family: ui-monospace, \"SF Mono\", Menlo, monospace;\n background: var(--_assistant-bg);\n border-radius: 8px;\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n.confirm-actions {\n display: flex;\n gap: 8px;\n justify-content: flex-end;\n}\n\n.confirm-btn {\n border: 1px solid var(--_border);\n border-radius: 8px;\n padding: 8px 14px;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n background: var(--_bg);\n color: var(--_fg);\n}\n\n.confirm-btn:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n.confirm-btn--confirm {\n border-color: var(--_accent);\n background: var(--_accent);\n color: #ffffff;\n}\n\n/* Approval card \u2014 the server-side-tool gate (approve/deny an interrupt). */\n.approval {\n align-self: stretch;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 12px;\n background: var(--_bg);\n border: 1px solid var(--_accent);\n border-radius: 10px;\n}\n\n.approval[data-resolved] {\n opacity: 0.7;\n border-color: var(--_border);\n}\n\n.approval-body {\n font-weight: 600;\n}\n\n.approval-actions {\n display: flex;\n gap: 8px;\n justify-content: flex-end;\n}\n\n.approval-btn {\n border: 1px solid var(--_border);\n border-radius: 8px;\n padding: 8px 14px;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n background: var(--_bg);\n color: var(--_fg);\n}\n\n.approval-btn:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n.approval-btn--approve {\n border-color: var(--_accent);\n background: var(--_accent);\n color: #ffffff;\n}\n\n/* Question card \u2014 the built-in ask_user prompt (radios and/or free text). */\n.question {\n align-self: stretch;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 12px;\n background: var(--_bg);\n border: 1px solid var(--_accent);\n border-radius: 10px;\n}\n\n.question[data-resolved] {\n opacity: 0.7;\n border-color: var(--_border);\n}\n\n.question-body {\n font-weight: 600;\n}\n\n.question-options {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.question-choice {\n display: flex;\n align-items: center;\n gap: 8px;\n cursor: pointer;\n}\n\n.question-input {\n box-sizing: border-box;\n width: 100%;\n padding: 8px 10px;\n font: inherit;\n color: var(--_fg);\n background: var(--_bg);\n border: 1px solid var(--_border);\n border-radius: 8px;\n}\n\n.question-input:disabled {\n opacity: 0.6;\n}\n\n.question-actions {\n display: flex;\n justify-content: flex-end;\n}\n\n.question-btn {\n border: 1px solid var(--_accent);\n border-radius: 8px;\n padding: 8px 14px;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n background: var(--_accent);\n color: #ffffff;\n}\n\n.question-btn:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n/* Skills \u2014 chips row + the /-command palette, above the input. */\n.skill-chips {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n padding: 10px 12px;\n}\n\n.skill-chip {\n border: 1px solid var(--_border);\n border-radius: 999px;\n padding: 4px 12px;\n font: inherit;\n font-size: 0.9em;\n cursor: pointer;\n background: var(--_assistant-bg);\n color: var(--_fg);\n}\n\n.skill-chip:hover {\n border-color: var(--_accent);\n}\n\n.skill-palette {\n margin: 8px 12px 0;\n display: flex;\n flex-direction: column;\n max-height: 220px;\n overflow: auto;\n background: var(--_bg);\n border: 1px solid var(--_border);\n border-radius: 8px;\n box-shadow: 0 8px 24px rgba(20, 20, 50, 0.16);\n}\n\n.skill-item {\n display: flex;\n flex-direction: column;\n gap: 2px;\n align-items: flex-start;\n padding: 8px 12px;\n border: none;\n background: none;\n font: inherit;\n text-align: left;\n cursor: pointer;\n color: var(--_fg);\n}\n\n.skill-item[aria-selected=\"true\"] {\n background: var(--_assistant-bg);\n}\n\n.skill-item-title {\n font-weight: 600;\n}\n\n.skill-item-desc {\n font-size: 0.85em;\n color: var(--_muted);\n}\n\n/* The hint sits directly above the composer's top border, so a zero bottom\n margin left the text touching the divider. */\n.skill-hint {\n margin: 8px 12px;\n font-size: 0.85em;\n line-height: 1.4;\n color: var(--_danger);\n}\n\n/* Chat-history drawer \u2014 a slide-over within the chat panel.\n The hidden attribute stays the single source of truth for open/closed (no\n JS-driven animation state): display is transitioned discretely, so on close\n the drawer stays displayed for the whole exit and is removed only at the end,\n which is what lets the backdrop fade and the panel slide out. */\n.drawer {\n position: absolute;\n inset: 0;\n z-index: 5;\n display: flex;\n transition: visibility var(--_motion) var(--_ease);\n}\n\n/* Closed. The overlay keeps its box (display, not none) so the backdrop and\n panel inside it stay rendered and can transition both ways; visibility is\n what takes the whole subtree out of the tab order, the a11y tree and hit\n testing at rest, and it holds off until the slide has finished. */\n.drawer[hidden] {\n display: flex;\n visibility: hidden;\n pointer-events: none;\n}\n\n.drawer[hidden] .drawer-backdrop {\n opacity: 0;\n}\n\n.drawer[hidden] .drawer-panel {\n transform: translateX(-100%);\n}\n\n.checkpoints {\n position: absolute;\n inset-block-start: 3rem;\n inset-inline: 0.75rem;\n z-index: 6;\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n padding: 0.5rem;\n border: 1px solid var(--_border);\n border-radius: 0.5rem;\n background: var(--_assistant-bg);\n box-shadow: 0 6px 24px rgb(0 0 0 / 12%);\n max-height: 60%;\n overflow-y: auto;\n transform-origin: top center;\n transition:\n opacity var(--_motion) var(--_ease),\n transform var(--_motion) var(--_ease),\n visibility var(--_motion) var(--_ease);\n}\n\n/* Same idiom as the drawer: laid out at rest, hidden by visibility, so the\n popover can animate open and closed. */\n.checkpoints[hidden] {\n display: flex;\n visibility: hidden;\n pointer-events: none;\n opacity: 0;\n transform: scale(0.96) translateY(-6px);\n}\n\n.checkpoints-title {\n font-size: 0.75rem;\n font-weight: 600;\n opacity: 0.7;\n}\n\n.checkpoints-empty {\n padding: 0.5rem 0.25rem;\n font-size: 0.8125rem;\n opacity: 0.7;\n}\n\n.checkpoint-row {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.25rem;\n border-radius: 0.375rem;\n}\n\n.checkpoint-row:hover {\n background: var(--_hover);\n}\n\n.checkpoint-label {\n flex: 1;\n font-size: 0.8125rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.checkpoint-branch {\n font-size: 0.6875rem;\n padding: 0 0.375rem;\n border-radius: 999px;\n background: var(--_hover);\n opacity: 0.8;\n}\n\n.checkpoint-action {\n font: inherit;\n font-size: 0.75rem;\n cursor: pointer;\n padding: 0.125rem 0.5rem;\n border: 1px solid var(--_border);\n border-radius: 0.375rem;\n background: transparent;\n color: inherit;\n}\n\n.checkpoint-action:hover {\n background: var(--_hover);\n}\n\n.drawer-backdrop {\n position: absolute;\n inset: 0;\n background: rgba(20, 20, 50, 0.32);\n opacity: 1;\n transition: opacity var(--_motion) var(--_ease);\n}\n\n.drawer-panel {\n position: relative;\n display: flex;\n flex-direction: column;\n width: min(300px, 85%);\n height: 100%;\n background: var(--_bg);\n border-right: 1px solid var(--_border);\n box-shadow: var(--_shadow);\n overflow: hidden;\n transform: none;\n transition: transform var(--_motion) var(--_ease);\n}\n\n.drawer-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--_space);\n padding: var(--_pad);\n border-bottom: 1px solid var(--_border);\n}\n\n.drawer-title {\n font-weight: 600;\n}\n\n.drawer-new {\n border: 1px solid var(--_border);\n border-radius: var(--_radius);\n background: var(--_bg);\n color: var(--_accent);\n padding: 4px 10px;\n font: inherit;\n font-size: 0.85em;\n cursor: pointer;\n}\n\n.drawer-list {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n}\n\n.drawer-empty {\n padding: var(--_pad);\n font-size: 0.9em;\n color: var(--_muted);\n}\n\n.drawer-row {\n display: flex;\n align-items: stretch;\n border-bottom: 1px solid var(--_border);\n}\n\n.drawer-row--active {\n background: var(--_assistant-bg);\n}\n\n.drawer-row-select {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 8px 12px;\n border: none;\n background: none;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.drawer-row-title {\n font-weight: 600;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n.drawer-row-time {\n font-size: 0.72em;\n color: var(--_muted);\n}\n\n.drawer-row-preview {\n font-size: 0.8em;\n color: var(--_muted);\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n.drawer-row-actions {\n display: flex;\n align-items: center;\n gap: 2px;\n padding: 0 6px;\n}\n\n.drawer-row-rename,\n.drawer-row-delete {\n border: none;\n background: none;\n color: var(--_muted);\n font-size: 0.9em;\n padding: 4px;\n cursor: pointer;\n}\n\n.drawer-rename-input {\n flex: 1;\n min-width: 0;\n margin: 6px 10px;\n padding: 4px 8px;\n border: 1px solid var(--_accent);\n border-radius: 6px;\n background: var(--_input-bg);\n color: var(--_fg);\n font: inherit;\n}\n\n.drawer-confirm {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n font-size: 0.85em;\n}\n\n.drawer-confirm-label {\n color: var(--_danger);\n}\n\n.drawer-confirm-yes {\n border: none;\n border-radius: 6px;\n background: var(--_danger);\n color: #ffffff;\n padding: 3px 10px;\n font: inherit;\n cursor: pointer;\n}\n\n.drawer-confirm-no {\n border: 1px solid var(--_border);\n border-radius: 6px;\n background: none;\n color: inherit;\n padding: 3px 10px;\n font: inherit;\n cursor: pointer;\n}\n\n/* Embedded placement: an inline, flush side panel rather than a dimmed,\n floating slide-over. */\n:host([placement=\"embedded\"]) .drawer-backdrop {\n background: none;\n}\n\n:host([placement=\"embedded\"]) .drawer-panel {\n width: 100%;\n border-right: none;\n box-shadow: none;\n}\n";
2
2
  //# sourceMappingURL=styles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/ui/styles.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM,gz2DAukElB,CAAC"}
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/ui/styles.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM,407DAunElB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artooi/ag-ui-web-component",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Framework-free <ag-ui-chat> Web Component over the AG-UI protocol. Drop-in chat sidebar with a pluggable client-side tool registry, DOM driver primitives, animations, and destructive-action confirmation modal.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/constants.ts CHANGED
@@ -193,3 +193,23 @@ export const ICON_VOICE = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="t
193
193
 
194
194
  /** The default launcher mark (a speech bubble), shown when the host slots none. */
195
195
  export const ICON_LAUNCHER = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 5.5A2.5 2.5 0 0 1 6.5 3h11A2.5 2.5 0 0 1 20 5.5v8a2.5 2.5 0 0 1-2.5 2.5H9l-5 4z"/></svg>`;
196
+
197
+ /**
198
+ * The attachment-chip type marks, one per coarse file family. Same contract as
199
+ * the chrome's glyphs above: static author-written markup on a 24x24 viewBox
200
+ * carrying the shared `glyph` class. A chip picks one by MIME family through
201
+ * `iconFor`; the MIME string selects a constant and is never interpolated into
202
+ * one, so nothing user- or server-supplied reaches the markup.
203
+ *
204
+ * The generic mark, for a family with nothing more specific to say.
205
+ */
206
+ export const ICON_FILE = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/></svg>`;
207
+
208
+ /** An image attachment: a framed picture with a horizon and a sun. */
209
+ export const ICON_FILE_IMAGE = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"><rect x="3.5" y="4.5" width="17" height="15" rx="2.5"/><circle cx="9" cy="10" r="1.5"/><path d="M4.5 17.5 9 13.5l3.5 3 3-2.5 4.5 4"/></svg>`;
210
+
211
+ /** A PDF: the generic page, banded to read as a labelled document. */
212
+ export const ICON_FILE_PDF = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/><rect class="glyph--solid" x="7.5" y="13.5" width="9" height="4.5" rx="1"/></svg>`;
213
+
214
+ /** A text document: the generic page, ruled. */
215
+ export const ICON_FILE_TEXT = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden="true"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/><path d="M8.5 13.5h7M8.5 17h4.5"/></svg>`;
@@ -277,14 +277,17 @@ export class AgUiChat extends HTMLElement {
277
277
  ];
278
278
 
279
279
  /**
280
- * Per-run context provider. Defaults to the compact page map (when a
281
- * {@link getPageMap} provider is set and {@link autoInjectPageMap} is on)
282
- * plus a one-line manifest of the files attached to the message being sent,
283
- * so the agent knows which `read_attachment` ids are available.
280
+ * Per-run context provider. Defaults to the compact page map, when a
281
+ * {@link getPageMap} provider is set and {@link autoInjectPageMap} is on.
282
+ *
283
+ * It used to append a one-line manifest of the message's attachments too.
284
+ * The server now derives that manifest from the refs riding the messages, so
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.
284
288
  */
285
289
  getContext: () => Context[] = () => [
286
290
  ...createPageMapContext(this.getPageMap, this.autoInjectPageMap),
287
- ...this.#attachmentContext(),
288
291
  ];
289
292
 
290
293
  /**
@@ -431,9 +434,6 @@ export class AgUiChat extends HTMLElement {
431
434
  /** Whether the element is currently in the DOM; gates the connect-time warning. */
432
435
  #connected = false;
433
436
 
434
- /** Refs attached to the message currently being sent (the context manifest). */
435
- #runAttachments: readonly AttachmentRef[] = [];
436
-
437
437
  #client: AgUiClient | null = null;
438
438
  // Seed for the next client. Once one exists it owns the live value (the
439
439
  // agent applies STATE_SNAPSHOT / STATE_DELTA into it), so this is only the
@@ -1242,22 +1242,6 @@ export class AgUiChat extends HTMLElement {
1242
1242
  });
1243
1243
  }
1244
1244
 
1245
- /** The one-line manifest of the message's attachments, for the run context. */
1246
- #attachmentContext(): Context[] {
1247
- if (this.#runAttachments.length === 0) {
1248
- return [];
1249
- }
1250
- const lines = this.#runAttachments.map(
1251
- (ref) => `- ${ref.name} (id: ${ref.id}, ${ref.mime || "unknown type"}, ${ref.size} bytes)`,
1252
- );
1253
- return [
1254
- {
1255
- description: "Files the user attached to this message",
1256
- value: `${lines.join("\n")}\nUse the read_attachment tool with an id to read a file's contents.`,
1257
- },
1258
- ];
1259
- }
1260
-
1261
1245
  /**
1262
1246
  * When `data-threads-url` is set, route thread enumeration / load / rename /
1263
1247
  * delete through that server endpoint (wrapping the current store as the
@@ -1687,7 +1671,6 @@ export class AgUiChat extends HTMLElement {
1687
1671
  this.#toolCards.clear();
1688
1672
  this.#serverSettled.clear();
1689
1673
  this.#initialMessages = [];
1690
- this.#runAttachments = [];
1691
1674
  this.#attachTray?.clear();
1692
1675
  // Keep the empty-state region; everything else clears.
1693
1676
  this.#messages.replaceChildren(this.#emptyWrap);
@@ -2439,8 +2422,6 @@ export class AgUiChat extends HTMLElement {
2439
2422
  if (attachments.length > 0) {
2440
2423
  bubble.appendChild(renderAttachmentChips(attachments));
2441
2424
  }
2442
- // Surfaced to the run via the context manifest until the run settles.
2443
- this.#runAttachments = attachments;
2444
2425
  this.dispatchEvent(
2445
2426
  new CustomEvent<SubmitDetail>(SUBMIT_EVENT, {
2446
2427
  detail: { content, attachments },
@@ -2804,9 +2785,6 @@ export class AgUiChat extends HTMLElement {
2804
2785
  this.#hidePending();
2805
2786
  this.#setRunning(false);
2806
2787
  this.#streamingBubble = null;
2807
- // The attachment manifest was for this run only; the model has read what
2808
- // it needed (results now live in history).
2809
- this.#runAttachments = [];
2810
2788
  // Belt-and-suspenders: a tool card still pending at settle (e.g. a
2811
2789
  // server tool whose result never streamed because the connection
2812
2790
  // dropped) would hang forever — settle it to the no-result fallback.
@@ -1,3 +1,4 @@
1
+ import { ICON_FILE, ICON_FILE_IMAGE, ICON_FILE_PDF, ICON_FILE_TEXT } from "../constants.js";
1
2
  import type { AttachmentRef } from "../core/attachment.js";
2
3
 
3
4
  /**
@@ -24,7 +25,7 @@ function renderChip(ref: AttachmentRef): HTMLDivElement {
24
25
  const icon = document.createElement("span");
25
26
  icon.className = "attachment-chip-icon";
26
27
  icon.setAttribute("part", "attachment-chip-icon");
27
- icon.textContent = iconFor(ref.mime);
28
+ icon.innerHTML = iconFor(ref.mime);
28
29
  icon.setAttribute("aria-hidden", "true");
29
30
 
30
31
  const name = document.createElement("span");
@@ -42,18 +43,29 @@ function renderChip(ref: AttachmentRef): HTMLDivElement {
42
43
  return chip;
43
44
  }
44
45
 
45
- /** A coarse type icon for a chip — image, document, or generic file. */
46
+ /**
47
+ * A coarse type mark for a chip — image, PDF, text document, or generic file —
48
+ * as the inline SVG markup of one of the built-in glyphs.
49
+ *
50
+ * Returns markup rather than a character because chips sit beside the
51
+ * composer's SVG send, attach and mic buttons: emoji render at a different
52
+ * optical weight, vary by platform, and take neither the glyph size nor
53
+ * `currentColor`, so the two never matched.
54
+ *
55
+ * The MIME string only selects among author-written constants and is never
56
+ * interpolated into one, so the result is safe to assign as markup.
57
+ */
46
58
  export function iconFor(mime: string): string {
47
59
  if (mime.startsWith("image/")) {
48
- return "🖼";
60
+ return ICON_FILE_IMAGE;
49
61
  }
50
62
  if (mime === "application/pdf") {
51
- return "📕";
63
+ return ICON_FILE_PDF;
52
64
  }
53
65
  if (mime.startsWith("text/")) {
54
- return "📄";
66
+ return ICON_FILE_TEXT;
55
67
  }
56
- return "📎";
68
+ return ICON_FILE;
57
69
  }
58
70
 
59
71
  /** A compact human-readable byte size (e.g. `1.2 MB`). */
@@ -215,7 +215,8 @@ export class AttachmentTray {
215
215
  const icon = document.createElement("span");
216
216
  icon.className = "attachment-chip-icon";
217
217
  icon.setAttribute("part", "attachment-chip-icon");
218
- icon.textContent = iconFor(item.file.type);
218
+ // Author-written glyph markup, selected by MIME family — see iconFor.
219
+ icon.innerHTML = iconFor(item.file.type);
219
220
  icon.setAttribute("aria-hidden", "true");
220
221
 
221
222
  const name = document.createElement("span");
package/src/ui/styles.ts CHANGED
@@ -1454,11 +1454,25 @@ export const STYLES = `
1454
1454
  display: contents;
1455
1455
  }
1456
1456
 
1457
+ /* The padding is the only separation the tray gets: its slot is display:
1458
+ contents, so the tray is a direct child of the .chat column, and that column
1459
+ sets no gap. The bottom value keeps a chip off the composer's top edge.
1460
+ The inline value here is only the default: the page placement overrides
1461
+ padding-inline further down to compute its reading-column gutter, and wins
1462
+ on specificity whichever way this declaration is written. Read the two
1463
+ together before changing either. */
1457
1464
  .attachment-tray {
1458
1465
  display: flex;
1459
1466
  flex-wrap: wrap;
1460
1467
  gap: 6px;
1461
- padding: 8px 12px 0;
1468
+ padding: 8px 12px;
1469
+ }
1470
+
1471
+ /* The tray sets the hidden property while empty, and an author display beats
1472
+ the UA stylesheet's rule for it, so without this the empty tray keeps
1473
+ laying out and its padding is permanent dead space above the composer. */
1474
+ .attachment-tray[hidden] {
1475
+ display: none;
1462
1476
  }
1463
1477
 
1464
1478
  .attachment-chips {
@@ -1494,15 +1508,29 @@ export const STYLES = `
1494
1508
  overflow-wrap: anywhere;
1495
1509
  }
1496
1510
 
1511
+ /* A chip carries its own text colour because it carries its own background.
1512
+ The same chip renders in two places with opposite inherited colours: in the
1513
+ composer tray it inherits the panel's, but on a sent user bubble it inherits
1514
+ the user foreground, which is white on the stock light theme. Against the
1515
+ assistant surface underneath that was a 1.13:1 filename — invisible, and only
1516
+ the size stayed legible because it sets its own muted colour. Pair the colour
1517
+ with the background it belongs to, and both placements read the same.
1518
+ Overridden below for an errored chip, which must keep its red. */
1497
1519
  .attachment-chip {
1498
1520
  display: inline-flex;
1499
1521
  align-items: center;
1500
1522
  gap: 6px;
1523
+ /* Without this the 100% below caps the content box, so the chip still
1524
+ overflows its container by its own padding and border. Invisible while a
1525
+ character cap kept names short; routine once the container is what bounds
1526
+ the name. */
1527
+ box-sizing: border-box;
1501
1528
  max-width: 100%;
1502
1529
  padding: 4px 8px;
1503
1530
  border: 1px solid var(--_border);
1504
1531
  border-radius: 999px;
1505
1532
  background: var(--_assistant-bg);
1533
+ color: var(--_text);
1506
1534
  font-size: 0.85em;
1507
1535
  position: relative;
1508
1536
  }
@@ -1512,11 +1540,31 @@ export const STYLES = `
1512
1540
  color: var(--_danger);
1513
1541
  }
1514
1542
 
1543
+ /* The type mark. Painted from currentColor, so it carries the chip's state
1544
+ with it and turns red along with an errored one; muted by opacity rather
1545
+ than a colour, which is what keeps that true. Sized from the chip's own text
1546
+ rather than --ag-ui-glyph-size, the way an icon holder's glyph takes the
1547
+ holder's size: the composer's 18px buttons would make a chip button-height. */
1548
+ .attachment-chip-icon {
1549
+ display: inline-flex;
1550
+ flex: none;
1551
+ opacity: 0.75;
1552
+ }
1553
+
1554
+ .attachment-chip-icon .glyph {
1555
+ width: 1.25em;
1556
+ height: 1.25em;
1557
+ }
1558
+
1559
+ /* No character cap: the chip is already max-width 100%, so its container is
1560
+ what bounds the name, and a fixed cap only truncated names the chip had room
1561
+ for. min-width: 0 is what lets the flex item shrink past its content, so the
1562
+ ellipsis appears at the container edge instead of the chip overflowing. */
1515
1563
  .attachment-chip-name {
1516
1564
  overflow: hidden;
1517
1565
  text-overflow: ellipsis;
1518
1566
  white-space: nowrap;
1519
- max-width: 14ch;
1567
+ min-width: 0;
1520
1568
  }
1521
1569
 
1522
1570
  .attachment-chip-size {
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION: string = "0.22.0";
1
+ export const VERSION: string = "0.23.0";