@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.
- package/CHANGELOG.md +81 -1
- package/README.md +34 -6
- package/dist/ag-ui-web-component.bundle.js +36 -23
- package/dist/ag-ui-web-component.bundle.js.map +4 -4
- package/dist/core/ag_ui_chat.d.ts +7 -0
- package/dist/core/ag_ui_chat.d.ts.map +1 -1
- package/dist/core/agui_client.d.ts +18 -2
- package/dist/core/agui_client.d.ts.map +1 -1
- package/dist/core/create_http_agent.d.ts +9 -0
- package/dist/core/create_http_agent.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +170 -21
- package/dist/index.js.map +3 -3
- package/dist/ui/confirmation_card.d.ts +10 -1
- package/dist/ui/confirmation_card.d.ts.map +1 -1
- package/dist/ui/prettify_tool_name.d.ts +11 -0
- package/dist/ui/prettify_tool_name.d.ts.map +1 -0
- package/dist/ui/render_markdown.d.ts +10 -1
- package/dist/ui/render_markdown.d.ts.map +1 -1
- package/dist/ui/styles.d.ts +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/package.json +2 -3
- package/src/core/ag_ui_chat.ts +92 -9
- package/src/core/agui_client.ts +63 -3
- package/src/core/create_http_agent.ts +23 -2
- package/src/index.ts +7 -2
- package/src/ui/confirmation_card.ts +22 -0
- package/src/ui/prettify_tool_name.ts +16 -0
- package/src/ui/render_markdown.ts +37 -11
- package/src/ui/styles.ts +13 -0
- package/src/version.ts +1 -1
|
@@ -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,
|
|
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":"
|
|
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"}
|
package/dist/ui/styles.d.ts
CHANGED
|
@@ -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
|
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,6mfAkoBlB,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.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/",
|
package/src/core/ag_ui_chat.ts
CHANGED
|
@@ -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.#
|
|
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
|
-
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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] ??
|
|
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);
|
package/src/core/agui_client.ts
CHANGED
|
@@ -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
|
|
60
|
-
* indicator cleared, input re-enabled) no matter how the
|
|
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.#
|
|
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
|
-
|
|
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 {
|
|
83
|
-
|
|
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
|
+
}
|