@artooi/ag-ui-web-component 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,15 @@ export interface ConfirmationRequest {
8
8
  */
9
9
  message?: string;
10
10
  }
11
+ /** Options for {@link requestConfirmation}. */
12
+ export interface ConfirmationOptions {
13
+ /**
14
+ * Aborting this signal resolves the card as declined (buttons disabled,
15
+ * `data-resolved="declined"`) — the hook a Stop control uses to dismiss a
16
+ * pending confirmation when the user cancels the whole run.
17
+ */
18
+ signal?: AbortSignal;
19
+ }
11
20
  /**
12
21
  * Append an inline confirmation card to ``host`` (the chat message list) and
13
22
  * resolve when the user decides.
@@ -18,5 +27,5 @@ export interface ConfirmationRequest {
18
27
  * cancel. The card stays in the transcript as a resolved record (buttons
19
28
  * disabled, `data-resolved` set) rather than vanishing.
20
29
  */
21
- export declare function requestConfirmation(host: Node & ParentNode, request: ConfirmationRequest): Promise<boolean>;
30
+ export declare function requestConfirmation(host: Node & ParentNode, request: ConfirmationRequest, options?: ConfirmationOptions): Promise<boolean>;
22
31
  //# sourceMappingURL=confirmation_card.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"confirmation_card.d.ts","sourceRoot":"","sources":["../../src/ui/confirmation_card.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAWD;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,IAAI,GAAG,UAAU,EACvB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,OAAO,CAAC,CAqClB"}
1
+ {"version":3,"file":"confirmation_card.d.ts","sourceRoot":"","sources":["../../src/ui/confirmation_card.ts"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAWD,+CAA+C;AAC/C,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,IAAI,GAAG,UAAU,EACvB,OAAO,EAAE,mBAAmB,EAC5B,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,OAAO,CAAC,CAgDlB"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Prettify a raw tool name for display: separators become spaces and the
3
+ * first letter is capitalised — `list_projects` → "List projects",
4
+ * `invoices.retrieve` → "Invoices retrieve".
5
+ *
6
+ * Final fallback of the tool-card label chain (`x-summary` →
7
+ * `toolSummaries` → fetched catalog → this). Purely cosmetic: the original
8
+ * name still rides on the card's dataset for debugging.
9
+ */
10
+ export declare function prettifyToolName(name: string): string;
11
+ //# sourceMappingURL=prettify_tool_name.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prettify_tool_name.d.ts","sourceRoot":"","sources":["../../src/ui/prettify_tool_name.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMrD"}
@@ -1,3 +1,12 @@
1
+ /** Options for {@link renderMarkdown}. */
2
+ export interface RenderMarkdownOptions {
3
+ /**
4
+ * Permit `<img>` tags (and their `src`/`alt`/`width`/`height` attributes)
5
+ * in the sanitised output. **Off by default** — see the allowlist note on
6
+ * the exfiltration risk. Only enable for trusted content sources.
7
+ */
8
+ readonly allowImages?: boolean;
9
+ }
1
10
  /**
2
11
  * Render markdown (and any embedded raw HTML) to a sanitised HTML string.
3
12
  *
@@ -9,5 +18,5 @@
9
18
  * The result is trimmed so a single-paragraph message round-trips to clean
10
19
  * `textContent` (no trailing newline from the wrapping `<p>`).
11
20
  */
12
- export declare function renderMarkdown(text: string): string;
21
+ export declare function renderMarkdown(text: string, options?: RenderMarkdownOptions): string;
13
22
  //# sourceMappingURL=render_markdown.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"render_markdown.d.ts","sourceRoot":"","sources":["../../src/ui/render_markdown.ts"],"names":[],"mappings":"AA8CA;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAUnD"}
1
+ {"version":3,"file":"render_markdown.d.ts","sourceRoot":"","sources":["../../src/ui/render_markdown.ts"],"names":[],"mappings":"AA0DA,0CAA0C;AAC1C,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;CAChC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAcpF"}
@@ -1,2 +1,2 @@
1
- export declare const STYLES = "\n:host {\n /* Colors */\n --ag-ui-bg: #ffffff;\n --ag-ui-fg: #1a1a2e;\n --ag-ui-accent: #4f46e5;\n --ag-ui-user-bg: #4f46e5;\n --ag-ui-user-fg: #ffffff;\n --ag-ui-assistant-bg: #f1f1f6;\n --ag-ui-input-bg: var(--ag-ui-bg);\n --ag-ui-tool-bg: var(--ag-ui-assistant-bg);\n --ag-ui-tool-fg: var(--ag-ui-accent);\n --ag-ui-header-bg: var(--ag-ui-accent);\n --ag-ui-header-fg: #ffffff;\n --ag-ui-border: #e2e2ec;\n --ag-ui-radius: 12px;\n\n /* Status accents for tool-call cards. */\n --ag-ui-success: #15803d;\n --ag-ui-danger: #b91c1c;\n --ag-ui-muted: #6b7280;\n\n /* Surface \u2014 set --ag-ui-shadow: none for a flush, embedded panel. */\n --ag-ui-shadow: 0 12px 32px rgba(20, 20, 50, 0.18);\n --ag-ui-font: inherit;\n --ag-ui-font-size: 14px;\n --ag-ui-code-font: ui-monospace, \"SF Mono\", Menlo, monospace;\n\n /* Spacing \u2014 the density preset overrides these. */\n --ag-ui-space: 10px;\n --ag-ui-pad: 16px;\n --ag-ui-msg-pad: 8px 12px;\n --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 --ag-ui-position: fixed;\n --ag-ui-z-index: 2147483000;\n --ag-ui-width: 380px;\n --ag-ui-height: 560px;\n --ag-ui-inset: auto 24px 24px auto;\n --ag-ui-max-width: calc(100vw - 48px);\n --ag-ui-max-height: calc(100vh - 48px);\n\n position: var(--ag-ui-position);\n inset: var(--ag-ui-inset);\n z-index: var(--ag-ui-z-index);\n width: var(--ag-ui-width);\n max-width: var(--ag-ui-max-width);\n height: var(--ag-ui-height);\n max-height: var(--ag-ui-max-height);\n display: flex;\n font-family: var(--ag-ui-font);\n font-size: var(--ag-ui-font-size);\n color: var(--ag-ui-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 --ag-ui-bg: #15151f;\n --ag-ui-fg: #e8e8f2;\n --ag-ui-assistant-bg: #25253a;\n --ag-ui-header-bg: #1f1f30;\n --ag-ui-header-fg: #e8e8f2;\n --ag-ui-border: #33334a;\n --ag-ui-muted: #9aa0b4;\n --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 --ag-ui-bg: #15151f;\n --ag-ui-fg: #e8e8f2;\n --ag-ui-assistant-bg: #25253a;\n --ag-ui-header-bg: #1f1f30;\n --ag-ui-header-fg: #e8e8f2;\n --ag-ui-border: #33334a;\n --ag-ui-muted: #9aa0b4;\n --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 --ag-ui-bg: #0d1117;\n --ag-ui-fg: #c9d1d9;\n --ag-ui-accent: #3fb950;\n --ag-ui-user-bg: #238636;\n --ag-ui-assistant-bg: #161b22;\n --ag-ui-header-bg: #010409;\n --ag-ui-header-fg: #c9d1d9;\n --ag-ui-border: #30363d;\n --ag-ui-muted: #8b949e;\n --ag-ui-font: var(--ag-ui-code-font);\n --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 --ag-ui-font-size: 13px;\n --ag-ui-space: 6px;\n --ag-ui-pad: 10px;\n --ag-ui-msg-pad: 5px 9px;\n --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 --ag-ui-inset: auto auto 24px 24px;\n}\n\n:host([placement=\"side\"]) {\n --ag-ui-inset: 0 0 0 auto;\n --ag-ui-width: 420px;\n --ag-ui-height: 100vh;\n --ag-ui-max-height: 100vh;\n --ag-ui-radius: 0;\n}\n\n:host([placement=\"full\"]) {\n --ag-ui-inset: 0;\n --ag-ui-width: 100vw;\n --ag-ui-height: 100vh;\n --ag-ui-max-width: 100vw;\n --ag-ui-max-height: 100vh;\n --ag-ui-radius: 0;\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 --ag-ui-position: static;\n --ag-ui-width: 100%;\n --ag-ui-height: 100%;\n --ag-ui-max-width: 100%;\n --ag-ui-max-height: 100%;\n --ag-ui-shadow: none;\n --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(--ag-ui-bg);\n border: 1px solid var(--ag-ui-border);\n border-radius: var(--ag-ui-radius);\n box-shadow: var(--ag-ui-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(--ag-ui-border);\n background: var(--ag-ui-header-bg);\n color: var(--ag-ui-header-fg);\n}\n\n.header-title {\n font-weight: 600;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\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/* Collapsed: keep just the header bar \u2014 hide the whole body, and let the host\n shrink to the header. (A host can also fully hide the element itself.) */\n:host([collapsed]) {\n height: auto;\n max-height: none;\n}\n\n:host([collapsed]) .messages,\n:host([collapsed]) .input-row,\n:host([collapsed]) .skill-chips,\n:host([collapsed]) .skill-palette,\n:host([collapsed]) .skill-hint {\n display: none;\n}\n\n/* Edge-docked placements pin top *and* bottom, which would otherwise keep the\n panel full-height when collapsed; unpin the bottom so it shrinks to the\n header. (Floating/bottom-left pin only the bottom, so they already shrink.) */\n:host([collapsed][placement=\"side\"]),\n:host([collapsed][placement=\"full\"]) {\n bottom: auto;\n}\n\n.messages {\n flex: 1;\n overflow-y: auto;\n padding: var(--ag-ui-pad);\n display: flex;\n flex-direction: column;\n gap: var(--ag-ui-space);\n}\n\n.message {\n max-width: 80%;\n padding: var(--ag-ui-msg-pad);\n border-radius: var(--ag-ui-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(--ag-ui-user-bg);\n color: var(--ag-ui-user-fg);\n border-bottom-right-radius: 4px;\n}\n\n.message--assistant {\n align-self: flex-start;\n background: var(--ag-ui-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(--ag-ui-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(--ag-ui-bg);\n border: 1px solid var(--ag-ui-border);\n border-radius: 6px;\n}\n\n.message--assistant pre code {\n background: none;\n padding: 0;\n}\n\n.message--assistant blockquote {\n padding-left: 10px;\n border-left: 3px solid var(--ag-ui-border);\n color: var(--ag-ui-muted);\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(--ag-ui-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(--ag-ui-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.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(--ag-ui-tool-bg);\n border: 1px solid var(--ag-ui-border);\n color: var(--ag-ui-tool-fg);\n}\n\n.tool-call-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.tool-call-name {\n font-weight: 600;\n word-break: break-word;\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(--ag-ui-muted);\n}\n\n.tool-call[data-status=\"done\"] .tool-call-status {\n color: var(--ag-ui-success);\n}\n\n.tool-call[data-status=\"error\"] .tool-call-status {\n color: var(--ag-ui-danger);\n}\n\n.tool-call[data-status=\"declined\"] .tool-call-status {\n color: var(--ag-ui-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(--ag-ui-bg);\n border: 1px solid var(--ag-ui-border);\n border-radius: 6px;\n white-space: pre-wrap;\n word-break: break-word;\n color: var(--ag-ui-fg);\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(--ag-ui-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.input-row {\n display: flex;\n gap: 8px;\n padding: 12px;\n border-top: 1px solid var(--ag-ui-border);\n}\n\n.input {\n flex: 1;\n resize: none;\n background: var(--ag-ui-input-bg);\n border: 1px solid var(--ag-ui-border);\n border-radius: 8px;\n padding: 8px 10px;\n font: inherit;\n color: inherit;\n outline: none;\n}\n\n.input:focus {\n border-color: var(--ag-ui-accent);\n}\n\n.send {\n border: none;\n border-radius: 8px;\n padding: 0 16px;\n background: var(--ag-ui-accent);\n color: #ffffff;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n}\n\n.send:disabled {\n opacity: 0.5;\n cursor: default;\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(--ag-ui-bg);\n border: 1px solid var(--ag-ui-accent);\n border-radius: 10px;\n}\n\n.confirm[data-resolved] {\n opacity: 0.7;\n border-color: var(--ag-ui-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(--ag-ui-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(--ag-ui-border);\n border-radius: 8px;\n padding: 8px 14px;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n background: var(--ag-ui-bg);\n color: var(--ag-ui-fg);\n}\n\n.confirm-btn:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n.confirm-btn--confirm {\n border-color: var(--ag-ui-accent);\n background: var(--ag-ui-accent);\n color: #ffffff;\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(--ag-ui-border);\n border-radius: 999px;\n padding: 4px 12px;\n font: inherit;\n font-size: 0.9em;\n cursor: pointer;\n background: var(--ag-ui-assistant-bg);\n color: var(--ag-ui-fg);\n}\n\n.skill-chip:hover {\n border-color: var(--ag-ui-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(--ag-ui-bg);\n border: 1px solid var(--ag-ui-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(--ag-ui-fg);\n}\n\n.skill-item[aria-selected=\"true\"] {\n background: var(--ag-ui-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(--ag-ui-muted);\n}\n\n.skill-hint {\n margin: 8px 12px 0;\n font-size: 0.85em;\n color: var(--ag-ui-danger);\n}\n";
1
+ export declare const STYLES = "\n:host {\n /* Colors */\n --ag-ui-bg: #ffffff;\n --ag-ui-fg: #1a1a2e;\n --ag-ui-accent: #4f46e5;\n --ag-ui-user-bg: #4f46e5;\n --ag-ui-user-fg: #ffffff;\n --ag-ui-assistant-bg: #f1f1f6;\n --ag-ui-input-bg: var(--ag-ui-bg);\n --ag-ui-tool-bg: var(--ag-ui-assistant-bg);\n --ag-ui-tool-fg: var(--ag-ui-accent);\n --ag-ui-header-bg: var(--ag-ui-accent);\n --ag-ui-header-fg: #ffffff;\n --ag-ui-border: #e2e2ec;\n --ag-ui-radius: 12px;\n\n /* Status accents for tool-call cards. */\n --ag-ui-success: #15803d;\n --ag-ui-danger: #b91c1c;\n --ag-ui-muted: #6b7280;\n\n /* Surface \u2014 set --ag-ui-shadow: none for a flush, embedded panel. */\n --ag-ui-shadow: 0 12px 32px rgba(20, 20, 50, 0.18);\n --ag-ui-font: inherit;\n --ag-ui-font-size: 14px;\n --ag-ui-code-font: ui-monospace, \"SF Mono\", Menlo, monospace;\n\n /* Spacing \u2014 the density preset overrides these. */\n --ag-ui-space: 10px;\n --ag-ui-pad: 16px;\n --ag-ui-msg-pad: 8px 12px;\n --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 --ag-ui-position: fixed;\n --ag-ui-z-index: 2147483000;\n --ag-ui-width: 380px;\n --ag-ui-height: 560px;\n --ag-ui-inset: auto 24px 24px auto;\n --ag-ui-max-width: calc(100vw - 48px);\n --ag-ui-max-height: calc(100vh - 48px);\n\n position: var(--ag-ui-position);\n inset: var(--ag-ui-inset);\n z-index: var(--ag-ui-z-index);\n width: var(--ag-ui-width);\n max-width: var(--ag-ui-max-width);\n height: var(--ag-ui-height);\n max-height: var(--ag-ui-max-height);\n display: flex;\n font-family: var(--ag-ui-font);\n font-size: var(--ag-ui-font-size);\n color: var(--ag-ui-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 --ag-ui-bg: #15151f;\n --ag-ui-fg: #e8e8f2;\n --ag-ui-assistant-bg: #25253a;\n --ag-ui-header-bg: #1f1f30;\n --ag-ui-header-fg: #e8e8f2;\n --ag-ui-border: #33334a;\n --ag-ui-muted: #9aa0b4;\n --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 --ag-ui-bg: #15151f;\n --ag-ui-fg: #e8e8f2;\n --ag-ui-assistant-bg: #25253a;\n --ag-ui-header-bg: #1f1f30;\n --ag-ui-header-fg: #e8e8f2;\n --ag-ui-border: #33334a;\n --ag-ui-muted: #9aa0b4;\n --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 --ag-ui-bg: #0d1117;\n --ag-ui-fg: #c9d1d9;\n --ag-ui-accent: #3fb950;\n --ag-ui-user-bg: #238636;\n --ag-ui-assistant-bg: #161b22;\n --ag-ui-header-bg: #010409;\n --ag-ui-header-fg: #c9d1d9;\n --ag-ui-border: #30363d;\n --ag-ui-muted: #8b949e;\n --ag-ui-font: var(--ag-ui-code-font);\n --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 --ag-ui-font-size: 13px;\n --ag-ui-space: 6px;\n --ag-ui-pad: 10px;\n --ag-ui-msg-pad: 5px 9px;\n --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 --ag-ui-inset: auto auto 24px 24px;\n}\n\n:host([placement=\"side\"]) {\n --ag-ui-inset: 0 0 0 auto;\n --ag-ui-width: 420px;\n --ag-ui-height: 100vh;\n --ag-ui-max-height: 100vh;\n --ag-ui-radius: 0;\n}\n\n:host([placement=\"full\"]) {\n --ag-ui-inset: 0;\n --ag-ui-width: 100vw;\n --ag-ui-height: 100vh;\n --ag-ui-max-width: 100vw;\n --ag-ui-max-height: 100vh;\n --ag-ui-radius: 0;\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 --ag-ui-position: static;\n --ag-ui-width: 100%;\n --ag-ui-height: 100%;\n --ag-ui-max-width: 100%;\n --ag-ui-max-height: 100%;\n --ag-ui-shadow: none;\n --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(--ag-ui-bg);\n border: 1px solid var(--ag-ui-border);\n border-radius: var(--ag-ui-radius);\n box-shadow: var(--ag-ui-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(--ag-ui-border);\n background: var(--ag-ui-header-bg);\n color: var(--ag-ui-header-fg);\n}\n\n.header-title {\n font-weight: 600;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\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/* Collapsed: keep just the header bar \u2014 hide the whole body, and let the host\n shrink to the header. (A host can also fully hide the element itself.) */\n:host([collapsed]) {\n height: auto;\n max-height: none;\n}\n\n:host([collapsed]) .messages,\n:host([collapsed]) .input-row,\n:host([collapsed]) .skill-chips,\n:host([collapsed]) .skill-palette,\n:host([collapsed]) .skill-hint {\n display: none;\n}\n\n/* Edge-docked placements pin top *and* bottom, which would otherwise keep the\n panel full-height when collapsed; unpin the bottom so it shrinks to the\n header. (Floating/bottom-left pin only the bottom, so they already shrink.) */\n:host([collapsed][placement=\"side\"]),\n:host([collapsed][placement=\"full\"]) {\n bottom: auto;\n}\n\n.messages {\n flex: 1;\n overflow-y: auto;\n padding: var(--ag-ui-pad);\n display: flex;\n flex-direction: column;\n gap: var(--ag-ui-space);\n}\n\n.message {\n max-width: 80%;\n padding: var(--ag-ui-msg-pad);\n border-radius: var(--ag-ui-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(--ag-ui-user-bg);\n color: var(--ag-ui-user-fg);\n border-bottom-right-radius: 4px;\n}\n\n.message--assistant {\n align-self: flex-start;\n background: var(--ag-ui-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(--ag-ui-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(--ag-ui-bg);\n border: 1px solid var(--ag-ui-border);\n border-radius: 6px;\n}\n\n.message--assistant pre code {\n background: none;\n padding: 0;\n}\n\n.message--assistant blockquote {\n padding-left: 10px;\n border-left: 3px solid var(--ag-ui-border);\n color: var(--ag-ui-muted);\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(--ag-ui-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(--ag-ui-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.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(--ag-ui-tool-bg);\n border: 1px solid var(--ag-ui-border);\n color: var(--ag-ui-tool-fg);\n}\n\n.tool-call-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n}\n\n.tool-call-name {\n font-weight: 600;\n word-break: break-word;\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(--ag-ui-muted);\n}\n\n.tool-call[data-status=\"done\"] .tool-call-status {\n color: var(--ag-ui-success);\n}\n\n.tool-call[data-status=\"error\"] .tool-call-status {\n color: var(--ag-ui-danger);\n}\n\n.tool-call[data-status=\"declined\"] .tool-call-status {\n color: var(--ag-ui-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(--ag-ui-bg);\n border: 1px solid var(--ag-ui-border);\n border-radius: 6px;\n white-space: pre-wrap;\n word-break: break-word;\n color: var(--ag-ui-fg);\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(--ag-ui-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.input-row {\n display: flex;\n gap: 8px;\n padding: 12px;\n border-top: 1px solid var(--ag-ui-border);\n}\n\n.input {\n flex: 1;\n resize: none;\n background: var(--ag-ui-input-bg);\n border: 1px solid var(--ag-ui-border);\n border-radius: 8px;\n padding: 8px 10px;\n font: inherit;\n color: inherit;\n outline: none;\n}\n\n.input:focus {\n border-color: var(--ag-ui-accent);\n}\n\n.send {\n border: none;\n border-radius: 8px;\n padding: 0 16px;\n background: var(--ag-ui-accent);\n color: #ffffff;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n}\n\n.send:disabled {\n opacity: 0.5;\n cursor: default;\n}\n\n/* The composer button doubles as the Stop control while a run is in flight. */\n.send[data-state=\"running\"] {\n background: var(--ag-ui-muted);\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(--ag-ui-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(--ag-ui-bg);\n border: 1px solid var(--ag-ui-accent);\n border-radius: 10px;\n}\n\n.confirm[data-resolved] {\n opacity: 0.7;\n border-color: var(--ag-ui-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(--ag-ui-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(--ag-ui-border);\n border-radius: 8px;\n padding: 8px 14px;\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n background: var(--ag-ui-bg);\n color: var(--ag-ui-fg);\n}\n\n.confirm-btn:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n.confirm-btn--confirm {\n border-color: var(--ag-ui-accent);\n background: var(--ag-ui-accent);\n color: #ffffff;\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(--ag-ui-border);\n border-radius: 999px;\n padding: 4px 12px;\n font: inherit;\n font-size: 0.9em;\n cursor: pointer;\n background: var(--ag-ui-assistant-bg);\n color: var(--ag-ui-fg);\n}\n\n.skill-chip:hover {\n border-color: var(--ag-ui-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(--ag-ui-bg);\n border: 1px solid var(--ag-ui-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(--ag-ui-fg);\n}\n\n.skill-item[aria-selected=\"true\"] {\n background: var(--ag-ui-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(--ag-ui-muted);\n}\n\n.skill-hint {\n margin: 8px 12px 0;\n font-size: 0.85em;\n color: var(--ag-ui-danger);\n}\n";
2
2
  //# sourceMappingURL=styles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/ui/styles.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM,4veAqnBlB,CAAC"}
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/ui/styles.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM,6mfAkoBlB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artooi/ag-ui-web-component",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Framework-free <ag-ui-chat> Web Component over the AG-UI protocol. Drop-in chat sidebar with a pluggable client-side tool registry, DOM driver primitives, animations, and destructive-action confirmation modal.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -13,8 +13,7 @@
13
13
  },
14
14
  "./bundle": {
15
15
  "import": "./dist/ag-ui-web-component.bundle.js"
16
- },
17
- "./style.css": "./dist/ag-ui-web-component.bundle.css"
16
+ }
18
17
  },
19
18
  "files": [
20
19
  "dist/",
@@ -19,6 +19,7 @@ import { parseToolCatalog } from "../tools/parse_tool_catalog.js";
19
19
  import { createRouteTools, type RouteMap } from "../tools/route_map.js";
20
20
  import { createStateHookTools, type StateHook } from "../tools/state_hook.js";
21
21
  import { type ConfirmationRequest, requestConfirmation } from "../ui/confirmation_card.js";
22
+ import { prettifyToolName } from "../ui/prettify_tool_name.js";
22
23
  import { renderMarkdown } from "../ui/render_markdown.js";
23
24
  import { wrapWords } from "../ui/reveal_words.js";
24
25
  import { SkillsMenu } from "../ui/skills_menu.js";
@@ -72,6 +73,14 @@ export class AgUiChat extends HTMLElement {
72
73
  /** Extra HTTP headers for the AG-UI endpoint (e.g. CSRF). */
73
74
  headers: Record<string, string> = {};
74
75
 
76
+ /**
77
+ * Permit `<img>` in rendered assistant markdown. **Off by default**: a
78
+ * model-controlled image URL is fetched with no user interaction, which
79
+ * makes it a zero-click exfiltration channel for prompt-injected page
80
+ * data. Enable only when the content source is trusted.
81
+ */
82
+ allowImages = false;
83
+
75
84
  /** When true, destructive tools execute without a confirmation modal. */
76
85
  autoConfirm = false;
77
86
 
@@ -186,6 +195,13 @@ export class AgUiChat extends HTMLElement {
186
195
  readonly #skillHint: HTMLDivElement;
187
196
 
188
197
  #client: AgUiClient | null = null;
198
+ // Whether an interaction is in flight (first onRunStart → onSettled). Drives
199
+ // the Send⇄Stop button: `agent.isRunning` is false between frontend-tool
200
+ // rounds, but the user must still be able to stop there.
201
+ #running = false;
202
+ // Aborting this dismisses (declines) an open confirmation card when the run
203
+ // is cancelled while the card awaits a decision. One controller per card.
204
+ #confirmAbort: AbortController | null = null;
189
205
  #streamingBubble: HTMLDivElement | null = null;
190
206
  // Text deltas applied to the current streaming bubble. >1 ⇒ the message
191
207
  // revealed progressively as it streamed, so the word reveal must not re-animate
@@ -453,6 +469,9 @@ export class AgUiChat extends HTMLElement {
453
469
  * in-memory run state, clear the transcript, and mint a new thread id.
454
470
  */
455
471
  newChat(): void {
472
+ // Stop any in-flight run first — discarding the client mid-run would
473
+ // leave the old agent streaming into a cleared transcript.
474
+ this.#cancelRun();
456
475
  this.conversationStore.clear(this.#threadId);
457
476
  this.#client = null;
458
477
  this.#streamingBubble = null;
@@ -462,7 +481,7 @@ export class AgUiChat extends HTMLElement {
462
481
  this.#initialMessages = [];
463
482
  this.#messages.replaceChildren();
464
483
  this.#threadId = this.conversationStore.threadId();
465
- this.#send.disabled = false;
484
+ this.#setRunning(false);
466
485
  }
467
486
 
468
487
  /**
@@ -569,7 +588,7 @@ export class AgUiChat extends HTMLElement {
569
588
  const bubble = document.createElement("div");
570
589
  bubble.className = `message message--${role}`;
571
590
  if (role === MESSAGE_ROLE.ASSISTANT) {
572
- bubble.innerHTML = renderMarkdown(content);
591
+ bubble.innerHTML = renderMarkdown(content, { allowImages: this.allowImages });
573
592
  } else {
574
593
  bubble.textContent = content;
575
594
  }
@@ -632,7 +651,15 @@ export class AgUiChat extends HTMLElement {
632
651
  this.#send.className = "send";
633
652
  this.#send.type = "button";
634
653
  this.#send.textContent = "Send";
654
+ this.#send.setAttribute("aria-label", "Send");
655
+ this.#send.dataset["state"] = "idle";
635
656
  this.#send.addEventListener("click", () => {
657
+ // One button, two states: Send while idle, Stop while a run is in
658
+ // flight (no layout change).
659
+ if (this.#running) {
660
+ this.#cancelRun();
661
+ return;
662
+ }
636
663
  void this.#submit();
637
664
  });
638
665
 
@@ -665,12 +692,39 @@ export class AgUiChat extends HTMLElement {
665
692
  event.preventDefault();
666
693
  return;
667
694
  }
695
+ // Escape-to-cancel — only reachable when the palette is closed (it
696
+ // consumed the key above otherwise), so the two Escapes don't clash.
697
+ if (event.key === "Escape" && this.#running) {
698
+ event.preventDefault();
699
+ this.#cancelRun();
700
+ return;
701
+ }
668
702
  if (event.key === "Enter" && !event.shiftKey) {
669
703
  event.preventDefault();
670
704
  void this.#submit();
671
705
  }
672
706
  }
673
707
 
708
+ /**
709
+ * Stop the in-flight run: decline any confirmation card awaiting a decision
710
+ * (the loop is suspended on it), then cancel the client run — the abort
711
+ * closes the streaming request, which is AG-UI's cancel (the server
712
+ * observes the disconnect).
713
+ */
714
+ #cancelRun(): void {
715
+ this.#confirmAbort?.abort();
716
+ this.#client?.cancel();
717
+ }
718
+
719
+ /** Swap the composer button between Send (idle) and Stop (running). */
720
+ #setRunning(running: boolean): void {
721
+ this.#running = running;
722
+ const label = running ? "Stop" : "Send";
723
+ this.#send.textContent = label;
724
+ this.#send.setAttribute("aria-label", label);
725
+ this.#send.dataset["state"] = running ? "running" : "idle";
726
+ }
727
+
674
728
  async #submit(): Promise<void> {
675
729
  const content = this.#input.value.trim();
676
730
  if (content === "") {
@@ -700,6 +754,10 @@ export class AgUiChat extends HTMLElement {
700
754
  const agent = this.agentFactory({
701
755
  endpoint: this.endpoint,
702
756
  headers: this.headers,
757
+ // Live getter: the client is built once and cached, but a rotated
758
+ // token must still reach every request — the factory's fetch wrapper
759
+ // re-reads this on each call.
760
+ getHeaders: () => this.headers,
703
761
  threadId: this.#threadId,
704
762
  initialMessages: this.#initialMessages,
705
763
  });
@@ -749,9 +807,15 @@ export class AgUiChat extends HTMLElement {
749
807
  if (typeof confirmText === "string") {
750
808
  request.message = confirmText;
751
809
  }
752
- const decision = requestConfirmation(this.#messages, request);
810
+ // The run loop is suspended on this card; a Stop while it's open aborts
811
+ // the controller, resolving the decision as declined.
812
+ this.#confirmAbort = new AbortController();
813
+ const decision = requestConfirmation(this.#messages, request, {
814
+ signal: this.#confirmAbort.signal,
815
+ });
753
816
  this.#messages.scrollTop = this.#messages.scrollHeight;
754
817
  const accepted = await decision;
818
+ this.#confirmAbort = null;
755
819
  if (!accepted) {
756
820
  const message = "User declined the action.";
757
821
  card.settle(TOOL_CALL_STATUS.DECLINED, message);
@@ -793,7 +857,7 @@ export class AgUiChat extends HTMLElement {
793
857
  #handlers(): AgUiClientHandlers {
794
858
  return {
795
859
  onRunStart: () => {
796
- this.#send.disabled = true;
860
+ this.#setRunning(true);
797
861
  this.#showPending();
798
862
  },
799
863
  onTextDelta: (buffer) => {
@@ -825,25 +889,42 @@ export class AgUiChat extends HTMLElement {
825
889
  this.#serverSettled.add(toolCallId);
826
890
  },
827
891
  onRunEnd: () => {
892
+ // Per-round end; the button stays on Stop until the whole interaction
893
+ // settles — the user must be able to cancel between tool rounds.
828
894
  this.#hidePending();
829
- this.#send.disabled = false;
830
895
  this.#streamingBubble = null;
831
896
  },
832
897
  onError: (message) => {
833
898
  this.#hidePending();
834
899
  this.#revealWords(this.appendMessage(MESSAGE_ROLE.ASSISTANT, `⚠️ ${message}`));
835
- this.#send.disabled = false;
900
+ this.#streamingBubble = null;
901
+ },
902
+ onCancelled: () => {
903
+ // Deliberate stop, not a failure: keep whatever partial text already
904
+ // streamed and add a muted note instead of an error bubble.
905
+ this.#hidePending();
906
+ this.#appendStoppedNote();
836
907
  this.#streamingBubble = null;
837
908
  },
838
909
  onSettled: () => {
839
910
  // Terminal guarantee: whatever path ended the run, return to rest.
840
911
  this.#hidePending();
841
- this.#send.disabled = false;
912
+ this.#setRunning(false);
842
913
  this.#streamingBubble = null;
843
914
  },
844
915
  };
845
916
  }
846
917
 
918
+ /** A muted "⏹ Stopped" line in the transcript (distinct from the ⚠️ error bubble). */
919
+ #appendStoppedNote(): void {
920
+ const note = document.createElement("div");
921
+ note.className = "stopped-note";
922
+ note.setAttribute("role", "status");
923
+ note.textContent = "⏹ Stopped";
924
+ this.#messages.appendChild(note);
925
+ this.#messages.scrollTop = this.#messages.scrollHeight;
926
+ }
927
+
847
928
  /**
848
929
  * Show a "thinking" indicator while the agent is being awaited — both the
849
930
  * silent stretch before the first token and the gap after a tool result
@@ -878,7 +959,7 @@ export class AgUiChat extends HTMLElement {
878
959
  this.#streamingBubble = this.appendMessage(MESSAGE_ROLE.ASSISTANT, "");
879
960
  this.#streamDeltas = 0;
880
961
  }
881
- this.#streamingBubble.innerHTML = renderMarkdown(buffer);
962
+ this.#streamingBubble.innerHTML = renderMarkdown(buffer, { allowImages: this.allowImages });
882
963
  this.#messages.scrollTop = this.#messages.scrollHeight;
883
964
  return this.#streamingBubble;
884
965
  }
@@ -901,7 +982,9 @@ export class AgUiChat extends HTMLElement {
901
982
  const summary =
902
983
  typeof labelled === "string"
903
984
  ? labelled
904
- : (this.toolSummaries[call.name] ?? this.#toolCatalog[call.name]);
985
+ : (this.toolSummaries[call.name] ??
986
+ this.#toolCatalog[call.name] ??
987
+ prettifyToolName(call.name));
905
988
  const card = new ToolCallCard(call.name, call.args, this.toolDisplay, summary);
906
989
  this.#toolCards.set(call.id, card);
907
990
  this.#messages.appendChild(card.element);
@@ -53,11 +53,19 @@ export interface AgUiClientHandlers {
53
53
  onToolResult(toolCallId: string, content: string): void;
54
54
  onRunEnd(): void;
55
55
  onError(message: string): void;
56
+ /**
57
+ * Fired when the user cancelled the run ({@link AgUiClient.cancel}) — the
58
+ * deliberate-stop sibling of `onError`. Any partial assistant text already
59
+ * streamed stays valid; the host should keep the bubble and show a muted
60
+ * "stopped" affordance rather than an error. `onSettled` still follows.
61
+ */
62
+ onCancelled(): void;
56
63
  /**
57
64
  * Fired exactly once when the whole interaction settles — after the run loop
58
65
  * ends for any reason (a server-only round, frontend-tool rounds exhausted,
59
- * or an error). The terminal guarantee that the UI returns to rest (pending
60
- * indicator cleared, input re-enabled) no matter how the run finished.
66
+ * a cancellation, or an error). The terminal guarantee that the UI returns
67
+ * to rest (pending indicator cleared, input re-enabled) no matter how the
68
+ * run finished.
61
69
  */
62
70
  onSettled(): void;
63
71
  }
@@ -100,6 +108,9 @@ export class AgUiClient {
100
108
  readonly #getContext: () => Context[];
101
109
  readonly #executeTool: ExecuteTool | null;
102
110
  readonly #onPersist: (messages: readonly Message[]) => void;
111
+ // Set by cancel(); reset at the top of each #run(). Checked by the loop so
112
+ // a cancel between frontend-tool rounds doesn't start another round.
113
+ #cancelled = false;
103
114
 
104
115
  constructor(config: AgUiClientConfig) {
105
116
  this.#agent = config.agent;
@@ -148,24 +159,64 @@ export class AgUiClient {
148
159
  this.#onPersist(this.#agent.messages);
149
160
  }
150
161
 
162
+ /**
163
+ * Cancel the in-flight run (AG-UI has no server cancel route — aborting the
164
+ * streaming request is the protocol's cancel; the server observes the
165
+ * disconnect). Safe to call with no run in flight. Partial text already
166
+ * streamed stays in history; {@link AgUiClientHandlers.onCancelled} fires
167
+ * instead of `onError`, and `onSettled` still follows.
168
+ */
169
+ cancel(): void {
170
+ this.#cancelled = true;
171
+ this.#agent.abortRun();
172
+ }
173
+
151
174
  async #run(): Promise<void> {
175
+ this.#cancelled = false;
152
176
  try {
153
177
  await this.#runLoop();
178
+ // `@ag-ui/client` filters abort errors inside `runAgent` (it resolves
179
+ // rather than rejects on a cancelled fetch), so a cancelled run usually
180
+ // ends here, not in the catch.
181
+ if (this.#cancelled) {
182
+ this.#onCancelled();
183
+ }
154
184
  } catch (error) {
155
- this.#handlers.onError(error instanceof Error ? error.message : String(error));
185
+ if (this.#cancelled || isAbortError(error)) {
186
+ this.#onCancelled();
187
+ } else {
188
+ this.#handlers.onError(error instanceof Error ? error.message : String(error));
189
+ }
156
190
  } finally {
157
191
  this.#handlers.onSettled();
158
192
  }
159
193
  }
160
194
 
195
+ #onCancelled(): void {
196
+ // The truncated exchange (including any partial assistant text the agent
197
+ // already applied) survives a reload.
198
+ this.#onPersist(this.#agent.messages);
199
+ this.#handlers.onCancelled();
200
+ }
201
+
161
202
  async #runLoop(): Promise<void> {
162
203
  for (let round = 0; round < MAX_TOOL_ROUNDS; round += 1) {
204
+ // A cancel during the previous round's frontend-tool execution lands
205
+ // here: the running handler completed, but no further round starts.
206
+ if (this.#cancelled) {
207
+ return;
208
+ }
163
209
  const pending: AgUiToolCall[] = [];
164
210
  await this.#agent.runAgent(
165
211
  { tools: this.#getTools(), context: this.#getContext() },
166
212
  this.#buildSubscriber(pending),
167
213
  );
168
214
  this.#onPersist(this.#agent.messages);
215
+ // Cancelled mid-stream: the user said stop — don't execute the tool
216
+ // calls collected before the abort.
217
+ if (this.#cancelled) {
218
+ return;
219
+ }
169
220
  if (this.#executeTool === null || pending.length === 0) {
170
221
  return;
171
222
  }
@@ -228,3 +279,12 @@ export class AgUiClient {
228
279
  };
229
280
  }
230
281
  }
282
+
283
+ /**
284
+ * Whether a rejection came from aborting the run's fetch. Belt-and-suspenders
285
+ * with the `#cancelled` flag: some `@ag-ui/client` versions re-throw the
286
+ * `AbortError` instead of filtering it.
287
+ */
288
+ function isAbortError(error: unknown): boolean {
289
+ return error instanceof Error && error.name === "AbortError";
290
+ }
@@ -5,6 +5,15 @@ import type { Message } from "@ag-ui/core";
5
5
  export interface HttpAgentOptions {
6
6
  endpoint: string;
7
7
  headers?: Record<string, string>;
8
+ /**
9
+ * Live header source, re-read on **every** request. `HttpAgent` bakes the
10
+ * static `headers` into its constructor and the element caches the agent
11
+ * for the whole conversation — so a rotated token (CSRF, short-lived JWT)
12
+ * would otherwise never reach the agent endpoint and a long session 401s
13
+ * mid-conversation. When set, the fetch wrapper overlays these values on
14
+ * each call; `headers` still seeds the initial/static configuration.
15
+ */
16
+ getHeaders?: () => Record<string, string>;
8
17
  /** Stable conversation id, so the agent's runs share a thread. */
9
18
  threadId?: string;
10
19
  /** Rehydrated history to seed the agent with (durable conversation). */
@@ -25,8 +34,20 @@ export function createHttpAgent(options: HttpAgentOptions): AbstractAgent {
25
34
  // HttpAgent invokes its configured fetch as a method (`this.fetch(...)`),
26
35
  // which would rebind the global `fetch` to the agent instance and trigger
27
36
  // "Illegal invocation" in browsers. Wrap it so `fetch` is always called as
28
- // a free function with the correct receiver.
29
- fetch: (url, init) => fetch(url, init),
37
+ // a free function with the correct receiver. The wrapper also overlays
38
+ // `getHeaders()` per request, so header rotation (CSRF, short-lived JWT)
39
+ // reaches the stream even though the agent instance is cached.
40
+ fetch: (url, init) => {
41
+ const fresh = options.getHeaders?.();
42
+ if (fresh === undefined) {
43
+ return fetch(url, init);
44
+ }
45
+ const headers = new Headers(init?.headers);
46
+ for (const [name, value] of Object.entries(fresh)) {
47
+ headers.set(name, value);
48
+ }
49
+ return fetch(url, { ...init, headers });
50
+ },
30
51
  // Spread conditionally: under `exactOptionalPropertyTypes` an explicit
31
52
  // `undefined` is not assignable to these optional config fields.
32
53
  ...(options.threadId !== undefined ? { threadId: options.threadId } : {}),
package/src/index.ts CHANGED
@@ -79,8 +79,13 @@ export {
79
79
  type RouteWithParams,
80
80
  } from "./tools/route_map.js";
81
81
  export { createStateHookTools, type StateHook } from "./tools/state_hook.js";
82
- export { type ConfirmationRequest, requestConfirmation } from "./ui/confirmation_card.js";
83
- export { renderMarkdown } from "./ui/render_markdown.js";
82
+ export {
83
+ type ConfirmationOptions,
84
+ type ConfirmationRequest,
85
+ requestConfirmation,
86
+ } from "./ui/confirmation_card.js";
87
+ export { prettifyToolName } from "./ui/prettify_tool_name.js";
88
+ export { type RenderMarkdownOptions, renderMarkdown } from "./ui/render_markdown.js";
84
89
  export {
85
90
  type SettledStatus,
86
91
  ToolCallCard,
@@ -18,6 +18,16 @@ function actionButton(modifier: string, label: string): HTMLButtonElement {
18
18
  return button;
19
19
  }
20
20
 
21
+ /** Options for {@link requestConfirmation}. */
22
+ export interface ConfirmationOptions {
23
+ /**
24
+ * Aborting this signal resolves the card as declined (buttons disabled,
25
+ * `data-resolved="declined"`) — the hook a Stop control uses to dismiss a
26
+ * pending confirmation when the user cancels the whole run.
27
+ */
28
+ signal?: AbortSignal;
29
+ }
30
+
21
31
  /**
22
32
  * Append an inline confirmation card to ``host`` (the chat message list) and
23
33
  * resolve when the user decides.
@@ -31,6 +41,7 @@ function actionButton(modifier: string, label: string): HTMLButtonElement {
31
41
  export function requestConfirmation(
32
42
  host: Node & ParentNode,
33
43
  request: ConfirmationRequest,
44
+ options: ConfirmationOptions = {},
34
45
  ): Promise<boolean> {
35
46
  return new Promise<boolean>((resolve) => {
36
47
  const card = document.createElement("div");
@@ -53,7 +64,12 @@ export function requestConfirmation(
53
64
  const cancel = actionButton("cancel", "Cancel");
54
65
  const confirm = actionButton("confirm", "Confirm");
55
66
 
67
+ let settled = false;
56
68
  const close = (accepted: boolean): void => {
69
+ if (settled) {
70
+ return;
71
+ }
72
+ settled = true;
57
73
  cancel.disabled = true;
58
74
  confirm.disabled = true;
59
75
  card.setAttribute("data-resolved", accepted ? "confirmed" : "declined");
@@ -62,10 +78,16 @@ export function requestConfirmation(
62
78
 
63
79
  cancel.addEventListener("click", () => close(false));
64
80
  confirm.addEventListener("click", () => close(true));
81
+ options.signal?.addEventListener("abort", () => close(false), { once: true });
65
82
 
66
83
  actions.append(cancel, confirm);
67
84
  card.append(body, args, actions);
68
85
  host.appendChild(card);
86
+ if (options.signal?.aborted === true) {
87
+ // The run was cancelled before the card could ask; record the decline.
88
+ close(false);
89
+ return;
90
+ }
69
91
  confirm.focus();
70
92
  });
71
93
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Prettify a raw tool name for display: separators become spaces and the
3
+ * first letter is capitalised — `list_projects` → "List projects",
4
+ * `invoices.retrieve` → "Invoices retrieve".
5
+ *
6
+ * Final fallback of the tool-card label chain (`x-summary` →
7
+ * `toolSummaries` → fetched catalog → this). Purely cosmetic: the original
8
+ * name still rides on the card's dataset for debugging.
9
+ */
10
+ export function prettifyToolName(name: string): string {
11
+ const spaced = name.replace(/[._-]+/g, " ").trim();
12
+ if (spaced === "") {
13
+ return name;
14
+ }
15
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
16
+ }