@artooi/ag-ui-web-component 0.25.2 → 0.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -1
- package/README.md +89 -0
- package/dist/ag-ui-web-component.bundle.js +73 -29
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/constants.d.ts +10 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/core/ag_ui_chat.d.ts +19 -0
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +12 -1
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +726 -165
- package/dist/index.js.map +4 -4
- package/dist/tools/client_tool_registry.d.ts +33 -1
- package/dist/tools/client_tool_registry.d.ts.map +1 -1
- package/dist/ui/chart_block.d.ts +39 -0
- package/dist/ui/chart_block.d.ts.map +1 -0
- package/dist/ui/chart_spec_from.d.ts +12 -0
- package/dist/ui/chart_spec_from.d.ts.map +1 -0
- package/dist/ui/chart_tool.d.ts +15 -0
- package/dist/ui/chart_tool.d.ts.map +1 -0
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +11 -0
- package/src/core/ag_ui_chat.ts +165 -3
- package/src/core/agui_client.ts +51 -3
- package/src/index.ts +8 -0
- package/src/tools/client_tool_registry.ts +34 -1
- package/src/ui/chart_block.ts +354 -0
- package/src/ui/chart_spec_from.ts +125 -0
- package/src/ui/chart_tool.ts +72 -0
- package/src/ui/styles.ts +44 -0
- package/src/version.ts +1 -1
|
@@ -6,11 +6,43 @@ import type { Tool } from "@ag-ui/core";
|
|
|
6
6
|
* `handler` receives the parsed arguments and returns a result that is
|
|
7
7
|
* JSON-serialised into the AG-UI tool-result message sent back to the agent.
|
|
8
8
|
*/
|
|
9
|
+
/**
|
|
10
|
+
* Draws one call from its arguments alone.
|
|
11
|
+
*
|
|
12
|
+
* Named separately so the replay path can take *this* and never the tool that
|
|
13
|
+
* owns it: a function of this type cannot reach a `handler`, which is what
|
|
14
|
+
* makes "a reload never re-runs a tool's effect" a property of the code rather
|
|
15
|
+
* than a note asking maintainers to be careful.
|
|
16
|
+
*/
|
|
17
|
+
export type ChartRenderer = (args: Record<string, unknown>) => Node | null;
|
|
9
18
|
export interface ClientTool {
|
|
10
19
|
name: string;
|
|
11
20
|
description: string;
|
|
12
21
|
parameters: Record<string, unknown>;
|
|
13
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Run the tool. `callId` identifies the call being executed, for a handler
|
|
24
|
+
* that renders into the transcript and needs to place itself against its own
|
|
25
|
+
* card; handlers that only act on the page ignore it.
|
|
26
|
+
*/
|
|
27
|
+
handler: (args: Record<string, unknown>, callId?: string) => unknown | Promise<unknown>;
|
|
28
|
+
/**
|
|
29
|
+
* Draw this call, from its arguments alone.
|
|
30
|
+
*
|
|
31
|
+
* Optional, and **the only half that is replayed**. A restored transcript
|
|
32
|
+
* redraws by calling `render`; it never calls {@link handler}. That is what
|
|
33
|
+
* keeps a replayable tool apart from an effectful one *structurally* rather
|
|
34
|
+
* than by promise: the restore path holds no reference to the half that acts,
|
|
35
|
+
* so re-running `fill_field` on every reload is not a mistake anyone can make.
|
|
36
|
+
*
|
|
37
|
+
* The contract this must keep, because it runs again on every restore:
|
|
38
|
+
*
|
|
39
|
+
* - a pure function of `args` — no host state, no network, no clock;
|
|
40
|
+
* - deterministic, so a reload reproduces what was there before;
|
|
41
|
+
* - free of effects outside the node it returns, which the component places.
|
|
42
|
+
*
|
|
43
|
+
* Return `null` for arguments that say nothing worth drawing.
|
|
44
|
+
*/
|
|
45
|
+
render?: ChartRenderer;
|
|
14
46
|
}
|
|
15
47
|
/**
|
|
16
48
|
* Holds the frontend tools a host has declared on an `<ag-ui-chat>` element.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client_tool_registry.d.ts","sourceRoot":"","sources":["../../src/tools/client_tool_registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAExC;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"client_tool_registry.d.ts","sourceRoot":"","sources":["../../src/tools/client_tool_registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAExC;;;;;;GAMG;AACH;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,GAAG,IAAI,CAAC;AAE3E,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC;;;;OAIG;IACH,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACxF;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED;;;;;;GAMG;AACH,qBAAa,kBAAkB;;IAG7B;;;;;OAKG;IACH,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAE/B;IAED,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEzB;IAED,yCAAyC;IACzC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAM5B;IAED,wDAAwD;IACxD,KAAK,IAAI,IAAI,EAAE,CAMd;CACF"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Draw a {@link ChartSpec} as SVG.
|
|
3
|
+
*
|
|
4
|
+
* Every node is built with `createElement`, never parsed from a string, so this
|
|
5
|
+
* path never reaches the markdown sanitiser and cannot be widened by model
|
|
6
|
+
* output. That is the argument for taking a *spec* rather than markup: the model
|
|
7
|
+
* chooses the numbers, this module chooses the DOM. It is why a chart can be
|
|
8
|
+
* shown on a surface that keeps `img` off by default, a model-controlled URL
|
|
9
|
+
* being a zero-click exfiltration channel.
|
|
10
|
+
*
|
|
11
|
+
* Hand-rolled rather than a charting library, and the difference is not
|
|
12
|
+
* marginal: the whole renderer costs single-digit kilobytes where a library
|
|
13
|
+
* costs roughly half this bundle again, in a component distributed over a CDN.
|
|
14
|
+
*/
|
|
15
|
+
/** One named series of numbers. */
|
|
16
|
+
export interface ChartSeries {
|
|
17
|
+
readonly label: string;
|
|
18
|
+
readonly points: readonly number[];
|
|
19
|
+
}
|
|
20
|
+
/** How a spec is drawn. */
|
|
21
|
+
export type ChartKind = "bar" | "line" | "pie" | "scatter" | "stacked";
|
|
22
|
+
/** A chart, as data. */
|
|
23
|
+
export interface ChartSpec {
|
|
24
|
+
readonly kind: ChartKind;
|
|
25
|
+
readonly title?: string;
|
|
26
|
+
readonly labels: readonly string[];
|
|
27
|
+
readonly series: readonly ChartSeries[];
|
|
28
|
+
}
|
|
29
|
+
/** The colour for series `index`, wrapping when there are more series than colours. */
|
|
30
|
+
export declare function seriesColor(index: number): string;
|
|
31
|
+
/**
|
|
32
|
+
* Render one spec as a self-contained block, or `null` when it says nothing.
|
|
33
|
+
*
|
|
34
|
+
* A spec with no labels or no series is not drawn: an empty frame reads as
|
|
35
|
+
* "there is no data" when the truth is "the caller sent nothing", and the two
|
|
36
|
+
* deserve different answers.
|
|
37
|
+
*/
|
|
38
|
+
export declare function renderChart(spec: ChartSpec): HTMLDivElement | null;
|
|
39
|
+
//# sourceMappingURL=chart_block.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chart_block.d.ts","sourceRoot":"","sources":["../../src/ui/chart_block.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,mCAAmC;AACnC,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED,2BAA2B;AAC3B,MAAM,MAAM,SAAS,GAAG,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,CAAC;AAEvE,wBAAwB;AACxB,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,SAAS,WAAW,EAAE,CAAC;CACzC;AAoBD,uFAAuF;AACvF,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIjD;AA6OD;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,GAAG,cAAc,GAAG,IAAI,CAoDlE"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow untrusted input into a {@link ChartSpec}.
|
|
3
|
+
*
|
|
4
|
+
* Both arrival routes are untrusted in the same way — a model writes one, a
|
|
5
|
+
* server the other — so neither is taken on shape. Kept out of the renderer
|
|
6
|
+
* because the renderer's job is drawing, and a value that reaches it has
|
|
7
|
+
* already been vouched for.
|
|
8
|
+
*/
|
|
9
|
+
import type { ChartSpec } from "./chart_block.js";
|
|
10
|
+
/** A well-formed spec, or `null` for anything that cannot be drawn honestly. */
|
|
11
|
+
export declare function chartSpecFrom(value: unknown): ChartSpec | null;
|
|
12
|
+
//# sourceMappingURL=chart_spec_from.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chart_spec_from.d.ts","sourceRoot":"","sources":["../../src/ui/chart_spec_from.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAa,SAAS,EAAE,MAAM,kBAAkB,CAAC;AA6E7D,gFAAgF;AAChF,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI,CAqC9D"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in `render_chart` tool: the agent-called route to a chart.
|
|
3
|
+
*
|
|
4
|
+
* A thin thing on purpose. It carries no `handler` of its own beyond reporting
|
|
5
|
+
* back to the model, because everything it does is drawing — which lives in
|
|
6
|
+
* `render`, the half the component replays. A consumer wanting charts gets this
|
|
7
|
+
* without writing a renderer; a consumer wanting something else writes their own
|
|
8
|
+
* tool against the same seam.
|
|
9
|
+
*/
|
|
10
|
+
import type { ClientTool } from "../tools/client_tool_registry.js";
|
|
11
|
+
/** The name the built-in chart tool registers under. */
|
|
12
|
+
export declare const CHART_TOOL_NAME = "render_chart";
|
|
13
|
+
/** The built-in chart tool. */
|
|
14
|
+
export declare function createChartTool(): ClientTool;
|
|
15
|
+
//# sourceMappingURL=chart_tool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chart_tool.d.ts","sourceRoot":"","sources":["../../src/ui/chart_tool.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAC;AAInE,wDAAwD;AACxD,eAAO,MAAM,eAAe,iBAAiB,CAAC;AAgB9C,+BAA+B;AAC/B,wBAAgB,eAAe,IAAI,UAAU,CAuC5C"}
|
package/dist/ui/styles.d.ts
CHANGED
|
@@ -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, so tokens put on a wrapper\n would have no effect. Reading the public name with the default as a var()\n fallback leaves it undeclared on the element, so an ancestor's value\n inherits normally while one aimed at the element still wins.\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, read only by the code-block copy button.\n The defaults restate what it inherits, so a host that sets neither sees\n 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 widget accelerates and settles as one thing. The\n default curve decelerates into place; the pop curve overshoots slightly,\n 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 every alias has a default in one place. */\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 capped at\n --ag-ui-content-max-width, where \"full\" is edge-to-edge and left-aligned.\n The column comes from symmetric auto padding on the scroll area and\n composer rather than a per-row wrapper, so user pills still right-align and\n 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 line up with the column\n too. Chips and tray are padding-based while palette and hint 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. It stays laid\n out at rest rather than display:none, which is what lets it animate both in\n and out: an unrendered element has no before-change style to transition\n from, and one flipping display to none cannot transition at all. visibility\n keeps it unpaintable, untabbable and unclickable in between, so the expanded\n panel's own controls underneath 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 toward the launcher's corner and fades, the launcher pops in from the\n same point. Both halves are transform and opacity only, so the morph runs on\n the compositor and never reflows the host page.\n\n The host box keeps its expanded size, since animating it would animate\n layout and a dragged --ag-ui-width would fight the launcher's own size.\n Nothing paints there once the panel is gone, so the box only has to stop\n swallowing clicks: pointer events go to none and the launcher takes them.\n\n Two placements collapse differently: \"sidebar\" slides to its rail (below),\n while \"embedded\" and \"page\" keep the header bar, having no corner for a\n floating circle that would escape the host's layout. */\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 keeps the panel out of the tab order and the a11y tree at rest\n without display:none killing the transition. It interpolates so any progress\n below 1 still counts as visible: the panel stays on screen for the whole\n collapse, flips hidden exactly at the end, and on expand is visible from the\n 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: an\n embedded host is position: static, which would let an absolutely-positioned\n circle escape the layout and land against whatever the page positions. */\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 turn renders as a 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/* The only reader of --ag-ui-surface and --ag-ui-text. Their defaults are\n transparent and the body foreground, which is what this button rendered as\n before either token existed; changing them repaints only this control. */\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 the sanitizer's\n ALLOWED_TAGS, so an agent can emit one. A wide table must scroll inside its\n own box rather than stretch the message: the bubble is width-constrained, so\n without this the columns either crush or push the 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/* Wraps, because the name is the only flexible child and every badge the row\n gains is taken out of it. An approved call adds a third fixed badge, which in\n a sidebar-width panel left the name 37px and broke it mid-word. Badges drop to\n their own row instead. */\n.tool-call-head {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.tool-call-name {\n /* An auto basis, and a min-width floor rather than zero: the name may shrink,\n but not below something readable, so wrapping moves a badge instead of\n shredding a word. Breaking anywhere still applies to a name that cannot fit\n on a line of its own, which is what keeps a long unbroken tool name inside\n the card. */\n flex: 1 1 auto;\n min-width: 6ch;\n font-weight: 600;\n overflow-wrap: anywhere;\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/* Deferred: no spinner, because nothing is spinning. A steady accent dot, since\n the state is waiting-on-you rather than an outcome. */\n.tool-call[data-status=\"deferred\"] .tool-call-icon {\n border-radius: 50%;\n background: var(--_accent);\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=\"deferred\"] .tool-call-status {\n color: var(--_accent);\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.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/* The lasting record of a human decision on a gated call \u2014 without it an\n approved call looks exactly like one that was never gated. */\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 at build time, so flipping\n data-tool-display re-styles cards already on screen. See ToolCallCard.\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. A deferred card is the same, and its\n arguments are shown unconditionally by the rules below. */\n.tool-call[data-status=\"pending\"] .tool-call-toggle,\n.tool-call[data-status=\"deferred\"] .tool-call-toggle,\n:host([data-tool-display=\"inline\"]) .tool-call[data-status=\"pending\"] .tool-call-toggle {\n display: none;\n}\n\n/* The approval prompt for a gated call, rendered inside that call's own card.\n Empty on every card nobody is being asked about, so it collapses instead of\n adding a gap to each one. */\n.tool-call-approval:empty {\n display: none;\n}\n\n.tool-call-approval {\n margin-top: 8px;\n}\n\n/* A card that is asking a question shows what it is asking about, in every\n display mode. Three gated calls of one tool ask the same words, so the\n arguments are the only thing telling them apart, and a density setting must\n not be able to hide the answer to \"which one is this\". */\n:host([data-tool-display=\"minimal\"]) .tool-call[data-status=\"deferred\"] .tool-call-body {\n display: flex;\n}\n\n/* The arguments region only, never every section: the result region carries the\n hidden attribute until a result exists, and a display value here overrides it,\n framing an empty RESULT heading under the question. */\n:host([data-tool-display=\"compact\"]) .tool-call[data-status=\"deferred\"] .tool-call-section--args,\n:host([data-tool-display=\"inline\"]) .tool-call[data-status=\"deferred\"] .tool-call-section--args {\n display: flex;\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, not to placement -- a floating panel a host right-aligns is\n anchored bottom-left -- so the element measures them and stamps a single\n hyphenated \"<y>-<x>\" token here. Equal specificity to the rules above, so\n source order is what lets the measured value win.\n\n Two traps, both of which silently draw the grip on the corner that moves:\n\n Never write these as [data-resize-anchor~=\"left\"]. The stamped value is one\n hyphenated token and ~= matches whitespace-separated words, so it can never\n match.\n\n Each rule must set both sides of its axis, not only the side it moves, or it\n cannot undo a placement guess that flipped the other way. */\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\n and its tool row sit inside it, rather than being siblings stretched to the\n textarea's height. */\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/* The same trap the attachment tray carries a note about, two rules along: an\n author display beats the UA stylesheet's rule for the hidden property, so a\n button the element has explicitly hidden keeps laying out and painting. The\n clip is hidden until a host supplies an upload handler or an attachments URL,\n and without this it is a visible control that cannot do anything.\n\n The mic needs no such rule, and the asymmetry is worth knowing before adding\n one: it is not hidden when unconfigured, it is never built. The voice wiring\n returns before constructing the button, leaving only an empty voice slot that\n is display: contents. A hidden-state rule for the mic would match nothing. */\n.attach-btn[hidden] {\n display: none;\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: the\n composer tray gives it the panel's, a sent user bubble gives it the user\n foreground, which is white on the stock light theme and near-invisible\n against the chip. Pairing the colour with the background it belongs to makes\n both placements read alike. Overridden below for an errored chip, which\n keeps 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/* A row is a label and two buttons, and nothing about the row itself is\n pressable. It used to light up on hover, which is the affordance of something\n clickable and made the buttons look like decoration on a clickable strip. The\n resting surface groups the row instead, so hover can mean what it says: only\n the buttons respond to it.\n\n It wraps for the same reason the tool-call head does. Every child but the label\n is fixed-width, so in a narrow panel the label is the only thing that can give\n -- and a flex-basis of zero lets it give everything. Adding the run id was\n enough to crush \"just now\" to zero pixels: present, correct, and invisible.\n Wrapping puts the buttons on their own line instead. */\n.checkpoint-row {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.5rem;\n padding: 0.3125rem 0.4375rem;\n border-radius: 0.375rem;\n background: var(--_hover);\n}\n\n/* Grows into spare room, and refuses to shrink past the shortest thing it ever\n says. A time is short and bounded, so there is no case for eliding it. */\n.checkpoint-label {\n flex: 1 1 auto;\n min-width: 7ch;\n font-size: 0.8125rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* When the label holds the run's first message, the time moves here: still worth\n showing, no longer what identifies the row. Muted and unshrinkable, so it does\n not compete with the words beside it. */\n.checkpoint-time {\n flex: 0 0 auto;\n font-size: 0.6875rem;\n opacity: 0.7;\n white-space: nowrap;\n}\n\n/* Enough of the run id to tell two runs apart when both say \"just now\". Muted\n and monospaced: it is a reference, not a name. */\n.checkpoint-id {\n flex: 0 0 auto;\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 0.6875rem;\n opacity: 0.55;\n}\n\n/* On the panel's own surface, not the row's: the row now paints the hover token\n itself, and a badge the same colour as what it sits on is not a badge. */\n.checkpoint-branch {\n font-size: 0.6875rem;\n padding: 0 0.375rem;\n border-radius: 999px;\n background: var(--_assistant-bg);\n opacity: 0.8;\n}\n\n/* The two things in the row that actually do something, so they are the two\n things that look like it: a filled surface at rest rather than a transparent\n outline, which on top of the old row highlight was nearly invisible. */\n.checkpoint-action {\n font: inherit;\n font-size: 0.75rem;\n line-height: 1.4;\n cursor: pointer;\n padding: 0.1875rem 0.5625rem;\n border: 1px solid var(--_border);\n border-radius: 0.375rem;\n background: var(--_bg);\n color: inherit;\n transition:\n background var(--_motion) var(--_ease),\n border-color var(--_motion) var(--_ease),\n transform var(--_motion) var(--_ease);\n}\n\n/* Resume is what a reader wants nine times in ten; fork is the deliberate choice\n beside it. Filled and outlined, the same pair the confirmation and approval\n cards already use for their primary and secondary action. */\n.checkpoint-resume {\n font-weight: 600;\n border-color: var(--_accent);\n background: var(--_accent);\n color: #ffffff;\n}\n\n.checkpoint-fork:hover {\n background: var(--_hover);\n border-color: var(--_accent);\n}\n\n/* The filled one cannot go lighter on hover without losing its contrast with the\n white label, so it dims instead. */\n.checkpoint-resume:hover {\n opacity: 0.88;\n}\n\n/* Pressed: a pixel down, so the click is felt as well as seen. */\n.checkpoint-action:active {\n transform: translateY(1px);\n}\n\n/* Keyboard focus was invisible here, in a panel that traps focus and is reached\n by Tab -- so the one navigation path guaranteed to land on these buttons was\n the one with nothing to show for it. */\n.checkpoint-action:focus-visible {\n outline: 2px solid var(--_accent);\n outline-offset: 2px;\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, so tokens put on a wrapper\n would have no effect. Reading the public name with the default as a var()\n fallback leaves it undeclared on the element, so an ancestor's value\n inherits normally while one aimed at the element still wins.\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, read only by the code-block copy button.\n The defaults restate what it inherits, so a host that sets neither sees\n 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 widget accelerates and settles as one thing. The\n default curve decelerates into place; the pop curve overshoots slightly,\n 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 every alias has a default in one place. */\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 capped at\n --ag-ui-content-max-width, where \"full\" is edge-to-edge and left-aligned.\n The column comes from symmetric auto padding on the scroll area and\n composer rather than a per-row wrapper, so user pills still right-align and\n 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 line up with the column\n too. Chips and tray are padding-based while palette and hint 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. It stays laid\n out at rest rather than display:none, which is what lets it animate both in\n and out: an unrendered element has no before-change style to transition\n from, and one flipping display to none cannot transition at all. visibility\n keeps it unpaintable, untabbable and unclickable in between, so the expanded\n panel's own controls underneath 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 toward the launcher's corner and fades, the launcher pops in from the\n same point. Both halves are transform and opacity only, so the morph runs on\n the compositor and never reflows the host page.\n\n The host box keeps its expanded size, since animating it would animate\n layout and a dragged --ag-ui-width would fight the launcher's own size.\n Nothing paints there once the panel is gone, so the box only has to stop\n swallowing clicks: pointer events go to none and the launcher takes them.\n\n Two placements collapse differently: \"sidebar\" slides to its rail (below),\n while \"embedded\" and \"page\" keep the header bar, having no corner for a\n floating circle that would escape the host's layout. */\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 keeps the panel out of the tab order and the a11y tree at rest\n without display:none killing the transition. It interpolates so any progress\n below 1 still counts as visible: the panel stays on screen for the whole\n collapse, flips hidden exactly at the end, and on expand is visible from the\n 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: an\n embedded host is position: static, which would let an absolutely-positioned\n circle escape the layout and land against whatever the page positions. */\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 turn renders as a 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/* The only reader of --ag-ui-surface and --ag-ui-text. Their defaults are\n transparent and the body foreground, which is what this button rendered as\n before either token existed; changing them repaints only this control. */\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 the sanitizer's\n ALLOWED_TAGS, so an agent can emit one. A wide table must scroll inside its\n own box rather than stretch the message: the bubble is width-constrained, so\n without this the columns either crush or push the 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/* Wraps, because the name is the only flexible child and every badge the row\n gains is taken out of it. An approved call adds a third fixed badge, which in\n a sidebar-width panel left the name 37px and broke it mid-word. Badges drop to\n their own row instead. */\n.tool-call-head {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.tool-call-name {\n /* An auto basis, and a min-width floor rather than zero: the name may shrink,\n but not below something readable, so wrapping moves a badge instead of\n shredding a word. Breaking anywhere still applies to a name that cannot fit\n on a line of its own, which is what keeps a long unbroken tool name inside\n the card. */\n flex: 1 1 auto;\n min-width: 6ch;\n font-weight: 600;\n overflow-wrap: anywhere;\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/* Deferred: no spinner, because nothing is spinning. A steady accent dot, since\n the state is waiting-on-you rather than an outcome. */\n.tool-call[data-status=\"deferred\"] .tool-call-icon {\n border-radius: 50%;\n background: var(--_accent);\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=\"deferred\"] .tool-call-status {\n color: var(--_accent);\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.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/* The lasting record of a human decision on a gated call \u2014 without it an\n approved call looks exactly like one that was never gated. */\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 at build time, so flipping\n data-tool-display re-styles cards already on screen. See ToolCallCard.\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. A deferred card is the same, and its\n arguments are shown unconditionally by the rules below. */\n.tool-call[data-status=\"pending\"] .tool-call-toggle,\n.tool-call[data-status=\"deferred\"] .tool-call-toggle,\n:host([data-tool-display=\"inline\"]) .tool-call[data-status=\"pending\"] .tool-call-toggle {\n display: none;\n}\n\n/* The approval prompt for a gated call, rendered inside that call's own card.\n Empty on every card nobody is being asked about, so it collapses instead of\n adding a gap to each one. */\n.tool-call-approval:empty {\n display: none;\n}\n\n.tool-call-approval {\n margin-top: 8px;\n}\n\n/* A card that is asking a question shows what it is asking about, in every\n display mode. Three gated calls of one tool ask the same words, so the\n arguments are the only thing telling them apart, and a density setting must\n not be able to hide the answer to \"which one is this\". */\n:host([data-tool-display=\"minimal\"]) .tool-call[data-status=\"deferred\"] .tool-call-body {\n display: flex;\n}\n\n/* The arguments region only, never every section: the result region carries the\n hidden attribute until a result exists, and a display value here overrides it,\n framing an empty RESULT heading under the question. */\n:host([data-tool-display=\"compact\"]) .tool-call[data-status=\"deferred\"] .tool-call-section--args,\n:host([data-tool-display=\"inline\"]) .tool-call[data-status=\"deferred\"] .tool-call-section--args {\n display: flex;\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, not to placement -- a floating panel a host right-aligns is\n anchored bottom-left -- so the element measures them and stamps a single\n hyphenated \"<y>-<x>\" token here. Equal specificity to the rules above, so\n source order is what lets the measured value win.\n\n Two traps, both of which silently draw the grip on the corner that moves:\n\n Never write these as [data-resize-anchor~=\"left\"]. The stamped value is one\n hyphenated token and ~= matches whitespace-separated words, so it can never\n match.\n\n Each rule must set both sides of its axis, not only the side it moves, or it\n cannot undo a placement guess that flipped the other way. */\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\n and its tool row sit inside it, rather than being siblings stretched to the\n textarea's height. */\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/* The same trap the attachment tray carries a note about, two rules along: an\n author display beats the UA stylesheet's rule for the hidden property, so a\n button the element has explicitly hidden keeps laying out and painting. The\n clip is hidden until a host supplies an upload handler or an attachments URL,\n and without this it is a visible control that cannot do anything.\n\n The mic needs no such rule, and the asymmetry is worth knowing before adding\n one: it is not hidden when unconfigured, it is never built. The voice wiring\n returns before constructing the button, leaving only an empty voice slot that\n is display: contents. A hidden-state rule for the mic would match nothing. */\n.attach-btn[hidden] {\n display: none;\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/* Charts.\n *\n * The SVG scales to the column and carries no colours of its own beyond the\n * series palette, so a host restyles it the same way it restyles everything\n * else. Series colours are custom properties with fallbacks rather than fixed\n * values, and the axis furniture inherits currentColor at low opacity so it\n * reads correctly in either theme without a second palette.\n */\n.chart-block {\n align-self: stretch;\n max-width: 100%;\n margin: 6px 0;\n color: var(--_fg);\n}\n\n.chart-title {\n margin-bottom: 2px;\n font-size: 0.85em;\n font-weight: 600;\n opacity: 0.85;\n}\n\n.chart-legend {\n display: flex;\n flex-wrap: wrap;\n gap: 4px 12px;\n margin-top: 4px;\n font-size: 0.78em;\n opacity: 0.75;\n}\n\n.chart-legend-item {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n}\n\n.chart-legend-swatch {\n width: 9px;\n height: 9px;\n border-radius: 2px;\n flex: 0 0 auto;\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: the\n composer tray gives it the panel's, a sent user bubble gives it the user\n foreground, which is white on the stock light theme and near-invisible\n against the chip. Pairing the colour with the background it belongs to makes\n both placements read alike. Overridden below for an errored chip, which\n keeps 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/* A row is a label and two buttons, and nothing about the row itself is\n pressable. It used to light up on hover, which is the affordance of something\n clickable and made the buttons look like decoration on a clickable strip. The\n resting surface groups the row instead, so hover can mean what it says: only\n the buttons respond to it.\n\n It wraps for the same reason the tool-call head does. Every child but the label\n is fixed-width, so in a narrow panel the label is the only thing that can give\n -- and a flex-basis of zero lets it give everything. Adding the run id was\n enough to crush \"just now\" to zero pixels: present, correct, and invisible.\n Wrapping puts the buttons on their own line instead. */\n.checkpoint-row {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.5rem;\n padding: 0.3125rem 0.4375rem;\n border-radius: 0.375rem;\n background: var(--_hover);\n}\n\n/* Grows into spare room, and refuses to shrink past the shortest thing it ever\n says. A time is short and bounded, so there is no case for eliding it. */\n.checkpoint-label {\n flex: 1 1 auto;\n min-width: 7ch;\n font-size: 0.8125rem;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* When the label holds the run's first message, the time moves here: still worth\n showing, no longer what identifies the row. Muted and unshrinkable, so it does\n not compete with the words beside it. */\n.checkpoint-time {\n flex: 0 0 auto;\n font-size: 0.6875rem;\n opacity: 0.7;\n white-space: nowrap;\n}\n\n/* Enough of the run id to tell two runs apart when both say \"just now\". Muted\n and monospaced: it is a reference, not a name. */\n.checkpoint-id {\n flex: 0 0 auto;\n font-family: ui-monospace, SFMono-Regular, Menlo, monospace;\n font-size: 0.6875rem;\n opacity: 0.55;\n}\n\n/* On the panel's own surface, not the row's: the row now paints the hover token\n itself, and a badge the same colour as what it sits on is not a badge. */\n.checkpoint-branch {\n font-size: 0.6875rem;\n padding: 0 0.375rem;\n border-radius: 999px;\n background: var(--_assistant-bg);\n opacity: 0.8;\n}\n\n/* The two things in the row that actually do something, so they are the two\n things that look like it: a filled surface at rest rather than a transparent\n outline, which on top of the old row highlight was nearly invisible. */\n.checkpoint-action {\n font: inherit;\n font-size: 0.75rem;\n line-height: 1.4;\n cursor: pointer;\n padding: 0.1875rem 0.5625rem;\n border: 1px solid var(--_border);\n border-radius: 0.375rem;\n background: var(--_bg);\n color: inherit;\n transition:\n background var(--_motion) var(--_ease),\n border-color var(--_motion) var(--_ease),\n transform var(--_motion) var(--_ease);\n}\n\n/* Resume is what a reader wants nine times in ten; fork is the deliberate choice\n beside it. Filled and outlined, the same pair the confirmation and approval\n cards already use for their primary and secondary action. */\n.checkpoint-resume {\n font-weight: 600;\n border-color: var(--_accent);\n background: var(--_accent);\n color: #ffffff;\n}\n\n.checkpoint-fork:hover {\n background: var(--_hover);\n border-color: var(--_accent);\n}\n\n/* The filled one cannot go lighter on hover without losing its contrast with the\n white label, so it dims instead. */\n.checkpoint-resume:hover {\n opacity: 0.88;\n}\n\n/* Pressed: a pixel down, so the click is felt as well as seen. */\n.checkpoint-action:active {\n transform: translateY(1px);\n}\n\n/* Keyboard focus was invisible here, in a panel that traps focus and is reached\n by Tab -- so the one navigation path guaranteed to land on these buttons was\n the one with nothing to show for it. */\n.checkpoint-action:focus-visible {\n outline: 2px solid var(--_accent);\n outline-offset: 2px;\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
|
package/dist/ui/styles.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/ui/styles.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/ui/styles.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM,+qmEAuxElB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@artooi/ag-ui-web-component",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.1",
|
|
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
|
@@ -210,3 +210,14 @@ export const ICON_FILE_PDF = `<svg class="glyph" viewBox="0 0 24 24" aria-hidden
|
|
|
210
210
|
|
|
211
211
|
/** A text document: the generic page, ruled. */
|
|
212
212
|
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>`;
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* ``activityType`` of the ``ACTIVITY_SNAPSHOT`` / ``ACTIVITY_DELTA`` events a
|
|
216
|
+
* server sends to draw a chart.
|
|
217
|
+
*
|
|
218
|
+
* A convention inside an extension point the protocol already provides, not a
|
|
219
|
+
* protocol extension: AG-UI defines the envelope and leaves ``activityType`` an
|
|
220
|
+
* open string. Another vendor's client does not know this name and ignores the
|
|
221
|
+
* event, which is the graceful outcome.
|
|
222
|
+
*/
|
|
223
|
+
export const CHART_ACTIVITY_TYPE = "chart";
|