@artooi/ag-ui-web-component 0.3.1 → 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 +39 -1
- package/README.md +25 -1
- package/dist/ag-ui-web-component.bundle.js +17 -4
- package/dist/ag-ui-web-component.bundle.js.map +3 -3
- 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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +120 -9
- package/dist/index.js.map +2 -2
- package/dist/ui/confirmation_card.d.ts +10 -1
- package/dist/ui/confirmation_card.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 +1 -1
- package/src/core/ag_ui_chat.ts +74 -6
- package/src/core/agui_client.ts +63 -3
- package/src/index.ts +5 -1
- package/src/ui/confirmation_card.ts +22 -0
- 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"}
|
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",
|
package/src/core/ag_ui_chat.ts
CHANGED
|
@@ -195,6 +195,13 @@ export class AgUiChat extends HTMLElement {
|
|
|
195
195
|
readonly #skillHint: HTMLDivElement;
|
|
196
196
|
|
|
197
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;
|
|
198
205
|
#streamingBubble: HTMLDivElement | null = null;
|
|
199
206
|
// Text deltas applied to the current streaming bubble. >1 ⇒ the message
|
|
200
207
|
// revealed progressively as it streamed, so the word reveal must not re-animate
|
|
@@ -462,6 +469,9 @@ export class AgUiChat extends HTMLElement {
|
|
|
462
469
|
* in-memory run state, clear the transcript, and mint a new thread id.
|
|
463
470
|
*/
|
|
464
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();
|
|
465
475
|
this.conversationStore.clear(this.#threadId);
|
|
466
476
|
this.#client = null;
|
|
467
477
|
this.#streamingBubble = null;
|
|
@@ -471,7 +481,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
471
481
|
this.#initialMessages = [];
|
|
472
482
|
this.#messages.replaceChildren();
|
|
473
483
|
this.#threadId = this.conversationStore.threadId();
|
|
474
|
-
this.#
|
|
484
|
+
this.#setRunning(false);
|
|
475
485
|
}
|
|
476
486
|
|
|
477
487
|
/**
|
|
@@ -641,7 +651,15 @@ export class AgUiChat extends HTMLElement {
|
|
|
641
651
|
this.#send.className = "send";
|
|
642
652
|
this.#send.type = "button";
|
|
643
653
|
this.#send.textContent = "Send";
|
|
654
|
+
this.#send.setAttribute("aria-label", "Send");
|
|
655
|
+
this.#send.dataset["state"] = "idle";
|
|
644
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
|
+
}
|
|
645
663
|
void this.#submit();
|
|
646
664
|
});
|
|
647
665
|
|
|
@@ -674,12 +692,39 @@ export class AgUiChat extends HTMLElement {
|
|
|
674
692
|
event.preventDefault();
|
|
675
693
|
return;
|
|
676
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
|
+
}
|
|
677
702
|
if (event.key === "Enter" && !event.shiftKey) {
|
|
678
703
|
event.preventDefault();
|
|
679
704
|
void this.#submit();
|
|
680
705
|
}
|
|
681
706
|
}
|
|
682
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
|
+
|
|
683
728
|
async #submit(): Promise<void> {
|
|
684
729
|
const content = this.#input.value.trim();
|
|
685
730
|
if (content === "") {
|
|
@@ -762,9 +807,15 @@ export class AgUiChat extends HTMLElement {
|
|
|
762
807
|
if (typeof confirmText === "string") {
|
|
763
808
|
request.message = confirmText;
|
|
764
809
|
}
|
|
765
|
-
|
|
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
|
+
});
|
|
766
816
|
this.#messages.scrollTop = this.#messages.scrollHeight;
|
|
767
817
|
const accepted = await decision;
|
|
818
|
+
this.#confirmAbort = null;
|
|
768
819
|
if (!accepted) {
|
|
769
820
|
const message = "User declined the action.";
|
|
770
821
|
card.settle(TOOL_CALL_STATUS.DECLINED, message);
|
|
@@ -806,7 +857,7 @@ export class AgUiChat extends HTMLElement {
|
|
|
806
857
|
#handlers(): AgUiClientHandlers {
|
|
807
858
|
return {
|
|
808
859
|
onRunStart: () => {
|
|
809
|
-
this.#
|
|
860
|
+
this.#setRunning(true);
|
|
810
861
|
this.#showPending();
|
|
811
862
|
},
|
|
812
863
|
onTextDelta: (buffer) => {
|
|
@@ -838,25 +889,42 @@ export class AgUiChat extends HTMLElement {
|
|
|
838
889
|
this.#serverSettled.add(toolCallId);
|
|
839
890
|
},
|
|
840
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.
|
|
841
894
|
this.#hidePending();
|
|
842
|
-
this.#send.disabled = false;
|
|
843
895
|
this.#streamingBubble = null;
|
|
844
896
|
},
|
|
845
897
|
onError: (message) => {
|
|
846
898
|
this.#hidePending();
|
|
847
899
|
this.#revealWords(this.appendMessage(MESSAGE_ROLE.ASSISTANT, `⚠️ ${message}`));
|
|
848
|
-
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();
|
|
849
907
|
this.#streamingBubble = null;
|
|
850
908
|
},
|
|
851
909
|
onSettled: () => {
|
|
852
910
|
// Terminal guarantee: whatever path ended the run, return to rest.
|
|
853
911
|
this.#hidePending();
|
|
854
|
-
this.#
|
|
912
|
+
this.#setRunning(false);
|
|
855
913
|
this.#streamingBubble = null;
|
|
856
914
|
},
|
|
857
915
|
};
|
|
858
916
|
}
|
|
859
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
|
+
|
|
860
928
|
/**
|
|
861
929
|
* Show a "thinking" indicator while the agent is being awaited — both the
|
|
862
930
|
* silent stretch before the first token and the gap after a tool result
|
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
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -79,7 +79,11 @@ 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 {
|
|
82
|
+
export {
|
|
83
|
+
type ConfirmationOptions,
|
|
84
|
+
type ConfirmationRequest,
|
|
85
|
+
requestConfirmation,
|
|
86
|
+
} from "./ui/confirmation_card.js";
|
|
83
87
|
export { prettifyToolName } from "./ui/prettify_tool_name.js";
|
|
84
88
|
export { type RenderMarkdownOptions, renderMarkdown } from "./ui/render_markdown.js";
|
|
85
89
|
export {
|
|
@@ -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
|
}
|
package/src/ui/styles.ts
CHANGED
|
@@ -501,6 +501,19 @@ export const STYLES = `
|
|
|
501
501
|
cursor: default;
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
+
/* The composer button doubles as the Stop control while a run is in flight. */
|
|
505
|
+
.send[data-state="running"] {
|
|
506
|
+
background: var(--ag-ui-muted);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/* Muted "⏹ Stopped" line after a cancelled run — a note, not an error bubble. */
|
|
510
|
+
.stopped-note {
|
|
511
|
+
align-self: flex-start;
|
|
512
|
+
color: var(--ag-ui-muted);
|
|
513
|
+
font-size: 12px;
|
|
514
|
+
padding: 2px 4px;
|
|
515
|
+
}
|
|
516
|
+
|
|
504
517
|
/* Inline confirmation card — lives in the transcript, no focus-stealing overlay. */
|
|
505
518
|
.confirm {
|
|
506
519
|
align-self: stretch;
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION: string = "0.
|
|
1
|
+
export const VERSION: string = "0.4.0";
|